/*******************************************************************************
 * Copyright (c) 2005 IBM Corporation and others.
 * 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.bpel.ui.editparts;

import java.util.ArrayList;
import java.util.EventObject;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.bpel.model.Activity;
import org.eclipse.bpel.model.Flow;
import org.eclipse.bpel.model.Links;
import org.eclipse.bpel.ui.BPELEditor;
import org.eclipse.bpel.ui.BPELUIPlugin;
import org.eclipse.bpel.ui.IBPELUIConstants;
import org.eclipse.bpel.ui.commands.CompoundCommand;
import org.eclipse.bpel.ui.commands.SetConstraintCommand;
import org.eclipse.bpel.ui.editparts.borders.DrawerBorder;
import org.eclipse.bpel.ui.editparts.borders.FlowBorder;
import org.eclipse.bpel.ui.editparts.figures.GradientFigure;
import org.eclipse.bpel.ui.editparts.policies.FlowHighlightEditPolicy;
import org.eclipse.bpel.ui.editparts.policies.FlowResizeEditPolicy;
import org.eclipse.bpel.ui.editparts.policies.FlowXYLayoutEditPolicy;
import org.eclipse.bpel.ui.editparts.util.BPELDecorationLayout;
import org.eclipse.bpel.ui.editparts.util.GraphAnimation;
import org.eclipse.bpel.ui.util.BatchedMultiObjectAdapter;
import org.eclipse.bpel.ui.util.FlowXYLayout;
import org.eclipse.bpel.ui.util.ModelHelper;
import org.eclipse.bpel.ui.util.NonclippingXYLayout;
import org.eclipse.bpel.ui.util.RowColumnLayout;
import org.eclipse.bpel.ui.util.marker.BPELEditPartMarkerDecorator;
import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.Label;
import org.eclipse.draw2d.LayoutManager;
import org.eclipse.draw2d.PositionConstants;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Insets;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.draw2d.graph.DirectedGraph;
import org.eclipse.draw2d.graph.DirectedGraphLayout;
import org.eclipse.draw2d.graph.Edge;
import org.eclipse.draw2d.graph.Node;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.notify.Notifier;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.util.EContentAdapter;
import org.eclipse.gef.EditPart;
import org.eclipse.gef.EditPolicy;
import org.eclipse.gef.GraphicalEditPart;
import org.eclipse.gef.commands.CommandStackListener;

public class FlowEditPart extends CollapsableEditPart {

	private FlowBorder flowBorder;

	boolean useFlowLayout = false;

	private EContentAdapter flowContentAdapter;

	private BatchedMultiObjectAdapter flowBatchedAdapter;

	protected boolean smoothLayout = false;

	protected FlowHighlightEditPolicy flowHighlightEditPolicy;

	@Override
	protected void addAllAdapters() {
		super.addAllAdapters();
		Links links = ((Flow) getActivity()).getLinks();
		if (links != null) {
			adapter.addToObject(links);
		}
	}

	// TODO: this looks strange.. what is it really doing??
	CommandStackListener stackListener = new CommandStackListener() {
		public void commandStackChanged(EventObject event) {
			if (!isSmoothLayout())
				return;

			setSmoothLayout(false);

			if (!GraphAnimation.captureLayout(getFigure()))
				return;

			while (GraphAnimation.step())
				getFigure().getUpdateManager().performUpdate();
			GraphAnimation.end();

		}
	};

	class FlowDecorationLayout extends BPELDecorationLayout {
		@Override
		protected Point calculateLocation(int locationHint, IFigure container,
				Dimension childDimension) {
			Rectangle area = container.getClientArea();
			switch (locationHint) {
				case PositionConstants.CENTER:
					// Center
					return new Point(area.x + area.width / 2
							- childDimension.width / 2, area.y + area.height
							/ 2 - childDimension.height / 2);
				case PositionConstants.TOP:
					// Top Center
					return new Point(area.x + area.width / 2
							- childDimension.width / 2, area.y
							+ FlowBorder.LINE_WIDTH);
				case PositionConstants.BOTTOM:
					// Bottom Center
					return new Point(area.x + area.width / 2
							- childDimension.width / 2, area.y + area.height
							- childDimension.height - FlowBorder.LINE_WIDTH);
				case PositionConstants.LEFT: {
					// Center Left
					int x = area.x + DrawerBorder.DRAWER_WIDTH;
					int y = area.y;
					if (isCollapsed()) {
						y += container.getBounds().height / 2;
					} else {
						y += area.height / 2 - childDimension.width / 2;
					}
					return new Point(x, y);
				}
				case PositionConstants.RIGHT: {
					// Center Right
					int x = area.x + area.width - DrawerBorder.DRAWER_WIDTH
							- childDimension.width;
					int y = area.y;
					if (isCollapsed()) {
						y += container.getBounds().height / 2;
					} else {
						y += area.height / 2 - childDimension.width / 2;
					}
					return new Point(x, y);
				}
				case PositionConstants.TOP | PositionConstants.LEFT: {
					// Top Left
					int x = area.x + DrawerBorder.DRAWER_WIDTH;
					int y = area.y;
					if (isCollapsed()) {
						y += image.getBounds().height / 2;
					}
					return new Point(x, y);
				}
				case PositionConstants.TOP | PositionConstants.RIGHT: {
					// Top Right
					int x = area.x + area.width - DrawerBorder.DRAWER_WIDTH
							- childDimension.width;
					int y = area.y;
					if (isCollapsed()) {
						y += image.getBounds().height / 2;
					}
					return new Point(x, y);
				}
				case PositionConstants.BOTTOM | PositionConstants.LEFT: {
					// Bottom Left
					int x = area.x + DrawerBorder.DRAWER_WIDTH;
					int y = area.y + area.height
							- (image.getBounds().height / 2);
					return new Point(x, y);
				}
				case PositionConstants.BOTTOM | PositionConstants.RIGHT: {
					// Bottom Right
					int x = area.x + area.width - DrawerBorder.DRAWER_WIDTH
							- childDimension.width;
					int y = area.y + area.height
							- (image.getBounds().height / 2);
					return new Point(x, y);
				}
				default:
					return new Point(area.x, area.y);
			}
		}
	}

	public FlowEditPart() {
		super();

		// in order to create a batched EContentAdapter we
		// basically delegate the notifications to a real
		// batched adapter
		flowContentAdapter = new EContentAdapter() {
			@Override
			public void notifyChanged(Notification n) {
				switch (n.getEventType()) {
					case Notification.ADD_MANY:
					case Notification.REMOVE_MANY:
					case Notification.ADD:
					case Notification.REMOVE:
					case Notification.SET:
					case Notification.UNSET:
					case Notification.MOVE:
						flowBatchedAdapter.notifyChanged(n);
				}
				super.notifyChanged(n);
			}
		};
		// do not add this adapter to model objects - it is called
		// from the flow content adapter
		flowBatchedAdapter = new BatchedMultiObjectAdapter() {
			protected boolean refreshLayout = false;

			@Override
			public void finish() {
				if (refreshLayout) {
					if (getAutoLayout())
						doAutoLayout(false);
				}
				refreshLayout = false;
			}

			@Override
			public void notify(Notification n) {
				if (isActive()) {
					refreshLayout = true;
				}
			}
		};
	}

	@Override
	protected void addChildVisual(EditPart childEditPart, int index) {
		IFigure child = ((GraphicalEditPart) childEditPart).getFigure();
		getContentPane().add(child,
				getFigure().getLayoutManager().getConstraint(child), index);
	}

	protected void setFlowEditPolicies() {
		installEditPolicy(EditPolicy.LAYOUT_ROLE, new FlowXYLayoutEditPolicy());
		flowHighlightEditPolicy = new FlowHighlightEditPolicy(!collapsed) {
			@Override
			protected int getDrawerInset() {
				return DrawerBorder.DRAWER_WIDTH;
			}

			@Override
			protected int getNorthInset() {
				if (isCollapsed()) {
					return 2;
				} else {
					return 2;
				}
			}

			@Override
			protected int getSouthInset() {
				return 0;
			}

			@Override
			protected int getEastInset() {
				return DrawerBorder.DRAWER_WIDTH;
			}

			@Override
			protected int getWestInset() {
				return DrawerBorder.DRAWER_WIDTH + 2;
			}
		};
		installEditPolicy(EditPolicy.SELECTION_FEEDBACK_ROLE,
				flowHighlightEditPolicy);
	}

	@Override
	protected void createEditPolicies() {
		super.createEditPolicies();
		// installEditPolicy(EditPolicy.NODE_ROLE, null);
		setFlowEditPolicies();
		installEditPolicy("childFlowResize", new FlowResizeEditPolicy()); //$NON-NLS-1$
	}

	@Override
	public Label getLabelFigure() {
		if (isCollapsed())
			return super.getLabelFigure();
		return null;
	}

	@Override
	public void setCollapsed(boolean collapsed) {
		if (collapsed != this.collapsed) {
			if (flowHighlightEditPolicy != null) {
				flowHighlightEditPolicy.setResizable(!collapsed);
			}
		}
		super.setCollapsed(collapsed);
	}

	@Override
	protected IFigure createFigure() {
		createEditPolicies(); // reset the edit policies based on flow display
								// mode
		initializeLabels();

		editPartMarkerDecorator = new BPELEditPartMarkerDecorator(
				(EObject) getModel(), new FlowDecorationLayout());
		editPartMarkerDecorator
				.addMarkerMotionListener(getMarkerMotionListener());

		IFigure figure = new GradientFigure(getModel());
		if (collapsed) {
			addCollapsedContents(figure);
		} else {
			configureExpandedFigure(figure);
		}
		this.contentFigure = figure;

		return editPartMarkerDecorator.createFigure(figure);
	}

	@Override
	protected void configureExpandedFigure(IFigure figure) {
		LayoutManager layout;

		if (!getShowFreeformFlow()) {
			layout = new RowColumnLayout();
		} else {
			FlowXYLayout xylayout = new FlowXYLayout(this);
			Dimension d = ModelHelper.getSize(getFlow());
			if (d.height != 0 && d.width != 0)
				xylayout.setSize(d);
			layout = xylayout;
		}

		figure.setLayoutManager(layout);

		if (!(figure.getBorder() instanceof FlowBorder)) {
			flowBorder = new FlowBorder(figure);
			figure.setBorder(this.flowBorder);
			this.flowBorder.setEditPart(this);
		}
		figure.addMouseMotionListener(getMouseMotionListener());
		this.flowBorder.setEditPart(this);
	}

	protected Flow getFlow() {
		return (Flow) getModel();
	}

	@Override
	protected boolean isCollapsable() {
		return true;
	}

	@Override
	public void deactivate() {
		if (!isActive())
			return;
		super.deactivate();
		((Notifier) getModel()).eAdapters().remove(flowContentAdapter);

		getViewer().getEditDomain().getCommandStack()
				.removeCommandStackListener(stackListener);
	}

	private boolean getShowFreeformFlow() {
		return BPELUIPlugin.INSTANCE.getPreferenceStore().getBoolean(
				IBPELUIConstants.PREF_SHOW_FREEFORM_FLOW);
	}

	private boolean getAutoLayout() {
		return BPELUIPlugin.INSTANCE.getPreferenceStore().getBoolean(
				IBPELUIConstants.PREF_AUTO_FLOW_LAYOUT);
	}

	@Override
	public IFigure getContentPane() {
		return contentFigure;
	}

	public boolean isShowFreeform() {
		return (getContentPane().getLayoutManager() instanceof NonclippingXYLayout);
	}

	@Override
	public void regenerateVisuals() {
		if (collapsed) {
			addCollapsedContents(this.contentFigure);
		} else {
			configureExpandedFigure(this.contentFigure);
		}
		setFlowEditPolicies();
		// This is necessary because..we may have replaced the border!
		refreshDrawerImages();

		if (getShowFreeformFlow() && getAutoLayout())
			doAutoLayout(false);
	}

	public DirectedGraph computeAutoLayoutGraph(Map partsToNodes) {
		DirectedGraph graph = new DirectedGraph();
		graph.setDefaultPadding(new Insets(8, 8, 10, 8));
		Node top = new Node(null);
		graph.nodes.add(top);
		top.width = top.height = 0;
		top.setPadding(new Insets(-8, 0, 0, 0));

		List nodesWithoutPreds = new ArrayList();

		for (Iterator it = getChildren().iterator(); it.hasNext();) {
			Object object = it.next();
			if (object instanceof BPELEditPart) {
				BPELEditPart editPart = (BPELEditPart) object;
				Node n = new Node(editPart);
				n.width = editPart.getFigure().getPreferredSize().width;
				n.height = editPart.getFigure().getPreferredSize().height;
				n.sortValue = getFigure().getBounds().x;
				graph.nodes.add(n);
				partsToNodes.put(editPart, n);
				nodesWithoutPreds.add(n);
				// TODO: pre-sort node list based on x-coords of existing
				// constraints?
			} else {
				// System.out.println(object);
			}
		}
		for (Iterator it = getChildren().iterator(); it.hasNext();) {
			Object object = it.next();
			if (object instanceof BPELEditPart) {
				Node target = (Node) partsToNodes.get(object);
				if (target == null)
					continue;

				for (Iterator it2 = ((BPELEditPart) object)
						.getTargetConnections().iterator(); it2.hasNext();) {
					Object object2 = it2.next();
					if (object2 instanceof LinkEditPart) {
						LinkEditPart link = (LinkEditPart) object2;
						Node source = (Node) partsToNodes.get(link.getSource());
						if (source != null) {
							graph.edges.add(new Edge(source, target));
							nodesWithoutPreds.remove(target);
						}
					}
				}
			}
		}
		for (Iterator it = nodesWithoutPreds.iterator(); it.hasNext();) {
			graph.edges.add(new Edge(top, (Node) it.next()));
		}

		new DirectedGraphLayout().visit(graph);

		return graph;
	}

	public void doImmediateAutoLayout() {
		Map<BPELEditPart, Node> partsToNodes = new HashMap<BPELEditPart, Node>();
		if (BPELUIPlugin.INSTANCE.getPreferenceStore().getBoolean(
				IBPELUIConstants.PREF_USE_ANIMATION)) {
			setSmoothLayout(true);
		}
		computeAutoLayoutGraph(partsToNodes);

		for (Iterator it = getChildren().iterator(); it.hasNext();) {
			Object object = it.next();
			if (object instanceof BPELEditPart) {
				BPELEditPart editPart = (BPELEditPart) object;

				Node n = partsToNodes.get(editPart);
				Point loc = new Point(n.x, n.y);
				// TODO: I think this is wrong
				// getFigure().translateToRelative(loc);

				ModelHelper.setLocation((Activity) editPart.getModel(), loc);
			}
		}
	}

	public void doAutoLayout() {
		doAutoLayout(true);
	}

	public void doAutoLayout(boolean withCommand) {
		Map<BPELEditPart, Node> partsToNodes = new HashMap<BPELEditPart, Node>();
		this.getFigure().invalidateTree();
		if (BPELUIPlugin.INSTANCE.getPreferenceStore().getBoolean(
				IBPELUIConstants.PREF_USE_ANIMATION)) {
			setSmoothLayout(true);
		}
		computeAutoLayoutGraph(partsToNodes);

		CompoundCommand cmd = new CompoundCommand();
		cmd.setLabel(IBPELUIConstants.CMD_AUTO_ARRANGE);

		BPELEditor bpelEditor = ModelHelper.getBPELEditor(getModel());

		for (Iterator it = getChildren().iterator(); it.hasNext();) {
			Object object = it.next();
			if (object instanceof BPELEditPart) {
				BPELEditPart editPart = (BPELEditPart) object;

				Node n = partsToNodes.get(editPart);
				Point loc = new Point(n.x, n.y);
				// TODO: I think this is wrong
				// getFigure().translateToRelative(loc);

				SetConstraintCommand cmd2 = new SetConstraintCommand(
						(Activity) editPart.getModel(), loc, null);
				if (withCommand) {
					cmd.add(cmd2);
				} else {
					cmd2.execute();
				}
			}
		}
		if (withCommand) {
			regenerateVisuals();
			bpelEditor.getCommandStack().execute(cmd);
		}
	}

	@Override
	protected void handleModelChanged() {
		// The size of the flow may have changed. Rebuild the edit part.

		// move this line to top of function, must call refreshChildren() to
		// make sure the gef is in sync with model before we regenerate visuals
		// and do an autolayout
		refreshChildren();

		super.handleModelChanged();
		regenerateVisuals();
	}

	@Override
	public void activate() {
		super.activate();
		((Notifier) getModel()).eAdapters().add(flowContentAdapter);

		getViewer().getEditDomain().getCommandStack().addCommandStackListener(
				stackListener);
	}

	public boolean isSmoothLayout() {
		return smoothLayout;
	}

	public void setSmoothLayout(boolean smoothLayout) {
		this.smoothLayout = smoothLayout;
	}

	/* a couple of utility classes for use in the cycle detection code */

	/** represents an edge connection for an editpart * */

	private class EditPartEdge {
		private EditPartNode source, dest;

		public EditPartEdge(EditPartNode source, EditPartNode dest) {
			this.source = source;
			this.dest = dest;
		}

		public EditPartNode getDest() {
			return dest;
		}

		public EditPartNode getSource() {
			return source;
		}
	}

	/**
	 * represents an node in a graph The main feature is a visit function which
	 * marks the visited node and returns false if cycle detected
	 */

	private class EditPartNode {
		public static final int VISITING = 1, VISITED = 2, NOTVISITED = 0;

		private EditPart part;

		private List edges = new ArrayList();

		public int visited = NOTVISITED;

		public EditPartNode(EditPart part) {
			this.part = part;
		}

		public EditPart getPart() {
			return part;
		}

		public int getVisited() {
			return visited;
		}

		public void addEdge(EditPartEdge edge) {
			edges.add(edge);
		}

		/** returns false if cycle detected * */
		public boolean visit() {
			EditPartEdge e;
			if (visited == VISITING) {
				return false;
			}
			if (visited == VISITED)
				return true;
			visited = VISITING;
			for (Iterator it = edges.iterator(); it.hasNext();) {
				e = (EditPartEdge) it.next();
				// if (e.getSource() == this)
				if (e.getDest().visit() == false)
					return false;
			}
			visited = VISITED;
			return true;
		}
	}

	/**
	 * checks if a new connection joining sourceNode and potentialDest will
	 * result in a cycle *
	 */

	public boolean detectImpendingCycle(EditPart sourceNode,
			EditPart potentialDest) {
		List<EditPartNode> nodes = new ArrayList<EditPartNode>();

		Map<BPELEditPart, EditPartNode> partsToNodes = new HashMap<BPELEditPart, EditPartNode>();

		// strategy: we'll build up a separate parallel graph that we can
		// traverse to detect cycles.
		// We are essentially duplicating the Flow's
		// graph structure, but this is easiest since we don't have a way of
		// "simulating a link between
		// for the proposed link, and we don't want to pollute the editparts
		// extraneous methods and fields
		// just for cycle detection logic, this will have to do for now.

		for (Iterator it = getChildren().iterator(); it.hasNext();) {
			Object object = it.next();
			if (object instanceof BPELEditPart) {
				BPELEditPart editPart = (BPELEditPart) object;
				EditPartNode n = new EditPartNode(editPart);
				nodes.add(n);
				partsToNodes.put(editPart, n);
			}
		}

		for (Iterator it = getChildren().iterator(); it.hasNext();) {
			Object object = it.next();
			if (object instanceof BPELEditPart) {
				EditPartNode source = partsToNodes.get(object);
				if (source == null)
					continue;

				for (Iterator it2 = ((BPELEditPart) object)
						.getSourceConnections().iterator(); it2.hasNext();) {
					Object targetObject = it2.next();
					if (targetObject instanceof LinkEditPart) {
						LinkEditPart linkEditPart = (LinkEditPart) targetObject;
						EditPartNode target = partsToNodes
								.get(linkEditPart.getTarget());
						if (target != null) {
							source.addEdge(new EditPartEdge(source, target));
						}
					}
				}
			}
		}

		// add the proposed edge
		EditPartNode source = partsToNodes.get(sourceNode);
		if (source != null) {
			EditPartNode target = partsToNodes
					.get(potentialDest);
			if (target != null) {
				source.addEdge(new EditPartEdge(source, target));
			}
		}

		/* visit each node checking if a cycle will result */
		for (Iterator<EditPartNode> it = nodes.iterator(); it.hasNext();) {
			EditPartNode v = it.next();
			if (v.getVisited() == EditPartNode.NOTVISITED)
				if (v.visit() == false)
					return false;
		}

		return true;
	}

	public void switchLayout(boolean horizontal) {
		// Actually does nothing
	}
}