/**
 * Copyright (c) 2007-2011 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.ecore.change.util;

import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.emf.common.util.BasicEList;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.EcorePackage;
import org.eclipse.emf.ecore.change.ChangeDescription;
import org.eclipse.emf.ecore.change.ChangeFactory;
import org.eclipse.emf.ecore.change.FeatureChange;
import org.eclipse.emf.ecore.change.ListChange;
import org.eclipse.emf.ecore.change.ResourceChange;
import org.eclipse.emf.ecore.resource.Resource;


/**
 * This class provides the basic methods required to implement a change recorder.
 * @since 2.3
 */
public class BasicChangeRecorder extends ListDifferenceAnalyzer
{
  protected boolean recording;
  protected boolean recordingTransientFeatures = true;

  protected ChangeDescription changeDescription;

  /**
   * @return true if this change recorder is recording or false otherwise.
   */
  public boolean isRecording()
  {
    return recording;
  }

  protected void setRecording(boolean recording)
  {
    this.recording = recording;
  }

  /**
   * @return true if this change recorder is recording transient features or false otherwise.
   * @since 2.7
   */
  public boolean isRecordingTransientFeatures()
  {
    return recordingTransientFeatures;
  }

  /**
   * @since 2.7
   */
  public void setRecordingTransientFeatures(boolean recordingTransientFeatures)
  {
    this.recordingTransientFeatures = recordingTransientFeatures;
  }

  protected ChangeDescription getChangeDescription()
  {
    return changeDescription;
  }

  protected void setChangeDescription(ChangeDescription changeDescription)
  {
    this.changeDescription = changeDescription;
  }

  /**
   * Disposes this change recorder.  This method ends a recording without
   * consolidating the changes.
   */
  public void dispose()
  {
    changeDescription = null;
  }

  /**
   * <p>Summarizes the changes made to the analyzed objects on the {@link ChangeDescription change description}
   * returned by the {@link #endRecording()} without ending the recording.</p>
   *
   * <p>This method doesn't do anything if this ChangeRecorder is not recording.</p>
   *
   * @return the {@link ChangeDescription} or <tt class="code">null</tt> if there is nothing being recorded.
   */
  public ChangeDescription summarize()
  {
    if (isRecording())
    {
      consolidateChanges();
      return getChangeDescription();
    }
    return null;
  }

  /**
   * Ends the recording and consolidates the changes on the {@link ChangeDescription change description}.
   * @return the {@link ChangeDescription} or <tt class="code">null</tt> if there is nothing being recorded.
   */
  public ChangeDescription endRecording()
  {
    if (isRecording())
    {
      setRecording(false);
      consolidateChanges();
      return getChangeDescription();
    }
    return null;
  }

  /**
   * Consolidates the changes that have happen since the last consolidation.
   */
  protected void consolidateChanges()
  {
    ChangeDescription changeDescription = getChangeDescription();
    for (Map.Entry<EObject, EList<FeatureChange>> entry : changeDescription.getObjectChanges())
    {
      EObject eObject = entry.getKey();
      for (FeatureChange featureChange : entry.getValue())
      {
        finalizeChange(featureChange, eObject);
      }
    }

    for (ResourceChange resourceChange : changeDescription.getResourceChanges())
    {
      finalizeChange(resourceChange);
    }

    eliminateEmptyChanges();
  }

  /**
   * Eliminates changes that result in a state that's equal to the current state.
   */
  protected void eliminateEmptyChanges()
  {
    ChangeDescription changeDescription = getChangeDescription();
    for (Iterator<Map.Entry<EObject, EList<FeatureChange>>> i = changeDescription.getObjectChanges().iterator(); i.hasNext();)
    {
      Map.Entry<EObject, EList<FeatureChange>> entry = i.next();
      EObject eObject = entry.getKey();
      EList<FeatureChange> featureChanges = entry.getValue();
      for (Iterator<FeatureChange> j = featureChanges.iterator(); j.hasNext(); )
      {
        FeatureChange featureChange  = j.next();
        EStructuralFeature feature = featureChange.getFeature();
        if (featureChange.isSet() == eObject.eIsSet(feature))
        {
          Object value = featureChange.getValue();
          Object eObjectValue = eObject.eGet(feature);
          if (value == null ? eObject.eGet(feature) == null : value.equals(eObjectValue))
          {
            j.remove();
          }
        }
      }
      if (featureChanges.isEmpty())
      {
        i.remove();
      }
    }

    for (Iterator<ResourceChange> i = changeDescription.getResourceChanges().iterator(); i.hasNext(); )
    {
      ResourceChange resourceChange  = i.next();
      if (resourceChange.getResource().getContents().equals(resourceChange.getValue()))
      {
        i.remove();
      }
    }
  }

  protected boolean shouldRecord(EStructuralFeature feature, EObject eObject)
  {
    return isRecording() &&
      !feature.isDerived() &&
      (isRecordingTransientFeatures() || !feature.isTransient()) &&
      feature != EcorePackage.Literals.ECLASS__ESUPER_TYPES &&
      feature != EcorePackage.Literals.ETYPED_ELEMENT__ETYPE &&
      feature != EcorePackage.Literals.EOPERATION__EEXCEPTIONS &&
      feature != EcorePackage.Literals.ECLASSIFIER__INSTANCE_CLASS_NAME;
  }

  protected void finalizeChange(ResourceChange change)
  {
    EList<Object> oldList = new BasicEList.FastCompare<Object>(change.getResource().getContents());
    EList<?> newList = change.getValue();
    change.getListChanges().clear();
    createListChanges(oldList, newList, change.getListChanges());
  }

  protected void finalizeChange(FeatureChange change, EObject eObject)
  {
    if (change.isSet())
    {
      EStructuralFeature feature = change.getFeature();
      if (feature.isMany())
      {
        EList<Object> oldList = new BasicEList<Object>((EList<?>)eObject.eGet(feature));
        EList<?> newList = (EList<?>)change.getValue();
        EList<ListChange> listChanges = change.getListChanges();
        listChanges.clear();
        createListChanges(oldList, newList, listChanges);
      }
    }
  }

  protected EList<ResourceChange> getResourceChanges()
  {
    return getChangeDescription().getResourceChanges();
  }

  protected ResourceChange getResourceChange(Resource resource)
  {
    List<ResourceChange> resourceChanges = getResourceChanges();
    for (int i = 0, size = resourceChanges.size(); i < size;)
    {
      ResourceChange resourceChange = resourceChanges.get(i++);
      if (resourceChange.getResource() == resource)
      {
        return resourceChange;
      }
    }
    return null;
  }

  protected List<FeatureChange> getFeatureChanges(EObject eObject)
  {
    ChangeDescription changeDescription = getChangeDescription();
    List<FeatureChange> featureChanges = changeDescription.getObjectChanges().get(eObject);
    if (featureChanges == null)
    {
      Map.Entry<EObject, EList<FeatureChange>> entry = ChangeFactory.eINSTANCE.createEObjectToChangesMapEntry(eObject);
      changeDescription.getObjectChanges().add(entry);
      featureChanges = entry.getValue();
    }
    return featureChanges;
  }

  protected FeatureChange getFeatureChange(List<FeatureChange> featureChanges, EStructuralFeature eStructuralFeature)
  {
    for (int i = 0, size = featureChanges.size(); i < size;)
    {
      FeatureChange featureChange = featureChanges.get(i++);
      if (featureChange.getFeature() == eStructuralFeature)
      {
        return featureChange;
      }
    }
    return null;
  }

  protected FeatureChange createFeatureChange(EObject eObject, EStructuralFeature eStructuralFeature, Object value, boolean isSet)
  {
    return ChangeFactory.eINSTANCE.createFeatureChange(eStructuralFeature, value, isSet);
  }

  protected ResourceChange createResourceChange(Resource resource, EList<Object> value)
  {
    return ChangeFactory.eINSTANCE.createResourceChange(resource, value);
  }

  protected ChangeDescription createChangeDescription()
  {
    return ChangeFactory.eINSTANCE.createChangeDescription();
  }
}
