/**
 * 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.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import org.eclipse.emf.common.command.Command;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.util.ExtendedMetaData;
import org.eclipse.emf.ecore.util.FeatureMap;
import org.eclipse.emf.ecore.util.FeatureMapUtil;
import org.eclipse.emf.ecore.xml.type.XMLTypePackage;
import org.eclipse.emf.edit.EMFEditPlugin;
import org.eclipse.emf.edit.domain.EditingDomain;


/**
 * The add command logically acts upon an owner object that has a collection-based feature to which other objects can be added.
 * 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 added to a many-valued feature of an owner,
 * i.e., it is equivalent of the call
 * <pre>
 *   ((EList)((EObject)owner).eGet((EStructuralFeature)feature)).addAll(index, (Collection)collection);
 * </pre>
 *
 * <p>
 * It can also be used as an equivalent to the call
 * <pre>
 *   ((EList)extent).addAll(index, (Collection)collection);
 * </pre>
 * which is how root objects are added into the contents of a resource.
 * Like all the low-level commands in this package, the add command is undoable.
 *
 * <p>
 * An add command is an {@link OverrideableCommand}.
 */
public class AddCommand extends AbstractOverrideableCommand
{
  /**
   * This creates a command to add a particular value to 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)
  {
    return create(domain, owner, feature, Collections.singleton(value), CommandParameter.NO_INDEX);
  }

  /**
   * This creates a command to insert particular value at 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 create(domain, owner, feature, Collections.singleton(value), index);
  }

  /**
   * This creates a command to add a collection of values to 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, Collection<?> collection)
  {
    return domain.createCommand(AddCommand.class, new CommandParameter(owner, feature, collection, CommandParameter.NO_INDEX));
  }

  /**
   * This creates a command to insert a collection of values at 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, Collection<?> collection, int index)
  {
    return domain.createCommand(AddCommand.class, new CommandParameter(owner, feature, collection, index));
  }

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

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

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

  /**
   * This is the collection of objects being added to the owner list.
   */
  protected Collection<?> collection;

  /**
   * This is the position at which the objects will be inserted.
   */
  protected int index;

  /**
   * This 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 add a particular value to the specified many-valued feature of the owner.
   */
  public AddCommand(EditingDomain domain, EObject owner, EStructuralFeature feature, Object value)
  {
    this(domain, owner, feature, Collections.singleton(value), CommandParameter.NO_INDEX);
  }

  /**
   * This constructs a primitive command to insert particular value into the specified many-valued feature of the owner.
   */
  public AddCommand(EditingDomain domain, EObject owner, EStructuralFeature feature, Object value, int index)
  {
    this(domain, owner, feature, Collections.singleton(value), index);
  }

  /**
   * This constructs a primitive command to add a collection of values to the specified many-valued feature of the owner.
   */
  public AddCommand(EditingDomain domain, EObject owner, EStructuralFeature feature, Collection<?> collection)
  {
    this(domain, owner, feature, collection, CommandParameter.NO_INDEX);
  }

  /**
   * This constructs a primitive command to insert a collection of values into the specified many-valued feature of the owner.
   */
  public AddCommand(EditingDomain domain, EObject owner, EStructuralFeature feature, Collection<?> collection, int index)
  {
    super(domain, LABEL, DESCRIPTION);

    this.owner = owner;
    this.feature = feature;
    this.collection = collection;
    this.index = index;

    ownerList = getOwnerList(owner, feature);
  }

  /**
   * This constructs a primitive command to add a particular value into the specified extent.
   */
  public AddCommand(EditingDomain domain, EList<?> list, Object value)
  {
    this(domain, list, Collections.singleton(value), CommandParameter.NO_INDEX);
  }

  /**
   * This constructs a primitive command to insert particular value into the specified extent.
   */
  public AddCommand(EditingDomain domain, EList<?> list, Object value, int index)
  {
    this(domain, list, Collections.singleton(value), index);
  }

  /**
   * This constructs a primitive command to insert a collection of values into the specified extent.
   */
  public AddCommand(EditingDomain domain, EList<?> list, Collection<?> collection)
  {
    this(domain, list, collection, CommandParameter.NO_INDEX);
  }

  /**
   * This constructs a primitive command to insert a collection of values into the specified extent.
   */
  public AddCommand(EditingDomain domain, EList<?> list, Collection<?> collection, int index)
  {
    super(domain, LABEL, DESCRIPTION_FOR_LIST);

    this.collection = collection;
    this.index = index;

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

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

  /**
   * This returns the position at which the objects will be added.
   */
  public int getIndex()
  {
    return index;
  }

  protected boolean isUserElement(EStructuralFeature entryFeature)
  {
    return
      entryFeature != XMLTypePackage.Literals.XML_TYPE_DOCUMENT_ROOT__TEXT &&
      entryFeature != XMLTypePackage.Literals.XML_TYPE_DOCUMENT_ROOT__CDATA &&
      entryFeature != XMLTypePackage.Literals.XML_TYPE_DOCUMENT_ROOT__COMMENT &&
      entryFeature != XMLTypePackage.Literals.XML_TYPE_DOCUMENT_ROOT__PROCESSING_INSTRUCTION;
  }

  @Override
  protected boolean prepare()
  {
    // If there is no list to add to, no collection or an empty collection from which to add, or the index is out of range...
    //
    if (ownerList == null || 
          collection == null ||
          collection.size() == 0 ||
          index != CommandParameter.NO_INDEX && (index < 0 || index > ownerList.size()))
    {
      return false;
    }

    if (feature != null) 
    {
      // If it's a feature map, we'll need to validate the entry feature and enforce its multiplicity restraints.
      //
      FeatureMapUtil.Validator validator = null;
      boolean documentRoot = false;
      Set<EStructuralFeature> entryFeatures = Collections.emptySet();

      if (FeatureMapUtil.isFeatureMap(feature))
      {
        EClass eClass = owner.eClass();
        validator = FeatureMapUtil.getValidator(eClass, feature); 

        // Keep track of all the entry features that are already in the feature map and that will be added, excluding
        // XML text, CDATA, and comments (if we're in a mixed type).
        //
        documentRoot = ExtendedMetaData.INSTANCE.getDocumentRoot(eClass.getEPackage()) == eClass;
        boolean mixed = documentRoot || ExtendedMetaData.INSTANCE.getContentKind(eClass) == ExtendedMetaData.MIXED_CONTENT;
        entryFeatures = new HashSet<EStructuralFeature>();
        for (Object entry : ownerList)
        {
          EStructuralFeature entryFeature = ((FeatureMap.Entry)entry).getEStructuralFeature();
          if (!mixed || isUserElement(entryFeature))
          {
            entryFeatures.add(entryFeature);
          }
        }
      }

      // Check each object... 
      //
      for (Object object : collection)
      {
        boolean containment = false;

        // Check type of object.
        //
        if (!feature.getEType().isInstance(object))
        {
          return false;
        }

        // Check that the object isn't already in a unique list.
        //
        if (feature.isUnique() && ownerList.contains(object))
        {
          return false;
        }

        // For feature maps, test that the entry feature is a valid type, that the entry value is an instance of it,
        // that there is not already something in a document root, and that there is not already something in a
        // single-valued entry feature.
        //
        if (validator != null)
        {
          FeatureMap.Entry entry = (FeatureMap.Entry)object;
          EStructuralFeature entryFeature = entry.getEStructuralFeature();
          containment = entryFeature instanceof EReference && ((EReference)entryFeature).isContainment();
          
          if (!validator.isValid(entryFeature) || !entryFeature.getEType().isInstance(entry.getValue()))
          {
            return false;
          }

          if (documentRoot)
          {
            if (isUserElement(entryFeature))
            {
              if (!entryFeatures.isEmpty())
              {
                return false;
              }
              entryFeatures.add(entryFeature);
            }
          }
          else if (!entryFeatures.add(entryFeature) && !FeatureMapUtil.isMany(owner, entryFeature)) 
          {
            return false;
          }
        }

        // Check to see if a container is being put into a contained object.
        //
        containment |= feature instanceof EReference && ((EReference)feature).isContainment();
        if (containment)
        {
          for (EObject container = owner; container != null; container = container.eContainer()) 
          {
            if (object == container) 
            {
              return false;
            }
          }
        }
      }
    }

    if (owner != null && domain.isReadOnly(owner.eResource()))
    {
      return false;
    }

    return true;
  }

  @Override
  public void doExecute() 
  {
    // Simply add the collection to the list.
    //
    if (index == CommandParameter.NO_INDEX)
    {
      ownerList.addAll(collection);
    }
    else
    {
      ownerList.addAll(index, collection);
    }

    // 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 doUndo() 
  {
    // Remove the collection from the list by index.
    //
    int i = index != CommandParameter.NO_INDEX ? index : ownerList.size() - collection.size();
    ownerList.subList(i, i + collection.size()).clear();    
  
    // Update the containing map, if necessary.
    //
    updateEMap(owner, feature);

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

  @Override
  public void doRedo()
  {
    // Simply add the collection to the list.
    //
    if (index == CommandParameter.NO_INDEX)
    {
      ownerList.addAll(collection);
    }
    else
    {
      ownerList.addAll(index, collection);
    }

    // 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 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()
  {
    StringBuffer result = new StringBuffer(super.toString());
    result.append(" (owner: " + owner + ")");
    result.append(" (feature: " + feature + ")");
    result.append(" (ownerList: " + ownerList + ")");
    result.append(" (collection: " + collection + ")");
    result.append(" (index: " + index + ")");
    result.append(" (affectedObjects:" + affectedObjects + ")");

    return result.toString();
  }
}
