package org.jgrapht.capi.graph;

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

import org.jgrapht.Graph;
import org.jgrapht.ListenableGraph;
import org.jgrapht.event.GraphEdgeChangeEvent;
import org.jgrapht.event.GraphListener;
import org.jgrapht.event.GraphVertexChangeEvent;
import org.jgrapht.event.VertexSetListener;
import org.jgrapht.graph.GraphDelegator;

public class CapiListenableGraph<V, E>
	extends
	GraphDelegator<V, E>
	implements ListenableGraph<V, E>, CapiGraphWrapper<V,E>
{
	private static final long serialVersionUID = 1L;

    private List<GraphListener<V, E>> graphListeners = new ArrayList<>();
    private List<VertexSetListener<V>> vertexSetListeners = new ArrayList<>();
    private FlyweightEdgeEvent<V, E> reuseableEdgeEvent;
    private FlyweightVertexEvent<V> reuseableVertexEvent;
    private boolean reuseEvents;
	private Graph<V, E> wrappedGraph;
	
	public CapiListenableGraph(Graph<V, E> graph) {
		super(graph);
		this.wrappedGraph = graph;
		this.reuseEvents = false;
        reuseableEdgeEvent = new FlyweightEdgeEvent<>(this, -1, null);
        reuseableVertexEvent = new FlyweightVertexEvent<>(this, -1, null);

        // the following restriction could be probably relaxed in the future.
        if (graph instanceof ListenableGraph<?, ?>) {
            throw new IllegalArgumentException("base graph cannot be listenable");
        }
	}
	
    public Graph<V, E> getWrappedGraph() {
		return wrappedGraph;
	}

	/**
     * If the <code>reuseEvents</code> flag is set to <code>true</code> this class will reuse
     * previously fired events and will not create a new object for each event. This option
     * increases performance but should be used with care, especially in multithreaded environment.
     *
     * @param reuseEvents whether to reuse previously fired event objects instead of creating a new
     *        event object for each event.
     */
    public void setReuseEvents(boolean reuseEvents)
    {
        this.reuseEvents = reuseEvents;
    }

    /**
     * Tests whether the <code>reuseEvents</code> flag is set. If the flag is set to
     * <code>true</code> this class will reuse previously fired events and will not create a new
     * object for each event. This option increases performance but should be used with care,
     * especially in multithreaded environment.
     *
     * @return the value of the <code>reuseEvents</code> flag.
     */
    public boolean isReuseEvents()
    {
        return reuseEvents;
    }

    @Override
    public E addEdge(V sourceVertex, V targetVertex)
    {
        E e = super.addEdge(sourceVertex, targetVertex);

        if (e != null) {
            fireEdgeAdded(e, sourceVertex, targetVertex, Graph.DEFAULT_EDGE_WEIGHT);
        }

        return e;
    }

    @Override
    public boolean addEdge(V sourceVertex, V targetVertex, E e)
    {
        boolean added = super.addEdge(sourceVertex, targetVertex, e);

        if (added) {
            fireEdgeAdded(e, sourceVertex, targetVertex, Graph.DEFAULT_EDGE_WEIGHT);
        }

        return added;
    }

    @Override
    public void addGraphListener(GraphListener<V, E> l)
    {
        addToListenerList(graphListeners, l);
    }

    @Override
    public V addVertex() {
        V v = super.addVertex();
        if (v != null) { 
            fireVertexAdded(v);    
        }
        return v;
    }
    
    
    @Override
    public boolean addVertex(V v)
    {
        boolean modified = super.addVertex(v);

        if (modified) {
            fireVertexAdded(v);
        }

        return modified;
    }

    @Override
    public void addVertexSetListener(VertexSetListener<V> l)
    {
        addToListenerList(vertexSetListeners, l);
    }

    @Override
    public E removeEdge(V sourceVertex, V targetVertex)
    {
        E e = super.getEdge(sourceVertex, targetVertex);
        if (e != null) {
            double weight = super.getEdgeWeight(e);
            if (super.removeEdge(e)) {
                fireEdgeRemoved(e, sourceVertex, targetVertex, weight);
            }
        }
        return e;
    }

    @Override
    public boolean removeEdge(E e)
    {
        V sourceVertex = getEdgeSource(e);
        V targetVertex = getEdgeTarget(e);
        double weight = getEdgeWeight(e);

        boolean modified = super.removeEdge(e);

        if (modified) {
            fireEdgeRemoved(e, sourceVertex, targetVertex, weight);
        }

        return modified;
    }

    @Override
    public void removeGraphListener(GraphListener<V, E> l)
    {
        graphListeners.remove(l);
    }

    @Override
    public boolean removeVertex(V v)
    {
        if (containsVertex(v)) {
            Set<E> touchingEdgesList = edgesOf(v);

            // copy set to avoid ConcurrentModificationException
            removeAllEdges(new ArrayList<>(touchingEdgesList));

            super.removeVertex(v); // remove the vertex itself

            fireVertexRemoved(v);

            return true;
        } else {
            return false;
        }
    }

    @Override
    public void setEdgeWeight(E e, double weight)
    {
        super.setEdgeWeight(e, weight);

        V sourceVertex = getEdgeSource(e);
        V targetVertex = getEdgeTarget(e);

        fireEdgeWeightUpdated(e, sourceVertex, targetVertex, weight);
    }

    @Override
    public void removeVertexSetListener(VertexSetListener<V> l)
    {
        vertexSetListeners.remove(l);
    }

    /**
     * Notify listeners that the specified edge was added.
     *
     * @param edge the edge that was added.
     * @param source edge source
     * @param target edge target
     * @param weight edge weight
     */
    protected void fireEdgeAdded(E edge, V source, V target, double weight)
    {
        GraphEdgeChangeEvent<V, E> e = createGraphEdgeChangeEvent(
            GraphEdgeChangeEvent.EDGE_ADDED, edge, source, target, weight);

        for (GraphListener<V, E> l : graphListeners) {
            l.edgeAdded(e);
        }
    }

    /**
     * Notify listeners that the specified edge was removed.
     *
     * @param edge the edge that was removed.
     * @param source edge source
     * @param target edge target
     * @param weight edge weight
     */
    protected void fireEdgeRemoved(E edge, V source, V target, double weight)
    {
        GraphEdgeChangeEvent<V, E> e = createGraphEdgeChangeEvent(
            GraphEdgeChangeEvent.EDGE_REMOVED, edge, source, target, weight);

        for (GraphListener<V, E> l : graphListeners) {
            l.edgeRemoved(e);
        }
    }

    /**
     * Notify listeners that the weight of an edge has changed.
     *
     * @param edge the edge whose weight has changed.
     * @param source edge source
     * @param target edge target
     * @param weight the edge weight
     */
    protected void fireEdgeWeightUpdated(E edge, V source, V target, double weight)
    {
        GraphEdgeChangeEvent<V, E> e = createGraphEdgeChangeEvent(
            GraphEdgeChangeEvent.EDGE_WEIGHT_UPDATED, edge, source, target, weight);

        for (GraphListener<V, E> l : graphListeners) {
            l.edgeWeightUpdated(e);
        }
    }

    /**
     * Notify listeners that the specified vertex was added.
     *
     * @param vertex the vertex that was added.
     */
    protected void fireVertexAdded(V vertex)
    {
        GraphVertexChangeEvent<V> e =
            createGraphVertexChangeEvent(GraphVertexChangeEvent.VERTEX_ADDED, vertex);

        for (VertexSetListener<V> l : vertexSetListeners) {
            l.vertexAdded(e);
        }

        for (GraphListener<V, E> l : graphListeners) {
            l.vertexAdded(e);
        }
    }

    /**
     * Notify listeners that the specified vertex was removed.
     *
     * @param vertex the vertex that was removed.
     */
    protected void fireVertexRemoved(V vertex)
    {
        GraphVertexChangeEvent<V> e =
            createGraphVertexChangeEvent(GraphVertexChangeEvent.VERTEX_REMOVED, vertex);

        for (VertexSetListener<V> l : vertexSetListeners) {
            l.vertexRemoved(e);
        }

        for (GraphListener<V, E> l : graphListeners) {
            l.vertexRemoved(e);
        }
    }

    private static <L extends EventListener> void addToListenerList(List<L> list, L l)
    {
        if (!list.contains(l)) {
            list.add(l);
        }
    }

    private GraphEdgeChangeEvent<V, E> createGraphEdgeChangeEvent(
        int eventType, E edge, V source, V target, double weight)
    {
        if (reuseEvents) {
            reuseableEdgeEvent.setType(eventType);
            reuseableEdgeEvent.setEdge(edge);
            reuseableEdgeEvent.setEdgeSource(source);
            reuseableEdgeEvent.setEdgeTarget(target);
            reuseableEdgeEvent.setEdgeWeight(weight);

            return reuseableEdgeEvent;
        } else {
            return new GraphEdgeChangeEvent<>(this, eventType, edge, source, target, weight);
        }
    }

    private GraphVertexChangeEvent<V> createGraphVertexChangeEvent(int eventType, V vertex)
    {
        if (reuseEvents) {
            reuseableVertexEvent.setType(eventType);
            reuseableVertexEvent.setVertex(vertex);

            return reuseableVertexEvent;
        } else {
            return new GraphVertexChangeEvent<>(this, eventType, vertex);
        }
    }

    /**
     * A reuseable edge event.
     *
     * @author Barak Naveh
     */
    private static class FlyweightEdgeEvent<VV, EE>
        extends
        GraphEdgeChangeEvent<VV, EE>
    {
        private static final long serialVersionUID = 3907207152526636089L;

        /**
         * @see GraphEdgeChangeEvent
         */
        public FlyweightEdgeEvent(Object eventSource, int type, EE e)
        {
            super(eventSource, type, e, null, null);
        }

        /**
         * Sets the edge of this event.
         *
         * @param e the edge to be set.
         */
        protected void setEdge(EE e)
        {
            this.edge = e;
        }

        protected void setEdgeSource(VV v)
        {
            this.edgeSource = v;
        }

        protected void setEdgeTarget(VV v)
        {
            this.edgeTarget = v;
        }

        protected void setEdgeWeight(double weight)
        {
            this.edgeWeight = weight;
        }

        /**
         * Set the event type of this event.
         *
         * @param type the type to be set.
         */
        protected void setType(int type)
        {
            this.type = type;
        }
    }

    /**
     * A reuseable vertex event.
     *
     * @author Barak Naveh
     */
    private static class FlyweightVertexEvent<VV>
        extends
        GraphVertexChangeEvent<VV>
    {
        private static final long serialVersionUID = 3257848787857585716L;

        /**
         * @see GraphVertexChangeEvent#GraphVertexChangeEvent(Object, int, Object)
         */
        public FlyweightVertexEvent(Object eventSource, int type, VV vertex)
        {
            super(eventSource, type, vertex);
        }

        /**
         * Set the event type of this event.
         *
         * @param type type to be set.
         */
        protected void setType(int type)
        {
            this.type = type;
        }

        /**
         * Sets the vertex of this event.
         *
         * @param vertex the vertex to be set.
         */
        protected void setVertex(VV vertex)
        {
            this.vertex = vertex;
        }
    }
	
	
	
}
