/*******************************************************************************
 *  Copyright (c) 2011, 2012 IBM Corporation and others.
 *
 *  This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License 2.0
 *  which accompanies this distribution, and is available at
 *  https://www.eclipse.org/legal/epl-2.0/
 *
 *  SPDX-License-Identifier: EPL-2.0
 *
 *  Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.core.resources;

import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.settings.model.ICStorageElement;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;

import com.ibm.icu.text.MessageFormat;

/**
 * A RefreshExclusion represents a rule for excluding certain resources from being refreshed.
 *
 * Clients should extend this class to provide support for their own custom exclusions.
 *
 * <strong>EXPERIMENTAL</strong>. This class or interface has been added as part of a work in progress. There
 * is no guarantee that this API will work or that it will remain the same. Please do not use this API without
 * consulting with the CDT team.
 *
 * @author crecoskie
 * @since 5.3
 *
 */
public abstract class RefreshExclusion implements Cloneable {

	public static final String CLASS_ATTRIBUTE_NAME = "class"; //$NON-NLS-1$
	public static final String CONTRIBUTOR_ID_ATTRIBUTE_NAME = "contributorId"; //$NON-NLS-1$
	public static final String DISPLAY_STRING_ATTRIBUTE_NAME = "displayString"; //$NON-NLS-1$
	public static final String EXCLUSION_ELEMENT_NAME = "exclusion"; //$NON-NLS-1$
	public static final String EXCLUSION_TYPE_ATTRIBUTE_NAME = "exclusionType"; //$NON-NLS-1$
	public static final String EXTENSION_DATA_ELEMENT_NAME = "extensionData"; //$NON-NLS-1$
	public static final String FILE_VALUE = "FILE"; //$NON-NLS-1$
	public static final String FOLDER_VALUE = "FOLDER"; //$NON-NLS-1$
	public static final String INSTANCE_ELEMENT_NAME = "instance"; //$NON-NLS-1$
	public static final String RESOURCE_VALUE = "RESOURCE"; //$NON-NLS-1$
	public static final String WORKSPACE_PATH_ATTRIBUTE_NAME = "workspacePath"; //$NON-NLS-1$

	public synchronized static List<RefreshExclusion> loadData(ICStorageElement parentElement,
			RefreshExclusion parentExclusion, IResource parentResource, RefreshScopeManager manager)
			throws CoreException {

		List<RefreshExclusion> exclusions = new LinkedList<>();

		// the parent element might contain any number of exclusions... iterate through the list
		ICStorageElement[] children = parentElement.getChildren();

		for (ICStorageElement child : children) {

			if (child.getName().equals(EXCLUSION_ELEMENT_NAME)) {

				// create an object of the proper type
				String className = child.getAttribute(CLASS_ATTRIBUTE_NAME);
				RefreshExclusion newExclusion = manager.getExclusionForClassName(className);

				if (newExclusion == null) {
					throw new CoreException(
							CCorePlugin.createStatus(MessageFormat.format(Messages.RefreshExclusion_0, className)));
				}

				// load the exclusion type
				String exclusionTypeString = child.getAttribute(EXCLUSION_TYPE_ATTRIBUTE_NAME);
				if (exclusionTypeString != null) {
					if (exclusionTypeString.equals(FILE_VALUE)) {
						newExclusion.fExclusionType = org.eclipse.cdt.core.resources.ExclusionType.FILE;
					}

					else if (exclusionTypeString.equals(FOLDER_VALUE)) {
						newExclusion.fExclusionType = org.eclipse.cdt.core.resources.ExclusionType.FOLDER;
					}

					else if (exclusionTypeString.equals(RESOURCE_VALUE)) {
						newExclusion.fExclusionType = org.eclipse.cdt.core.resources.ExclusionType.RESOURCE;
					}

					else {
						// error
					}
				}

				// set parent if nested
				newExclusion.fParentExclusion = parentExclusion;

				// set parent resource if there is one
				newExclusion.fParentResource = parentResource;

				newExclusion.fContributorId = child.getAttribute(CONTRIBUTOR_ID_ATTRIBUTE_NAME);

				// get the extension element
				ICStorageElement[] grandchildren = child.getChildren();

				for (ICStorageElement grandchild : grandchildren) {

					if (grandchild.getName().equals(RefreshExclusion.EXTENSION_DATA_ELEMENT_NAME)) {

						// load the extension's data
						newExclusion.loadExtendedData(grandchild);
					}

					else if (grandchild.getName().equals(INSTANCE_ELEMENT_NAME)) {

						// load the instance data
						ExclusionInstance instance = ExclusionInstance.loadInstanceData(grandchild, manager);
						newExclusion.fExclusionInstanceList.add(instance);
					}
				}

				// load nested exclusions
				List<RefreshExclusion> nestedExclusions = loadData(child, newExclusion, null, manager);

				// add to parent
				for (RefreshExclusion nestedExclusion : nestedExclusions) {
					newExclusion.addNestedExclusion(nestedExclusion);
				}

				// add the new exclusion to the list of exclusions to return
				exclusions.add(newExclusion);
			}
		}

		return exclusions;
	}

	protected String fContributorId = ""; //$NON-NLS-1$
	protected List<ExclusionInstance> fExclusionInstanceList = new LinkedList<>();
	protected ExclusionType fExclusionType = ExclusionType.RESOURCE;
	protected List<RefreshExclusion> fNestedExclusions = new LinkedList<>();

	protected RefreshExclusion fParentExclusion;

	protected IResource fParentResource;

	/**
	 * Adds an instance to the list of instances of this exclusion.
	 *
	 * @param exclusionInstance
	 */
	public synchronized void addExclusionInstance(ExclusionInstance exclusionInstance) {
		exclusionInstance.setParentExclusion(this);
		fExclusionInstanceList.add(exclusionInstance);
	}

	public synchronized void addNestedExclusion(RefreshExclusion exclusion) {
		fNestedExclusions.add(exclusion);
		exclusion.setParentExclusion(this);
	}

	/**
	 * @return a String corresponding to the ID of the RefreshExclusionContributor that was used to create
	 *         this exclusion.
	 */
	public synchronized String getContributorId() {
		return fContributorId;
	}

	/**
	 * @return an unmodifiable list of all the instance of this exclusion
	 */
	public synchronized List<ExclusionInstance> getExclusionInstances() {
		return Collections.unmodifiableList(fExclusionInstanceList);
	}

	public synchronized ExclusionType getExclusionType() {
		return fExclusionType;
	}

	/**
	 * @return a String corresponding to the human-readable name for this exclusion.
	 */
	public abstract String getName();

	/**
	 *
	 * @return an unmodifiable list of exclusions to this exclusion.
	 */
	public synchronized List<RefreshExclusion> getNestedExclusions() {
		return Collections.unmodifiableList(fNestedExclusions);
	}

	/**
	 * If this is a nested exclusion, returns the exclusion which is the direct parent of this one.
	 *
	 * @return RefreshExclusion
	 */
	public synchronized RefreshExclusion getParentExclusion() {
		return fParentExclusion;
	}

	/**
	 * If this exclusion is a direct descendant of a resource, returns that resource. Otherwise, returns null;
	 *
	 * @return IResource
	 */
	public synchronized IResource getParentResource() {
		return fParentResource;
	}

	protected synchronized void loadExtendedData(ICStorageElement grandchild) {
		// override to provide extension specific behaviour if desired
	}

	public synchronized void persistData(ICStorageElement parentElement) {
		// persist the common data that all RefreshExclusions have
		ICStorageElement exclusionElement = parentElement.createChild(EXCLUSION_ELEMENT_NAME);

		// persist the type of the object we are
		exclusionElement.setAttribute(CLASS_ATTRIBUTE_NAME, this.getClass().getName());

		// persist the exclusion type
		String exclusionType = null;
		switch (getExclusionType()) {
		case FILE:
			exclusionType = FILE_VALUE;
			break;

		case FOLDER:
			exclusionType = FOLDER_VALUE;
			break;

		case RESOURCE:
			exclusionType = RESOURCE_VALUE;
			break;
		}

		if (exclusionType != null) {
			exclusionElement.setAttribute(EXCLUSION_TYPE_ATTRIBUTE_NAME, exclusionType);
		}

		// note: no need to persist parent, the parent relationship will be determined on load by
		// the structure of the XML tree

		exclusionElement.setAttribute(CONTRIBUTOR_ID_ATTRIBUTE_NAME, getContributorId());

		// persist instances
		for (ExclusionInstance instance : fExclusionInstanceList) {
			instance.persistInstanceData(exclusionElement);
		}

		// provide a place for extenders to store their own data
		ICStorageElement extensionElement = exclusionElement.createChild(EXTENSION_DATA_ELEMENT_NAME);

		// call extender to store any extender-specific data
		persistExtendedData(extensionElement);

		// persist nested exclusions
		for (RefreshExclusion exclusion : fNestedExclusions) {
			exclusion.persistData(exclusionElement);
		}
	}

	protected synchronized void persistExtendedData(ICStorageElement extensionElement) {
		// override to provide extension specific behaviour if desired
	}

	/**
	 * Removes an exclusion instance from the list of instances of this exclusion.
	 *
	 * @param exclusionInstance
	 */
	public synchronized void removeExclusionInstance(ExclusionInstance exclusionInstance) {
		fExclusionInstanceList.remove(exclusionInstance);
	}

	/**
	 * Removes the given nested exclusion. The exclusion must be a direct child of this exclusion.
	 *
	 * @param exclusion
	 */
	public synchronized void removeNestedExclusion(RefreshExclusion exclusion) {
		fNestedExclusions.remove(exclusion);
	}

	public synchronized void setContributorId(String id) {
		fContributorId = id;
	}

	public synchronized void setExclusionType(ExclusionType exclusionType) {
		fExclusionType = exclusionType;
	}

	public synchronized void setParentExclusion(RefreshExclusion parent) {
		fParentExclusion = parent;
	}

	/**
	 * Sets the parent resource of this exclusion.
	 *
	 * @param parentResource
	 *            the parent resource to set
	 */
	public synchronized void setParentResource(IResource parentResource) {
		this.fParentResource = parentResource;
	}

	/**
	 * @return true if this exclusion supports exclusion instances
	 */
	public abstract boolean supportsExclusionInstances();

	/**
	 * Tests a given resource to see if this exclusion applies to it.
	 *
	 * @param resource
	 *            the resource to be tested.
	 * @return true if the resource triggers the exclusion, false otherwise (including if this exclusion does
	 *         not apply).
	 */
	public abstract boolean testExclusion(IResource resource);

	/**
	 * Tests this exclusion and recursively test all of its nested exclusions to determine whether this
	 * exclusion should be triggered or not.
	 *
	 * @param resource
	 *            the resource to be tested
	 * @return true if the exclusion is triggered, false otherwise (including if this exclusion does not
	 *         apply)
	 */
	public synchronized boolean testExclusionChain(IResource resource) {
		// first check and see if this exclusion would be triggered in the first place
		boolean currentValue = testExclusion(resource);

		if (currentValue) {
			List<RefreshExclusion> nestedExclusions = getNestedExclusions();
			for (RefreshExclusion exclusion : nestedExclusions) {

				boolean nestedValue = exclusion.testExclusionChain(resource);

				if (nestedValue) {
					// the nested exclusion says to do the opposite of what we originally thought, so negate
					// the current value
					currentValue = (!currentValue);

					// since the first exclusion chain to trump us wins, then, break out of the loop
					break;
				}

			}
		}

		return currentValue;

	}

	/**
	 * Duplicate this refresh exclusion to the given one.
	 * @param destination - the refresh exclusion to be modified
	 * @since 5.4
	 */
	protected void copyTo(RefreshExclusion destination) {
		destination.setContributorId(getContributorId());
		destination.setExclusionType(getExclusionType());
		destination.setParentResource(getParentResource());

		Iterator<RefreshExclusion> iterator = getNestedExclusions().iterator();
		while (iterator.hasNext()) {
			RefreshExclusion nestedExclusion = iterator.next();
			RefreshExclusion clone;
			clone = (RefreshExclusion) nestedExclusion.clone();
			clone.setParentExclusion(destination);
			destination.addNestedExclusion(clone);
		}

		Iterator<ExclusionInstance> exclusionInstances = getExclusionInstances().iterator();

		while (exclusionInstances.hasNext()) {
			ExclusionInstance next = exclusionInstances.next();
			ExclusionInstance newInstance = new ExclusionInstance();
			newInstance.setDisplayString(next.getDisplayString());
			newInstance.setExclusionType(next.getExclusionType());
			newInstance.setParentExclusion(destination);
			newInstance.setResource(next.getResource());
			destination.addExclusionInstance(newInstance);
		}
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#clone()
	 */
	@Override
	public abstract Object clone();
}
