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

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

import org.eclipse.draw2d.Connection;
import org.eclipse.draw2d.PositionConstants;
import org.eclipse.draw2d.geometry.Point;

import org.eclipse.gef.AccessibleAnchorProvider;
import org.eclipse.gef.ConnectionEditPart;
import org.eclipse.gef.DragTracker;
import org.eclipse.gef.EditPart;
import org.eclipse.gef.Request;
import org.eclipse.gef.RequestConstants;
import org.eclipse.gef.SharedCursors;
import org.eclipse.gef.Tool;
import org.eclipse.gef.requests.ReconnectRequest;

/**
 * A DragTracker that moves the endpoint of a connection to another location.
 */
public class ConnectionEndpointTracker extends TargetingTool implements
		DragTracker {

	private static final int FLAG_SOURCE_FEEBBACK = TargetingTool.MAX_FLAG << 1;
	/** The max flag */
	protected static final int MAX_FLAG = FLAG_SOURCE_FEEBBACK;

	private String commandName;
	private List exclusionSet;

	private ConnectionEditPart connectionEditPart;

	/**
	 * Constructs a new ConnectionEndpointTracker for the given
	 * ConnectionEditPart.
	 * 
	 * @param cep
	 *            the ConnectionEditPart
	 */
	public ConnectionEndpointTracker(ConnectionEditPart cep) {
		setConnectionEditPart(cep);
		setDisabledCursor(SharedCursors.NO);
	}

	/**
	 * Returns a custom "plug" cursor if this tool is in the initial, drag or
	 * accessible drag state. Otherwise defers to <code>super</code>.
	 * 
	 * @return the cursor
	 */
	protected Cursor calculateCursor() {
		if (isInState(STATE_INITIAL | STATE_DRAG | STATE_ACCESSIBLE_DRAG))
			return getDefaultCursor();
		return super.calculateCursor();
	}

	/**
	 * Erases source and target feedback and executes the current command.
	 * 
	 * @see DragTracker#commitDrag()
	 */
	public void commitDrag() {
		eraseSourceFeedback();
		eraseTargetFeedback();
		executeCurrentCommand();
	}

	/**
	 * Creates the target request, a {@link ReconnectRequest}.
	 * 
	 * @return the target request
	 */
	protected Request createTargetRequest() {
		ReconnectRequest request = new ReconnectRequest(getCommandName());
		request.setConnectionEditPart(getConnectionEditPart());
		return request;
	}

	/**
	 * Erases feedback and sets the viewer's focus to <code>null</code>. This
	 * will remove any focus rectangles that were painted to show the new target
	 * or source edit part.
	 * 
	 * @see Tool#deactivate()
	 */
	public void deactivate() {
		eraseSourceFeedback();
		getCurrentViewer().setFocus(null);
		super.deactivate();
	}

	/**
	 * Erases the source feedback.
	 */
	protected void eraseSourceFeedback() {
		if (!getFlag(FLAG_SOURCE_FEEBBACK))
			return;
		setFlag(FLAG_SOURCE_FEEBBACK, false);
		getConnectionEditPart().eraseSourceFeedback(getTargetRequest());
	}

	/**
	 * @see AbstractTool#getCommandName()
	 */
	protected String getCommandName() {
		return commandName;
	}

	/**
	 * Returns the ConnectionEditPart's figure.
	 * 
	 * @return the connection
	 */
	protected Connection getConnection() {
		return (Connection) getConnectionEditPart().getFigure();
	}

	/**
	 * Returns the ConnectionEditPart.
	 * 
	 * @return the ConnectionEditPart
	 */
	protected ConnectionEditPart getConnectionEditPart() {
		return connectionEditPart;
	}

	/**
	 * @see AbstractTool#getDebugName()
	 */
	protected String getDebugName() {
		return "Connection Endpoint Tool";//$NON-NLS-1$
	}

	/**
	 * @see org.eclipse.gef.tools.TargetingTool#getExclusionSet()
	 */
	protected Collection getExclusionSet() {
		if (exclusionSet == null) {
			exclusionSet = new ArrayList();
			exclusionSet.add(getConnection());
		}
		return exclusionSet;
	}

	/**
	 * If currently in the drag-in-progress state, it goes into the terminal
	 * state erases feedback and executes the current command.
	 * 
	 * @see org.eclipse.gef.tools.AbstractTool#handleButtonUp(int)
	 */
	protected boolean handleButtonUp(int button) {
		if (stateTransition(STATE_DRAG_IN_PROGRESS, STATE_TERMINAL)) {
			eraseSourceFeedback();
			eraseTargetFeedback();
			executeCurrentCommand();
		}
		return true;
	}

	/**
	 * Updates the request and the mouse target, asks to show feedback, and gets
	 * the current command.
	 * 
	 * @return <code>true</code>
	 */
	protected boolean handleDragInProgress() {
		updateTargetRequest();
		updateTargetUnderMouse();
		showSourceFeedback();
		showTargetFeedback();
		setCurrentCommand(getCommand());
		return true;
	}

	/**
	 * @see org.eclipse.gef.tools.AbstractTool#handleDragStarted()
	 */
	protected boolean handleDragStarted() {
		stateTransition(STATE_INITIAL, STATE_DRAG_IN_PROGRESS);
		return false;
	}

	/**
	 * @see org.eclipse.gef.tools.TargetingTool#handleHover()
	 */
	protected boolean handleHover() {
		if (isInDragInProgress())
			updateAutoexposeHelper();
		return true;
	}

	/**
	 * Processes the arrow keys (to choose a different source or target edit
	 * part) and forwardslash and backslash keys (to try to connect to another
	 * connection).
	 * 
	 * @see org.eclipse.gef.tools.AbstractTool#handleKeyDown(org.eclipse.swt.events.KeyEvent)
	 */
	protected boolean handleKeyDown(KeyEvent e) {
		if (acceptArrowKey(e)) {
			if (stateTransition(STATE_INITIAL,
					STATE_ACCESSIBLE_DRAG_IN_PROGRESS)) {
				// When the drag first starts, set the focus Part to be one end
				// of the connection
				if (isTarget()) {
					getCurrentViewer().setFocus(
							getConnectionEditPart().getTarget());
					getCurrentViewer().reveal(
							getConnectionEditPart().getTarget());
				} else {
					getCurrentViewer().setFocus(
							getConnectionEditPart().getSource());
					getCurrentViewer().reveal(
							getConnectionEditPart().getSource());
				}
			}
			int direction = 0;
			switch (e.keyCode) {
			case SWT.ARROW_DOWN:
				direction = PositionConstants.SOUTH;
				break;
			case SWT.ARROW_UP:
				direction = PositionConstants.NORTH;
				break;
			case SWT.ARROW_RIGHT:
				direction = isCurrentViewerMirrored() ? PositionConstants.WEST
						: PositionConstants.EAST;
				break;
			case SWT.ARROW_LEFT:
				direction = isCurrentViewerMirrored() ? PositionConstants.EAST
						: PositionConstants.WEST;
				break;
			}

			boolean consumed = false;
			if (direction != 0 && e.stateMask == 0)
				consumed = navigateNextAnchor(direction);
			if (!consumed) {
				e.stateMask |= SWT.CONTROL;
				e.stateMask &= ~SWT.SHIFT;
				if (getCurrentViewer().getKeyHandler().keyPressed(e)) {
					navigateNextAnchor(0);
					return true;
				}
			}
		}
		if (e.character == '/' || e.character == '\\') {
			e.stateMask |= SWT.CONTROL;
			if (getCurrentViewer().getKeyHandler().keyPressed(e)) {
				// Do not try to connect to the same connection being dragged.
				if (getCurrentViewer().getFocusEditPart() != getConnectionEditPart())
					navigateNextAnchor(0);
				return true;
			}
		}

		return false;
	}

	private boolean isTarget() {
		return getCommandName() == RequestConstants.REQ_RECONNECT_TARGET;
	}

	boolean navigateNextAnchor(int direction) {
		EditPart focus = getCurrentViewer().getFocusEditPart();
		AccessibleAnchorProvider provider;
		provider = (AccessibleAnchorProvider) focus
				.getAdapter(AccessibleAnchorProvider.class);
		if (provider == null)
			return false;

		List list;
		if (isTarget())
			list = provider.getTargetAnchorLocations();
		else
			list = provider.getSourceAnchorLocations();

		Point start = getLocation();
		int distance = Integer.MAX_VALUE;
		Point next = null;
		for (int i = 0; i < list.size(); i++) {
			Point p = (Point) list.get(i);
			if (p.equals(start)
					|| (direction != 0 && (start.getPosition(p) != direction)))
				continue;
			int d = p.getDistanceOrthogonal(start);
			if (d < distance) {
				distance = d;
				next = p;
			}
		}

		if (next != null) {
			placeMouseInViewer(next);
			return true;
		}
		return false;
	}

	/**
	 * Sets the command name.
	 * 
	 * @param newCommandName
	 *            the new command name
	 */
	public void setCommandName(String newCommandName) {
		commandName = newCommandName;
	}

	/**
	 * Sets the connection edit part that is being reconnected.
	 * 
	 * @param cep
	 *            the connection edit part
	 */
	public void setConnectionEditPart(ConnectionEditPart cep) {
		this.connectionEditPart = cep;
	}

	/**
	 * Asks the ConnectionEditPart to show source feedback.
	 */
	protected void showSourceFeedback() {
		getConnectionEditPart().showSourceFeedback(getTargetRequest());
		setFlag(FLAG_SOURCE_FEEBBACK, true);
	}

	/**
	 * Updates the request location.
	 * 
	 * @see org.eclipse.gef.tools.TargetingTool#updateTargetRequest()
	 */
	protected void updateTargetRequest() {
		ReconnectRequest request = (ReconnectRequest) getTargetRequest();
		Point p = getLocation();
		request.setLocation(p);
	}

}