/* Copyright 2014 The Johns Hopkins University Applied Physics Laboratory
 *
 * Licensed 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 edu.jhuapl.tinkerpop;

import java.util.Collections;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedSet;

import org.apache.accumulo.core.client.AccumuloException;
import org.apache.accumulo.core.client.BatchDeleter;
import org.apache.accumulo.core.client.MutationsRejectedException;
import org.apache.accumulo.core.client.admin.TableOperations;
import org.apache.accumulo.core.data.Range;
import org.apache.commons.configuration.Configuration;
import org.apache.hadoop.io.Text;

import com.tinkerpop.blueprints.Edge;
import com.tinkerpop.blueprints.Element;
import com.tinkerpop.blueprints.Features;
import com.tinkerpop.blueprints.Graph;
import com.tinkerpop.blueprints.GraphFactory;
import com.tinkerpop.blueprints.GraphQuery;
import com.tinkerpop.blueprints.Index;
import com.tinkerpop.blueprints.IndexableGraph;
import com.tinkerpop.blueprints.KeyIndexableGraph;
import com.tinkerpop.blueprints.Parameter;
import com.tinkerpop.blueprints.Vertex;
import com.tinkerpop.blueprints.util.DefaultGraphQuery;
import com.tinkerpop.blueprints.util.ExceptionFactory;

import edu.jhuapl.tinkerpop.cache.ElementCaches;

/**
 * This is an implementation of the TinkerPop Blueprints 2.6 API using
 * Apache Accumulo as the backend. This combines the many benefits and flexibility
 * of Blueprints with the scalability and performance of Accumulo.
 * 
 * <p/>In addition to the basic Blueprints functionality, we provide a number of
 * enhanced features, including:
 * <ol>
 * <li>Indexing implementations via IndexableGraph and KeyIndexableGraph</li>
 * <li>Support for mock, mini, and distributed instances of Accumulo</li>
 * <li>Numerous performance tweaks and configuration parameters</li>
 * <li>Support for high speed ingest</li>
 * <li>Hadoop integration</li>
 * </ol>
 */
public class AccumuloGraph implements Graph, KeyIndexableGraph, IndexableGraph {

  private final GlobalInstances globals;

  /**
   * Factory method for {@link GraphFactory}.
   */
  public static AccumuloGraph open(Configuration properties) throws AccumuloException {
    return new AccumuloGraph(properties);
  }

  /**
   * Instantiate from a generic {@link Configuration} populated
   * with appropriate AccumuloGraph parameters.
   * @param cfg
   */
  public AccumuloGraph(Configuration cfg) {
    this(new AccumuloGraphConfiguration(cfg));
  }

  /**
   * Main constructor.
   * @param config
   */
  public AccumuloGraph(AccumuloGraphConfiguration config) {
    config.validate();

    AccumuloGraphUtils.handleCreateAndClear(config);

    try {
      globals = new GlobalInstances(config, config.getConnector()
          .createMultiTableBatchWriter(config.getBatchWriterConfig()),
          new ElementCaches(config));
    } catch (Exception e) {
      throw new AccumuloGraphException(e);
    }
  }

  /**
   * This is at package level for the {@link AutoIndexTest} unit test
   * and probably should disappear.
   * @return
   */
  GlobalInstances getGlobals() {
    return globals;
  }

  @Override
  public Features getFeatures() {
    return AccumuloFeatures.get();
  }

  @Override
  public Vertex addVertex(Object id) {
    if (id == null) {
      id = AccumuloGraphUtils.generateId();
    }

    String idStr = id.toString();

    Vertex vert = null;
    if (!globals.getConfig().getSkipExistenceChecks()) {
      vert = getVertex(idStr);
      if (vert != null) {
        throw ExceptionFactory.vertexWithIdAlreadyExists(idStr);
      }
    }

    vert = new AccumuloVertex(globals, idStr);

    globals.getVertexWrapper().writeVertex(vert);
    globals.checkedFlush();

    globals.getCaches().cache(vert, Vertex.class);

    return vert;
  }
  
  /**
   * 
   * @return an immutable copy of the configuration running this graph
   */
  public Configuration getConfiguration(){
    return globals.getConfig().getConfiguration();
  }
  
  /**
   * Flushes the backing writers so the data is persisted.
   * @throws MutationsRejectedException
   */
  public void flush() throws MutationsRejectedException{
    globals.getMtbw().flush();
  }
  

  @Override
  public Vertex getVertex(Object id) {
    if (id == null) {
      throw ExceptionFactory.vertexIdCanNotBeNull();
    }
    String myID = id.toString();

    Vertex vertex = globals.getCaches().retrieve(myID, Vertex.class);
    if (vertex != null) {
      return vertex;
    }

    vertex = new AccumuloVertex(globals, myID);
    if (!globals.getConfig().getSkipExistenceChecks()) {
      // In addition to just an "existence" check, we will also load
      // any "preloaded" properties now, which saves us a round-trip
      // to Accumulo later.
      String[] preload = globals.getConfig().getPreloadedProperties();
      if (preload == null && !globals.getConfig().getPreloadAllProperties()) {
        preload = new String[]{};
      }

      Map<String, Object> props = globals.getVertexWrapper()
          .readProperties(vertex, preload);
      if (props == null) {
        return null;
      }

      for (Entry<String, Object> ents : props.entrySet()) {
        ((AccumuloElement) vertex).setPropertyInMemory(ents.getKey(), ents.getValue());
      }
    }

    globals.getCaches().cache(vertex, Vertex.class);

    return vertex;
  }

  @Override
  public void removeVertex(Vertex vertex) {
    vertex.remove();
  }

  @Override
  public Iterable<Vertex> getVertices() {
    return globals.getVertexWrapper().getVertices();
  }

  /**
   * Retrieve vertices with ids within the given range,
   * inclusive. The range is calculated using the string
   * representations of the given ids. If fromId or
   * toId is null, use negative infinity and positive
   * infinity, respectively.
   * <p/>Note: This does not use indexes.
   * @param fromId
   * @param toId
   * @return
   */
  public Iterable<Vertex> getVerticesInRange(Object fromId, Object toId) {
    return globals.getVertexWrapper().getVerticesInRange(fromId, toId);
  }

  @Override
  public Iterable<Vertex> getVertices(String key, Object value) {
    AccumuloGraphUtils.validateProperty(key, value);
    if (globals.getConfig().getAutoIndex() || getIndexedKeys(Vertex.class).contains(key)) {
      return globals.getVertexKeyIndexWrapper().getVertices(key, value);
    } else {
      return globals.getVertexWrapper().getVertices(key, value);
    }
  }

  @Override
  public Edge addEdge(Object id, Vertex outVertex, Vertex inVertex, String label) {
    return ((AccumuloVertex) outVertex).addEdge(id, label, inVertex);
  }

  @Override
  public Edge getEdge(Object id) {
    if (id == null) {
      throw ExceptionFactory.edgeIdCanNotBeNull();
    }
    String idStr = id.toString();

    Edge edge = globals.getCaches().retrieve(idStr, Edge.class);
    if (edge != null) {
      return edge;
    }

    edge = new AccumuloEdge(globals, idStr);

    if (!globals.getConfig().getSkipExistenceChecks()) {
      // In addition to just an "existence" check, we will also load
      // any "preloaded" properties now, which saves us a round-trip
      // to Accumulo later.
      String[] preload = globals.getConfig().getPreloadedProperties();
      if (preload == null) {
        preload = new String[]{};
      }

      Map<String, Object> props = globals.getEdgeWrapper()
          .readProperties(edge, preload);
      // This will be null if the element does not exist,
      // in which case return null.
      if (props == null) {
        return null;
      }

      for (Entry<String, Object> ents : props.entrySet()) {
        ((AccumuloElement) edge).setPropertyInMemory(ents.getKey(), ents.getValue());
      }
    }

    globals.getCaches().cache(edge, Edge.class);

    return edge;
  }

  @Override
  public void removeEdge(Edge edge) {
    edge.remove();
  }

  @Override
  public Iterable<Edge> getEdges() {
    return globals.getEdgeWrapper().getEdges();
  }

  @Override
  public Iterable<Edge> getEdges(String key, Object value) {
    AccumuloGraphUtils.nullCheckProperty(key, value);
    if (key.equalsIgnoreCase("label")) {
      key = Constants.LABEL;
    }

    if (globals.getConfig().getAutoIndex() || getIndexedKeys(Edge.class).contains(key)) {
      return globals.getEdgeKeyIndexWrapper().getEdges(key, value);
    } else {
      return globals.getEdgeWrapper().getEdges(key, value);
    }
  }

  // TODO Eventually
  @Override
  public GraphQuery query() {
    return new DefaultGraphQuery(this);
  }

  @Override
  public void shutdown() {
    try {
      globals.getMtbw().close();
      globals.getVertexWrapper().close();
      globals.getEdgeWrapper().close();
    } catch (MutationsRejectedException e) {
      throw new AccumuloGraphException(e);
    }
    globals.getCaches().clear(Vertex.class);
    globals.getCaches().clear(Edge.class);
  }

  @Override
  public String toString() {
    return AccumuloGraphConfiguration.ACCUMULO_GRAPH_CLASS.getSimpleName().toLowerCase();
  }

  @SuppressWarnings("rawtypes")
  @Override
  public <T extends Element> Index<T> createIndex(String indexName,
      Class<T> indexClass, Parameter... indexParameters) {
    if (indexClass == null) {
      throw ExceptionFactory.classForElementCannotBeNull();
    }
    else if (globals.getConfig().getIndexableGraphDisabled()) {
      throw new UnsupportedOperationException("IndexableGraph is disabled via the configuration");
    }

    for (Index<?> index : globals.getIndexMetadataWrapper().getIndices()) {
      if (index.getIndexName().equals(indexName)) {
        throw ExceptionFactory.indexAlreadyExists(indexName);
      }
    }

    return globals.getIndexMetadataWrapper().createIndex(indexName, indexClass);
  }

  @Override
  public <T extends Element> Index<T> getIndex(String indexName, Class<T> indexClass) {
    if (indexClass == null) {
      throw ExceptionFactory.classForElementCannotBeNull();
    }
    else if (globals.getConfig().getIndexableGraphDisabled()) {
      throw new UnsupportedOperationException("IndexableGraph is disabled via the configuration");
    }

    return globals.getIndexMetadataWrapper().getIndex(indexName, indexClass);
  }

  @Override
  public Iterable<Index<? extends Element>> getIndices() {
    if (globals.getConfig().getIndexableGraphDisabled()) {
      throw new UnsupportedOperationException("IndexableGraph is disabled via the configuration");
    }
    return globals.getIndexMetadataWrapper().getIndices();
  }

  @Override
  public void dropIndex(String indexName) {
    if (globals.getConfig().getIndexableGraphDisabled())
      throw new UnsupportedOperationException("IndexableGraph is disabled via the configuration");

    for (Index<? extends Element> index : getIndices()) {
      if (index.getIndexName().equals(indexName)) {
        globals.getIndexMetadataWrapper().clearIndexNameEntry(indexName, index.getIndexClass());

        try {
          globals.getConfig().getConnector().tableOperations().delete(globals.getConfig()
              .getNamedIndexTableName(indexName));
        } catch (Exception e) {
          throw new AccumuloGraphException(e);
        }

        return;
      }
    }

    throw new AccumuloGraphException("Index does not exist: "+indexName);
  }

  @Override
  public <T extends Element> void dropKeyIndex(String key, Class<T> elementClass) {
    // TODO Move below to somewhere appropriate.
    if (elementClass == null) {
      throw ExceptionFactory.classForElementCannotBeNull();
    }

    globals.getIndexMetadataWrapper().clearKeyMetadataEntry(key, elementClass);

    String table = null;
    if (elementClass.equals(Vertex.class)) {
      table = globals.getConfig().getVertexKeyIndexTableName();
    } else {
      table = globals.getConfig().getEdgeKeyIndexTableName();
    }
    BatchDeleter bd = null;
    try {
      bd = globals.getConfig().getConnector().createBatchDeleter(table, globals.getConfig().getAuthorizations(), globals.getConfig().getMaxWriteThreads(), globals.getConfig().getBatchWriterConfig());
      bd.setRanges(Collections.singleton(new Range()));
      bd.fetchColumnFamily(new Text(key));
      bd.delete();
    } catch (Exception e) {
      throw new AccumuloGraphException(e);
    } finally {
      if (bd != null)
        bd.close();
    }
    globals.checkedFlush();
  }

  @SuppressWarnings("rawtypes")
  @Override
  public <T extends Element> void createKeyIndex(String key,
      Class<T> elementClass, Parameter... indexParameters) {
    // TODO Move below to somewhere appropriate.
    if (elementClass == null) {
      throw ExceptionFactory.classForElementCannotBeNull();
    }

    // Add key to indexed keys list.
    globals.getIndexMetadataWrapper().writeKeyMetadataEntry(key, elementClass);
    globals.checkedFlush();

    // Reindex graph.
    globals.getKeyIndexTableWrapper(elementClass).rebuildIndex(key, elementClass);
    globals.getVertexKeyIndexWrapper().dump();
    globals.checkedFlush();
  }

  @Override
  public <T extends Element> Set<String> getIndexedKeys(Class<T> elementClass) {
    return globals.getIndexMetadataWrapper().getIndexedKeys(elementClass);
  }

  /**
   * Clear out this graph. This drops and recreates the backing tables.
   */
  public void clear() {
    shutdown();

    try {
      TableOperations tableOps = globals.getConfig()
          .getConnector().tableOperations();
      for (Index<? extends Element> index : getIndices()) {
        tableOps.delete(((AccumuloIndex<? extends Element>)
            index).getTableName());
      }

      for (String table : globals.getConfig().getTableNames()) {
        if (tableOps.exists(table)) {
          tableOps.delete(table);
          tableOps.create(table);

          SortedSet<Text> splits = globals.getConfig().getSplits();
          if (splits != null) {
            tableOps.addSplits(table, splits);
          }
        }
      }
    } catch (Exception e) {
      throw new AccumuloGraphException(e);
    }
  }

  public boolean isEmpty() {
    try {
      TableOperations tableOps = globals.getConfig().getConnector().tableOperations();
      for (String table : globals.getConfig().getTableNames()) {
        if (tableOps.getMaxRow(table, globals.getConfig().getAuthorizations(),
            null, true, null, true) != null) {
          return false;
        }
      }
      return true;

    } catch (Exception e) {
      throw new AccumuloGraphException(e);
    }
  }
}