/*******************************************************************************
 * Copyright (c) 2003, 2019 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
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * Contributors:
 * IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.wst.common.internal.emfworkbench.integration;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import org.eclipse.emf.common.command.AbstractCommand;
import org.eclipse.emf.common.command.Command;
import org.eclipse.emf.common.command.CommandStack;
import org.eclipse.emf.common.util.Enumerator;
import org.eclipse.emf.ecore.EEnum;
import org.eclipse.emf.ecore.EEnumLiteral;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.edit.command.AddCommand;
import org.eclipse.emf.edit.command.RemoveCommand;
import org.eclipse.emf.edit.command.SetCommand;
import org.eclipse.emf.edit.domain.EditingDomain;
import org.eclipse.wst.common.internal.emf.utilities.ExtendedEcoreUtil;

/**
 * Insert the type's description here. Creation date: (4/6/2001 3:40:35 PM)
 * 
 * @author: Administrator
 */
public class ModelModifier {
	private static final String SET_PATTERN = "Set {0}"; //$NON-NLS-1$
	private static final String ADD_PATTERN = "Add {0}"; //$NON-NLS-1$
	private static final String REMOVE_PATTERN = "Remove {0}"; //$NON-NLS-1$
	private static final String DEFAULT_COMMAND_LABEL = "Command"; //$NON-NLS-1$
	private EditingDomain editingDomain;
	private List helpers;
	private List extendedHelpers;
	protected List additionalCommands;
	protected int status;
	public static final int NO_VALUE_CHANGE = 0;
	public static final int VALUE_CHANGE = 1;
	public static final int ERROR = 2;

	/**
	 * J2EEModelModifier constructor comment.
	 */
	public ModelModifier() {
		super();
	}

	/**
	 * J2EEModelModifier constructor comment.
	 * 
	 * @param aDomain
	 *            EditingDomain
	 */
	public ModelModifier(EditingDomain aDomain) {
		setEditingDomain(aDomain);
	}

	/**
	 * Add
	 * 
	 * @aHelper to the list of helper that will be executed.
	 */
	public void addHelper(ModifierHelper aHelper) {
		if (aHelper != null && !getHelpers().contains(aHelper))
			getHelpers().add(aHelper);
	}

	public void addAdditionalCommand(Command aCommand) {
		if (aCommand != null && !getAdditionalCommands().contains(aCommand))
			additionalCommands.add(aCommand);
	}

	/**
	 * Return true if this modifier can create a command that will perform the necessary operation.
	 */
	public boolean canExecuteCommand() {
		return getEditingDomain() != null;
	}

	public int executeWithStatus() {
		try {
			execute();
			return status;
		} finally {
			status = -1;
		}
	}

	/**
	 * Execute this modifier using the recording mechanism of the StructedTextUndoManager. If this
	 * modifier cannot record, try to execute using the CommandStack (if it can execute commands).
	 * Return true if the execution was attempted.
	 * 
	 * @see canExecuteCommand()
	 * @see canRecord()
	 * @see run()
	 */
	public boolean execute(ModifierHelper aHelper) {
		addHelper(aHelper);
		return execute();
	}

	/**
	 * Execute this modifier using the recording mechanism of the StructedTextUndoManager. If this
	 * modifier cannot record, try to execute using the CommandStack (if it can execute commands).
	 * Return true if the execution was attempted.
	 * 
	 * @see canExecuteCommand()
	 * @see canRecord()
	 * @see run()
	 */
	public boolean execute(List someHelpers) {
		setHelpers(someHelpers);
		return execute();
	}

	/**
	 * Execute this modifier by creating a Command that is executed on the CommandStack. If this
	 * modifier cannot execute commands, the execution will not take place. Return true if the
	 * execution was attempted.
	 * 
	 * @see canExecuteCommand()
	 */
	public boolean execute() {
		boolean result = false;
		if (canExecuteCommand()) {
			try {
				Command command = createCommand();
				result = command != null;
				if (result) {
					getCommandStack().execute(command);
				}
			} finally {
				release();
			}
		} else {
			setStatus(ERROR);
		}
		return result;
	}

	protected CommandStack getCommandStack() {
		if (getEditingDomain() != null)
			return getEditingDomain().getCommandStack();
		return null;
	}

	/**
	 * Insert the method's description here. Creation date: (4/6/2001 2:53:17 PM)
	 * 
	 * @return EditingDomain
	 */
	public EditingDomain getEditingDomain() {
		return editingDomain;
	}

	/**
	 * Insert the method's description here. Creation date: (4/10/2001 8:46:35 AM)
	 * 
	 * @return J2EEModifierHelper
	 */
	public ModifierHelper getFirstHelper() {
		if (helpers != null && getHelpers().size() > 0)
			return (ModifierHelper) getHelpers().get(0);
		return null;
	}

	/**
	 * Insert the method's description here. Creation date: (4/10/2001 8:46:35 AM)
	 * 
	 * @return java.util.List
	 */
	public java.util.List getHelpers() {
		if (helpers == null)
			helpers = new ArrayList();
		return helpers;
	}

	public java.util.List getAdditionalCommands() {
		if (additionalCommands == null)
			additionalCommands = new ArrayList();
		return additionalCommands;
	}

	/**
	 * Release all model artifacts.
	 */
	protected void release() {
		setEditingDomain(null);
		setHelpers(null);
	}

	/**
	 * Insert the method's description here. Creation date: (4/6/2001 2:53:17 PM)
	 * 
	 * @param newEditingDomain
	 *            EditingDomain
	 */
	public void setEditingDomain(EditingDomain newEditingDomain) {
		editingDomain = newEditingDomain;
	}

	/**
	 * Insert the method's description here. Creation date: (4/10/2001 8:46:35 AM)
	 * 
	 * @param newHelpers
	 *            java.util.List
	 */
	public void setHelpers(java.util.List newHelpers) {
		helpers = newHelpers;
	}

	protected void setStatus(int statusCode) {
		if (statusCode > status)
			status = statusCode;
	}

	/**
	 * Return an AddCommand that will be executed by a CommandStack.
	 */
	protected Command createAddCommand(ModifierHelper aHelper) {
		Object value = getValue(aHelper);
		Command command = null;
		if (valueChanged(aHelper.getOwner(), aHelper.getFeature(), value, false)) {
			command = AddCommand.create(getEditingDomain(), aHelper.getOwner(), aHelper.getFeature(), value);
			((AbstractCommand) command).setLabel(createCommandLabel(ADD_PATTERN, aHelper.getFeature()));
			setStatus(VALUE_CHANGE);
		} else {
			setStatus(NO_VALUE_CHANGE);
		}
		return command;
	}

	/**
	 * Return a Command that will be executed by a CommandStack. The default is to return null.
	 * Subclasses should override this method.
	 */
	public Command createCommand() {
		Command chainedCommand = createCommand(null, getHelpers());
		if (null == chainedCommand && additionalCommands != null && additionalCommands.isEmpty()) {
			setStatus(ERROR);
			return null;
		}
		chainedCommand = appendAdditionalCommands(chainedCommand);
		return chainedCommand;
	}

	protected Command createCommand(Command chainedCommand, List helpersArg) {
		Command innerChainedCommand = chainedCommand;
		if (null == extendedHelpers) {
			extendedHelpers = new ArrayList();
		}

		if (!helpersArg.isEmpty()) {
			Iterator it = helpersArg.iterator();
			Command nextCommand = null;
			while (it.hasNext()) {
				nextCommand = createCommand((ModifierHelper) it.next());
				if (innerChainedCommand == null)
					innerChainedCommand = nextCommand;
				else if (nextCommand != null)
					innerChainedCommand = innerChainedCommand.chain(nextCommand);
			}
		}
		if (!extendedHelpers.isEmpty()) {
			List copy = new ArrayList();
			copy.addAll(extendedHelpers);
			extendedHelpers.clear();
			innerChainedCommand = createCommand(innerChainedCommand, copy);
		}
		return innerChainedCommand;
	}

	protected Command appendAdditionalCommands(Command chainedCommand) {
		Command innerChainedCommand = chainedCommand;
		if (additionalCommands != null && !additionalCommands.isEmpty()) {
			Command command;
			for (int i = 0; i < additionalCommands.size(); i++) {
				command = (Command) additionalCommands.get(i);
				if (innerChainedCommand == null)
					innerChainedCommand = command;
				else
					innerChainedCommand = innerChainedCommand.chain(command);
			}
		}
		return innerChainedCommand;
	}

	/**
	 * Return a Command that will be executed by a CommandStack.
	 */
	protected Command createCommand(ModifierHelper aHelper) {
		if (aHelper == null)
			return null;
		Command command1, command2;
		ModifierHelper ownerHelper = aHelper.getOwnerHelper();
		if (aHelper.shouldUnsetValue() && ownerHelper != null)
			return null; //we are unsetting a value on an owner that does not exist so do not
		// create the owner
		command1 = createCommand(ownerHelper);
		command2 = primCreateCommand(aHelper);

		if (command1 != null) {
			if (command2 == null)
				command2 = command1;
			else
				command2 = command2.chain(command1);
		}
		return command2;
	}

	protected String createCommandLabel(String aPattern, EStructuralFeature feature) {
		String replacement = feature == null ? DEFAULT_COMMAND_LABEL : feature.getName();
		return java.text.MessageFormat.format(aPattern, new Object[]{replacement});
	}

	/**
	 * Return a Command that will be executed by a CommandStack. The default is to return null.
	 * Subclasses should override this method.
	 */
	protected Command createManyCommand(ModifierHelper aHelper) {
		if (aHelper.shouldUnsetValue())
			return createRemoveCommand(aHelper);
		return createAddCommand(aHelper);
	}

	protected EObject createObjectFromHelper(ModifierHelper aHelper) {
		return aHelper.createNewObjectFromFeature();
	}

	
	/**
	 * Used to remove an object on unset.  Makes the object a proxy  
	 * on execute and redo commands.  Undo will remove the proxy.
	 */
	public class ProxyWrappingCommand extends AbstractCommand {
		protected Command baseCommand = null;
		protected EObject eObject = null;
		protected Resource resource = null;

		public ProxyWrappingCommand(Command baseCommand, EObject eObject) {
			this.baseCommand = baseCommand;
			this.eObject = eObject;
			this.resource = eObject.eResource();
		}

		@Override
		public boolean canExecute() {
			return baseCommand.canExecute();
		}

		@Override
		public void execute() {
			ExtendedEcoreUtil.becomeProxy(eObject, resource);
			baseCommand.execute();
		}

		@Override
		public boolean canUndo() {
			return baseCommand.canUndo();
		}

		@Override
		public void undo() {
			baseCommand.undo();
			ExtendedEcoreUtil.removeProxy(eObject, resource);
		}

		@Override
		public void redo() {
			ExtendedEcoreUtil.becomeProxy(eObject, resource);
			baseCommand.redo();
		}

		@Override
		public Collection getResult() {
			return baseCommand.getResult();
		}

		@Override
		public Collection getAffectedObjects() {
			return baseCommand.getAffectedObjects();
		}

		@Override
		public String getLabel() {
			return baseCommand.getLabel();
		}

		@Override
		public String getDescription() {
			return baseCommand.getDescription();
		}

		@Override
		public void dispose() {
			super.dispose();
			baseCommand.dispose();
		}
	}

	/**
	 * Return a Remove Command that will be executed by a CommandStack.
	 */
	protected Command createRemoveCommand(ModifierHelper aHelper) {
		Object value = getValue(aHelper);
		Command command = null;
		EStructuralFeature feature = aHelper.getFeature();
		if (valueChanged(aHelper.getOwner(), feature, value, true)) {
			if (isValueEqual(aHelper, value)) {
				command = RemoveCommand.create(getEditingDomain(), aHelper.getOwner(), feature, (Collection) value);
			} else {
				command = RemoveCommand.create(getEditingDomain(), aHelper.getOwner(), feature, value);
			}
			((AbstractCommand) command).setLabel(createCommandLabel(REMOVE_PATTERN, feature));
			setStatus(VALUE_CHANGE);
		} else {
			setStatus(NO_VALUE_CHANGE);
		}
		return command;
	}

	private boolean isValueEqual(ModifierHelper aHelper, Object value) {
		return aHelper.getOwner().eGet(aHelper.getFeature()) == value;
	}

	/**
	 * Return a SetCommand that will be executed by a CommandStack.
	 */
	protected Command createSingleCommand(ModifierHelper aHelper) {
		Object value = getValue(aHelper);
		Command command = null;
		if (valueChanged(aHelper.getOwner(), aHelper.getFeature(), value, aHelper.shouldUnsetValue())) {
			command = SetCommand.create(getEditingDomain(), aHelper.getOwner(), aHelper.getFeature(), value);
			((AbstractCommand) command).setLabel(createCommandLabel(SET_PATTERN, aHelper.getFeature()));
			setStatus(VALUE_CHANGE);
		} else {
			setStatus(NO_VALUE_CHANGE);
		}
		return command;
	}

	protected Object createValueFromHelper(ModifierHelper aHelper) {
		EObject newObject = createObjectFromHelper(aHelper);
		setNewObjectAttributes(newObject, aHelper);
		return newObject;
	}

	protected boolean enumValueChanged(EObject anObject, EStructuralFeature aFeature, Object aValue) {
		if (!anObject.eIsSet(aFeature))
			return true;
		Enumerator existingEnumerator = (Enumerator) anObject.eGet(aFeature);
		Enumerator newEnumerator = getEnumerator(aFeature, aValue);
		return existingEnumerator != newEnumerator;
	}

	private Enumerator getEnumerator(EStructuralFeature aFeature, Object aValue) {
		if (aValue instanceof Enumerator)
			return (Enumerator) aValue;
		EEnum anEnum = (EEnum) aFeature.getEType();
		EEnumLiteral literal = null;
		if (aValue instanceof String)
			literal = anEnum.getEEnumLiteral((String) aValue);
		else if (aValue instanceof Integer)
			literal = anEnum.getEEnumLiteral(((Integer) aValue).intValue());
		if (literal != null)
			return literal.getInstance();
		return null;
	}

	protected Object getValue(ModifierHelper aHelper) {
		if (aHelper.mustCreateValue()) {
			Object value = createValueFromHelper(aHelper);
			aHelper.setValue(value);
		}
		return aHelper.getValue();
	}

	protected boolean manyValueChanged(EObject anObject, EStructuralFeature aFeature, Object aValue, boolean isUnset) {
		List list = (List) anObject.eGet(aFeature);
		if (isUnset)
			return list.contains(aValue) || (list == aValue && !list.isEmpty());
		return !list.contains(aValue);
	}

	/**
	 * Return a Command that will be executed by a CommandStack. The default is to return null.
	 * Subclasses should override this method.
	 */
	protected Command primCreateCommand(ModifierHelper aHelper) {
		Command command = doCreateCommand(aHelper);
		if (aHelper.shouldUnsetValue()) {
			Object value = aHelper.getValue();
			if (value instanceof EObject && !((EObject) value).eIsProxy()) {
				command = new ProxyWrappingCommand(command, (EObject) value);
			}
		}
		return command;
	}

	protected Command doCreateCommand(ModifierHelper aHelper) {
		if (!aHelper.isComplete()) {
			setStatus(ERROR);
			return null;
		}
		Command command = null;
		if (aHelper.getFeature().isMany())
			command = createManyCommand(aHelper);
		else
			command = createSingleCommand(aHelper);

		if (null != command) {
			List localHelpers = ModifierHelperRegistry.getInstance().getHelpers(aHelper);
			if (null != localHelpers) {
				extendedHelpers.addAll(localHelpers);
			}
		}
		return command;

	}

	/**
	 * Run using
	 * 
	 * @aHelper. This will set a MOF attibute value to the owner of the helper.
	 */
	protected void primRun(ModifierHelper aHelper) {
		if (aHelper.isComplete()) {
			Object value = getValue(aHelper);
			if (valueChanged(aHelper.getOwner(), aHelper.getFeature(), value, aHelper.shouldUnsetValue()))
				setObjectAttribute(aHelper.getOwner(), aHelper.getFeature(), value, aHelper.shouldUnsetValue());
		}
	}

	/**
	 * The default is to do nothing. Subclasses should override this method if they are using
	 * recordable commands. The implementation of this method should update the MOF model directly.
	 * Any modification will be recorded.
	 */
	public void run() {
		if (!getHelpers().isEmpty()) {
			Iterator it = getHelpers().iterator();
			while (it.hasNext())
				run((ModifierHelper) it.next());
		}
	}

	/**
	 * Run using
	 * 
	 * @aHelper's ownerHelper first before running with
	 * @aHelper.
	 */
	protected void run(ModifierHelper aHelper) {
		if (aHelper != null) {
			run(aHelper.getOwnerHelper());
			primRun(aHelper);
		}
	}

	protected void setNewObjectAttributes(EObject anObject, ModifierHelper aHelper) {
		HashMap attributes = aHelper.getAttributes();
		Iterator it = attributes.keySet().iterator();
		EStructuralFeature feature;
		Object value = null;
		while (it.hasNext()) {
			feature = (EStructuralFeature) it.next();
			value = attributes.get(feature);
			setObjectAttribute(anObject, feature, value, false);
		}
	}

	protected void setObjectAttribute(EObject anObject, EStructuralFeature aFeature, Object aValue, boolean shouldUnsetValue) {
		if (aFeature.isMany())
			setObjectManyAttribute(anObject, aFeature, aValue, shouldUnsetValue);
		else
			setObjectSingleAttribute(anObject, aFeature, aValue, shouldUnsetValue);
	}

	protected void setObjectEnumAttribute(EObject anObject, EStructuralFeature aFeature, Object aValue) {
		Enumerator enumerator = getEnumerator(aFeature, aValue);
		anObject.eSet(aFeature, enumerator);
	}

	protected void setObjectManyAttribute(EObject anObject, EStructuralFeature aFeature, Object aValue, boolean shouldUnsetValue) {
		List list = (List) anObject.eGet(aFeature);
		if (shouldUnsetValue)
			list.remove(aValue);
		else
			list.add(aValue);
	}

	protected void setObjectSingleAttribute(EObject anObject, EStructuralFeature aFeature, Object aValue, boolean shouldUnsetValue) {
		if (shouldUnsetValue)
			anObject.eUnset(aFeature);
		else if (aFeature.getEType() instanceof EEnum)
			setObjectEnumAttribute(anObject, aFeature, aValue);
		else
			anObject.eSet(aFeature, aValue);
	}

	protected boolean singleValueChanged(EObject anObject, EStructuralFeature aFeature, Object aValue, boolean isUnset) {
		if (aFeature.getEType() instanceof EEnum)
			return enumValueChanged(anObject, aFeature, aValue);

		Object existingValue = anObject.eGet(aFeature);
		if (existingValue == null && aValue == null)
			return false;
		if (existingValue != null && !existingValue.equals(aValue))
			return true;
		if (aValue != null && !aValue.equals(existingValue))
			return true;
		return false;
	}

	protected boolean valueChanged(EObject anObject, EStructuralFeature aFeature, Object aValue, boolean isUnset) {
		if (aFeature.isMany())
			return manyValueChanged(anObject, aFeature, aValue, isUnset);
		return singleValueChanged(anObject, aFeature, aValue, isUnset);
	}
}
