//------------------------------------------------------------------------------
// 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.configuration;

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

import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.edit.ui.provider.AdapterFactoryContentProvider;
import org.eclipse.epf.library.IConfigurationClosure;
import org.eclipse.epf.library.IConfigurationManager;
import org.eclipse.epf.library.LibraryPlugin;
import org.eclipse.epf.library.LibraryResources;
import org.eclipse.epf.library.LibraryService;
import org.eclipse.epf.library.edit.util.TngUtil;
import org.eclipse.epf.library.services.DependencyManager;
import org.eclipse.epf.library.services.ElementDependency;
import org.eclipse.epf.library.services.ElementReference;
import org.eclipse.epf.library.services.PackageReference;
import org.eclipse.epf.library.util.LibraryUtil;
import org.eclipse.epf.uma.Activity;
import org.eclipse.epf.uma.BreakdownElement;
import org.eclipse.epf.uma.MethodConfiguration;
import org.eclipse.epf.uma.MethodElement;
import org.eclipse.epf.uma.MethodLibrary;
import org.eclipse.epf.uma.MethodPackage;
import org.eclipse.epf.uma.MethodPlugin;
import org.eclipse.epf.uma.ProcessComponent;
import org.eclipse.ui.views.properties.IPropertyDescriptor;
import org.eclipse.ui.views.properties.IPropertySource;

/**
 * A method configuration closure.
 * 
 * @author Jinhua Xi
 * @author Kelvin Low
 * @since 1.0
 */
public class ConfigurationClosure implements IConfigurationClosure {

	// If true, generate debug traces.
	protected static boolean debug = LibraryPlugin.getDefault().isDebugging();

	protected MethodConfiguration config = null;

	protected MethodLibrary library = null;

	protected IConfigurationManager configManager = null;

	protected DependencyManager dependencyManager = null;

	// Node change information. The object are the model objects
	// check the linked objects in needed
	protected List selected = new ArrayList();

	// A map of invalid nodes to ElementDependencyError objects.
	protected Map invalidNodesMap = new HashMap();

	protected List changedNodes = new ArrayList();

	/**
	 * Creates a new instance.
	 * 
	 * @param config
	 *            A method configuration.
	 */
	public ConfigurationClosure(MethodConfiguration config) {
		this.config = config;

		configManager = LibraryService.getInstance().getConfigurationManager(
				config);
		if (configManager != null) {
			library = configManager.getMethodLibrary();
			dependencyManager = configManager.getDependencyManager();
		}

		// cleanup the old status and rebuild the list
		selected.clear();
		changedNodes.clear();
		invalidNodesMap.clear();

		// configuration changed, re-build the selection list
		try {
			buildList(library);
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}

	/**
	 * Returns the method configuration manager.
	 * 
	 * @return A <code>ConfigurationManager</code>.
	 */
	public IConfigurationManager getConfigurationManager() {
		return configManager;
	}

	/**
	 * Returns the method configuration.
	 * 
	 * @return A <code>MethodConfiguration</code>.
	 */
	public MethodConfiguration getConfiguration() {
		return config;
	}

	/**
	 * Returns the containining method library.
	 * 
	 * @return A <code>MethodConfiguration</code>.
	 */
	public MethodLibrary getLibrary() {
		return library;
	}

	/**
	 * Builds the selection list based on the method configuration.
	 * 
	 * @param input
	 */
	private void buildList(EObject input) {
		// Validate the method configuration.
		LibraryUtil.validateMethodConfiguration(config);

		selected.addAll(config.getMethodPluginSelection());
		selected.addAll(config.getMethodPackageSelection());

		// Re-build the selections to auto add the process packages.
		setSelections(selected.toArray());
	}

	/**
	 * Sets the method plug-ins and packages selection.
	 * 
	 * @param elements
	 *            An array of method plug-ins and packages.
	 */
	public void setSelections(Object[] elements) {
		if (elements == null) {
			return;
		}

		// Cleanup the old status and rebuild the list.
		selected.clear();
		changedNodes.clear();
		invalidNodesMap.clear();
		beginUpdate();

		for (int i = 0; i < elements.length; i++) {
			Object e = elements[i];
			if (!selected.contains(e)) {
				if ((e instanceof EObject)) {
					add((EObject) e, false);

					// If the selected element is a process component,
					// select all the process packages that contains the
					// activities.
					if (e instanceof ProcessComponent) {
						selectProcessPackages(((ProcessComponent) e)
								.getProcess());
					}
				} else {
					// The selected element is a UI folder.
					selected.add(e);
				}
			}
		}

		endUpdate();
	}

	private void selectProcessPackages(Activity a) {
		if (a == null) {
			return;
		}

		for (Iterator it = a.getBreakdownElements().iterator(); it.hasNext();) {
			BreakdownElement e = (BreakdownElement) it.next();
			Object pkg = e.eContainer();
			if (!selected.contains(pkg)) {
				selected.add(pkg);
				changedNodes.add(pkg);
			}

			if (e instanceof Activity) {
				selectProcessPackages((Activity) e);
			}
		}
	}

	/**
	 * Adds a method plug-in or package to the closure.
	 * 
	 * @param element
	 *            A method element.
	 * @param addChildren
	 *            if <code>true</code>, add all child method elements.
	 */
	private void add(EObject element, boolean addChildren) {
		if (!LibraryUtil.selectable(element)) {
			return;
		}

		if (!selected.contains(element)) {
			selected.add(element);

			// Save the changed nodes so that we can update the status later.
			addChanged(element);

			if (element instanceof MethodPlugin) {
				selectSystemPackages((MethodPlugin) element);
			}
		}

		// Add the parent method element as well.
		EObject parent = element.eContainer();
		if ((parent != null) && !selected.contains(parent)) {
			add(parent, false);
		}

		// Add children as needed.
		if (addChildren) {
			EList elements = element.eContents();
			if (elements != null) {
				for (Iterator it = elements.iterator(); it.hasNext();) {
					EObject child = (EObject) it.next();
					add(child, true);
				}
			}
		}
	}

	private void addChanged(Object element) {
		if (!changedNodes.contains(element)) {
			changedNodes.add(element);
		}
	}

	private void selectSystemPackages(MethodPlugin plugin) {
		List pkgs = TngUtil.getAllSystemPackages(plugin);
		for (Iterator it = pkgs.iterator(); it.hasNext();) {
			EObject pkg = (EObject) it.next();
			add(pkg, false);
		}
	}

	/**
	 * Checks whether a method plug-in or package is selected.
	 * 
	 * @return <code>true</code> if the given element is selected.
	 */
	public boolean isSelected(Object input) {
		if ((input instanceof MethodLibrary) || input == config
				|| selected.contains(input)) {
			return true;
		}
		return false;
	}

	/**
	 * Gets the element dependency error for a method element.
	 * 
	 * @element A method element.
	 * 
	 * @return An array of <code>ElementDependencyError</code>.
	 */
	public ElementDependencyError getError(Object element) {
		return getError(element, false);
	}

	private ElementDependencyError getError(Object element, boolean create) {
		ElementDependencyError error = (ElementDependencyError) invalidNodesMap
				.get(element);
		if (error == null && create) {
			error = new ElementDependencyError(element);
			invalidNodesMap.put(element, error);
		}
		return error;
	}

	public boolean hasError() {
		for (Iterator it = invalidNodesMap.values().iterator(); it.hasNext();) {
			ElementDependencyError error = (ElementDependencyError) it.next();
			if (error.isError()) {
				return true;
			}
		}
		return false;
	}

	public boolean hasProblem() {
		return invalidNodesMap.size() > 0;
	}

	/**
	 * Returns all the errors.
	 * 
	 * @return A a list of <code>ErrorInfo</code>.
	 */
	public List getAllErrors() {
		List errors = new ArrayList();
		for (Iterator it = invalidNodesMap.values().iterator(); it.hasNext();) {
			ElementDependencyError error = (ElementDependencyError) it.next();
			errors.addAll(error.getAll());
		}
		return errors;
	}

	/**
	 * Returns all the dependency errors.
	 * 
	 * @return An array of <code>ElementDependencyError</code>
	 */
	public Object[] getDependencyErrors() {
		return invalidNodesMap.values().toArray();
	}

	/**
	 * Returns all the invalid elements.
	 * 
	 * @return A list of invalid elements.
	 */
	public List getInvalidElements() {
		return new ArrayList(invalidNodesMap.keySet());
	}

	private void removeError(Object element) {
		if (invalidNodesMap.containsKey(element)) {
			invalidNodesMap.remove(element);

			// Error status changed, add it to the changed list.
			if (!changedNodes.contains(element)) {
				changedNodes.add(element);
			}
		}
	}

	/**
	 * Returns all the changed elements. These are elements whose check states
	 * have changed or whose image have changed due to error.
	 * 
	 * @return A list of changed method elements.
	 */
	public List getChangedElements() {
		List items = new ArrayList(changedNodes);
		for (Iterator it = invalidNodesMap.keySet().iterator(); it.hasNext();) {
			Object item = it.next();
			if (!items.contains(item)) {
				items.add(item);
			}
		}
		return items;
	}

	/**
	 * Returns the method plug-ins and packages selection.
	 * 
	 * @return An array of method plug-ins and packages.
	 */
	public Object[] getSelection() {
		return selected.toArray();
	}

	public void beginUpdate() {
		changedNodes.clear();
	}

	public void endUpdate() {
		// Process the changed elements.
		// Note: additional elements may be added to the changesNodes due to
		// error status change but only the original changed elements need to be
		// processed.
		for (Iterator it = new ArrayList(changedNodes).iterator(); it.hasNext();) {
			Object changedElement = it.next();
			ElementDependency dependency = dependencyManager
					.getDependency((MethodElement) changedElement);
			if (dependency == null) {
				continue;
			}

			if (isSelected(changedElement)) {
				validateSelected(dependency);
			} else {
				validateUnSelected(dependency);
			}
		}

		if (debug) {
			System.out
					.println("There are (" + invalidNodesMap.size() + ") errors"); //$NON-NLS-1$ //$NON-NLS-2$
		}
	}

	/**
	 * Validates the element dependency when the element is selected. When the
	 * element is selected, we need to do the following: 1. Check error for
	 * references 2. Remove error for dependents associated with element 3.
	 * Update parents: if the selection is valid, remove all errors from parents
	 * associated with this element if the selection is invalid, set error to
	 * all parents.
	 * 
	 * @param dependency
	 *            ElementDependency
	 */
	private void validateSelected(ElementDependency dependency) {
		Object changedElement = dependency.getElement();

		// Since this element is selected, remove all previous errors.
		removeError(changedElement);

		if (changedElement instanceof MethodLibrary) {
			return;
		}

		// Since this element is selected, remove all dependency errors in the
		// dependent elements.
		List dependents = dependency.getDependents();
		if (dependents != null && dependents.size() > 0) {
			Object element;
			for (Iterator itr = dependents.iterator(); itr.hasNext();) {
				element = itr.next();

				ElementDependencyError error = getError(element, false);
				if (error != null) {
					error.removeError(changedElement);
					if (error.size() == 0) {
						removeError(element);

						// Clear the parent error introduced by this element.
						updateParentsForErrors((EObject) element);

					}
				}
			}
		}

		// If an element is checked, check the element it depends on,
		// which should be checked as well.
		List refs = dependency.getReferences();
		if (refs != null && refs.size() > 0) {
			PackageReference ref;
			Object element;
			for (Iterator itr = refs.iterator(); itr.hasNext();) {
				ref = (PackageReference) itr.next();
				element = ref.getRefElement();

				if (element instanceof MethodLibrary) {
					continue;
				}

				// RATLC00382739 - Don't warn on optional inputs not being
				// present, so added the canIgnore() method
				if (!isSelected(element) && !canIgnore(ref)) {
					String message;
					int errorType = 0;
					if (ref.hasBaseReference()) {
						errorType = ErrorInfo.ERROR;
						message = LibraryResources.configClosureWarning_msg2;
					} else {
						errorType = ErrorInfo.WARNING;
						message = LibraryResources.configClosureWarning_msg3;
					}

					ElementDependencyError error = getError(changedElement,
							true);
					error.addError(new ErrorInfo(errorType, message,
							changedElement, element, ErrorInfo.REFERENCE_TO));
				} else {
					ElementDependencyError error = getError(changedElement,
							false);
					if (error != null) {
						error.removeError(element);
					}
				}
			}
		}

		// Finally, update the parents.
		updateParentsForErrors((EObject) changedElement);
	}

	/**
	 * Validates the ElementDependency when the element is unselected. When the
	 * element is unselected, we need to do the following: 1. check error for
	 * dependencts 2. remove error for references associated with element, in
	 * case of any added when the element was check 3. update parents: if the
	 * selection is valid, remove all errors from parents associated with this
	 * element if the selection is invalid, set error to all parents.
	 * 
	 * @param dependency
	 *            A <code>ElementDependency</code> object.
	 */
	private void validateUnSelected(ElementDependency dependency) {
		Object changedElement = dependency.getElement();

		// Since this element is un-selected, remove all previous errors.
		removeError(changedElement);

		if (changedElement instanceof MethodLibrary) {
			return;
		}

		// Since this element is un-selected, remove all errors in the
		// referenced elements.
		List refs = dependency.getReferences();
		if (refs != null && refs.size() > 0) {
			ElementReference ref;
			Object element;
			for (Iterator itr = refs.iterator(); itr.hasNext();) {
				ref = (ElementReference) itr.next();
				element = ref.getRefElement();

				ElementDependencyError error = getError(element, false);
				if (error != null) {
					error.removeError(changedElement);
					if (error.size() == 0) {
						removeError(element);

						// Clear the parent error introduced by this element.
						updateParentsForErrors((EObject) element);

					}
				}
			}
		}

		// If an element is unchecked, check the dependent elements.
		// If there are check elements depending on it, the element can't be
		// unchecked.
		List dependents = dependency.getDependents();
		if (dependents != null && dependents.size() > 0) {
			Object element;
			for (Iterator itr = dependents.iterator(); itr.hasNext();) {
				element = itr.next();

				if (element instanceof MethodLibrary) {
					continue;
				}

				if (isSelected(element)) {
					// Determine the type of dependency.
					ElementDependency childDep = dependencyManager
							.getDependency((MethodElement) element);

					validateSelected(childDep);
				} else {
					removeError(changedElement);
				}
			}
		}

		// finally, update the parents
		updateParentsForErrors((EObject) changedElement);
	}

	private void updateParentError(EObject parent, EObject element,
			int errorType) {
		if (parent == null || (parent instanceof MethodLibrary)) {
			return;
		}

		if ((parent instanceof MethodPackage)
				&& ConfigurationHelper.isGlobalPackage((MethodPackage) parent)) {
			updateParentError(parent.eContainer(), element, errorType);
			return;
		}

		// Remove the error associated with this element from all parents.
		ElementDependencyError error = getError(parent, false);
		if (error != null && error.size() > 0) {
			error.removeError(element);
		}

		if (errorType != ErrorInfo.NONE) {
			// Propegate the error to all parents.
			error = getError(parent, true);
			String message = LibraryResources.configClosureWarning_msg1;
			error.addError(new ErrorInfo(errorType, message, parent, element,
					ErrorInfo.NONE));
		} else if ((error != null) && (error.size() == 0)) {
			removeError(parent);
		}

		updateParentError(parent.eContainer(), element, errorType);
	}

	private void updateParentsForErrors(EObject element) {
		int errorType = ErrorInfo.NONE;

		ElementDependencyError error = getError(element);
		if (error != null && error.size() > 0) {
			if (error.isError() || error.isChildError()) {
				errorType = ErrorInfo.CHILD_ERROR;
			} else if (error.isWarning() || error.isChildWarning()) {
				errorType = ErrorInfo.CHILD_WARNING;
			}
		}

		updateParentError(element.eContainer(), element, errorType);
	}

	/**
	 * accept the cutrrent selection and ignore any warning message. make the
	 * configuration.
	 * 
	 */
	public void makeClosure() {
		// If no error. update the method configuration.
		while (hasError()) {
			fixProblems(true);
		}
	}

	/**
	 * fix all error(s) and warnign(s)
	 * 
	 */
	public void fixProblems() {
		// If no error, update the method configuration.
		while (hasProblem()) {
			fixProblems(false);
		}
	}

	private void fixProblems(boolean errorOnly) {
		// Note: make closure will select elements as needed.
		// so we need to make a copy of the current selcted ones
		// in order to trace the status
		List currentSelected = new ArrayList(selected);

		// beginUpdate();

		// list of errorInfo objects
		List errors = getAllErrors();
		if (errors.size() > 0) {
			invalidNodesMap.clear();
			ErrorInfo error;
			EObject ownerElement, causeElement;
			boolean ownerSelected, causeSelected;
			for (Iterator it = errors.iterator(); it.hasNext();) {
				error = (ErrorInfo) it.next();

				ownerElement = (EObject) error.getOwnerElement();
				causeElement = (EObject) error.getCauseElement();
				addChanged(ownerElement);
				addChanged(causeElement);
				if (error.isChildError() || error.isChildWarning()) {
					continue;
				}

				if (error.isWarning() && errorOnly) {
					continue;
				}

				ownerSelected = currentSelected.contains(ownerElement);
				causeSelected = currentSelected.contains(causeElement);

				// If the owner element is not selected
				// the error is caused by un-selecting this element,
				// select it will fix the error

				// If the owner element is selected, the error is caused by
				// un-selected references.
				// select those cause elements will fix the error
				if (!ownerSelected) {
					selectErrorElement(ownerElement);
				} else if (!causeSelected) {
					selectErrorElement(causeElement);
				}
			}
		}

		endUpdate();
	}

	private void selectErrorElement(EObject element) {
		// The selection is based on a package level. When a non-package element
		// is selected, all it's non-package siblings must be selected as well.
		if (LibraryUtil.selectable(element)) {
			add(element, true);
		}
	}

	public void saveMethodConfiguration() {
		List plugins = config.getMethodPluginSelection();
		List packages = config.getMethodPackageSelection();

		plugins.clear();
		packages.clear();

		EObject element;
		for (Iterator it = selected.iterator(); it.hasNext();) {
			element = (EObject) it.next();
			if (element instanceof MethodPlugin) {
				if (!plugins.contains(element)) {
					plugins.add(element);
				}
			} else if ((element instanceof MethodPackage)
					&& !ConfigurationHelper
							.isGlobalPackage((MethodPackage) element)) {
				if (!packages.contains(element)) {
					packages.add(element);
				}
			}
		}
	}

	/**
	 * Packages the library based on the selection.
	 * <p>
	 * Note: This will change the current library. Before calling this method, a
	 * copy of the current library should be created with the following steps:
	 * 1. Create a new <code>ConfigurationManager</code> with a copy of the
	 * original library, 2. Rebuild the dependency, 3. Create a
	 * <code>ConfigurationClosure</code> with the current configuration.
	 * 
	 * @return A <code>MethodLibrary</code>.
	 */
	public MethodLibrary packageLibrary(boolean removeBrokenReferences) {
		processSelection(library, removeBrokenReferences);

		// Remove the configurations except for the current one.
		List configs = library.getPredefinedConfigurations();
		configs.clear();
		configs.add(config);

		return library;
	}

	/**
	 * process the selected package by removeing all unselected elements and any
	 * missing references
	 * 
	 * @param element
	 */
	private void processSelection(EObject element,
			boolean removeBrokenReferences) {
		if (removeBrokenReferences) {
			// Iterator the references and remove broken references.
			EList references = element.eCrossReferences();
			if (references != null) {
				for (Iterator it = new ArrayList(references).iterator(); it
						.hasNext();) {
					EObject ref = (EObject) it.next();
					EObject pkgRef = LibraryUtil.getSelectable(ref);
					if (pkgRef != null && !isSelected(pkgRef)) {
						removeReference(element, ref);
					}
				}
			}
		}

		EList elements = element.eContents();
		if (elements != null) {
			for (Iterator it = new ArrayList(elements).iterator(); it.hasNext();) {
				EObject child = (EObject) it.next();

				// If the child element is selectable but it is not in the
				// configuration, remove it.
				if (LibraryUtil.selectable(child) && !isSelected(child)) {
					EcoreUtil.remove(child);
				} else {
					processSelection(child, removeBrokenReferences);
				}
			}
		}
	}

	private void removeReference(EObject ownerElement, EObject refElement) {
		AdapterFactoryContentProvider provider = configManager
				.getContentProvider();
		IPropertySource ps = provider.getPropertySource(ownerElement);
		IPropertyDescriptor[] pds = ps.getPropertyDescriptors();
		if (pds != null && pds.length > 0) {
			for (int i = 0; i < pds.length; i++) {
				IPropertyDescriptor descriptor = (IPropertyDescriptor) pds[i];
				Object id = descriptor.getId();
				Object value = ps.getPropertyValue(id);

				// Check whether the value needs to be converted to an editable
				// value.
				IPropertySource source = provider.getPropertySource(value);
				if (source != null) {
					value = source.getEditableValue();
				}
				if (value instanceof EList) {
					EList refList = (EList) value;
					if (refList.contains(refElement)) {
						if (debug) {
							System.out
									.println("Reference [" + LibraryUtil.getName(refElement) //$NON-NLS-1$
											+ "] removed from [" //$NON-NLS-1$
											+ LibraryUtil.getName(ownerElement)
											+ "]'s reference list"); //$NON-NLS-1$
						}
						refList.remove(refElement);
						ps.setPropertyValue(id, refList);
					}
				} else if (value instanceof MethodElement) {
					if (debug) {
						System.out
								.println("Reference [" + LibraryUtil.getName(refElement) //$NON-NLS-1$
										+ "] removed from [" //$NON-NLS-1$
										+ LibraryUtil.getName(ownerElement)
										+ "]"); //$NON-NLS-1$
					}
					ps.setPropertyValue(id, null);
				}

			}
		}
	}

	/**
	 * Disposes resources allocated by this closure.
	 */
	public void dispose() {
		configManager = null;
		config = null;
		library = null;
		dependencyManager = null;
		selected.clear();
		changedNodes.clear();
		invalidNodesMap.clear();
	}

	private boolean canIgnore(PackageReference pkgRef) {
		return pkgRef.canIgnore();
	}

}
