/******************************************************************************
 * Copyright (c) 2006, Intalio Inc.
 * 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:
 *     Intalio Inc. - initial API and implementation
 *******************************************************************************/
package org.eclipse.stp.bpmn.diagram.providers;

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

import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.ImageFigure;
import org.eclipse.draw2d.Locator;
import org.eclipse.draw2d.PositionConstants;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.emf.common.notify.Adapter;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.notify.impl.AdapterImpl;
import org.eclipse.emf.ecore.EAnnotation;
import org.eclipse.emf.ecore.EModelElement;
import org.eclipse.emf.ecore.EcorePackage;
import org.eclipse.gef.EditDomain;
import org.eclipse.gef.EditPart;
import org.eclipse.gef.GraphicalEditPart;
import org.eclipse.gef.editparts.AbstractConnectionEditPart;
import org.eclipse.gef.handles.HandleBounds;
import org.eclipse.gmf.runtime.common.core.service.AbstractProvider;
import org.eclipse.gmf.runtime.common.core.service.IOperation;
import org.eclipse.gmf.runtime.diagram.ui.editpolicies.DecorationEditPolicy.DecoratorTarget;
import org.eclipse.gmf.runtime.diagram.ui.parts.DiagramEditDomain;
import org.eclipse.gmf.runtime.diagram.ui.services.decorator.CreateDecoratorsOperation;
import org.eclipse.gmf.runtime.diagram.ui.services.decorator.IDecoration;
import org.eclipse.gmf.runtime.diagram.ui.services.decorator.IDecorator;
import org.eclipse.gmf.runtime.diagram.ui.services.decorator.IDecoratorProvider;
import org.eclipse.gmf.runtime.diagram.ui.services.decorator.IDecoratorTarget;
import org.eclipse.gmf.runtime.diagram.ui.services.decorator.IDecoratorTarget.Direction;
import org.eclipse.gmf.runtime.draw2d.ui.mapmode.MapModeUtil;
import org.eclipse.gmf.runtime.notation.Edge;
import org.eclipse.gmf.runtime.notation.View;
import org.eclipse.stp.bpmn.diagram.edit.parts.BpmnDiagramEditPart;
import org.eclipse.stp.bpmn.diagram.part.BpmnDiagramEditor;
import org.eclipse.stp.bpmn.diagram.part.BpmnDiagramEditorPlugin;
import org.eclipse.stp.bpmn.diagram.part.BpmnVisualIDRegistry;
import org.eclipse.stp.bpmn.dnd.IEAnnotationDecorator;
import org.eclipse.swt.graphics.Image;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.PlatformUI;

/**
 * Decoration in charge of showing icons and tooltips on views 
 * that are associated with modele elements that contain annotations.
 * @author <a href="mailto:atoulme@intalio.com">Antoine Toulmé</a>
 * @author <a href="http://www.intalio.com">&copy; Intalio, Inc.</a>
 */
public class BpmnEAnnotationDecoratorProvider extends AbstractProvider 
	implements IDecoratorProvider {

	/**
	 * the key for the decorator.
	 */
	private static final String KEY = "eannotationdecoration"; //$NON-NLS-1$
	
	/**
	 * Creates the decorator for a decorator target, 
	 * which is actually an edit part.
	 */
	public void createDecorators(IDecoratorTarget decoratorTarget) {
		EditPart editPart = (EditPart) decoratorTarget
		.getAdapter(EditPart.class);
		if (editPart instanceof GraphicalEditPart
				|| editPart instanceof AbstractConnectionEditPart) {
			Object model = editPart.getModel();
			if ((model instanceof View)) {
				View view = (View) model;
				if (!(view instanceof Edge) && !view.isSetElement()) {
					return;
				}
			}
			EditDomain ed = editPart.getViewer().getEditDomain();
			if (!(ed instanceof DiagramEditDomain)) {
				return;
			}
			if (((DiagramEditDomain) ed).getEditorPart() 
					instanceof BpmnDiagramEditor) {
				decoratorTarget.installDecorator(KEY, new EAnnotationDecorator(
						decoratorTarget));
			}
		}

	}

	/**
	 * Provides for a CreatesDecoratorsOperation, if the view is part of the
//	 * Bpmn model.
	 */
	public boolean provides(IOperation operation) {
		if (!(operation instanceof CreateDecoratorsOperation)) {
			return false;
		}

		IDecoratorTarget decoratorTarget = ((CreateDecoratorsOperation) 
				operation).getDecoratorTarget();
		View view = (View) decoratorTarget.getAdapter(View.class);
		return view != null
				&& BpmnDiagramEditPart.MODEL_ID.equals(BpmnVisualIDRegistry
						.getModelID(view));
	}

	/**
	 * Handles the decorator, attached to an edit part.
	 * Called by the decoration edit policy.
	 * @author <a href="mailto:atoulme@intalio.com">Antoine Toulmé</a>
	 * @author <a href="http://www.intalio.com">&copy; Intalio, Inc.</a>
	 */
	public class EAnnotationDecorator implements IDecorator {

		/** the object to be decorated */
		private DecoratorTarget decoratorTarget;
		
		private List<IDecoration> decorations = new ArrayList<IDecoration>();
		
		/**
		 * ListenerListener dropped on the EModelElement,
		 * listening to events related to annotations.
		 */
		private Adapter refreshListener = 
			new AdapterImpl() {

			/* (non-Javadoc)
			 * @see org.eclipse.emf.ecore.util.EContentAdapter#notifyChanged(org.eclipse.emf.common.notify.Notification)
			 */
			@Override
			public void notifyChanged(Notification notification) {
				if (notification.getFeatureID(EModelElement.class) == 
					EcorePackage.EMODEL_ELEMENT__EANNOTATIONS) {
					refresh();
				}
			}
		};

		/**
		 * Default constructor.
		 * @param decoratorTarget
		 */
		public EAnnotationDecorator(IDecoratorTarget decoratorTarget) {
			this.decoratorTarget = (DecoratorTarget) decoratorTarget;
		}

		/**
		 * Activates the decorator by adding a listener to the model element
		 * behind the view.
		 */
		@SuppressWarnings("unchecked")
		public void activate() {
			View view = (View) decoratorTarget.getAdapter(View.class);
			if (view == null || view.eResource() == null) {
				return;
			}
			if (view.getElement() instanceof EModelElement) {
				view.getElement().eAdapters().add(refreshListener);
			}
		}

		/**
		 * Refreshes the decoration, 
		 * called by the listener over the model element.
		 * It adds decorations and tooltips to the shape.
		 * 
		 * @author atoulme
		 * For now it has an issue when several icons are on one shape.
		 * 
		 */
		public void refresh() {
			removeDecorations();

			View view = (View) decoratorTarget.getAdapter(View.class);
			EditPart editPart = (EditPart) decoratorTarget.getAdapter(
					EditPart.class);
			if (view == null || view.eResource() == null) {
				return;
			}

			if (view.getElement() instanceof EModelElement) {
				EModelElement elt = (EModelElement) view.getElement();
				int offset = -1; 
				for (Object objAnn : elt.getEAnnotations()) {
					
					EAnnotation ann = (EAnnotation) objAnn;
					// add decoration
					if (editPart instanceof GraphicalEditPart) {
						Image image = getImage(editPart,elt,ann);
						if (image == null) {
							return;
						}
						if (view instanceof Edge) {
							setLastDecoration(decoratorTarget.
							addConnectionDecoration(image, 55 + 
									7 * elt.getEAnnotations().indexOf(ann), 
									false));
						} else {
							
							
							IFigure parentFigure = ((GraphicalEditPart)
									decoratorTarget.
									getAdapter(GraphicalEditPart.class))
									.getFigure();
							ImageFigure figure = new ImageFigure(image);
							figure.setParent(parentFigure);
							figure.setSize(image.getBounds().width,
									image.getBounds().height);
							IDecoration deco = decoratorTarget
									.addDecoration(figure,
										new StickyToBorderLocator(
													parentFigure,
												getPositionConstant(
									getDirection(editPart,elt,ann)),offset),
											false);
							offset = offset - image.getBounds().width - 1;
							// TODO add better support for multiple icons
							// anything better here is welcome
							// since for now the icons 
							// are displayed oriented to the center.
							setLastDecoration(deco);
							
						}
						((IFigure) getLastDecoration()).
							setToolTip(getToolTip(editPart,elt,ann));
					}
				}
			}

			
		}

		/**
		 * Borrowed from DecorationEditPolicy
		 * Converts the direction to an int as defined in PositionConstant.
		 * 
		 * @param direction
		 * @return the int as defined in PositionConstant
		 */
		private int getPositionConstant(Direction direction) {

			if (direction == Direction.CENTER) {
				return PositionConstants.CENTER;
			} else

			if (direction == Direction.NORTH) {
				return PositionConstants.NORTH;
			} else

			if (direction == Direction.SOUTH) {
				return PositionConstants.SOUTH;
			} else

			if (direction == Direction.WEST) {
				return PositionConstants.WEST;
			} else

			if (direction == Direction.EAST) {
				return PositionConstants.EAST;
			} else

			if (direction == Direction.NORTH_EAST) {
				return PositionConstants.NORTH_EAST;
			} else

			if (direction == Direction.NORTH_WEST) {
				return PositionConstants.NORTH_WEST;
			} else

			if (direction == Direction.SOUTH_EAST) {
				return PositionConstants.SOUTH_EAST;
			} else

			if (direction == Direction.SOUTH_WEST) {
				return PositionConstants.SOUTH_WEST;
			}

			return PositionConstants.CENTER;
		}
		private IDecoration getLastDecoration() {
			if (!decorations.isEmpty()) {
			return decorations.get(decorations.size() - 1);
			}
			return null;
		}

		private void removeDecorations() {
			for (IDecoration deco : decorations) {
				if (deco != null && ((IFigure) deco).getParent() != null) {
					decoratorTarget.removeDecoration(deco);
				}
			}

		}

		private void setLastDecoration(IDecoration deco) {
			decorations.add(deco);
		}

		/**
		 * 
		 * @param part
		 * @param elt
		 * @param ann
		 * @return the direction given by the extension point for the annotation
		 * source or the default direction, Direction.NORTH_EAST
		 */
		private Direction getDirection(
				EditPart part,EModelElement elt,EAnnotation ann) {
			IEAnnotationDecorator decorator = BpmnDiagramEditorPlugin.
			getInstance().getEAnnotationDecorator(ann.getSource());
			if (decorator != null) {
				return decorator.getDirection(part, elt, ann);
			}
			return Direction.NORTH_EAST;
		}
		
		/**
		 * @param part
		 * @param elt
		 * @param ann
		 * @return the image associated with this source through the 
		 * EAnnotationDecorator extension point.
		 */
		private Image getImage(EditPart part,EModelElement elt,EAnnotation ann){
			IEAnnotationDecorator decorator = BpmnDiagramEditorPlugin.
			getInstance().getEAnnotationDecorator(ann.getSource());
			if (decorator != null) {
				return decorator.getImage(part, elt, ann);
			}
			return null;
		}

		/**
		 * 
		 * @param elt
		 * @param ann
		 * @return the tooltip associated with this source through the 
		 * EAnnotationDecorator extension point.
		 */
		private IFigure getToolTip(EditPart part,
				EModelElement elt,EAnnotation ann) {
			IEAnnotationDecorator decorator = BpmnDiagramEditorPlugin.
			getInstance().getEAnnotationDecorator(ann.getSource());
			if (decorator != null) {
				return decorator.getToolTip(part, elt, ann);
			}
			return null;
		}

		/* (non-Javadoc)
		 * @see org.eclipse.gmf.runtime.diagram.ui.services.decorator.AbstractDecorator#deactivate()
		 */
		public void deactivate() {
			removeDecorations();
			View view = (View) decoratorTarget.getAdapter(View.class);
			if (view == null || view.eResource() == null) {
				return;
			}
			if (view.getElement() instanceof EModelElement) {
				view.getElement().eAdapters().remove(refreshListener);
			}

		}

	}
	
	/**
	 * Locator that sticks to the border it is attached to.
	 * @author <a href="mailto:atoulme@intalio.com">Antoine Toulmé</a>
	 * @author <a href="http://www.intalio.com">&copy; Intalio, Inc.</a>
	 */
	private class StickyToBorderLocator implements Locator {
		/**
		 * Constant to offset the decoration on the north west direction
		 * of the size of an other decoration.
		 */
		private static final int OTHER_DECORATION = 16;

		/** the offset to add to the location of the object */
		private int offset;

		/** the direction */
		protected int direction;

		/** the parent figure */
		protected IFigure reference;

		/**
		 * Constructor for <code>StickyToBorderLocator</code>.
		 * 
		 * @param reference the parent figure
		 * @param direction A direction to place the figure relative to the reference
		 * figure as defined in {@link PositionConstants}
		 * @param margin The margin is the space between the reference figure's edge 
		 * and the figure.  A positive margin will place the figure outside the
		 * reference figure, a negative margin will place the figure inside the 
		 * reference figure.
		 */
		public StickyToBorderLocator(
			IFigure reference,
			int direction,
			int offset) {
				
			this.reference = reference;
			this.direction = direction;
			this.offset = offset;
		}

		/**
		 * Puts the figure either inside or outside the parent edge (where the edge 
		 * is the same as where the resize/move handles would be placed),
		 * identified by the direction, with a margin.
		 * 
		 * @see org.eclipse.draw2d.Locator#relocate(org.eclipse.draw2d.IFigure)
		 */
		public void relocate(IFigure target) {
			Rectangle bounds =
				reference instanceof HandleBounds
					? ((HandleBounds) reference).getHandleBounds().getCopy()
					: reference.getBounds().getCopy();	

			reference.translateToAbsolute(bounds);
			target.translateToRelative(bounds);

			int width = target.getBounds().width;
			int halfWidth = width / 2;

			int height = target.getBounds().height;
			int halfHeight = height / 2;

			if (direction == PositionConstants.CENTER) {

				Dimension shift = new Dimension(-halfWidth, -halfHeight);
				target.setLocation(bounds.getCenter().getTranslated(shift));

			} else if (offset < 0) {

				if (direction == PositionConstants.NORTH_WEST) {

					Dimension shift = new Dimension(
							-offset + OTHER_DECORATION, 0);
					target.setLocation(bounds.getTopLeft().getTranslated(shift));

				} else if (direction == PositionConstants.NORTH) {

					Dimension shift = new Dimension(-offset, -halfWidth);
					target.setLocation(bounds.getTop().getTranslated(shift));

				} else if (direction == PositionConstants.NORTH_EAST) {

					Dimension shift = new Dimension(-(width + -offset), 0);
					target.setLocation(bounds.getTopRight().getTranslated(shift));

				} else if (direction == PositionConstants.SOUTH_WEST) {

					Dimension shift = new Dimension(0, -(height + -offset));
					target.setLocation(bounds.getBottomLeft().getTranslated(shift));

				} else if (direction == PositionConstants.SOUTH) {

					Dimension shift = new Dimension(-halfWidth, -(height + -offset));
					target.setLocation(bounds.getBottom().getTranslated(shift));

				} else if (direction == PositionConstants.SOUTH_EAST) {

					Dimension shift = new Dimension(-(width + -offset),
						-(height));
					target
						.setLocation(bounds.getBottomRight().getTranslated(shift));

				} else if (direction == PositionConstants.WEST) {

					Dimension shift = new Dimension(-offset, -halfHeight);
					target.setLocation(bounds.getLeft().getTranslated(shift));

				} else if (direction == PositionConstants.EAST) {

					Dimension shift = new Dimension(-(width + -offset), -halfHeight);
					target.setLocation(bounds.getRight().getTranslated(shift));

				}
			} else {

				if (direction == PositionConstants.NORTH_WEST) {

					Dimension shift =
						new Dimension(offset,0);
					target.setLocation(bounds.getTopLeft().getTranslated(shift));

				} else if (direction == PositionConstants.NORTH) {

					Dimension shift =
						new Dimension(-halfWidth, - (height + offset));
					target.setLocation(bounds.getTop().getTranslated(shift));

				} else if (direction == PositionConstants.NORTH_EAST) {

					Dimension shift = new Dimension(0, - (height + offset));
					target.setLocation(bounds.getTopRight().getTranslated(shift));

				} else if (direction == PositionConstants.SOUTH_WEST) {

					Dimension shift = new Dimension(- (width + offset), 0);
					target.setLocation(bounds.getBottomLeft().getTranslated(shift));

				} else if (direction == PositionConstants.SOUTH) {

					Dimension shift = new Dimension(-halfWidth, 0);
					target.setLocation(bounds.getBottom().getTranslated(shift));

				} else if (direction == PositionConstants.SOUTH_EAST) {

					Dimension shift = new Dimension(offset, 0);
					target.setLocation(
						bounds.getBottomRight().getTranslated(shift));

				} else if (direction == PositionConstants.WEST) {

					Dimension shift =
						new Dimension(- (width + offset), -halfHeight);
					target.setLocation(bounds.getLeft().getTranslated(shift));

				} else if (direction == PositionConstants.EAST) {

					Dimension shift = new Dimension(offset, -halfHeight);
					target.setLocation(bounds.getRight().getTranslated(shift));

				}
			}
		}
	}
}
