/*
 * 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.commons.configuration.Configuration;
import org.apache.tinkerpop.gremlin.process.computer.GraphComputer;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy;
import org.apache.tinkerpop.gremlin.process.traversal.Traverser;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.DefaultGraphTraversal;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
import org.apache.tinkerpop.gremlin.process.traversal.traverser.B_LP_O_P_S_SE_SL_Traverser;
import org.apache.tinkerpop.gremlin.process.traversal.traverser.B_LP_O_S_SE_SL_Traverser;
import org.apache.tinkerpop.gremlin.process.traversal.traverser.B_O_S_SE_SL_Traverser;
import org.apache.tinkerpop.gremlin.process.traversal.traverser.B_O_Traverser;
import org.apache.tinkerpop.gremlin.process.traversal.traverser.O_Traverser;
import org.apache.tinkerpop.gremlin.structure.Edge;
import org.apache.tinkerpop.gremlin.structure.Element;
import org.apache.tinkerpop.gremlin.structure.Graph;
import org.apache.tinkerpop.gremlin.structure.Property;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.apache.tinkerpop.gremlin.structure.VertexProperty;
import org.apache.tinkerpop.gremlin.structure.util.GraphFactory;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

/**
 * Those developing Gremlin implementations must provide a GraphProvider implementation so that the
 * different test suites know how to instantiate their implementations.  Implementers may choose to have multiple
 * {@code GraphProvider} implementations to mix and match with multiple test suite implementations.  For example,
 * create one {@code GraphProvider} that has no indices defined and a separate {@code GraphProvider} that has
 * indices.  Then create separate test suite implementations for each {@code GraphProvider}.  This approach will
 * have the test suites executed once for each {@code GraphProvider} ensuring that the {@link Graph} implementation
 * works under multiple configurations.  Consider making these "extra" tests "integration tests" so that they
 * don't have to be executed on every run of the build so as to save time.  Run the "integration tests" periodically
 * to ensure overall compliance.
 *
 * @author Stephen Mallette (http://stephen.genoprime.com)
 */
public interface GraphProvider {

    /**
     * Implementations from {@code gremlin-core} that need to be part of the clear process.  This does not exempt
     * providers from having to register their extensions to any of these classes, but does prevent them from
     * having to register them in addition to their own.
     */
    public static final Set<Class> CORE_IMPLEMENTATIONS = new HashSet<Class>() {{
        add(__.class);
        add(DefaultGraphTraversal.class);
        add(GraphTraversalSource.class);
        add(B_O_S_SE_SL_Traverser.class);
        add(B_LP_O_P_S_SE_SL_Traverser.class);
        add(B_LP_O_S_SE_SL_Traverser.class);
        add(B_O_Traverser.class);
        add(O_Traverser.class);
    }};

    /**
     * Create a {@link GraphTraversalSource} from a {@link Graph} instance.  The default implementation does not
     * use {@link GraphComputer} so providers should override as necessary if their implementation is testing
     * something that requires a different engine type, like {@link GraphComputer}.
     */
    public default GraphTraversalSource traversal(final Graph graph) {
        return graph.traversal();
    }

    /**
     * Create a {@link GraphTraversalSource} from a {@link Graph} instance.  The default implementation does not use
     * {@link GraphComputer} so providers should override as necessary if their implementation is testing
     * something that requires a different engine type, like {@link GraphComputer}.
     * <p/>
     * Implementations should apply strategies as necessary to the
     * {@link GraphTraversalSource} before calling it's {@code create} method.
     */
    public default GraphTraversalSource traversal(final Graph graph, final TraversalStrategy... strategies) {
        return this.traversal(graph).withStrategies(strategies);
    }

    /**
     * Create a {@link GraphComputer} from the {@link Graph} instance. The default implementation simply calls {@code graph.compute()}.
     *
     * @param graph the graph to get the graph computer from
     * @return a new graph computer
     */
    public default GraphComputer getGraphComputer(final Graph graph) {
        return graph.compute();
    }

    /**
     * Creates a new {@link Graph} instance using the default
     * {@code org.apache.commons.configuration.Configuration} from
     * {@link #standardGraphConfiguration(Class, String, LoadGraphWith.GraphData)}.
     */
    default public Graph standardTestGraph(final Class<?> test, final String testMethodName, final LoadGraphWith.GraphData loadGraphWith) {
        return GraphFactory.open(standardGraphConfiguration(test, testMethodName, loadGraphWith));
    }

    /**
     * Creates a new {@link Graph} instance from the {@code Configuration} object using {@link GraphFactory}.
     * The assumption here is that the {@code Configuration} has been created by one of the
     * {@link #newGraphConfiguration(String, Class, String, LoadGraphWith.GraphData)} methods and has therefore
     * already been modified by the implementation as necessary for {@link Graph} creation.
     */
    default public Graph openTestGraph(final Configuration config) {
        return GraphFactory.open(config);
    }

    /**
     * Gets the {@code Configuration} object that can construct a {@link Graph} instance from {@link GraphFactory}.
     * Note that this method should create a {@link Graph} using the {@code graphName} of "standard", meaning it
     * should always return a configuration instance that generates the same {@link Graph} from the
     * {@link GraphFactory}.
     */
    default public Configuration standardGraphConfiguration(final Class<?> test, final String testMethodName, final LoadGraphWith.GraphData loadGraphWith) {
        return newGraphConfiguration("standard", test, testMethodName, Collections.<String, Object>emptyMap(), loadGraphWith);
    }

    /**
     * If possible (usually with persisted graph) clear the space on disk given the configuration that would be used
     * to construct the graph.  The default implementation simply calls
     * {@link #clear(Graph, org.apache.commons.configuration.Configuration)} with
     * a null graph argument.
     * <p/>
     * Implementations should be able to accept an argument of null for the {@code org.apache.commons.configuration.Configuration}
     * as well, and a proper handling is needed. Otherwise, a NullPointerException may be thrown.
     */
    public default void clear(final Configuration configuration) throws Exception {
        clear(null, configuration);
    }

    /**
     * Clears a {@link Graph} of all data and settings.  Implementations will have different ways of handling this.
     * It is typically expected that {@link Graph#close()} will be called and open transactions will be closed.
     * For a brute force approach, implementers can simply delete data directories provided in the configuration.
     * Implementers may choose a more elegant approach if it exists.
     * <p/>
     * Implementations should be able to accept an argument of null for the {@code Graph}, in which case the only action
     * that can be performed is a clear given the configuration.  The method will typically be called this way
     * as clean up task on setup to ensure that a persisted graph has a clear space to create a test graph.
     * <p/>
     * Implementations should be able to accept an argument of null for the {@code org.apache.commons.configuration.Configuration}
     * as well, and a proper handling is needed. Otherwise, a NullPointerException may be thrown.
     * <p/>
     * Calls to this method may occur multiple times for a specific test. Develop this method to be idempotent.
     */
    public void clear(final Graph graph, final Configuration configuration) throws Exception;

    /**
     * Converts an identifier from a test to an identifier accepted by the {@link Graph} instance.  Test that try to
     * utilize an Element identifier will pass it to this method before usage.  This method should be sure to
     * be consistent in the return value such that calling it with "x" should always return the same transformed
     * value.
     */
    default public Object convertId(final Object id, final Class<? extends Element> c) {
        return id;
    }

    /**
     * Converts an label from a test to an label accepted by the Graph instance.  Test that try to
     * utilize a label will pass it to this method before usage.
     */
    default public String convertLabel(final String label) {
        return label;
    }

    /**
     * When implementing this method ensure that a test suite can override any settings EXCEPT the
     * "gremlin.graph" setting which should be defined by the implementer. It should provide a
     * {@code Configuration} that will generate a graph unique to that {@code graphName}.
     *
     * @param graphName              a unique test graph name
     * @param test                   the test class
     * @param testMethodName         the name of the test
     * @param configurationOverrides settings to override defaults with.
     * @param loadGraphWith          the data set to load and will be null if no data is to be loaded
     */
    public Configuration newGraphConfiguration(final String graphName,
                                               final Class<?> test,
                                               final String testMethodName,
                                               final Map<String, Object> configurationOverrides,
                                               final LoadGraphWith.GraphData loadGraphWith);

    /**
     * When implementing this method ensure that a test suite can override any settings EXCEPT the
     * "gremlin.graph" setting which should be defined by the implementer. It should provide a
     * {@code Configuration} that will generate a graph unique to that {@code graphName}.
     *
     * @param graphName      a unique test graph name
     * @param test           the test class
     * @param testMethodName the name of the test
     * @param loadGraphWith  the data set to load and will be null if no data is to be loaded
     */
    default public Configuration newGraphConfiguration(final String graphName,
                                                       final Class<?> test,
                                                       final String testMethodName,
                                                       final LoadGraphWith.GraphData loadGraphWith) {
        return newGraphConfiguration(graphName, test, testMethodName, new HashMap<>(), loadGraphWith);
    }

    /**
     * Tests are annotated with a {@link LoadGraphWith} annotation. These annotations tell the test what kind of data
     * to preload into the graph instance.  It is up to the implementation to load the graph with the data specified
     * by that annotation. This method also represents the place where indices should be configured according the
     * the {@link Graph} implementation's API. Implementers can use the {@code testClass} and {@code testName}
     * arguments to implement test specific configurations to their graphs.
     *
     * @param graph         the {@link Graph} instance to load data into constructed by this {@code GraphProvider}
     * @param loadGraphWith the annotation for the currently running test - this value may be null if no graph
     *                      data is to be loaded in front of the test.
     * @param testClass     the test class being executed
     * @param testName      the name of the test method being executed
     */
    public void loadGraphData(final Graph graph, final LoadGraphWith loadGraphWith, final Class testClass, final String testName);

    /**
     * Get the set of concrete implementations of certain classes and interfaces utilized by the test suite. This
     * method should return any implementations or extensions of the following interfaces or classes:
     * <ul>
     * <li>{@link Edge}</li>
     * <li>{@link Element}</li>
     * <li>{@link DefaultGraphTraversal}</li>
     * <li>{@link Graph}</li>
     * <li>{@link Graph.Variables}</li>
     * <li>{@link GraphTraversal}</li>
     * <li>{@link B_LP_O_P_S_SE_SL_Traverser}</li>
     * <li>{@link Property}</li>
     * <li>{@link B_O_S_SE_SL_Traverser}</li>
     * <li>{@link Traversal}</li>
     * <li>{@link Traverser}</li>
     * <li>{@link Vertex}</li>
     * <li>{@link VertexProperty}</li>
     * </ul>
     * <p/>
     * The test suite only enforces registration of the following core structure interfaces (i.e. these classes must
     * be registered or the tests will fail to execute):
     * <ul>
     * <li>{@link Edge}</li>
     * <li>{@link Element}</li>
     * <li>{@link Graph}</li>
     * <li>{@link Graph.Variables}</li>
     * <li>{@link Property}</li>
     * <li>{@link Vertex}</li>
     * <li>{@link VertexProperty}</li>
     * </ul>
     * <p/>
     * The remaining interfaces and classes should be registered however as failure to do so, might cause failures
     * in the Groovy environment testing suite.
     * <p/>
     * Internally speaking, tests that make use of this method should bind in {@link #CORE_IMPLEMENTATIONS} to the
     * {@link Set} because these represent {@code gremlin-core} implementations that are likely not registered
     * by the vendor implementations.
     */
    public Set<Class> getImplementations();

    /**
     * Helper method for those build {@link GraphProvider} implementations that need a standard working directory
     * for tests (e.g. graphs that persist data to disk). Typically, there is no need to override the default
     * behavior of this method and if it is overridden, it is usually best to continue to use the {@link TestHelper}
     * to produce the working directory as it will create the path in the appropriate build directories.
     * @return UNIX-formatted absolute directory path
     */
    public default String getWorkingDirectory() {
        return TestHelper.makeTestDataDirectory(this.getClass(), "graph-provider-data");
    }

    /**
     * Returns a {@link TestListener} implementation that provides feedback to the {@link GraphProvider} implementation.
     * By default, this returns an empty listener.
     */
    public default Optional<TestListener> getTestListener() {
        return Optional.empty();
    }

    /**
     * Gets a {@link Graph.Features} implementation that contains graph feature configuration that will never change
     * from execution to execution of the tests given this current {@code GraphProvider} implementation. Implementing
     * this method will allow the test suite to avoid creation of a {@link Graph} instance and thus speed up the
     * execution of tests if that creation process is expensive. It is important that this static set of features be
     * representative of what the {@link Graph} instance produced by this {@code GraphProvider} can actually do or
     * else the cost of {@link Graph} instantiation will be incurred when it doesn't need to be. It is also important
     * that this method be faster than the cost of {@link Graph} creation in the first place or there really won't be
     * any difference in execution speed. In cases where some features are static and others are not, simply throw
     * an {@code UnsupportedOperationException} from those features to let the test suite know that it cannot rely
     * on them and the test suite will revert to using a constructed {@link Graph} instance.
     */
    public default Optional<Graph.Features> getStaticFeatures() {
        return Optional.empty();
    }

    /**
     * An annotation to be applied to a {@code GraphProvider} implementation that provides additional information
     * about its intentions. The {@code Descriptor} is required by those {@code GraphProvider} implementations
     * that will be assigned to test suites that use
     * {@link org.apache.tinkerpop.gremlin.process.traversal.TraversalEngine.Type#COMPUTER}.
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Inherited
    public @interface Descriptor {

        /**
         * The {@link GraphComputer} implementation that the {@code GraphProvider} will use when it constructs
         * a {@link Traversal} with {@link #traversal(Graph)} or {@link #traversal(Graph, TraversalStrategy[])}.
         * This value should be null if a {@link GraphComputer} is not being used.
         */
        public Class<? extends GraphComputer> computer();
    }

    public interface TestListener {
        public default void onTestStart(final Class<?> test, final String testName) {
            // do nothing
        }

        public default void onTestEnd(final Class<?> test, final String testName) {
            // do nothing
        }
    }
}
