/*******************************************************************************
 * Copyright (c) 2000, 2010 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 API and implementation
 *     Martin Oberhuber (Wind River) - [245937] setLinkLocation() detects non-change
 *     Serge Beauchamp (Freescale Semiconductor) - [229633] Project Path Variable Support
 *     Markus Schorn (Wind River) - [306575] Save snapshot location with project
 *     Broadcom Corporation - build configurations and references
 *******************************************************************************/
package org.eclipse.core.internal.resources;

import java.net.URI;
import java.util.*;
import java.util.Map.Entry;
import org.eclipse.core.filesystem.URIUtil;
import org.eclipse.core.internal.events.BuildCommand;
import org.eclipse.core.internal.utils.FileUtil;
import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.*;

public class ProjectDescription extends ModelObject implements IProjectDescription {
	// constants
	private static final IBuildConfiguration[] EMPTY_BUILD_CONFIG_REFERENCE_ARRAY = new IBuildConfiguration[0];
	private static final ICommand[] EMPTY_COMMAND_ARRAY = new ICommand[0];
	private static final IProject[] EMPTY_PROJECT_ARRAY = new IProject[0];
	private static final String[] EMPTY_STRING_ARRAY = new String[0];
	private static final String EMPTY_STR = ""; //$NON-NLS-1$

	protected static boolean isReading = false;

	//flags to indicate when we are in the middle of reading or writing a
	// workspace description
	//these can be static because only one description can be read at once.
	protected static boolean isWriting = false;
	protected ICommand[] buildSpec = EMPTY_COMMAND_ARRAY;
	protected String comment = EMPTY_STR;

	// Build configuration + References state
	/** Id of the currently active build configuration */
	protected String activeConfiguration = IBuildConfiguration.DEFAULT_CONFIG_NAME;
	/** 
	 * The 'real' build configuration names set on this project. 
	 * This doesn't contain the generated 'default' build configuration with name
	 * {@link IBuildConfiguration#DEFAULT_CONFIG_NAME}
	 * when no build configurations have been defined. 
	 */
	protected String[] configNames = EMPTY_STRING_ARRAY;
	// Static + Dynamic project level references
	protected IProject[] staticRefs = EMPTY_PROJECT_ARRAY;
	protected IProject[] dynamicRefs = EMPTY_PROJECT_ARRAY;
	/** Map from config name in this project -> build configurations in other projects */
	protected HashMap<String, IBuildConfiguration[]> dynamicConfigRefs = new HashMap<String, IBuildConfiguration[]>(1);

	// Cache of the build configurations
	protected volatile IBuildConfiguration[] cachedBuildConfigs;
	// Cached build configuration references. Not persisted.
	protected Map<String, IBuildConfiguration[]> cachedConfigRefs = Collections.synchronizedMap(new HashMap<String, IBuildConfiguration[]>(1));
	// Cached project level references.
	protected volatile IProject[] cachedRefs = null;

	/**
	 * Map of (IPath -> LinkDescription) pairs for each linked resource
	 * in this project, where IPath is the project relative path of the resource.
	 */
	protected HashMap<IPath,LinkDescription> linkDescriptions = null;
	
	/**
	 * Map of (IPath -> LinkedList<FilterDescription>) pairs for each filtered resource
	 * in this project, where IPath is the project relative path of the resource.
	 */
	protected HashMap<IPath,LinkedList<FilterDescription>> filterDescriptions = null;

	/**
	 * Map of (String -> VariableDescription) pairs for each variable in this
	 * project, where String is the name of the variable.
	 */
	protected HashMap<String,VariableDescription> variableDescriptions = null;

	// fields
	protected URI location = null;
	protected String[] natures = EMPTY_STRING_ARRAY;
	protected URI snapshotLocation= null;

	public ProjectDescription() {
		super();
	}

	@SuppressWarnings("unchecked")
	public Object clone() {
		ProjectDescription clone = (ProjectDescription) super.clone();
		//don't want the clone to have access to our internal link locations table or builders
		clone.linkDescriptions = null;
		clone.filterDescriptions = null;
		if (variableDescriptions != null)
			clone.variableDescriptions =  (HashMap<String, VariableDescription>) variableDescriptions.clone();
		clone.buildSpec = getBuildSpec(true);
		clone.dynamicConfigRefs = (HashMap<String, IBuildConfiguration[]>) dynamicConfigRefs.clone();
		clone.cachedConfigRefs = Collections.synchronizedMap(new HashMap<String, IBuildConfiguration[]>(1));
		clone.clearCachedReferences(null);
		return clone;
	}

	/**
	 * Clear cached references for the specified build config name
	 * or all if configName is null.
	 */
	private void clearCachedReferences(String configName)	{
		if (configName == null)
			cachedConfigRefs.clear();
		else
			cachedConfigRefs.remove(configName);
		cachedRefs = null;
	}

	/**
	 * Returns a copy of the given array of build configs with all duplicates removed
	 */
	private IBuildConfiguration[] copyAndRemoveDuplicates(IBuildConfiguration[] values) {
		Set<IBuildConfiguration> set = new LinkedHashSet<IBuildConfiguration>(Arrays.asList(values));
		return set.toArray(new IBuildConfiguration[set.size()]);
	}

	/**
	 * Returns a copy of the given array with all duplicates removed
	 */
    private IProject[] copyAndRemoveDuplicates(IProject[] projects) {
        IProject[] result = new IProject[projects.length];
        int count = 0;
        next: for (int i = 0; i < projects.length; i++) {
                IProject project = projects[i];
                // scan to see if there are any other projects by the same name
                for (int j = 0; j < count; j++)
                        if (project.equals(result[j]))
                                continue next;
                // not found
                result[count++] = project;
        }
        if (count < projects.length) {
                //shrink array
                IProject[] reduced = new IProject[count];
                System.arraycopy(result, 0, reduced, 0, count);
                return reduced;
        }
        return result;
    }

	/**
	 * Helper to turn an array of projects into an array of {@link IBuildConfiguration} to the
	 * projects' active configuration
	 * Order is preserved - the buildConfigs appear for each project in the order
	 * that the projects were specified.
	 * @param projects projects to get the active configuration from
	 * @return collection of build config references
	 */
	private Collection<BuildConfiguration> getBuildConfigReferencesFromProjects(IProject[] projects) {
		List<BuildConfiguration> refs = new ArrayList<BuildConfiguration>(projects.length);
		for (int i = 0; i < projects.length; i++)
			refs.add(new BuildConfiguration(projects[i], null));
		return refs;
	}

	/**
	 * Helper to fetch projects from an array of build configuration references
	 * @param refs
	 * @return List<IProject>
	 */
	private Collection<IProject> getProjectsFromBuildConfigRefs(IBuildConfiguration[] refs) {
		LinkedHashSet<IProject> projects = new LinkedHashSet<IProject>(refs.length);
		for (int i = 0; i < refs.length; i++)
			projects.add(refs[i].getProject());
		return projects;
	}

	public String getActiveBuildConfig() {
		return activeConfiguration;
	}

	/**
	 * Returns the union of the description's static and dynamic project references,
	 * with duplicates omitted. The calculation is optimized by caching the result
	 * Call the configuration based implementation.
	 * @see #getAllBuildConfigReferences(String, boolean)
	 */
	public IProject[] getAllReferences(boolean makeCopy) {
		IProject[] projRefs = cachedRefs;
		if (projRefs == null) {
			IBuildConfiguration[] refs;
			if (hasBuildConfig(activeConfiguration))
				refs = getAllBuildConfigReferences(activeConfiguration, false);
			else if (configNames.length > 0)
				refs = getAllBuildConfigReferences(configNames[0], false);
			else // No build configuration => fall-back to default
				refs = getAllBuildConfigReferences(IBuildConfiguration.DEFAULT_CONFIG_NAME, false);
			Collection<IProject> l = getProjectsFromBuildConfigRefs(refs);
			projRefs = cachedRefs = l.toArray(new IProject[l.size()]);
		}
		//still need to copy the result to prevent tampering with the cache
		return makeCopy ? (IProject[]) projRefs.clone() : projRefs;
	}

	/**
	 * The main entrance point to fetch the full set of Project references.
	 *
	 * Returns the union of all the description's references. Includes static and dynamic 
	 * project level references as well as build configuration references for the configuration
	 * with the given id. 
	 * Duplicates are omitted.  The calculation is optimized by caching the result.
	 * Note that these BuildConfiguration references may have <code>null</code> name.  They must
	 * be resolved using {@link BuildConfiguration#getBuildConfig()} before use.
	 * Returns an empty array if the given configName does not exist in the description.
	 */
	public IBuildConfiguration[] getAllBuildConfigReferences(String configName, boolean makeCopy) {
		if (!hasBuildConfig(configName))
			return EMPTY_BUILD_CONFIG_REFERENCE_ARRAY;
		IBuildConfiguration[] refs = cachedConfigRefs.get(configName);
		if (refs == null) {
			Set<IBuildConfiguration> references = new LinkedHashSet<IBuildConfiguration>();
			IBuildConfiguration[] dynamicBuildConfigs = dynamicConfigRefs.containsKey(configName) ?
														dynamicConfigRefs.get(configName) : EMPTY_BUILD_CONFIG_REFERENCE_ARRAY;
			Collection<BuildConfiguration> dynamic = getBuildConfigReferencesFromProjects(dynamicRefs);
			Collection<BuildConfiguration> statik = getBuildConfigReferencesFromProjects(staticRefs);

			// Combine all references:
			// New build config references (which only come in dynamic form) trump all others.
			references.addAll(Arrays.asList(dynamicBuildConfigs));
			// We preserve the previous order of static project references before dynamic project references
			references.addAll(statik);
			references.addAll(dynamic);
			refs = references.toArray(new IBuildConfiguration[references.size()]);
			cachedConfigRefs.put(configName, refs);
		}
		return makeCopy ? (IBuildConfiguration[]) refs.clone() : refs;
	}

	/**
	 * Used by Project to get the buildConfigs on the description.
	 * @return the project configurations
	 */
	public IBuildConfiguration[] getBuildConfigs(IProject project, boolean makeCopy) {
		IBuildConfiguration[] configs = cachedBuildConfigs;
		// Ensure project is up to date in the cache
		if (configs != null && !project.equals(configs[0].getProject()))
			configs = null;
		if (configs == null) {
			if (configNames.length == 0)
				configs = new IBuildConfiguration[] { new BuildConfiguration(project) };
			else {
				configs = new IBuildConfiguration[configNames.length];
				for (int i = 0; i < configs.length; i++)
					configs[i] = new BuildConfiguration(project, configNames[i]);
			}
			cachedBuildConfigs = configs;
		}
		return makeCopy ? (IBuildConfiguration[])configs.clone() : configs;
	}

	/* (non-Javadoc)
	 * @see IProjectDescription#getBuildConfigReferences(String)
	 */
	public IBuildConfiguration[] getBuildConfigReferences(String configName) {
		return getBuildConfigRefs(configName, true);
	}

	public IBuildConfiguration[] getBuildConfigRefs(String configName, boolean makeCopy) {
		if (!hasBuildConfig(configName) || !dynamicConfigRefs.containsKey(configName))
			return EMPTY_BUILD_CONFIG_REFERENCE_ARRAY;

		return makeCopy ? (IBuildConfiguration[])dynamicConfigRefs.get(configName).clone()
						 						: dynamicConfigRefs.get(configName);
	}

	/**
	 * Returns the build configuration references map
	 * @param makeCopy
	 */
	@SuppressWarnings("unchecked")
	public Map<String, IBuildConfiguration[]> getBuildConfigReferences(boolean makeCopy) {
		return makeCopy ? (Map<String, IBuildConfiguration[]>)dynamicConfigRefs.clone() : dynamicConfigRefs;
	}

	/* (non-Javadoc)
	 * @see IProjectDescription#getBuildSpec()
	 */
	public ICommand[] getBuildSpec() {
		return getBuildSpec(true);
	}

	public ICommand[] getBuildSpec(boolean makeCopy) {
		//thread safety: copy reference in case of concurrent write
		ICommand[] oldCommands = this.buildSpec;
		if (oldCommands == null)
			return EMPTY_COMMAND_ARRAY;
		if (!makeCopy)
			return oldCommands;
		ICommand[] result = new ICommand[oldCommands.length];
		for (int i = 0; i < result.length; i++)
			result[i] = (ICommand) ((BuildCommand) oldCommands[i]).clone();
		return result;
	}

	/* (non-Javadoc)
	 * @see IProjectDescription#getComment()
	 */
	public String getComment() {
		return comment;
	}

	/* (non-Javadoc)
	 * @see IProjectDescription#getDynamicReferences()
	 */
	public IProject[] getDynamicReferences() {
		return getDynamicReferences(true);
	}

	public IProject[] getDynamicReferences(boolean makeCopy) {
		return makeCopy ? (IProject[])dynamicRefs.clone() : dynamicRefs;
	}

	/**
	 * Returns the link location for the given resource name. Returns null if
	 * no such link exists.
	 */
	public URI getLinkLocationURI(IPath aPath) {
		if (linkDescriptions == null)
			return null;
		LinkDescription desc = linkDescriptions.get(aPath);
		return desc == null ? null : desc.getLocationURI();
	}

	/**
	 * Returns the filter for the given resource name. Returns null if
	 * no such filter exists.
	 */
	synchronized public LinkedList<FilterDescription> getFilter(IPath aPath) {
		if (filterDescriptions == null)
			return null;
		return filterDescriptions.get(aPath);
	}

	/**
	 * Returns the map of link descriptions (IPath (project relative path) -> LinkDescription).
	 * Since this method is only used internally, it never creates a copy.
	 * Returns null if the project does not have any linked resources.
	 */
	public HashMap<IPath,LinkDescription> getLinks() {
		return linkDescriptions;
	}

	/**
	 * Returns the map of filter descriptions (IPath (project relative path) -> LinkedList<FilterDescription>).
	 * Since this method is only used internally, it never creates a copy.
	 * Returns null if the project does not have any filtered resources.
	 */
	public HashMap<IPath, LinkedList<FilterDescription>> getFilters() {
		return filterDescriptions;
	}

	/**
	 * Returns the map of variable descriptions (String (variable name) ->
	 * VariableDescription). Since this method is only used internally, it never
	 * creates a copy. Returns null if the project does not have any variables.
	 */
	public HashMap<String,VariableDescription> getVariables() {
		return variableDescriptions;
	}

	/**
	 * @see IProjectDescription#getLocation()
	 * @deprecated
	 */
	public IPath getLocation() {
		if (location == null)
			return null;
		return FileUtil.toPath(location);
	}

	/* (non-Javadoc)
	 * @see IProjectDescription#getLocationURI()
	 */
	public URI getLocationURI() {
		return location;
	}

	/* (non-Javadoc)
	 * @see IProjectDescription#getNatureIds()
	 */
	public String[] getNatureIds() {
		return getNatureIds(true);
	}

	public String[] getNatureIds(boolean makeCopy) {
		if (natures == null)
			return EMPTY_STRING_ARRAY;
		return makeCopy ? (String[]) natures.clone() : natures;
	}

	/* (non-Javadoc)
	 * @see IProjectDescription#getReferencedProjects()
	 */
	public IProject[] getReferencedProjects() {
		return getReferencedProjects(true);
	}

	public IProject[] getReferencedProjects(boolean makeCopy) {
		if (staticRefs == null)
			return EMPTY_PROJECT_ARRAY;
		return makeCopy ? (IProject[]) staticRefs.clone() : staticRefs;
	}

	/** 
	 * Returns the URI to load a resource snapshot from.
	 * May return <code>null</code> if no snapshot is set.
	 * <p>
	 * <strong>EXPERIMENTAL</strong>. This constant 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 Platform Core team.
	 * </p>
	 * @return the snapshot location URI,
	 *   or <code>null</code>.
	 * @see IProject#loadSnapshot(int, URI, IProgressMonitor)
	 * @see #setSnapshotLocationURI(URI)
	 * @since 3.6
	 */
	public URI getSnapshotLocationURI() {
		return snapshotLocation;
	}

	/* (non-Javadoc)
	 * @see IProjectDescription#hasNature(String)
	 */
	public boolean hasNature(String natureID) {
		String[] natureIDs = getNatureIds(false);
		for (int i = 0; i < natureIDs.length; ++i)
			if (natureIDs[i].equals(natureID))
				return true;
		return false;
	}

	/**
	 * Helper method to compare two maps of Configuration Name -> IBuildConfigurationReference[]
	 * @return boolean indicating if there are differences between the two maps
	 */
	private static boolean configRefsHaveChanges(Map<String, IBuildConfiguration[]> m1, Map<String, IBuildConfiguration[]> m2) {
		if (m1.size() != m2.size())
			return true;
		for (Iterator<Entry<String,IBuildConfiguration[]>> it = m1.entrySet().iterator(); it.hasNext();) {
			Entry<String,IBuildConfiguration[]> e = it.next();
			if (!m2.containsKey(e.getKey()))
				return true;
			if (!Arrays.equals(e.getValue(), 
					m2.get(e.getKey())))
				return true;
		}
		return false;
	}

	/**
	 * Internal method to check if the description has a given build configuration.
	 */
	boolean hasBuildConfig(String buildConfigName) {
		Assert.isNotNull(buildConfigName);
		if (configNames.length == 0)
			return IBuildConfiguration.DEFAULT_CONFIG_NAME.equals(buildConfigName);
		for (int i = 0; i < configNames.length; i++)
			if (configNames[i].equals(buildConfigName))
				return true;
		return false;
	}

	/**
	 * Returns true if any private attributes of the description have changed.
	 * Private attributes are those that are not stored in the project description
	 * file (.project).
	 */
	public boolean hasPrivateChanges(ProjectDescription description) {
		if (location == null) {
			if (description.location != null)
				return true;
		} else if (!location.equals(description.location))
			return true;

		if (!Arrays.equals(dynamicRefs, description.dynamicRefs))
			return true;

		// Build Configuration state
		if (!activeConfiguration.equals(description.activeConfiguration))
			return true;
		if (!Arrays.equals(configNames, description.configNames))
			return true;
		// Configuration level references
		if (configRefsHaveChanges(dynamicConfigRefs, description.dynamicConfigRefs))
			return true;

		return false;
	}

	/**
	 * Returns true if any public attributes of the description have changed.
	 * Public attributes are those that are stored in the project description
	 * file (.project).
	 */
	public boolean hasPublicChanges(ProjectDescription description) {
		if (!getName().equals(description.getName()))
			return true;
		if (!comment.equals(description.getComment()))
			return true;
		//don't bother optimizing if the order has changed
		if (!Arrays.equals(buildSpec, description.getBuildSpec(false)))
			return true;
		if (!Arrays.equals(staticRefs, description.getReferencedProjects(false)))
			return true;
		if (!Arrays.equals(natures, description.getNatureIds(false)))
			return true;
		
		HashMap<IPath, LinkedList<FilterDescription>> otherFilters = description.getFilters();
		if ((filterDescriptions == null) && (otherFilters != null))
			return otherFilters != null;
		if ((filterDescriptions != null) && !filterDescriptions.equals(otherFilters))
			return true;

		HashMap<String,VariableDescription> otherVariables = description.getVariables();
		if ((variableDescriptions == null) && (otherVariables != null))
			return true;
		if ((variableDescriptions != null) && !variableDescriptions.equals(otherVariables))
			return true;

		final HashMap<IPath,LinkDescription> otherLinks = description.getLinks();
		if (linkDescriptions != otherLinks) { 
			if (linkDescriptions == null || !linkDescriptions.equals(otherLinks))
				return true;
		}
		
		final URI otherSnapshotLoc= description.getSnapshotLocationURI();
		if (snapshotLocation != otherSnapshotLoc) {
			if (snapshotLocation == null || !snapshotLocation.equals(otherSnapshotLoc))
				return true;
		}
		return false;
	}

	/* (non-Javadoc)
	 * @see IProjectDescription#newCommand()
	 */
	public ICommand newCommand() {
		return new BuildCommand();
	}

	public void setActiveBuildConfig(String configName) {
		Assert.isNotNull(configName);
		if (!configName.equals(activeConfiguration))
			clearCachedReferences(null);
		activeConfiguration = configName;
	}

	/* (non-Javadoc)
	 * @see IProjectDescription#setBuildSpec(ICommand[])
	 */
	public void setBuildSpec(ICommand[] value) {
		Assert.isLegal(value != null);
		//perform a deep copy in case clients perform further changes to the command
		ICommand[] result = new ICommand[value.length];
		for (int i = 0; i < result.length; i++) {
			result[i] = (ICommand) ((BuildCommand) value[i]).clone();
			//copy the reference to any builder instance from the old build spec
			//to preserve builder states if possible.
			for (int j = 0; j < buildSpec.length; j++) {
				if (result[i].equals(buildSpec[j])) {
					((BuildCommand) result[i]).setBuilders(((BuildCommand) buildSpec[j]).getBuilders());
					break;
				}
			}
		}
		buildSpec = result;
	}

	/* (non-Javadoc)
	 * @see IProjectDescription#setComment(String)
	 */
	public void setComment(String value) {
		comment = value;
	}

	/* (non-Javadoc)
	 * @see IProjectDescription#setDynamicReferences(IProject[])
	 */
	public void setDynamicReferences(IProject[] value) {
		Assert.isLegal(value != null);
		dynamicRefs = copyAndRemoveDuplicates(value);
		clearCachedReferences(null);
	}

	public void setBuildConfigReferences(HashMap<String, IBuildConfiguration[]> refs) {
		dynamicConfigRefs = new HashMap<String, IBuildConfiguration[]>(refs);
		clearCachedReferences(null);
	}

	/* (non-Javadoc)
	 * @see IProjectDescription#setDynamicConfigReferences(String, IBuildConfiguration[])
	 */
	public void setBuildConfigReferences(String configName, IBuildConfiguration[] references) {
		Assert.isLegal(configName != null);
		Assert.isLegal(references != null);
		if (!hasBuildConfig(configName))
			return;
		dynamicConfigRefs.put(configName, copyAndRemoveDuplicates(references));
		clearCachedReferences(configName);
	}

	/*
	 * (non-Javadoc)
	 * @see IProjectDescription#setBuildConfigurations(String[])
	 */
	public void setBuildConfigs(String[] names) {
		// Remove references for deleted buildConfigs
		LinkedHashSet<String> buildConfigNames = new LinkedHashSet<String>();

		if (names == null || names.length == 0) {
			configNames = EMPTY_STRING_ARRAY;
			buildConfigNames.add(IBuildConfiguration.DEFAULT_CONFIG_NAME);
		} else {
			// Filter out duplicates
			for (String n : names) {
				Assert.isLegal(n != null);
				buildConfigNames.add(n);
			}

			if (buildConfigNames.size() == 1 && ((buildConfigNames.iterator().next())).equals(IBuildConfiguration.DEFAULT_CONFIG_NAME))
				configNames = EMPTY_STRING_ARRAY;
			else
				configNames = buildConfigNames.toArray(new String[buildConfigNames.size()]);
		}

		// Remove references for deleted buildConfigs
		boolean modified = dynamicConfigRefs.keySet().retainAll(buildConfigNames);
		if (modified)
			clearCachedReferences(null);
		// Clear the cached IBuildConfiguration[]
		cachedBuildConfigs = null;
	}

	/**
	 * Sets the map of link descriptions (String name -> LinkDescription).
	 * Since this method is only used internally, it never creates a copy. May
	 * pass null if this project does not have any linked resources
	 */
	public void setLinkDescriptions(HashMap<IPath,LinkDescription> linkDescriptions) {
		this.linkDescriptions = linkDescriptions;
	}

	/**
	 * Sets the map of filter descriptions (String name -> LinkedList<LinkDescription>).
	 * Since this method is only used internally, it never creates a copy. May
	 * pass null if this project does not have any filtered resources
	 */
	public void setFilterDescriptions(HashMap<IPath,LinkedList<FilterDescription>> filterDescriptions) {
		this.filterDescriptions = filterDescriptions;
	}

	/**
	 * Sets the map of variable descriptions (String name ->
	 * VariableDescription). Since this method is only used internally, it never
	 * creates a copy. May pass null if this project does not have any variables
	 */
	public void setVariableDescriptions(HashMap<String,VariableDescription> variableDescriptions) {
		this.variableDescriptions = variableDescriptions;
	}

	/**
	 * Sets the description of a link. Setting to a description of null will
	 * remove the link from the project description.
	 * @return <code>true</code> if the description was actually changed,
	 *     <code>false</code> otherwise.
	 * @since 3.5 returns boolean (was void before)
	 */
	@SuppressWarnings("unchecked")
	public boolean setLinkLocation(IPath path, LinkDescription description) {
		HashMap<IPath,LinkDescription> tempMap = linkDescriptions;
		if (description != null) {
			//addition or modification
			if (tempMap == null)
				tempMap = new HashMap<IPath,LinkDescription>(10);
			else 
				//copy on write to protect against concurrent read
				tempMap = (HashMap<IPath, LinkDescription>) tempMap.clone();
			Object oldValue = tempMap.put(path, description);
			if (oldValue!=null && description.equals(oldValue)) {
				//not actually changed anything
				return false;
			}
			linkDescriptions = tempMap;
		} else {
			//removal
			if (tempMap == null)
				return false;
			//copy on write to protect against concurrent access
			HashMap<IPath,LinkDescription> newMap = (HashMap<IPath,LinkDescription>) tempMap.clone();
			Object oldValue = newMap.remove(path);
			if (oldValue == null) {
				//not actually changed anything
				return false;
			}
			linkDescriptions = newMap.size() == 0 ? null : newMap;
		}
		return true;
	}

	/**
	 * Add the description of a filter. Setting to a description of null will
	 * remove the filter from the project description.
	 */
	synchronized public void addFilter(IPath path, FilterDescription description) {
		Assert.isNotNull(description);
		if (filterDescriptions == null)
			filterDescriptions = new HashMap<IPath,LinkedList<FilterDescription>>(10);
		LinkedList<FilterDescription> descList = filterDescriptions.get(path);
		if (descList == null) {
			descList = new LinkedList<FilterDescription>();
			filterDescriptions.put(path, descList);
		}
		descList.add(description);
	}
	
	/**
	 * Add the description of a filter. Setting to a description of null will
	 * remove the filter from the project description.
	 */
	synchronized public void removeFilter(IPath path, FilterDescription description) {
		if (filterDescriptions != null) {
			LinkedList<FilterDescription> descList = filterDescriptions.get(path);
			if (descList != null) {
				descList.remove(description);
				if (descList.size() == 0) {
					filterDescriptions.remove(path);
					if (filterDescriptions.size() == 0)
						filterDescriptions = null;
				}
			}
		}
	}

	/**
	 * Sets the description of a variable. Setting to a description of null will
	 * remove the variable from the project description.
	 * @return <code>true</code> if the description was actually changed,
	 *     <code>false</code> otherwise.
	 * @since 3.5
	 */
	@SuppressWarnings("unchecked")
	public boolean setVariableDescription(String name,
			VariableDescription description) {
		HashMap<String,VariableDescription> tempMap = variableDescriptions;
		if (description != null) {
			// addition or modification
			if (tempMap == null)
				tempMap = new HashMap<String,VariableDescription>(10);
			else
				// copy on write to protect against concurrent read
				tempMap = (HashMap<String,VariableDescription>) tempMap.clone();
			Object oldValue = tempMap.put(name, description);
			if (oldValue!=null && description.equals(oldValue)) {
				//not actually changed anything
				return false;
			}
			variableDescriptions = tempMap;
		} else {
			// removal
			if (tempMap == null)
				return false;
			// copy on write to protect against concurrent access
			HashMap<String,VariableDescription> newMap = (HashMap<String,VariableDescription>) tempMap.clone();
			Object oldValue = newMap.remove(name);
			if (oldValue == null) {
				//not actually changed anything
				return false;
			}
			variableDescriptions = newMap.size() == 0 ? null : newMap;
		}
		return true;
	}

	/**
	 * set the filters for a given resource. Setting to a description of null will
	 * remove the filter from the project description.
	 * @return <code>true</code> if the description was actually changed,
	 *     <code>false</code> otherwise.
	 */
	synchronized public boolean setFilters(IPath path, LinkedList<FilterDescription> descriptions) {
		if (descriptions != null) {
			// addition
			if (filterDescriptions == null)
				filterDescriptions = new HashMap<IPath,LinkedList<FilterDescription>>(10);
			Object oldValue = filterDescriptions.put(path, descriptions);
			if (oldValue!=null && descriptions.equals(oldValue)) {
				//not actually changed anything
				return false;
			}
		}
		else { 
			// removal
			if (filterDescriptions == null)
				return false;
			
			Object oldValue = filterDescriptions.remove(path);
			if (oldValue == null) {
				//not actually changed anything
				return false;
			}
			if (filterDescriptions.size() == 0)
				filterDescriptions = null;
		}
		return true;
	}

	/* (non-Javadoc)
	 * @see IProjectDescription#setLocation(IPath)
	 */
	public void setLocation(IPath path) {
		this.location = path == null ? null : URIUtil.toURI(path);
	}

	public void setLocationURI(URI location) {
		this.location = location;
	}

	/* (non-Javadoc)
	 * @see IProjectDescription#setName(String)
	 */
	public void setName(String value) {
		super.setName(value);
	}

	/* (non-Javadoc)
	 * @see IProjectDescription#setNatureIds(String[])
	 */
	public void setNatureIds(String[] value) {
		natures = value.clone();
	}

	/* (non-Javadoc)
	 * @see IProjectDescription#setReferencedProjects(IProject[])
	 */
	public void setReferencedProjects(IProject[] value) {
		Assert.isLegal(value != null);
		staticRefs = copyAndRemoveDuplicates(value);
		clearCachedReferences(null);
	}

	/**
	 * Sets the location URI for a project snapshot that may be
	 * loaded automatically when the project is created in a workspace.
	 * <p>
	 * <strong>EXPERIMENTAL</strong>. This method 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 Platform Core team.
	 * </p>
	 * @param snapshotLocation the location URI or
	 *    <code>null</code> to clear the setting 
	 * @see IProject#loadSnapshot(int, URI, IProgressMonitor)
	 * @see #getSnapshotLocationURI()
	 * @since 3.6 
	 */
	public void setSnapshotLocationURI(URI snapshotLocation) {
		this.snapshotLocation = snapshotLocation;
	}

	public URI getGroupLocationURI(IPath projectRelativePath) {
		return LinkDescription.VIRTUAL_LOCATION;
	}

	/**
	 * Updates the dynamic build configuration and reference state to that of the passed in 
	 * description.
	 * Copies in:
	 * <ul>
	 * <li>Active configuration name</li>
	 * <li>Dynamic Project References</li>
	 * <li>Build configurations list</li>
	 * <li>Build Configuration References</li>
	 * </ul>
	 * @param description Project description to copy dynamic state from
	 * @return boolean indicating if anything changed requing re-calculation of WS build order
	 */
	public boolean updateDynamicState(ProjectDescription description) {
		boolean changed = false;
		if (!activeConfiguration.equals(description.activeConfiguration)) {
			changed = true;
			activeConfiguration = description.activeConfiguration;
		}
		if (!Arrays.equals(dynamicRefs, description.dynamicRefs)) {
			changed = true;
			setDynamicReferences(description.dynamicRefs);
		}
		if (!Arrays.equals(configNames, description.configNames)) {
			changed = true;
			setBuildConfigs(description.configNames);
		}
		if (configRefsHaveChanges(dynamicConfigRefs, description.dynamicConfigRefs)) {
			changed = true;
			dynamicConfigRefs = new HashMap<String, IBuildConfiguration[]>(description.dynamicConfigRefs);
		}
		if (changed)
			clearCachedReferences(null);
		return changed;
	}
}
