//////////////////////////////////////////////////////////////////////////////////////////
//
// Implementation of the TinkerPop OLTP Provider API for ArangoDB
//
// Copyright triAGENS GmbH Cologne and The University of York
//
//////////////////////////////////////////////////////////////////////////////////////////

package com.arangodb.tinkerpop.gremlin.utils;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.configuration.BaseConfiguration;
import org.apache.commons.lang3.tuple.ImmutableTriple;
import org.apache.commons.lang3.tuple.Triple;
import org.apache.tinkerpop.gremlin.structure.Graph;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.arangodb.Protocol;
import com.arangodb.entity.LoadBalancingStrategy;
import com.arangodb.tinkerpop.gremlin.structure.ArangoDBGraph;


/**
 * The Class ArangoDBConfigurationBuilder provides a convenient method for creating ArangoDB graph
 * configurations.
 * If no parameters are set the default values used are:
 * <ul>
 * <li>dbName: tinkerpop
 * <li>graphName: graph
 * <li>vertices: (empty - use default ArangoDBGraph settings)
 * <li>edges: (empty - use default ArangoDBGraph settings)
 * <li>relations: (empty - use default ArangoDBGraph settigns)
 * <li>user: gremlin
 * <li>password: gremlin
 * <li>other db settings: (default ArangoDB Java driver settings).
 * <li>collectionNames: prefixed with graphName</li>
 * </ul>
 * @author Horacio Hoyos Rodriguez (https://www.york.ac.uk)
 */
public class ArangoDBConfigurationBuilder {
	
	private static final Logger logger = LoggerFactory.getLogger(ArangoDBConfigurationBuilder.class);
	
	private static final String PROPERTY_KEY_HOSTS = "arangodb.hosts";
	private static final String PROPERTY_KEY_TIMEOUT = "arangodb.timeout";
	private static final String PROPERTY_KEY_USER = "arangodb.user";
	private static final String PROPERTY_KEY_PASSWORD = "arangodb.password";
	private static final String PROPERTY_KEY_USE_SSL = "arangodb.usessl";
	private static final String PROPERTY_KEY_V_STREAM_CHUNK_CONTENT_SIZE = "arangodb.chunksize";
	private static final String PROPERTY_KEY_MAX_CONNECTIONS = "arangodb.connections.max";
	private static final String PROPERTY_KEY_CONNECTION_TTL = "arangodb.connections.ttl";
	private static final String PROPERTY_KEY_ACQUIRE_HOST_LIST = "arangodb.acquireHostList";
	private static final String PROPERTY_KEY_LOAD_BALANCING_STRATEGY = "arangodb.loadBalancingStrategy";
	private static final String PROPERTY_KEY_PROTOCOL = "arangodb.protocol";
	private static final String PROPERTY_KEY_SHOULD_PREFIX_COLLECTION_NAMES = "arangodb.shouldPrefixCollectionNames";

	/** The db name. */
	private String dbName = "tinkerpop";
	
	/** The graph name. */
	private String graphName = "graph";
	
	/** The user. */
	private String user = "gremlin";
	
	/** The password. */
	private String password = "gremlin";
	
	/** The acquire host list flag. */
	private boolean hostList;
	
	/** The use ssl flag. */
	private boolean useSsl;
	
	/** The max number of connections. */
	private Integer connections;
	
	/** The timeout. */
	private Integer timeout;
	
	/** The VelocyStream chunk size. */
	private Long velocyStreamChunk;
	
	/** The connection ttl. */
	private Long connectionTtl;
	
	/** The protocol. */
	private Protocol protocol;
	
	/** The strategy. */
	private LoadBalancingStrategy strategy;
	
	/** The edges. */
	private Set<String> edges = new HashSet<>();
	
	/** The vertices. */
	private Set<String> vertices = new HashSet<>();
	
	/** The relations. */
	private Set<Triple<String, Set<String>, Set<String>>> relations = new HashSet<>();
	
	/** The hosts. */
	private Set<String> hosts = new HashSet<>();

	/** If Collection Names should be prefixed with Graph name. **/
	private Boolean shouldPrefixCollectionNames = true;

	/**
	 * Instantiates a new arango DB configuration builder.
	 */
	
	public ArangoDBConfigurationBuilder() {
		
	}
	
	/**
	 * Build the configuration.
	 *
	 * @return a configuration that can be used to instantiate a new {@link ArangoDBGraph}.
	 * @see ArangoDBGraph#open(org.apache.commons.configuration.Configuration)
	 */
	
	public BaseConfiguration build() {
		BaseConfiguration config = new BaseConfiguration();
		config.setListDelimiter('/');
		config.addProperty(fullPropertyKey(ArangoDBGraph.PROPERTY_KEY_DB_NAME), dbName);
		config.addProperty(fullPropertyKey(ArangoDBGraph.PROPERTY_KEY_GRAPH_NAME), graphName);
		config.addProperty(fullPropertyKey(ArangoDBGraph.PROPERTY_KEY_VERTICES), vertices);
		config.addProperty(fullPropertyKey(ArangoDBGraph.PROPERTY_KEY_EDGES), edges);
		List<String> rels = new ArrayList<>();
		for (Triple<String, Set<String>, Set<String>> r : relations) {
			// Make sure edge and vertex collections have been added
			StringBuilder rVal = new StringBuilder();
			rVal.append(r.getLeft());
			if (!edges.contains(r.getLeft())) {
				logger.warn("Missing edege collection {} from relations added to edge collections");
				edges.add(r.getLeft());
			}
			rVal.append(":");
			for (String sv : r.getMiddle()) {
				if (!vertices.contains(sv)) {
					logger.warn("Missing vertex collection {} from relations added to vertex collections");
					vertices.add(r.getLeft());
				}
			}
			rVal.append(r.getMiddle().stream().collect(Collectors.joining(",")));
			rVal.append("->");
			for (String sv : r.getRight()) {
				if (!vertices.contains(sv)) {
					logger.warn("Missing vertex collection {} from relations added to vertex collections");
					vertices.add(r.getLeft());
				}
			}
			rVal.append(r.getRight().stream().collect(Collectors.joining(",")));
			rels.add(rVal.toString());
			
		}
		if (!rels.isEmpty()) {
			config.addProperty(fullPropertyKey(ArangoDBGraph.PROPERTY_KEY_RELATIONS), rels.stream().collect(Collectors.joining("/")));
		}
		config.addProperty(fullPropertyKey(PROPERTY_KEY_USER), user);
		config.addProperty(fullPropertyKey(PROPERTY_KEY_PASSWORD), password);
		if (hostList) {
			config.addProperty(fullPropertyKey(PROPERTY_KEY_ACQUIRE_HOST_LIST), Boolean.toString(hostList));
		}
		if(useSsl) {
			config.addProperty(fullPropertyKey(PROPERTY_KEY_USE_SSL), Boolean.toString(useSsl));
		}
		if (connections != null) {
			config.addProperty(fullPropertyKey(PROPERTY_KEY_MAX_CONNECTIONS), connections);
		}
		if (timeout != null) {
			config.addProperty(fullPropertyKey(PROPERTY_KEY_TIMEOUT), timeout);
		}
		if (velocyStreamChunk != null) {
			config.addProperty(fullPropertyKey(PROPERTY_KEY_V_STREAM_CHUNK_CONTENT_SIZE), velocyStreamChunk);
		}
		if (connectionTtl != null) {
			config.addProperty(fullPropertyKey(PROPERTY_KEY_CONNECTION_TTL), connectionTtl);
		}
		if (protocol != null) {
			config.addProperty(fullPropertyKey(PROPERTY_KEY_PROTOCOL), protocol.name());
		}
		if (strategy != null) {
			config.addProperty(fullPropertyKey(PROPERTY_KEY_LOAD_BALANCING_STRATEGY), strategy.name());
		}
		if (!hosts.isEmpty()) {
			config.addProperty(fullPropertyKey(PROPERTY_KEY_HOSTS), hosts.stream().collect(Collectors.joining(",")));
		}
		if(shouldPrefixCollectionNames != null){
			config.addProperty(fullPropertyKey(PROPERTY_KEY_SHOULD_PREFIX_COLLECTION_NAMES), shouldPrefixCollectionNames);
		}

		config.addProperty(Graph.GRAPH, ArangoDBGraph.class.getName());
		return config;
	}
	
	private String fullPropertyKey(String key) {
		return ArangoDBGraph.PROPERTY_KEY_PREFIX + "." + key;
	}
	
	/**
	 * Name of the database to use.
	 *
	 * @param name 				the db name
	 * @return a reference to this object.
	 */
	
	public ArangoDBConfigurationBuilder dataBase(String name) {
		dbName = name;
		return this;
	}
	
	/**
	 * Name of the graph to use.
	 *
	 * @param name 			the graph name
	 * @return a reference to this object.
	 */
	
	public ArangoDBConfigurationBuilder graph(String name) {
		graphName = name;
		return this;
	}
	
	/**
	 * Add vertex collection.
	 *
	 * @param name 				the vertex collection name
	 * @return a reference to this object.
	 */
	
	public ArangoDBConfigurationBuilder withVertexCollection(String name) {
		vertices.add(name);
		return this;
	}
	
	/**
	 * Add edge collection.
	 *
	 * @param name 				the edge collection name
	 * @return a reference to this object.
	 */
	
	public ArangoDBConfigurationBuilder withEdgeCollection(String name) {
		edges.add(name);
		return this;
	}
	
	/**
	 * Configure a 1-to-1 edge, i.e. for a given edge collection define the source and target vertex
	 * collections.
	 *
	 * @param edgeCollection 		the edge collection
	 * @param sourceCollection 		the source vertex collection
	 * @param targetCollection 		the target vertex collection
	 * @return a reference to this object.
	 */
	
	public ArangoDBConfigurationBuilder configureEdge(
		String edgeCollection,
		String sourceCollection,
		String targetCollection) {
		Set<String> source = new HashSet<>();
		Set<String> target = new HashSet<>();
		source.add(sourceCollection);
		target.add(targetCollection);
		ImmutableTriple<String, Set<String>, Set<String>> triple = new ImmutableTriple<>(edgeCollection, source, target);
		relations.add(triple);
		return this;
	}
	
	/**
	 * Configure a 1-to-many edge, i.e. for a given edge collection define the source and target vertex
	 * collections.
	 *
	 * @param edgeCollection 		the edge collection
	 * @param sourceCollection 		the source vertex collection
	 * @param targetCollections 	the target vertices collections
	 * @return a reference to this object.
	 */
	
	public ArangoDBConfigurationBuilder configureEdge(
		String edgeCollection,
		String sourceCollection,
		Set<String> targetCollections) {
		Set<String> source = new HashSet<>();
		source.add(sourceCollection);
		ImmutableTriple<String, Set<String>, Set<String>> triple = new ImmutableTriple<>(edgeCollection, source, Collections.unmodifiableSet(targetCollections));
		relations.add(triple);
		return this;
	}
	
	
	/**
	 * Configure a many-to-1 edge, i.e. for a given edge collection define the source and target vertex
	 * collections.
	 *
	 * @param edgeCollection 		the edge collection
	 * @param sourceCollections 	the source vertices collections
	 * @param targetCollection 		the target vertex collection
	 * @return a reference to this object.
	 */
	
	public ArangoDBConfigurationBuilder configureEdge(
		String edgeCollection,
		Set<String> sourceCollections,
		String targetCollection) {
		Set<String> target = new HashSet<>();
		target.add(targetCollection);
		ImmutableTriple<String, Set<String>, Set<String>> triple = new ImmutableTriple<>(edgeCollection, Collections.unmodifiableSet(sourceCollections), target);
		relations.add(triple);
		return this;
	}
	
	
	/**
	 * Configure a many-to-many edge, i.e. for a given edge collection define the source and target vertex
	 * collections.
	 *
	 * @param edgeCollection 		the edge collection
	 * @param sourceCollections 	the source vertices collections
	 * @param targetCollections 	the target vertices collections
	 * @return a reference to this object.
	 */
	
	public ArangoDBConfigurationBuilder configureEdge(
		String edgeCollection,
		Set<String> sourceCollections,
		Set<String> targetCollections) {
		ImmutableTriple<String, Set<String>, Set<String>> triple = new ImmutableTriple<>(edgeCollection, Collections.unmodifiableSet(sourceCollections), Collections.unmodifiableSet(targetCollections));
		relations.add(triple);
		return this;
	}
	
	
	/**
	 * ArangoDB hosts.
	 * <p>
	 * This method can be used multiple times to add multiple hosts (i.e. fallback hosts).
	 *
	 * @param host 					the host in the form url:port
	 * @return a reference to this object.
	 * @see <a href="https://github.com/arangodb/arangodb-java-driver/blob/master/docs/Drivers/Java/Reference/Setup.md">ArangoDB Java Driver</a>
	 */
	
	public ArangoDBConfigurationBuilder arangoHosts(String host) {
		this.hosts.add(host);
		return this;
	}
	
	/**
	 * ArangoDB socket connect timeout(milliseconds).
	 *
	 * @param timeout 					the tiemout
	 * @return a reference to this object.
	 * @see <a href="https://github.com/arangodb/arangodb-java-driver/blob/master/docs/Drivers/Java/Reference/Setup.md">ArangoDB Java Driver</a>
	 */
	
	public ArangoDBConfigurationBuilder arangoTimeout(int timeout) {
		this.timeout = timeout;
		return this;
	}
	
	/**
	 * ArangoDB Basic Authentication User.
	 *
	 * @param user 					the user
	 * @return a reference to this object.
	 * @see <a href="https://github.com/arangodb/arangodb-java-driver/blob/master/docs/Drivers/Java/Reference/Setup.md">ArangoDB Java Driver</a>
	 */
	
	public ArangoDBConfigurationBuilder arangoUser(String user) {
		this.user = user;
		return this;
	}
	
	
	/**
	 * ArangoDB Basic Authentication Password.
	 *
	 * @param password the password
	 * @return a reference to this object.
	 * @see <a href="https://github.com/arangodb/arangodb-java-driver/blob/master/docs/Drivers/Java/Reference/Setup.md">ArangoDB Java Driver</a>
	 */
	
	public ArangoDBConfigurationBuilder arangoPassword(String password) {
		this.password = password;
		return this;
	}
	
	/**
	 * ArangoDB use SSL connection.
	 *
	 * @param useSsl 					true, to use SSL connection.
	 * @return a reference to this object.
	 * @see <a href="https://github.com/arangodb/arangodb-java-driver/blob/master/docs/Drivers/Java/Reference/Setup.md">ArangoDB Java Driver</a>
	 */
	
	public ArangoDBConfigurationBuilder arangoSSL(boolean useSsl) {
		this.useSsl = useSsl;
		return this;
	}
	
	/**
	 * ArangoDB VelocyStream Chunk content-size(bytes).
	 *
	 * @param size 					the size
	 * @return a reference to this object.
	 * @see <a href="https://github.com/arangodb/arangodb-java-driver/blob/master/docs/Drivers/Java/Reference/Setup.md">ArangoDB Java Driver</a>
	 */
	
	public ArangoDBConfigurationBuilder arangoVelocyStreamChunk(long size) {
		this.velocyStreamChunk = size;
		return this;
	}
	
	
	/**
	 * ArangoDB max number of connections.
	 *
	 * @param connections 					the connections
	 * @return a reference to this object.
	 * @see <a href="https://github.com/arangodb/arangodb-java-driver/blob/master/docs/Drivers/Java/Reference/Setup.md">ArangoDB Java Driver</a>
	 */
	
	public ArangoDBConfigurationBuilder arangoMaxConnections(int connections) {
		this.connections = connections;
		return this;
	}
	
	/**
	 * ArangoDB Connection time to live (ms).
	 *
	 * @param time 				the time
	 * @return a reference to this object.
	 * @see <a href="https://github.com/arangodb/arangodb-java-driver/blob/master/docs/Drivers/Java/Reference/Setup.md">ArangoDB Java Driver</a>
	 */
	
	public ArangoDBConfigurationBuilder arangoTTL(long time) {
		this.connectionTtl = time;
		return this;
	}
	
	
	/**
	 * ArangoDB used network protocol.
	 * <p>
	 * <b>Note:</b> If you are using ArangoDB 3.0.x you have to set the protocol to Protocol.HTTP_JSON
	 * because it is the only one supported.
	 *
	 * @param protocol 				the protocol
	 * @return a reference to this object.
	 * @see <a href="https://github.com/arangodb/arangodb-java-driver/blob/master/docs/Drivers/Java/Reference/Setup.md">ArangoDB Java Driver</a>
	 */
	
	public ArangoDBConfigurationBuilder arangoNetworkProtocol(Protocol protocol) {
		this.protocol = protocol;
		return this;
	}
	
	
	/**
	 * ArangoDB load balancing strategy.
	 *
	 * @param strategy 				the strategy
	 * @return a reference to this object.
	 * @see <a href="https://github.com/arangodb/arangodb-java-driver/blob/master/docs/Drivers/Java/Reference/Setup.md">ArangoDB Java Driver</a>
	 */
	
	public ArangoDBConfigurationBuilder arangoNetworkProtocol(LoadBalancingStrategy strategy) {
		this.strategy = strategy;
		return this;
	}
	
	/**
	 * ArangoDB acquire a list of all known hosts in the cluster.
	 *
	 * @param hostList the host list
	 * @return a reference to this object.
	 * @see <a href="https://github.com/arangodb/arangodb-java-driver/blob/master/docs/Drivers/Java/Reference/Setup.md">ArangoDB Java Driver</a>
	 */
	
	public ArangoDBConfigurationBuilder arangoAcquireHostList(boolean hostList) {
		this.hostList = hostList;
		return this;
	}

	/**
	 * In case of colliding collection names in Graph, these names can be prefixed with Graph Name.
	 * If set to true collection names are in {@code %s_%s} format (where first %s is graph name, second %s is collection name).
	 * If set to false, collection names are without any prefix.
	 * Default set to <b>true</b>.
	 * @param shouldPrefixCollectionNames whether it should prefixed or not.
	 * @return a reference to this object.
	 */
	public ArangoDBConfigurationBuilder shouldPrefixCollectionNamesWithGraphName(boolean shouldPrefixCollectionNames){
		this.shouldPrefixCollectionNames = shouldPrefixCollectionNames;
		return this;
	}

}