/**
 * 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.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;

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 remove command logically acts upon an owner object that has a collection-type feature from which objects can be removed.
 * The static create methods delegate 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 low-level and EMF specific;
 * it allows one or more objects to be removed from a many-valued feature of an owner.
 * i.e., it is almost equivalent of the call
 * <pre>
 *   ((EList)((EObject)owner).eGet((EStructuralFeature)feature)).removeAll((Collection)collection);
 * </pre>
 *
 * <p>
 * It can also be used as a near-equivalent to the call
 * <pre>
 *   ((EList)list).removeAll((Collection)collection);
 * </pre>
 * which is how root objects are removed from the contents of a resource.
 *
 * <p>
 * The one difference is that, while <code>EList.removeAll(Collection)</code> removes all values equal to a value in
 * the collection, this command will remove no more than one value per value in the collection. When duplicates are
 * allowed and present in the list, this command will first look for identical (<code>==</code>) values, in order, and
 * failing that, equal values (<code>.equals()</code>).
 *
 * <p>
 * Like all the low-level commands in this package, the remove command is undoable. 
 *
 * <p>
 * A remove command is an {@link OverrideableCommand}.
 */
public class RemoveCommand extends AbstractOverrideableCommand
{
  /**
   * This creates a command to remove an object.
   */
  public static Command create(EditingDomain domain, Object value) 
  {
    return create(domain, null, null, Collections.singleton(value));
  }

  /**
   * This creates a command to remove a particular value from the specified feature of the owner.
   */
  public static Command create(EditingDomain domain, Object owner, Object feature, Object value) 
  {
    return create(domain, owner, feature, Collections.singleton(value));
  }

  /**
   * This creates a command to remove multiple objects.
   */
  public static Command create(final EditingDomain domain, final Collection<?> collection) 
  {
    return create(domain, null, null, collection);
  }

  /**
   * This creates a command to remove the value at the specified index from the specified feature of the owner.
   * @since 2.15
   */
  public static Command create(EditingDomain domain, Object owner, Object feature, int index)
  {
    return create(domain, owner, feature, Collections.singleton(CommandParameter.Indices.create(index)));
  }
  
  /**
   * This creates a command to remove the values at the specified indices from the specified feature of the owner.
   * @since 2.15
   */
  public static Command create(EditingDomain domain, Object owner, Object feature, int... indices)
  {
    return create(domain, owner, feature, Collections.singleton(CommandParameter.Indices.create(indices)));
  }

  /**
   * This creates a command to remove a collection of values from the specified feature of the owner.
   */
  public static Command create(final EditingDomain domain, final Object owner, final Object feature, final Collection<?> collection)
  {
    return domain.createCommand(RemoveCommand.class, new CommandParameter(owner, feature, collection));
  }


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

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

  /**
   * This caches the description for a list-based command.
   */
  protected static final String DESCRIPTION_FOR_LIST = EMFEditPlugin.INSTANCE.getString("_UI_RemoveCommand_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 from which the command will remove.
   */
  protected EList<Object> ownerList;

  /**
   * This is the collection of objects being removed.
   */
  protected Collection<Object> collection;

  /**
   * These are the indices at which to reinsert the removed objects during an undo so as to achieve the original list order.
   */ 
  protected int[] indices;

  /**
   * The is the value returned by {@link Command#getAffectedObjects}.
   * The affected objects are different after an execute than after an undo, so we record it.
   */
  protected Collection<?> affectedObjects;

  /**
   * This constructs a primitive command to remove a particular value from the specified feature of the owner.
   */
  public RemoveCommand(EditingDomain domain, EObject owner, EStructuralFeature feature, Object value) 
  {
    this(domain, owner, feature, Collections.singleton(value));
  }

  /**
   * This constructs a primitive command to remove the value at the specified index from the specified feature of the owner.
   * @since 2.15
   */
  public RemoveCommand(EditingDomain domain, EObject owner, EStructuralFeature feature, int index) 
  {
    this(domain, owner, feature, Collections.singleton(CommandParameter.Indices.create(index)));
  }
  
  /**
   * This constructs a primitive command to remove the values at the specified indices from the specified feature of the owner.
   * @since 2.15
   */
  public RemoveCommand(EditingDomain domain, EObject owner, EStructuralFeature feature, int... indices) 
  {
    this(domain, owner, feature, Collections.singleton(CommandParameter.Indices.create(indices)));
  }

  /**
   * This constructs a primitive command to remove a collection of values from the specified feature of the owner.
   */
  public RemoveCommand(EditingDomain domain, EObject owner, EStructuralFeature feature, Collection<?> collection)
  {
    super(domain, LABEL, DESCRIPTION);

    // Initialize all the fields from the command parameter.
    //
    this.owner = owner;
    this.feature = feature;
    this.collection = collection == null ? null : new ArrayList<Object>(collection);

    ownerList = getOwnerList(this.owner, feature);
  }

  /**
   * This constructs a primitive command to remove a particular value from the specified list.
   */
  public RemoveCommand(EditingDomain domain, EList<?> list, Object value) 
  {
    this(domain, list, Collections.singleton(value));
  }

  /**
   * This constructs a primitive command to remove the value at the specified index from the specified list.
   * @since 2.15
   */
  public RemoveCommand(EditingDomain domain, EList<?> list, int index) 
  {
    this(domain, list, Collections.singleton(CommandParameter.Indices.create(index)));
  }
  
  /**
   * This constructs a primitive command to remove the values at the specified indices from the specified list.
   * @since 2.15
   */
  public RemoveCommand(EditingDomain domain, EList<?> list, int... indices) 
  {
    this(domain, list, Collections.singleton(CommandParameter.Indices.create(indices)));
  }

  /**
   * This constructs a primitive command to remove a collection of values from the specified list.
   */
  public RemoveCommand(EditingDomain domain, EList<?> list, Collection<?> collection)
  {
    super(domain, LABEL, DESCRIPTION_FOR_LIST);

    // Initialize all the fields from the command parameter.
    //
    this.collection = collection == null ? null : new ArrayList<Object>(collection);

    @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 from which the command will remove.
   */
  public EList<Object> getOwnerList()
  {
    return ownerList;
  }

  /**
   * This returns the collection of objects being removed.
   */
  public Collection<?> getCollection()
  {
    return collection;
  }

  /**
   * These returns the indices at which to reinsert the removed objects during an undo so as to achieve the original list order.
   */ 
  public int[] getIndices()
  {
    return indices;
  }

  @Override
  protected boolean prepare() 
  {
    // This can execute if there is an owner list and a collection and the owner list contains all the objects of the collection.
    //
    boolean result =
      ownerList != null && 
        collection != null && 
        prepareCollection() && 
        (owner == null || !domain.isReadOnly(owner.eResource()));

    return result;
  }

  /**
   * Called by {@link #prepare()} if the {@link #ownerList} and the {@link #collection} are both not {@code null}.
   *
   * @return {@code} true if the collection is valid,
   * i.e., either all its values are contained by the owner list,
   * or it contains a single {@link CommandParameter.Indices} instance all of whose {@link CommandParameter.Indices#getClass() indices} are within the bounds of the list.
   *
   * @since 2.15
   */
  protected boolean prepareCollection()
  {
    if (ownerList.containsAll(collection))
    {
      return true;
    }
    else if (collection.size() != 1)
    {
      return false;
    }
    else
    {
      Object value = collection.iterator().next();
      if (value instanceof CommandParameter.Indices)
      {
        int[] indices = ((CommandParameter.Indices)value).getIndices();
        Arrays.sort(indices);
        Collection<Object> collection = new ArrayList<Object>();
        int size = ownerList.size();
        for (int i : indices)
        {
          if (i < 0 || i >= size)
          {
            return false;
          }
          collection.add(ownerList.get(i));
        }
        this.indices = indices;
        this.collection = collection;
        return true;
      }
      else
      {
        return false;
      }
    }
  }

  @Override
  public void doExecute() 
  {
    if (indices == null)
    {
      // Iterate over the owner list twice, first matching objects from the collection by identity (==), then matching
      // objects by value equality (.equals()). The positions of matched objects in the owner list are recorded, and
      // the objects are stored in the same order. The lists are then merged to form a final, in-order list of objects
      // and corresponding indices in ownerList. This is very important for undo to interpret the indices correctly.
      // Also, this yields exactly one object removed for each object in the collection, with preference given to
      // identity over value equality.
      //
      List<Object> identity = new ArrayList<Object>(collection.size());
      int[] identityIndices = new int[collection.size()];
  
      int i = 0;
  
      for (ListIterator<Object> ownedObjects = ownerList.listIterator(); ownedObjects.hasNext(); )
      {
        Object ownedObject = ownedObjects.next();
  
        // If this owned object is one from the collection...
        //
        if (containsExact(collection, ownedObject))
        {
          // Remove the object from the collection and add it to the identity list.
          //
          removeExact(collection, ownedObject);
          identity.add(ownedObject);
  
          // Record the index.
          //
          identityIndices[i++] = ownedObjects.previousIndex();
        }
      }
  
      while (i < identityIndices.length)
      {
        identityIndices[i++] = -1;
      }
  
      // Second pass: match by value equality.
      //
      List<Object> equality = new ArrayList<Object>(collection.size());
      int[] equalityIndices = new int[collection.size()];
      i = 0;
  
      for (ListIterator<Object> ownedObjects = ownerList.listIterator(); ownedObjects.hasNext(); )
      {
        Object ownedObject = ownedObjects.next();
        int index = ownedObjects.previousIndex();
  
        // If this owned object is equal to one from the collection...
        //
        if (collection.contains(ownedObject) && !contains(identityIndices, index))
        {
          // Remove the object from the collection and add it to the equality list. 
          //
          collection.remove(ownedObject);
          equality.add(ownedObject);
  
          // Record the index.
          //
          equalityIndices[i++] = index;
        }
      }
  
      // Merge the lists.
      //
      merge(identity, identityIndices, equality, equalityIndices);
    }

    // Remove objects from the owner list by index, starting from the end.
    //
    for (int i = indices.length - 1; i >= 0; i--)
    {
      ownerList.remove(indices[i]);
    }

    // Update the containing map, if necessary.
    //
    updateEMap(owner, feature);

    // We'd like the owner selected after this remove completes.
    //
    affectedObjects = owner == null ? Collections.EMPTY_SET : Collections.singleton(owner);
  }

  /**
   * Returns whether the given collection contains the given target object itself (according to ==, not .equals()).
   */
  protected boolean containsExact(Collection<?> collection, Object target)
  {
    for (Object object : collection)
    {
      if (object == target) return true;
    }
    return false;
  }

  /**
   * Returns whether the given int array contains the given target value.
   */
  protected boolean contains(int[] values, int target)
  {
    for (int i = 0, len = values.length; i < len; i++)
    {
      if (values[i] == target) return true;
    }
    return false;
  }

  /**
   * Removes the first occurrence of the given target object, itself, from the collection.
   */
  protected boolean removeExact(Collection<?> collection, Object target)
  {
    for (Iterator<?> i = collection.iterator(); i.hasNext(); )
    {
      if (i.next() == target)
      {
        i.remove();
        return true;
      }
    }
    return false;
  }

  /**
   * Merges two sets of object lists and index arrays, such that both are ordered by increasing indices. The results
   * are stored as the {@link #collection} and {@link #indices}. The two input sets must already be in increasing index
   * order, with the corresponding object-index pairs in the same positions.
   */
  protected void merge(List<Object> objects1, int[] indices1, List<Object> objects2, int[] indices2)
  {
    // If either list is empty, the result is simply the other.
    //
    if (objects2.isEmpty())
    {
      collection = objects1;
      indices = indices1;
      return;
    }

    if (objects1.isEmpty())
    {
      collection = objects2;
      indices = indices2;
      return;
    }

    // Allocate list and array for objects and indices.
    //
    int size = objects1.size() + objects2.size();
    collection = new ArrayList<Object>(size);
    indices = new int[size];

    // Index counters into indices1, indices2, and indices. 
    //
    int i1 = 0;
    int i2 = 0;
    int i = 0;

    // Object iterators.
    //
    Iterator<Object> iter1 = objects1.iterator();
    Iterator<Object> iter2 = objects2.iterator();

    Object o1 = iter1.hasNext() ? iter1.next() : null;
    Object o2 = iter2.hasNext() ? iter2.next() : null;

    // Repeatedly select the lower index and corresponding object, and advance past the selected pair.
    //
    while (o1 != null && o2 != null)
    {
      if (indices1[i1] < indices2[i2])
      {
        indices[i++] = indices1[i1++];
        collection.add(o1);
        o1 = iter1.hasNext() ? iter1.next() : null;
      }
      else
      {
        indices[i++] = indices2[i2++];
        collection.add(o2);
        o2 = iter2.hasNext() ? iter2.next() : null;
      }
    }

    // Add any remaining object-index pairs from either set.
    //
    while (o1 != null)
    {
      indices[i++] = indices1[i1++];
      collection.add(o1);
      o1 = iter1.hasNext() ? iter1.next() : null;
    }

    while (o2 != null)
    {
      indices[i++] = indices2[i2++];
      collection.add(o2);
      o2 = iter2.hasNext() ? iter2.next() : null;
    }
  }

  @Override
  public void doUndo() 
  {
    // Note that the way they are sorted, the values of index[i++] always increase,
    // so the objects are added from right to left in the list.
    //
    // EATM  TODO
    //
    // We could make this more efficient by grouping the adds when indices increment by one,
    // so that a single grouped notification would result.
    //
    int i = 0;
    for (Object object : collection)
    {
      ownerList.add(indices[i++], object);
    }
  
    // Update the containing map, if necessary.
    //
    updateEMap(owner, feature);

    // We'd like the collection of things added to be selected after this command completes.
    //
    affectedObjects = collection;
  }
  
  @Override
  public void doRedo()
  {
    // Remove objects from the owner list by index, starting from the end.
    //
    for (int i = indices.length - 1; i >= 0; i--)
    {
      ownerList.remove(indices[i]);
    }

    // Update the containing map, if necessary.
    //
    updateEMap(owner, feature);

    // We'd like the owner selected after this remove completes.
    //
    affectedObjects = owner == null ? Collections.EMPTY_SET : Collections.singleton(owner);
  }

  @Override
  public Collection<?> doGetResult()
  {
    return collection;
  }

  @Override
  public Collection<?> doGetAffectedObjects()
  {
    return affectedObjects;
  }

  /**
   * 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(" (collection: " + collection + ")");
    result.append(" (indices: " + Arrays.toString(indices) + ")");
    result.append(" (affectedObjects: " + affectedObjects + ")");

    return result.toString();
  }
}
