/*******************************************************************************
 * Copyright (c) 2004, 2019 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 * 
 * Contributors:
 *     IBM Corporation - Initial API and implementation
 *     Jens Lukowski/Innoopract - initial renaming/restructuring
 *******************************************************************************/
package org.eclipse.wst.common.ui.internal.dnd;


import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;

import org.eclipse.core.commands.Command;
import org.eclipse.jface.viewers.AbstractTreeViewer;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DropTargetAdapter;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.TransferData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Item;
import org.eclipse.swt.widgets.ScrollBar;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.swt.widgets.Widget;


/**
 * This implementation of a drop target listener 
 * is designed to turn a drag and drop operation into a {@link Command} based on the model objects of an {@link EditingDomain}
 * and created by {@link DragAndDropManager#create}.
 * It is designed to do early data transfer so the the enablement and feedback of the drag and drop interaction
 * can intimately depend on the state of the model objects involed.
 * <p>
 * The base implementation of this class should be sufficient for most applications.
 * Any change in behaviour is typically accomplished by overriding 
 * {@link ItemProviderAdapter}.createDragAndDropCommand
 * to return a derived implementation of {@link DragAndDropCommand}.
 * This is how one these adapters is typically hooked up:
 * <pre>
 *   viewer.addDropSupport
 *     (DND.DROP_COPY | DND.DROP_MOVE | DND.DROP_LINK,
 *      new Transfer [] { ObjectTransfer.getInstance() },
 *      EditingDomainViewerDropAdapter(viewer));
 * </pre>
 * <p>
 * This implementation prefers to use a {@link ObjectTransfer}, 
 * which short-circuits the transfer process for simple transfers within the workbench,
 * the method {@link #getDragSource} can be overriden to change the behaviour.
 * The implementation also only handles an {@link IStructuredSelection},
 * but the method {@link #extractDragSource} can be overriden to change the behaviour.
 * <p>
 * You can call {@link #setHoverThreshold} to set the amount of time, in milliseconds, 
 * to hover over an item before {@link #hover} is called;
 * the default is 1500 milliseconds.
 */
public class ViewerDropAdapter extends DropTargetAdapter
{
  /**
   * This is the viewer for which this is a drop target listener.
   */
  protected Viewer viewer;

  /**
   * This is the collection of source objects being dragged.
   */
  protected Collection source;

  /**
   * This is the command created during dragging which provides the feedback and will carry out the action upon completion.
   */
  //  protected Command command;
  protected DragAndDropCommand command;
 
  /**
   * This records the object for which the {@link #command} was created.
   */
  protected Object commandTarget;

  /**
   * The amount of time to hover over a tree item before expanding it
   */
  protected int hoverThreshold = 1500;

  /**
   * The is the time the mouse first started hovering over the current item.
   */
  protected long hoverStart = 0;

  /**
   * This keeps track of the most recent item for the {@link #hoverStart}.
   */
  protected Widget previousItem;

  /**
   * This keeps track of the original operation that was in effect before we set the event.detail in here.
   */
  protected int originalOperation;

  /**
   * This keeps track of the information used to create the current command.
   */
  protected DragAndDropCommandInformation dragAndDropCommandInformation;

  protected DragAndDropManager dragAndDropManager;

  /**
   * This creates and instance of the given domain and viewer.
   */
  public ViewerDropAdapter(Viewer viewer, DragAndDropManager dragAndDropManager)
  {
    this.viewer = viewer;
    this.dragAndDropManager = dragAndDropManager;
  }

  /**
   * This is called when the mouse first enters or starts dragging in the viewer.
   */
  public void dragEnter(DropTargetEvent event)
  {
    originalOperation = event.detail;
    helper(event);
  }

  /**
   * This is called when the mouse leaves or stops dragging in the viewer
   */
  public void dragLeave(DropTargetEvent event)
  {
    // Clean up the command if there is one.
    //
    if (command != null)
    {
//        command.dispose();
      command = null;
      commandTarget = null;
    }

    // Reset the other values.
    //
    previousItem = null;
    hoverStart = 0;
    source = null;
  }

  /**
   * This is called when the operation has changed in some way, typically because the user changes keyboard modifiers.
   */
  public void dragOperationChanged(DropTargetEvent event)
  {
    originalOperation = event.detail;
    helper(event);
  }

  /**
   * This is called repeated when the mouse over the viewer.
   */
  public void dragOver(DropTargetEvent event) 
  {
    helper(event);
  }

  /**
   * This is called just as the mouse is released over the viewer to initiate a drop.
   */
  public void dropAccept(DropTargetEvent event) 
  {
    // There seems to be a bug in SWT that the view may have scrolled.
    // helper(event);
  }

  /**
   * This is called to indate that the drop action should be invoked.
   */
  public void drop(DropTargetEvent event)
  {
    // There seems to be a bug in SWT that the view may have scrolled.
    // helper(event);
    if (dragAndDropCommandInformation != null)
    {
      command = dragAndDropCommandInformation.createCommand();

      // Execute the command
      command.execute();

      // Clean up the state.
      //
      command = null;
      commandTarget = null;
      previousItem = null;
      hoverStart = 0;
      source = null;
    }
  }

  /**
   * This method is called the same way for each of the {@link org.eclipse.swt.dnd.DropTargetListener} methods, except during leave.
   */
  protected void helper(DropTargetEvent event)
  {
    // Try to get the source if there isn't one.
    //
    if (source == null)
    {
      source = getDragSource(event);
    }                   
    else if (event.currentDataType == null)
    {
      setCurrentDataType(event);
    }

    // If there's still no source, wait until the next time to try again.
    //
    if (source == null)
    {
      event.detail = DND.DROP_NONE;
      event.feedback = DND.FEEDBACK_SELECT;
    }
    // Otherwise, if we need to scroll...
    //
    else if (scrollIfNeeded(event))
    {
      // In the case that we scroll, we just do all the work on the next event and only just scroll now.
      //
      event.feedback = DND.FEEDBACK_SELECT;
    }
    else
    {
      // Get the data from the item, if there is one.
      //
      Object target = event.item == null ? null : event.item.getData();
      if (target instanceof Item)
      {
        target = ((Item)target).getData();
      }

      // Do the logic to determine the hover information.
      // If we're over a new item from before.
      //
      if (event.item != previousItem)
      {
        // Remember the item and the time.
        //
        previousItem = event.item;
        hoverStart = event.time;
      } 
      else if (target != null)
      {
        if (event.time - hoverStart > hoverThreshold)
        {
          hover(target);

          // We don't need to hover over this guy again.
          //
          hoverStart = Integer.MAX_VALUE;
        }
      }

      // Determine if we can create a valid command at the current mouse location.
      //
      boolean valid = false;

      // If we don't have a previous cached command...
      //
      if (command == null)
      {
        // Create the command and test if it is executable.
        //
        commandTarget = target;
        command = dragAndDropManager.createCommand(target, getLocation(event), event.operations, event.detail, source);
        if (command != null)
        {
        	valid = command.canExecute();
        }
      }
      else
      {
        int operation = originalOperation != event.detail ? originalOperation : event.detail;

        // Check if the cached command is able to provide drag and drop feedback.
        //
        if (target == commandTarget)// && command instanceof DragAndDropFeedback)
        {
          float location = getLocation(event);

          dragAndDropCommandInformation = 
            new DragAndDropCommandInformation(target, location, event.operations, operation, source);

          // If so, revalidate the command.
          //
          command.reinitialize(target, location, event.operations, operation, source);
          if (command != null)
          {
          valid = command.canExecute();
          }
        }
        else
        {
          // If not, dispose the current command and create a new one.
          //
          //          command.dispose();
          commandTarget = target;

          dragAndDropCommandInformation = 
            new DragAndDropCommandInformation(target, getLocation(event), event.operations, operation, source);

          // DragAndDropManager.create(domain, target, getLocation(event), event.operations, operation, source);
          //
          command = dragAndDropCommandInformation.createCommand();

          if (command != null)
          {
          	valid = command.canExecute();
          }
        }
      }

      // If this command can provide detailed drag and drop feedback...
      //
      //if (command instanceof DragAndDropCommand)
      if (command != null)
      {
        // Use the feedback for the operation and mouse point from the command.
        //
        event.detail = command.getOperation();
        event.feedback = command.getFeedback();
      }
      else if (valid)
      {
        // All we can know is to provide selection feedback.
        //
        event.feedback = DND.FEEDBACK_SELECT;
      }
      else
      {
        // There is no executable command, so we'd better nix the whole deal.
        //
        event.detail = DND.DROP_NONE;
        event.feedback = DND.FEEDBACK_SELECT;
      }
    }
  }
    

  protected void setCurrentDataType(DropTargetEvent event)
  {                                
    ObjectTransfer objectTransfer = ObjectTransfer.getInstance();
    TransferData [] dataTypes = event.dataTypes;
    for (int i = 0; i < dataTypes.length; ++i)
    {
      TransferData transferData = dataTypes[i];
      // If the local tansfer supports this datatype, switch to that data type
      //
      if (objectTransfer.isSupportedType(transferData))
      {
        event.currentDataType = transferData;
      }
    }
  }

  /** 
   * This  attempts to extract the drag source from the event early, i.e., before the drop method.
   * This implementation tries to use a {@link org.eclipse.wst.common.ui.internal.dnd.ObjectTransfer}.
   */
  protected Collection getDragSource(DropTargetEvent event)
  {
    // Check whether the current data type can be transfered locally.
    //
    ObjectTransfer objectTransfer = ObjectTransfer.getInstance();
    if (!objectTransfer.isSupportedType(event.currentDataType))
    {
      // Iterate over the data types to see if there is a datatype that supports a local transfer.
      //
      setCurrentDataType(event);
      return null;
    }
    else
    {
      // Transfer the data and extract it.
      //
      Object object = objectTransfer.nativeToJava(event.currentDataType);
      if (object == null)
      {
      	return null;
      }
      else
      {
      	return extractDragSource(object);
      }
    }
  }

  /**
   * This extracts a collection of dragged source objects from the given object retrieved from the transfer agent.
   * This default implementation converts a structured selection into a collection of elements.
   */
  protected Collection extractDragSource(Object object)
  {
    // Transfer the data and convert the structured selection to a collection of objects.
    //
    if (object instanceof IStructuredSelection)
    {
      Collection result = new ArrayList();
      for (Iterator elements = ((IStructuredSelection)object).iterator(); elements.hasNext(); )
      {
        result.add(elements.next());
      }
      return result;
    }
    else
    {
      return Collections.EMPTY_LIST;
    }
  }

  /**
   * This gets the amount of time, in milliseconds, to hover over an item before {@link #hover} is called.
   */
  public int getHoverThreshold()
  {
    return hoverThreshold;
  }

  /**
   * This set the amount of time, in milliseconds, to hover over an item before {@link #hover} is called.
   */
  public void setHoverThreshold(int hoverThreshold)
  {
    this.hoverThreshold = hoverThreshold;
  }

  /**
   * This is called when the cursor has hovered over the given target for longer than {@link #hoverThreshold}.
   */
  protected void hover(Object target)
  {
    if (viewer instanceof AbstractTreeViewer)
    {
      ((AbstractTreeViewer)viewer).expandToLevel(target, 1);
    }
  }

  /** 
   * This returns whether a scroll was performed based on the given drag coordinates.
   */
  protected boolean scrollIfNeeded(DropTargetEvent event)
  {
    // By default we'll not scroll
    //
    boolean result = false;

    // We only handle a tree item right now.
    //
    if (event.item instanceof TreeItem)
    {
      // Tree items have special data that will help.
      //
      TreeItem treeItem = (TreeItem)event.item;

      // We need need the point in the coordinates of the control and the control's bounds.
      //
      Tree tree = treeItem.getParent();
      Point point = tree.toControl(new Point(event.x, event.y));
      Rectangle bounds = tree.getClientArea();

      // This is the distance in pixels from the top or bottom that will cause scrolling.
      //
      int scrollEpsilon = Math.min(treeItem.getBounds().height, bounds.height / 3);

      // This will be the item that should be scrolled into the view.
      //
      TreeItem scrollTreeItem = null;

      // If we should scroll up.
      //
      if (point.y < scrollEpsilon)
      {
        // Determine the parent to find the sibling.
        //
        TreeItem parent = treeItem.getParentItem();
        // Walk through the siblings.
        //
        TreeItem [] children = parent == null ? tree.getItems() : parent.getItems();
        for (int i = 0; i < children.length; ++i)
        {
          // Is this a match.
          //
          if (children[i] == treeItem)
          {
            // If there is a previous sibling...
            //
            if (i > 0)
            {
              scrollTreeItem = children[i - 1];

              // Get the last deepest descendent of this previous sibling.
              //
              for (;;)
              {
                children = scrollTreeItem.getItems();
                if (children != null && children.length != 0 && scrollTreeItem.getExpanded())
                {
                  scrollTreeItem = children[children.length - 1];
                }
                else
                {
                  break;
                }
              }
            }
            else
            {
              // The parent must be the previous.
              //
              scrollTreeItem = parent;
            }

            // We're done after the match.
            //
            break;
          }
        }
      }
      // If we should scroll down...
      //
      else if (bounds.height - point.y < scrollEpsilon)
      {
        // If this thing has visible children, then the first child must be next.
        //
        TreeItem [] children = treeItem.getItems();
        if (children != null && children.length != 0 && treeItem.getExpanded())
        {
          scrollTreeItem = children[0];
        }
        else
        {
          // We need the parent to determine siblings and will walk up the tree if we are the last sibling.
          //
          while (scrollTreeItem == null)
          {
            // If there's no parent, we're done.
            //
            TreeItem parent = treeItem.getParentItem();
            // Walk the children.
            //
            children = parent == null ? tree.getItems() : parent.getItems();
            for (int i = 0; i < children.length; ++i)
            {
              // When we find the child.
              //
              if (children[i] == treeItem)
              {
                // If the next index is a valid index...
                //
                if (++i < children.length)
                {
                  // We've found the item.
                  //
                  scrollTreeItem = children[i];
                }

                // We're done with this parent.
                //
                break;
              }
            }

            if (parent == null)
            {
              break;
            }

            // Walk up.
            //
            treeItem = parent;
          }
        }
      }

      // If we should scroll.
      //
      if (scrollTreeItem != null)
      {
        // Only scroll if we're on an item for a while.
        //
        if (previousItem != null && event.time - hoverStart > 200)
        {
          ScrollBar verticalScrollBar = tree.getVerticalBar();
          if (verticalScrollBar != null)
          {
            int before = verticalScrollBar.getSelection();

            // Make sure the item is scrolled in place.
            //
            tree.showItem(scrollTreeItem);

            // Make sure we don't scroll again quickly.
            //
            previousItem = null;

            // Indicate that we've done a scroll and nothing else should be done.
            //
            result = before != verticalScrollBar.getSelection();
          }
        }
        else
        {
          // If the item changes, reset the timer information.
          //
          if (event.item != previousItem)
          {
            previousItem = event.item;
            hoverStart = event.time;
          }
        }
      }
    }
    else if (event.item instanceof TableItem)
    {
      // Table items have special data that will help.
      //
      TableItem tableItem = (TableItem)event.item;

      // We need need the point in the coordinates of the control and the control's bounds.
      //
      Table table = tableItem.getParent();
      Point point = table.toControl(new Point(event.x, event.y));
      Rectangle bounds = table.getClientArea();
      if (table.getHeaderVisible())
      {
        int offset = table.getItemHeight();
        bounds.y += offset;
        bounds.height -= offset;
        point.y -= offset;
      }

      // The position of this item.
      //
      int index = table.indexOf(tableItem);

      // This is the distance in pixels from the top or bottom that will cause scrolling.
      //
      int scrollEpsilon = Math.min(tableItem.getBounds(0).height, bounds.height / 3);

      // This will be the item that should be scrolled into the view.
      //
      TableItem scrollTableItem = null;

      // If we should scroll up.
      //
      if (point.y < scrollEpsilon)
      {
        if (index > 0)
        {
          scrollTableItem = table.getItems()[index - 1];
        }
      }
      // If we should scroll down...
      //
      else if (bounds.height - point.y < scrollEpsilon)
      {
        if (index + 1 < table.getItems().length)
        {
          scrollTableItem =  table.getItems()[index + 1];
        }
      }

      // If we should scroll.
      //
      if (scrollTableItem != null)
      {
        // Only scroll if we're on an item for a while.
        //
        if (previousItem != null && event.time - hoverStart > 200)
        {
          ScrollBar verticalScrollBar = table.getVerticalBar();
          if (verticalScrollBar != null)
          {
            int before = verticalScrollBar.getSelection();

            // Make sure the item is scrolled in place.
            //
            table.showItem(scrollTableItem);

            // Make sure we don't scroll again quickly.
            //
            previousItem = null;

            // Indicate that we've done a scroll and nothing else should be done.
            //
            result = before != verticalScrollBar.getSelection();
          }
        }
        else
        {
          // If the item changes, reset the timer information.
          //
          if (event.item != previousItem)
          {
            previousItem = event.item;
            hoverStart = event.time;
          }
        }
      }
    }

    return result;
  }

  protected static float getLocation(DropTargetEvent event) 
  {
    if (event.item instanceof TreeItem)
    {
      TreeItem treeItem = (TreeItem)event.item;
      Control control = treeItem.getParent();
      Point point = control.toControl(new Point(event.x, event.y));
      Rectangle bounds = treeItem.getBounds();
      return (float)(point.y - bounds.y) / (float)bounds.height;
    }
    else if (event.item instanceof TableItem)
    {
      TableItem tableItem = (TableItem)event.item;
      Control control = tableItem.getParent();
      Point point = control.toControl(new Point(event.x, event.y));
      Rectangle bounds = tableItem.getBounds(0);
      return (float)(point.y - bounds.y) / (float)bounds.height;
    }
    else
    {
      return 0.0F;
    }
  }

  protected class DragAndDropCommandInformation
  {
    //    protected EditingDomain domain;
    protected Object target;
    protected float location;
    protected int operations;
    protected int operation;
    protected Collection source;
    public DragAndDropCommandInformation
      (Object target, float location, int operations, int operation, Collection source)
    {
      this.target = target;
      this.location = location;
      this.operations = operations;
      this.operation = operation;
      this.source = new ArrayList(source);
    }

    public DragAndDropCommand createCommand()
    {
      return dragAndDropManager.createCommand(target, location, operations, operation, source);
    }
  }
}
