/*
 * This file has been developed at the University of Munich, Chair for Programming & Software Engineering.
 * 
 * This file is licensed under the Eclipse Public License (EPL) 1.0
 * 
 */
package eu.uml4soa.utbm.u2m.model;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import net.miowb.model.mio.Action;
import net.miowb.model.mio.InputAction;
import net.miowb.model.mio.InternalAction;
import net.miowb.model.mio.MayTransition;
import net.miowb.model.mio.MioFactory;
import net.miowb.model.mio.ModalIOAutomaton;
import net.miowb.model.mio.MustTransition;
import net.miowb.model.mio.OutputAction;
import net.miowb.model.mio.State;
import net.miowb.model.mio.Transition;
import eu.uml4soa.utbm.u2m.SemanticsException;

/**
 * An automaton.
 * 
 * Note that although we need sets of states, transitions, etc., they are
 * implemented as lists with custom non-equals and -hashcode functions. The
 * rationale is a) ordering for the tests, and b) problematic behaviour in
 * removing transitions from linked hash sets (basically, I gave up figuring out
 * why it didn't work properly).
 * 
 * @author Philip Mayer, mayer@pst.ifi.lmu.de
 * 
 */
public class CAutomaton {

	public static CAutomaton EMPTY= new CAutomaton();

	/**
	 * All states. This includes end states, event-attachable states, and the
	 * start state.
	 */
	private List<CState> fStates;

	/**
	 * All transitions. This is now a list to ensure always the same ordering
	 * when writing out EMF (for the test cases).
	 */
	private List<CTransition> fTransitions;

	/**
	 * The start state. Note that it may have incoming transitions.
	 */
	private CState fStart;

	/**
	 * The set of non-error end states. They may have outgoing transitions.
	 */
	private List<CState> fNonErrorEndStates;

	/**
	 * The set of error end states. They may have outgoing transitions.
	 */
	private List<CState> fErrorEndStates;


	/**
	 * Maps names of activities to compensation handlers in existence for these
	 * activities
	 */
	private Map<String, CAutomaton> fActivityNameToCompensationAutomaton;

	public CAutomaton() {
		fStates= new ArrayList<CState>();
		fTransitions= new ArrayList<CTransition>();
		fActivityNameToCompensationAutomaton= new LinkedHashMap<String, CAutomaton>();
		fNonErrorEndStates= new ArrayList<CState>();
		fErrorEndStates= new ArrayList<CState>();
	}

	public List<CState> getStates() {
		return fStates;
	}

	public List<CTransition> getTransitions() {
		return fTransitions;
	}

	public CState addState(String name) {

		CState state= findState(name);
		if (state == null) {
			state= new CState(this, name);
			fStates.add(state);
		}
		return state;
	}

	public CState findState(String name) {
		for (CState state : fStates) {
			if (state.getName().equals(name))
				return state;
		}
		return null;
	}

	public CState getStart() {
		return fStart;
	}

	public void setStart(String begin) {
		fStart= addState(begin);
	}

	public CState addNonErrorEnd(String end) {
		CState theState= addState(end);
		fNonErrorEndStates.add(theState);
		return theState;
	}

	public void addNonErrorEnds(List<CState> endStates) {
		fNonErrorEndStates.addAll(endStates);
	}

	public void addErrorEnd(String end) {
		fErrorEndStates.add(addState(end));
	}

	public void addErrorEnds(List<CState> errorStates) {
		fErrorEndStates.addAll(errorStates);
	}

	public List<CState> getErrorEndStates() {
		return fErrorEndStates;
	}

	public List<CState> getNonErrorEndStates() {
		return fNonErrorEndStates;
	}

	public void removeNonErrorEndState(CState cState) {
		fNonErrorEndStates.remove(cState);
	}

	public void removeErrorEndState(CState errorEnd) {
		fErrorEndStates.remove(errorEnd);
	}

	public CTransition addTransition(String name, CTType type, CDirection dir, String begin, String end) {

		CTransition t= findTransition(name, type, dir, begin, end);
		if (t != null) {
			System.out.println("Reusing transition " + t);
			return t;
		}

		CState from= addState(begin);
		CState to= addState(end);

		CTransition c= new CTransition(name, type, dir);
		c.setFrom(from);
		c.setTo(to);
		fTransitions.add(c);

		return c;
	}

	public CTransition findTransition(String name, CTType type, CDirection dir, String begin, String end) {

		for (CTransition t : fTransitions) {
			if (t.getFrom().getName().equals(begin) && t.getTo().getName().equals(end) && t.getActionLabel().equals(name)
					&& t.getTransitionType().equals(type) && t.getActionDirection().equals(dir))
				return t;
		}

		return null;
	}

	/**
	 * Merges the states and transitions of the given automaton into this one.
	 * Nothing is done to start, end, and error states.
	 * 
	 * @param someAutomaton
	 */
	public void addStatesAndTransitions(CAutomaton someAutomaton) {

		List<CState> states= someAutomaton.getStates();
		for (CState cState : states) {
			fStates.add(cState);
			cState.setAutomaton(this);
		}

		fTransitions.addAll(someAutomaton.getTransitions());

		for (String key : someAutomaton.fActivityNameToCompensationAutomaton.keySet()) {
			fActivityNameToCompensationAutomaton.put(key, someAutomaton.fActivityNameToCompensationAutomaton.get(key));
		}

	}

	/**
	 * Merges two states. Does NOT consider whether the merged state is an end
	 * or error state, and which compensation handlers are attached.
	 * 
	 * @param firstOne
	 * @param secondOne
	 * @return
	 */
	public CState mergeStates(CState firstOne, CState secondOne) {

		CState merged= addState("Merged[" + firstOne.getName() + "+" + secondOne.getName() + "]");

		for (CTransition cTransition : firstOne.getIncoming())
			cTransition.setTo(merged);

		for (CTransition cTransition : firstOne.getOutgoing())
			cTransition.setFrom(merged);

		for (CTransition cTransition : secondOne.getIncoming())
			cTransition.setTo(merged);

		for (CTransition cTransition : secondOne.getOutgoing())
			cTransition.setFrom(merged);

		/*
		 * Note that mergeStates DOES also copy installed comp handlers to the
		 * merged state. It does not follow any paths, though (this is the
		 * correct behaviour).
		 */

		removeState(firstOne);
		removeState(secondOne);

		return merged;
	}

	public void removeState(CState cState) {
		fStates.remove(cState);
	}

	public boolean removeTransition(CTransition cTransition) {

		boolean found= false;
		List<CTransition> newSet= new ArrayList<CTransition>();
		for (CTransition cTransition2 : fTransitions) {
			if (!cTransition2.equals(cTransition))
				newSet.add(cTransition2);
			else
				found= true;
		}

		fTransitions= newSet;

		return found;
	}

	public CAutomaton createSuffixCopy(int i) {

		Map<CState, CState> oldToNew= new LinkedHashMap<CState, CState>();

		CAutomaton suffixed= new CAutomaton();
		for (CState oldState : fStates) {
			CState newState= suffixed.addState(oldState.getName() + " copy(" + i + ")");
			oldToNew.put(oldState, newState);

			if (getNonErrorEndStates().contains(oldState))
				suffixed.addNonErrorEnd(newState.getName());
			if (getErrorEndStates().contains(oldState))
				suffixed.addErrorEnd(newState.getName());
		}

		for (CTransition oldTrans : fTransitions) {

			CState newFrom= oldToNew.get(oldTrans.getFrom());
			CState newTo= oldToNew.get(oldTrans.getTo());
			suffixed.addTransition(oldTrans.getActionLabel(), oldTrans.getTransitionType(), oldTrans.getActionDirection(), newFrom.getName(),
					newTo.getName());
		}

		suffixed.setStart(oldToNew.get(getStart()).getName());

		for (String key : fActivityNameToCompensationAutomaton.keySet())
			suffixed.fActivityNameToCompensationAutomaton.put(key, fActivityNameToCompensationAutomaton.get(key));

		return suffixed;
	}

	/**
	 * Tries to merge non-error ends if possible. It is possible if they have
	 * the same comp.handlers installed.
	 * 
	 * @throws SemanticsException
	 * 
	 */
	public void mergeEnds(String reason) throws SemanticsException {

		/*
		 * Look for all comp. installations in the path. Only merge those ends
		 * which have the same set of comp. installations.
		 */
		if (fNonErrorEndStates.size() > 1) {
			Map<Set<String>, List<CState>> mergableEnds= new HashMap<Set<String>, List<CState>>();
			for (CState neEnd : fNonErrorEndStates) {

				Set<String> whatsInstalledHere= getWhatsInstalledHere(neEnd);
				List<CState> list= mergableEnds.get(whatsInstalledHere);
				if (list == null) {
					list= new ArrayList<CState>();
					mergableEnds.put(whatsInstalledHere, list);
				}
				list.add(neEnd);
			}
			int i= 0;
			fNonErrorEndStates.clear();
			for (Set<String> mergeName : mergableEnds.keySet()) {
				i++;
				CState theOne= addState(reason + (i > 1 ? ("(" + i + ")") : ""));
				List<CState> list= mergableEnds.get(mergeName);
				for (CState cState : list) {
					theOne= mergeStates(theOne, cState);
				}
				fNonErrorEndStates.add(theOne);
			}
		}

		if (fErrorEndStates.size() > 1) {

			Map<String, List<CState>> mergableEnds= new HashMap<String, List<CState>>();
			for (CState state : fErrorEndStates) {
				List<CTransition> incoming= state.getIncoming();
				for (CTransition cTransition : incoming) {
					String thrown= getThrownException(cTransition);
					if (thrown == null)
						thrown= "NOEXCEPTION"; // this is a terminate


					List<CState> list= mergableEnds.get(thrown);
					if (list == null) {
						list= new ArrayList<CState>();
						mergableEnds.put(thrown, list);
					}
					list.add(state);
				}
			}

			int i= 0;
			fErrorEndStates.clear();
			for (String mergeName : mergableEnds.keySet()) {
				i++;
				CState theOne= addState(reason + (i > 1 ? ("(" + i + ")") : ""));
				List<CState> list= mergableEnds.get(mergeName);
				for (CState cState : list) {
					theOne= mergeStates(theOne, cState);
				}
				fErrorEndStates.add(theOne);
			}

		}
	}

	private String getThrownException(CTransition transition) {
		String actionLabel= transition.getActionLabel();
		if (actionLabel.startsWith("throw")) {
			int first= actionLabel.indexOf("(");
			int last= actionLabel.indexOf(")");
			if (first != -1 && last != -1)
				return actionLabel.substring(first + 1, last);
		}
		return null;
	}

	/**
	 * Returns a set of activity names for which compensation handlers have been
	 * installed in paths leading to state.
	 * 
	 * @param neEnd
	 * @return
	 */
	public Set<String> getWhatsInstalledHere(CState state) {

		Set<String> installed= new HashSet<String>();

		List<CTransition> incoming= state.getIncoming();
		for (CTransition cTransition : incoming) {
			List<CTransition> whatsInstalledHere= getWhatsInstalledHere(cTransition);
			for (CTransition cTransition2 : whatsInstalledHere) {
				installed.addAll(cTransition2.getReferencedActivities());
			}
		}

		return installed;
	}

	/**
	 * Returns a set of states in which comp.handlers have been installed.
	 * 
	 * @param neEnd
	 * @return
	 */
	public List<CTransition> getWhatsInstalledHere(CTransition cTransition2) {

		List<CTransition> installed= new ArrayList<CTransition>();

		List<String> uninstalled= new ArrayList<String>();

		Set<CState> seen= new HashSet<CState>();
		List<CState> queue= new ArrayList<CState>();
		queue.add(cTransition2.getTo());

		while (!queue.isEmpty()) {

			List<CState> queue2= new ArrayList<CState>();

			for (CState cState : queue) {
				if (seen.contains(cState))
					continue;

				seen.add(cState);

				List<CTransition> incoming= cState.getIncoming();
				for (CTransition cTransition : incoming) {

					if (cTransition.getActionLabel().startsWith("compInstalled")) {
						installed.add(cTransition);
					}

					if (cTransition.getActionLabel().startsWith("compHandled")) {
						uninstalled.addAll(cTransition.getReferencedActivities());
					}

					CState from= cTransition.getFrom();
					queue2.add(from);
				}
			}

			queue= queue2;
		}

		// Remove uninstalled ones...
		for (Iterator<CTransition> iterator= installed.iterator(); iterator.hasNext();) {
			CTransition inst= (CTransition) iterator.next();
			Collection<? extends String> instNames= inst.getReferencedActivities();
			for (String instName : instNames) {
				if (uninstalled.contains(instName))
					iterator.remove();
			}

		}

		return installed;
	}

	public void addCompensationHandler(String name, CAutomaton compMio) {
		fActivityNameToCompensationAutomaton.put(name, compMio);
	}

	public List<CTransition> getCompensationCallTransitions() {

		List<CTransition> l= new ArrayList<CTransition>();
		for (CTransition t : fTransitions) {
			if (t.getActionLabel().contains("compensate"))
				l.add(t);
		}
		return l;
	}

	public CAutomaton getCompensationHandlerOfScope(String scopeName) {
		return fActivityNameToCompensationAutomaton.get(scopeName);
	}

	public boolean isInLoop(CTransition cInstallTransition) {

		Set<CState> visited= new LinkedHashSet<CState>();
		List<CState> queue= new ArrayList<CState>();
		queue.add(cInstallTransition.getFrom());
		while (!queue.isEmpty()) {

			List<CState> queue2= new ArrayList<CState>();
			for (CState currentState : queue) {

				if (visited.contains(currentState))
					continue;
				visited.add(currentState);

				List<CTransition> outgoing= currentState.getOutgoing();
				for (CTransition cTransition : outgoing) {
					CState to= cTransition.getTo();
					if (to == cInstallTransition.getFrom())
						return true;
					queue2.add(to);
				}
			}

			queue= queue2;
		}

		return false;
	}

	public ModalIOAutomaton toEMF() {

		boolean sort= true;

		ModalIOAutomaton mod= MioFactory.eINSTANCE.createModalIOAutomaton();
		mod.setName("Generated MIO");

		Map<String, State> stateMap= new LinkedHashMap<String, State>();

		if (sort)
			Collections.sort(fStates, new Comparator<CState>() {
				@Override
				public int compare(CState o1, CState o2) {
					return o1.getName().compareTo(o2.getName());
				}
			});

		for (CState state : fStates) {
			State s= MioFactory.eINSTANCE.createState();
			String prefix= "";
			if (getNonErrorEndStates().contains(state))
				prefix= "NONERREND:";
			if (getErrorEndStates().contains(state))
				prefix= "ERREND:";
			if (getStart().equals(s))
				prefix= "START:";

			s.setLabel(prefix + state.getName());
			mod.getStates().add(s);
			stateMap.put(state.getName(), s);
		}

		mod.setStart(stateMap.get(fStart.getName()));

		if (sort)
			Collections.sort(fTransitions, new Comparator<CTransition>() {
				@Override
				public int compare(CTransition o1, CTransition o2) {
					return o1.getActionLabel().compareTo(o2.getActionLabel());
				}
			});

		for (CTransition trans : fTransitions) {

			String actionLabel= trans.getActionLabel();
			CDirection actionDirection= trans.getActionDirection();

			Action a= null;
			switch (actionDirection) {
				case IN:
					a= getAction(mod.getInputs(), actionLabel);
					if (a == null) {
						a= MioFactory.eINSTANCE.createInputAction();
						mod.getInputs().add((InputAction) a);
					}
					break;
				case OUT:
					a= getAction(mod.getOutputs(), actionLabel);
					if (a == null) {
						a= MioFactory.eINSTANCE.createOutputAction();
						mod.getOutputs().add((OutputAction) a);
					}
					break;
				case INT:
					a= getAction(mod.getInternals(), actionLabel);
					if (a == null) {
						a= MioFactory.eINSTANCE.createInternalAction();
						mod.getInternals().add((InternalAction) a);
					}
					break;
			}

			a.setLabel(actionLabel);

			Transition t= null;

			CTType transitionType= trans.getTransitionType();
			switch (transitionType) {
				case MUST:
					t= MioFactory.eINSTANCE.createMustTransition();
					mod.getMustTransitions().add((MustTransition) t);
					break;
				case MAY:
					t= MioFactory.eINSTANCE.createMayTransition();
					mod.getMayTransitions().add((MayTransition) t);
					break;
			}
			t.setAction(a);

			t.setFrom(stateMap.get(trans.getFrom().getName()));
			t.setTo(stateMap.get(trans.getTo().getName()));
		}

		return mod;
	}



	private <E extends Action> Action getAction(List<E> objects, String actionLabel) {
		for (E something : objects) {
			if (something.getLabel().equals(actionLabel))
				return something;
		}
		return null;
	}

	public void clearAllCompensationInstallations() {

		// TODO "cross out" compInstall() transitions?

		fActivityNameToCompensationAutomaton.clear();
	}

	public CState addPostSpacing(CState oldState) {
		return addSpacing(oldState, false);
	}

	public CState addPreSpacing(CState oldState) {
		return addSpacing(oldState, true);
	}

	private CState addSpacing(CState oldState, boolean before) {
		CState newState= addState("MovedFrom" + oldState.getName());

		if (fErrorEndStates.contains(oldState)) {
			fErrorEndStates.remove(oldState);
			fErrorEndStates.add(newState);
		}
		if (fNonErrorEndStates.contains(oldState)) {
			fNonErrorEndStates.remove(oldState);
			fNonErrorEndStates.add(newState);
		}

		if (getStart().equals(oldState))
			setStart(newState.getName());

		if (before)
			addTransition("interspace", CTType.MUST, CDirection.INT, newState.getName(), oldState.getName());
		else
			addTransition("interspace", CTType.MUST, CDirection.INT, oldState.getName(), newState.getName());

		return newState;
	}



}
