/**
 * <copyright>
 * Copyright (c) 2010-2014 Henshin developers. All rights reserved. 
 * This program and the accompanying materials are made available 
 * under the terms of the Eclipse Public License v1.0 which 
 * accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * </copyright>
 */
package org.eclipse.emf.henshin.interpreter.impl;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.henshin.interpreter.Change;
import org.eclipse.emf.henshin.interpreter.EGraph;
import org.eclipse.emf.henshin.interpreter.util.InterpreterUtil;

/**
 * Default implementation of {@link Change} and its sub-interfaces.
 * @author Christian Krause
 */
public abstract class ChangeImpl implements Change {

	/**
	 * Flag indicating whether warnings should be printed.
	 */
	public static boolean PRINT_WARNINGS = true;
	
	/** 
	 * {@link EGraph} to be changed.
	 */
	protected final EGraph graph;

	/**
	 * Default constructor.
	 * @param graph {@link EGraph} to be changed.
	 */
	public ChangeImpl(EGraph graph) {
		this.graph = graph;
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.eclipse.emf.henshin.interpreter.Change#getEGraph()
	 */
	@Override
	public EGraph getEGraph() {
		return graph;
	}
	
	/**
	 * Default implementation of {@link ObjectChange}.
	 * @author Christian Krause
	 */
	public static final class ObjectChangeImpl extends ChangeImpl implements ObjectChange {

		private final EObject object;
		private boolean create;
		
		public ObjectChangeImpl(EGraph graph, EObject object, boolean create) {
			super(graph);
			this.object = object;
			this.create = create;
		}

		/*
		 * (non-Javadoc)
		 * @see org.eclipse.emf.henshin.interpreter.Change#applyAndReverse()
		 */
		@Override
		public void applyAndReverse() {
			if (create) {
				graph.add(object);
			} else {
				graph.remove(object);
			}
			create = !create;
		}

		/*
		 * (non-Javadoc)
		 * @see org.eclipse.emf.henshin.interpreter.Change.ObjectChange#getObject()
		 */
		@Override
		public EObject getObject() {
			return object;
		}

		/*
		 * (non-Javadoc)
		 * @see org.eclipse.emf.henshin.interpreter.Change.ObjectChange#isCreate()
		 */
		@Override
		public boolean isCreate() {
			return create;
		}
		
	}
	
	/**
	 * Default implementation of {@link AttributeChange}.
	 * @author Christian Krause
	 */
	public static final class AttributeChangeImpl extends ChangeImpl implements AttributeChange {

		private final EObject object;
		private EAttribute attribute;
		private Object oldValue;
		private Object newValue;
		/**
		 * The semantics of deleting an attribute is to unset it.
		 */
		private boolean delete;
		private boolean initialized;
		private boolean revert = false;

		public AttributeChangeImpl(EGraph graph, EObject object, EAttribute attribute, Object newValue) {
			super(graph);
			this.object = object;
			this.attribute = attribute;
			this.newValue = newValue;
			this.delete = false;
			this.initialized = false;
		}
		
		public AttributeChangeImpl(EGraph graph, EObject object, EAttribute attribute) {
			this(graph, object, attribute, null);
			this.delete = true;
		}

		/*
		 * (non-Javadoc)
		 * @see org.eclipse.emf.henshin.interpreter.Change#applyAndReverse()
		 */
		@SuppressWarnings({ "rawtypes", "unchecked" })
		@Override
		public void applyAndReverse() {
			// Need to initialize?
			 if (!initialized && attribute != null) { 
				oldValue = object.eGet(attribute);
				if (!delete 
						&& ((oldValue==null && newValue==null) 
								|| (oldValue!=null && oldValue.equals(newValue)))) {
					attribute = null;
				}
			}
			// Nothing to do?
			if (attribute==null) {
				return;
			}
			if (delete && !revert) {
				object.eUnset(attribute);				
			} else if (attribute.isMany()) {
				List values = (List) object.eGet(attribute);
				values.clear();
				if (newValue instanceof List) {
					values.addAll((List) newValue);
				} else {
					values.add(newValue);
				}
			} else {
				object.eSet(attribute, newValue);
			}
			java.lang.Object dummy = oldValue;
			oldValue = newValue;
			newValue = dummy;
			revert = !revert;
		}

		/*
		 * (non-Javadoc)
		 * @see org.eclipse.emf.henshin.interpreter.Change.AttributeChange#getObject()
		 */
		@Override
		public EObject getObject() {
			return object;
		}
	
		/*
		 * (non-Javadoc)
		 * @see org.eclipse.emf.henshin.interpreter.Change.AttributeChange#getAttribute()
		 */
		@Override
		public EAttribute getAttribute() {
			return attribute;
		}

		/*
		 * (non-Javadoc)
		 * @see org.eclipse.emf.henshin.interpreter.Change.AttributeChange#getOldValue()
		 */
		@Override
		public Object getOldValue() {
			return oldValue;
		}

		/*
		 * (non-Javadoc)
		 * @see org.eclipse.emf.henshin.interpreter.Change.AttributeChange#getNewValue()
		 */
		@Override
		public Object getNewValue() {
			return newValue;
		}		
	}
	
	/**
	 * Default implementation of {@link ReferenceChange}.
	 * @author Christian Krause
	 */
	public static final class ReferenceChangeImpl extends ChangeImpl implements ReferenceChange {
		
		private final EObject source;
		private EReference reference;
		private int index;
		private EObject target, oldTarget; 
		private boolean create;
		private boolean initialized;
		
		public ReferenceChangeImpl(EGraph graph, EObject source, EObject target, EReference reference, boolean create) {
			super(graph);
			this.source = source;
			this.target = target;
			this.create = create;
			this.reference = reference;
			this.initialized = false;
		}

		/*
		 * (non-Javadoc)
		 * @see org.eclipse.emf.henshin.interpreter.Change#applyAndReverse()
		 */
		@SuppressWarnings({ "rawtypes", "unchecked" })
		@Override
		public void applyAndReverse() {
			// Need to initialize ?
			if (!initialized) {
				if (reference.isMany()) {
					List values = (List) source.eGet(reference);
					index = values.indexOf(target);
					if ((create && index>=0) || (!create && index<0)) {
						reference = null; // nothing to do
					}
					if (create && index<0) {
						index = values.size(); // append the new element at the end
					}
					// TODO: add a warning for containment side effects
				} else {
					oldTarget = (EObject) source.eGet(reference);
					// reference is redirected to the same target
					if (((create && target==oldTarget) || (!create && target!=oldTarget))) {
						if (PRINT_WARNINGS){
							System.out.println("WARNING (Hidden step): recreating '" + reference.getName() + "'-edge from " +
									InterpreterUtil.objectToString(source) + " to " +
									InterpreterUtil.objectToString(oldTarget));
						}
						reference = null; // nothing to do
					}
					// reference is redirected to a new target
					if (create && oldTarget!=null && reference!=null && PRINT_WARNINGS) {
						System.out.println("WARNING (Side effect):  deleting '" + reference.getName() + "'-edge from " +
											InterpreterUtil.objectToString(source) + " to " +
											InterpreterUtil.objectToString(oldTarget));
					}
					// reference is containment and new child (target) already has a parent
					if (create && reference!=null && reference.isContainment() && target.eContainer()!=null && PRINT_WARNINGS) {
						System.out.println("WARNING (Side effect):  deleting '" + reference.getName() + "'-edge from " +
											InterpreterUtil.objectToString(target.eContainer()) + " to " +
											InterpreterUtil.objectToString(target));
					}
					if (!create) {
						target = null; // we want to remove it
					}
				}
				initialized = true;
			}
			// Nothing to do?
			if (reference==null) {
				return;
			}
			// Otherwise do the change:
			if (reference.isMany()) {
				List values = (List) source.eGet(reference);
				if (create) {
					values.add(index, target);
				} else {
					values.remove(index);
				}
				create = !create;
			} else {
				source.eSet(reference, target); // set the new target
				EObject dummy = target; // switch target and old target
				target = oldTarget;
				oldTarget = dummy;
			}
		}

		/*
		 * (non-Javadoc)
		 * @see org.eclipse.emf.henshin.interpreter.Change.ReferenceChange#getSource()
		 */
		@Override
		public EObject getSource() {
			return source;
		}

		/*
		 * (non-Javadoc)
		 * @see org.eclipse.emf.henshin.interpreter.Change.ReferenceChange#getTarget()
		 */
		@Override
		public EObject getTarget() {
			return target;
		}

		/*
		 * (non-Javadoc)
		 * @see org.eclipse.emf.henshin.interpreter.Change.ReferenceChange#getReference()
		 */
		@Override
		public EReference getReference() {
			return reference;
		}

		/*
		 * (non-Javadoc)
		 * @see org.eclipse.emf.henshin.interpreter.Change.ReferenceChange#isCreate()
		 */
		@Override
		public boolean isCreate() {
			return create;
		}
		
	}

	/**
	 * Default implementation of {@link IndexChange}.
	 * @author Christian Krause
	 */
	public static final class IndexChangeImpl extends ChangeImpl implements IndexChange {
		
		private final EObject source;
		private final EObject target; 
		private final EReference reference;
		private int oldIndex;
		private int newIndex;
		private boolean initialized;
		
		public IndexChangeImpl(EGraph graph, EObject source, EObject target, EReference reference, int newIndex) {
			super(graph);
			this.source = source;
			this.target = target;
			this.reference = reference;
			this.newIndex = newIndex;
			this.initialized = false;
		}

		/*
		 * (non-Javadoc)
		 * @see org.eclipse.emf.henshin.interpreter.Change#applyAndReverse()
		 */
		@SuppressWarnings({ "rawtypes" })
		@Override
		public void applyAndReverse() {
			// Need to initialize ?
			if (!initialized) {
				if (reference.isMany()) {
					List values = (List) source.eGet(reference);
					oldIndex = values.indexOf(target);
				} else {
					if (target==source.eGet(reference)) {
						oldIndex = 0;
					} else {
						oldIndex = -1;
					}
				}
				if (oldIndex<0) {
					throw new RuntimeException("Error changing edge index for reference "
							+ reference + " in object " + source + ": target " + target + " not found");
				}
				initialized = true;
			}
			// Try to move the element to the new position:
			if (reference.isMany()) {
				EList values = (EList) source.eGet(reference);
				values.move(newIndex, oldIndex);
			} else if (newIndex!=0) {
				throw new RuntimeException("Cannot move element to position " + newIndex
						+ " in the non-multi reference" + reference);
			}
			// Revert the change:
			int dummy = oldIndex;
			oldIndex = newIndex;
			newIndex = dummy;
		}

		/*
		 * (non-Javadoc)
		 * @see org.eclipse.emf.henshin.interpreter.Change.IndexChange#getSource()
		 */
		@Override
		public EObject getSource() {
			return source;
		}

		/*
		 * (non-Javadoc)
		 * @see org.eclipse.emf.henshin.interpreter.Change.IndexChange#getTarget()
		 */
		@Override
		public EObject getTarget() {
			return target;
		}

		/*
		 * (non-Javadoc)
		 * @see org.eclipse.emf.henshin.interpreter.Change.IndexChange#getReference()
		 */
		@Override
		public EReference getReference() {
			return reference;
		}

		/*
		 * (non-Javadoc)
		 * @see org.eclipse.emf.henshin.interpreter.Change.IndexChange#getOldIndex()
		 */
		@Override
		public int getOldIndex() {
			return oldIndex;
		}

		/*
		 * (non-Javadoc)
		 * @see org.eclipse.emf.henshin.interpreter.Change.IndexChange#getNewIndex()
		 */
		@Override
		public int getNewIndex() {
			return newIndex;
		}
		
	}
	
	/**
	 * Default implementation of {@link CompoundChange}.
	 * @author Christian Krause
	 */
	public static final class CompoundChangeImpl extends ChangeImpl implements CompoundChange {

		// The list of changes:
		private final List<Change> changes;
		
		// Whether to apply them in reverse order:
		private boolean reverse;
		
		/**
		 * Default constructor.
		 * @param graph EGraph to be changed.
		 */
		public CompoundChangeImpl(EGraph graph) {
			super(graph);
			changes = new ArrayList<Change>();
			reverse = false;
		}

		/*
		 * (non-Javadoc)
		 * @see org.eclipse.emf.henshin.interpreter.Change#applyAndReverse()
		 */
		@Override
		public void applyAndReverse() {
			int size = changes.size();
			if (reverse) {
				for (int i=size-1; i>=0; i--) {
					changes.get(i).applyAndReverse();
				}
			} else {
				for (int i=0; i<size; i++) {
					changes.get(i).applyAndReverse();
				}
			}
			reverse = !reverse;
		}

		/*
		 * (non-Javadoc)
		 * @see org.eclipse.emf.henshin.interpreter.Change.ComplexChange#getChanges()
		 */
		@Override
		public List<Change> getChanges() {
			return changes;
		}
		
	}

}
