/**
 * Copyright (c) 2002-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 v2.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v20.html
 * 
 * Contributors: 
 *   IBM - Initial API and implementation
 */
package org.eclipse.emf.edit.command;


import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.ListIterator;

import org.eclipse.emf.common.command.AbstractCommand;
import org.eclipse.emf.common.command.Command;
import org.eclipse.emf.common.command.CommandWrapper;
import org.eclipse.emf.common.command.CompoundCommand;
import org.eclipse.emf.common.command.IdentityCommand;
import org.eclipse.emf.common.command.UnexecutableCommand;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.edit.EMFEditPlugin;
import org.eclipse.emf.edit.domain.AdapterFactoryEditingDomain;
import org.eclipse.emf.edit.domain.EditingDomain;


/**
 * The drag and drop command logically acts upon an owner object onto which a collection of things is being dragged.
 * The static create method delegates command creation to {@link EditingDomain#createCommand EditingDomain.createCommand},
 * which may or may not result in the actual creation of an instance of this class.
 * <p>
 * The implementation of this class is high-level and generic;
 * it ultimately delegates all behaviour to other types of command,
 * and is typically undoable as a result.
 */
public class DragAndDropCommand extends AbstractCommand implements DragAndDropFeedback
{
  /**
   * This class is used to encode the drag and drop arguments 
   * into an object that will be passed as the feature of a {@link CommandParameter}.
   */
  public static class Detail
  {
    public float location;
    public int operations;
    public int operation;

    public Detail(float location, int operations, int operation)
    {
      this.location = location;
      this.operations = operations;
      this.operation = operation;
    }
  }
 
  /**
   * This creates a command to perform a drag and drop operation upon the owner.
   * See {@link DragAndDropCommand DragAndDropCommand} for a description of the arguments.
   */
  public static Command create(EditingDomain domain, Object owner, float location, int operations, int operation, Collection<?> collection)
  {
    return 
      domain.createCommand
        (DragAndDropCommand.class, new CommandParameter(owner, new Detail(location, operations, operation), collection));
  }

  /**
   * This caches the label.
   */
  protected static final String LABEL = EMFEditPlugin.INSTANCE.getString("_UI_DragAndDropCommand_label");

  /**
   * This caches the description.
   */
  protected static final String DESCRIPTION = EMFEditPlugin.INSTANCE.getString("_UI_DragAndDropCommand_description");

  /**
   * This keeps track of the domain in which this command is created.
   */
  protected EditingDomain domain;

  /**
   * This keeps track of the owner that is the target of the drag and drop.
   */
  protected Object owner;

  /**
   * This keeps track of the location of the drag and drop.
   */
  protected float location;

  /**
   * This keeps track of the lower range of locations in which the effect of this command remains unchanged.
   */
  protected float lowerLocationBound;

  /**
   * This keeps track of the upper range of locations in which the effect of this command remains unchanged.
   */
  protected float upperLocationBound;

  /**
   * This keeps track of the permitted operations.
   */
  protected int operations;

  /**
   * This keeps track of the current operation that will be returned by {@link #getOperation}.
   */
  protected int operation;

  /**
   * This keeps track of the feedback that will be returned by {@link #getFeedback}.
   */
  protected int feedback;

  /**
   * This keeps track of the collection of dragged sources.
   */
  protected Collection<?> collection;

  /**
   * This keeps track of the command that implements the drag side of the operation.
   */
  protected Command dragCommand;

  /**
   * This keeps track of whether execute has been called on the {@link #dragCommand}.
   */
  protected boolean isDragCommandExecuted;

  /**
   * This keeps track of the command that implements the drop side of the operation.
   */
  protected Command dropCommand;

  /**
   * This controls whether or not to optimize the {@link #prepare}.
   */
  protected boolean optimize;

  /**
   * This is to remember which owner to use for the drop command in the optimized mode.
   */
  protected Object optimizedDropCommandOwner;

  /**
   * This creates and instance in the given domain and for the given information.
   * The location should be in the range of 0.0 to 1.0,
   * indicating the relative vertical location of the drag operation,
   * where 0.0 is at the top and 1.0 is at the bottom.
   * The operations is a bitwise mask of the DROP_* values.
   * The operation is the desired operation as specified by a DROP_* value.
   * And the collection contains the source objects being dragged.
   */
  public DragAndDropCommand(EditingDomain domain, Object owner, float location, int operations, int operation, Collection<?> collection)
  {
    this(domain, owner, location, operations, operation, collection, domain == null ? false : domain.getOptimizeCopy());
  }
  
  public DragAndDropCommand
    (EditingDomain domain, Object owner, float location, int operations, int operation, Collection<?> collection, boolean optimize)
  {
    super(LABEL, DESCRIPTION);

    this.domain = domain;
    this.owner = owner;
    this.location = location;
    this.operations = operations;
    this.operation = operation;
    this.collection = collection;
    this.optimize = optimize;

    // EATM Leave this disabled for now.
    //
    this.optimize = false;
  }
  
  protected boolean analyzeForNonContainment(Command command)
  {
    if (command instanceof AddCommand)
    {
      return isNonContainment(((AddCommand)command).getFeature());
    }
    else if (command instanceof SetCommand)
    {
      return isNonContainment(((SetCommand)command).getFeature());
    }
    else if (command instanceof CommandWrapper) 
    {
      return analyzeForNonContainment(((CommandWrapper)command).getCommand());
    }
    else if (command instanceof CompoundCommand) 
    {
      for (Command childCommand : ((CompoundCommand)command).getCommandList())
      {
        if (analyzeForNonContainment(childCommand))
        {
          return true;          
        }
      }
    }
  
    return false;
  }
  
  protected boolean isNonContainment(EStructuralFeature feature)
  {
    return feature instanceof EReference && !((EReference)feature).isContainment();
  }

  public Object getOwner()
  {
    return owner;
  }

  public float getLocation()
  {
    return location;
  }

  public int getOperations()
  {
    return operations;
  }

  public Collection<?> getCollection()
  {
    return collection;
  }

  /**
   * This implementation of prepare is called again to implement {@link #validate validate}.
   * The method {@link #reset} will have been called before doing so.
   */
  @Override
  protected boolean prepare()
  {
    // We'll default to this.
    //
    boolean result = false;

    // If there isn't something obviously wrong with the arguments...
    //
    if (owner != null && collection != null && operations != DROP_NONE && operation != DROP_NONE)
    {
      // If the location is near the boundary, we'll start by trying to do a drop insert.
      //
      if (location <= 0.20 || location >= 0.80)
      {
        // If we could do a drop insert operation...
        //
        result = prepareDropInsert();
        if (result)
        {
          // Set the bounds so that we re-check when we are closer to the middle.
          //
          if (location <= 0.20)
          {
            lowerLocationBound = 0.0F;
            upperLocationBound = 0.2F;
          }
          else 
          {
            lowerLocationBound = 0.8F;
            upperLocationBound = 1.0F;
          }
        }
        else
        {
          // We can try to do a drop on instead.
          //
          reset();
          result = prepareDropOn();

          // Set the bounds so that we re-check when we get near the other end.
          //
          if (location <= 0.20)
          {
            lowerLocationBound = 0.0F;
            upperLocationBound = 0.8F;
          }
          else
          {
            lowerLocationBound = 0.2F;
            upperLocationBound = 1.0F;
          }
        }
      }
      // We are near the middle, so we'll start by trying to do a drop on.
      //
      else
      {
        // If we can do a drop on operation.
        //
        result = prepareDropOn();
        if (result)
        {
          // Set the range so that we re-check when we get aren't in the middle.
          //
          lowerLocationBound = 0.2F;
          upperLocationBound = 0.8F;
        }
        else
        {
          // We can reset and try a drop insert instead.
          //
          reset();
          result = prepareDropInsert();

          // Set the range so that we re-check when we get into the other half.
          //
          if (location <= 0.50)
          {
            lowerLocationBound = 0.0F;
            upperLocationBound = 0.5F;
          }
          else 
          {
            lowerLocationBound = 0.5F;
            upperLocationBound = 1.0F;
          }
        }
      }
    }
    else
    {
      // We'll always be wrong for these arguments, so don't bother re-checking.
      //
      lowerLocationBound = 0.0F;
      upperLocationBound = 1.0F;
    }

    return result;
  }

  /**
   * This can be overridden to determine the parent of an object; this implementation uses {@link EditingDomain#getParent}.
   */
  protected Object getParent(Object object)
  {
    return domain.getParent(object);
  }

  /**
   * This can be overridden to determine the children of an object; this implementation uses {@link EditingDomain#getChildren}.
   */
  protected Collection<?> getChildren(Object object)
  {
    return domain.getChildren(object);
  }

  /**
   * This attempts to prepare a drop insert operation.
   */
  protected boolean prepareDropInsert()
  {
    // This will be the default return value.
    //
    boolean result = false;

    // The feedback is set based on which half we are in.
    // If the command isn't executable, these values won't be used.
    //
    feedback = location < 0.5 ? FEEDBACK_INSERT_BEFORE : FEEDBACK_INSERT_AFTER;

    // If we can't determine the parent.
    //
    Object parent = getParent(owner);
    if (parent == null)
    {
      dragCommand = UnexecutableCommand.INSTANCE;
      dropCommand = UnexecutableCommand.INSTANCE;
    }
    else 
    {
      // Iterate over the children to find the owner.
      //
      Collection<?> children = getChildren(parent);
      int i = 0;
      for (Object child : children)
      {
        // When we match the owner, we're done.
        //
        if (child == owner)
        {
          break;
        }
        ++i;
      }
  
      // If the location indicates after, add one more.
      //
      if (location >= 0.5)
      {
        ++i;
      }

      // Try to create a specific command based on the current desired operation.
      //
      switch (operation)
      {
        case DROP_MOVE:
        {
          result = prepareDropMoveInsert(parent, children, i); 
          break;
        }
        case DROP_COPY:
        {
          result = prepareDropCopyInsert(parent, children, i);
          break;
        }
        case DROP_LINK:
        {
          result = prepareDropLinkInsert(parent, children, i);
          break;
        }
      }

      // If there isn't an executable command we should maybe try a copy operation, but only if we're allowed and not doing a link.
      //
      if (!result && operation != DROP_COPY && operation != DROP_LINK && (operations & DROP_COPY) != 0)
      {
        // Try again.
        //
        reset();
        result = prepareDropCopyInsert(parent, children, i);

        if (result)
        {
          // We've switch the operation!
          //
          operation = DROP_COPY;
        }
      }

      // If there isn't an executable command we should maybe try a link operation, but only if we're allowed and not doing a link.
      //
      if (!result && operation != DROP_LINK && (operations & DROP_LINK) != 0)
      {
        // Try again.
        //
        reset();
        result = prepareDropLinkInsert(parent, children, i);
        if (result)
        {
          // We've switch the operation!
          //
          operation = DROP_LINK;
        }
      }
    }

    return result;
  }

  /**
   * This attempts to prepare a drop move insert operation.
   */
  protected boolean prepareDropMoveInsert(Object parent, Collection<?> children, int index)
  {
    // We don't want to move insert an object before or after itself...
    //
    if (collection.contains(owner))
    {
      dragCommand = IdentityCommand.INSTANCE;
      dropCommand = UnexecutableCommand.INSTANCE;
    }
    // If the dragged objects share a parent...
    //
    else if (children.containsAll(collection))
    {
      dragCommand = IdentityCommand.INSTANCE;

      // Create move commands for all the objects in the collection.
      //
      CompoundCommand compoundCommand = new CompoundCommand();
      List<Object> before = new ArrayList<Object>();
      List<Object> after = new ArrayList<Object>();

      int j = 0;
      for (Object object : children)
      {
        if (collection.contains(object))
        {
          if (j < index)
          {
            before.add(object);
          }
          else if (j > index)
          {
            after.add(object);
          }
        }
        ++j;
      }

      for (Object object : before)
      {
        compoundCommand.append(MoveCommand.create(domain, parent, null, object, index - 1));
      }

      for (ListIterator<Object> objects = after.listIterator(after.size()); objects.hasPrevious(); )
      {
        Object object = objects.previous();
        compoundCommand.append(MoveCommand.create(domain, parent, null, object, index));
      }

      dropCommand = compoundCommand.getCommandList().size() == 0 ? (Command)IdentityCommand.INSTANCE : compoundCommand;
    }
    else if (isCrossDomain())
    {
      dragCommand = IdentityCommand.INSTANCE;
      dropCommand = UnexecutableCommand.INSTANCE;
    }
    else
    {
      // Just remove the objects and add them.
      //
      dropCommand = AddCommand.create(domain, parent, null, collection, index);
      if (analyzeForNonContainment(dropCommand))
      {
        dropCommand.dispose();
        dropCommand = UnexecutableCommand.INSTANCE;
        dragCommand = IdentityCommand.INSTANCE;
      }
      else
      {
        dragCommand = RemoveCommand.create(domain, collection);
      }
    }

    boolean result = dragCommand.canExecute() && dropCommand.canExecute();
    return result;
  }

  protected boolean isCrossDomain()
  {
    for (Object item : collection)
    {
      EditingDomain itemDomain = AdapterFactoryEditingDomain.getEditingDomainFor(item);
      if (itemDomain != null && itemDomain != domain)
      {
        return true;
      }
    }
    return false;
  }

  /**
   * This attempts to prepare a drop copy insert operation.
   */
  protected boolean prepareDropCopyInsert(final Object parent, Collection<?> children, final int index)
  {
    boolean result;
    
    // We don't want to copy insert an object before or after itself...
    //
    if (collection.contains(owner))
    {
      dragCommand = IdentityCommand.INSTANCE;
      dropCommand = UnexecutableCommand.INSTANCE;
      result = false;
    }
    else
    {
      // Copy the collection
      //
      dragCommand = CopyCommand.create(domain, collection);

      if (optimize) 
      {
        result = optimizedCanExecute();
        if (result)
        {
          optimizedDropCommandOwner = parent;
        }
      }
      else 
      {
        if (dragCommand.canExecute() && dragCommand.canUndo())
        {
          dragCommand.execute();
          isDragCommandExecuted = true;

          // And add the copy.
          //
          dropCommand = AddCommand.create(domain, parent, null, dragCommand.getResult(), index);
          if (analyzeForNonContainment(dropCommand))
          {
            dropCommand.dispose();
            dropCommand = UnexecutableCommand.INSTANCE;

            dragCommand.undo();
            dragCommand.dispose();
            isDragCommandExecuted = false;
            dragCommand = IdentityCommand.INSTANCE;
          }
          result = dropCommand.canExecute();
        }
        else
        {
          dropCommand = UnexecutableCommand.INSTANCE;
          result = false;
        }
      } // if optimize
    } // if collection

    return result;
  }

  /**
   * This attempts to prepare a drop link insert operation.
   */
  protected boolean prepareDropLinkInsert(Object parent, Collection<?> children, int index)
  {
    boolean result;
    
    // We don't want to insert an object before or after itself...
    //
    if (collection.contains(owner))
    {
      dragCommand = IdentityCommand.INSTANCE;
      dropCommand = UnexecutableCommand.INSTANCE;
      result = false;
    }
    else
    {
      dragCommand = IdentityCommand.INSTANCE;

      // Add the collection
      //
      dropCommand = AddCommand.create(domain, parent, null, collection, index);
      if (!analyzeForNonContainment(dropCommand))
      {
        dropCommand.dispose();
        dropCommand = UnexecutableCommand.INSTANCE;
      }
      result = dropCommand.canExecute();
    }

    return result;
  }

  /**
   * This attempts to prepare a drop on operation.
   */
  protected boolean prepareDropOn()
  {
    // This is the default return value.
    //
    boolean result = false;

    // This is the feedback we use to indicate drop on; it will only be used if the command is executable.
    //
    feedback = FEEDBACK_SELECT;

    // Prepare the right type of operation.
    //
    switch (operation)
    {
      case DROP_MOVE:
      {
        result = prepareDropMoveOn();
        break;
      }
      case DROP_COPY:
      {
        result = prepareDropCopyOn();
        break;
      }
      case DROP_LINK:
      {
        result = prepareDropLinkOn();
        break;
      }
    }

    // If there isn't an executable command we should maybe try a copy operation, but only if we're allowed and not doing a link.
    //
    if (!result && operation != DROP_COPY && operation != DROP_LINK && (operations & DROP_COPY) != 0)
    {
      reset();
      result = prepareDropCopyOn();
      if (result)
      {
        operation = DROP_COPY;
      }
    }

    // If there isn't an executable command we should maybe try a link operation, but only if we're allowed and not doing a link.
    //
    if (!result && operation != DROP_LINK && (operations & DROP_LINK) != 0)
    {
      reset();
      result = prepareDropLinkOn();
      if (result)
      {
        operation = DROP_LINK;
      }
    }

    return result;
  }

  /**
   * This attempts to prepare a drop move on operation.
   */
  protected boolean prepareDropMoveOn()
  {
    if (isCrossDomain())
    {
      dragCommand = IdentityCommand.INSTANCE;
      dropCommand = UnexecutableCommand.INSTANCE;
    }
    else
    {
      dropCommand = AddCommand.create(domain, owner, null, collection);
      if (analyzeForNonContainment(dropCommand))
      {
        dropCommand.dispose();
        dropCommand = UnexecutableCommand.INSTANCE;
        dragCommand = IdentityCommand.INSTANCE;
      }
      else
      {
        dragCommand = RemoveCommand.create(domain, collection);
      }
    }

    boolean result = dragCommand.canExecute() && dropCommand.canExecute();
    return result;
  }

  /**
   * This attempts to prepare a drop copy on operation.
   */
  protected boolean prepareDropCopyOn()
  {
    boolean result;
    
    dragCommand = CopyCommand.create(domain, collection);

    if (optimize) 
    {
      result = optimizedCanExecute();
      if (result)
      {
        optimizedDropCommandOwner = owner;
      }
    }
    else 
    {
      if (dragCommand.canExecute() && dragCommand.canUndo())
      {
        dragCommand.execute();
        isDragCommandExecuted = true; 
        dropCommand = AddCommand.create(domain, owner, null, dragCommand.getResult());
        if (analyzeForNonContainment(dropCommand))
        {
          dropCommand.dispose();
          dropCommand = UnexecutableCommand.INSTANCE;

          dragCommand.undo();
          dragCommand.dispose();
          isDragCommandExecuted = false;
          dragCommand = IdentityCommand.INSTANCE;
        }
      }
      else
      {
        dropCommand = UnexecutableCommand.INSTANCE;
      }
  
      result = dragCommand.canExecute() && dropCommand.canExecute();
    }
    return result;
  }

  /**
   * This attempts to prepare a drop link on operation.
   */
  protected boolean prepareDropLinkOn()
  {
    dragCommand = IdentityCommand.INSTANCE;
    dropCommand = SetCommand.create(domain, owner, null, collection);

    // If we can't set the collection, try setting use the single value of the collection.
    //
    if (!dropCommand.canExecute() && collection.size() == 1)
    {
      dropCommand.dispose();
      dropCommand = SetCommand.create(domain, owner, null, collection.iterator().next());
    }

    if (!dropCommand.canExecute() || !analyzeForNonContainment(dropCommand))
    {
      dropCommand.dispose();
      dropCommand = AddCommand.create(domain, owner, null, collection);
      if (!analyzeForNonContainment(dropCommand))
      {
        dropCommand.dispose();
        dropCommand = UnexecutableCommand.INSTANCE;
      }
    }

    boolean result = dropCommand.canExecute();
    return result;
  }

  protected boolean optimizedCanExecute()
  {
    // We'll assume that the copy command can execute and that adding a copy of the clipboard
    // is the same test as adding the clipboard contents itself.
    //
    Command addCommand = AddCommand.create(domain, owner, null, collection);
    boolean result = addCommand.canExecute() && !analyzeForNonContainment(addCommand);
    addCommand.dispose();
    return result;
  }

  /**
   * This restores the command to its default initialized state, disposing an command that may have been contained.
   */
  protected void reset()
  {
    if (dragCommand != null)
    {
      if (isDragCommandExecuted)
      {
        if (dragCommand.canUndo())
        {
          dragCommand.undo();
        }
      }
      dragCommand.dispose();
    }

    if (dropCommand != null)
    {
      dropCommand.dispose();
    }
  
    // Reset as in the constructor.
    //
    isPrepared = false;
    isExecutable = false;

    dragCommand = null;
    isDragCommandExecuted = false;
    dropCommand = null;
    optimizedDropCommandOwner = null;
  }

  /**
   * This is called by EditingDomainViewerDropAdapter to determine if the drag and drop operation is still enabled.
   */
  public boolean validate(Object owner, float location, int operations, int operation, Collection<?> collection)
  {
    // If the operation has changed significantly...
    //
    if (owner != this.owner ||
         location != this.location || (location < lowerLocationBound || location > upperLocationBound) ||
         operation != this.operation ||
         collection != this.collection)
    {
      // Clean it up.
      //
      reset();

      // Set the arguments again.
      //
      this.owner = owner;
      this.location = location;
      this.operations = operations;
      this.operation = operation;
      this.collection = collection;

      // Determine if the operation is executable.
      //
      return canExecute();
    }
    else
    {
      // Just return the cached result.
      //
      return isExecutable;
    }
  }

  public int getFeedback()
  {
    // Only return the feedback for an executable command.
    //
    return isExecutable ? feedback : FEEDBACK_SELECT;
  }

  public int getOperation()
  {
    // Only return the operation for an executable command.
    //
    return isExecutable ? operation : DROP_NONE;
  }

  public void execute()
  {
    if (dragCommand != null && !isDragCommandExecuted)
    {
      // special case in optimized mode, the drag command wasn't executed, so drag command was not yet created
      //if ((optimizedDropCommandOwner != null) && (dropCommand == null)) {
      if (optimizedDropCommandOwner != null) 
      {
        if (dragCommand.canExecute()) 
        {
          dragCommand.execute();
          dropCommand = AddCommand.create(domain, optimizedDropCommandOwner, null, dragCommand.getResult());
        }
        else 
        {
          // Thread.dumpStack();
        }
      }
      else 
      {
        dragCommand.execute();
      }
    }

    if (dropCommand != null)
    {
      if (dropCommand.canExecute()) 
      {
        dropCommand.execute();
      }
      else 
      {
        // Thread.dumpStack();
      }
    }
  }

  @Override
  public void undo()
  {
    if (dropCommand != null)
    {
      dropCommand.undo();
    }

    if (dragCommand != null)
    {
      dragCommand.undo();
    }
  }

  public void redo()
  {
    if (dragCommand != null)
    {
      dragCommand.redo();
    }

    if (dropCommand != null)
    {
      dropCommand.redo();
    }
  }

  @Override
  public void dispose()
  {
    if (dragCommand != null)
    {
      dragCommand.dispose();
    }
    if (dropCommand != null)
    {
      dropCommand.dispose();
    }
  }

  @Override
  public Collection<?> getResult()
  {
    return
      dropCommand != null ?
        dropCommand.getResult() :
        super.getResult();
  }

  @Override
  public Collection<?> getAffectedObjects()
  {
    return 
      dropCommand != null ?
        dropCommand.getAffectedObjects() :
        super.getAffectedObjects();
  }

  /**
   * This gives an abbreviated name using this object's own class' name, without package qualification,
   * followed by a space separated list of <tt>field:value</tt> pairs.
   */
  @Override
  public String toString()
  {
    StringBuffer result = new StringBuffer(super.toString());
    result.append(" (domain: " + domain + ")");
    result.append(" (owner: " + owner + ")");
    result.append(" (location: " + location + ")");
    result.append(" (lowerLocationBound: " + lowerLocationBound + ")");
    result.append(" (upperLocationBound: " + upperLocationBound + ")");
    result.append(" (operations: " + operations + ")");
    result.append(" (operation: " + operation + ")");
    result.append(" (collection: " + collection + ")");
    result.append(" (feedback: " + feedback + ")");

    return result.toString();
  }
}
