package com.tinkerpop.blueprints.util.wrappers.event;

import com.tinkerpop.blueprints.Edge;
import com.tinkerpop.blueprints.Features;
import com.tinkerpop.blueprints.Graph;
import com.tinkerpop.blueprints.GraphQuery;
import com.tinkerpop.blueprints.Vertex;
import com.tinkerpop.blueprints.util.ElementHelper;
import com.tinkerpop.blueprints.util.StringFactory;
import com.tinkerpop.blueprints.util.wrappers.WrappedGraphQuery;
import com.tinkerpop.blueprints.util.wrappers.WrapperGraph;
import com.tinkerpop.blueprints.util.wrappers.event.listener.EdgeAddedEvent;
import com.tinkerpop.blueprints.util.wrappers.event.listener.EdgeRemovedEvent;
import com.tinkerpop.blueprints.util.wrappers.event.listener.GraphChangedListener;
import com.tinkerpop.blueprints.util.wrappers.event.listener.VertexAddedEvent;
import com.tinkerpop.blueprints.util.wrappers.event.listener.VertexRemovedEvent;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * An EventGraph is a wrapper to existing Graph implementations and provides for graph events to be raised
 * to one or more listeners on changes to the Graph.  Notifications to the listeners occur for the
 * following events: new vertex/edge, vertex/edge property changed, vertex/edge property removed,
 * vertex/edge removed.
 *
 * The limiting factor to events being raised is related to out-of-process functions changing graph elements.
 *
 * To gather events from EventGraph, simply provide an implementation of the {@link GraphChangedListener} to
 * the EventGraph by utilizing the addListener method.  EventGraph allows the addition of multiple GraphChangedListener
 * implementations.  Each listener will be notified in the order that it was added.
 *
 * @author Stephen Mallette
 */
public class EventGraph<T extends Graph> implements Graph, WrapperGraph<T> {

    protected EventTrigger trigger;

    protected final T baseGraph;

    protected final List<GraphChangedListener> graphChangedListeners = new ArrayList<GraphChangedListener>();

    private final Features features;

    public EventGraph(final T baseGraph) {
        this.baseGraph = baseGraph;
        this.features = this.baseGraph.getFeatures().copyFeatures();
        this.features.isWrapper = true;

        this.trigger = new EventTrigger(this, false);
    }

    public void removeAllListeners() {
        this.graphChangedListeners.clear();
    }

    public void addListener(final GraphChangedListener listener) {
        this.graphChangedListeners.add(listener);
    }

    public Iterator<GraphChangedListener> getListenerIterator() {
        return this.graphChangedListeners.iterator();
    }

    public EventTrigger getTrigger() {
        return this.trigger;
    }

    public void removeListener(final GraphChangedListener listener) {
        this.graphChangedListeners.remove(listener);
    }

    protected void onVertexAdded(Vertex vertex) {
        this.trigger.addEvent(new VertexAddedEvent(vertex));
    }

    protected void onVertexRemoved(final Vertex vertex, Map<String, Object> props) {
        this.trigger.addEvent(new VertexRemovedEvent(vertex, props));
    }

    protected void onEdgeAdded(Edge edge) {
        this.trigger.addEvent(new EdgeAddedEvent(edge));
    }

    protected void onEdgeRemoved(final Edge edge, Map<String, Object> props) {
        this.trigger.addEvent(new EdgeRemovedEvent(edge, props));
    }

    /**
     * Raises a vertexAdded event.
     */
    public Vertex addVertex(final Object id) {
        final Vertex vertex = this.baseGraph.addVertex(id);
        if (vertex == null) {
            return null;
        } else {
            this.onVertexAdded(vertex);
            return new EventVertex(vertex, this);
        }
    }

    public Vertex getVertex(final Object id) {
        final Vertex vertex = this.baseGraph.getVertex(id);
        if (vertex == null) {
            return null;
        } else {
            return new EventVertex(vertex, this);
        }
    }

    /**
     * Raises a vertexRemoved event.
     */
    public void removeVertex(final Vertex vertex) {
        Vertex vertexToRemove = vertex;
        if (vertex instanceof EventVertex) {
            vertexToRemove = ((EventVertex) vertex).getBaseVertex();
        }

        Map<String, Object> props = ElementHelper.getProperties(vertex);
        this.baseGraph.removeVertex(vertexToRemove);
        this.onVertexRemoved(vertex, props);
    }

    public Iterable<Vertex> getVertices() {
        return new EventVertexIterable(this.baseGraph.getVertices(), this);
    }

    public Iterable<Vertex> getVertices(final String key, final Object value) {
        return new EventVertexIterable(this.baseGraph.getVertices(key, value), this);
    }

    /**
     * Raises an edgeAdded event.
     */
    public Edge addEdge(final Object id, final Vertex outVertex, final Vertex inVertex, final String label) {
        Vertex outVertexToSet = outVertex;
        if (outVertex instanceof EventVertex) {
            outVertexToSet = ((EventVertex) outVertex).getBaseVertex();
        }

        Vertex inVertexToSet = inVertex;
        if (inVertex instanceof EventVertex) {
            inVertexToSet = ((EventVertex) inVertex).getBaseVertex();
        }

        final Edge edge = this.baseGraph.addEdge(id, outVertexToSet, inVertexToSet, label);
        if (edge == null) {
            return null;
        } else {
            this.onEdgeAdded(edge);
            return new EventEdge(edge, this);
        }
    }

    public Edge getEdge(final Object id) {
        final Edge edge = this.baseGraph.getEdge(id);
        if (edge == null) {
            return null;
        } else {
            return new EventEdge(edge, this);
        }
    }

    /**
     * Raises an edgeRemoved event.
     */
    public void removeEdge(final Edge edge) {
        Edge edgeToRemove = edge;
        if (edge instanceof EventEdge) {
            edgeToRemove = ((EventEdge) edge).getBaseEdge();
        }

        Map<String, Object> props = ElementHelper.getProperties(edge);
        this.baseGraph.removeEdge(edgeToRemove);
        this.onEdgeRemoved(edge, props);
    }

    public Iterable<Edge> getEdges() {
        return new EventEdgeIterable(this.baseGraph.getEdges(), this);
    }

    public Iterable<Edge> getEdges(final String key, final Object value) {
        return new EventEdgeIterable(this.baseGraph.getEdges(key, value), this);
    }

    public GraphQuery query() {
        final EventGraph eventGraph = this;
        return new WrappedGraphQuery(this.baseGraph.query()) {
            @Override
            public Iterable<Edge> edges() {
                return new EventEdgeIterable(this.query.edges(), eventGraph);
            }

            @Override
            public Iterable<Vertex> vertices() {
                return new EventVertexIterable(this.query.vertices(), eventGraph);
            }
        };
    }

    public void shutdown() {
        try {
            this.baseGraph.shutdown();

            // TODO: hmmmmmm??
            this.trigger.fireEventQueue();
            this.trigger.resetEventQueue();
        } catch (Exception re) {

        }
    }

    public String toString() {
        return StringFactory.graphString(this, this.baseGraph.toString());
    }

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

    public Features getFeatures() {
        return this.features;
    }
}