/**
 * Copyright (c) 2012-2014 "Indexia Technologies, ltd."
 *
 * This file is part of Antiquity.
 *
 * Antiquity is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package co.indexia.antiquity.graph;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import com.google.common.base.Preconditions;
import com.google.common.collect.Ordering;
import com.tinkerpop.blueprints.Direction;
import com.tinkerpop.blueprints.Edge;
import com.tinkerpop.blueprints.Element;
import com.tinkerpop.blueprints.GraphQuery;
import com.tinkerpop.blueprints.KeyIndexableGraph;
import com.tinkerpop.blueprints.Parameter;
import com.tinkerpop.blueprints.Vertex;
import com.tinkerpop.blueprints.util.wrappers.WrapperGraph;
import com.tinkerpop.blueprints.util.wrappers.readonly.ReadOnlyGraph;
import com.tinkerpop.blueprints.util.wrappers.readonly.ReadOnlyTokens;
import co.indexia.antiquity.graph.identifierBehavior.GraphIdentifierBehavior;
import co.indexia.antiquity.range.Range;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * {@link com.tinkerpop.blueprints.Graph} wrapper to query for historical
 * elements.
 * 
 * <p>
 * This class must be kept immutable.
 * </p>
 */
public class HistoricVersionedGraph<T extends KeyIndexableGraph, V extends Comparable<V>> extends
        VersionedGraphBase<T, V> implements WrapperGraph<T> {
    Logger log = LoggerFactory.getLogger(HistoricVersionedGraph.class);

    private final ReadOnlyGraph<T> baseGraph;

    public HistoricVersionedGraph(T baseGraph, GraphIdentifierBehavior<V> identifierBehavior, Configuration conf) {
        super(baseGraph, identifierBehavior, conf);
        this.baseGraph = new ReadOnlyGraph(baseGraph);
        validate();
    }

    @Override
    protected void validate() {
        // TODO super.validate();
    }

    @Override
    public T getBaseGraph() {
        return baseGraph.getBaseGraph();
    }

    @Override
    public Vertex getRootVertex() {
        return getRootVertex(VEProps.GRAPH_TYPE.HISTORIC);
    }

    // Graph operation overrides
    // --------------------------------------------------------------

    // Get a vertex by the active id, the latest revision is returned.
    @Override
    public Vertex getVertex(Object id) {
        Preconditions.checkNotNull(id, "id must be set.");

        List<HistoricVersionedVertex<V>> chain = buildVertexChain(id);

        if (chain.size() == 0) {
            log.debug("Vertex with [{}] was not found.", id);
            return null;
        } else {
            return chain.get(0);
        }
    }

    public HistoricVersionedVertex<V> getVertexByHardId(Object id) {
        Preconditions.checkNotNull(id, "id must be set.");

        Vertex vertex;
        if (isNaturalIds()) {
            vertex = getSingleVertex(VEProps.NATURAL_VERTEX_ID_PROP_KEY, id);
        } else {
            vertex = getBaseGraph().getVertex(id);
        }

        if (vertex != null) {
            utils.ensureHistoricType(vertex);

            if (vertex instanceof HistoricVersionedVertex) {
                return (HistoricVersionedVertex<V>) vertex;
            } else {
                return new HistoricVersionedVertex<V>(vertex, this, Range.range(utils.getStartVersion(vertex),
                        utils.getStartVersion(vertex)));
            }
        } else {
            log.debug("Vertex with [{}] was not found.", id);
            return null;
        }
    }

    @Override
    public Iterable<Vertex> getVertices() {
        return getVertices(null, null);
    }

    @Override
    public Iterable<Vertex> getVertices(String key, Object value) {
        // NOTE: This methods returns *ALL* vertices (including all revisions)
        // in the entire graph.
        if (key != null) {
            return query().has(key, value).vertices();
        } else {
            return query().vertices();
        }

    }

    // Get a edge by the active id, if multiple revisions found, the latest is
    // returned.
    @Override
    public Edge getEdge(Object id) {
        Preconditions.checkNotNull(id, "id must be set.");

        List<HistoricVersionedEdge<V>> chain = buildEdgeChain(id);

        if (chain.size() == 0) {
            log.debug("Edge with [{}] was not found.", id);
            return null;
        } else {
            return chain.get(0);
        }
    }

    public HistoricVersionedEdge<V> getEdgeByHardId(Object id) {
        Preconditions.checkNotNull(id, "id must be set.");

        Edge edge;
        if (isNaturalIds()) {
            edge = getSingleEdge(VEProps.NATURAL_EDGE_ID_PROP_KEY, id);
        } else {
            edge = getBaseGraph().getEdge(id);
        }

        if (edge != null) {
            if (edge instanceof HistoricVersionedEdge) {
                return (HistoricVersionedEdge<V>) edge;
            } else {
                utils.ensureHistoricType(edge);
                V start = utils.getStartVersion(edge);
                return new HistoricVersionedEdge<V>(edge, this, Range.range(start, start));
            }
        } else {
            log.debug("Edge with [{}] was not found.", id);
            return null;
        }
    }

    @Override
    public Iterable<Edge> getEdges() {
        return query().edges();
    }

    @Override
    public Iterable<Edge> getEdges(String key, Object value) {
        // NOTE: This methods returns *ALL* edges in the graph without internal
        // elements.
        return ((HistoricGraphQuery<V>) query()).withInternals(false).has(key, value).edges();
    }

    @Override
    public GraphQuery query() {
        return new HistoricGraphQuery(this, this.baseGraph.getBaseGraph().query());
    }

    @Override
    public <T extends Element> Set<String> getIndexedKeys(Class<T> elementClass) {
        throw new UnsupportedOperationException(ReadOnlyTokens.MUTATE_ERROR_MESSAGE);
    }

    // ---- write protected methods
    @Override
    public Vertex addVertex(Object id) {
        return baseGraph.addVertex(id);
    }

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

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

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

    @Override
    public <T extends Element> void dropKeyIndex(String key, Class<T> elementClass) {
        throw new UnsupportedOperationException(ReadOnlyTokens.MUTATE_ERROR_MESSAGE);
    }

    @Override
    public <T extends Element> void createKeyIndex(String key, Class<T> elementClass, Parameter... indexParameters) {
        throw new UnsupportedOperationException(ReadOnlyTokens.MUTATE_ERROR_MESSAGE);
    }

    // Enhanced methods for the standard blueprint graph API
    // ------------------------------------------------------

    /**
     * Get a vertex revision from the specified active vertex.
     * 
     * @param active The active vertex to find the matched historic version for
     * @param version The version to find
     * @return a historic vertex revision for the specified version
     */
    public HistoricVersionedVertex<V> getVertexForVersion(ActiveVersionedVertex<V> active, V version) {
        HistoricVersionedVertex<V> latest = getLatestHistoricRevision(active);
        latest.setVersion(Range.range(version, version));

        return getMatchedHistoricVersion(latest, version);
    }

    /**
     * Get a vertex revision from the specified active vertex.
     * 
     * @param activeId The active vertex to find the matched historic version
     *        for
     * @param version The version to find
     * @return a historic vertex revision for the specified version
     */
    public HistoricVersionedVertex<V> getVertexForVersion(Object activeId, V version) {
        HistoricVersionedVertex<V> latest = (HistoricVersionedVertex<V>) getVertex(activeId);
        latest.setVersion(Range.range(version, version));

        return getMatchedHistoricVersion(latest, version);
    }


    public HistoricVersionedEdge<V> getEdgeForVersion(Object activeId, V version) {
        HistoricVersionedEdge<V> latest = (HistoricVersionedEdge<V>) getEdge(activeId);
        latest.setVersion(Range.range(version, version));
        Range<V> verRange = utils.getVersionRange(latest);

        if (verRange.contains(version)) {
            return latest;
        }

        return null;
    }

    public HistoricVersionedEdge<V> getEdgeForVersion(ActiveVersionedEdge<V> active, V version) {
        HistoricVersionedEdge<V> edge = getLatestHistoricRevision(active);
        edge.setVersion(Range.range(version, version));
        Range<V> verRange = utils.getVersionRange(edge);

        if (verRange.contains(version)) {
            return edge;
        }

        return null;
    }

    public HistoricVersionedVertex<V> getMatchedHistoricVersion(HistoricVersionedVertex later, V version) {
        Range<V> verRange = utils.getVersionRange(later);

        log.trace("Finding vertex[{}] in revision history for version [{}].", later, version);
        log.trace("Is vertex [{}] with range [{}] contains version [{}]?", later, verRange, version);

        if (!verRange.contains(version)) {
            Iterable<Edge> prevVerEdges = (later.getBaseElement()).getEdges(Direction.OUT, VEProps.PREV_VERSION_LABEL);

            if (!prevVerEdges.iterator().hasNext()) {
                // throw ExceptionFactory.notFoundException(String.format(
                // "Cannot find vertex %s in revision history for version [%s].",
                // later, version));
                log.info("Cannot find vertex %s in revision history for version [%s].", later, version);
                return null;
            }

            Edge rawEdge = prevVerEdges.iterator().next();
            Vertex rawVertex = rawEdge.getVertex(Direction.IN);
            HistoricVersionedVertex<V> nextLater =
                    new HistoricVersionedVertex<V>(rawVertex, this, Range.range(version, version));
            return getMatchedHistoricVersion((HistoricVersionedVertex<V>) nextLater, version);
        }

        log.debug("Found vertex[{}] in revision history for version [{}].", later, version);
        return later;
    }

    /**
     * Get all vertices for the specified version.
     * 
     * @param version The version to get all vertices for.
     * @return An {@link Iterable} of the found vertices for the specified
     *         version.
     */
    public HistoricVersionedVertexIterable<V> getVertices(V version) {
        Preconditions.checkNotNull(version, "Version must be specified.");

        return new HistoricVersionedVertexIterable<V>(getVertices(), this, Range.range(version, version));
    }

    /**
     * Return an iterable to all the vertices in the graph that have a
     * particular key/value property for the specified version.
     * 
     * @param key The key of the property to filter vertices by
     * @param value The value of the property to filter vertices by
     * @param version version The version to get the vertices for
     * @return An {@link Iterable} of the found vertices for the specified
     *         criteria.
     */
    public Iterable<Vertex> getVertices(final String key, final Object value, V version) {
        Preconditions.checkNotNull(key, "Key is required.");
        Preconditions.checkNotNull(version, "Version is required.");

        // TODO: Consider forbidding retrieving edges by internal keys
        // (especially NATURAL_VERTEX_ID_PROP_KEY), otherwise throw exception.
        return new HistoricVersionedVertexIterable<V>(getVertices(key, value), this, Range.range(version, version));
    }

    /**
     * Get a single historic vertex by the specified key / value criteria.
     * 
     * This method is not very useful as typically multiple elements will answer
     * the same criteria.
     * 
     * @param key key to match
     * @param value value to match
     * @return a single found element or exception if multiple elements found.
     */
    public Vertex getSingleVertex(String key, Object value) {
        return ElementUtils.getSingleElement(this, key, value, Vertex.class);
    }

    /**
     * Get a single historic edge by the specified key / value criteria.
     * 
     * This method is not very useful as typically multiple elements will answer
     * the same criteria.
     * 
     * @param key key to match
     * @param value value to match
     * @return a single found element or exception if multiple elements found.
     */
    public Edge getSingleEdge(String key, Object value) {
        return ElementUtils.getSingleElement(this, key, value, Edge.class);
    }

    /**
     * Return an iterable to all the edges in the graph for the specified
     * version
     * 
     * @param version The version to get the edges for
     * @return An {@link Iterable} of the found edges for the specified version.
     */
    public Iterable<Edge> getEdges(V version) {
        return ((HistoricGraphQuery<V>) query()).forVersion(version).withInternals(false).edges();
    }

    /**
     * Return an iterable to all the edges in the graph that have a particular
     * key/value property for the specified version.
     * 
     * @param key The key of the property to filter edges by
     * @param value The value of the property to filter edges by
     * @param version The version to get the edges for
     * @return An {@link Iterable} of the found edges for the specified criteria
     */
    public Iterable<Edge> getEdges(final String key, final Object value, V version) {
        // TODO: Consider forbidding retrieving edges by internal keys
        // (especially NATURAL_VERTEX_ID_PROP_KEY), otherwise throw exception.
        return ((HistoricGraphQuery<V>) query()).forVersion(version).edges();
    }

    /**
     * Get the latest historic revision for the specified active vertex.
     * 
     * @param a the vertex to find the latest historic revision for
     * @return the latest historic revision.
     */
    public HistoricVersionedVertex<V> getLatestHistoricRevision(ActiveVersionedVertex<V> a) {
        return getVertexByHardId(a.getProperty(VEProps.REF_TO_LATEST_HISTORIC_ID_KEY));
    }

    /**
     * Get the latest historic revision for the specified historic vertex.
     * 
     * @param h one of the historic vertex in the chain
     * @return the latest historic revision.
     */
    public HistoricVersionedVertex<V> getLatestHistoricRevision(HistoricVersionedVertex<V> h) {
        return getLatestHistoricRevision(h.getProperty(VEProps.REF_TO_LATEST_HISTORIC_ID_KEY));
    }

    /**
     * Get the latest historic revision for the specified id.
     * 
     * @param historicLatestId the id of the latest historic revision
     * @return the latest historic revision.
     */
    public HistoricVersionedVertex<V> getLatestHistoricRevision(Object historicLatestId) {
        // TODO: Ensure it is the latest version, otherwise fail with an
        // exception.
        return getVertexByHardId(historicLatestId);
    }


    /**
     * Get the latest historic revision for the specified active edge.
     * 
     * @param a the edge to find the latest historic revision for
     * @return the latest historic revision.
     */
    public HistoricVersionedEdge<V> getLatestHistoricRevision(ActiveVersionedEdge<V> a) {
        return getEdgeByHardId(a.getProperty(VEProps.REF_TO_LATEST_HISTORIC_ID_KEY));
    }

    /**
     * Get the vertex (history) chain of the specified active vertex id. The
     * returned list is sorted in reserved order where the first element is the
     * latest committed revision.
     * 
     * @param activeId The active ID to get the historic chain for
     * @return reserved versions ordered list
     */
    public List<HistoricVersionedVertex<V>> buildVertexChain(Object activeId) {
        Iterable<Vertex> vertices = getVertices(VEProps.REF_TO_ACTIVE_ID_KEY, activeId);

        // TODO: Better approach to do this comparison?
        List<HistoricVersionedVertex<V>> historicVertices = new ArrayList<HistoricVersionedVertex<V>>();
        for (Vertex v : vertices) {
            historicVertices.add((HistoricVersionedVertex<V>) v);
        }

        return Ordering.from(utils.getHistoricVersionedVertexComparator()).reverse().sortedCopy(historicVertices);
    }

    /**
     * An overload method to {@link #buildVertexChain(Object)} but receive
     * active element instead.
     * 
     * @param a The vertex to get the chain for
     * @return reserved versions ordered list
     */
    public List<HistoricVersionedVertex<V>> buildVertexChain(ActiveVersionedVertex<V> a) {
        return buildVertexChain(a.getId());
    }

    /**
     * Get the edge (history) chain of the specified active edge id. The
     * returned list is sorted in reserved order where the first element is the
     * latest committed revision.
     * 
     * @param activeId The active ID to get the historic chain for
     * @return reserved versions ordered list
     */
    public List<HistoricVersionedEdge<V>> buildEdgeChain(Object activeId) {
        Iterable<Edge> edges = getEdges(VEProps.REF_TO_ACTIVE_ID_KEY, activeId);

        // TODO: Better approach to do this comparison?
        List<HistoricVersionedEdge<V>> historicEdges = new ArrayList<HistoricVersionedEdge<V>>();
        for (Edge e : edges) {
            historicEdges.add((HistoricVersionedEdge<V>) e);
        }

        return Ordering.from(utils.getHistoricVersionedEdgeComparator()).reverse().sortedCopy(historicEdges);
    }

    /**
     * An overload method to {@link #buildVertexChain(Object)} but receive
     * active element instead.
     * 
     * @param a The edge to get the chain for
     * @return reserved versions ordered list
     */
    public List<HistoricVersionedEdge<V>> buildEdgeChain(HistoricVersionedEdge<V> a) {
        return buildEdgeChain(a.getId());
    }
}