/**
 * 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.EGenericType;
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 replace command logically acts upon an owner object that has a collection-type feature 
 * in which an object can be replaced by a collection of other objects.
 * 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 an object from a many-valued feature of an owner to be replaced by a collection of other objects.
 * i.e., it is equivalent of the call
 * <pre>
 *   int index = ((EList)((EObject)owner).eGet((EStructuralFeature)feature)).indexOf(value);
 *   ((EList)((EObject)owner).eGet((EStructuralFeature)feature)).remove(value);
 *   ((EList)((EObject)owner).eGet((EStructuralFeature)feature)).addAll(index, (Collection)collection);
 * </pre>
 *
 * <p>
 * It can also be used as an equivalent to the call
 * <pre>
 *   int index = ((EList)extent).indexOf(value);
 *   ((EList)extent).remove(value);
 *   ((EList)extent).addAll(index, (Collection)collection);
 * </pre>
 * which is how root objects are replaced in the contents of a resource.
 * Like all the low level commands in this package, the replace command is undoable.
 *
 * <p>
 * A replace command is an {@link OverrideableCommand}.
 */
public class ReplaceCommand extends AbstractOverrideableCommand
{
  /**
   * This creates a command to replace an object with a collection of replacements.
   */
  public static Command create(EditingDomain domain, Object value, Collection<?> collection) 
  {
    return create(domain, null, null, value, collection);
  }

  /**
   * This creates a command to replace a particular value in the specified feature of the owner with a collection replacements objects.
   */
  public static Command create(EditingDomain domain, Object owner, Object feature, Object value, Collection<?> collection) 
  {
    return domain.createCommand(ReplaceCommand.class, new CommandParameter(owner, feature, value, collection));
  }

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

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

  /**
   * 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 replace.
   */
  protected EList<Object> ownerList;

  /**
   * This is value that is being replaced.
   */
  protected Object value;

  /**
   * This is the collection of replacements.
   */
  protected Collection<?> collection;

  /**
   * This is the index at which to reinsert the replaced object during an undo so as to achieve the original list order.
   */ 
  protected int index;

  /**
   * 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 replace a particular value in the specified feature of the owner 
   * with the specified replacement.
   */
  public ReplaceCommand(EditingDomain domain, EObject owner, EStructuralFeature feature, Object value, Object replacement) 
  {
    this(domain, owner, feature, value, Collections.singleton(replacement));
  }

  /**
   * This constructs a primitive command to replace a particular value in the specified feature of the owner
   * with the specified collection of replacements.
   */
  public ReplaceCommand(EditingDomain domain, EObject owner, EStructuralFeature feature, Object value, Collection<?> collection)
  {
    super(domain, LABEL, DESCRIPTION);

    // Initialize all the fields from the command parameter.
    //
    this.owner = owner;
    this.feature = feature;
    this.value = value;
    this.collection = collection;

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

  /**
   * This constructs a primitive command to replace a particular value in the specified extent with the given replacement.
   */
  public ReplaceCommand(EditingDomain domain, EList<?> list, Object value, Object replacement) 
  {
    this(domain, list, value, Collections.singleton(replacement));
  }

  /**
   * This constructs a primitive command to replace a particular value in the specified extent with the given collection of replacements.
   */
  public ReplaceCommand(EditingDomain domain, EList<?> list, Object value, Collection<?> collection)
  {
    super(domain, LABEL, DESCRIPTION);

    // Initialize all the fields from the command parameter.
    //
    this.value = value;
    this.collection = 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 in which the command will replace.
   */
  public EList<Object> getOwnerList()
  {
    return ownerList;
  }

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

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

  /**
   * This returns the index at which to reinsert the replace objects during an undo so as to achieve the original list order.
   */ 
  public int getIndex()
  {
    return index;
  }

  @Override
  protected boolean prepare() 
  {
    // This can't execute if there is no owner list 
    // or the owner list doesn't contain the value being replaced or 
    // there are not replacements.
    //
    if (ownerList == null || !ownerList.contains(value) || collection == null || collection.isEmpty())
    {
      return false;
    }
    else if (owner != null && domain.isReadOnly(owner.eResource()))
    {
      return false;
    }
    else if (feature == null)
    {
      // An extent allows anything to be added.
      //
      return true;
    }
    else
    {
      // Make sure each object conforms to the type of the feature.
      //
      EGenericType eType = owner == null ? feature.getEGenericType() : owner.eClass().getFeatureType(feature);
      for (Object replacement : collection)
      {
        if (!eType.isInstance(replacement))
        {
          return false;
        }
      }

      return true;
    }
  }

  @Override
  public void doExecute() 
  {
    // Record the position of the value in the owner list.
    //
    index = ownerList.indexOf(value);
  
    // Simply remove the object from the owner list.
    //
    ownerList.remove(value);

    // Insert the collection at the right place.
    //
    ownerList.addAll(index, collection);
  
    // Update the containing map, if necessary.
    //
    updateEMap(owner, feature);

    // We'd like the collection of replacements selected after this replace completes.
    //
    affectedObjects = collection;
  }

  @Override
  public void doUndo() 
  {
    // Remove the collection of replacements.
    //
    ownerList.removeAll(collection);

    // Add the value back in the right place.
    //
    ownerList.add(index, value);

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

    // We'd like the replaced selected after this undo replace completes.
    //
    affectedObjects = Collections.singleton(value);
  }
  
  @Override
  public void doRedo()
  {
    // Simply remove the object from the owner list.
    //
    ownerList.remove(value);

    // Insert the collection at the right place.
    //
    ownerList.addAll(index, collection);
  
    // Update the containing map, if necessary.
    //
    updateEMap(owner, feature);

    // We'd like the collection of replacements selected after this replace completes.
    //
    affectedObjects = collection;
  }

  @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(" (value: " + value + ")");
    result.append(" (collection: " + collection + ")");
    result.append(" (index: " + index + ")");
    result.append(" (affectedObjects:" + affectedObjects + ")");

    return result.toString();
  }
}
