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

package com.arangodb.tinkerpop.gremlin.client;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import com.arangodb.entity.*;
import org.apache.tinkerpop.gremlin.structure.Direction;
import org.apache.tinkerpop.gremlin.structure.Graph;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.arangodb.ArangoCollection;
import com.arangodb.ArangoCursor;
import com.arangodb.ArangoDB;
import com.arangodb.ArangoDBException;
import com.arangodb.ArangoDatabase;
import com.arangodb.ArangoGraph;
import com.arangodb.model.AqlQueryOptions;
import com.arangodb.model.GraphCreateOptions;
import com.arangodb.tinkerpop.gremlin.client.ArangoDBQueryBuilder.UniqueVertices;
import com.arangodb.tinkerpop.gremlin.structure.ArangoDBEdge;
import com.arangodb.tinkerpop.gremlin.structure.ArangoDBGraph;
import com.arangodb.tinkerpop.gremlin.structure.ArangoDBGraphVariables;
import com.arangodb.tinkerpop.gremlin.structure.ArangoDBVertex;
import com.arangodb.tinkerpop.gremlin.utils.ArangoDBUtil;

/**
 * The arangodb graph client class handles the HTTP connection to arangodb and performs database
 * operations on the ArangoDatabase.
 *
 * @author Achim Brandt (http://www.triagens.de)
 * @author Johannes Gocke (http://www.triagens.de)
 * @author Guido Schwab (http://www.triagens.de)
 * @author Jan Steemann (http://www.triagens.de)
 * @author Horacio Hoyos Rodriguez (https://www.york.ac.uk)
 */

public class ArangoDBGraphClient {

	/**
     * Common exceptions to use with an ArangoDB. This class is intended to translate ArangoDB error codes into
	 * meaningful exceptions with standard messages. ArangoDBException exception is a RuntimeException intended to
	 * break execution.
     */
	
    public static class ArangoDBExceptions {
    	
    	/** The error code regex. Matches response messages from the ArangoDB client */

	    public static Pattern ERROR_CODE = Pattern.compile("^Response:\\s\\d+,\\sError:\\s(\\d+)\\s-\\s([a-z\\s]+).+");
    	
        /**
         * Instantiation happens via factory method
         */

        private ArangoDBExceptions() { }
        
        /**
         * Translate ArangoDB Error code into exception (@see <a href="https://docs.arangodb.com/latest/Manual/Appendix/ErrorCodes.html">Error codes</a>)
         *
         * @param ex the ex
         * @return The ArangoDBClientException
         */

        public static ArangoDBGraphException getArangoDBException(ArangoDBException ex) {
        	String errorMessage = ex.getMessage();
			Matcher m = ERROR_CODE.matcher(errorMessage);
        	if (m.matches()) {
        		int code = Integer.parseInt(m.group(1));
        		String msg = m.group(2);
        		switch ((int)code/100) {
        		case 10:	// Internal ArangoDB storage errors
        			return new ArangoDBGraphException(code, String.format("Internal ArangoDB storage error (%s): %s", code, msg), ex);
        		case 11:
        			return new ArangoDBGraphException(code, String.format("External ArangoDB storage error (%s): %s", code, msg), ex);
        		case 12:
        			return new ArangoDBGraphException(code, String.format("General ArangoDB storage error (%s): %s", code, msg), ex);
        		case 13:
        			return new ArangoDBGraphException(code, String.format("Checked ArangoDB storage error (%s): %s", code, msg), ex);
        		case 14:
        			return new ArangoDBGraphException(code, String.format("ArangoDB replication/cluster error (%s): %s", code, msg), ex);
        		case 15:
        			return new ArangoDBGraphException(code, String.format("ArangoDB query error (%s): %s", code, msg), ex);
        		case 19:
        			return new ArangoDBGraphException(code, String.format("Graph / traversal errors (%s): %s", code, msg), ex);
        		}
        	}
        	return new ArangoDBGraphException("General ArangoDB error (unkown error code)", ex);
        }

        /** "name to long" Message. */

        public static String NAME_TO_LONG = "Name is too long: {} bytes (max 64 bytes for labels, 256 for keys)";

        /**
         * Gets the naming convention error.
         *
         * @param cause the cause
         * @param details the details
         * @return the naming convention error
         */

        public static ArangoDBGraphException getNamingConventionError(String cause, String details) {
            return new ArangoDBGraphException("The provided label or name name does not satisfy the naming conventions." +
                    String.format(cause, details));
        }

        /**
         * Error persisting element property.
         *
         * @param ex the ex
         * @return the arango DB graph exception
         */

        public static ArangoDBGraphException errorPersistingElmenentProperty(ArangoDBGraphException ex) {
            return new ArangoDBGraphException("Error persisting property in element. ", ex);
        }
    }
	
	private static final Logger logger = LoggerFactory.getLogger(ArangoDBGraphClient.class);

	private final ArangoDB driver;
	
	private final ArangoDatabase db;

	private final int batchSize;

	private final ArangoDBGraph graph;

    /**
     * Create a simple graph client and connect to the provided db. If the DB does not exist, the driver will try to
	 * create one.
     *
	 * @param graph					the ArangoDB graph that uses this client
	 * @param properties            the ArangoDB configuration properties
	 * @param dbname                the ArangoDB name to connect to or create
	 * @param batchSize             the size of the batch mode chunks
	 * @throws ArangoDBGraphException 	If the db does not exist and cannot be created
     */

    public ArangoDBGraphClient(
		ArangoDBGraph graph,
		Properties properties,
		String dbname,
		int batchSize)
        throws ArangoDBGraphException {
        this(graph, properties, dbname, batchSize, false);
    }
	
	/**
	 * Create a simple graph client and connect to the provided db. The create flag controls what is the
	 * behaviour if the db is not found
	 *
	 * @param graph					the ArangoDB graph that uses this client
	 * @param properties            the ArangoDB configuration properties
	 * @param dbname                the ArangoDB name to connect to or create
	 * @param batchSize             the size of the batch mode chunks
	 * @param createDatabase        if true, the driver will attempt to crate the DB if it does not exist
     * @throws ArangoDBGraphException 	If the db does not exist and cannot be created
	 */
	
	public ArangoDBGraphClient(
		ArangoDBGraph graph,
		Properties properties,
		String dbname,
		int batchSize,
		boolean createDatabase)
		throws ArangoDBGraphException {	
		logger.info("Initiating the ArangoDb Client");
		this.graph = graph;
		ByteArrayOutputStream os = new ByteArrayOutputStream();
		try {
			properties.store(os, null);
			InputStream targetStream = new ByteArrayInputStream(os.toByteArray());
			driver = new ArangoDB.Builder().loadProperties(targetStream)
					.build();
		} catch (IOException e) {
			throw new ArangoDBGraphException("Unable to read properties", e);
		}
		db = driver.db(dbname);
		if (createDatabase) {
			if (!db.exists()) {
				logger.info("DB not found, attemtping to create it.");
				try {
					if (!driver.createDatabase(dbname)) {
						throw new ArangoDBGraphException("Unable to crate the database " + dbname);
					}
				}
				catch (ArangoDBException ex) {
					throw ArangoDBExceptions.getArangoDBException(ex);
				}
			}
		}
		else {
			boolean exists = false;
			try {
				exists = db.exists();
			} catch (ArangoDBException ex) {
				// Pass
			}
			if (!exists) {
				logger.error("Database does not exist, or the user has no access");
				throw new ArangoDBGraphException(String.format("DB not found or user has no access: {}@{}",
						properties.getProperty("arangodb.user"), dbname));
			}
		}
		this.batchSize = batchSize;
	}

	/**
	 * Shutdown the client and free resources.
	 */
	
	public void shutdown() {
		logger.debug("Shutdown");
		if (db != null) {
			if (db.exists()) {
				db.clearQueryCache();
			}
		}
		if (driver != null) driver.shutdown();
	}
	
	/**
	 * Drop the graph and its related collections.
	 *
	 * @param graph 					the graph to clear
	 * @throws ArangoDBGraphException	if there was an error dropping the graph and its collections
	 */
	
	public void clear(ArangoDBGraph graph) throws ArangoDBGraphException {
		logger.info("Clear {}", graph.name());
		deleteGraph(graph.name());
	}

	/**
	 * Request the version of ArangoDB.
	 *
	 * @return the Version number
	 * @throws ArangoDBGraphException if user has no access to the db
	 */
	
	public String getVersion() throws ArangoDBGraphException {
		try {
			return db.getVersion().getVersion();
		} catch (ArangoDBException ex) {
            throw ArangoDBExceptions.getArangoDBException(ex);
        }
	}

	
	/**
	 * Gets the database.
	 *
	 * @return the ArangoDB
	 */
	
	public ArangoDatabase getDB() {
		return db;
	}

	/**
	 * Test if the db exists.
	 *
	 * @return true if the db exists
	 */
	
	public boolean dbExists() {
		return db == null ? false: db.exists();
	}
	
	/**
	 * Delete the current database accessed by the driver.
	 *
	 * @throws ArangoDBGraphException if there was an error
	 */
	
	public void deleteDb() throws ArangoDBGraphException {
		logger.info("Delete current db");
		if (db !=null) {
			try {
				db.drop();
			} catch (ArangoDBException e) {
                throw ArangoDBExceptions.getArangoDBException(e);
			}
		}
	}
	
	/**
	 * Get a document from the database. The method is generic so we it can be used to retrieve
	 * vertices, properties or variables.
	 *
	 * @param <V> 					the value type
	 * @param id            		the id of the document (should be a valid ArangoDB _id)
	 * @param collection 			the collection from which the document is retrieved
	 * @param docClass 				the returned document class
	 * @return the document
	 * @throws ArangoDBGraphException 	If there was an error retrieving the document
	 */
	
	public <V extends ArangoDBBaseDocument> V getDocument(
		String id,
		String collection,
		Class<V> docClass) {
		logger.debug("Get document with id {} from {}:{}", id, graph.name(), collection);
		V result;
		try {
			result = db.graph(graph.name())
					.vertexCollection(collection)
                    .getVertex(id, docClass);
		} catch (ArangoDBException e) {
			logger.error("Failed to retrieve vertex: {}", e.getErrorMessage());
			throw new ArangoDBGraphException("Failed to retrieve vertex.", e);
		}
		result.collection(collection);
		result.graph(graph);
		return result;
	}

	/**
	 * Insert a ArangoDBBaseDocument in the graph. The document is updated with the id, rev and key (if not present).
	 * @param document 				the document
	 * @throws ArangoDBGraphException 	If there was an error inserting the document
	 */

	public void insertDocument(ArangoDBBaseDocument document) {
		logger.debug("Insert document {} in {}", document, graph.name());
		insertDocument(document, document.collection());
	}

	/**
	 * Delete a document from the graph.
	 * @param document            	the document to delete
	 * @throws ArangoDBGraphException 	If there was an error deleting the document
	 */
	
	public void deleteDocument(ArangoDBBaseDocument document) {
		logger.debug("Delete document {} in {}", document, graph.name());
		try {
			db.graph(graph.name())
			.vertexCollection(document.collection())
			.deleteVertex(document._key());
		} catch (ArangoDBException e) {
			logger.error("Failed to delete document: {}", e.getErrorMessage());
            throw ArangoDBExceptions.getArangoDBException(e);
		}
		document.setPaired(false);
	}

	/**
	 * Update the document in the graph.
	 * @param document 				the document
	 *
	 * @throws ArangoDBGraphException 	If there was an error updating the document
	 */
	
	public void updateDocument(ArangoDBBaseDocument document) {
		logger.debug("Update document {} in {}", document, graph.name());
		VertexUpdateEntity vertexEntity;
		try {
			vertexEntity = db.graph(graph.name())
					.vertexCollection(document.collection())
					.updateVertex(document._key(), document);
		} catch (ArangoDBException e) {
			logger.error("Failed to update document: {}", e.getErrorMessage());
            throw ArangoDBExceptions.getArangoDBException(e);
		}
		logger.info("Document updated, new rev {}", vertexEntity.getRev());
		document._rev(vertexEntity.getRev());
	}
	
	/**
	 * Get an edge from the graph.
	 *
	 * @param <V> 					the value type
	 * @param id            		the id (name) of the edge
	 * @param collection 			the collection from which the edge is retrieved
	 * @param edgeClass 			the edge's specialised class
	 * @return the edge
	 * @throws ArangoDBGraphException 	If there was an error retrieving the edge
	 */
	
	public <V extends ArangoDBBaseEdge> V getEdge(
		String id,
		String collection,
        Class<V> edgeClass) {
		logger.debug("Get edge {} from {}:{}", id, graph.name(), graph.getPrefixedCollectioName(collection));
		V result;
		try {
			result = db.graph(graph.name())
					.edgeCollection(graph.getPrefixedCollectioName(collection))
					.getEdge(id, edgeClass);
		} catch (ArangoDBException e) {
			logger.error("Failed to retrieve edge: {}", e.getErrorMessage());
            throw ArangoDBExceptions.getArangoDBException(e);
		}
		result.collection(collection);
		result.graph(graph);
		return result;
	}

	/**
	 * Insert an edge in the graph. The edge is updated with the id, rev and name (if not
	 * present)
	 * @param edge            		the edge
	 * @throws ArangoDBGraphException 	If there was an error inserting the edge
	 */

	public void insertEdge(ArangoDBBaseEdge edge) {
		logger.debug("Insert edge {} in {} ", edge, graph.name());
		try {
			db.graph(graph.name())
					.edgeCollection(edge.collection())
					.insertEdge(edge);
		} catch (ArangoDBException e) {
			logger.error("Failed to insert edge: {}", e.getErrorMessage());
			throw ArangoDBExceptions.getArangoDBException(e);
		}
		edge.setPaired(true);
	}
	
	/**
	 * Delete an edge from the graph.
	 * @param edge            		the edge
	 * @throws ArangoDBGraphException 	If there was an error deleting the edge
	 */

	public void deleteEdge(ArangoDBBaseEdge edge) {
		logger.debug("Delete edge {} in {}", edge, graph.name());
		try {
			db.graph(graph.name())
			.edgeCollection(edge.collection())
			.deleteEdge(edge._key());
		} catch (ArangoDBException e) {
			logger.error("Failed to delete vertex: {}", e.getErrorMessage());
            throw ArangoDBExceptions.getArangoDBException(e);
		}
		edge.setPaired(false);
	}
	
	/**
	 * Update the edge in the graph.
	 * @param edge 					the edge
	 * @throws ArangoDBGraphException 	If there was an error updating the edge
	 */
	
	public void updateEdge(ArangoDBBaseEdge edge) {
		logger.debug("Update edge {} in {}", edge, graph.name());
		EdgeUpdateEntity edgeEntity;
		try {
			edgeEntity = db.graph(graph.name())
					.edgeCollection(edge.collection())
					.updateEdge(edge._key(), edge);
		} catch (ArangoDBException e) {
			logger.error("Failed to update vertex: {}", e.getErrorMessage());
            throw ArangoDBExceptions.getArangoDBException(e);
		}
		logger.info("Edge updated, new rev {}", edgeEntity.getRev());
		edge._rev(edgeEntity.getRev());
	}

	public ArangoDBGraphVariables getGraphVariables() {
		logger.debug("Get graph variables");
		ArangoDBGraphVariables result;
		try {
			result = db
					.collection(ArangoDBGraph.GRAPH_VARIABLES_COLLECTION)
					.getDocument(graph.name(), ArangoDBGraphVariables.class);
		} catch (ArangoDBException e) {
			logger.error("Failed to retrieve vertex: {}", e.getErrorMessage());
			throw new ArangoDBGraphException("Failed to retrieve vertex.", e);
		}
		result.collection(result.label);
		return result;
	}

	/**
	 * Insert a ArangoDBBaseDocument in the graph. The document is updated with the id, rev and name
	 * (if not * present)
	 * @param document 				the document
	 * @throws ArangoDBGraphException 	If there was an error inserting the document
	 */

	public void insertGraphVariables(ArangoDBGraphVariables document) {
		logger.debug("Insert graph variables {} in {}", document, graph.name());
		if (document.isPaired()) {
			throw new ArangoDBGraphException("Paired docuements can not be inserted, only updated");
		}
		ArangoCollection gVars = db.collection(document.collection());
		if (!gVars.exists()) {
			CollectionEntity ce = db.createCollection(document.collection());
			System.out.println(ce.getStatus());
		}
		DocumentCreateEntity<ArangoDBGraphVariables> vertexEntity;
		try {
			vertexEntity = gVars.insertDocument(document);
		} catch (ArangoDBException e) {
			logger.error("Failed to insert document: {}", e.getMessage());
			ArangoDBGraphException arangoDBException = ArangoDBExceptions.getArangoDBException(e);
			if (arangoDBException.getErrorCode() == 1210) {
				throw Graph.Exceptions.vertexWithIdAlreadyExists(document._key);
			}
			throw arangoDBException;
		}
		document._id(vertexEntity.getId());
		document._rev(vertexEntity.getRev());
		if (document._key() == null) {
			document._key(vertexEntity.getKey());
		}
		document.setPaired(true);
	}

	/**
	 * Delete a document from the graph.
	 * @param document            	the document to delete
	 * @throws ArangoDBGraphException 	If there was an error deleting the document
	 */

	public void deleteGraphVariables(ArangoDBGraphVariables document) {
		logger.debug("Delete variables {} in {}", document, graph.name());
		try {
			db.collection(document.collection())
					.deleteDocument(document._key());
		} catch (ArangoDBException e) {
			logger.error("Failed to delete document: {}", e.getErrorMessage());
			throw ArangoDBExceptions.getArangoDBException(e);
		}
		document.setPaired(false);
	}

	/**
	 * Update the document in the graph.
	 * @param document 				the document
	 *
	 * @throws ArangoDBGraphException 	If there was an error updating the document
	 */

	public void updateGraphVariables(ArangoDBGraphVariables document) {
		logger.debug("Update variables {} in {}", document, graph.name());
		DocumentUpdateEntity updateEntity;
		try {
			updateEntity = db.collection(document.collection())
								.updateDocument(document._key(), document);
		} catch (ArangoDBException e) {
			logger.error("Failed to update document: {}", e.getErrorMessage());
			throw ArangoDBExceptions.getArangoDBException(e);
		}
		logger.info("Document updated, new rev {}", updateEntity.getRev());
		document._rev(updateEntity.getRev());
	}
	
	/**
	 * Create a query to get all the edges of a vertex. 
	 *
	 * @param vertex            	the vertex
	 * @param edgeLabels        	a list of edge labels to follow, empty if all type of edges
	 * @param direction         	the direction of the edges
	 * @return ArangoDBBaseQuery the query object
	 * @throws ArangoDBException if there is an error executing the query
	 */

	public ArangoCursor<ArangoDBEdge> getVertexEdges(
		ArangoDBVertex vertex,
		List<String> edgeLabels,
		Direction direction)
		throws ArangoDBException {
		logger.debug("Get Vertex's {}:{} Edges, in {}, from collections {}", vertex, direction, graph.name(), edgeLabels);
		Map<String, Object> bindVars = new HashMap<>();
		ArangoDBQueryBuilder queryBuilder = new ArangoDBQueryBuilder();
		ArangoDBQueryBuilder.Direction arangoDirection = ArangoDBUtil.getArangoDirectionFromGremlinDirection(direction);
		logger.debug("Creating query");
		queryBuilder.iterateGraph(graph.name(), "v", Optional.of("e"),
				Optional.empty(), Optional.empty(), Optional.empty(),
				arangoDirection, vertex._id(), bindVars)
			.graphOptions(Optional.of(UniqueVertices.NONE), Optional.empty(), true)
			.filterSameCollections("e", edgeLabels, bindVars)
			.ret("e");
		
		String query = queryBuilder.toString();
		return executeAqlQuery(query, bindVars, null, ArangoDBEdge.class);
	}
	
	/**
	 * Get all neighbours of a document.
	 *
	 * @param <T> 					the document type
	 * @param document              the document
	 * @param edgeLabelsFilter      a list of edge types to follow
	 * @param direction             a direction
	 * @param propertyFilter 		filter the neighbours on the given property:value values
	 * @param resultType 			the result type
	 * @return ArangoDBBaseQuery	the query object
	 */

	public <T> ArangoCursor<T> getDocumentNeighbors(
        ArangoDBBaseDocument document,
        List<String> edgeLabelsFilter,
        Direction direction,
        ArangoDBPropertyFilter propertyFilter,
        Class<T> resultType) {
		logger.debug("Get Document's {}:{} Neighbors, in {}, from collections {}", document, direction, graph.name(), edgeLabelsFilter);
		Map<String, Object> bindVars = new HashMap<>();
		ArangoDBQueryBuilder queryBuilder = new ArangoDBQueryBuilder();
		ArangoDBQueryBuilder.Direction arangoDirection = ArangoDBUtil.getArangoDirectionFromGremlinDirection(direction);
		logger.debug("Creating query");
		queryBuilder.iterateGraph(graph.name(), "v", Optional.of("e"),
				Optional.empty(), Optional.empty(), Optional.empty(),
				arangoDirection, document._id(), bindVars)
			.graphOptions(Optional.of(UniqueVertices.GLOBAL), Optional.empty(), true)
			.filterSameCollections("e", edgeLabelsFilter, bindVars)
			.filterProperties(propertyFilter, "v", bindVars)
			.ret("v");
		
		String query = queryBuilder.toString();
		return executeAqlQuery(query, bindVars, null, resultType);
	}

	/**
	 * Gets the element properties.
	 *
	 * @param <T> 					the generic type
	 * @param document              the document
	 * @param edgeLabelsFilter      a list of edge types to follow
	 * @param propertyFilter 		Filter the neighbours on the given property:value values
	 * @param propertyType 			the property type
	 * @return ArangoDBBaseQuery	the query object
	 */

	public <T> ArangoCursor<T> getElementProperties(
        ArangoDBBaseDocument document,
        List<String> edgeLabelsFilter,
        ArangoDBPropertyFilter propertyFilter,
        Class<T> propertyType) {
		logger.debug("Get Vertex's {}:{} Neighbors, in {}, from collections {}", document, graph.name(), edgeLabelsFilter);
		Map<String, Object> bindVars = new HashMap<>();
		ArangoDBQueryBuilder queryBuilder = new ArangoDBQueryBuilder();
		logger.debug("Creating query");
        queryBuilder.iterateGraph(graph.name(), "v", Optional.of("e"),
				Optional.empty(), Optional.empty(), Optional.empty(),
				ArangoDBQueryBuilder.Direction.OUT, document._id(), bindVars)
			.graphOptions(Optional.of(UniqueVertices.GLOBAL), Optional.empty(), true)
			.filterSameCollections("e", edgeLabelsFilter, bindVars)
			.filterProperties(propertyFilter, "v", bindVars)
			.ret("v");
		
		String query = queryBuilder.toString();
		logger.debug("AQL {}", query);
		return executeAqlQuery(query, bindVars, null, propertyType);
	}
	
	
	/**
	 * Get vertices of a graph. If no ids are provided, get all vertices.
	 *
	 * @param ids 					the ids to match
	 * @param collections 			the collections to search within
	 * @return ArangoDBBaseQuery 	the query object
	 */

	public ArangoCursor<ArangoDBVertex> getGraphVertices(
		final List<String> ids,
		final List<String> collections) {
		logger.debug("Get all {} graph vertices, filtered by ids: {}", graph.name(), ids);
		Map<String, Object> bindVars = new HashMap<>();
		ArangoDBQueryBuilder queryBuilder = new ArangoDBQueryBuilder();
        List<String> prefixedColNames = graph.vertexCollections().stream().map(graph::getPrefixedCollectioName).collect(Collectors.toList());
        if (ids.isEmpty()) {
			if (prefixedColNames.size() > 1) {
				queryBuilder.union(prefixedColNames, "v", bindVars);
			} else {
				queryBuilder.iterateCollection("v", prefixedColNames.get(0), bindVars);
			}
		}
		else {
			if (!collections.isEmpty()) {
                prefixedColNames = collections.stream().map(graph::getPrefixedCollectioName).collect(Collectors.toList());
			}
			queryBuilder.with(prefixedColNames, bindVars)
					.documentsById(ids, "v", bindVars);
			
		}
		queryBuilder.ret("v");
		String query = queryBuilder.toString();
		logger.debug("AQL {}", query);
		return executeAqlQuery(query, bindVars, null, ArangoDBVertex.class);
	}
	
	/**
	 * Get edges of a graph. If no ids are provided, get all edges.
	 *
	 * @param ids 					the ids to match
	 * @param collections 			the collections to search within
	 * @return ArangoDBBaseQuery	the query object
	 */
	
	public ArangoCursor<ArangoDBEdge> getGraphEdges(
		List<String> ids,
		List<String> collections) {
		logger.debug("Get all {} graph edges, filtered by ids: {}", graph.name(), ids);
		Map<String, Object> bindVars = new HashMap<>();
		ArangoDBQueryBuilder queryBuilder = new ArangoDBQueryBuilder();
        List<String> prefixedColNames = graph.edgeCollections().stream().map(graph::getPrefixedCollectioName).collect(Collectors.toList());
		if (ids.isEmpty()) {
			if (prefixedColNames.size() > 1) {
				queryBuilder.union(prefixedColNames, "e", bindVars);
			} else {
				queryBuilder.iterateCollection("e", prefixedColNames.get(0), bindVars);
			}
		}
		else {
            if (!collections.isEmpty()) {
                prefixedColNames = collections.stream().map(graph::getPrefixedCollectioName).collect(Collectors.toList());
            }
		    queryBuilder.with(prefixedColNames, bindVars)
				    .documentsById(ids, "e", bindVars);
			
		}
		queryBuilder.ret("e");
		String query = queryBuilder.toString();
		logger.debug("AQL {}", query);
		return executeAqlQuery(query, bindVars, null, ArangoDBEdge.class);
	}
	
	/**
	 * Gets the edge vertices.
	 *
	 * @param edgeId 				the edge id
	 * @param edgeCollection 		the edge collection
	 * @param from 					if true, get incoming vertex
	 * @param to 					if true, get outgoing edge
	 * @return the edge vertices
	 */

	public ArangoCursor<ArangoDBVertex> getEdgeVertices(
		String edgeId,
		String edgeCollection,
		boolean from, boolean to) {
		Map<String, Object> bindVars = new HashMap<>();
		ArangoDBQueryBuilder queryBuilder = new ArangoDBQueryBuilder();
		List<String> edgeCollections = new ArrayList<>();
		List<String> vertices = new ArrayList<>();
		edgeCollections.add(graph.getPrefixedCollectioName(edgeCollection));
		if (from) {
			vertices.add("_from");
		}
		if (to) {
			vertices.add("_to");
		}
		queryBuilder.with(edgeCollections, bindVars)
			.documentById(edgeId, "e", bindVars)
			.append("FOR v IN ")
			.append(vertices.stream().map(v->String.format("e.%s", v))
					.collect(Collectors.joining(",", "[", "]\n")))
			.ret("Document(v)");
		String query = queryBuilder.toString();
		logger.debug("AQL {}", query);
		return executeAqlQuery(query, bindVars, null, ArangoDBVertex.class);
	}
	
	/**
	 * Delete a graph from the db, and all its collections.
	 *
	 * @param name 					the name of the graph to delete
	 * @return true, if the graph was deleted
	 */
	
	public boolean deleteGraph(String name) {
		return deleteGraph(name, true);
	}
	
	/**
	 * Delete a graph from the db. If dropCollection is true, then all the graph collections are also 
	 * dropped
	 *
	 * @param name 				the name
	 * @param dropCollections 	true to drop the graph collections too
	 * @return true if the graph was deleted
	 */
	
	public boolean deleteGraph(String name, boolean dropCollections) {
		if (db != null) {
			ArangoGraph graph = db.graph(name);
			if (graph.exists()) {
				try {
					Collection<String> edgeDefinitions = dropCollections ? graph.getEdgeDefinitions() : Collections.emptyList();
					Collection<String> vertexCollections = dropCollections ? graph.getVertexCollections(): Collections.emptyList();
					// Drop graph first because it will break if the graph collections do not exist
					graph.drop();
					for (String definitionName : edgeDefinitions) {
						String collectioName = definitionName;
						if (db.collection(collectioName).exists()) {
							db.collection(collectioName).drop();
						}
					}
					for (String vc : vertexCollections) {
						String collectioName = vc;
						if (db.collection(collectioName).exists()) {
							db.collection(collectioName).drop();
						}
					}
					// Delete the graph variables
					db.collection(ArangoDBGraph.GRAPH_VARIABLES_COLLECTION).deleteDocument(name);
				} catch (ArangoDBException e) {
					System.out.println(e);
				}
				return true;
			} else {
				try {
					graph.drop();
				} catch (ArangoDBException e) {
                    //throw ArangoDBExceptions.getArangoDBException(e);
				}
			}
		}
		return false;
	}

	/**
	 * Delete collection.
	 *
	 * @param name the name
	 * @return true, if successful
	 */
	
	public boolean deleteCollection(String name) {
		ArangoCollection collection = db.collection(name);
		if (collection.exists()) {
			collection.drop();
			return collection.exists();
		}
		return false;
	}
	
	/**
	 * Create a new graph.
	 *
	 * @param name            			the name of the new graph
	 * @param edgeDefinitions 			the edge definitions for the graph
	 * @throws ArangoDBGraphException 	If the graph can not be created
	 */
	
	public void createGraph(String name, List<EdgeDefinition> edgeDefinitions)
		throws ArangoDBGraphException {
		this.createGraph(name, edgeDefinitions, null);
	}
	
	/**
	 * Create a new graph.
	 *
	 * @param name 						the name of the new graph
	 * @param edgeDefinitions			the edge definitions for the graph
	 * @param options 					additional graph options
	 * @return the arango graph
	 * @throws ArangoDBGraphException 	If the graph can not be created
	 */
	
	public ArangoGraph createGraph(String name,
        List<EdgeDefinition> edgeDefinitions,
		GraphCreateOptions options)
		throws ArangoDBGraphException {
		logger.info("Creating graph {}", name);
		try {
			logger.info("Creating graph in database.");
			db.createGraph(name, edgeDefinitions, options);
		} catch (ArangoDBException e) {
            logger.info("Error creating graph in database.", e);
            throw ArangoDBExceptions.getArangoDBException(e);
        }
		ArangoGraph g = db.graph(name);
		return g;
	}


	/**
	 * Get the ArangoGraph that is linked to the client's graph
	 *
	 * @return the graph or null if the graph was not found
	 */
	
	public ArangoGraph getArangoGraph() {
		return db.graph(graph.name());
	}
	
	/**
	 * Execute AQL query.
	 *
	 * @param <T> 						the generic type of the returned values
	 * @param query 					the query string
	 * @param bindVars 					the value of the bind parameters
	 * @param aqlQueryOptions 			the aql query options
	 * @param type            			the type of the result (POJO class, VPackSlice, String for Json, or Collection/List/Map)
	 * @return the cursor result
	 * @throws ArangoDBGraphException	if executing the query raised an exception
	 */

	public <T> ArangoCursor<T> executeAqlQuery(
		String query,
		Map<String, Object> bindVars,
		AqlQueryOptions aqlQueryOptions,
		final Class<T> type)
		throws ArangoDBGraphException {
		logger.debug("Executing AQL query ({}) against db, with bind vars: {}", query, bindVars);
		try {
			return db.query(query, bindVars, aqlQueryOptions, type);
		} catch (ArangoDBException e) {
			logger.error("Error executing query", e);
            throw ArangoDBExceptions.getArangoDBException(e);
		}
	}

    /**
     * Insert a document into a specific collection.
     * @param document              the document to insert
     * @param colName               the collection
     */

    private void insertDocument(ArangoDBBaseDocument document, String colName) {
        VertexEntity vertexEntity;
        if (document.isPaired()) {
            throw new ArangoDBGraphException("Paired docuemnts can not be inserted, only updated");
        }
        try {
            vertexEntity = db.graph(graph.name())
                    .vertexCollection(colName)
                    .insertVertex(document);
        } catch (ArangoDBException e) {
            logger.error("Failed to insert document: {}", e.getMessage());
            ArangoDBGraphException arangoDBException = ArangoDBExceptions.getArangoDBException(e);
            if (arangoDBException.getErrorCode() == 1210) {
                throw Graph.Exceptions.vertexWithIdAlreadyExists(document._key);
            }
            throw arangoDBException;
        }
        document._id(vertexEntity.getId());
        document._rev(vertexEntity.getRev());
        if (document._key() == null) {
            document._key(vertexEntity.getKey());
        }
        document.setPaired(true);
    }

    // TODO Decide what of these methods should be restored.
//	
//	/**
//	 * Creates vertices (bulk import).
//	 *
//	 * @param graph            The graph
//	 * @param vertices            The list of new vertices
//	 * @param details            True, for details
//	 * @return a ImportResultEntity object
//	 * @throws ArangoDBException             if an error occurs
//	 */
//	public ImportResultEntity createVertices(
//		ArangoDBSimpleGraph graph,
//		List<ArangoDBSimpleVertex> vertices,
//		boolean details) throws ArangoDBException {
//
//		List<Map<String, Object>> values = new ArrayList<Map<String, Object>>();
//
//		for (ArangoDBSimpleVertex v : vertices) {
//			values.add(v.getProperties());
//		}
//
//		try {
//			return driver.importDocuments(graph.getVertexCollection(), true, values);
//		} catch (ArangoException e) {
//			throw new ArangoDBException(e);
//		}
//	}
//
//	/**
//	 * Creates edges (bulk import).
//	 *
//	 * @param graph            The graph
//	 * @param edges            The list of new edges
//	 * @param details            True, for details
//	 * @return a ImportResultEntity object
//	 * @throws ArangoDBException             if an error occurs
//	 */
//	public ImportResultEntity createEdges(ArangoDBSimpleGraph graph, List<ArangoDBSimpleEdge> edges, boolean details)
//			throws ArangoDBException {
//
//		List<Map<String, Object>> values = new ArrayList<Map<String, Object>>();
//
//		for (ArangoDBSimpleEdge e : edges) {
//			values.add(e.getProperties());
//		}
//
//		try {
//			return driver.importDocuments(graph.getEdgeCollection(), true, values);
//		} catch (ArangoException e) {
//			throw new ArangoDBException(e);
//		}
//	}

//	/**
//	 * Create an index on collection keys.
//	 *
//	 * @param graph            the simple graph
//	 * @param type            the index type ("cap", "geo", "hash", "skiplist")
//	 * @param unique            true for a unique name
//	 * @param fields            a list of name fields
//	 * @return ArangoDBIndex the index
//	 * @throws ArangoDBException             if creation failed
//	 */
//
//	public ArangoDBIndex createVertexIndex(
//		ArangoDBSimpleGraph graph,
//		IndexType type,
//		boolean unique,
//		List<String> fields) throws ArangoDBException {
//		return createIndex(graph.getVertexCollection(), type, unique, fields);
//	}
//
//	/**
//	 * Create an index on collection keys.
//	 *
//	 * @param graph            the simple graph
//	 * @param type            the index type ("cap", "geo", "hash", "skiplist")
//	 * @param unique            true for a unique name
//	 * @param fields            a list of name fields
//	 * @return ArangoDBIndex the index
//	 * @throws ArangoDBException             if creation failed
//	 */
//
//	public ArangoDBIndex createEdgeIndex(ArangoDBSimpleGraph graph, IndexType type, boolean unique, List<String> fields)
//			throws ArangoDBException {
//		return createIndex(graph.getEdgeCollection(), type, unique, fields);
//	}
//
//	/**
//	 * Get an index.
//	 *
//	 * @param id            id of the index
//	 * @return ArangoDBIndex the index, or null if the index was not found
//	 * @throws ArangoDBException             if creation failed
//	 */
//
//	public ArangoDBIndex getIndex(String id) throws ArangoDBException {
//		IndexEntity index;
//		try {
//			index = driver.getIndex(id);
//		} catch (ArangoException e) {
//
//			if (e.getErrorNumber() == ErrorNums.ERROR_ARANGO_INDEX_NOT_FOUND) {
//				return null;
//			}
//
//			throw new ArangoDBException(e);
//		}
//		return new ArangoDBIndex(index);
//	}
//
//	/**
//	 * Returns the indices of the vertex collection.
//	 *
//	 * @param graph            The graph
//	 * @return List of indices
//	 * @throws ArangoDBException             if an error occurs
//	 */
//	public List<ArangoDBIndex> getVertexIndices(ArangoDBSimpleGraph graph) throws ArangoDBException {
//		return getIndices(graph.getVertexCollection());
//	}
//
//	/**
//	 * Returns the indices of the edge collection.
//	 *
//	 * @param graph            The graph
//	 * @return List of indices
//	 * @throws ArangoDBException             if an error occurs
//	 */
//	public List<ArangoDBIndex> getEdgeIndices(ArangoDBSimpleGraph graph) throws ArangoDBException {
//		return getIndices(graph.getEdgeCollection());
//	}
//
//	/**
//	 * Deletes an index.
//	 *
//	 * @param id            The identifier of the index
//	 * @return true, if the index was deleted
//	 * @throws ArangoDBException             if an error occurs
//	 */
//	public boolean deleteIndex(String id) throws ArangoDBException {
//		try {
//			driver.deleteIndex(id);
//		} catch (ArangoException e) {
//			throw new ArangoDBException(e);
//		}
//
//		return true;
//	}
//
//	/**
//	 * Create an index on collection keys.
//	 *
//	 * @param collectionName            the collection name
//	 * @param type            the index type ("cap", "geo", "hash", "skiplist")
//	 * @param unique            true for a unique name
//	 * @param fields            a list of name fields
//	 * @return ArangoDBIndex the index
//	 * @throws ArangoDBException             if creation failed
//	 */
//
//	private ArangoDBIndex createIndex(String collectionName, IndexType type, boolean unique, List<String> fields)
//			throws ArangoDBException {
//
//		IndexEntity indexEntity;
//		try {
//			indexEntity = driver.createIndex(collectionName, type, unique, fields.toArray(new String[0]));
//		} catch (ArangoException e) {
//			throw new ArangoDBException(e);
//		}
//
//		return new ArangoDBIndex(indexEntity);
//	}
//
//	/**
//	 * Get the List of indices of a collection.
//	 *
//	 * @param collectionName            the collection name
//	 * @return Vector<ArangoDBIndex> List of indices
//	 * @throws ArangoDBException             if creation failed
//	 */
//
//	private List<ArangoDBIndex> getIndices(String collectionName) throws ArangoDBException {
//		List<ArangoDBIndex> indices = new ArrayList<ArangoDBIndex>();
//
//		IndexesEntity indexes;
//		try {
//			indexes = driver.getIndexes(collectionName);
//		} catch (ArangoException e) {
//			throw new ArangoDBException(e);
//		}
//
//		for (IndexEntity indexEntity : indexes.getIndexes()) {
//			indices.add(new ArangoDBIndex(indexEntity));
//		}
//
//		return indices;
//	}
//
//	/**
//	 * Returns the current connection configuration.
//	 *
//	 * @param collectionName the collection name
//	 * @return the configuration
//	 * @throws ArangoDBException the arango DB exception
//	 */
////	public ArangoDBConfiguration getConfiguration() {
////		return configuration;
////	}
//
//	/**
//	 * Truncates a collection
//	 * 
//	 * @param collectionName
//	 */
//	public void truncateCollection(String collectionName) throws ArangoDBException {
//		try {
//			driver.truncateCollection(collectionName);
//		} catch (ArangoException e) {
//			throw new ArangoDBException(e);
//		}
//	}

}