/**
 * Copyright (c) 2013 committers of YAKINDU 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:
 * 	committers of YAKINDU - initial API and implementation
 * 
 */
package org.yakindu.sct.ui.editor.policies;

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

import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.PrecisionRectangle;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.gef.EditPart;
import org.eclipse.gef.Request;
import org.eclipse.gef.RequestConstants;
import org.eclipse.gef.commands.Command;
import org.eclipse.gef.commands.CompoundCommand;
import org.eclipse.gef.editpolicies.AbstractEditPolicy;
import org.eclipse.gef.requests.ChangeBoundsRequest;
import org.eclipse.gmf.runtime.diagram.ui.commands.ICommandProxy;
import org.eclipse.gmf.runtime.diagram.ui.commands.SetBoundsCommand;
import org.eclipse.gmf.runtime.diagram.ui.editparts.IGraphicalEditPart;
import org.eclipse.gmf.runtime.diagram.ui.l10n.DiagramUIMessages;
import org.eclipse.gmf.runtime.emf.core.util.EObjectAdapter;
import org.yakindu.sct.ui.editor.editparts.RegionEditPart;
import org.yakindu.sct.ui.editor.editparts.StateEditPart;

/**
 * 
 * @author andreas muelder - Initial contribution and API
 * 
 */
public class EnlargeContainerEditPolicy extends AbstractEditPolicy {

	// Key for edit policy installation
	public static final Object ROLE = "ResizeContainer";

	// Space between the border of the container and the moved figure
	public static final int SPACEING = 25;

	private Map<IFigure, Rectangle> boundsCache = new HashMap<IFigure, Rectangle>();

	private List<IGraphicalEditPart> containerHierachy;

	@SuppressWarnings("unchecked")
	@Override
	public Command getCommand(Request request) {
		if (!RequestConstants.REQ_RESIZE.equals(request.getType())
				&& !RequestConstants.REQ_MOVE.equals(request.getType())) {
			return null;
		}

		if (request instanceof SetPreferredSizeRequest) {
			showSourceFeedback(request);
		}

		ChangeBoundsRequest cbr = (ChangeBoundsRequest) request;
		CompoundCommand result = new CompoundCommand();

		// Update Bounds of the container hierachy
		for (IGraphicalEditPart currentContainer : containerHierachy) {
			IFigure figure = currentContainer.getFigure();
			SetBoundsCommand boundsCommand = new SetBoundsCommand(getHost().getEditingDomain(),
					DiagramUIMessages.SetLocationCommand_Label_Resize, new EObjectAdapter(
							currentContainer.getNotationView()), figure.getBounds());
			result.add(new ICommandProxy(boundsCommand));

			// Update child bounds of elements that stand in the way...
			List<IGraphicalEditPart> children = currentContainer.getParent().getChildren();
			for (IGraphicalEditPart childPart : children) {
				if (cbr.getEditParts().contains(childPart))
					continue;
				IFigure childFigure = childPart.getFigure();
				if (childPart == currentContainer)
					continue;
				SetBoundsCommand childBoundsCommand = new SetBoundsCommand(getHost().getEditingDomain(),
						DiagramUIMessages.SetLocationCommand_Label_Resize, new EObjectAdapter(
								childPart.getNotationView()), childFigure.getBounds());
				result.add(new ICommandProxy(childBoundsCommand));
			}
		}
		return result;
	}

	@Override
	public IGraphicalEditPart getHost() {
		return (IGraphicalEditPart) super.getHost();
	}

	@Override
	public void showSourceFeedback(Request request) {
		if (containerHierachy == null) {
			containerHierachy = collectContainerHierachy();
		}
		if (!RequestConstants.REQ_RESIZE.equals(request.getType())
				&& !RequestConstants.REQ_MOVE.equals(request.getType()))
			return;
		showContainerFeedback((ChangeBoundsRequest) request);
	}

	@Override
	public void eraseSourceFeedback(Request request) {
		boundsCache.clear();
		containerHierachy = null;
		super.eraseSourceFeedback(request);
	}

	protected void showContainerFeedback(ChangeBoundsRequest request) {
		for (int level = 1; level <= containerHierachy.size(); level++) {
			IGraphicalEditPart containerEditPart = containerHierachy.get(level - 1);
			IFigure containerFigure = containerEditPart.getFigure();
			Rectangle feedbackBounds = getOriginalBounds(containerFigure);
			containerFigure.getParent().translateToAbsolute(feedbackBounds);
			feedbackBounds = calculateFeedbackBounds(request, feedbackBounds, level, containerFigure);
			showChildrenFeedback(containerEditPart, containerFigure, feedbackBounds, request);
			containerFigure.translateToRelative(feedbackBounds);
			setBounds(containerFigure, feedbackBounds);
		}
	}

	protected List<IGraphicalEditPart> collectContainerHierachy() {
		List<IGraphicalEditPart> result = new ArrayList<IGraphicalEditPart>();
		IGraphicalEditPart containerEditPart = (IGraphicalEditPart) getHost();
		while (containerEditPart != null) {
			containerEditPart = getContainer(containerEditPart);
			if (containerEditPart != null)
				result.add(containerEditPart);
		}
		return result;
	}

	@SuppressWarnings("unchecked")
	/**
	 * containerFeedbackBounds as absolute
	 * 
	 * @param containerEditPart
	 * @param containerFigure
	 * @param containerFeedbackBounds
	 */
	protected void showChildrenFeedback(final IGraphicalEditPart containerEditPart, final IFigure containerFigure,
			final Rectangle containerFeedbackBounds, ChangeBoundsRequest request) {
		Rectangle originalBounds = getOriginalBounds(containerFigure);

		Point moveDelta = new Point(containerFeedbackBounds.width - originalBounds.width,
				containerFeedbackBounds.height - originalBounds.height);

		List<IGraphicalEditPart> children = containerEditPart.getParent().getChildren();

		for (IGraphicalEditPart childPart : children) {
			if (request.getEditParts().contains(childPart)) {
				continue;
			}
			if (childPart == containerEditPart)
				continue;
			showChildFeedback(childPart, moveDelta, containerFeedbackBounds);
		}
	}

	protected void showChildFeedback(IGraphicalEditPart childPart, Point moveDelta, Rectangle containerFeedbackBounds) {

		IFigure childFigure = childPart.getFigure();
		Rectangle originalChildBounds = getOriginalBounds(childFigure);
		childFigure.getParent().translateToAbsolute(originalChildBounds);

		boolean horizontalAffected = isHorizontalAffected(containerFeedbackBounds, moveDelta, originalChildBounds);
		boolean verticalAffected = isVerticalAffected(containerFeedbackBounds, moveDelta, originalChildBounds);
		if (!(horizontalAffected || verticalAffected)) {
			return;
		}
		if (horizontalAffected) {
			originalChildBounds.x += moveDelta.x;
		}
		if (verticalAffected) {
			originalChildBounds.y += moveDelta.y;
		}
		childFigure.getParent().translateToRelative(originalChildBounds);
		setBounds(childFigure, originalChildBounds);
	}

	/**
	 * Return a copy of the bounds from the cache
	 * 
	 * @param figure
	 * @return
	 */
	private Rectangle getOriginalBounds(IFigure figure) {
		Rectangle originalContainerBounds = boundsCache.get(figure);
		if (originalContainerBounds == null) {
			originalContainerBounds = figure.getBounds().getCopy();
			boundsCache.put(figure, originalContainerBounds);
		}
		return boundsCache.get(figure).getCopy();
	}

	private IGraphicalEditPart getContainer(IGraphicalEditPart host) {
		IGraphicalEditPart containerEditPart = getParentState(host);
		if (containerEditPart == null) {
			containerEditPart = getParentRegion(host);
			if (containerEditPart == null)
				return null;
		}
		return containerEditPart;
	}

	private boolean isVerticalAffected(Rectangle newBounds, Point moveDelta, Rectangle bounds) {
		boolean verticalAffected = (bounds.x > newBounds.x || bounds.x + bounds.width > newBounds.x)
				&& bounds.x < newBounds.x + newBounds.width || bounds.x + bounds.width < newBounds.x + newBounds.width;
		if (verticalAffected) {
			verticalAffected = bounds.y + moveDelta.y > newBounds.y + newBounds.height;
		}
		return verticalAffected;
	}

	private boolean isHorizontalAffected(Rectangle newBounds, Point moveDelta, Rectangle bounds) {
		boolean horizontalAffected = (bounds.y > newBounds.y || bounds.y + bounds.height > newBounds.y)
				&& bounds.y < newBounds.y + newBounds.height
				|| bounds.y + bounds.height < newBounds.y + newBounds.height;
		if (horizontalAffected) {
			horizontalAffected = bounds.x + moveDelta.x > newBounds.x + newBounds.width;
		}
		return horizontalAffected;
	}

	protected void setBounds(IFigure figure, Rectangle bounds) {
		figure.setBounds(bounds);
		figure.getParent().setConstraint(figure, bounds);
	}

	@SuppressWarnings({ "unchecked" })
	private Rectangle calculateFeedbackBounds(ChangeBoundsRequest request, Rectangle feedbackBounds, int level,
			IFigure containerFigure) {
		Rectangle result = feedbackBounds.getCopy();
		List<IGraphicalEditPart> editParts = request.getEditParts();
		for (IGraphicalEditPart editPart : editParts) {
			PrecisionRectangle transformedRect = new PrecisionRectangle(editPart.getFigure().getBounds());
			editPart.getFigure().translateToAbsolute(transformedRect);
			transformedRect.translate(request.getMoveDelta());
			transformedRect.resize(request.getSizeDelta());
			transformedRect.expand(SPACEING * level, SPACEING * level);
			result.union(transformedRect);
			Dimension preferredSize = containerFigure.getPreferredSize().getCopy();
			editPart.getFigure().translateToAbsolute(preferredSize);
			Dimension max = Dimension.max(result.getSize(), preferredSize);
			result.setSize(max);
			if (result.x < feedbackBounds.x || result.y < feedbackBounds.y) {
				return feedbackBounds;
			}
		}
		return result;
	}

	protected IGraphicalEditPart getParentRegion(EditPart part) {
		part = part.getParent();
		while (!(part instanceof RegionEditPart)) {
			part = part.getParent();
			if (part == null)
				return null;
		}
		return (IGraphicalEditPart) part;
	}

	protected IGraphicalEditPart getParentState(EditPart part) {
		part = part.getParent();
		while (!(part instanceof StateEditPart)) {
			part = part.getParent();
			if (part == null)
				return null;
		}
		return (IGraphicalEditPart) part;
	}
}
