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

import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.DragSourceEvent;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;

import org.eclipse.draw2d.geometry.Point;

import org.eclipse.gef.AccessibleHandleProvider;
import org.eclipse.gef.DragTracker;
import org.eclipse.gef.EditDomain;
import org.eclipse.gef.EditPart;
import org.eclipse.gef.EditPartViewer;
import org.eclipse.gef.GraphicalViewer;
import org.eclipse.gef.Handle;
import org.eclipse.gef.KeyHandler;
import org.eclipse.gef.Request;
import org.eclipse.gef.RequestConstants;
import org.eclipse.gef.requests.LocationRequest;
import org.eclipse.gef.requests.SelectionRequest;

/**
 * Tool to select and manipulate figures. A selection tool is in one of three
 * states, e.g., background selection, figure selection, handle manipulation.
 * The different states are handled by different child tools.
 */
public class SelectionTool extends TargetingTool {

	private static final int FLAG_HOVER_FEEDBACK = TargetingTool.MAX_FLAG << 1;
	/** Max flag */
	protected static final int MAX_FLAG = FLAG_HOVER_FEEDBACK;

	/** Traverse handle state */
	protected static final int STATE_TRAVERSE_HANDLE = TargetingTool.MAX_STATE << 1;
	/** Max state */
	protected static final int MAX_STATE = STATE_TRAVERSE_HANDLE;

	private int handleIndex;
	private DragTracker dragTracker;
	private LocationRequest hoverRequest;

	private WeakReference cachedHandlePart;

	/**
	 * Default constructor.
	 */
	public SelectionTool() {
	}

	private boolean acceptTraverseHandle(KeyEvent e) {
		return (e.character == '.' || e.character == '>')
				&& isInState(STATE_INITIAL | STATE_ACCESSIBLE_DRAG
						| STATE_ACCESSIBLE_DRAG_IN_PROGRESS)
				&& ((e.stateMask & (SWT.ALT | SWT.CONTROL)) == 0);
	}

	/**
	 * Creates the hover request (a {@link LocationRequest}) and sets its type
	 * to {@link RequestConstants#REQ_SELECTION_HOVER}.
	 */
	protected void createHoverRequest() {
		hoverRequest = new LocationRequest();
		hoverRequest.setType(RequestConstants.REQ_SELECTION_HOVER);
	}

	/**
	 * Creates a {@link SelectionRequest} for the target request.
	 * 
	 * @see TargetingTool#createTargetRequest()
	 */
	protected Request createTargetRequest() {
		SelectionRequest request = new SelectionRequest();
		request.setType(getCommandName());
		return request;
	}

	/**
	 * Deactivates the tool. This method is called whenever the user switches to
	 * another tool. Use this method to do some clean-up when the tool is
	 * switched. Sets the drag tracker to <code>null</code>.
	 */
	public void deactivate() {
		setDragTracker(null); // deactivates the current drag tracker
		super.deactivate();
	}

	/**
	 * Erases the hover feedback by calling
	 * {@link EditPart#eraseTargetFeedback(Request)}.
	 */
	protected void eraseHoverFeedback() {
		if (getTargetEditPart() == null)
			return;
		if (getTargetHoverRequest() == null)
			return;
		getTargetEditPart().eraseTargetFeedback(getTargetHoverRequest());
	}

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

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

	/**
	 * Returns the current drag tracker.
	 * 
	 * @return the drag tracker
	 */
	protected DragTracker getDragTracker() {
		return dragTracker;
	}

	private EditPart getLastHandleProvider() {
		if (cachedHandlePart == null)
			return null;
		EditPart part = (EditPart) cachedHandlePart.get();
		if (cachedHandlePart.isEnqueued())
			return null;
		return part;
	}

	/**
	 * Returns a new Conditional that evaluates to <code>true</code> if the
	 * queried edit part's {@link EditPart#isSelectable()} method returns
	 * <code>true</code>.
	 * 
	 * @see TargetingTool#getTargetingConditional()
	 */
	protected EditPartViewer.Conditional getTargetingConditional() {
		return new EditPartViewer.Conditional() {
			public boolean evaluate(EditPart editpart) {
				EditPart targetEditPart = editpart
						.getTargetEditPart(getTargetRequest());
				return targetEditPart != null && targetEditPart.isSelectable();
			}
		};
	}

	/**
	 * Returns the target hover request. If <code>null</code>, it will be
	 * created via {@link #createHoverRequest()}.
	 * 
	 * @return the hover request
	 */
	protected Request getTargetHoverRequest() {
		if (hoverRequest == null)
			createHoverRequest();
		return hoverRequest;
	}

	/**
	 * If there is a {@link Handle} under the mouse, this method sets the drag
	 * tracker returned from the handle. If there's an {@link EditPart} under
	 * the mouse, this method sets the drag tracker returned from the edit part.
	 * 
	 * @see AbstractTool#handleButtonDown(int)
	 */
	protected boolean handleButtonDown(int button) {
		if (!stateTransition(STATE_INITIAL, STATE_DRAG)) {
			resetHover();
			return true;
		}
		resetHover();
		EditPartViewer viewer = getCurrentViewer();
		Point p = getLocation();

		if (getDragTracker() != null)
			getDragTracker().deactivate();

		if (viewer instanceof GraphicalViewer) {
			Handle handle = ((GraphicalViewer) viewer).findHandleAt(p);
			if (handle != null) {
				setDragTracker(handle.getDragTracker());
				return true;
			}
		}
		updateTargetRequest();
		((SelectionRequest) getTargetRequest()).setLastButtonPressed(button);
		updateTargetUnderMouse();
		EditPart editpart = getTargetEditPart();
		if (editpart != null) {
			setDragTracker(editpart.getDragTracker(getTargetRequest()));
			lockTargetEditPart(editpart);
			return true;
		}
		return false;
	}

	/**
	 * Resets this tool when the last button is released.
	 * 
	 * @see AbstractTool#handleButtonUp(int)
	 */
	protected boolean handleButtonUp(int button) {
		if (getCurrentInput().isAnyButtonDown())
			return false;
		((SelectionRequest) getTargetRequest()).setLastButtonPressed(0);
		setDragTracker(null);
		setState(STATE_INITIAL);
		unlockTargetEditPart();
		return true;
	}

	/**
	 * @see AbstractTool#handleCommandStackChanged()
	 */
	protected boolean handleCommandStackChanged() {
		if (getDragTracker() == null)
			return super.handleCommandStackChanged();
		return false;
	}

	/**
	 * Sets the drag tracker to <code>null</code> and goes into the initial
	 * state when focus is lost.
	 * 
	 * @see AbstractTool#handleFocusLost()
	 */
	protected boolean handleFocusLost() {
		if (isInState(STATE_ACCESSIBLE_DRAG | STATE_ACCESSIBLE_DRAG_IN_PROGRESS
				| STATE_DRAG | STATE_DRAG_IN_PROGRESS)) {
			if (getDragTracker() != null)
				setDragTracker(null);
			setState(STATE_INITIAL);
			return true;
		}
		return false;
	}

	/**
	 * Called when the mouse hovers. Calls {@link #showHoverFeedback()}.
	 * 
	 * @see AbstractTool#handleHover()
	 */
	protected boolean handleHover() {
		setHoverActive(true);
		showHoverFeedback();
		return true;
	}

	/**
	 * Called when the mouse hover stops (i.e. the mouse moves or a button is
	 * clicked). Calls {@link #eraseHoverFeedback()}.
	 * 
	 * @see TargetingTool#handleHoverStop()
	 */
	protected boolean handleHoverStop() {
		eraseHoverFeedback();
		return true;
	}

	/**
	 * Processes key down events. Specifically, arrow keys for moving edit
	 * parts, the ESC key for aborting a drag, the period '.' key for traversing
	 * handles, and the ENTER key for committing a drag. If none of these keys
	 * were pressed and the current viewer has a {@link KeyHandler}, it calls
	 * {@link KeyHandler#keyPressed(KeyEvent)}.
	 * 
	 * @see AbstractTool#handleKeyDown(KeyEvent)
	 */
	protected boolean handleKeyDown(KeyEvent e) {
		resetHover();

		if (acceptArrowKey(e))
			if (stateTransition(STATE_ACCESSIBLE_DRAG,
					STATE_ACCESSIBLE_DRAG_IN_PROGRESS))
				return true;

		if (acceptAbort(e)) {
			if (getDragTracker() != null)
				setDragTracker(null);
			if (isInState(STATE_TRAVERSE_HANDLE | STATE_ACCESSIBLE_DRAG
					| STATE_ACCESSIBLE_DRAG_IN_PROGRESS))
				placeMouseInViewer(getStartLocation().getTranslated(6, 6));
			setState(STATE_INITIAL);
			setLastHandleProvider(null);
			return true;
		}

		if (acceptTraverseHandle(e)) {
			if (isInState(STATE_ACCESSIBLE_DRAG_IN_PROGRESS))
				if (getDragTracker() != null)
					getDragTracker().commitDrag();
			if (isInState(STATE_ACCESSIBLE_DRAG
					| STATE_ACCESSIBLE_DRAG_IN_PROGRESS)) {
				setDragTracker(null);
				getCurrentViewer().flush();
			}
			if (!handleTraverseHandle(e))
				setState(STATE_INITIAL);
			return true;
		}

		if (acceptDragCommit(e)) {
			if (getDragTracker() != null)
				getDragTracker().commitDrag();
			setDragTracker(null);
			setState(STATE_INITIAL);
			handleIndex--;
			placeMouseInViewer(getLocation().getTranslated(6, 6));
			return true;
		}

		if (isInState(STATE_INITIAL)) {
			if (getCurrentViewer().getKeyHandler() != null)
				return getCurrentViewer().getKeyHandler().keyPressed(e);
		}

		return false;
	}

	/**
	 * If in the initial state and the viewer has a {@link KeyHandler}, calls
	 * {@link KeyHandler#keyReleased(KeyEvent)} sending it the given key event.
	 * 
	 * @see AbstractTool#handleKeyUp(KeyEvent)
	 */
	protected boolean handleKeyUp(KeyEvent e) {
		if (isInState(STATE_INITIAL)
				&& getCurrentViewer().getKeyHandler() != null
				&& getCurrentViewer().getKeyHandler().keyReleased(e))
			return true;

		return false;
	}

	/**
	 * If in the initial state, updates the request and the mouse target and
	 * asks to show target feedback. If in the traverse handle state, finds the
	 * next handle, moves the mouse cursor to that handle, and gets a drag
	 * tracker from the handle.
	 * 
	 * @see AbstractTool#handleMove()
	 */
	protected boolean handleMove() {
		if (stateTransition(STATE_ACCESSIBLE_DRAG, STATE_INITIAL))
			setDragTracker(null);
		if (isInState(STATE_INITIAL)) {
			updateTargetRequest();
			updateTargetUnderMouse();
			showTargetFeedback();
			return true;
		} else if (isInState(STATE_TRAVERSE_HANDLE)) {
			EditPartViewer viewer = getCurrentViewer();
			if (viewer instanceof GraphicalViewer) {
				Handle handle = ((GraphicalViewer) viewer)
						.findHandleAt(getLocation());
				if (handle != null) {
					setState(STATE_ACCESSIBLE_DRAG);
					setStartLocation(getLocation());
					setDragTracker(handle.getDragTracker());
					return true;
				} else {
					setState(STATE_INITIAL);
				}
			}
		}
		return false;
	}

	/**
	 * If there's a drag tracker, calls handleNativeDragFinished() on the drag
	 * tracker and then sets the drag tracker to <code>null</code>.
	 * 
	 * @see AbstractTool#handleNativeDragFinished(DragSourceEvent)
	 */
	public boolean handleNativeDragFinished(DragSourceEvent event) {
		if (getDragTracker() != null)
			getDragTracker().nativeDragFinished(event, getCurrentViewer());
		setDragTracker(null);
		unlockTargetEditPart();
		return true;
	}

	/**
	 * If there's a drag tracker, calls nativeDragStarted() on the drag tracker.
	 * 
	 * @see AbstractTool#handleNativeDragStarted(DragSourceEvent)
	 */
	public boolean handleNativeDragStarted(DragSourceEvent event) {
		if (getDragTracker() != null)
			getDragTracker().nativeDragStarted(event, getCurrentViewer());
		setState(STATE_INITIAL);
		return true;
	}

	private boolean handleTraverseHandle(KeyEvent e) {
		EditPart focus = getCurrentViewer().getFocusEditPart();
		if (focus.getSelected() == EditPart.SELECTED_NONE)
			return false;
		getCurrentViewer().reveal(focus);

		AccessibleHandleProvider provider;
		provider = (AccessibleHandleProvider) focus
				.getAdapter(AccessibleHandleProvider.class);
		if (provider == null
				|| provider.getAccessibleHandleLocations().isEmpty())
			return false;

		/*
		 * At this point, a handle provider with 1 or more handles has been
		 * obtained
		 */
		setState(STATE_TRAVERSE_HANDLE);
		List locations = provider.getAccessibleHandleLocations();
		// Goto next index, wrapping if necessary
		if (e.character == '.')
			handleIndex = (++handleIndex) % locations.size();
		else
			handleIndex = (--handleIndex + locations.size()) % locations.size();
		if (getLastHandleProvider() != focus) {
			handleIndex = 0;
			setLastHandleProvider(focus);
		}

		Point loc = (Point) locations.get(handleIndex);
		Point current = new Point(getCurrentViewer().getControl().toControl(
				Display.getCurrent().getCursorLocation()));

		if (current.equals(loc)) {
			// The cursor is already at the location that it is to be moved to.
			// So, we
			// move to the next handle instead. If there are no more handles,
			// then we
			// cancel the drag.
			if (locations.size() > 1)
				if (e.character == '.')
					handleIndex = (++handleIndex) % locations.size();
				else
					handleIndex = (--handleIndex + locations.size())
							% locations.size();
			else {
				placeMouseInViewer(loc.getTranslated(6, 6));
				return false;
			}
		}
		placeMouseInViewer((Point) locations.get(handleIndex));
		return true;
	}

	/**
	 * If there's a drag tracker, sets it to <code>null</code> and then sets
	 * this tool's state to the initial state.
	 * 
	 * @see AbstractTool#handleViewerExited()
	 */
	protected boolean handleViewerExited() {
		if (isInState(STATE_ACCESSIBLE_DRAG | STATE_ACCESSIBLE_DRAG_IN_PROGRESS
				| STATE_TRAVERSE_HANDLE | STATE_DRAG | STATE_DRAG_IN_PROGRESS)) {
			if (getDragTracker() != null)
				setDragTracker(null);
			setState(STATE_INITIAL);
		}
		return super.handleViewerExited();
	}

	/**
	 * Forwards the key down event to the drag tracker, if one exists.
	 * 
	 * @see org.eclipse.gef.Tool#keyDown(KeyEvent,
	 *      org.eclipse.gef.EditPartViewer)
	 */
	public void keyDown(KeyEvent evt, EditPartViewer viewer) {
		if (getDragTracker() != null)
			getDragTracker().keyDown(evt, viewer);
		super.keyDown(evt, viewer);
	}

	/**
	 * Forwards the key up event to the drag tracker, if one exists.
	 * 
	 * @see org.eclipse.gef.Tool#keyUp(KeyEvent, org.eclipse.gef.EditPartViewer)
	 */
	public void keyUp(KeyEvent evt, EditPartViewer viewer) {
		if (getDragTracker() != null)
			getDragTracker().keyUp(evt, viewer);
		super.keyUp(evt, viewer);
	}

	/**
	 * Forwards the mouse down event to the drag tracker, if one exists.
	 * 
	 * @see org.eclipse.gef.Tool#mouseDown(MouseEvent,
	 *      org.eclipse.gef.EditPartViewer)
	 */
	public void mouseDown(MouseEvent e, EditPartViewer viewer) {
		super.mouseDown(e, viewer);
		if (getDragTracker() != null)
			getDragTracker().mouseDown(e, viewer);
	}

	/**
	 * Forwards the mouse double clicked event to the drag tracker, if one
	 * exists.
	 * 
	 * @see org.eclipse.gef.Tool#mouseDoubleClick(MouseEvent,
	 *      org.eclipse.gef.EditPartViewer)
	 */
	public void mouseDoubleClick(MouseEvent e, EditPartViewer viewer) {
		super.mouseDoubleClick(e, viewer);
		if (getDragTracker() != null)
			getDragTracker().mouseDoubleClick(e, viewer);
	}

	/**
	 * Forwards the mouse drag event to the drag tracker, if one exists.
	 * 
	 * @see org.eclipse.gef.Tool#mouseDrag(MouseEvent,
	 *      org.eclipse.gef.EditPartViewer)
	 */
	public void mouseDrag(MouseEvent e, EditPartViewer viewer) {
		if (getDragTracker() != null)
			getDragTracker().mouseDrag(e, viewer);
		super.mouseDrag(e, viewer);
	}

	/**
	 * Forwards the mouse hover event to the drag tracker, if one exists.
	 * 
	 * @see org.eclipse.gef.Tool#mouseHover(MouseEvent,
	 *      org.eclipse.gef.EditPartViewer)
	 */
	public void mouseHover(MouseEvent me, EditPartViewer viewer) {
		if (getDragTracker() != null)
			getDragTracker().mouseHover(me, viewer);
		super.mouseHover(me, viewer);
	}

	/**
	 * Forwards the mouse move event to the drag tracker, if one exists.
	 * 
	 * @see org.eclipse.gef.Tool#mouseMove(MouseEvent,
	 *      org.eclipse.gef.EditPartViewer)
	 */
	public void mouseMove(MouseEvent me, EditPartViewer viewer) {
		if (getDragTracker() != null)
			getDragTracker().mouseMove(me, viewer);
		super.mouseMove(me, viewer);
	}

	/**
	 * Forwards the mouse up event to the drag tracker, if one exists.
	 * 
	 * @see org.eclipse.gef.Tool#mouseUp(MouseEvent,
	 *      org.eclipse.gef.EditPartViewer)
	 */
	public void mouseUp(MouseEvent e, EditPartViewer viewer) {
		if (getDragTracker() != null)
			getDragTracker().mouseUp(e, viewer);
		super.mouseUp(e, viewer);
	}

	/**
	 * Delegates the scrolling to the DragTracker (if there is one). If not,
	 * invokes the super method.
	 * 
	 * @see org.eclipse.gef.Tool#mouseWheelScrolled(org.eclipse.swt.widgets.Event,
	 *      org.eclipse.gef.EditPartViewer)
	 */
	public void mouseWheelScrolled(Event event, EditPartViewer viewer) {
		if (getDragTracker() != null) {
			getDragTracker().mouseWheelScrolled(event, viewer);
			event.doit = false;
		} else
			super.mouseWheelScrolled(event, viewer);
	}

	/**
	 * If there is a drag tracker, this method does nothing so that the drag
	 * tracker can take care of the cursor. Otherwise, calls <code>super</code>.
	 * 
	 * @see AbstractTool#refreshCursor()
	 */
	protected void refreshCursor() {
		// If we have a DragTracker, let it control the Cursor
		if (getDragTracker() == null)
			super.refreshCursor();
	}

	/**
	 * Sets the drag tracker for this SelectionTool. If the current drag tracker
	 * is not <code>null</code>, this method deactivates it. If the new drag
	 * tracker is not <code>null</code>, this method will activate it and set
	 * the {@link EditDomain} and {@link EditPartViewer}.
	 * 
	 * @param newDragTracker
	 *            the new drag tracker
	 */
	public void setDragTracker(DragTracker newDragTracker) {
		if (newDragTracker == dragTracker)
			return;
		if (dragTracker != null)
			dragTracker.deactivate();
		dragTracker = newDragTracker;

		// if (!getCurrentInput().isMouseButtonDown(3))
		// setMouseCapture(dragTracker != null);
		if (newDragTracker != null) {
			newDragTracker.setEditDomain(getDomain());
			newDragTracker.activate();
			newDragTracker.setViewer(getCurrentViewer());
		}

		refreshCursor();
	}

	private void setLastHandleProvider(EditPart part) {
		if (part == null)
			cachedHandlePart = null;
		else
			cachedHandlePart = new WeakReference(part);
	}

	/**
	 * Asks the target edit part (if there is one) to show hover feedback via
	 * {@link EditPart#showTargetFeedback(Request)} with a hover request.
	 */
	protected void showHoverFeedback() {
		if (getTargetEditPart() == null)
			return;
		if (getTargetHoverRequest() == null)
			return;
		getTargetEditPart().showTargetFeedback(getTargetHoverRequest());
	}

	/**
	 * Updates the location of the hover request.
	 */
	protected void updateHoverRequest() {
		LocationRequest request = (LocationRequest) getTargetHoverRequest();
		request.setLocation(getLocation());
	}

	/**
	 * Sets the modifiers , type and location of the target request (which is a
	 * {@link SelectionRequest}) and then calls {@link #updateHoverRequest()}.
	 * 
	 * @see TargetingTool#updateTargetRequest()
	 */
	protected void updateTargetRequest() {
		SelectionRequest request = (SelectionRequest) getTargetRequest();
		request.setModifiers(getCurrentInput().getModifiers());
		request.setType(getCommandName());
		request.setLocation(getLocation());
		updateHoverRequest();
	}

	/**
	 * @see AbstractTool#getDebugNameForState(int)
	 */
	protected String getDebugNameForState(int state) {
		if (state == STATE_TRAVERSE_HANDLE)
			return "Traverse Handle"; //$NON-NLS-1$
		return super.getDebugNameForState(state);
	}

}
