/**
 * File:    EMFHelper.java
 * Created: 19.01.2008
 *
 *
/*******************************************************************************
 * Copyright (c) 2005-2011
 * University of Augsburg, Germany <www.ds-lab.org>
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Christian Saad, Programming distributed Systems Lab, University of Augsburg 
 *    	- initial API and implementation
 *******************************************************************************/

package org.eclipse.jwt.we.misc.util;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import org.eclipse.emf.common.util.BasicEList;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jwt.meta.commands.helper.CommandHelper;
import org.eclipse.jwt.meta.model.core.Model;
import org.eclipse.jwt.meta.model.core.ReferenceableElement;
import org.eclipse.jwt.meta.model.data.Data;
import org.eclipse.jwt.meta.model.processes.Action;
import org.eclipse.jwt.meta.model.processes.Activity;
import org.eclipse.jwt.meta.model.processes.ActivityEdge;
import org.eclipse.jwt.meta.model.processes.ActivityLinkNode;
import org.eclipse.jwt.meta.model.processes.ActivityNode;
import org.eclipse.jwt.meta.model.processes.Scope;
import org.eclipse.jwt.we.PluginProperties;
import org.eclipse.jwt.we.editors.WEEditor;
import org.eclipse.jwt.we.misc.logging.Logger;
import org.eclipse.jwt.we.model.view.Diagram;
import org.eclipse.jwt.we.model.view.EdgeDirection;
import org.eclipse.jwt.we.model.view.LayoutData;
import org.eclipse.jwt.we.model.view.Reference;
import org.eclipse.jwt.we.model.view.ReferenceEdge;
import org.eclipse.jwt.we.model.view.impl.ViewFactoryImpl;
import org.eclipse.osgi.util.NLS;

/**
 * This class contains a set of static functions for handling model objects.
 * 
 * @version $Id: EMFHelper.java,v 1.12 2010-06-30 10:46:33 chsaad Exp $
 * @author Christian Saad (csaad@gmx.net), Programming distributed Systems Lab,
 *         University of Augsburg, Germany, www.ds-lab.org
 */
public class EMFHelper extends CommandHelper
{

	/**
	 * A Logger.
	 */
	private static Logger log = Logger.getLogger(EMFHelper.class);


	/**
	 * Returns the root element of the model.
	 * 
	 * @return The root element.
	 */
	public static Model getRoot(WEEditor editor)
	{
		return (Model) editor.getModel();
	}


	/**
	 * Returns the diagram data of the model.
	 * 
	 * @return The diagram element.
	 */
	public static Diagram getDiagram(WEEditor editor)
	{
		return (Diagram) editor.getDiagramData();
	}


	/**
	 * Checks a given list of Activities and ReferenceableElements on whether
	 * they are still referenced somewhere in the model. A list of the subset of
	 * relevant elements is returned that are still referenced, or with
	 * returnReferences, the references (ActivityLinkNodes, References)
	 * themselves.
	 * 
	 * @param relevantElements
	 *            The elements which should be tested
	 * @param returnReferences
	 *            Returns the references instead of the referenced elements.
	 * @return Collection of ReferenceableElements and Activities which are
	 *         still referenced somewhere.
	 */
	public static Set collectReferencedElements(WEEditor editor, Set relevantElements,
			boolean returnReferences)
	{
		Set result = new HashSet();

		// iterate over all contents to search for activitylinknodes
		for (Iterator iter = getRoot(editor).eAllContents(); iter.hasNext();)
		{
			EObject eobject = (EObject) iter.next();

			// check if referenced activitylinknode->activity should be deleted
			if (eobject instanceof ActivityLinkNode)
			{
				Activity activity = ((ActivityLinkNode) eobject).getLinksto();
				if (relevantElements.contains(activity))
				{
					if (returnReferences)
					{
						result.add(eobject);
					}
					else
					{
						result.add(activity);
					}
				}
			}
		}

		// check all references if their refelem is in the list of relevant
		// elements
		for (Object object : getDiagram(editor).getReferences())
		{
			ReferenceableElement refElem = ((Reference) object).getReference();
			if (relevantElements.contains(refElem))
			{
				if (returnReferences)
				{
					result.add(object);
				}
				else
				{
					result.add(refElem);
				}
			}
		}

		// returns the list of activities/refelems that are still referenced
		return result;
	}


	/**
	 * Unset the referenceableElement features of actions if the are not
	 * connected to the action with a referenceEdge, i.e. if the user did not
	 * select the corresponding Reference to be copied into the clipboard.
	 */
	public static void unsetUnconnectedRefElements(WEEditor editor, Collection objectList)
	{
		for (Iterator iterator = objectList.iterator(); iterator.hasNext();)
		{
			// an object from the clipboard
			EObject eObject = (EObject) iterator.next();

			// if its an action, unset all features which are not in the
			// clipboard
			if (eObject instanceof Action)
			{
				// compile a list of all referenceableelements to which the
				// action is
				// connected through referenceedges
				EList connectedReferenceableElements = new BasicEList();

				for (Iterator iterator2 = getReferenceEdgesForAction(editor,
						((Action) eObject)).iterator(); iterator2.hasNext();)
				{
					ReferenceEdge refEdge = (ReferenceEdge) iterator2.next();

					connectedReferenceableElements.add(refEdge.getReference()
							.getReference());
				}

				// unset all referenceableelements to which the action is not
				// connected
				// through referenceedges
				EList actionEReferences = eObject.eClass().getEReferences();

				for (Iterator iterator2 = actionEReferences.iterator(); iterator2
						.hasNext();)
				{
					EReference reference = (EReference) iterator2.next();

					// single feature
					if (!reference.isMany())
					{
						// the referenced object
						EObject referenceValue = (EObject) eObject.eGet(reference);

						// if the references object is not in the clipboard ->
						// unset
						if (referenceValue instanceof ReferenceableElement
								&& !connectedReferenceableElements
										.contains(referenceValue))
						{
							eObject.eUnset(reference);
						}
					}
					// multiple feature
					else
					{
						// a list of all referenced objects
						EList referenceValues = (EList) eObject.eGet(reference);
						EList removeValues = new BasicEList();

						// collect all referenced objects which are not in the
						// clipboard
						for (Iterator iterator3 = referenceValues.iterator(); iterator3
								.hasNext();)
						{
							Object referenceValue = (Object) iterator3.next();

							if (referenceValue instanceof ReferenceableElement
									&& !connectedReferenceableElements
											.contains(referenceValue))
							{
								removeValues.add(referenceValue);
							}
						}

						// unset missing referenced objects
						referenceValues.removeAll(removeValues);
					}
				}

			}
		}
	}


	/**
	 * This method iterates over the input collection of objects and returns all
	 * ActivityEdges and ReferenceEdges in the model that exist between the
	 * contained elements, even if the edges themselves are not in the
	 * collection.
	 * 
	 * @param affectedObjects
	 * @return The set of edges which exist between the objects.
	 */
	public static Set getAffectedEdges(Collection affectedObjects)
	{
		Set affectedEdges = new HashSet();

		for (Iterator iterator = affectedObjects.iterator(); iterator.hasNext();)
		{
			EObject object = (EObject) iterator.next();

			// Check ActivityNode.In & ActivityNode.Out
			if (object instanceof ActivityNode)
			{
				ActivityNode activityNode = (ActivityNode) object;

				for (Iterator iterator2 = activityNode.getIn().iterator(); iterator2
						.hasNext();)
				{
					ActivityEdge actEdge = (ActivityEdge) iterator2.next();

					if (affectedObjects.contains(actEdge.getSource()))
					{
						affectedEdges.add(actEdge);
					}
				}

				for (Iterator iterator2 = activityNode.getOut().iterator(); iterator2
						.hasNext();)
				{
					ActivityEdge actEdge = (ActivityEdge) iterator2.next();

					if (affectedObjects.contains(actEdge.getTarget()))
					{
						affectedEdges.add(actEdge);
					}
				}
			}

			// Check Reference.referenceEdges
			if (object instanceof Reference)
			{
				Reference reference = (Reference) object;

				for (Iterator iterator2 = reference.getReferenceEdges().iterator(); iterator2
						.hasNext();)
				{
					ReferenceEdge refEdge = (ReferenceEdge) iterator2.next();

					if (affectedObjects.contains(refEdge.getAction()))
					{
						affectedEdges.add(refEdge);
					}
				}
			}
		}

		return affectedEdges;
	}


	/**
	 * Returns all referenceEdges for an action.
	 * 
	 * @param action
	 * @return All referenceEdges for an action.
	 */
	public static EList<ReferenceEdge> getReferenceEdgesForAction(WEEditor editor,
			Action action)
	{
		EList referenceEdges = new BasicEList<ReferenceEdge>();

		for (Object referenceEdge : getDiagram(editor).getReferenceEdges())
		{
			if (((ReferenceEdge) referenceEdge).getAction() != null
					&& ((ReferenceEdge) referenceEdge).getAction().equals(action))
			{
				referenceEdges.add(((ReferenceEdge) referenceEdge));
			}
		}

		return referenceEdges;
	}


	/**
	 * The referenceEdge for an action and a given referenceableElement.
	 * 
	 * @param action
	 * @param refElem
	 * @return referenceEdge for an action and a given referenceableElement.
	 */
	public static ReferenceEdge getReferenceEdgeForAction(WEEditor editor, Action action,
			ReferenceableElement refElem)
	{
		for (Object referenceEdge : getDiagram(editor).getReferenceEdges())
		{
			if (referenceEdge != null
					&& ((ReferenceEdge) referenceEdge).getAction() != null
					&& ((ReferenceEdge) referenceEdge).getAction().equals(action)
					&& ((ReferenceEdge) referenceEdge).getReference().getReference() == refElem)
			{
				return (ReferenceEdge) referenceEdge;
			}
		}

		return null;
	}


	/**
	 * Returns all referenceEdges in a scope.
	 * 
	 * @param scope
	 * @return All referenceEdges in a scope.
	 */
	public static EList<ReferenceEdge> getReferenceEdgesForScope(WEEditor editor,
			Scope scope)
	{
		EList referenceEdges = new BasicEList<ReferenceEdge>();

		for (Object referenceEdge : getDiagram(editor).getReferenceEdges())
		{
			if (((ReferenceEdge) referenceEdge).getContainedIn() != null
					&& ((ReferenceEdge) referenceEdge).getContainedIn().equals(scope))
			{
				referenceEdges.add(((ReferenceEdge) referenceEdge));
			}
		}

		return referenceEdges;
	}


	/**
	 * The referenceEdge for a scope and a given referenceableElement.
	 * 
	 * @param scope
	 * @return referenceEdge for a scope and a given referenceableElement.
	 */
	public static ReferenceEdge getReferenceEdgeForScope(WEEditor editor, Scope scope,
			ReferenceableElement refElem)
	{
		for (Object referenceEdge : getDiagram(editor).getReferenceEdges())
		{
			if (((ReferenceEdge) referenceEdge).getContainedIn().equals(scope)
					&& ((ReferenceEdge) referenceEdge).getReference().getReference() == refElem)
			{
				return (ReferenceEdge) referenceEdge;
			}
		}

		return null;
	}


	/**
	 * Returns all references in a scope.
	 * 
	 * @param scope
	 * @return All references in a scope.
	 */
	public static EList<Reference> getReferencesForScope(WEEditor editor, Scope scope)
	{
		EList references = new BasicEList<Reference>();

		for (Object reference : getDiagram(editor).getReferences())
		{
			if (((Reference) reference).getContainedIn() != null
					&& ((Reference) reference).getContainedIn().equals(scope))
			{
				references.add(((Reference) reference));
			}
		}

		return references;
	}


	/**
	 * The referenceEdge for a scope and a given referenceableElement.
	 * 
	 * @param scope
	 * @param refElem
	 * @return referenceEdge for a scope and a given referenceableElement.
	 */
	public static Reference getReferenceForScope(WEEditor editor, Scope scope,
			ReferenceableElement refElem)
	{
		for (Object reference : getDiagram(editor).getReferences())
		{
			if (((Reference) reference).getContainedIn() != null
					&& ((Reference) reference).getContainedIn().equals(scope)
					&& ((Reference) reference).getReference() == refElem)
			{
				return (Reference) reference;
			}
		}

		return null;
	}


	/**
	 * Returns all contained referenceedges recursively.
	 * 
	 * @param collection
	 * @return The contained referenceedges.
	 */
	public static Collection getAllContainedReferenceEdges(WEEditor editor,
			Collection collection)
	{
		Collection result = new ArrayList<LayoutData>();
		for (Object object : collection)
		{
			if (object instanceof Scope)
			{
				result.addAll(EMFHelper.getReferenceEdgesForScope(editor, (Scope) object));
			}
			if (object instanceof EObject && ((EObject) object).eContents() != null)
			{
				result.addAll(getAllContainedReferenceEdges(editor,
						((EObject) object).eContents()));
			}
		}

		return result;
	}


	/**
	 * Returns all contained references recursively.
	 * 
	 * @param collection
	 * @return The contained references.
	 */
	public static Collection getAllContainedReferences(WEEditor editor,
			Collection collection)
	{
		Collection result = new ArrayList<LayoutData>();
		for (Object object : collection)
		{
			if (object instanceof Scope)
			{
				result.addAll(EMFHelper.getReferencesForScope(editor, (Scope) object));
			}
			if (object instanceof EObject && ((EObject) object).eContents() != null)
			{
				result.addAll(getAllContainedReferences(editor,
						((EObject) object).eContents()));
			}
		}

		return result;
	}


	/**
	 * A "garbage collection" that removes unused view data (layoutdata,
	 * references, referenceedges) and should be run each time before saving a
	 * workflow file.
	 */
	public static void cleanViewData(WEEditor editor)
	{
		log.debug("running garbage collection to remove unneccesary view data"); //$NON-NLS-1$

		try
		{
			// the view datas
			Object[] layoutDatas = editor.getDiagramData().getLayoutData().toArray();
			Object[] references = editor.getDiagramData().getReferences().toArray();
			Object[] referenceEdges = editor.getDiagramData().getReferenceEdges()
					.toArray();

			// create a list with all model elements
			BasicEList allModelElements = new BasicEList();
			for (Iterator iter = editor.getMainModelResource().getAllContents(); iter
					.hasNext();)
			{
				allModelElements.add((EObject) iter.next());
			}

			// remove all referenceEdges which are not referenced
			for (Object object : referenceEdges)
			{
				ReferenceEdge refEdge = (ReferenceEdge) object;

				if (refEdge.getContainedIn() == null
						|| !allModelElements.contains(refEdge.getContainedIn())
						|| refEdge.getAction() == null || refEdge.getReference() == null)
				{
					refEdge.setAction(null);
					refEdge.setReference(null);
					refEdge.setContainedIn(null);
					editor.getDiagramData().getLayoutData().remove(refEdge);
				}
			}

			// remove all references which are not referenced
			for (Object object : references)
			{
				Reference ref = (Reference) object;

				if (ref.getContainedIn() == null
						|| !allModelElements.contains(ref.getContainedIn())
						|| ref.getReference() == null)
				{
					ref.setReference(null);
					ref.setContainedIn(null);
					editor.getDiagramData().getLayoutData().remove(ref);
				}
			}

			// remove all layoutdata which are not referenced
			for (Object object : layoutDatas)
			{
				LayoutData ld = (LayoutData) object;
				EObject target = ld.getDescribesElement();

				if (ld.getDescribesElement() == null
						|| (!allModelElements.contains(target) && !editor
								.getDiagramData().getReferences().contains(target)))
				{
					ld.setDescribesElement(null);
					editor.getDiagramData().getLayoutData().remove(ld);
				}
			}

			// dispose
			layoutDatas = null;
			references = null;
			referenceEdges = null;
			allModelElements.clear();
			allModelElements = null;

		}
		catch (Exception e)
		{
			log.severe("error running view data garbage collection"); //$NON-NLS-1$
			e.printStackTrace();
		}
	}


	/**
	 * Ensures that References/ReferenceEdges exist only if the corresponding
	 * ReferenceableElements are stored in an Action. While "cleanViewData"
	 * removes dangling view elements, this methods ensures that the model
	 * integrity concerning References/ReferenceEdges is preserved. Also
	 * dangling ReferenceableElements are removed.
	 */
	public static void correctReferences(WEEditor editor, boolean currentlySaving)
	{
		log.debug("running consistency check for references/referenceedges"); //$NON-NLS-1$

		try
		{
			Boolean corrected = false;

			// create a list with all model elements
			BasicEList allModelElements = new BasicEList();
			for (Iterator iter = editor.getMainModelResource().getAllContents(); iter
					.hasNext();)
			{
				allModelElements.add((EObject) iter.next());
			}

			// create a list with all actions
			EList<Action> allActions = new BasicEList<Action>();
			for (Iterator iter = editor.getMainModelResource().getAllContents(); iter
					.hasNext();)
			{
				EObject object = (EObject) iter.next();
				if (object instanceof Action)
				{
					allActions.add((Action) object);
				}
			}

			// for all actions
			for (Action action : allActions)
			{
				// all referenceableelements for this action
				EList<ReferenceableElement> referenceableElements = new BasicEList<ReferenceableElement>();

				for (Iterator iter = action.eCrossReferences().iterator(); iter.hasNext();)
				{
					Object object = (Object) iter.next();
					if (object instanceof ReferenceableElement)
					{
						referenceableElements.add((ReferenceableElement) object);
					}
				}

				// for all refelem of this action -> check if refedges/refs
				// exist
				for (ReferenceableElement refElem : referenceableElements)
				{
					// refelem not found in model
					if (!allModelElements.contains(refElem))
					{
						if (action.getExecutedBy() == refElem)
						{
							action.setExecutedBy(null);
						}
						else if (action.getPerformedBy() == refElem)
						{
							action.setPerformedBy(null);
						}
						else if (action.getInputs().contains(refElem))
						{
							action.getInputs().remove(refElem);
						}
						else if (action.getOutputs().contains(refElem))
						{
							action.getOutputs().remove(refElem);
						}

						corrected = true;
						continue;
					}

					// look for referenceedge
					ReferenceEdge refEdge = getReferenceEdgeForAction(editor, action,
							refElem);

					// if refedge was found, there is no error here
					if (refEdge != null)
					{
						continue;
					}

					// now we're in trouble
					corrected = true;

					// create new reference and referenceedge
					Reference newReference = ViewFactoryImpl.eINSTANCE.createReference();
					ReferenceEdge newReferenceEdge = ViewFactoryImpl.eINSTANCE
							.createReferenceEdge();
					editor.getDiagramData().getReferences().add(newReference);
					editor.getDiagramData().getReferenceEdges().add(newReferenceEdge);

					// set attributes
					newReference.setContainedIn((Scope) action.eContainer());
					newReference.setReference(refElem);
					newReferenceEdge.setAction(action);
					newReferenceEdge.setContainedIn((Scope) action.eContainer());
					newReferenceEdge.setReference(newReference);
					EdgeDirection edgeDirection = EdgeDirection.NONE;
					if (refElem instanceof Data)
					{
						if (action.getInputs().contains(refElem)
								&& action.getOutputs().contains(refElem))
						{
							edgeDirection = EdgeDirection.INOUT;
						}
						else if (action.getInputs().contains(refElem))
						{
							edgeDirection = EdgeDirection.IN;
						}
						else if (action.getOutputs().contains(refElem))
						{
							edgeDirection = EdgeDirection.OUT;
						}
					}
					newReferenceEdge.setDirection(edgeDirection);
				}
			}

			// show message and save
			if (corrected)
			{
				boolean save = true;

				if (!GeneralHelper.isjUnitMode())
				{
					if (currentlySaving)
					{
						save = false;
						MessageDialog.openInformation(editor.getEditorSite().getShell(),
								PluginProperties.editor_ErrorMessage_title, NLS.bind(
										PluginProperties.editor_ErrorReferences2_message,
										editor.getEditorInput().getName()));
					}
					else
					{
						save = MessageDialog.openQuestion(editor.getEditorSite()
								.getShell(), PluginProperties.editor_ErrorMessage_title,
								NLS.bind(PluginProperties.editor_ErrorReferences_message,
										editor.getEditorInput().getName()));
					}
				}

				if (save)
				{
					editor.doSave(null);
				}
			}

			// dispose
			allActions.clear();
			allActions = null;
		}
		catch (Exception e)
		{
			log.severe("error running consistency check for references/referenceedges"); //$NON-NLS-1$
			e.printStackTrace();
		}
	}
}