/*
 * 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.mdd4soa.trans.uml2smm;

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

import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.TreeIterator;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.uml2.uml.Action;
import org.eclipse.uml2.uml.ActivityEdge;
import org.eclipse.uml2.uml.ActivityNode;
import org.eclipse.uml2.uml.ActivityParameterNode;
import org.eclipse.uml2.uml.Behavior;
import org.eclipse.uml2.uml.CallBehaviorAction;
import org.eclipse.uml2.uml.CallEvent;
import org.eclipse.uml2.uml.CallOperationAction;
import org.eclipse.uml2.uml.Class;
import org.eclipse.uml2.uml.DecisionNode;
import org.eclipse.uml2.uml.ExceptionHandler;
import org.eclipse.uml2.uml.ForkNode;
import org.eclipse.uml2.uml.InitialNode;
import org.eclipse.uml2.uml.JoinNode;
import org.eclipse.uml2.uml.MergeNode;
import org.eclipse.uml2.uml.Operation;
import org.eclipse.uml2.uml.RaiseExceptionAction;
import org.eclipse.uml2.uml.ReplyAction;
import org.eclipse.uml2.uml.StructuredActivityNode;
import org.eclipse.uml2.uml.Trigger;

import eu.mdd4soa.smm.exception.TransformationException;
import eu.mdd4soa.trans.uml2smm.StereotypeUtil.StereoType;

public class UMLNodeUtil {

	/**
	 * Returns true if the given node is an exception handler, i.e. it has an
	 * incoming exception edge to a pin (not to the body).
	 * 
	 * @param current an ActivityNode
	 * @return
	 */
	public static boolean isExceptionHandler(ActivityNode current) {
		if (current instanceof Action) {
			// there is no way to get the exception edge from here:
			// search all edges in the model:
			for (TreeIterator<EObject> iter= current.getModel().eAllContents(); iter.hasNext();) {
				EObject e= iter.next();
				if (e instanceof ExceptionHandler) {
					ExceptionHandler handler= (ExceptionHandler) e;
					if (handler.getHandlerBody() != null && handler.getHandlerBody().equals(current)) {
						return true;
					}
				}
			}
		}
		return false;
	}

	/**
	 * Returns true if the given node is the parameter node of an activity.
	 * 
	 * @param current
	 * @return
	 */
	public static boolean isActivityParameterNode(ActivityNode current) {
		return current instanceof ActivityParameterNode;
	}

	/**
	 * Returns true if the given node has an outgoing edge which crosses a
	 * structured node border.
	 * 
	 * @param current
	 * @return
	 */
	public static boolean hasOutgoingInterruptingEdge(ActivityNode current) {

		if (current.getIncomings().size() != 0)
			return false;

		if (current.getOutgoings().size() == 0)
			return false;

		EList<ActivityEdge> outgoings= current.getOutgoings();
		for (ActivityEdge activityEdge : outgoings) {
			StructuredActivityNode sourceStructuredNode= activityEdge.getSource().getInStructuredNode();
			StructuredActivityNode targetStructuredNode= activityEdge.getTarget().getInStructuredNode();
			if (sourceStructuredNode == null) {
				// top-level actions; cannot be interrupting.
				return false;
			} else if (!sourceStructuredNode.equals(targetStructuredNode))
				return true;
		}

		return false;
	}

	public static String getNodeNames(List<ActivityNode> firstNodes) {
		String nodenames= "";
		for (int i= 0; i < firstNodes.size(); i++) {
			if (i > 0) {
				nodenames+= ( (i < firstNodes.size() - 1) ? ", " : " and ");
			}
			nodenames+= getNodeName(firstNodes.get(i));
		}
		return nodenames;
	}

	/**
	 * Helps in case a uml element is unnamed
	 * 
	 * @param name
	 * @param activityNode
	 * @return
	 */
	public static String getNodeName(ActivityNode activityNode) {
		String name= activityNode.getName();
		if (name == null || name.isEmpty()) {
			if (activityNode instanceof CallOperationAction) {
				CallOperationAction cop= (CallOperationAction) activityNode;
				if (cop.getOperation() != null) {
					name= cop.getOperation().getName();
				}
			} else if (activityNode instanceof CallBehaviorAction) {
				Behavior behave= ((CallBehaviorAction) activityNode).getBehavior();
				if (behave != null) {
					name= behave.getName();
				}
			} else if (activityNode instanceof ReplyAction) {
				name= "<reply> ";
				ReplyAction repa= (ReplyAction) activityNode;
				if (repa.getReplyToCall() != null) {
					Trigger rplTrigger= repa.getReplyToCall();
					if (rplTrigger != null && rplTrigger.getEvent() instanceof CallEvent) {
						Operation op= ((CallEvent) rplTrigger.getEvent()).getOperation();
						if (op != null) {
							name+= op.getName();
						}
					}
				}
			} else {
				name= activityNode.getClass().getSimpleName();
			}
		}
		return name;
	}

	public static String getOwnerName(ActivityNode activityNode) {
		String name= "<unknown element>";
		if (activityNode.getOwner() instanceof Class) {
			name= ((Class) activityNode.getOwner()).getName();
		}
		if (activityNode.getOwner() instanceof StructuredActivityNode) {
			name= ((StructuredActivityNode) activityNode.getOwner()).getName();
		}
		return name;
	}


	public static boolean isDecisionNode(ActivityNode current) {
		return current instanceof DecisionNode;
	}

	public static boolean isMergeNode(ActivityNode current) {
		return current instanceof MergeNode;
	}

	public static boolean isForkNode(ActivityNode current) {
		return current instanceof ForkNode;
	}

	public static boolean isJoinNode(ActivityNode current) {
		return current instanceof JoinNode;
	}

	public static boolean isServiceAction(ActivityNode currentNode) {

		if (! (currentNode instanceof Action))
			return false;

		return StereotypeUtil.hasStereotype(currentNode, StereoType.SEND, StereoType.RECEIVE, StereoType.SENDANDRECEIVE, StereoType.REPLY);
	}


	public static boolean isContributingAction(ActivityNode currentNode) {

		if (! (currentNode instanceof Action))
			return false;

		if (currentNode instanceof RaiseExceptionAction)
			return true;

		return StereotypeUtil.hasStereotype(currentNode, StereoType.DATA, StereoType.COMPENSATE, StereoType.COMPENSATEALL);
	}


	public static boolean isServiceActivity(ActivityNode currentNode) {
		return (currentNode instanceof StructuredActivityNode && StereotypeUtil.hasStereotype(currentNode, StereoType.SERVICEACTIVITY));

	}

	public static List<ActivityNode> getNextElements(ActivityNode newElement) {

		List<ActivityNode> nodes= new ArrayList<ActivityNode>();
		EList<ActivityEdge> outgoings= newElement.getOutgoings();
		for (ActivityEdge activityEdge : outgoings) {
			nodes.add(activityEdge.getTarget());
		}

		return nodes;
	}

	/**
	 * Returns the first node in the list of elements. A first node is one
	 * without incoming edges, with the exception of interrupting receives and
	 * their targets.
	 * 
	 * @param listToSearch
	 * @return
	 * @throws TransformationException
	 */
	public static ActivityNode getFirstNode(List<ActivityNode> elements) throws TransformationException {

		List<ActivityNode> firstNodes= new ArrayList<ActivityNode>();
		List<ActivityNode> activityNodes= new ArrayList<ActivityNode>();

		for (ActivityNode current : elements) {
			activityNodes.add(current);


			if (current.getIncomings().size() == 0) {
				/*
				 * We may falsely assume a start node based on no incoming links
				 * for exception handlers (they have their incoming link to the
				 * exception pin), parameter nodes, and interrupting actions
				 * which leave the current region.
				 */
				if (!UMLNodeUtil.isExceptionHandler(current) && !UMLNodeUtil.isActivityParameterNode(current)
						&& !UMLNodeUtil.hasOutgoingInterruptingEdge(current)) {
					firstNodes.add(current);
				}
			} else {
				/*
				 * If a node has incoming edges, it might still be a first node
				 * if the incoming edges come from the outside, as is the case
				 * in loop or branch partitions.
				 */
				EList<ActivityEdge> incomings= current.getIncomings();
				for (ActivityEdge activityEdge : incomings) {

					ActivityNode source= activityEdge.getSource();
					// Source is in our list? -> Not a first node.
					if (elements.contains(source))
						continue;

					// Source is not in list, but is in an inner service
					// activity? -> not a first node.
					StructuredActivityNode parent= source.getInStructuredNode();
					if (parent != null && elements.contains(parent))
						continue;

					// Else: Is a loop or branch start.
					firstNodes.add(current);
				}
			}
		}

		// No first node. This can happen, if someone puts a loop into a
		// serviceActivity,
		// where no beginning is specified (Nodes a,b) (Edges a->b, b->a)
		// if no activity nodes exist, it is just an empty branch/path, null
		// will be returned then later
		if (firstNodes.size() == 0 && activityNodes.size() > 0) {
			String nodeNames= UMLNodeUtil.getNodeNames(activityNodes);
			throw new TransformationException("You need to declare one node of [" + nodeNames
					+ "] as start node, or add a specific start node which points to them.");
		}

		// Multiple first nodes...
		if (firstNodes.size() > 1) {
			String nodenames= UMLNodeUtil.getNodeNames(firstNodes);
			String ownerName= UMLNodeUtil.getOwnerName(firstNodes.get(0));
			throw new TransformationException("Too many possible first nodes in " + ownerName + ". Cannot determine where to continue/start.\n"
					+ "Please pick only one of [" + nodenames + "]");
		}

		if (activityNodes.size() == 0) {
			return null;
		} else {
			// one first node... ignore initial node
			ActivityNode firstNode= firstNodes.get(0);
			if (firstNode instanceof InitialNode) {
				InitialNode initialNode= (InitialNode) firstNode;
				if (initialNode.getOutgoings().size() == 1) {
					ActivityNode realFirstNode= initialNode.getOutgoings().get(0).getTarget();
					if (!realFirstNode.getOwner().equals(firstNode.getOwner())) {
						throw new TransformationException("Cannot cross border of service activity " + firstNode.getOwner()
								+ " after an initial node.");
					} else {
						firstNode= realFirstNode;
					}
				}
			}
			return firstNode;
		}
	}

	/**
	 * Gets the one next outgoing target from the given node. It is an error if
	 * there isn't one, or if there are too many.
	 */
	public static ActivityNode getOneNextNode(ActivityNode someNode) throws TransformationException {

		List<ActivityEdge> candidates= new ArrayList<ActivityEdge>(someNode.getOutgoings());
		for (Iterator<ActivityEdge> iterator= candidates.iterator(); iterator.hasNext();) {
			ActivityEdge activityEdge= iterator.next();
			if (StereotypeUtil.hasStereotype(activityEdge, StereoType.COMPENSATIONFLOW)
					|| StereotypeUtil.hasStereotype(activityEdge, StereoType.EVENTFLOW))
				iterator.remove();
		}

		if (candidates.size() > 1)
			throw new TransformationException("Found a normal action with more than one outgoing edge: " + someNode
					+ ". This is not allowed. Please use branches.");
		if (!candidates.isEmpty())
			return candidates.get(0).getTarget();

		return null;
	}

}
