/*******************************************************************************
 * 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.dnd;

import java.util.Collection;
import java.util.Collections;

import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DropTarget;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.DropTargetListener;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.dnd.TransferData;

import org.eclipse.draw2d.geometry.Point;

import org.eclipse.gef.AutoexposeHelper;
import org.eclipse.gef.EditPart;
import org.eclipse.gef.EditPartViewer;
import org.eclipse.gef.Request;
import org.eclipse.gef.commands.Command;

/**
 * An abstract implementation of TransferDropTargetListener for use with
 * {@link EditPartViewer}s. The Viewer's <code>Control</code> should be the Drop
 * target. In order to communicate with EditParts in a consistent way,
 * DropTargetEvents are processed into {@link Request Requests}.
 * <P>
 * Dropping is inherently a targeting interaction. This class handles
 * calculating the <i>target</i> EditPart. It also handles common targeting
 * behavior, such as interacting with the target EditPart or its ancestors to
 * achieve things like auto-scroll/auto-expose.
 */
public abstract class AbstractTransferDropTargetListener implements
		TransferDropTargetListener {

	private DropTargetEvent currentEvent;
	private AutoexposeHelper exposeHelper;
	private boolean hovering = false;
	private boolean askForCommand;
	private long hoverStartTime = -1;
	private Point prevMouseLoc;
	private Request request;
	private boolean showingFeedback;
	private EditPart target;
	private Transfer transfer;
	private EditPartViewer viewer;

	/**
	 * Constructs a new AbstractTransferDropTargetListener and sets the
	 * EditPartViewer. If you use this constructor, you must set the Transfer
	 * yourself using {@link #setTransfer(Transfer)}.
	 * 
	 * @param viewer
	 *            the EditPartViewer
	 */
	public AbstractTransferDropTargetListener(EditPartViewer viewer) {
		setViewer(viewer);
	}

	/**
	 * Constructs a new AbstractTransferDropTargetListener and sets the
	 * EditPartViewer and Transfer. The Viewer's Control should be the Drop
	 * target.
	 * 
	 * @param viewer
	 *            the EditPartViewer
	 * @param xfer
	 *            the Transfer
	 */
	public AbstractTransferDropTargetListener(EditPartViewer viewer,
			Transfer xfer) {
		setViewer(viewer);
		setTransfer(xfer);
	}

	private EditPart calculateTargetEditPart() {
		EditPart ep = getViewer().findObjectAtExcluding(getDropLocation(),
				getExclusionSet(), new EditPartViewer.Conditional() {
					public boolean evaluate(EditPart editpart) {
						return editpart.getTargetEditPart(getTargetRequest()) != null;
					}
				});
		if (ep != null)
			ep = ep.getTargetEditPart(getTargetRequest());
		return ep;
	}

	/**
	 * Creates and returns the <code>Request</code> that will be sent to the
	 * targeted EditPart. Subclasses can override to create specialized
	 * requests.
	 * 
	 * @return the <code>Request</code> to be used with the <i>target</i>
	 *         EditPart
	 */
	protected Request createTargetRequest() {
		return new Request();
	}

	/**
	 * Stores the information about the current DropTargetEvent. This method may
	 * not be called on the listener, because the listener may not be made
	 * active until after the mouse has entered the drop target.
	 * 
	 * @see DropTargetListener#dragEnter(DropTargetEvent)
	 */
	public void dragEnter(DropTargetEvent event) {
		resetHover();
		setCurrentEvent(event);
	}

	/**
	 * Stores the information about the current DropTargetEvent and then calls
	 * <code>unload()</code>. Subclasses should override {@link #unload()} to
	 * perform actions for this event. For some reason, SWT also calls
	 * <code>dragLeave()</code> when the actual drop is performed, even though
	 * the mouse has not left the drop target.
	 * 
	 * @see DropTargetListener#dragLeave(DropTargetEvent)
	 */
	public void dragLeave(DropTargetEvent event) {
		setCurrentEvent(event);
		unload();
	}

	/**
	 * Stores the information about the current DropTargetEvent and then calls
	 * <code>handleDragOperationChanged()</code>. Subclasses should override
	 * {@link #handleDragOperationChanged()} to perform actions for this event.
	 * 
	 * @see DropTargetListener#dragOperationChanged(DropTargetEvent)
	 */
	public void dragOperationChanged(DropTargetEvent event) {
		resetHover();
		setCurrentEvent(event);
		handleDragOperationChanged();
	}

	/**
	 * Stores the information about the current DropTargetEvent and then calls
	 * <code>handleDragOver()</code>. Subclasses should override
	 * {@link #handleDragOver()} to perform actions for this event.
	 * 
	 * @see DropTargetListener#dragOver(org.eclipse.swt.dnd.DropTargetEvent)
	 */
	public void dragOver(DropTargetEvent event) {
		setCurrentEvent(event);
		handleDragOver();
		if (testAndSet(event)) {
			resetHover();
		} else {
			if (hovering)
				return;
			long currentTime = event.time;
			if (hoverStartTime == -1) {
				hoverStartTime = currentTime;
			} else if (currentTime - hoverStartTime > 400) {
				handleHover();
				hovering = true;
			}
		}
	}

	/**
	 * Stores the information about the current DropTargetEvent and then calls
	 * {@link #handleDrop()}, followed by {@link #unload()}. Subclasses should
	 * override these methods to perform actions for this event.
	 * 
	 * @see DropTargetListener#drop(DropTargetEvent)
	 */
	public void drop(DropTargetEvent event) {
		setCurrentEvent(event);
		eraseTargetFeedback();
		handleDrop();
		unload();
	}

	/**
	 * Stores the current <code>DropTargetEvent</code> and does nothing. By
	 * default, the drop is accepted.
	 * 
	 * @see DropTargetListener#dropAccept(DropTargetEvent)
	 */
	public void dropAccept(DropTargetEvent event) {
		setCurrentEvent(event);
	}

	/**
	 * Calls <code>eraseTargetFeedback(Request)</code> on the current
	 * <i>target</i>, using the target Request. Does nothing if there is no
	 * target, or if the target has not been requested to show target feedback.
	 */
	protected void eraseTargetFeedback() {
		if (getTargetEditPart() != null && showingFeedback) {
			showingFeedback = false;
			getTargetEditPart().eraseTargetFeedback(getTargetRequest());
		}
	}

	/**
	 * Returns the current command from the target EditPart.
	 * 
	 * @return The current command from the target EditPart
	 */
	protected Command getCommand() {
		return getTargetEditPart().getCommand(getTargetRequest());
	}

	/**
	 * Returns the current <code>DropTargetEvent</code>.
	 * 
	 * @return the current event
	 */
	public DropTargetEvent getCurrentEvent() {
		return currentEvent;
	}

	/**
	 * Returns the current mouse location, as a {@link Point}. The location is
	 * relative to the control's client area.
	 * 
	 * @return the drop location
	 */
	protected Point getDropLocation() {
		org.eclipse.swt.graphics.Point swt;
		swt = new org.eclipse.swt.graphics.Point(getCurrentEvent().x,
				getCurrentEvent().y);
		DropTarget target = (DropTarget) getCurrentEvent().widget;
		swt = target.getControl().toControl(swt);
		return new Point(swt.x, swt.y);
	}

	/**
	 * Returns a Collection of {@link EditPart EditParts} that are to be
	 * excluded when searching for the target EditPart.
	 * 
	 * @return A Collection of EditParts to be excluded
	 */
	protected Collection getExclusionSet() {
		return Collections.EMPTY_LIST;
	}

	/**
	 * Returns the current <i>target</i> <code>EditPart</code>.
	 * 
	 * @return the target EditPart
	 */
	protected EditPart getTargetEditPart() {
		return target;
	}

	/**
	 * Returns the target <code>Request</code>. If the target Request is
	 * <code>null</code>, {@link #createTargetRequest()} is called and the newly
	 * created Request is returned.
	 * 
	 * @return the target Request
	 */
	protected Request getTargetRequest() {
		if (request == null)
			request = createTargetRequest();
		return request;
	}

	/**
	 * @see TransferDropTargetListener#getTransfer()
	 */
	public Transfer getTransfer() {
		return transfer;
	}

	/**
	 * Returns the <code>EditPartViewer</code> that is the target of the drop.
	 * 
	 * @return the EditPartViewer
	 */
	protected EditPartViewer getViewer() {
		return viewer;
	}

	/**
	 * Called when the user changes the Drag operation. By default, target
	 * feedback is erased. The target Request and target EditPart are updated,
	 * and target feedback is re-displayed on the new target.
	 */
	protected void handleDragOperationChanged() {
		// Erase any old feedback now, in case the request changes substantially
		eraseTargetFeedback();

		// Update request based on the new operation type
		updateTargetRequest();

		// Update the target based on the updated request
		updateTargetEditPart();
	}

	/**
	 * Called whenever the User drags over the target. By default, the target
	 * Request and target EditPart are updated, feedback is shown, and
	 * auto-expose occurs.
	 */
	protected void handleDragOver() {
		updateTargetRequest();
		updateTargetEditPart();
		showTargetFeedback();
		if (exposeHelper != null) {
			// If the expose helper does not wish to continue, set helper to
			// null.
			if (!exposeHelper.step(getDropLocation()))
				exposeHelper = null;
		}
	}

	/**
	 * Updates the target Request and target EditPart, and performs the drop. By
	 * default, the drop is performed by asking the target EditPart for a
	 * Command using the target Request. This Command is then executed on the
	 * CommandStack.
	 * <P>
	 * If there is no target EditPart or no executable Command, the event's
	 * <code>detail</code> field is set to <code>DND.DROP_NONE</code>.
	 */
	protected void handleDrop() {
		updateTargetRequest();
		updateTargetEditPart();

		if (getTargetEditPart() != null) {
			Command command = getCommand();
			if (command != null && command.canExecute())
				getViewer().getEditDomain().getCommandStack().execute(command);
			else
				getCurrentEvent().detail = DND.DROP_NONE;
		} else
			getCurrentEvent().detail = DND.DROP_NONE;
	}

	/**
	 * Called when a new target EditPart has been entered. By default, the new
	 * target is asked to show feedback.
	 */
	protected void handleEnteredEditPart() {
	}

	/**
	 * Called as the current target EditPart is being exited. By default, the
	 * target is asked to erase feedback.
	 */
	protected void handleExitingEditPart() {
		eraseTargetFeedback();
	}

	/**
	 * Called when the mouse hovers during drag and drop.
	 */
	protected void handleHover() {
		updateAutoexposeHelper();
	}

	/**
	 * Called when the mouse resumes motion after having hovered.
	 */
	protected void handleHoverStop() {
	}

	/**
	 * Returns <code>true</code> if this TransferDropTargetListener is enabled
	 * for the specified <code>DropTargetEvent</code>. By default, this is
	 * calculated by comparing the event's {@link DropTargetEvent#dataTypes
	 * dataTypes} with the <code>Transfer's</code> supported types (
	 * {@link Transfer#isSupportedType(TransferData)}). If a dataType is
	 * supported, an attempt is made to find a <i>target</i>
	 * <code>EditPart</code> at the current drop location. If a target
	 * <code>EditPart</code> is found, <code>true</code> is returned, and the
	 * DropTargetEvent's {@link DropTargetEvent#currentDataType} is set to the
	 * dataType that matched.
	 * 
	 * @param event
	 *            the DropTargetEvent
	 * @return <code>true</code> if this TransferDropTargetListener is enabled
	 *         for the given DropTargetEvent
	 */
	public boolean isEnabled(DropTargetEvent event) {
		for (int i = 0; i < event.dataTypes.length; i++) {
			if (getTransfer().isSupportedType(event.dataTypes[i])) {
				setCurrentEvent(event);
				event.currentDataType = event.dataTypes[i];
				updateTargetRequest();
				EditPart oldTarget = target;
				updateTargetEditPart();
				boolean result;
				if (target == null)
					result = false;
				else if (askForCommand) {
					Command command = getCommand();
					result = command != null && command.canExecute();
				} else
					result = true;
				request = null;
				target = oldTarget;
				return result;
			}
		}
		return false;
	}

	/**
	 * Returns <code>true</code> if {@link #isEnabled(DropTargetEvent)} is
	 * determined by asking the potential target for a Command.
	 * 
	 * @return <code>true</code> if the target will be queried for a
	 *         <code>Command</code>
	 * @since 3.1
	 */
	protected boolean isEnablementDeterminedByCommand() {
		return askForCommand;
	}

	private void resetHover() {
		if (hovering) {
			handleHoverStop();
			hovering = false;
			hoverStartTime = -1;
			prevMouseLoc = null;
		}
	}

	/**
	 * Sets the current autoexpose helper.
	 * 
	 * @param helper
	 *            the autoexpose helper
	 */
	protected void setAutoexposeHelper(AutoexposeHelper helper) {
		exposeHelper = helper;
	}

	/**
	 * Determines if the target editpart should be asked for a Command during
	 * {@link #isEnabled(DropTargetEvent)}. For most DND operations, the data is
	 * not available, thus asking for a command would not make sense. The
	 * default value is <code>false</code>.
	 * 
	 * @param value
	 *            <code>true</code> if a
	 * @since 3.1
	 */
	protected void setEnablementDeterminedByCommand(boolean value) {
		askForCommand = value;
	}

	/**
	 * Sets the current DropTargetEvent.
	 * 
	 * @param currentEvent
	 *            the DropTargetEvent
	 */
	public void setCurrentEvent(DropTargetEvent currentEvent) {
		this.currentEvent = currentEvent;
	}

	/**
	 * Sets the <i>target</i> <code>EditPart</code>. If the target is changing,
	 * {@link #handleExitingEditPart()} is called before the target changes, and
	 * {@link #handleEnteredEditPart()} is called afterwards.
	 * 
	 * @param ep
	 *            the new target EditPart
	 */
	protected void setTargetEditPart(EditPart ep) {
		if (ep != target) {
			if (target != null)
				handleExitingEditPart();
			target = ep;
			if (target != null)
				handleEnteredEditPart();
		}
	}

	/**
	 * Sets the Tranfer type that this listener can handle.
	 * 
	 * @param xfer
	 *            the Transfer
	 */
	protected void setTransfer(Transfer xfer) {
		transfer = xfer;
	}

	/**
	 * Sets the EditPartViewer.
	 * 
	 * @param viewer
	 *            the EditPartViewer
	 */
	protected void setViewer(EditPartViewer viewer) {
		this.viewer = viewer;
	}

	/**
	 * Asks the target <code>EditPart</code> to show target feedback if it is
	 * not <code>null</code>.
	 * 
	 * @see EditPart#showTargetFeedback(Request)
	 */
	protected void showTargetFeedback() {
		if (getTargetEditPart() != null) {
			showingFeedback = true;
			getTargetEditPart().showTargetFeedback(getTargetRequest());
		}
	}

	/**
	 * Tests whether the given event's location is different than the previous
	 * event's location, and sets the remembered location to the current event's
	 * location.
	 * 
	 * @param event
	 * @return boolean
	 */
	private boolean testAndSet(DropTargetEvent event) {
		boolean result = prevMouseLoc == null
				|| !(prevMouseLoc.x == event.x && prevMouseLoc.y == event.y);
		if (prevMouseLoc == null)
			prevMouseLoc = new Point();
		prevMouseLoc.x = event.x;
		prevMouseLoc.y = event.y;
		return result;
	}

	/**
	 * Erases target feedback and sets the request to <code>null</code>.
	 */
	protected void unload() {
		resetHover();
		eraseTargetFeedback();
		request = null;
		setTargetEditPart(null);
		setCurrentEvent(null);
		setAutoexposeHelper(null);
	}

	/**
	 * Updates the active {@link AutoexposeHelper}. Does nothing if there is
	 * still an active helper. Otherwise, obtains a new helper (possible
	 * <code>null</code>) at the current mouse location and calls
	 * {@link #setAutoexposeHelper(AutoexposeHelper)}.
	 */
	protected void updateAutoexposeHelper() {
		if (exposeHelper != null)
			return;
		AutoexposeHelper.Search search;
		search = new AutoexposeHelper.Search(getDropLocation());
		getViewer().findObjectAtExcluding(getDropLocation(),
				Collections.EMPTY_LIST, search);
		setAutoexposeHelper(search.result);
	}

	/**
	 * Updates the target EditPart.
	 */
	protected void updateTargetEditPart() {
		setTargetEditPart(calculateTargetEditPart());
	}

	/**
	 * Subclasses must implement this to update the target Request.
	 */
	protected abstract void updateTargetRequest();

}
