/*******************************************************************************
 * Copyright (c) 2000, 2011 QNX Software Systems 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:
 *     QNX Software Systems - Initial API and implementation
 *     Anton Leherbauer (Wind River Systems)
 *******************************************************************************/
package org.eclipse.cdt.internal.core.model;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.cdt.core.CCProjectNature;
import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.CProjectNature;
import org.eclipse.cdt.core.IBinaryParser;
import org.eclipse.cdt.core.IBinaryParser.IBinaryArchive;
import org.eclipse.cdt.core.IBinaryParser.IBinaryFile;
import org.eclipse.cdt.core.IBinaryParser.IBinaryObject;
import org.eclipse.cdt.core.model.CModelException;
import org.eclipse.cdt.core.model.CoreModel;
import org.eclipse.cdt.core.model.CoreModelUtil;
import org.eclipse.cdt.core.model.IArchiveContainer;
import org.eclipse.cdt.core.model.IBinaryContainer;
import org.eclipse.cdt.core.model.ICContainer;
import org.eclipse.cdt.core.model.ICElement;
import org.eclipse.cdt.core.model.ICModelStatusConstants;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.cdt.core.model.IIncludeEntry;
import org.eclipse.cdt.core.model.IIncludeReference;
import org.eclipse.cdt.core.model.ILibraryEntry;
import org.eclipse.cdt.core.model.ILibraryReference;
import org.eclipse.cdt.core.model.IOutputEntry;
import org.eclipse.cdt.core.model.IPathEntry;
import org.eclipse.cdt.core.model.ISourceEntry;
import org.eclipse.cdt.core.model.ISourceRoot;
import org.eclipse.cdt.core.settings.model.CSourceEntry;
import org.eclipse.cdt.core.settings.model.ICConfigurationDescription;
import org.eclipse.cdt.core.settings.model.ICProjectDescription;
import org.eclipse.cdt.core.settings.model.ICSourceEntry;
import org.eclipse.cdt.internal.core.settings.model.CProjectDescriptionManager;
import org.eclipse.cdt.internal.core.util.MementoTokenizer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ProjectScope;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Preferences;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.IScopeContext;
import org.osgi.service.prefs.BackingStoreException;

public class CProject extends Openable implements ICProject {

	private static final String CUSTOM_DEFAULT_OPTION_VALUE = "#\r\n\r#custom-non-empty-default-value#\r\n\r#"; //$NON-NLS-1$

	public CProject(ICElement parent, IProject project) {
		super(parent, project, ICElement.C_PROJECT);
	}

	@Override
	public IBinaryContainer getBinaryContainer() throws CModelException {
		return ((CProjectInfo) getElementInfo()).getBinaryContainer();
	}

	@Override
	public IArchiveContainer getArchiveContainer() throws CModelException {
		return ((CProjectInfo) getElementInfo()).getArchiveContainer();
	}

	@Override
	public IProject getProject() {
		return getUnderlyingResource().getProject();
	}

	@Override
	public ICElement findElement(IPath path) throws CModelException {
		ICElement celem = null;
		if (path.isAbsolute()) {
			celem = CModelManager.getDefault().create(path);
		} else {
			IProject project = getProject();
			if (project != null) {
				IPath p = project.getFullPath().append(path);
				celem = CModelManager.getDefault().create(p);
			}
		}
		if (celem == null) {
			CModelStatus status = new CModelStatus(ICModelStatusConstants.INVALID_PATH, path);
			throw new CModelException(status);
		}
		return celem;
	}

	public static boolean hasCNature(IProject p) {
		try {
			return p.hasNature(CProjectNature.C_NATURE_ID);
		} catch (CoreException e) {
			//throws exception if the project is not open.
		}
		return false;
	}

	public static boolean hasCCNature(IProject p) {
		try {
			return p.hasNature(CCProjectNature.CC_NATURE_ID);
		} catch (CoreException e) {
			//throws exception if the project is not open.
		}
		return false;
	}

	private boolean isCProject() {
		return hasCNature(getProject()) || hasCCNature(getProject());
	}

	/**
	 * Returns true if this handle represents the same C project
	 * as the given handle. Two handles represent the same
	 * project if they are identical or if they represent a project with
	 * the same underlying resource and occurrence counts.
	 *
	 * @see CElement#equals(Object)
	 */
	@Override
	public boolean equals(Object o) {
		if (this == o)
			return true;

		if (!(o instanceof CProject))
			return false;

		CProject other = (CProject) o;
		return getProject().equals(other.getProject());
	}

	@Override
	protected CElementInfo createElementInfo() {
		return new CProjectInfo(this);
	}

	// CHECKPOINT: CProjects will return the hash code of their underlying IProject
	@Override
	public int hashCode() {
		return getProject().hashCode();
	}

	@Override
	public IIncludeReference[] getIncludeReferences() throws CModelException {
		CProjectInfo pinfo = (CProjectInfo)CModelManager.getDefault().peekAtInfo(this);
		IIncludeReference[] incRefs = null;
		if (pinfo != null) {
			incRefs = pinfo.incReferences;
		}
		if (incRefs == null) {
			IPathEntry[] entries = getResolvedPathEntries();
			ArrayList<IncludeReference> list = new ArrayList<IncludeReference>(entries.length);
			for (IPathEntry entrie : entries) {
				if (entrie.getEntryKind() == IPathEntry.CDT_INCLUDE) {
					IIncludeEntry entry = (IIncludeEntry) entrie;
					list.add(new IncludeReference(this, entry));
				}
			}
			incRefs = list.toArray(new IIncludeReference[0]);
			if (pinfo != null) {
				pinfo.incReferences = incRefs;
			}
		}
		return incRefs;
	}

	@Override
	public ILibraryReference[] getLibraryReferences() throws CModelException {
		CProjectInfo pinfo = (CProjectInfo)CModelManager.getDefault().peekAtInfo(this);
		ILibraryReference[] libRefs = null;
		if (pinfo != null) {
			libRefs = pinfo.libReferences;
		}

		if (libRefs == null) {
			BinaryParserConfig[] binConfigs = CModelManager.getDefault().getBinaryParser(getProject());
			IPathEntry[] entries = getResolvedPathEntries();
			ArrayList<ILibraryReference> list = new ArrayList<ILibraryReference>(entries.length);
			for (IPathEntry entrie : entries) {
				if (entrie.getEntryKind() == IPathEntry.CDT_LIBRARY) {
					ILibraryEntry entry = (ILibraryEntry) entrie;
					ILibraryReference lib = getLibraryReference(this, binConfigs, entry);
					if (lib != null) {
						list.add(lib);
					}
				}
			}
			libRefs = list.toArray(new ILibraryReference[0]);
			if (pinfo != null) {
				pinfo.libReferences = libRefs;
			}
		}
		return libRefs;
	}

	private static ILibraryReference getLibraryReference(ICProject cproject, BinaryParserConfig[] binConfigs, ILibraryEntry entry) {
		if (binConfigs == null) {
			binConfigs = CModelManager.getDefault().getBinaryParser(cproject.getProject());
		}
		ILibraryReference lib = null;
		if (binConfigs != null) {
			for (BinaryParserConfig binConfig : binConfigs) {
				IBinaryFile bin;
				try {
					IBinaryParser parser = binConfig.getBinaryParser();
					bin = parser.getBinary(entry.getFullLibraryPath());
					if (bin != null) {
						if (bin.getType() == IBinaryFile.ARCHIVE) {
							lib = new LibraryReferenceArchive(cproject, entry, (IBinaryArchive)bin);
						} else if (bin instanceof IBinaryObject){
							lib = new LibraryReferenceShared(cproject, entry, (IBinaryObject)bin);
						}
						break;
					}
				} catch (IOException e) {
				} catch (CoreException e) {
				}
			}
		}
		if (lib == null) {
			lib = new LibraryReference(cproject, entry);
		}
		return lib;
	}

	/**
	 * @see ICProject#getRequiredProjectNames()
	 */
	@Override
	public String[] getRequiredProjectNames() throws CModelException {
		return projectPrerequisites(getResolvedPathEntries());
	}

	public String[] projectPrerequisites(IPathEntry[] entries) throws CModelException {
		return PathEntryManager.getDefault().projectPrerequisites(entries);
	}

	/**
	 * @see org.eclipse.cdt.core.model.ICProject#getOption(String, boolean)
	 */
	@Override
	public String getOption(String optionName, boolean inheritCCoreOptions) {
		if (CModelManager.OptionNames.contains(optionName)) {
			IEclipsePreferences preferences = getPreferences();
			final String cCoreDefault= inheritCCoreOptions ? CCorePlugin.getOption(optionName) : null;
			if (preferences == null) {
				return cCoreDefault;
			}
			String value= preferences.get(optionName, cCoreDefault).trim();
			return value == null ? null : value.trim();
		}

		return null;
	}

	/**
	 * @see org.eclipse.cdt.core.model.ICProject#getOptions(boolean)
	 */
	@Override
	public Map<String, String> getOptions(boolean inheritCCoreOptions) {
		// initialize to the defaults from CCorePlugin options pool
		Map<String, String> options= inheritCCoreOptions ? CCorePlugin.getOptions() : new HashMap<String, String>(5);

		IEclipsePreferences preferences = getPreferences();
		if (preferences == null)
			return options;
		HashSet<String> optionNames= CModelManager.OptionNames;

		// create project options
		try {
			String[] propertyNames= preferences.keys();
			for (String propertyName : propertyNames) {
				String value= preferences.get(propertyName, null);
				if (value != null && optionNames.contains(propertyName)){
					options.put(propertyName, value.trim());
				}
			}
		} catch (BackingStoreException e) {
			// ignore silently
		}
		return options;
	}

	@Override
	public void setOption(String optionName, String optionValue) {
		if (!CModelManager.OptionNames.contains(optionName))
			return; // unrecognized option

		IEclipsePreferences projectPreferences= getPreferences();
		if (optionValue == null) {
			// remove preference
			projectPreferences.remove(optionName);
		} else {
			projectPreferences.put(optionName, optionValue);
		}

		// Dump changes
		try {
			projectPreferences.flush();
		} catch (BackingStoreException e) {
			// problem with pref store - quietly ignore
		}
	}

	@Override
	public void setOptions(Map<String, String> newOptions) {
		Preferences preferences = new Preferences();
		setPreferences(preferences); // always reset (26255)

		if (newOptions != null) {
			for (Map.Entry<String, String> e : newOptions.entrySet()) {
				String key = e.getKey();
				if (!CModelManager.OptionNames.contains(key))
					continue; // unrecognized option

				// no filtering for encoding (custom encoding for project is allowed)
				String value = e.getValue();
				preferences.setDefault(key, CUSTOM_DEFAULT_OPTION_VALUE); // empty string isn't the default (26251)
				preferences.setValue(key, value);
			}
		}

		// persist options
		savePreferences(preferences);
	}

	/**
	 * Returns the project custom preference pool.
	 * Project preferences may include custom encoding.
	 * @return IEclipsePreferences or <code>null</code> if the project
	 * 	does not have a C nature.
	 */
	private IEclipsePreferences getPreferences() {
		if (!(isCProject())) {
			return null;
		}
		IScopeContext context= new ProjectScope(getProject());
		final IEclipsePreferences preferences= context.getNode(CCorePlugin.PLUGIN_ID);
		return preferences;
	}

	/**
	 * Save project custom preferences to persistent properties
	 */
	private void savePreferences(Preferences preferences) {
		if (preferences == null)
			return;
		if (!isCProject()) {
			return; // ignore
		}
		Iterator<String> iter = CModelManager.OptionNames.iterator();

		while (iter.hasNext()) {
			String qualifiedName = iter.next();
			String dequalifiedName = qualifiedName.substring(CCorePlugin.PLUGIN_ID.length() + 1);
			String value = null;

			try {
				value = preferences.getString(qualifiedName);

				if (value != null && !value.equals(preferences.getDefaultString(qualifiedName))) {
					resource.setPersistentProperty(new QualifiedName(CCorePlugin.PLUGIN_ID, dequalifiedName), value);
				} else {
					resource.setPersistentProperty(new QualifiedName(CCorePlugin.PLUGIN_ID, dequalifiedName), null);
				}
			} catch (CoreException e) {
			}
		}
	}

	/**
	 * Sets cached preferences, no preferences are saved, only info is updated
	 */
	private void setPreferences(Preferences preferences) {
		if (!isCProject()) {
			return; // ignore
		}
		// Do nothing
	}

	@Override
	public IPathEntry[] getResolvedPathEntries() throws CModelException {
		return CoreModel.getResolvedPathEntries(this);
	}

	@Override
	public IPathEntry[] getRawPathEntries() throws CModelException {
		return CoreModel.getRawPathEntries(this);
	}

	@Override
	public void setRawPathEntries(IPathEntry[] newEntries, IProgressMonitor monitor) throws CModelException {
		CoreModel.setRawPathEntries(this, newEntries, monitor);
	}

	@Override
	public ISourceRoot getSourceRoot(ISourceEntry entry) throws CModelException {
		return getSourceRoot(new CSourceEntry(entry.getPath(), entry.getExclusionPatterns(), 0));
	}

	public ISourceRoot getSourceRoot(ICSourceEntry entry) throws CModelException {
		IPath p = getPath();
		IPath sp = entry.getFullPath();
		if (p.isPrefixOf(sp)) {
			int count = sp.matchingFirstSegments(p);
			sp = sp.removeFirstSegments(count);
			IResource res = null;
			if (sp.isEmpty()) {
				res = getProject();
			} else {
				res = getProject().findMember(sp);
			}
			if (res != null) {
				return new SourceRoot(this, res, entry);
			}
		}
		return null;
	}

	@Override
	public ISourceRoot findSourceRoot(IResource res) {
	    try {
			ISourceRoot[] roots = getAllSourceRoots();
			for (ISourceRoot root : roots) {
				if (root.isOnSourceEntry(res)) {
					return root;
				}
			}
	    } catch (CModelException e) {
	    }
		return null;
	}

	@Override
	public ISourceRoot findSourceRoot(IPath path) {
	    try {
			ISourceRoot[] roots = getAllSourceRoots();
			for (ISourceRoot root : roots) {
			    if (root.getPath().equals(path)) {
					return root;
				}
			}
	    } catch (CModelException e) {
	    }
		return null;
	}

	@Override
	public ISourceRoot[] getSourceRoots() throws CModelException {
		Object[] children = getChildren();
		ArrayList<ISourceRoot> result = new ArrayList<ISourceRoot>(children.length);
		for (Object element : children) {
			if (element instanceof ISourceRoot) {
				result.add((ISourceRoot) element);
			}
        }
		return result.toArray(new ISourceRoot[result.size()]);
	}

	/**
	 * Returns all source roots.
	 *
	 * @return all source roots
	 * @throws CModelException
	 */
	@Override
	public ISourceRoot[] getAllSourceRoots() throws CModelException {
		CProjectInfo pinfo = (CProjectInfo)CModelManager.getDefault().peekAtInfo(this);
		ISourceRoot[] roots = null;
		if (pinfo != null) {
			if (pinfo.sourceRoots != null) {
				roots = pinfo.sourceRoots;
			} else {
				List<ISourceRoot> list = computeSourceRoots();
				roots = pinfo.sourceRoots = list.toArray(new ISourceRoot[list.size()]);
			}
		} else {
			List<ISourceRoot> list = computeSourceRoots();
			roots = list.toArray(new ISourceRoot[list.size()]);
		}
		return roots;
	}

	@Override
	public IOutputEntry[] getOutputEntries() throws CModelException {
		CProjectInfo pinfo = (CProjectInfo) CModelManager.getDefault().peekAtInfo(this);
		IOutputEntry[] outs = null;
		if (pinfo != null) {
			if (pinfo.outputEntries != null) {
				outs = pinfo.outputEntries;
			} else {
				IPathEntry[] entries = getResolvedPathEntries();
				outs = pinfo.outputEntries = getOutputEntries(entries);
			}
		} else {
			IPathEntry[] entries = getResolvedPathEntries();
			outs = getOutputEntries(entries);
		}
		return outs;
	}

	public IOutputEntry[] getOutputEntries(IPathEntry[] entries) throws CModelException {
		ArrayList<IPathEntry> list = new ArrayList<IPathEntry>(entries.length);
		for (IPathEntry entrie : entries) {
			if (entrie.getEntryKind() == IPathEntry .CDT_OUTPUT) {
				list.add(entrie);
			}
		}
		IOutputEntry[] outputs = new IOutputEntry[list.size()];
		list.toArray(outputs);
		return outputs;
	}

	@Override
	public boolean isOnOutputEntry(IResource resource) {
		IPath path = resource.getFullPath();

		// ensure that folders are only excluded if all of their children are excluded
		if (resource.getType() == IResource.FOLDER || resource.getType() == IResource.PROJECT) {
			path = path.append("*"); //$NON-NLS-1$
		}

		try {
			IOutputEntry[] entries = getOutputEntries();
			for (IOutputEntry entrie : entries) {
				boolean on = isOnOutputEntry(entrie, path);
				if (on) {
					return on;
				}
			}
		} catch (CModelException e) {
			//
		}
		return false;
	}

	private boolean isOnOutputEntry(IOutputEntry entry, IPath path) {
		if (entry.getPath().isPrefixOf(path) && !CoreModelUtil.isExcluded(path, entry.fullExclusionPatternChars())) {
			return true;
		}
		return false;
	}

	@Override
	protected boolean buildStructure(OpenableInfo info, IProgressMonitor pm,
			Map<ICElement, CElementInfo> newElements, IResource underlyingResource)
			throws CModelException {
		boolean validInfo = false;
		try {
			IResource res = getResource();
			if (res != null && res.isAccessible()) {
				validInfo = computeChildren(info, res);
			} else {
				throw newNotPresentException();
			}
		} finally {
			if (!validInfo) {
				CModelManager.getDefault().removeInfo(this);
			}
		}
		return validInfo;
	}

	protected List<ISourceRoot> computeSourceRoots() throws CModelException {
		//IPathEntry[] entries = getResolvedPathEntries();
		ICSourceEntry[] entries = null;
		ICProjectDescription des = CProjectDescriptionManager.getInstance().getProjectDescription(getProject(), false);
		if(des != null){
			ICConfigurationDescription cfg = des.getDefaultSettingConfiguration();
			if(cfg != null)
				entries = cfg.getResolvedSourceEntries();
		}

		if(entries != null){
			ArrayList<ISourceRoot> list = new ArrayList<ISourceRoot>(entries.length);
			for (ICSourceEntry sourceEntry : entries) {
				ISourceRoot root = getSourceRoot(sourceEntry);
					if (root != null) {
						list.add(root);
					}
			}
			return list;
		}
		return new ArrayList<ISourceRoot>(0);
	}

	protected boolean computeChildren(OpenableInfo info, IResource res) throws CModelException {
		List<ISourceRoot> sourceRoots = computeSourceRoots();
		List<ICContainer> children = new ArrayList<ICContainer>(sourceRoots.size());
		children.addAll(sourceRoots);

		boolean projectIsSourceRoot = false;
		for (ISourceRoot sourceRoot : sourceRoots) {
			if (sourceRoot.getResource().equals(getProject())) {
				projectIsSourceRoot = true;
				break;
			}
		}

		// Now look for output folders
		try {
			IResource[] resources = getProject().members();
			for (IResource child : resources) {
				if (child.getType() == IResource.FOLDER) {
					boolean found = false;
					for (ISourceRoot sourceRoot : sourceRoots) {
						if (sourceRoot.isOnSourceEntry(child)) {
							found = true;
							break;
						}
					}

					// Not in source folder, check if it's a container on output entry
					// Also make sure I'm not a source root since my SourceRoot object would
					// have already added this.
					if (!found && isOnOutputEntry(child) && !projectIsSourceRoot)
						children.add(new CContainer(this, child));
				}
			}
		} catch (CoreException e) {
			// ignore
		}

		info.setChildren(children);
		if (info instanceof CProjectInfo) {
			CProjectInfo pinfo = (CProjectInfo)info;
			pinfo.sourceRoots= sourceRoots.toArray(new ISourceRoot[sourceRoots.size()]);
			pinfo.setNonCResources(null);
		}
		return true;
	}

	@Override
	public boolean isOnSourceRoot(ICElement element) {
		try {
			ISourceRoot[] roots = getSourceRoots();
			for (ISourceRoot root : roots) {
				if (root.isOnSourceEntry(element)) {
					return true;
				}
			}
		} catch (CModelException e) {
			// ..
		}
		return false;
	}

	@Override
	public boolean isOnSourceRoot(IResource resource) {
		try {
			ISourceRoot[] roots = getSourceRoots();
			for (ISourceRoot root : roots) {
				if (root.isOnSourceEntry(resource)) {
					return true;
				}
			}
		} catch (CModelException e) {
			//
		}
		return false;
	}

	@Override
	public boolean exists() {
		if (!isCProject()) {
			return false;
		}
		return true;
	}

	@Override
	public Object[] getNonCResources() throws CModelException {
		return ((CProjectInfo) getElementInfo()).getNonCResources(getResource());
	}

	@Override
	protected void closing(Object info) throws CModelException {
		if (info instanceof CProjectInfo) {
			CModelManager.getDefault().removeBinaryRunner(this);
			CProjectInfo pinfo = (CProjectInfo)info;
			if (pinfo.vBin != null) {
				pinfo.vBin.close();
			}
			if (pinfo.vLib != null) {
				pinfo.vLib.close();
			}
			pinfo.resetCaches();
		}
		super.closing(info);
	}

	/**
	 * Resets this project's caches
	 */
	public void resetCaches() {
		CProjectInfo pinfo = (CProjectInfo) CModelManager.getDefault().peekAtInfo(this);
		if (pinfo != null){
			pinfo.resetCaches();
		}
	}

	@Override
	public ICElement getHandleFromMemento(String token, MementoTokenizer memento) {
		switch (token.charAt(0)) {
		case CEM_SOURCEROOT:
			IPath rootPath = Path.EMPTY;
			token = null;
			while (memento.hasMoreTokens()) {
				token = memento.nextToken();
				char firstChar = token.charAt(0);
				if (firstChar != CEM_SOURCEFOLDER && firstChar != CEM_TRANSLATIONUNIT) {
					rootPath= rootPath.append(token);
					token= null;
				} else {
					break;
				}
			}
			if (!rootPath.isAbsolute()) {
				rootPath= getProject().getFullPath().append(rootPath);
			}
			CElement root = (CElement)findSourceRoot(rootPath);
			if (root != null) {
				if (token != null) {
					return root.getHandleFromMemento(token, memento);
				} else {
					return root.getHandleFromMemento(memento);
				}
			}
			break;
		case CEM_TRANSLATIONUNIT:
			if (!memento.hasMoreTokens()) return this;
			String tuName = memento.nextToken();
			final IPath path= Path.fromPortableString(tuName);
			CElement tu= null;
			if (!path.isAbsolute()) {
				final IProject project= getProject();
				if (project != null) {
					IResource resource= project.findMember(path);
					if (resource != null && resource.getType() == IResource.FILE) {
						final IFile file= (IFile) resource;
						tu= (CElement) CModelManager.getDefault().create(file, this);
						if (tu == null) {
							String contentTypeId= CoreModel.getRegistedContentTypeId(project, file.getName());
							if (contentTypeId != null) {
								tu= new TranslationUnit(this, file, contentTypeId);
							}
						}
					}
				}
			} else {
				tu= (CElement) CoreModel.getDefault().createTranslationUnitFrom(this, path);
			}
			if (tu != null) {
				return tu.getHandleFromMemento(memento);
			}
			break;
		}
		return null;
	}

	@Override
	protected char getHandleMementoDelimiter() {
		return CEM_CPROJECT;
	}
}
