/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Eclipse Public License, Version 1.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.eclipse.org/org/documents/epl-v10.php
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.ide.eclipse.adt.internal.editors.layout.gle2;

import com.android.ide.common.api.DropFeedback;
import com.android.ide.common.api.INode;
import com.android.ide.common.api.InsertType;
import com.android.ide.common.api.Point;
import com.android.ide.common.api.Rect;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode.NodeCreationListener;

import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.TreePath;
import org.eclipse.jface.viewers.TreeSelection;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.TransferData;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.widgets.Display;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * The Move gesture provides the operation for moving widgets around in the canvas.
 */
public class MoveGesture extends DropGesture {
    /** The associated {@link LayoutCanvas}. */
    private LayoutCanvas mCanvas;

    /** Overlay which paints the drag &amp; drop feedback. */
    private MoveOverlay mOverlay;

    private static final boolean DEBUG = false;

    /**
     * The top view right under the drag'n'drop cursor.
     * This can only be null during a drag'n'drop when there is no view under the cursor
     * or after the state was all cleared.
     */
    private CanvasViewInfo mCurrentView;

    /**
     * The elements currently being dragged. This will always be non-null for a valid
     * drag'n'drop that happens within the same instance of Eclipse.
     * <p/>
     * In the event that the drag and drop happens between different instances of Eclipse
     * this will remain null.
     */
    private SimpleElement[] mCurrentDragElements;

    /**
     * The first view under the cursor that responded to onDropEnter is called the "target view".
     * It can differ from mCurrentView, typically because a terminal View doesn't
     * accept drag'n'drop so its parent layout became the target drag'n'drop receiver.
     * <p/>
     * The target node is the proxy node associated with the target view.
     * This can be null if no view under the cursor accepted the drag'n'drop or if the node
     * factory couldn't create a proxy for it.
     */
    private NodeProxy mTargetNode;

    /**
     * The latest drop feedback returned by IViewRule.onDropEnter/Move.
     */
    private DropFeedback mFeedback;

    /**
     * {@link #dragLeave(DropTargetEvent)} is unfortunately called right before data is
     * about to be dropped (between the last {@link #dragOver(DropTargetEvent)} and the
     * next {@link #dropAccept(DropTargetEvent)}). That means we can't just
     * trash the current DropFeedback from the current view rule in dragLeave().
     * Instead we preserve it in mLeaveTargetNode and mLeaveFeedback in case a dropAccept
     * happens next.
     */
    private NodeProxy mLeaveTargetNode;

    /**
     * @see #mLeaveTargetNode
     */
    private DropFeedback mLeaveFeedback;

    /**
     * @see #mLeaveTargetNode
     */
    private CanvasViewInfo mLeaveView;

    /** Singleton used to keep track of drag selection in the same Eclipse instance. */
    private final GlobalCanvasDragInfo mGlobalDragInfo;

    /**
     * Constructs a new {@link MoveGesture}, tied to the given canvas.
     *
     * @param canvas The canvas to associate the {@link MoveGesture} with.
     */
    public MoveGesture(LayoutCanvas canvas) {
        this.mCanvas = canvas;
        mGlobalDragInfo = GlobalCanvasDragInfo.getInstance();
    }

    @Override
    public List<Overlay> createOverlays() {
        mOverlay = new MoveOverlay();
        return Collections.<Overlay> singletonList(mOverlay);
    }

    @Override
    public void begin(ControlPoint pos, int startMask) {
        super.begin(pos, startMask);

        // Hide selection overlays during a move drag
        mCanvas.getSelectionOverlay().setHidden(true);
    }

    @Override
    public void end(ControlPoint pos, boolean canceled) {
        super.end(pos, canceled);

        mCanvas.getSelectionOverlay().setHidden(false);

        // Ensure that the outline is back to showing the current selection, since during
        // a drag gesture we temporarily set it to show the current target node instead.
        mCanvas.getSelectionManager().syncOutlineSelection();
    }

    /* TODO: Pass modifier mask to drag rules as well! This doesn't work yet since
       the drag &amp; drop code seems to steal keyboard events.
    @Override
    public boolean keyPressed(KeyEvent event) {
        update(mCanvas.getGestureManager().getCurrentControlPoint());
        mCanvas.redraw();
        return true;
    }

    @Override
    public boolean keyReleased(KeyEvent event) {
        update(mCanvas.getGestureManager().getCurrentControlPoint());
        mCanvas.redraw();
        return true;
    }
    */

    /*
     * The cursor has entered the drop target boundaries.
     * {@inheritDoc}
     */
    @Override
    public void dragEnter(DropTargetEvent event) {
        if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drag enter", event);

        // Make sure we don't have any residual data from an earlier operation.
        clearDropInfo();
        mLeaveTargetNode = null;
        mLeaveFeedback = null;
        mLeaveView = null;

        // Get the dragged elements.
        //
        // The current transfered type can be extracted from the event.
        // As described in dragOver(), this works basically works on Windows but
        // not on Linux or Mac, in which case we can't get the type until we
        // receive dropAccept/drop().
        // For consistency we try to use the GlobalCanvasDragInfo instance first,
        // and if it fails we use the event transfer type as a backup (but as said
        // before it will most likely work only on Windows.)
        // In any case this can be null even for a valid transfer.

        mCurrentDragElements = mGlobalDragInfo.getCurrentElements();

        if (mCurrentDragElements == null) {
            SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
            if (sxt.isSupportedType(event.currentDataType)) {
                mCurrentDragElements = (SimpleElement[]) sxt.nativeToJava(event.currentDataType);
            }
        }

        // if there is no data to transfer, invalidate the drag'n'drop.
        // The assumption is that the transfer should have at least one element with a
        // a non-null non-empty FQCN. Everything else is optional.
        if (mCurrentDragElements == null ||
                mCurrentDragElements.length == 0 ||
                mCurrentDragElements[0] == null ||
                mCurrentDragElements[0].getFqcn() == null ||
                mCurrentDragElements[0].getFqcn().length() == 0) {
            event.detail = DND.DROP_NONE;
        }

        dragOperationChanged(event);
    }

    /*
     * The operation being performed has changed (e.g. modifier key).
     * {@inheritDoc}
     */
    @Override
    public void dragOperationChanged(DropTargetEvent event) {
        if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drag changed", event);

        checkDataType(event);
        recomputeDragType(event);
    }

    private void recomputeDragType(DropTargetEvent event) {
        if (event.detail == DND.DROP_DEFAULT) {
            // Default means we can now choose the default operation, either copy or move.
            // If the drag comes from the same canvas we default to move, otherwise we
            // default to copy.

            if (mGlobalDragInfo.getSourceCanvas() == mCanvas &&
                    (event.operations & DND.DROP_MOVE) != 0) {
                event.detail = DND.DROP_MOVE;
            } else if ((event.operations & DND.DROP_COPY) != 0) {
                event.detail = DND.DROP_COPY;
            }
        }

        // We don't support other types than copy and move
        if (event.detail != DND.DROP_COPY && event.detail != DND.DROP_MOVE) {
            event.detail = DND.DROP_NONE;
        }
    }

    /*
     * The cursor has left the drop target boundaries OR data is about to be dropped.
     * {@inheritDoc}
     */
    @Override
    public void dragLeave(DropTargetEvent event) {
        if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drag leave");

        // dragLeave is unfortunately called right before data is about to be dropped
        // (between the last dropMove and the next dropAccept). That means we can't just
        // trash the current DropFeedback from the current view rule, we need to preserve
        // it in case a dropAccept happens next.
        // See the corresponding kludge in dropAccept().
        mLeaveTargetNode = mTargetNode;
        mLeaveFeedback = mFeedback;
        mLeaveView = mCurrentView;

        clearDropInfo();
    }

    /*
     * The cursor is moving over the drop target.
     * {@inheritDoc}
     */
    @Override
    public void dragOver(DropTargetEvent event) {
        processDropEvent(event);
    }

    /*
     * The drop is about to be performed.
     * The drop target is given a last chance to change the nature of the drop.
     * {@inheritDoc}
     */
    @Override
    public void dropAccept(DropTargetEvent event) {
        if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drop accept");

        checkDataType(event);

        // If we have a valid target node and it matches the one we saved in
        // dragLeave then we restore the DropFeedback that we saved in dragLeave.
        if (mLeaveTargetNode != null) {
            mTargetNode = mLeaveTargetNode;
            mFeedback = mLeaveFeedback;
            mCurrentView = mLeaveView;
        }

        if (mFeedback != null && mFeedback.invalidTarget) {
            // The script said we can't drop here.
            event.detail = DND.DROP_NONE;
        }

        if (mLeaveTargetNode == null || event.detail == DND.DROP_NONE) {
            clearDropInfo();
        }

        mLeaveTargetNode = null;
        mLeaveFeedback = null;
        mLeaveView = null;
    }

    /*
     * The data is being dropped.
     * {@inheritDoc}
     */
    @Override
    public void drop(final DropTargetEvent event) {
        if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "dropped");

        SimpleElement[] elements = null;

        SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();

        if (sxt.isSupportedType(event.currentDataType)) {
            if (event.data instanceof SimpleElement[]) {
                elements = (SimpleElement[]) event.data;
            }
        }

        if (elements == null || elements.length < 1) {
            if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drop missing drop data");
            return;
        }

        if (mCurrentDragElements != null && Arrays.equals(elements, mCurrentDragElements)) {
            elements = mCurrentDragElements;
        }

        if (mTargetNode == null) {
            ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
            if (viewHierarchy.isValid() && viewHierarchy.isEmpty()) {
                // There is no target node because the drop happens on an empty document.
                // Attempt to create a root node accordingly.
                createDocumentRoot(elements);
            } else {
                if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "dropped on null targetNode");
            }
            return;
        }

        updateDropFeedback(mFeedback, event);

        final SimpleElement[] elementsFinal = elements;
        final LayoutPoint canvasPoint = getDropLocation(event).toLayout();
        String label = computeUndoLabel(mTargetNode, elements, event.detail);

        // Create node listener which (during the drop) listens for node additions
        // and stores the list of added node such that they can be selected afterwards.
        final List<UiElementNode> added = new ArrayList<UiElementNode>();
        // List of "index within parent" for each node
        final List<Integer> indices = new ArrayList<Integer>();
        NodeCreationListener listener = new NodeCreationListener() {
            public void nodeCreated(UiElementNode parent, UiElementNode child, int index) {
                if (parent == mTargetNode.getNode()) {
                    added.add(child);

                    // Adjust existing indices
                    for (int i = 0, n = indices.size(); i < n; i++) {
                        int idx = indices.get(i);
                        if (idx >= index) {
                            indices.set(i, idx + 1);
                        }
                    }

                    indices.add(index);
                }
            }

            public void nodeDeleted(UiElementNode parent, UiElementNode child, int previousIndex) {
                if (parent == mTargetNode.getNode()) {
                    // Adjust existing indices
                    for (int i = 0, n = indices.size(); i < n; i++) {
                        int idx = indices.get(i);
                        if (idx >= previousIndex) {
                            indices.set(i, idx - 1);
                        }
                    }

                    // Make sure we aren't removing the same nodes that are being added
                    assert !added.contains(child);
                }
            }
        };

        try {
            UiElementNode.addNodeCreationListener(listener);
            mCanvas.getLayoutEditor().wrapUndoEditXmlModel(label, new Runnable() {
                public void run() {
                    InsertType insertType = getInsertType(event, mTargetNode);
                    mCanvas.getRulesEngine().callOnDropped(mTargetNode,
                            elementsFinal,
                            mFeedback,
                            new Point(canvasPoint.x, canvasPoint.y),
                            insertType);
                    mTargetNode.applyPendingChanges();
                    // Clean up drag if applicable
                    if (event.detail == DND.DROP_MOVE) {
                        GlobalCanvasDragInfo.getInstance().removeSource();
                    }
                }
            });
        } finally {
            UiElementNode.removeNodeCreationListener(listener);
        }

        final List<INode> nodes = new ArrayList<INode>();
        NodeFactory nodeFactory = mCanvas.getNodeFactory();
        for (UiElementNode uiNode : added) {
            if (uiNode instanceof UiViewElementNode) {
                NodeProxy node = nodeFactory.create((UiViewElementNode) uiNode);
                if (node != null) {
                    nodes.add(node);
                }
            }
        }

        // Select the newly dropped nodes:
        // Find out which nodes were added, and look up their corresponding
        // CanvasViewInfos.
        final SelectionManager selectionManager = mCanvas.getSelectionManager();
        // Don't use the indices to search for corresponding nodes yet, since a
        // render may not have happened yet and we'd rather use an up to date
        // view hierarchy than indices to look up the right view infos.
        if (!selectionManager.selectDropped(nodes, null /* indices */)) {
            // In some scenarios we can't find the actual view infos yet; this
            // seems to happen when you drag from one canvas to another (see the
            // related comment next to the setFocus() call below). In that case
            // defer selection briefly until the view hierarchy etc is up to
            // date.
            Display.getDefault().asyncExec(new Runnable() {
                public void run() {
                    selectionManager.selectDropped(nodes, indices);
                }
            });
        }

        clearDropInfo();
        mCanvas.redraw();
        // Request focus: This is *necessary* when you are dragging from one canvas editor
        // to another, because without it, the redraw does not seem to be processed (the change
        // is invisible until you click on the target canvas to give it focus).
        mCanvas.setFocus();
    }

    /**
     * Returns the right {@link InsertType} to use for the given drop target event and the
     * given target node
     *
     * @param event the drop target event
     * @param mTargetNode the node targeted by the drop
     * @return the {link InsertType} to use for the drop
     */
    public static InsertType getInsertType(DropTargetEvent event, NodeProxy mTargetNode) {
        GlobalCanvasDragInfo dragInfo = GlobalCanvasDragInfo.getInstance();
        if (event.detail == DND.DROP_MOVE) {
            SelectionItem[] selection = dragInfo.getCurrentSelection();
            if (selection != null) {
                for (SelectionItem item : selection) {
                    if (item.getNode() != null
                            && item.getNode().getParent() == mTargetNode) {
                        return InsertType.MOVE_WITHIN;
                    }
                }
            }

            return InsertType.MOVE_INTO;
        } else if (dragInfo.getSourceCanvas() != null) {
            return InsertType.PASTE;
        } else {
            return InsertType.CREATE;
        }
    }

    /**
     * Computes a suitable Undo label to use for a drop operation, such as
     * "Drop Button in LinearLayout" and "Move Widgets in RelativeLayout".
     *
     * @param targetNode The target of the drop
     * @param elements The dragged widgets
     * @param detail The DnD mode, as used in {@link DropTargetEvent#detail}.
     * @return A string suitable as an undo-label for the drop event
     */
    public static String computeUndoLabel(NodeProxy targetNode,
            SimpleElement[] elements, int detail) {
        // Decide whether it's a move or a copy; we'll label moves specifically
        // as a move and consider everything else a "Drop"
        String verb = (detail == DND.DROP_MOVE) ? "Move" : "Drop";

        // Get the type of widget being dropped/moved, IF there is only one. If
        // there is more than one, just reference it as "Widgets".
        String object;
        if (elements != null && elements.length == 1) {
            object = getSimpleName(elements[0].getFqcn());
        } else {
            object = "Widgets";
        }

        String where = getSimpleName(targetNode.getFqcn());

        // When we localize this: $1 is the verb (Move or Drop), $2 is the
        // object (such as "Button"), and $3 is the place we are doing it (such
        // as "LinearLayout").
        return String.format("%1$s %2$s in %3$s", verb, object, where);
    }

    /**
     * Returns simple name (basename, following last dot) of a fully qualified
     * class name.
     *
     * @param fqcn The fqcn to reduce
     * @return The base name of the fqcn
     */
    public static String getSimpleName(String fqcn) {
        // Note that the following works even when there is no dot, since
        // lastIndexOf will return -1 so we get fcqn.substring(-1+1) =
        // fcqn.substring(0) = fqcn
        return fqcn.substring(fqcn.lastIndexOf('.') + 1);
    }

    /**
     * Updates the {@link DropFeedback#isCopy} and {@link DropFeedback#sameCanvas} fields
     * of the given {@link DropFeedback}. This is generally called right before invoking
     * one of the callOnXyz methods of GRE to refresh the fields.
     *
     * @param df The current {@link DropFeedback}.
     * @param event An optional event to determine if the current operation is copy or move.
     */
    private void updateDropFeedback(DropFeedback df, DropTargetEvent event) {
        if (event != null) {
            df.isCopy = event.detail == DND.DROP_COPY;
        }
        df.sameCanvas = mCanvas == mGlobalDragInfo.getSourceCanvas();
        df.invalidTarget = false;
        df.dipScale = mCanvas.getLayoutEditor().getGraphicalEditor().getDipScale();
        df.modifierMask = mCanvas.getGestureManager().getRuleModifierMask();

        // Set the drag bounds, after converting it from control coordinates to
        // layout coordinates
        GlobalCanvasDragInfo dragInfo = GlobalCanvasDragInfo.getInstance();
        Rect dragBounds = null;
        Rect controlDragBounds = dragInfo.getDragBounds();
        if (controlDragBounds != null) {
            CanvasTransform ht = mCanvas.getHorizontalTransform();
            CanvasTransform vt = mCanvas.getVerticalTransform();
            double horizScale = ht.getScale();
            double verticalScale = vt.getScale();
            int x = (int) (controlDragBounds.x / horizScale);
            int y = (int) (controlDragBounds.y / verticalScale);
            int w = (int) (controlDragBounds.w / horizScale);
            int h = (int) (controlDragBounds.h / verticalScale);
            dragBounds = new Rect(x, y, w, h);
        }
        int baseline = dragInfo.getDragBaseline();
        if (baseline != -1) {
            df.dragBaseline = baseline;
        }
        df.dragBounds = dragBounds;
    }

    /**
     * Verifies that event.currentDataType is of type {@link SimpleXmlTransfer}.
     * If not, try to find a valid data type.
     * Otherwise set the drop to {@link DND#DROP_NONE} to cancel it.
     *
     * @return True if the data type is accepted.
     */
    private static boolean checkDataType(DropTargetEvent event) {

        SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();

        TransferData current = event.currentDataType;

        if (sxt.isSupportedType(current)) {
            return true;
        }

        // We only support SimpleXmlTransfer and the current data type is not right.
        // Let's see if we can find another one.

        for (TransferData td : event.dataTypes) {
            if (td != current && sxt.isSupportedType(td)) {
                // We like this type better.
                event.currentDataType = td;
                return true;
            }
        }

        // We failed to find any good transfer type.
        event.detail = DND.DROP_NONE;
        return false;
    }

    /**
     * Returns the mouse location of the drop target event.
     *
     * @param event the drop target event
     * @return a {@link ControlPoint} location corresponding to the top left corner
     */
    private ControlPoint getDropLocation(DropTargetEvent event) {
        return ControlPoint.create(mCanvas, event);
    }

    /**
     * Called on both dragEnter and dragMove.
     * Generates the onDropEnter/Move/Leave events depending on the currently
     * selected target node.
     */
    private void processDropEvent(DropTargetEvent event) {
        if (!mCanvas.getViewHierarchy().isValid()) {
            // We don't allow drop on an invalid layout, even if we have some obsolete
            // layout info for it.
            event.detail = DND.DROP_NONE;
            clearDropInfo();
            return;
        }

        LayoutPoint p = getDropLocation(event).toLayout();

        // Is the mouse currently captured by a DropFeedback.captureArea?
        boolean isCaptured = false;
        if (mFeedback != null) {
            Rect r = mFeedback.captureArea;
            isCaptured = r != null && r.contains(p.x, p.y);
        }

        // We can't switch views/nodes when the mouse is captured
        CanvasViewInfo vi;
        if (isCaptured) {
            vi = mCurrentView;
        } else {
            vi = mCanvas.getViewHierarchy().findViewInfoAt(p);

            // When dragging into the canvas, if you are not over any other view, target
            // the root element (since it may not "fill" the screen, e.g. if you have a linear
            // layout but have layout_height wrap_content, then the layout will only extend
            // to cover the children in the layout, not the whole visible screen area, which
            // may be surprising
            if (vi == null) {
                vi = mCanvas.getViewHierarchy().getRoot();
            }
        }

        boolean isMove = true;
        boolean needRedraw = false;

        if (vi != mCurrentView) {
            // Current view has changed. Does that also change the target node?
            // Note that either mCurrentView or vi can be null.

            if (vi == null) {
                // vi is null but mCurrentView is not, no view is a target anymore
                // We don't need onDropMove in this case
                isMove = false;
                needRedraw = true;
                event.detail = DND.DROP_NONE;
                clearDropInfo(); // this will call callDropLeave.

            } else {
                // vi is a new current view.
                // Query GRE for onDropEnter on the ViewInfo hierarchy, starting from the child
                // towards its parent, till we find one that returns a non-null drop feedback.

                DropFeedback df = null;
                NodeProxy targetNode = null;

                for (CanvasViewInfo targetVi = vi;
                     targetVi != null && df == null;
                     targetVi = targetVi.getParent()) {
                    targetNode = mCanvas.getNodeFactory().create(targetVi);
                    df = mCanvas.getRulesEngine().callOnDropEnter(targetNode,
                            targetVi.getViewObject(), mCurrentDragElements);

                    if (df != null) {
                        // We should also dispatch an onDropMove() call to the initial enter
                        // position, such that the view is notified of the position where
                        // we are within the node immediately (before we for example attempt
                        // to draw feedback). This is necessary since most views perform the
                        // guideline computations in onDropMove (since only onDropMove is handed
                        // the -position- of the mouse), and we want this computation to happen
                        // before we ask the view to draw its feedback.
                        updateDropFeedback(df, event);
                        df = mCanvas.getRulesEngine().callOnDropMove(targetNode,
                                mCurrentDragElements, df, new Point(p.x, p.y));
                    }

                    if (df != null &&
                            event.detail == DND.DROP_MOVE &&
                            mCanvas == mGlobalDragInfo.getSourceCanvas()) {
                        // You can't move an object into itself in the same canvas.
                        // E.g. case of moving a layout and the node under the mouse is the
                        // layout itself: a copy would be ok but not a move operation of the
                        // layout into himself.

                        SelectionItem[] selection = mGlobalDragInfo.getCurrentSelection();
                        if (selection != null) {
                            for (SelectionItem cs : selection) {
                                if (cs.getViewInfo() == targetVi) {
                                    // The node that responded is one of the selection roots.
                                    // Simply invalidate the drop feedback and move on the
                                    // parent in the ViewInfo chain.

                                    updateDropFeedback(df, event);
                                    mCanvas.getRulesEngine().callOnDropLeave(
                                            targetNode, mCurrentDragElements, df);
                                    df = null;
                                    targetNode = null;
                                }
                            }
                        }
                    }
                }

                if (df == null) {
                    // Provide visual feedback that we are refusing the drop
                    event.detail = DND.DROP_NONE;
                    clearDropInfo();

                } else if (targetNode != mTargetNode) {
                    // We found a new target node for the drag'n'drop.
                    // Release the previous one, if any.
                    callDropLeave();

                    // And assign the new one
                    mTargetNode = targetNode;
                    mFeedback = df;

                    // We don't need onDropMove in this case
                    isMove = false;
                }
            }

            mCurrentView = vi;
        }

        if (isMove && mTargetNode != null && mFeedback != null) {
            // this is a move inside the same view
            com.android.ide.common.api.Point p2 =
                new com.android.ide.common.api.Point(p.x, p.y);
            updateDropFeedback(mFeedback, event);
            DropFeedback df = mCanvas.getRulesEngine().callOnDropMove(
                    mTargetNode, mCurrentDragElements, mFeedback, p2);
            mCanvas.getGestureManager().updateMessage(mFeedback);

            if (df == null) {
                // The target is no longer interested in the drop move.
                event.detail = DND.DROP_NONE;
                callDropLeave();

            } else if (df != mFeedback) {
                mFeedback = df;
            }
        }

        if (mFeedback != null) {
            if (event.detail == DND.DROP_NONE && !mFeedback.invalidTarget) {
                // If we previously provided visual feedback that we were refusing
                // the drop, we now need to change it to mean we're accepting it.
                event.detail = DND.DROP_DEFAULT;
                recomputeDragType(event);

            } else if (mFeedback.invalidTarget) {
                // Provide visual feedback that we are refusing the drop
                event.detail = DND.DROP_NONE;
            }
        }

        if (needRedraw || (mFeedback != null && mFeedback.requestPaint)) {
            mCanvas.redraw();
        }

        // Update outline to show the target node there
        OutlinePage outline = mCanvas.getOutlinePage();
        TreeSelection newSelection = TreeSelection.EMPTY;
        if (mCurrentView != null && mTargetNode != null) {
            // Find the view corresponding to the target node. The current view can be a leaf
            // view whereas the target node is always a parent layout.
            if (mCurrentView.getUiViewNode() != mTargetNode.getNode()) {
                mCurrentView = mCurrentView.getParent();
            }
            if (mCurrentView != null && mCurrentView.getUiViewNode() == mTargetNode.getNode()) {
                TreePath treePath = SelectionManager.getTreePath(mCurrentView);
                newSelection = new TreeSelection(treePath);
            }
        }

        ISelection currentSelection = outline.getSelection();
        if (currentSelection == null || !currentSelection.equals(newSelection)) {
            outline.setSelection(newSelection);
        }
    }

    /**
     * Calls onDropLeave on mTargetNode with the current mFeedback. <br/>
     * Then clears mTargetNode and mFeedback.
     */
    private void callDropLeave() {
        if (mTargetNode != null && mFeedback != null) {
            updateDropFeedback(mFeedback, null);
            mCanvas.getRulesEngine().callOnDropLeave(mTargetNode, mCurrentDragElements, mFeedback);
        }

        mTargetNode = null;
        mFeedback = null;
    }

    private void clearDropInfo() {
        callDropLeave();
        mCurrentView = null;
        mCanvas.redraw();
    }

    /**
     * Creates a root element in an empty document.
     * Only the first element's FQCN of the dragged elements is used.
     * <p/>
     * Actual XML handling is done by {@link LayoutCanvas#createDocumentRoot(String)}.
     */
    private void createDocumentRoot(SimpleElement[] elements) {
        if (elements == null || elements.length < 1 || elements[0] == null) {
            return;
        }

        String rootFqcn = elements[0].getFqcn();

        mCanvas.createDocumentRoot(rootFqcn);
    }

    /**
     * An {@link Overlay} to paint the move feedback. This just delegates to the
     * layout rules.
     */
    private class MoveOverlay extends Overlay {
        @Override
        public void paint(GC gc) {
            if (mTargetNode != null && mFeedback != null) {
                RulesEngine rulesEngine = mCanvas.getRulesEngine();
                rulesEngine.callDropFeedbackPaint(mCanvas.getGcWrapper(), mTargetNode, mFeedback);
                mFeedback.requestPaint = false;
            }
        }
    }
}
