/*
 * 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.structure.util;

import org.apache.tinkerpop.gremlin.structure.Graph;
import org.apache.tinkerpop.gremlin.util.config.YamlConfiguration;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.MapConfiguration;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.configuration.XMLConfiguration;

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

/**
 * Factory to construct new {@link org.apache.tinkerpop.gremlin.structure.Graph} instances from a
 * {@code Configuration} object or properties file.
 *
 * @author Stephen Mallette (http://stephen.genoprime.com)
 */
public final class GraphFactory {

    private GraphFactory() {}

    /**
     * Open a graph.  See each {@link Graph} instance for its configuration options.
     *
     * @param configuration A configuration object that specifies the minimally required properties for a                        I
     *                      {@link Graph} instance. This minimum is determined by the
     *                      {@link Graph} instance itself.
     * @return A {@link org.apache.tinkerpop.gremlin.structure.Graph} instance.
     * @throws IllegalArgumentException if {@code configuration}
     */
    public static Graph open(final Configuration configuration) {
        if (null == configuration)
            throw Graph.Exceptions.argumentCanNotBeNull("configuration");

        final String clazz = configuration.getString(Graph.GRAPH, null);
        if (null == clazz)
            throw new RuntimeException(String.format("Configuration must contain a valid '%s' setting", Graph.GRAPH));

        final Class<?> graphClass;
        try {
            graphClass = Class.forName(clazz);
        } catch (final ClassNotFoundException e) {
            throw new RuntimeException(String.format("GraphFactory could not find [%s] - Ensure that the jar is in the classpath", clazz));
        }

        // If the graph class specifies a factory class then use that instead of the specified class.
        final GraphFactoryClass factoryAnnotation = graphClass.getAnnotation(GraphFactoryClass.class);
        final Class<?> factoryClass = factoryAnnotation != null ? factoryAnnotation.value() : graphClass;

        return open(configuration, factoryClass);
    }

    private static Graph open(final Configuration configuration, final Class<?> graphFactoryClass)
    {
        final Graph g;
        try {
            // will use open(Configuration c) to instantiate
            g = (Graph) graphFactoryClass.getMethod("open", Configuration.class).invoke(null, configuration);
        } catch (final NoSuchMethodException e1) {
            throw new RuntimeException(String.format("GraphFactory can only instantiate Graph implementations from classes that have a static open() method that takes a single Apache Commons Configuration argument - [%s] does not seem to have one", graphFactoryClass));
        } catch (final Exception e2) {
            throw new RuntimeException(String.format("GraphFactory could not instantiate this Graph implementation [%s]", graphFactoryClass), e2);
        }
        return g;
    }

    /**
     * Open a graph.  See each {@link Graph} instance for its configuration options. This file may be XML, YAML,
     * or a standard properties file. How the configuration is used (and which kind is required) is dependent on
     * the implementation.
     * <p/>
     * If using XML, ensure that the appropriate version of Apache {@code commons-collections} is available on the
     * classpath as it is an optional dependency of Apache {@code commons-configuration}, the library that
     * {@code GraphFactory} depends on.
     *
     * @param configurationFile The location of a configuration file that specifies the minimally required properties
     *                          for a {@link org.apache.tinkerpop.gremlin.structure.Graph} instance. This minimum is determined by the {@link org.apache.tinkerpop.gremlin.structure.Graph} instance
     *                          itself.
     * @return A {@link org.apache.tinkerpop.gremlin.structure.Graph} instance.
     * @throws IllegalArgumentException if {@code configurationFile} is null
     */
    public static Graph open(final String configurationFile) {
        if (null == configurationFile) throw Graph.Exceptions.argumentCanNotBeNull("configurationFile");
        return open(getConfiguration(new File(configurationFile)));
    }

    /**
     * Open a graph. See each {@link org.apache.tinkerpop.gremlin.structure.Graph} instance for its configuration options.
     *
     * @param configuration A {@link java.util.Map} based configuration that will be converted to an {@link org.apache.commons.configuration.Configuration} object
     *                      via {@link org.apache.commons.configuration.MapConfiguration} and passed to the appropriate overload.
     * @return A Graph instance.
     */
    public static Graph open(final Map configuration) {
        if (null == configuration) throw Graph.Exceptions.argumentCanNotBeNull("configuration");
        return open(new MapConfiguration(configuration));
    }

    private static Configuration getConfiguration(final File configurationFile) {
        if (!configurationFile.isFile())
            throw new IllegalArgumentException(String.format("The location configuration must resolve to a file and [%s] does not", configurationFile));

        try {
            final String fileName = configurationFile.getName();
            final String fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1);

            switch (fileExtension) {
                case "yml":
                case "yaml":
                    final YamlConfiguration config = new YamlConfiguration();
                    config.load(configurationFile);
                    return config;
                case "xml":
                    return new XMLConfiguration(configurationFile);
                default:
                    return new PropertiesConfiguration(configurationFile);
            }
        } catch (final ConfigurationException e) {
            throw new IllegalArgumentException(String.format("Could not load configuration at: %s", configurationFile), e);
        }
    }
}