/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.tinkerpop.gremlin;

import org.apache.tinkerpop.gremlin.structure.Graph;
import org.apache.tinkerpop.gremlin.structure.io.GraphReader;
import org.apache.tinkerpop.gremlin.structure.io.IoRegistry;
import org.apache.tinkerpop.gremlin.structure.io.gryo.GryoIo;
import org.apache.tinkerpop.gremlin.structure.io.gryo.GryoReader;
import org.apache.commons.configuration.BaseConfiguration;
import org.apache.commons.configuration.Configuration;
import org.apache.tinkerpop.gremlin.structure.util.GraphFactory;
import org.apache.tinkerpop.gremlin.structure.io.gryo.GryoResourceAccess;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;

/**
 * A basic GraphProvider which simply requires the implementer to supply their base configuration for their
 * Graph instance.  Minimally this is just the setting for "gremlin.graph".
 *
 * @author Stephen Mallette (http://stephen.genoprime.com)
 */
public abstract class AbstractGraphProvider implements GraphProvider {
    private static final Logger logger = LoggerFactory.getLogger(AbstractGraphProvider.class);

    /**
     * Provides a basic configuration for a particular {@link Graph} instance and uses the {@code graphName} to ensure
     * that the instance is unique.  It is up to the Gremlin implementation to determine how best to use the
     * {@code graphName} to ensure uniqueness.  For example, Neo4j, might use the {@code graphName} might be used to
     * create a different sub-directory where the graph is stored.
     * <p/>
     * The @{code test} and @{code testMethodName} can be used to alter graph configurations for specific tests.
     * For example, a graph that has support for different transaction isolation levels might only support a feature
     * in a specific configuration.  Using these arguments, the implementation could detect when a test was being
     * fired that required the database to be configured in a specific isolation level and return a configuration
     * to support that.
     * <p/>
     * Ultimately, the returned {@code Map} should minimally contain a configuration that can be given to a
     * {@link GraphFactory} so that it can be instantiated.
     *
     * @param graphName      a value that represents a unique configuration for a graph
     * @param test           the test class
     * @param testMethodName the name of the test method
     * @param loadGraphWith  the data set to load and will be null if no data is to be loaded
     * @return a configuration {@code Map} that should be unique per the {@code graphName}
     */
    public abstract Map<String, Object> getBaseConfiguration(final String graphName, final Class<?> test,
                                                             final String testMethodName, final LoadGraphWith.GraphData loadGraphWith);

    @Override
    public Configuration newGraphConfiguration(final String graphName, final Class<?> test,
                                               final String testMethodName,
                                               final Map<String, Object> configurationOverrides,
                                               final LoadGraphWith.GraphData loadGraphWith) {
        final Configuration conf = new BaseConfiguration();
        getBaseConfiguration(graphName, test, testMethodName, loadGraphWith).entrySet().stream()
                .forEach(e -> conf.setProperty(e.getKey(), e.getValue()));

        // assign overrides but don't allow gremlin.graph setting to be overridden.  the test suite should
        // not be able to override that.
        configurationOverrides.entrySet().stream()
                .filter(c -> !c.getKey().equals(Graph.GRAPH))
                .forEach(e -> conf.setProperty(e.getKey(), e.getValue()));
        return conf;
    }

    @Override
    public void loadGraphData(final Graph graph, final LoadGraphWith loadGraphWith, final Class testClass, final String testName) {
        try {
            // loadGraphWith will be null if an annotation isn't assigned.  it simply means that the graph is
            // created in an ad-hoc manner for the tests - just don't try to read any data into it.
            if (loadGraphWith != null) readIntoGraph(graph, loadGraphWith.value().location());
        } catch (IOException ioe) {
            throw new RuntimeException("Graph could not be loaded with data for test: " + ioe.getMessage(), ioe);
        }
    }

    /**
     * Helper method for those building {@link GraphProvider} implementations that need to clean directories
     * between test runs.
     */
    protected static void deleteDirectory(final File directory) {
        if (directory.exists()) {
            for (File file : directory.listFiles()) {
                if (file.isDirectory()) {
                    deleteDirectory(file);
                } else {
                    file.delete();
                }
            }
            directory.delete();
        }

        // overkill code, simply allowing us to detect when data dir is in use.  useful though because without it
        // tests may fail if a database is re-used in between tests somehow.  this directory really needs to be
        // cleared between tests runs and this exception will make it clear if it is not. this code used to
        // throw an exception but that fails windows builds in some cases unecessarily - hopefully the print
        // to screen is enough to hint failures due to the old directory still being in place.
        if (directory.exists()) logger.error("unable to delete directory " + directory.getAbsolutePath());
    }

    /**
     * Utility method to help produce an appropriate unique directory for a test. Designed to be called from
     * {@link #getBaseConfiguration(String, Class, String, LoadGraphWith.GraphData)} for those graph providers that
     * need a data directory for their {@link Graph} implementations.
     */
    protected String makeTestDirectory(final String graphName, final Class<?> test, final String testMethodName) {
        return getWorkingDirectory() + File.separator
                + TestHelper.cleanPathSegment(this.getClass().getSimpleName()) + File.separator
                + TestHelper.cleanPathSegment(test.getSimpleName()) + File.separator
                + TestHelper.cleanPathSegment(graphName) + File.separator
                + cleanParameters(TestHelper.cleanPathSegment(testMethodName));
    }

    protected String cleanParameters(String methodName) {
        int random = (int) (Math.random() * Integer.MAX_VALUE);
        return methodName.replaceAll("[0-9, -]+$", String.valueOf(random));
    }

    /**
     * Used by the default implementation of {@link AbstractGraphProvider#loadGraphData(Graph, LoadGraphWith, Class, String)}
     * to read the graph from a Kryo file using the default {@link GryoReader} implementation. If the default
     * implementation does not work (perhaps a graph implementation needs to register some special {@link IoRegistry}
     * then this method or its caller should be overridden to suit the implementation.
     *
     * @param graph the graph to load to
     * @param path the path to the file to load into the graph
     */
    protected void readIntoGraph(final Graph graph, final String path) throws IOException {
        final String dataFile = TestHelper.generateTempFileFromResource(graph.getClass(),
                GryoResourceAccess.class, path.substring(path.lastIndexOf(File.separator) + 1), "", false).getAbsolutePath();
        graph.traversal().io(dataFile).read().iterate();
    }
}
