/*******************************************************************************
 * 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.Collections;
import java.util.List;

import org.eclipse.bpel.common.ui.markers.IModelMarkerConstants;
import org.eclipse.bpel.model.Link;
import org.eclipse.bpel.ui.BPELUIPlugin;
import org.eclipse.bpel.ui.IBPELUIConstants;
import org.eclipse.bpel.ui.adapters.ILabeledElement;
import org.eclipse.bpel.ui.adapters.IMarkerHolder;
import org.eclipse.bpel.ui.editparts.borders.ContainerBorder;
import org.eclipse.bpel.ui.editparts.borders.DrawerBorder;
import org.eclipse.bpel.ui.editparts.borders.LeafBorder;
import org.eclipse.bpel.ui.editparts.figures.CollapsableContainerFigure;
import org.eclipse.bpel.ui.editparts.util.BPELDecorationLayout;
import org.eclipse.bpel.ui.figures.CenteredConnectionAnchor;
import org.eclipse.bpel.ui.figures.ILayoutAware;
import org.eclipse.bpel.ui.util.BPELDragEditPartsTracker;
import org.eclipse.bpel.ui.util.BPELUtil;
import org.eclipse.bpel.ui.util.marker.BPELEditPartMarkerDecorator;
import org.eclipse.core.resources.IMarker;
import org.eclipse.draw2d.Border;
import org.eclipse.draw2d.ConnectionAnchor;
import org.eclipse.draw2d.FlowLayout;
import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.Label;
import org.eclipse.draw2d.Layer;
import org.eclipse.draw2d.PositionConstants;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.emf.common.notify.Adapter;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.gef.DragTracker;
import org.eclipse.gef.Request;
import org.eclipse.swt.graphics.Image;


/**
 * Subclasses of CollapsableEditPart represent BPEL activities which
 * can be graphically represented in an expanded or collapsed manner.
 * 
 * In the collapsed state, this class will render the node. In the
 * expanded state, the subclass will be expected to render the node.
 */
public abstract class CollapsableEditPart extends CompositeActivityEditPart implements ILayoutAware {
	
	// Whether or not the edit part is collapsed
	protected boolean collapsed;
	
	// The primary image for the edit part
	protected Image image;
	
	// The images in the top and bottom drawer, if any
	protected Image topImage, bottomImage;
	
	// The label to display when collapsed. Subclasses may also use it
	// when expanded if desired.
	protected Label collapsedLabel;
	
	// Marker support
	protected IFigure contentFigure;
	protected BPELEditPartMarkerDecorator editPartMarkerDecorator;

	protected Adapter childrenAdapter;

	public class CollapsableDecorationLayout extends BPELDecorationLayout {
		private int borderImageWidth;
		public CollapsableDecorationLayout(int borderImageWidth) {
			this.borderImageWidth = borderImageWidth;
		}
		@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 + borderImageWidth / 2);
				case PositionConstants.BOTTOM:
					// Bottom Center
					return new Point(area.x + area.width / 2 - childDimension.width / 2, area.y + area.height - childDimension.height);
				case PositionConstants.LEFT: {
					// Center Left
					int x = area.x + DrawerBorder.DRAWER_WIDTH;
					int y = area.y + (area.height / 2) - (childDimension.width / 2);
					if (!isCollapsed()) {
						y += DrawerBorder.DRAWER_HALF_HEIGHT / 2;
					}
					return new Point(x, y);
				}
				case PositionConstants.RIGHT: {
					// Center Right
					int x = area.x + area.width - childDimension.width - DrawerBorder.DRAWER_WIDTH;
					int y = area.y + (area.height / 2) - (childDimension.width / 2);
					if (!isCollapsed()) {
						y += DrawerBorder.DRAWER_HALF_HEIGHT / 2;
					}
					return new Point(x, y);
				}
				case PositionConstants.TOP | PositionConstants.LEFT: {
					// Top Left
					int x = area.x + DrawerBorder.DRAWER_WIDTH;
					int y = area.y + (borderImageWidth / 2);
					if (!isCollapsed()) {
						y += (collapsedLabel.getSize().height / 2) - (IBPELUIConstants.ARC_WIDTH / 2);
					}
					return new Point(x, y);
				}
				case PositionConstants.TOP | PositionConstants.RIGHT: {
					// Top Right
					int x = area.x + area.width - childDimension.width - DrawerBorder.DRAWER_WIDTH;
					int y = area.y + (borderImageWidth / 2);
					if (!isCollapsed()) {
						y += (collapsedLabel.getSize().height / 2) - (IBPELUIConstants.ARC_WIDTH / 2);
					} else {
						x -= 1; // 1 is a magic number
					}
					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 - IBPELUIConstants.ARC_WIDTH;
					if (!isCollapsed()) {
						y -= DrawerBorder.DRAWER_HALF_HEIGHT / 2;
					} else {
						y -= DrawerBorder.DRAWER_HALF_HEIGHT;
					}
					return new Point(x, y);
				}
				case PositionConstants.BOTTOM | PositionConstants.RIGHT: {
					// Bottom Right
					int x = area.x + area.width - childDimension.width - DrawerBorder.DRAWER_WIDTH;
					int y = area.y + area.height - IBPELUIConstants.ARC_WIDTH;
					if (!isCollapsed()) {
						y -= DrawerBorder.DRAWER_HALF_HEIGHT / 2;
					} else {
						y -= DrawerBorder.DRAWER_HALF_HEIGHT;
						x -= 1; // 1 is a magic number
					}
					return new Point(x, y);
				}
				default:
					return new Point(area.x, area.y);
			}
		}
	}

	public CollapsableEditPart() {
		this(false);
	}
	public CollapsableEditPart(boolean collapsed) {
		this.collapsed = collapsed;
	}
	protected void initializeLabels() {
		this.collapsedLabel = new Label(getLabel());
		
		// Get Image from registry
		this.image = getImg();
	}
	
	private boolean isGenericContainerBorder() {
		Border border = this.getContentPane().getBorder();
		return (border != null && border instanceof ContainerBorder);
	}
		
	@Override
	protected DrawerBorder getDrawerBorder() {
		Border border = getContentPane().getBorder();
		if (border instanceof DrawerBorder) return (DrawerBorder)border;
		return null;
	}
	
	@Override
	public Label getLabelFigure() {
		if (isGenericContainerBorder()) {
			ContainerBorder border = (ContainerBorder)(this.getContentPane().getBorder());
			return border.getLabel();
		}
		return collapsedLabel;
	}

	@Override
	protected IFigure createFigure() {		
		initializeLabels();
		editPartMarkerDecorator = new BPELEditPartMarkerDecorator(
			(EObject)getModel(),
			new CollapsableDecorationLayout(image.getBounds().width)
		);
		editPartMarkerDecorator.addMarkerMotionListener(getMarkerMotionListener());
		
		IFigure figure = getNewContentPane(editPartMarkerDecorator.getDecorationLayer());
		this.contentFigure = figure;
		
		figure.setForegroundColor(BPELUIPlugin.INSTANCE.getColorRegistry().get(IBPELUIConstants.COLOR_BLACK));		
		if (isCollapsed()) {
			addCollapsedContents(figure);
		} else {
			configureExpandedFigure(figure);
		}
		
		return editPartMarkerDecorator.createFigure(figure);
	}
	
	protected IFigure getNewContentPane(Layer layer) {
		CollapsableContainerFigure fig =  new CollapsableContainerFigure(getModel(), image, getLabel());
		fig.addMouseMotionListener(getMouseMotionListener());
		fig.setEditPart(this);
		return fig;
	}
	
	protected CollapsableContainerFigure getContentFigure() {
		return (CollapsableContainerFigure)this.contentFigure;
	}
	
	// subclasses can override this to change edit policies, etc.
	protected void notifyCollapsed(boolean collapsed) { }
	
	public void setCollapsed(boolean collapsed) {
		if (!isCollapsable()) return;

		if (isCollapsed() == collapsed) return;
		this.collapsed = collapsed;
		if (isGenericContainerBorder())
			getContentFigure().setCollapsed(collapsed);
		
		notifyCollapsed(collapsed);
		
		IFigure figure = getContentPane();
		if (isCollapsed()) {
			// First refresh children, which removes all model children's 
			// figures
			refreshChildren();
			
			// Manually remove the rest of the children
			for (Object o : figure.getChildren()) {
				figure.remove((IFigure)o);
			}
			// Now restore the collapsed children, border and layout
			addCollapsedContents(figure);
		} else {
			// Manually remove the children
			for (Object o : figure.getChildren()) {
				figure.remove((IFigure)o);
			}
			// Now restore the expanded children, border and layout
			configureExpandedFigure(figure);
			refreshChildren();
		}
		
		refreshSourceConnections();
		refreshTargetConnections();
		
		// Switching collapsed states may have changed the border, which is
		// responsible for drawing the drawer markers. Refresh these markers
		// now.
		refreshDrawerImages();
		// Force a repaint, as the drawer images may have changed.
		getFigure().repaint();
	}
	
	public boolean isCollapsed() {
		if (isGenericContainerBorder())
			return getContentFigure().isCollapsed();
		return collapsed;
	}
	
	protected boolean isCollapsable() {
		return true;
	}

	@Override
	protected void unregisterVisuals() {
		this.image = null;
		this.editPartMarkerDecorator = null;
		this.topImage = null;
		this.bottomImage = null;
		super.unregisterVisuals();
	}

	protected void addCollapsedContents(IFigure figure) {
		figure.setLayoutManager(new FlowLayout());
		
		if (!isGenericContainerBorder()) {
			LeafBorder lBorder = new LeafBorder(figure);
			// FIX: We have to set the editpart!
			lBorder.setEditPart(this);
			figure.setBorder(lBorder);
			figure.addMouseMotionListener(getMouseMotionListener());
			figure.add(collapsedLabel);
		}
	}
	@Override
	protected List getModelChildren() {
		if (isCollapsed()) return Collections.EMPTY_LIST;

		return getExpandedChildren();
	}
	
	protected boolean isPointInCollapseIcon(Point point) {
		if (isGenericContainerBorder()) {
			return getContentFigure().isPointInCollapseImage(point.x, point.y);
		}
		return true;
	}
	
	@Override
	public DragTracker getDragTracker(Request request) {
		return new BPELDragEditPartsTracker(this) {
			@Override
			protected boolean handleDoubleClick(int button) {
				if (!isGenericContainerBorder()) 
					setCollapsed(!isCollapsed());
				return true;
			}
			
			@Override
			protected boolean handleButtonDown(int button) {
				if (isGenericContainerBorder()) {
					if (isPointInCollapseIcon(getLocation())) {
						setCollapsed(!isCollapsed());
						return true;
					}
				}
				return super.handleButtonDown(button);
			}
		};	
	}
	
	/**
	 * @see org.eclipse.gef.editparts.AbstractEditPart#refreshVisuals()
	 */
	@Override
	public void refreshVisuals() {
		super.refreshVisuals();
		// The name has changed, change the corresponding label
		if (!isGenericContainerBorder()) {
			collapsedLabel.setText(getLabel());
		} else {
			getContentFigure().setName(getLabel());
			getContentFigure().revalidate();
		}
		// Refresh any decorations on this edit part
		editPartMarkerDecorator.refresh();
		refreshDrawerImages();
		// Force a repaint, as the drawer images may have changed.
		getFigure().repaint();
	}
	/**
	 * Return the image that was created for the receiver. This image corresponds
	 * to the image descriptor which the subclass provided in getImageDescriptor().
	 * 
	 * This image may be used by subclasses for the lifetime of this edit part.
	 * TODO: Get rid of this. Subclasses should handle the lifecycle.
	 */	
	protected Image getImage() {
		return image;
	}
	/**
	 * Return an image which should be displayed in this node while
	 * collapsed. Subclasses may override.
	 */
	protected Image getImg() {
		ILabeledElement element = BPELUtil.adapt(getActivity(), ILabeledElement.class);
		if (element != null) {
			return element.getSmallImage(getActivity());
		}
		return null;
	}
	
	/**
	 * Return a string which should be displayed in this node while collapsed.
	 */
	protected String getLabel() {
		ILabeledElement element = BPELUtil.adapt(getActivity(), ILabeledElement.class);
		if (element != null) {
			return element.getLabel(getActivity());
		}
		return null;
	}
	
	/**
	 * Return a list of the model children that should be displayed while
	 * the node is in expanded mode. Subclasses may override.
	 */
	protected List getExpandedChildren() {
		return super.getModelChildren();
	}
	/**
	 * Configure the given figure for expanded mode. Subclasses should do work
	 * in here to set border and layout information for expanded mode.
	 */
	protected abstract void configureExpandedFigure(IFigure figure);
	
	/**
	 * A CollapsableEditPart has extra responsibilities for source and target connections,
	 * because it must show flow links for any activities which are inside it but not
	 * visible (in the case that the CollapsableEditPart is collapsed).
	 */	
	@Override
	protected List<Link> getModelSourceConnections() {
		final List<Link> result = super.getModelSourceConnections();
//		if (isCollapsed()) {
//			BPELUtil.visitModelDepthFirst(getActivity(), new IModelVisitor() {
//				public boolean visit(Object modelObject) {
//					if (modelObject instanceof Activity) {
//						List sources = ((Activity)modelObject).getSources();
//						Iterator it = sources.iterator();
//						while (it.hasNext()) {
//							Source source = (Source)it.next();
//							Target target = source.getLink().getTarget();
//							Activity targetActivity = target.getActivity();
//							// If the target is not contained in this model object, add the 
//							// connection.
//							if (!containsModel(targetActivity)) {
//								result.add(source.getLink());
//							}
//						}
//					}
//					return true;
//				}
//			});
//		}
		return result;
	}

	/**
	 * A CollapsableEditPart has extra responsibilities for source and target connections,
	 * because it must show flow links for any activities which are inside it but not
	 * visible (in the case that the CollapsableEditPart is collapsed).
	 */
	@Override
	protected List<Link> getModelTargetConnections() {
		final List<Link> result = super.getModelTargetConnections();
//		if (isCollapsed()) {
//			BPELUtil.visitModelDepthFirst(getActivity(), new IModelVisitor() {
//				public boolean visit(Object modelObject) {
//					if (modelObject instanceof Activity) {
//						List targets = ((Activity)modelObject).getTargets();
//						Iterator it = targets.iterator();
//						while (it.hasNext()) {
//							Target target = (Target)it.next();
//							Source source = target.getLink().getSource();
//							Activity sourceActivity = source.getActivity();
//							// If the source is not contained in this model object, add the 
//							// connection.
//							if (!containsModel(sourceActivity)) {
//								result.add(target.getLink());
//							}
//						}
//					}
//					return true;
//				}
//			});
//		}
		return result;
	}
	
	@Override
	public IFigure getContentPane() {
		return contentFigure;
	}

	public Label getCollapsedLabel() {
		return collapsedLabel;
	}

	protected void refreshDrawerImages() {
		DrawerBorder border = (DrawerBorder)getContentPane().getBorder();
		
		if (topImage != null) {
			topImage.dispose();
			topImage = null;
		}
		if (bottomImage != null) {
			bottomImage.dispose();
			bottomImage = null;
		}
		IMarkerHolder holder = BPELUtil.adapt(getActivity(), IMarkerHolder.class);
		
		int topMarkerPriority = Integer.MIN_VALUE;
		int bottomMarkerPriority = Integer.MIN_VALUE;
		IMarker topMarker = null;
		IMarker bottomMarker = null;
		
		for (IMarker marker : holder.getMarkers(getActivity())) {
			
			if (marker.getAttribute(IModelMarkerConstants.DECORATION_MARKER_VISIBLE_ATTR, true) == false) {
				continue;
			}
			
			String value = marker.getAttribute(IModelMarkerConstants.DECORATION_GRAPHICAL_MARKER_ANCHOR_POINT_ATTR,EMPTY_STRING);  
			
			if (value.equals(IBPELUIConstants.MARKER_ANCHORPOINT_DRAWER_TOP)) {
				int priority = marker.getAttribute(IModelMarkerConstants.DECORATION_MARKER_PRIORITY_ATTR, 0);
				if (priority > topMarkerPriority) {
					topMarkerPriority = priority;
					topImage = BPELUtil.getImage(marker);
					topMarker = marker;
				}
			} else if (value.equals(IBPELUIConstants.MARKER_ANCHORPOINT_DRAWER_BOTTOM)) {
				int priority = marker.getAttribute(IModelMarkerConstants.DECORATION_MARKER_PRIORITY_ATTR, 0);
				if (priority > bottomMarkerPriority) {
					bottomMarkerPriority = priority;
					bottomImage = BPELUtil.getImage(marker);
					bottomMarker = marker;
				}
			}
		}
		
		border.setTopImage(topImage);
		border.setBottomImage(bottomImage);
		border.setTopMarker(topMarker);
		border.setBottomMarker(bottomMarker);
	}
	
	/**
	 * Get an anchor at the given location for this edit part.
	 * 
	 * This must be called after the figure for this edit part has been created.
	 */
	@Override
	public ConnectionAnchor getConnectionAnchor(int location) {
		switch(location){
		case CenteredConnectionAnchor.TOP_INNER:
			return new CenteredConnectionAnchor(getFigure(), location, 30);
		case CenteredConnectionAnchor.LEFT:
			return new CenteredConnectionAnchor(getFigure(), CenteredConnectionAnchor.LEFT_INNER, 0);
		case CenteredConnectionAnchor.RIGHT:
			return new CenteredConnectionAnchor(getFigure(), CenteredConnectionAnchor.RIGHT_INNER, 0);
		default:
				return super.getConnectionAnchor(location);
		}
	}
}
