/*******************************************************************************
 * Copyright (c) 2000, 2010 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.gef.tools;

import java.util.List;

import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Cursor;

import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.PositionConstants;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.PrecisionDimension;
import org.eclipse.draw2d.geometry.PrecisionPoint;
import org.eclipse.draw2d.geometry.PrecisionRectangle;

import org.eclipse.gef.EditPart;
import org.eclipse.gef.GraphicalEditPart;
import org.eclipse.gef.Request;
import org.eclipse.gef.SharedCursors;
import org.eclipse.gef.SnapToHelper;
import org.eclipse.gef.commands.Command;
import org.eclipse.gef.commands.CompoundCommand;
import org.eclipse.gef.handles.HandleBounds;
import org.eclipse.gef.requests.ChangeBoundsRequest;

/**
 * A Tracker for dragging a resize handle. The ResizeTracker will resize all of
 * the selected editparts in the viewer which understand a RESIZE request. A
 * {@link ChangeBoundsRequest} is sent to each member of the operation set. The
 * tracker allows for the resize direction to be specified in the constructor.
 * 
 * @author hudsonr
 */
public class ResizeTracker extends SimpleDragTracker {

	/**
	 * Key modifier for centered resizing. It's ALT on all platforms.
	 */
	static final int MODIFIER_CENTERED_RESIZE = SWT.ALT;

	/**
	 * Key modifier for constrained resizing. It's SHIFT on all platforms.
	 */
	static final int MODIFIER_CONSTRAINED_RESIZE = SWT.SHIFT;

	private static int FLAG_TARGET_FEEDBACK = SimpleDragTracker.MAX_FLAG << 1;

	/**
	 * The maximum flag used by this class.
	 */
	protected static final int MAX_FLAG = new Integer(FLAG_TARGET_FEEDBACK)
			.intValue();

	private int direction;
	private GraphicalEditPart owner;
	private PrecisionRectangle sourceRect;
	private SnapToHelper snapToHelper;

	/**
	 * Constructs a resize tracker that resizes in the specified direction. The
	 * direction is specified using {@link PositionConstants#NORTH},
	 * {@link PositionConstants#NORTH_EAST}, etc.
	 * 
	 * @deprecated use ResizeTracker(GraphicalEditPart, int) instead
	 * @param direction
	 *            the direction
	 */
	public ResizeTracker(int direction) {
		this(null, direction);
	}

	/**
	 * Constructs a resize tracker that resizes in the specified direction. The
	 * direction is specified using {@link PositionConstants#NORTH},
	 * {@link PositionConstants#NORTH_EAST}, etc.
	 * 
	 * @param owner
	 *            of the resize handle which returned this tracker
	 * @param direction
	 *            the direction
	 */
	public ResizeTracker(GraphicalEditPart owner, int direction) {
		this.owner = owner;
		this.direction = direction;
		setDisabledCursor(SharedCursors.NO);
	}

	/**
	 * @see org.eclipse.gef.Tool#activate()
	 */
	public void activate() {
		super.activate();
		if (owner != null) {
			if (getTargetEditPart() != null)
				snapToHelper = (SnapToHelper) getTargetEditPart().getAdapter(
						SnapToHelper.class);

			IFigure figure = owner.getFigure();
			if (figure instanceof HandleBounds)
				sourceRect = new PrecisionRectangle(
						((HandleBounds) figure).getHandleBounds());
			else
				sourceRect = new PrecisionRectangle(figure.getBounds());
			figure.translateToAbsolute(sourceRect);
		}
	}

	/**
	 * @see org.eclipse.gef.tools.AbstractTool#commitDrag()
	 */
	public void commitDrag() {
		eraseTargetFeedback();
		super.commitDrag();
	}

	/**
	 * Returns all selected parts which understand resizing.
	 * 
	 * @see org.eclipse.gef.tools.AbstractTool#createOperationSet()
	 */
	protected List createOperationSet() {
		List list = super.createOperationSet();
		ToolUtilities.filterEditPartsUnderstanding(list, getSourceRequest());
		return list;
	}

	/**
	 * @see org.eclipse.gef.tools.SimpleDragTracker#createSourceRequest()
	 */
	protected Request createSourceRequest() {
		ChangeBoundsRequest request;
		request = new ChangeBoundsRequest(REQ_RESIZE);
		request.setResizeDirection(getResizeDirection());
		return request;
	}

	/**
	 * @see org.eclipse.gef.tools.AbstractTool#deactivate()
	 */
	public void deactivate() {
		// For the case where ESC key was hit while resizing
		eraseTargetFeedback();

		sourceRect = null;
		snapToHelper = null;
		super.deactivate();
	}

	/**
	 * This method is invoked when the resize operation is complete. It notifies
	 * the {@link #getTargetEditPart() target} to erase target feedback.
	 */
	protected void eraseTargetFeedback() {
		if (!getFlag(FLAG_TARGET_FEEDBACK))
			return;
		if (getTargetEditPart() != null)
			getTargetEditPart().eraseTargetFeedback(getSourceRequest());
		setFlag(FLAG_TARGET_FEEDBACK, false);
	}

	/**
	 * @see org.eclipse.gef.tools.AbstractTool#getCommand()
	 */
	protected Command getCommand() {
		List editparts = getOperationSet();
		EditPart part;
		CompoundCommand command = new CompoundCommand();
		command.setDebugLabel("Resize Handle Tracker");//$NON-NLS-1$
		for (int i = 0; i < editparts.size(); i++) {
			part = (EditPart) editparts.get(i);
			command.add(part.getCommand(getSourceRequest()));
		}
		return command.unwrap();
	}

	/**
	 * @see org.eclipse.gef.tools.AbstractTool#getCommandName()
	 */
	protected String getCommandName() {
		return REQ_RESIZE;
	}

	/**
	 * @see org.eclipse.gef.tools.AbstractTool#getDefaultCursor()
	 */
	protected Cursor getDefaultCursor() {
		return SharedCursors.getDirectionalCursor(direction,
				getTargetEditPart().getFigure().isMirrored());
	}

	/**
	 * @see org.eclipse.gef.tools.AbstractTool#getDebugName()
	 */
	protected String getDebugName() {
		return "Resize Handle Tracker";//$NON-NLS-1$
	}

	/**
	 * Returns the owner of this resize tracker.
	 * 
	 * @since 3.7
	 */
	protected GraphicalEditPart getOwner() {
		return owner;
	}

	/**
	 * Returns the direction of the resize (NORTH, EAST, NORTH_EAST, etc.).
	 * These constants are from {@link PositionConstants}.
	 * 
	 * @return the resize direction.
	 */
	protected int getResizeDirection() {
		return direction;
	}

	/**
	 * The TargetEditPart is the parent of the EditPart being resized.
	 * 
	 * @return The target EditPart; may be <code>null</code> in 2.1 applications
	 *         that use the now deprecated
	 *         {@link ResizeTracker#ResizeTracker(int) constructor}.
	 */
	protected GraphicalEditPart getTargetEditPart() {
		if (owner != null)
			return (GraphicalEditPart) owner.getParent();
		return null;
	}

	/**
	 * If dragging is in progress, cleans up feedback and calls performDrag().
	 * 
	 * @see org.eclipse.gef.tools.SimpleDragTracker#handleButtonUp(int)
	 */
	protected boolean handleButtonUp(int button) {
		if (stateTransition(STATE_DRAG_IN_PROGRESS, STATE_TERMINAL)) {
			eraseSourceFeedback();
			eraseTargetFeedback();
			performDrag();
		}
		return true;
	}

	/**
	 * Updates the command and the source request, and shows feedback.
	 * 
	 * @see org.eclipse.gef.tools.SimpleDragTracker#handleDragInProgress()
	 */
	protected boolean handleDragInProgress() {
		if (isInDragInProgress()) {
			updateSourceRequest();
			showSourceFeedback();
			showTargetFeedback();
			setCurrentCommand(getCommand());
		}
		return true;
	}

	/**
	 * This method is invoked as the drag is happening. It notifies the
	 * {@link #getTargetEditPart() target} to show target feedback.
	 */
	protected void showTargetFeedback() {
		setFlag(FLAG_TARGET_FEEDBACK, true);
		if (getTargetEditPart() != null)
			getTargetEditPart().showTargetFeedback(getSourceRequest());
	}

	/**
	 * @see org.eclipse.gef.tools.SimpleDragTracker#updateSourceRequest()
	 */
	protected void updateSourceRequest() {
		ChangeBoundsRequest request = (ChangeBoundsRequest) getSourceRequest();
		Dimension d = getDragMoveDelta();

		Point location = new Point(getLocation());
		Point moveDelta = new Point(0, 0);
		Dimension resizeDelta = new Dimension(0, 0);

		request.setConstrainedResize(getCurrentInput().isModKeyDown(
				MODIFIER_CONSTRAINED_RESIZE));
		request.setCenteredResize(getCurrentInput().isModKeyDown(
				MODIFIER_CENTERED_RESIZE));
		request.setSnapToEnabled(!getCurrentInput().isModKeyDown(
				MODIFIER_NO_SNAPPING));

		if (request.isConstrainedResize() && owner != null) {
			request.setConstrainedResize(true);

			int origHeight = owner.getFigure().getBounds().height;
			int origWidth = owner.getFigure().getBounds().width;
			float ratio = 1;

			if (origWidth != 0 && origHeight != 0)
				ratio = ((float) origHeight / (float) origWidth);

			if (getResizeDirection() == PositionConstants.SOUTH_EAST) {
				if (d.height > (d.width * ratio))
					d.width = (int) (d.height / ratio);
				else
					d.height = (int) (d.width * ratio);
			} else if (getResizeDirection() == PositionConstants.NORTH_WEST) {
				if (d.height < (d.width * ratio))
					d.width = (int) (d.height / ratio);
				else
					d.height = (int) (d.width * ratio);
			} else if (getResizeDirection() == PositionConstants.NORTH_EAST) {
				if (-(d.height) > (d.width * ratio))
					d.width = -(int) (d.height / ratio);
				else
					d.height = -(int) (d.width * ratio);
			} else if (getResizeDirection() == PositionConstants.SOUTH_WEST) {
				if (-(d.height) < (d.width * ratio))
					d.width = -(int) (d.height / ratio);
				else
					d.height = -(int) (d.width * ratio);
			}
		}

		if ((getResizeDirection() & PositionConstants.NORTH) != 0) {
			if (request.isCenteredResize()) {
				resizeDelta.height -= d.height;
			}
			moveDelta.y += d.height;
			resizeDelta.height -= d.height;
		}
		if ((getResizeDirection() & PositionConstants.SOUTH) != 0) {
			if (request.isCenteredResize()) {
				moveDelta.y -= d.height;
				resizeDelta.height += d.height;
			}
			resizeDelta.height += d.height;
		}
		if ((getResizeDirection() & PositionConstants.WEST) != 0) {
			if (request.isCenteredResize()) {
				resizeDelta.width -= d.width;
			}
			moveDelta.x += d.width;
			resizeDelta.width -= d.width;
		}
		if ((getResizeDirection() & PositionConstants.EAST) != 0) {
			if (request.isCenteredResize()) {
				moveDelta.x -= d.width;
				resizeDelta.width += d.width;
			}
			resizeDelta.width += d.width;
		}

		request.setMoveDelta(moveDelta);
		request.setSizeDelta(resizeDelta);
		request.setLocation(location);
		request.setEditParts(getOperationSet());
		request.getExtendedData().clear();
		request.setResizeDirection(getResizeDirection());

		if (request.isSnapToEnabled() && snapToHelper != null) {
			PrecisionRectangle rect = sourceRect.getPreciseCopy();
			rect.translate(moveDelta);
			rect.resize(resizeDelta);
			PrecisionRectangle result = new PrecisionRectangle();

			snapToHelper.snapRectangle(request, request.getResizeDirection(),
					rect, result);
			if (request.isCenteredResize()) {
				if (result.preciseX() != 0.0)
					result.setPreciseWidth(result.preciseWidth()
							- result.preciseX());
				else if (result.preciseWidth() != 0.0) {
					result.setPreciseX(-result.preciseWidth());
					result.setPreciseWidth(result.preciseWidth() * 2.0);
				}

				if (result.preciseY() != 0.0)
					result.setPreciseHeight(result.preciseHeight()
							- result.preciseY());
				else if (result.preciseHeight() != 0.0) {
					result.setPreciseY(-result.preciseHeight());
					result.setPreciseHeight(result.preciseHeight() * 2.0);
				}
			}

			PrecisionPoint preciseMove = new PrecisionPoint(result.preciseX()
					+ moveDelta.x, result.preciseY() + moveDelta.y);

			PrecisionDimension preciseResize = new PrecisionDimension(
					result.preciseWidth() + resizeDelta.width,
					result.preciseHeight() + resizeDelta.height);

			request.setMoveDelta(preciseMove);
			request.setSizeDelta(preciseResize);
		}

		enforceConstraintsForResize(request);
	}

	/**
	 * Ensures size constraints (by default minimum and maximum) are respected
	 * by the given request. May be overwritten by clients to enforce additional
	 * constraints.
	 * 
	 * @param changeBoundsRequest
	 *            The request to validate
	 * @since 3.7
	 */
	protected void enforceConstraintsForResize(
			ChangeBoundsRequest changeBoundsRequest) {
		// adjust request, so that minimum and maximum size constraints are
		// respected
		if (owner != null) {
			PrecisionRectangle originalConstraint = new PrecisionRectangle(
					owner.getFigure().getBounds());
			owner.getFigure().translateToAbsolute(originalConstraint);
			PrecisionRectangle manipulatedConstraint = new PrecisionRectangle(
					changeBoundsRequest
							.getTransformedRectangle(originalConstraint));
			owner.getFigure().translateToRelative(manipulatedConstraint);
			// validate constraint (maximum and minimum size are regarded to be
			// 'normalized', i.e. relative to this figure's bounds coordinates).
			manipulatedConstraint.setSize(Dimension.max(
					manipulatedConstraint.getSize(),
					getMinimumSizeFor(changeBoundsRequest)));
			manipulatedConstraint.setSize(Dimension.min(
					manipulatedConstraint.getSize(),
					getMaximumSizeFor(changeBoundsRequest)));
			// translate back to absolute
			owner.getFigure().translateToAbsolute(manipulatedConstraint);
			Dimension newSizeDelta = manipulatedConstraint.getSize()
					.getShrinked(originalConstraint.getSize());
			changeBoundsRequest.setSizeDelta(newSizeDelta);
		}
	}

	/**
	 * Determines the <em>maximum</em> size that the host can be resized to for
	 * a given request. By default, a default value is returned. The value is
	 * interpreted to be a dimension in the host figure's coordinate system
	 * (i.e. relative to its bounds), so it is not affected by zooming affects.
	 * 
	 * @param request
	 *            the ChangeBoundsRequest
	 * @return the minimum size
	 * @since 3.7
	 */
	protected Dimension getMaximumSizeFor(ChangeBoundsRequest request) {
		return IFigure.MAX_DIMENSION;
	}

	/**
	 * Determines the <em>minimum</em> size that the specified child can be
	 * resized to.By default, a default value is returned. The value is
	 * interpreted to be a dimension in the host figure's coordinate system
	 * (i.e. relative to its bounds), so it is not affected by zooming effects.
	 * 
	 * @param request
	 *            the ChangeBoundsRequest
	 * @return the minimum size
	 * @since 3.7
	 */
	protected Dimension getMinimumSizeFor(ChangeBoundsRequest request) {
		return IFigure.MIN_DIMENSION;
	}

}
