/**
 * Copyright (c) 2002-2006 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.Collection;
import java.util.Collections;

import org.eclipse.emf.common.command.Command;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.edit.EMFEditPlugin;
import org.eclipse.emf.edit.domain.EditingDomain;


/**
 * The move command logically acts upon an owner object that has a collection-based feature 
 * containing an object that is to be moved to a new position within the collection.
 * 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.
 * Like all the low level commands in this package, the move command is undoable.
 *
 * <p>
 * The implementation of this class is low-level and EMF specific;
 * it allows an object to be moved to a new position within a many-valued feature of an owner,
 * i.e., it is equivalent of the call
 * <pre>
 *   ((EList)((EObject)owner).eGet((EStructuralFeature)feature)).move(index, object);
 * </pre>
 *
 * <p>
 * It can also be used as an equivalent to the call
 * <pre>
 *   ((EList)extent).move(index, object);
 * </pre>
 * which is how root objects are moved within the contents of a resource.
 * Like all the low-level commands in this package, the move command is undoable.
 *
 * <p>
 * A move command is an {@link OverrideableCommand}.
 */
public class MoveCommand extends AbstractOverrideableCommand
{
  /**
   * This creates a command to move particular value to a particular index in the specified feature of the owner.
   * The feature will often be null because the domain will deduce it.
   */
  public static Command create(EditingDomain domain, Object owner, Object feature, Object value, int index)
  {
    return domain.createCommand(MoveCommand.class, new CommandParameter(owner, feature, value, index));
  }

  /**
   * This creates a command to move the value at the source index to the target index in the specified feature of the owner.
   * The feature should typically not be null because the domain likely can't deduce it.
   * @since 2.15
   */
  public static Command create(EditingDomain domain, Object owner, Object feature, int sourceIndex, int targetIndex)
  {
    return create(domain, owner, feature, CommandParameter.Indices.create(sourceIndex), targetIndex);
  }

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

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

  /**
   * This caches the description for a list-based command.
   */
  protected static final String DESCRIPTION_FOR_LIST = EMFEditPlugin.INSTANCE.getString("_UI_MoveCommand_description_for_list");

  /**
   * This is the owner object upon which the command will act.
   * It could be null in the case that we are dealing with an {@link org.eclipse.emf.common.util.EList}.
   */
  protected EObject owner;

  /**
   * This is the feature of the owner object upon the command will act.
   * It could be null, in the case that we are dealing with an {@link org.eclipse.emf.common.util.EList}.
   */
  protected EStructuralFeature feature;

  /**
   * This is the list in which the command will move an object.
   */
  protected EList<Object> ownerList;

  /**
   * This is the value being moved within the owner list.
   */
  protected Object value;

  /**
   * This is the position to which the object will be moved.
   */
  protected int index;

  /**
   * This is the original position to which the object will be moved upon undo.
   */
  protected int oldIndex;

  /**
   * This constructs a primitive command to move a particular value to a particular index 
   * of the specified many-valued feature of the owner.
   */
  public MoveCommand(EditingDomain domain, EObject owner, EStructuralFeature feature, Object value, int index)
  {
    super(domain, LABEL, DESCRIPTION);

    this.owner = owner;
    this.feature = feature;
    this.value = value;
    this.index = index;
 
    ownerList = getOwnerList(this.owner, feature);
    oldIndex = -1;
  }

  /**
   * This constructs a primitive command to move a value at a particular index to another particular index 
   * of the specified many-valued feature of the owner.
   * @since 2.7
   */
  public MoveCommand(EditingDomain domain, EObject owner, EStructuralFeature feature, int sourceIndex, int targetIndex)
  {
    super(domain, LABEL, DESCRIPTION);

    this.owner = owner;
    this.feature = feature;

    index = targetIndex;
    oldIndex = sourceIndex;

    ownerList = getOwnerList(this.owner, feature);
    if (ownerList != null && ownerList.size() > sourceIndex && sourceIndex >= 0)
    {
      value = ownerList.get(sourceIndex);
    }
  }

  /**
   * This constructs a primitive command to move a particular value to a particular index of the specified extent.
   */
  public MoveCommand(EditingDomain domain, EList<?> list, Object value, int index)
  {
    super(domain, LABEL, DESCRIPTION_FOR_LIST);

    this.value = value;
    this.index = index;

    oldIndex = -1;

    @SuppressWarnings("unchecked")
    EList<Object> untypedList = (EList<Object>)list;
    ownerList = untypedList;
  }

  /**
   * This constructs a primitive command to move a value at a particular index to another particular index of the specified extent.
   * @since 2.7
   */
  public MoveCommand(EditingDomain domain, EList<?> list, int sourceIndex, int targetIndex)
  {
    super(domain, LABEL, DESCRIPTION_FOR_LIST);

    if (list != null && list.size() > sourceIndex && sourceIndex >= 0)
    {
      value = list.get(sourceIndex);
    }
    index = targetIndex;
    oldIndex = sourceIndex;

    @SuppressWarnings("unchecked")
    EList<Object> untypedList = (EList<Object>)list;
    ownerList = untypedList;
  }

  /**
   * This returns the owner object upon which the command will act.
   * It could be null in the case that we are dealing with an {@link org.eclipse.emf.common.util.EList}.
   */
  public EObject getOwner()
  {
    return owner;
  }

  /**
   * This returns the feature of the owner object upon the command will act.
   * It could be null, in the case that we are dealing with an {@link org.eclipse.emf.common.util.EList}.
   */
  public EStructuralFeature getFeature()
  {
    return feature;
  }

  /**
   * This returns the list in which the command will move an object.
   */
  public EList<Object> getOwnerList()
  {
    return ownerList;
  }

  /**
   * This returns the value being moved.
   */
  public Object getValue()
  {
    return value;
  }

  /**
   * This returns the position to which the value will be moved.
   */
  public int getIndex()
  {
    return index;
  }

  /**
   * This returns the original position to which the object will be moved upon undo.
   */
  public int getOldIndex()
  {
    return oldIndex;
  }

  @Override
  protected boolean prepare()
  {
    // Return whether there is a list and the indices are within range.
    //
    boolean result =
      ownerList != null  && 
         index >= 0 && 
         index < ownerList.size() &&
         prepareValue() &&
         (owner == null || !domain.isReadOnly(owner.eResource()));

    return result;
  }

  /**
   * Called by {@link #prepare()} if the {@link #ownerList} is not {@code null} and the {@link #index} is a valid index for the {@code ownerList}.
   * <p>
   * It handles the case that the {@link #value} is an {@link CommandParameter.Indices},
   * in which case  it must specify a single {@link CommandParameter.Indices#getIndices() index}
   * and that index but be valid with respect to the {@code ownerList}.
   * The {@link #value} is updated with the value at that index,
   * and the {@link #oldIndex} is updated to be that index.
   * </p>
   *
   * @since 2.15
   * @returns {@code true} if the {@code value} and {@code oldIndex} are valid with respect to the {@code ownerList}.
   */
  protected boolean prepareValue()
  {
    if (value instanceof CommandParameter.Indices)
    {
      int[] indices = ((CommandParameter.Indices)value).getIndices();
      if (indices.length != 1)
      {
        return false;
      }
      else
      {
        int oldIndex = indices[0];
        if (oldIndex < 0 || oldIndex >= ownerList.size())
        {
          return false;
        }
        else
        {
          this.oldIndex = oldIndex;
          value = ownerList.get(oldIndex);
          return true;
        }
      }
    }
    else
    {
       return (oldIndex == -1 ? ownerList.contains(value) : oldIndex >= 0 && oldIndex < ownerList.size());
    }
  }

  @Override
  public void doExecute() 
  {
    if (oldIndex == -1)
    {
      oldIndex = ownerList.indexOf(value);
    }
    ownerList.move(index, oldIndex);
  }

  @Override
  public void doUndo() 
  {
    ownerList.move(oldIndex, index);
  }

  @Override
  public void doRedo()
  {
    ownerList.move(index, oldIndex);
  }

  @Override
  public Collection<?> doGetResult()
  {
    return Collections.singleton(value);
  }

  @Override
  public Collection<?> doGetAffectedObjects()
  {
    return Collections.singleton(value);
  }

  /**
   * 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()
  {
    StringBuilder result = new StringBuilder(super.toString());
    result.append(" (owner: " + owner + ")");
    result.append(" (feature: " + feature + ")");
    result.append(" (ownerList: " + ownerList + ")");
    result.append(" (value: " + value + ")");
    result.append(" (index: " + index + ")");
    result.append(" (oldIndex: " + oldIndex + ")");

    return result.toString();
  }
}
