package nfa;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.Iterator;

import org.jgrapht.graph.DirectedPseudograph;

import nfa.transitionlabel.TransitionLabel;
import nfa.transitionlabel.TransitionLabel.TransitionType;

/**
 * A graph representing an NFA.
 * 
 * @author N. H. Weideman
 *
 */
public class NFAGraph extends DirectedPseudograph<NFAVertexND, NFAEdge> {
	private static final long serialVersionUID = 1L;

	/* The vertex representing the initial state of the NFA */
	private NFAVertexND initialState;

	public NFAVertexND getInitialState() {
		return initialState;
	}

	public void setInitialState(NFAVertexND initialState) {
		if (!super.containsVertex(initialState)) {
			throw new IllegalArgumentException("Graph does not contain vertex: " + initialState);
		}
		this.initialState = initialState;
	}

	/* The accepting states of the NFA */
	private HashSet<NFAVertexND> acceptingStates;

	public void addAcceptingState(NFAVertexND acceptingState) {
		if (!super.containsVertex(acceptingState)) {
			throw new IllegalArgumentException("Graph does not contain vertex: " + acceptingState);
		}
		acceptingStates.add(acceptingState);
	}

	public boolean isAcceptingState(String stateNumber) {
		return acceptingStates.contains(new NFAVertexND(stateNumber));
	}

	public boolean isAcceptingState(NFAVertexND state) {
		return acceptingStates.contains(state);
	}

	public void removeAcceptingState(NFAVertexND acceptingState) {
		if (!super.containsVertex(acceptingState)) {
			throw new IllegalArgumentException("Graph does not contains accepting state: " + acceptingState);
		}
		acceptingStates.remove(acceptingState);
	}

	public Set<NFAVertexND> getAcceptingStates() {
		return acceptingStates;
	}

	public NFAGraph() {
		super(NFAEdge.class);
		acceptingStates = new HashSet<NFAVertexND>();
	}

	/**
	 * @return A new instance of a NFAGraph equal to this instance
	 */
	public NFAGraph copy() {
		NFAGraph c = new NFAGraph();
		for (NFAVertexND v : super.vertexSet()) {
			c.addVertex(v.copy());
		}
		for (NFAEdge e : super.edgeSet()) {
			c.addEdge(e.copy());
		}

		if (initialState != null) {
			c.initialState = initialState.copy();
		}
		
		for (NFAVertexND v : acceptingStates) {
			c.addAcceptingState(v.copy());
		}
		return c;
	}

	/**
	 * Adds a new edge to the NFA graph
	 * 
	 * @param newEdge
	 *            The new edge to add
	 * @return true if this graph did not already contain the specified edge
	 */
	public boolean addEdge(NFAEdge newEdge) {
		if (newEdge == null) {
			throw new NullPointerException("New edge cannot be null");
		}
		if (newEdge.getTransitionLabel().isEmpty()) {
			return false;
		}
		
		NFAVertexND s = newEdge.getSourceVertex();
		NFAVertexND t = newEdge.getTargetVertex();
		if (super.containsEdge(newEdge)) {
			/* if the edge exists increase the number of its parallel edges */
			NFAEdge e = getEdge(newEdge);
			e.incNumParallel();
		} else if (newEdge.getIsEpsilonTransition()) {
			/* check if the NFA already has an epsilon transition between these states */
			Set<NFAEdge> es = super.getAllEdges(s, t);
			for (NFAEdge currentEdge : es) {
				if (currentEdge.equals(newEdge)) {
					/* if it does, add the new edge as a parallel edge (priorities don't matter between the same states) */
					currentEdge.incNumParallel();
					return true;
				}
			}
		} else {
			/* check if the new edge overlaps the current edges */
			Set<NFAEdge> es = super.getAllEdges(s, t);
			// TODO lightly tested
			for (NFAEdge currentEdge : es) {
				/* epsilon edges cannot overlap */
				if (currentEdge.getTransitionType() == TransitionType.SYMBOL) {
					TransitionLabel tlCurrentEdge = currentEdge.getTransitionLabel();
					TransitionLabel tlNewEdge = newEdge.getTransitionLabel();
					TransitionLabel intersection = tlNewEdge.intersection(tlCurrentEdge);
					if (!intersection.isEmpty()) {
						/* overlapping edge */
						TransitionLabel currentEdgeRelabel = tlCurrentEdge.intersection(tlNewEdge.complement());
						int currentEdgeWeight = 0;
						if (!currentEdgeRelabel.isEmpty()) {
							currentEdgeWeight = currentEdge.getNumParallel();
							removeEdge(currentEdge);
							
							NFAEdge currentEdgeRelabeled = new NFAEdge(s, t, currentEdgeRelabel);
							currentEdgeRelabeled.setNumParallel(currentEdgeWeight);
							addEdge(currentEdgeRelabeled);
						}
						
						
						NFAEdge overlappingEdge = new NFAEdge(s, t, intersection);
						overlappingEdge.setNumParallel(currentEdgeWeight + newEdge.getNumParallel());
						addEdge(overlappingEdge);
						
						TransitionLabel newEdgeRelabel = tlNewEdge.intersection(tlCurrentEdge.complement());
						if (!newEdgeRelabel.isEmpty()) {
							int newEdgeWeight = newEdge.getNumParallel();
							
							NFAEdge newEdgeRelabeled = new NFAEdge(s, t, newEdgeRelabel);
							newEdgeRelabeled.setNumParallel(newEdgeWeight);
							addEdge(newEdgeRelabeled);
						}

						
						return true;
						
					}
				}
				
			}
		}
		if (!super.containsVertex(newEdge.getSourceVertex())) {
			throw new IllegalArgumentException("Graph doesn't contain vertex: " + newEdge.getSourceVertex());
		}
		if (!super.containsVertex(newEdge.getTargetVertex())) {
			throw new IllegalArgumentException("Graph doesn't contain vertex: " + newEdge.getTargetVertex());
		}
		return super.addEdge(newEdge.getSourceVertex(), newEdge.getTargetVertex(), newEdge);
	}

	/**
	 * All the edges representing an epsilon transition from a vertex
	 * 
	 * @param v
	 *            The vertex to find the epsilon transitions from.
	 * @return A set of NFA edges representing the epsilon transitions.
	 */
	public Set<NFAEdge> outgoingEpsilonEdgesOf(NFAVertexND v) {
		Set<NFAEdge> allEdges = super.outgoingEdgesOf(v);

		Set<NFAEdge> toReturn = new HashSet<NFAEdge>();

		for (NFAEdge e : allEdges) {
			if (e.getIsEpsilonTransition()) {
				toReturn.add(e);
			}
		}

		return toReturn;
	}

	@Override
	public boolean addVertex(NFAVertexND v) {
		if (containsVertex(v)) {
			throw new IllegalArgumentException("Graph already contains vertex: " + v);
		}
		return super.addVertex(v);
	}
	
	public NFAEdge getEdge(NFAEdge e) {
		if (!super.containsEdge(e)) {
			throw new IllegalArgumentException("Graph does not contain edge: " + e.getSourceVertex() + "->" + e.getTargetVertex() + ":" + e.getTransitionLabel());
		}
		Set<NFAEdge> edges = super.getAllEdges(e.getSourceVertex(), e.getTargetVertex());
		for (NFAEdge currentE : edges) {
			if (currentE.equals(e)) {
				return currentE;
			}
		}
		return null;
	}

	@Override
	public boolean equals(Object o) {
		if (!super.equals(o)) {
			return false;
		}
		
		/* testing that the amount of parallel edges are equal */
		NFAGraph n = (NFAGraph) o;
		for (NFAEdge e : n.edgeSet()) {
			Set<NFAEdge> nEdges = super.getAllEdges(e.getSourceVertex(), e.getTargetVertex());
			for (NFAEdge nEdge : nEdges) {
				if (e.equals(nEdge) && e.getNumParallel() != nEdge.getNumParallel()) {
					return false;
				}
			}
			
		}
		
		if (initialState != null && !initialState.equals(n.getInitialState())) {
			return false;
		}
		
		HashSet<NFAVertexND> myAcceptingStates = new HashSet<NFAVertexND>(acceptingStates);
		HashSet<NFAVertexND> otherAcceptingStates = new HashSet<NFAVertexND>(n.getAcceptingStates());

		boolean condition1 = myAcceptingStates.size() == otherAcceptingStates.size();
		boolean condition2 = myAcceptingStates.containsAll(otherAcceptingStates);
		boolean condition3 = otherAcceptingStates.containsAll(myAcceptingStates);
		/* first condition might be redundant */
		return condition1 && condition2 && condition3 ;

	}
	
	public NFAGraph reverse() {
		NFAGraph reversedGraph = this.copy();
		
		for (NFAEdge e : edgeSet()) {
			NFAVertexND newSource = e.getTargetVertex();
			NFAVertexND newTarget = e.getSourceVertex();
			NFAEdge reversedEdge = new NFAEdge(newSource, newTarget, e.getTransitionLabel());
			reversedGraph.removeEdge(e);
			reversedGraph.addEdge(reversedEdge);
		}
		
		return reversedGraph;
	}
	
	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder("I:" + initialState + " A:");
		if (!acceptingStates.isEmpty()) {
			for (NFAVertexND a : acceptingStates) {
				sb.append(a + ";");
			}
		} else {
			sb.append("No Accepting states;");
		}

		
		return sb.toString() + " " + super.toString();
	}
			
	private String nameState(NFAVertexND v) {
			StringBuilder sb = new StringBuilder("\"");
			ArrayList<String> states = v.getStates();
			Collections.sort(states);
			Iterator<String> stateIterator = states.iterator();
			while (stateIterator.hasNext()) {
				sb.append(stateIterator.next());
				if (stateIterator.hasNext()) {
					sb.append(",");
				}
			}
			sb.append("\"");
			return sb.toString();
	}
}