//------------------------------------------------------------------------------
// Copyright (c) 2005, 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 v1.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// Contributors:
// IBM Corporation - initial implementation
//------------------------------------------------------------------------------
package org.eclipse.epf.library.edit.command;

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;

import org.eclipse.emf.common.command.AbstractCommand;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.epf.library.edit.ui.UserInteractionHelper;
import org.eclipse.epf.library.edit.util.Misc;
import org.eclipse.epf.library.edit.util.TngUtil;
import org.eclipse.epf.uma.ContentCategory;
import org.eclipse.epf.uma.ContentPackage;
import org.eclipse.epf.uma.CustomCategory;
import org.eclipse.epf.uma.MethodElement;
import org.eclipse.epf.uma.MethodPlugin;
import org.eclipse.epf.uma.UmaFactory;
import org.eclipse.epf.uma.VariabilityType;
import org.eclipse.epf.uma.ecore.impl.MultiResourceEObject;
import org.eclipse.epf.uma.ecore.util.OppositeFeature;
import org.eclipse.epf.uma.util.UmaUtil;

/**
 * This command is used to assign a method element to a category. If the category
 * is in a base method plug-in of the element, this command will automatically
 * create a contributor to the base category and assign the element to the
 * contributing category. This is avoids modifying the base method plug-in.
 * 
 * @author Phong Nguyen Le
 * @since 1.0
 */
public class AddToCategoryCommand extends AbstractCommand implements
		IResourceAwareCommand {

	private ContentCategory category;

	private EStructuralFeature feature;

	private MethodElement element;

	private String[] categoryPkgPath;

	private boolean checkForExistingContributor;

	private ContentCategory usedCategory = null;

	private boolean createNewContributor = false;

	private ContentPackage categoryPkg;

	private Collection modifiedResources;

	private Object oldOppositeFeatureValue;

	public AddToCategoryCommand(ContentCategory category,
			MethodElement element, EStructuralFeature feature,
			String[] categoryPkgPath, boolean checkForExistingContributor) {
		this.category = category;
		this.feature = feature;
		this.element = element;
		this.categoryPkgPath = categoryPkgPath;
		this.checkForExistingContributor = checkForExistingContributor;
	}

	protected boolean prepare() {
		// check if this operation will modify the element in opposite feature
		// value
		//
		return UserInteractionHelper.checkModifyOpposite(category, feature,
				element);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.emf.common.command.AbstractCommand#getLabel()
	 */
	public String getLabel() {
		return "Add To Category";
	}

	public void execute() {
		// create contributor for category if it is not in the same plugin with
		// the element
		//
		MethodPlugin elementPlugin = UmaUtil.getMethodPlugin(element);
		MethodPlugin categoryPlugin = UmaUtil.getMethodPlugin(category);
		if (categoryPlugin != elementPlugin
				&& Misc.isBaseOf(categoryPlugin, elementPlugin)) {
			if (category.getVariabilityBasedOnElement() != null) {
				throw new IllegalArgumentException(
						"Could not add element to an extended category that is in a different plug-in: " + category); //$NON-NLS-1$
			}
			if (!feature.isMany()) {
				throw new UnsupportedOperationException();
			}
			categoryPkg = UmaUtil.findContentPackage(elementPlugin,
					categoryPkgPath);
			ContentCategory contrib = null;
			if (checkForExistingContributor) {
				// look for the existing contributor for the category
				//
				contrib = TngUtil.findContributor(categoryPkg, category);
			}

			if (contrib == null) {
				createNewContributor = true;
			} else {
				usedCategory = contrib;
			}
		} else {
			usedCategory = category;
		}

		modifiedResources = new HashSet();

		redo();
	}

	public void redo() {
		if (createNewContributor) {
			ContentCategory contrib = (ContentCategory) UmaFactory.eINSTANCE
					.create(category.eClass());
			contrib.setName(category.getName());
			contrib.setVariabilityBasedOnElement(category);
			contrib.setVariabilityType(VariabilityType.CONTRIBUTES_LITERAL);
			categoryPkg.getContentElements().add(contrib);
			usedCategory = contrib;
			if (usedCategory instanceof CustomCategory) {
				CustomCategory root = TngUtil.getRootCustomCategory(UmaUtil
						.getMethodPlugin(usedCategory));
				List list = root.getCategorizedElements();
				list.add(usedCategory);
			}
		}
		OppositeFeature oppositeFeature = OppositeFeature
				.getOppositeFeature(feature);
		MultiResourceEObject mrEObj = (MultiResourceEObject) element;
		oldOppositeFeatureValue = mrEObj.getOppositeFeatureMap().get(
				oppositeFeature);
		if (feature.isMany()) {
			Collection collection = (Collection) usedCategory.eGet(feature);
			if (oldOppositeFeatureValue != null && !oppositeFeature.isMany()) {
				if (oldOppositeFeatureValue != usedCategory) {
					EObject oldOppVal = (EObject) oldOppositeFeatureValue;
					((Collection) oldOppVal.eGet(feature)).remove(element);
					if (oldOppVal.eResource() != null) {
						modifiedResources.add(oldOppVal.eResource());
					}

					collection.add(element);
				}
			}
			if (!collection.contains(element)) {
				collection.add(element);
			}
		} else {
			if (oldOppositeFeatureValue != null && !oppositeFeature.isMany()) {
				if (oldOppositeFeatureValue != usedCategory) {
					EObject oldOppVal = (EObject) oldOppositeFeatureValue;
					oldOppVal.eSet(feature, null);
					if (oldOppVal.eResource() != null) {
						modifiedResources.add(oldOppVal.eResource());
					}
				}
			}
			if (usedCategory != oldOppositeFeatureValue) {
				usedCategory.eSet(feature, element);
			}
		}

		if (usedCategory.eResource() != null) {
			modifiedResources.add(usedCategory.eResource());
		}
	}

	public void undo() {
		OppositeFeature oppositeFeature = OppositeFeature
				.getOppositeFeature(feature);
		if (feature.isMany()) {
			Collection collection = (Collection) usedCategory.eGet(feature);
			collection.remove(element);
		} else {
			usedCategory.eSet(feature, null);
		}

		// restore old value of the single-value opposite feature
		//
		if (!oppositeFeature.isMany() && oldOppositeFeatureValue != null
				&& oldOppositeFeatureValue != usedCategory) {
			EObject oldOppVal = (EObject) oldOppositeFeatureValue;
			if (feature.isMany()) {
				((Collection) oldOppVal.eGet(feature)).add(element);
			} else {
				oldOppVal.eSet(feature, element);
			}
		}

		if (createNewContributor) {
			if (TngUtil.isEmpty(usedCategory)) {
				EcoreUtil.remove(usedCategory);
				usedCategory = null;
			}
		}

	}

	public Collection getAffectedObjects() {
		return Collections.singletonList(usedCategory);
	}

	public Collection getModifiedResources() {
		
		// https://bugs.eclipse.org/bugs/show_bug.cgi?id=146662
		// Below adding category's resource to modifiedResources is needed, 
		// because before executing command, need to verify if resource is mutable or not
		// getModifiedResources() called before executing actual execute() in few cases.
		// Useful in special cases like if resource is in version control.
		//
		if(category != null && category.eResource() != null
				&& modifiedResources == null){
			modifiedResources = new HashSet();
			modifiedResources.add(category.eResource());
		}
		
		if (modifiedResources == null) {
			return Collections.EMPTY_LIST;
		}
		return modifiedResources;
	}

}
