/**
 * File:    CreateLayoutDataCommand.java
 * Created: 28.12.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.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.logging.Level;

import org.eclipse.draw2d.geometry.Point;
import org.eclipse.gef.commands.Command;
import org.eclipse.jwt.meta.model.core.GraphicalElement;
import org.eclipse.jwt.meta.model.processes.Action;
import org.eclipse.jwt.meta.model.processes.ActivityEdge;
import org.eclipse.jwt.meta.model.processes.ActivityNode;
import org.eclipse.jwt.meta.model.processes.Scope;
import org.eclipse.jwt.meta.model.processes.StructuredActivityNode;
import org.eclipse.jwt.we.editors.WEEditor;
import org.eclipse.jwt.we.misc.layouting.CustomDirectedGraphLayoutAlgorithm;
import org.eclipse.jwt.we.misc.layouting.EdgeRelationship;
import org.eclipse.jwt.we.misc.layouting.GraphicalElementEntity;
import org.eclipse.jwt.we.misc.logging.Logger;
import org.eclipse.jwt.we.misc.util.EMFHelper;
import org.eclipse.jwt.we.misc.views.LayoutDataManager;
import org.eclipse.jwt.we.misc.views.Views;
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.zest.layouts.LayoutAlgorithm;
import org.eclipse.zest.layouts.LayoutEntity;
import org.eclipse.zest.layouts.LayoutRelationship;
import org.eclipse.zest.layouts.LayoutStyles;
import org.eclipse.zest.layouts.algorithms.AbstractLayoutAlgorithm;
import org.eclipse.zest.layouts.algorithms.CompositeLayoutAlgorithm;

/**
 * A command to create layout data using a layouting algorithm.
 * 
 * @version $Id: CreateLayoutDataCommand.java,v 1.2 2009/11/04 17:18:38 chsaad
 *          Exp $
 * @author Christian Saad, Programming distributed Systems Lab, University of
 *         Augsburg, Germany, www.ds-lab.org
 */
public class CreateLayoutDataCommand extends Command
{

	/**
	 * A Logger.
	 */
	private static final Logger logger = Logger.getLogger(CreateLayoutDataCommand.class);

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

	/**
	 * The table to store copies of the original layoutdata and the target
	 * layout data.
	 */
	private HashMap layoutData;

	/**
	 * The layouting algorithm class.
	 */
	private Class layoutClass;

	/**
	 * The selected activities.
	 */
	private Set<Scope> selectedScopes;


	/**
	 * Creates a new command for changing constraints of a element.
	 * 
	 * @param layoutClass
	 *            The layouting algorithm.
	 */
	public CreateLayoutDataCommand(WEEditor weeditor, Set<Scope> selectedActivities,
			Class layoutClass)
	{
		this.layoutClass = layoutClass;
		this.selectedScopes = selectedActivities;
		this.weeditor = weeditor;
	}


	/**
	 * Looks for the entity corresponding to the given graphical element in the
	 * collection and returns it. If it is not found, than a new entity is
	 * created and returned.
	 * 
	 * @param entities
	 * @param graphicalElement
	 * @return The entity.
	 */
	private GraphicalElementEntity findEntityForElement(
			ArrayList<GraphicalElementEntity> entities, GraphicalElement graphicalElement)
	{
		// if object is not displayed, exit
		if (graphicalElement == null
				|| !Views.getInstance().displayObject(graphicalElement))
		{
			return null;
		}

		// search for entity for given graphical element
		GraphicalElementEntity foundEntity = null;

		for (GraphicalElementEntity entity : entities)
		{
			if (entity.getGraphicalElement() == graphicalElement)
			{
				foundEntity = entity;
				break;
			}
		}

		// if entity was not found, create new and add to list
		if (foundEntity == null)
		{
			foundEntity = new GraphicalElementEntity(weeditor);
			foundEntity.setGraphicalElement(graphicalElement);
			entities.add(foundEntity);
		}

		return foundEntity;
	}


	/**
	 * Layout the given scope using the selected layouting algorithm.
	 * 
	 * @param scope
	 *            The scope.
	 */
	private void layoutScope(Scope scope)
	{
		ArrayList<GraphicalElementEntity> entities = new ArrayList<GraphicalElementEntity>();
		ArrayList<EdgeRelationship> relationships = new ArrayList<EdgeRelationship>();

		// create relationship objects for the activityedges
		for (Object object : scope.getEdges())
		{
			ActivityNode sourceElement = ((ActivityEdge) object).getSource();
			ActivityNode targetElement = ((ActivityEdge) object).getTarget();

			// look for entity corresponding to the edge's source and target (or
			// create one if not found)
			GraphicalElementEntity sourceEntity = findEntityForElement(entities,
					sourceElement);
			GraphicalElementEntity targetEntity = findEntityForElement(entities,
					targetElement);

			// if source and target entities exist...
			if (sourceEntity != null && targetEntity != null)
			{
				// ...create and add the relationship
				EdgeRelationship relationship = new EdgeRelationship(sourceEntity,
						targetEntity);
				relationships.add(relationship);
			}
		}

		// create relationship objects for the referenceedges
		for (ReferenceEdge refEdge : EMFHelper.getReferenceEdgesForScope(weeditor, scope))
		{
			Action sourceAction = refEdge.getAction();
			Reference targetReference = refEdge.getReference();

			// look for entity corresponding to the edge's source and target (or
			// create one if not found)
			GraphicalElementEntity sourceEntity = findEntityForElement(entities,
					sourceAction);
			GraphicalElementEntity targetEntity = findEntityForElement(entities,
					targetReference);

			// if source and target entities exist...
			if (sourceEntity != null && targetEntity != null)
			{
				// ...create and add the relationship
				EdgeRelationship relationship = new EdgeRelationship(sourceEntity,
						targetEntity);
				relationships.add(relationship);
			}
		}

		// add graphicalelements which are not connected through edges
		// note: bad for spring layouting algorithm!
		/*
		for (Object scopeChild : scope.getNodes())
		{
			if (!(scopeChild instanceof GraphicalElement))
			{
				continue;
			}

			boolean found = false;
			for (GraphicalElementEntity entity : entities)
			{
				if (entity.getGraphicalElement() == scopeChild)
				{
					found = true;
					break;
				}

			}
			if (!found)
			{
				GraphicalElementEntity gee = new GraphicalElementEntity(weeditor);
				gee.setGraphicalElement((GraphicalElement) scopeChild);
				entities.add(gee);
			}
		}
*/
		// prepare entity and relationship lists
		LayoutEntity[] layoutEntities = new LayoutEntity[entities.size()];
		LayoutRelationship[] layoutRelationships = new LayoutRelationship[relationships
				.size()];
		entities.toArray(layoutEntities);
		relationships.toArray(layoutRelationships);

		// put all current layout data in hashtable so it can be restored by
		// undo
		for (GraphicalElementEntity entity : entities)
		{
			// SAN info was already computed and set
			if (entity.getGraphicalElement() instanceof StructuredActivityNode)
			{
				continue;
			}

			// the layout data as it is now
			LayoutData before = LayoutDataManager.getLayoutDataForView(weeditor, entity
					.getGraphicalElement(), Views.getInstance().getSelectedView()
					.getInternalName());
			LayoutData after = ViewFactoryImpl.eINSTANCE.createLayoutData();
			LayoutDataManager.transferLayoutDataValues(before, after);

			// put the layout data in the table
			layoutData.put(before, after);
		}

		try
		{
			// instantiate layout algorithm
			AbstractLayoutAlgorithm selected_algorithm = (AbstractLayoutAlgorithm) layoutClass
					.getConstructor(new Class[]
					{ int.class }).newInstance(LayoutStyles.NO_LAYOUT_NODE_RESIZING);

			// combine with directed graph layout
			AbstractLayoutAlgorithm algorithm = new CompositeLayoutAlgorithm(
					LayoutStyles.NO_LAYOUT_NODE_RESIZING, new LayoutAlgorithm[]
					{
							new CustomDirectedGraphLayoutAlgorithm(
									LayoutStyles.NO_LAYOUT_NODE_RESIZING),
							selected_algorithm });

			// the start for the target size
			int x_size = 200;
			int y_size = 100;

			int layout_iterations = 1;
			int max_iterations = 10;

			// repeat layouting until max_iterations has been reached or
			// distance between
			// elements is satisfactory
			while (layout_iterations < max_iterations)
			{
				// apply layout algorithm
				algorithm.applyLayout(layoutEntities, layoutRelationships, 0, 0, x_size,
						y_size, false, false);

				// check wheter elements are too close together
				boolean problem_found = false;
				for (LayoutRelationship layoutRelationship : layoutRelationships)
				{
					LayoutEntity sourceEntity = layoutRelationship.getSourceInLayout();
					LayoutEntity targetEntity = layoutRelationship
							.getDestinationInLayout();

					// the actual x/y distances between source and target
					double x_dist = Math.abs((sourceEntity.getXInLayout() + sourceEntity
							.getWidthInLayout() / 2)
							- (targetEntity.getXInLayout() + targetEntity
									.getWidthInLayout() / 2));

					double y_dist = Math.abs((sourceEntity.getYInLayout() + sourceEntity
							.getHeightInLayout() / 2)
							- (targetEntity.getYInLayout() + targetEntity
									.getHeightInLayout() / 2));

					// the desired minimal x/y distances between source and
					// target
					double x_mindist = 50 + sourceEntity.getWidthInLayout() / 2
							+ targetEntity.getWidthInLayout() / 2;
					double y_mindist = 30 + sourceEntity.getHeightInLayout() / 2
							+ targetEntity.getHeightInLayout() / 2;

					// if either x or y minimal distance is too small, increase
					// size and try again
					if (x_dist < x_mindist && y_dist < y_mindist)
					{
						layout_iterations++;

						// increase size in the direction that has already a
						// bigger distance
						if (x_dist > y_dist)
						{
							x_size += layout_iterations * 30;
							y_size += layout_iterations * 10;
						}
						else
						{
							y_size += layout_iterations * 30;
							x_size += layout_iterations * 10;
						}

						problem_found = true;
						break;
					}
				}

				// if no problems have been found, quit the layouting process
				if (!problem_found)
				{
					layout_iterations = max_iterations;
				}
			}

			// for all contents of the scope
			if (scope instanceof StructuredActivityNode)
			{
				int san_width = 10;
				int san_height = 10;

				// for all contents of structuredactivitynode
				Set scopeChildren = new HashSet();
				for (Iterator iter = scope.eAllContents(); iter.hasNext();)
				{
					scopeChildren.add(iter.next());
				}
				scopeChildren.addAll(EMFHelper.getReferencesForScope(weeditor, scope));

				// remove top/left whitespace in san
				Point minPoint = new Point(Integer.MAX_VALUE, Integer.MAX_VALUE);
				Point maxPoint = new Point(0, 0);
				for (Object element : scopeChildren)
				{
					// ignore elements with x=0 (probably unset coordinates)
					if (element instanceof GraphicalElement
							&& LayoutDataManager.getX(weeditor,
									(GraphicalElement) element) != 0)
					{
						minPoint = Point.min(minPoint, new Point(LayoutDataManager.getX(
								weeditor, (GraphicalElement) element), LayoutDataManager
								.getY(weeditor, (GraphicalElement) element)));

						maxPoint = Point.max(maxPoint, new Point(LayoutDataManager.getX(
								weeditor, (GraphicalElement) element)
								+ LayoutDataManager.getWidth(weeditor,
										(GraphicalElement) element), LayoutDataManager
								.getY(weeditor, (GraphicalElement) element)
								+ LayoutDataManager.getHeight(weeditor,
										(GraphicalElement) element)));
					}
				}

				for (Object element : scopeChildren)
				{
					if (element instanceof GraphicalElement)
					{
						LayoutDataManager.setX(weeditor, (GraphicalElement) element,
								LayoutDataManager.getX(weeditor,
										(GraphicalElement) element)
										- minPoint.x);
						LayoutDataManager.setY(weeditor, (GraphicalElement) element,
								LayoutDataManager.getY(weeditor,
										(GraphicalElement) element)
										- minPoint.y);
					}
				}

				// adjust width/height of san according to its elements
				for (Object ge : scopeChildren)
				{
					if (ge instanceof GraphicalElement)
					{
						int ge_x = LayoutDataManager
								.getX(weeditor, (GraphicalElement) ge);
						int ge_y = LayoutDataManager
								.getY(weeditor, (GraphicalElement) ge);
						int ge_width = LayoutDataManager.getWidth(weeditor,
								(GraphicalElement) ge);
						int ge_height = LayoutDataManager.getHeight(weeditor,
								(GraphicalElement) ge);

						if (ge_x + ge_width > san_width)
						{
							san_width = (ge_x + ge_width);
						}
						if (ge_y + ge_height > san_height)
						{
							san_height = (ge_y + ge_height);
						}
					}
				}

				// transfer old layout information
				LayoutData before = LayoutDataManager.getLayoutDataForView(weeditor,
						(StructuredActivityNode) scope, Views.getInstance()
								.getSelectedView().getInternalName());
				LayoutData after = ViewFactoryImpl.eINSTANCE.createLayoutData();
				LayoutDataManager.transferLayoutDataValues(before, after);

				// put the layout data in the table
				layoutData.put(before, after);

				// set san layout information
				LayoutDataManager.setWidth(weeditor, (StructuredActivityNode) scope,
						san_width + 50);
				LayoutDataManager.setHeight(weeditor, (StructuredActivityNode) scope,
						san_height + 50);
			}

		}
		catch (Exception e)
		{
			logger.log(Level.WARNING, "could not instantiate algorithm " //$NON-NLS-1$
					+ layoutClass.toString());
		}
	}


	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.gef.commands.Command#execute()
	 */
	@Override
	public void execute()
	{
		layoutData = new HashMap();

		// prepare the layout of all activities in the hashtable and layout them
		// directly
		for (Scope scope : selectedScopes)
		{
			if (scope instanceof StructuredActivityNode)
			{
				layoutScope(scope);
			}
		}
		for (Scope scope : selectedScopes)
		{
			if (!(scope instanceof StructuredActivityNode))
			{
				layoutScope(scope);
			}
		}
	}


	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.gef.commands.Command#redo()
	 */
	@Override
	public void redo()
	{
		// switch layoutdata of before and after
		for (Object src : layoutData.keySet())
		{
			LayoutData source = (LayoutData) src;
			LayoutData target = (LayoutData) layoutData.get(src);
			LayoutData temp = ViewFactoryImpl.eINSTANCE.createLayoutData();

			LayoutDataManager.transferLayoutDataValues(source, temp);
			LayoutDataManager.transferLayoutDataValues(target, source);
			LayoutDataManager.transferLayoutDataValues(temp, target);
		}
	}


	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.gef.commands.Command#undo()
	 */
	@Override
	public void undo()
	{
		// switch layoutdata
		redo();
	}


	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.gef.commands.Command#dispose()
	 */
	@Override
	public void dispose()
	{
		layoutData.clear();
		layoutData = null;

		layoutClass = null;

		selectedScopes.clear();
		selectedScopes = null;

		super.dispose();
	}

}
