/**
 * File:    CreateReferenceEdgeCommand.java
 * Created: 09.06.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.commands.view;

import java.util.Collections;
import java.util.Iterator;

import org.eclipse.emf.common.command.AbstractCommand;
import org.eclipse.emf.common.command.Command;
import org.eclipse.emf.common.command.CompoundCommand;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.edit.command.AddCommand;
import org.eclipse.emf.edit.command.DeleteCommand;
import org.eclipse.emf.edit.command.SetCommand;
import org.eclipse.emf.edit.domain.EditingDomain;
import org.eclipse.jwt.meta.model.application.Application;
import org.eclipse.jwt.meta.model.core.ReferenceableElement;
import org.eclipse.jwt.meta.model.data.Data;
import org.eclipse.jwt.meta.model.organisations.Role;
import org.eclipse.jwt.meta.model.processes.Action;
import org.eclipse.jwt.meta.model.processes.ProcessesPackage;
import org.eclipse.jwt.meta.model.processes.provider.ActionItemProvider;
import org.eclipse.jwt.we.editors.WEEditor;
import org.eclipse.jwt.we.misc.util.EMFHelper;
import org.eclipse.jwt.we.model.view.EdgeDirection;
import org.eclipse.jwt.we.model.view.Reference;
import org.eclipse.jwt.we.model.view.ReferenceEdge;
import org.eclipse.jwt.we.model.view.ViewFactory;
import org.eclipse.jwt.we.model.view.ViewPackage;

/**
 * This command creates a ReferenceEdge between an Action and a Reference. It
 * also sets the corresponding feature of the Action (PerformedBy, ExecutedBy,
 * Data In/Out).
 * 
 * If the Reference is of the type Data, it sets the corresponding direction or
 * extends an existing IN or OUT edge to the type INOUT.
 * 
 * Since this is a very complex process, it is STRONGLY DISADVISED to externally
 * create ReferenceEdges, which is why the {@link ActionItemProvider} intercepts
 * commands to change PerformedBy, ExecutedBy and Data In/Out features and
 * instead redirects to this command.
 * 
 * @version $Id: CreateReferenceEdgeCommand.java,v 1.2 2009/11/04 17:18:38
 *          chsaad Exp $
 * @author Christian Saad (csaad@gmx.net), Programming distributed Systems Lab,
 *         University of Augsburg, Germany, www.ds-lab.org
 */
public class CreateReferenceEdgeCommand extends AbstractCommand
{

	/**
	 * The workflow editor.
	 */
	private WEEditor weeditor;

	/**
	 * The EMF EditingDomain.
	 */
	private EditingDomain editingDomain;

	/**
	 * The original source endpoint.
	 */
	private Object conSource;

	/**
	 * The original target endpoint.
	 */
	private Object conTarget;

	/**
	 * The direction of the connection (0: target action, 1: target reference).
	 */
	private int direction;

	/**
	 * EMF command to remove the previous entry of the action.
	 */
	private Command removePreviousEntryCommand;

	/**
	 * EMF command to set the feature of the Action.
	 */
	private Command setFeatureCommand;

	/**
	 * EMF command to add the reference edge to the scope.
	 */
	private CompoundCommand addReferenceEdgeCommand;

	/**
	 * Indicates whether the canExecute method should be bypassed.
	 */
	private boolean skipCanExecute = false;


	/**
	 * @param editingDomain
	 *            The EMF EditingDomain.
	 * @param edge
	 *            The edge to reconnect.
	 */
	public CreateReferenceEdgeCommand(WEEditor weeditor, EditingDomain editingDomain)
	{
		super();
		this.editingDomain = editingDomain;
		this.weeditor = weeditor;
	}


	/**
	 * If this value is set to true, the canExecute test of the command is
	 * skipped. This may be necessary, if this command is part of a
	 * CompoundCommand and requires that another command must be executed in
	 * order for the canExecute check to return true.
	 * 
	 * @param skipCanExecute
	 *            The skipCanExecute to set.
	 */
	public void setSkipCanExecute(boolean skipCanExecute)
	{
		this.skipCanExecute = skipCanExecute;
	}


	/**
	 * Sets the connection source.
	 * 
	 * @param source
	 */
	public void setSource(Object source)
	{
		conSource = source;
	}


	/**
	 * Sets the connection target.
	 * 
	 * @param source
	 */
	public void setTarget(Object target)
	{
		conTarget = target;
	}


	/**
	 * Sets the direction of the connection
	 * 
	 * 0: from reference to action (input)
	 * 
	 * 1: from action to reference (output)
	 */
	public void setDirection(int direc)
	{
		direction = direc;
	}


	/**
	 * The Action.
	 * 
	 * @return The action.
	 */
	private Action getAction()
	{
		if (conSource instanceof Action)
		{
			return (Action) conSource;
		}
		else if (conTarget instanceof Action)
		{
			return (Action) conTarget;
		}
		else
		{
			return null;
		}
	}


	/**
	 * The Reference
	 * 
	 * @return the reference.
	 */
	private Reference getReference()
	{
		if (conSource instanceof Reference)
		{
			return (Reference) conSource;
		}
		else if (conTarget instanceof Reference)
		{
			return (Reference) conTarget;
		}
		else
		{
			return null;
		}
	}


	/**
	 * The ReferenceableElement of the Reference.
	 * 
	 * @return The referenceable element.
	 */
	private ReferenceableElement getRefElem()
	{
		if (getReference() != null)
		{
			return getReference().getReference();
		}
		else
		{
			return null;
		}
	}


	/**
	 * Checks if the connection is valid.
	 * 
	 * @return If the connection is valid.
	 */
	public boolean checkConnection()
	{
		// if action or reference is missing -> abort
		if (getAction() == null || getReference() == null)
		{
			return false;
		}

		// if the target reference is of the type APPLICATION or ROLE
		if (getRefElem() instanceof Application || getRefElem() instanceof Role)
		{
			// if an refEdge to the SAME reference already exists -> abort
			for (Iterator iterator = getReference().getReferenceEdges().iterator(); iterator
					.hasNext();)
			{
				ReferenceEdge refEdge = (ReferenceEdge) iterator.next();

				if (refEdge.getAction().equals(getAction())
						&& refEdge.getReference().equals(getReference()))
				{
					return false;
				}
			}
		}

		// if the target reference is of the type DATA
		if (getRefElem() instanceof Data)
		{
			// checks if the element is already present in the action
			boolean inputPresent = getAction().getInputs().contains(getRefElem());
			boolean outputPresent = getAction().getOutputs().contains(getRefElem());

			// if an input/output edge already exists
			if (inputPresent && outputPresent)
				return false;

			// if an input edge already exists and the direction is input
			if (inputPresent && direction == 0)
				return false;

			// if an output edge already exists and the direction is output
			if (outputPresent && direction == 1)
				return false;

			// if an input edge is present and an output edge should be created,
			// then
			// the existing edge must instead be converted from input to in/out.
			// however, this requires that the action/reference of the existing
			// edge
			// are the same as for the new edge
			if ((inputPresent && direction == 1) || (outputPresent && direction == 0))
			{
				for (Iterator i = EMFHelper.getReferenceEdgesForAction(weeditor,
						getAction()).iterator(); i.hasNext();)
				{
					ReferenceEdge edge = (ReferenceEdge) i.next();
					if (edge.getReference().getReference() == getRefElem())
					{
						if (edge.getAction() != getAction()
								|| edge.getReference() != getReference())
						{
							return false;
						}
					}
				}
			}

		}

		return true;
	}


	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.emf.common.command.AbstractCommand#canExecute()
	 */
	@Override
	public boolean canExecute()
	{
		// skip check?
		if (skipCanExecute)
		{
			return true;
		}

		if (editingDomain == null)
		{
			return false;
		}

		// check if the connection is valid
		return checkConnection();
	}


	/**
	 * Returns the direction of the edge. If the refElem type is Application or
	 * Role, the direction is DEFAULT. If the type is Data, the direction
	 * (IN/OUT/INOUT) of an existing edge to the refElem is returned. If no edge
	 * exists, it also returns DEFAULT.
	 * 
	 * @return The direction of an existing edge to the referenceable element.
	 */
	private EdgeDirection getDirectionOfExistingEdge()
	{
		if (getRefElem() instanceof Data)
		{
			boolean input = getAction().getInputs().contains(getRefElem());
			boolean output = getAction().getOutputs().contains(getRefElem());

			// if input and output edge already exist
			if (input && output)
			{
				return EdgeDirection.INOUT;
			}
			// if input edge already exists
			else if (input)
			{
				return EdgeDirection.IN;
			}
			// if output edge already exists
			else if (output)
			{
				return EdgeDirection.OUT;
			}
		}

		// if type is Application or Role or no Data edge already exists
		return EdgeDirection.DEFAULT;
	}


	/**
	 * Searches the action for the ReferenceEdge which connects to the given
	 * ReferenceableElement.
	 * 
	 * @param refElem
	 * @return Reference edge.
	 */
	private ReferenceEdge findRefEdge(ReferenceableElement refElem)
	{
		// look for a referenceEdge to the given element
		for (Iterator iterator = EMFHelper.getReferenceEdgesForAction(weeditor,
				getAction()).iterator(); iterator.hasNext();)
		{
			ReferenceEdge refEdge = (ReferenceEdge) iterator.next();

			if (refEdge.getReference().getReference() == refElem)
			{
				return refEdge;
			}
		}

		return null;
	}


	/**
	 * Returns the feature slot, in which the referenceable element should be
	 * stored.
	 * 
	 * @return The feature.
	 */
	private EStructuralFeature getFeature()
	{
		EStructuralFeature feature = null;
		if (getRefElem() instanceof Application)
		{
			feature = ProcessesPackage.eINSTANCE.getAction_ExecutedBy();
		}
		else if (getRefElem() instanceof Role)
		{
			feature = ProcessesPackage.eINSTANCE.getAction_PerformedBy();
		}
		else if (getRefElem() instanceof Data && direction == 0)
		{
			feature = ProcessesPackage.Literals.ACTION__INPUTS;
		}
		else if (getRefElem() instanceof Data && direction == 1)
		{
			feature = ProcessesPackage.Literals.ACTION__OUTPUTS;
		}
		return feature;
	}


	/**
	 * Create a command, that sets the properties of a reference edge and adds
	 * it to the scope.
	 * 
	 * @param existingInputEdge
	 * @param existingOutputEdge
	 * @param feature
	 * @return The command to create the reference edge.
	 */
	private CompoundCommand createReferenceEdgeCommand(boolean existingInputEdge,
			boolean existingOutputEdge, EStructuralFeature feature)
	{
		ReferenceEdge refEdge = ViewFactory.eINSTANCE.createReferenceEdge();
		EdgeDirection dir = null;

		if (!(getRefElem() instanceof Data))
		{
			dir = EdgeDirection.DEFAULT;
		}
		else
		{
			// if the existing and new edge are IN and OUT -> INOUT
			if ((existingInputEdge && feature == ProcessesPackage.Literals.ACTION__OUTPUTS)
					|| (existingOutputEdge && feature == ProcessesPackage.Literals.ACTION__INPUTS))
			{
				dir = EdgeDirection.INOUT;
			}
			// no previous edge was found, the new edge is IN -> IN
			else if (feature == ProcessesPackage.Literals.ACTION__INPUTS)
			{
				dir = EdgeDirection.IN;
			}
			// no previous edge was found, the new edge is OUT -> OUT
			else if (feature == ProcessesPackage.Literals.ACTION__OUTPUTS)
			{
				dir = EdgeDirection.OUT;
			}
		}

		// create the add command
		CompoundCommand addRefEdgeCommand = new CompoundCommand();

		addRefEdgeCommand = new CompoundCommand();
		addRefEdgeCommand.append(SetCommand.create(editingDomain, refEdge,
				ViewPackage.Literals.REFERENCE_EDGE__REFERENCE, getReference()));
		addRefEdgeCommand.append(SetCommand.create(editingDomain, refEdge,
				ViewPackage.Literals.REFERENCE_EDGE__ACTION, getAction()));
		addRefEdgeCommand.append(SetCommand.create(editingDomain, refEdge,
				ViewPackage.Literals.REFERENCE_EDGE__DIRECTION, dir));
		addRefEdgeCommand.append(SetCommand.create(editingDomain, refEdge,
				ViewPackage.Literals.REFERENCE_EDGE__CONTAINED_IN, getAction()
						.eContainer()));
		addRefEdgeCommand.append(AddCommand.create(editingDomain, weeditor
				.getDiagramData(), ViewPackage.Literals.DIAGRAM__REFERENCE_EDGES,
				Collections.singleton(refEdge)));

		return addRefEdgeCommand;
	}


	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.emf.common.command.Command#execute()
	 */
	public void execute()
	{
		setFeatureCommand = null;
		removePreviousEntryCommand = null;
		addReferenceEdgeCommand = null;

		// check if an Data IN or OUT edge to the reference already exists
		boolean existingInputEdge = getDirectionOfExistingEdge() == EdgeDirection.IN;
		boolean existingOutputEdge = getDirectionOfExistingEdge() == EdgeDirection.OUT;

		// get the structural feature type for the refElem
		EStructuralFeature feature = getFeature();

		// if the the ExecutedBy or PerformedBy Slot is already occupied by any
		// other
		// RefElem, create a command that will remove the corresponding
		// reference edge
		if ((feature == ProcessesPackage.eINSTANCE.getAction_ExecutedBy() || feature == ProcessesPackage.eINSTANCE
				.getAction_PerformedBy())
				&& getAction().eGet(feature) != null)
		{
			removePreviousEntryCommand = new DeleteCommand(editingDomain, Collections
					.singleton(findRefEdge((ReferenceableElement) getAction().eGet(
							feature))));
		}

		// if the the Input or Output Slot already contains this RefElem (= an
		// INOUT Edge
		// should be created), create a command to remove the old IN or OUT edge
		if (existingInputEdge || existingOutputEdge)
		{
			removePreviousEntryCommand = new DeleteCommand(editingDomain, Collections
					.singleton(findRefEdge(getRefElem())));
		}

		// IMPORTANT: The command to set the feature is NOT created via the
		// ActionItemProvider, because the ActionItemProvider handles the
		// creation of
		// References using the properties view (which would result in a
		// recursive call).
		// Instead a generic EMF command add/set-command is used.
		if (feature.isMany())
		{
			if (existingInputEdge || existingOutputEdge)
			{
				// if an entry already exists it will be deleted along with the
				// edge by
				// removePreviousEntryCommand, therefore we add it again
				setFeatureCommand = new AddCommand(editingDomain, getAction(),
						ProcessesPackage.Literals.ACTION__INPUTS, getRefElem());
				setFeatureCommand = setFeatureCommand.chain(new AddCommand(editingDomain,
						getAction(), ProcessesPackage.Literals.ACTION__OUTPUTS,
						getRefElem()));
			}
			else
			{
				setFeatureCommand = new AddCommand(editingDomain, getAction(), feature,
						getRefElem());
			}
		}
		else
		{
			setFeatureCommand = new SetCommand(editingDomain, getAction(), feature,
					getRefElem());
		}

		// create the command(s) to add the reference edge
		addReferenceEdgeCommand = createReferenceEdgeCommand(existingInputEdge,
				existingOutputEdge, feature);

		// check if the commands can be executed
		boolean canExecute = addReferenceEdgeCommand.canExecute();

		if (removePreviousEntryCommand != null)
		{
			canExecute = canExecute && removePreviousEntryCommand.canExecute();
		}

		// execute the commands, if possible
		if (canExecute)
		{
			// remove previous edge
			if (removePreviousEntryCommand != null)
			{
				removePreviousEntryCommand.execute();
			}

			// check if set feature can be executed (because of delayed in/out
			// it it may
			// not work earlier)
			if (setFeatureCommand.canExecute())
			{
				// add feature and remove existing edge if necessary
				setFeatureCommand.execute();

				// create the reference edge
				addReferenceEdgeCommand.execute();
			}
			else
			{
				// undo
				if (removePreviousEntryCommand != null)
				{
					removePreviousEntryCommand.undo();
				}
			}
		}
	}


	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.emf.common.command.AbstractCommand#undo()
	 */
	@Override
	public void undo()
	{
		// undo the reference edge
		addReferenceEdgeCommand.undo();

		// undo the feature of the action
		setFeatureCommand.undo();

		// restore the old edge
		if (removePreviousEntryCommand != null)
		{
			removePreviousEntryCommand.undo();
		}
	}


	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.emf.common.command.Command#redo()
	 */
	public void redo()
	{
		// remove existing entry in action
		if (removePreviousEntryCommand != null)
		{
			removePreviousEntryCommand.redo();
		}

		// set new feature in action
		setFeatureCommand.redo();

		// redo the reference edge
		addReferenceEdgeCommand.redo();
	}


	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.emf.common.command.AbstractCommand#dispose()
	 */
	@Override
	public void dispose()
	{
		super.dispose();

		if (removePreviousEntryCommand != null)
		{
			removePreviousEntryCommand.dispose();
		}

		if (setFeatureCommand != null)
		{
			setFeatureCommand.dispose();
		}

		if (addReferenceEdgeCommand != null)
		{
			addReferenceEdgeCommand.dispose();
		}
	}

}