/*******************************************************************************
 * Copyright (c) 2008, 2011 Broadcom 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:
 *     James Blackburn (Broadcom Corp.)
 *******************************************************************************/

package org.eclipse.cdt.internal.core.settings.model;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.settings.model.ICProjectDescription;
import org.eclipse.cdt.internal.core.CConfigBasedDescriptorManager;
import org.eclipse.cdt.internal.core.XmlUtil;
import org.eclipse.cdt.internal.core.settings.model.ICProjectDescriptionStorageType.CProjectDescriptionStorageTypeProxy;
import org.eclipse.cdt.internal.core.settings.model.xml.XmlProjectDescriptionStorage;
import org.eclipse.cdt.internal.core.settings.model.xml2.XmlProjectDescriptionStorage2;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileInfo;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourceAttributes;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.osgi.framework.Version;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.ProcessingInstruction;

/**
 * Class that marshals creation of AbstractCProjectDescriptionStorages
 * for a given project.
 *
 * Persist Storage type ID in the .cproject file, and provides backwards compatibility
 * for existing project descriptions which don't encode the storage type in the project
 * description.
 */
public class CProjectDescriptionStorageManager {

	/* Extension point data */
	/** CProjectDescriptionStorage extension point ID */
	private static final String CPROJ_DESC_STORAGE_EXT_ID = "CProjectDescriptionStorage"; //$NON-NLS-1$
	/** storage type element ID */
	private static final String CPROJ_STORAGE_TYPE = "CProjectStorageType"; //$NON-NLS-1$

	// TODO provide some UI to select this
	/** Default project description Storage type */
	private static final String DEFAULT_STORAGE_TYPE =  XmlProjectDescriptionStorage.STORAGE_TYPE_ID; //  XmlProjectDescriptionStorage2.STORAGE_TYPE_ID;
	/** Default project description Storage version */
	private static final Version DEFAULT_STORAGE_VERSION =   XmlProjectDescriptionStorage.STORAGE_DESCRIPTION_VERSION; // XmlProjectDescriptionStorage2.STORAGE_DESCRIPTION_VERSION;

	/** Map of StorageType ID -> List of StorageTypes */
	private volatile Map<String, List<CProjectDescriptionStorageTypeProxy>> storageTypeMap;
	/** Map from IProject -> AbstractCProjectDescriptionStorage which is responsible for (de)serializing the project */
	private ConcurrentHashMap<IProject, AbstractCProjectDescriptionStorage> fDescriptionStorageMap = new ConcurrentHashMap<IProject, AbstractCProjectDescriptionStorage>();


	private volatile static CProjectDescriptionStorageManager instance;

	private CProjectDescriptionStorageManager() {}

	public static CProjectDescriptionStorageManager getInstance() {
		if (instance == null) {
			synchronized (CProjectDescriptionStorageManager.class) {
				if (instance == null)
					instance = new CProjectDescriptionStorageManager();
			}
		}
		return instance;
	}


	/**
	 * Return a AbstractCProjectDescriptionStorage for a particular project or
	 * null if none were found and the default storage type wasn't found
	 * @param project
	 * @return project description storage or null
	 */
	public AbstractCProjectDescriptionStorage getProjectDescriptionStorage(IProject project) {
		if (!project.isAccessible()) {
			assert(!fDescriptionStorageMap.contains(project));
			return null;
		}
		AbstractCProjectDescriptionStorage projStorage = fDescriptionStorageMap.get(project);
		if (projStorage == null) {
			projStorage = loadProjectStorage(project);
			fDescriptionStorageMap.putIfAbsent(project, projStorage);
		}
		return fDescriptionStorageMap.get(project);
	}

	/**
	 * Sets the Project description by delegating to the appropriate project storage type.
	 *
	 * If the storage type returns false on {@link ICProjectDescriptionStorageType#createsCProjectXMLFile()} then
	 * we create a .cproject file with type id and version.
	 *
	 * If no existing project storage is found then we throw a core exception (users should have called
	 * #getProjectDescriptionStorage(...) before calling this.
	 *
	 * @param project
	 * @param description
	 * @throws CoreException on failure
	 */
	public void setProjectDescription(IProject project, ICProjectDescription description, int flags, IProgressMonitor monitor) throws CoreException {
		AbstractCProjectDescriptionStorage storage = fDescriptionStorageMap.get(project);
		if (storage == null)
			throw ExceptionFactory.createCoreException("Can't set ProjectDescription before getProjectDescriptionStorage!"); //$NON-NLS-1$
		if (!storage.type.createsCProjectXMLFile())
			writeProjectStorageType(project, storage.type);
		storage.setProjectDescription(description, flags, monitor);
	}


	/**
	 * Persist the type and version of this particular project description storage type
	 * (for description storages that don't perform this job themselves).
	 * @param project
	 * @param type
	 */
	private void writeProjectStorageType(IProject project, CProjectDescriptionStorageTypeProxy type) throws CoreException{
		Document doc;
		try {
			doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
			// Set the version
			ProcessingInstruction instruction = doc.createProcessingInstruction(ICProjectDescriptionStorageType.STORAGE_VERSION_NAME, type.version.toString());
			doc.appendChild(instruction);
			// Set the type id
			Element el = doc.createElement(ICProjectDescriptionStorageType.STORAGE_ROOT_ELEMENT_NAME);
			el.setAttribute(ICProjectDescriptionStorageType.STORAGE_TYPE_ATTRIBUTE, type.id);
			doc.appendChild(el);
			XmlUtil.prettyFormat(doc);

			ByteArrayOutputStream stream = new ByteArrayOutputStream();
			Transformer transformer = TransformerFactory.newInstance().newTransformer();
			transformer.setOutputProperty(OutputKeys.METHOD, "xml"); //$NON-NLS-1$
			transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); //$NON-NLS-1$
			transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$
			DOMSource source = new DOMSource(doc);
			StreamResult result = new StreamResult(stream);
			transformer.transform(source, result);

			InputStream input = new ByteArrayInputStream(stream.toByteArray());

			// Set the project description storage type
			IFile f = project.getFile(ICProjectDescriptionStorageType.STORAGE_FILE_NAME);
			if (!f.exists())
				f.refreshLocal(IResource.DEPTH_INFINITE, new NullProgressMonitor());
			ensureWritable(f);
			if (!f.exists())
				f.create(input, true, new NullProgressMonitor());
			else
				f.setContents(input, IResource.FORCE, new NullProgressMonitor());
		} catch (ParserConfigurationException e) {
			throw ExceptionFactory.createCoreException(e);
		} catch (TransformerConfigurationException e) {
			throw ExceptionFactory.createCoreException(e);
		} catch (TransformerFactoryConfigurationError e) {
			throw ExceptionFactory.createCoreException(e);
		} catch (TransformerException e) {
			throw ExceptionFactory.createCoreException(e);
		}
	}


	/**
	 * Given a project, this method attempts to discover the type of the storage
	 * and return the AbstractCProjectDescriptionStorage responsible for loading it.
	 *
	 * @return AbstractCProjectDescription or null if not found
	 */
	private AbstractCProjectDescriptionStorage loadProjectStorage(IProject project) {
		if (storageTypeMap == null)
			initExtensionPoints();

		// If no project description found, then use the default
		Version version = DEFAULT_STORAGE_VERSION;
		String storageTypeID = DEFAULT_STORAGE_TYPE;

		InputStream stream = null;
		try{
			DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
			stream = getInputStreamForIFile(project, ICProjectDescriptionStorageType.STORAGE_FILE_NAME);
			if(stream != null){
				Document doc = builder.parse(stream);
				// Get the first element in the project file
				Node rootElement = doc.getFirstChild();
				if (rootElement.getNodeType() != Node.PROCESSING_INSTRUCTION_NODE)
					throw ExceptionFactory.createCoreException(SettingsModelMessages.getString("CProjectDescriptionManager.7")); //$NON-NLS-1$
				else
					version = new Version(rootElement.getNodeValue());

				// Now get the project root element (there should be only one)
				NodeList nodes = doc.getElementsByTagName(ICProjectDescriptionStorageType.STORAGE_ROOT_ELEMENT_NAME);
				if (nodes.getLength() == 0)
					throw ExceptionFactory.createCoreException(SettingsModelMessages.getString("CProjectDescriptionManager.9")); //$NON-NLS-1$
				Node node = nodes.item(0);
				if(node.getNodeType() != Node.ELEMENT_NODE)
					throw ExceptionFactory.createCoreException(SettingsModelMessages.getString("CProjectDescriptionManager.10")); //$NON-NLS-1$
				// If we've got this far, then we're at least dealing with an old style project:
				//  as this didn't use to provide a type, specify one explicitly
				// Choose new style -- separated out storage modules by default as this is backwards compatible...
				storageTypeID = XmlProjectDescriptionStorage2.STORAGE_TYPE_ID;
				if (((Element)node).hasAttribute(ICProjectDescriptionStorageType.STORAGE_TYPE_ATTRIBUTE))
					storageTypeID = ((Element)node).getAttribute(ICProjectDescriptionStorageType.STORAGE_TYPE_ATTRIBUTE);
			}
		} catch (Exception e) {
			// Catch all, if not found, we use the old-style defaults...
		} finally {
			if(stream != null){
				try {
					stream.close();
				} catch (IOException e) {}
			}
		}

		List<CProjectDescriptionStorageTypeProxy> types = storageTypeMap.get(storageTypeID);
		if (types != null) {
			for (CProjectDescriptionStorageTypeProxy type : types) {
				if (type.isCompatible(version)) {
					return type.getProjectDescriptionStorage(type, project, version);
				}
			}
		}

		// No type found!
		CCorePlugin.log("CProjectDescriptionStorageType: " + storageTypeID + "  for version: " + version + " not found!");  //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
		return null;
	}


	private InputStream getInputStreamForIFile(IProject project, String name) throws CoreException {
		IFile f = project.getFile(name);
		if (f.exists())
			return f.getContents(true);
		else {
			URI location = f.getLocationURI();
			if (location != null) {
				IFileStore file = EFS.getStore(location);
				IFileInfo info = null;

				if (file != null) {
					info = file.fetchInfo();
					if (info != null && info.exists())
						return file.openInputStream(EFS.NONE, null);
				}
			}
		}
		throw ExceptionFactory.createCoreException("No project des file found..."); //$NON-NLS-1$
	}


	/*
	 * Resource Change Callbacks
	 */

	/**
	 * Callback indicating that a project has moved
	 * @param fromProject
	 * @param toProject
	 */
	void projectMove(IProject fromProject, IProject toProject) {
		AbstractCProjectDescriptionStorage projStorage = fDescriptionStorageMap.get(fromProject);
		if (projStorage != null) {
			fDescriptionStorageMap.put(toProject, projStorage);
			projStorage.projectMove(toProject);
			fDescriptionStorageMap.remove(fromProject);
		}
		// Notify the CConfigBasedDescriptorManager as well
		CConfigBasedDescriptorManager.getInstance().projectMove(fromProject, toProject);
	}

	/**
	 * Callback indicating project has been closed or deleted
	 * @param project
	 */
	void projectClosedRemove(IProject project) {
		AbstractCProjectDescriptionStorage projStorage = fDescriptionStorageMap.get(project);
		if (projStorage != null)
			projStorage.projectCloseRemove();
		fDescriptionStorageMap.remove(project);
		// Remove from ICDescriptorManager as well
		CConfigBasedDescriptorManager.getInstance().projectClosedRemove(project);
	}

	/**
	 * Cleanup state
	 */
	public void shutdown() {
		instance = null;
	}


	/**
	 * Initialize the project description storage types
	 */
	private synchronized void initExtensionPoints() {
		if (storageTypeMap != null)
			return;
		Map<String, List<CProjectDescriptionStorageTypeProxy>> m = new HashMap<String, List<CProjectDescriptionStorageTypeProxy>>();
        IExtensionPoint extpoint = Platform.getExtensionRegistry().getExtensionPoint(CCorePlugin.PLUGIN_ID, CPROJ_DESC_STORAGE_EXT_ID);
		for (IExtension extension : extpoint.getExtensions()) {
			for (IConfigurationElement configEl : extension.getConfigurationElements()) {
				if (configEl.getName().equalsIgnoreCase(CPROJ_STORAGE_TYPE)) {
					CProjectDescriptionStorageTypeProxy type = initStorageType(configEl);
					if (type != null) {
						if (!m.containsKey(type.id))
							m.put(type.id, new LinkedList<CProjectDescriptionStorageTypeProxy>());
						m.get(type.id).add(type);
					}
				}
			}
		}
		storageTypeMap = m;
	}

	/**
	 * Initialize a storage type from a configuration element
	 * @param el
	 * @return
	 */
	private static CProjectDescriptionStorageTypeProxy initStorageType(IConfigurationElement el) {
		CProjectDescriptionStorageTypeProxy type = null;
		try {
			type = new CProjectDescriptionStorageTypeProxy(el);
		} catch (CoreException e) {
			CCorePlugin.log("Couldn't instantiate CProjectDescriptionStorageType " + //$NON-NLS-1$
					el.getDeclaringExtension().getNamespaceIdentifier() + " " + e.getMessage()); //$NON-NLS-1$
		} catch (IllegalArgumentException e) {
			CCorePlugin.log("Failed to load CProjectDescriptionStorageType " + //$NON-NLS-1$
					el.getDeclaringExtension().getNamespaceIdentifier() + " " + e.getMessage()); //$NON-NLS-1$
		}
		return type;
	}

	/**
	 * Helper method to ensure that a resource is writable. This means: <br/>
	 *  - If the resource doesn't exist, it can be created (its parent is made writable) <br/>
	 *  - If the resource exists and its a file, it's made writable <br/>
	 *  - If the resource exists and its a directory, it and its (direct) children
	 * 		are made writable
	 * @param resource
	 * @throws CoreException on failure
	 */
	public static void ensureWritable(IResource resource) throws CoreException {
		if (!resource.exists())
			resource.refreshLocal(IResource.DEPTH_INFINITE, null);
		if (!resource.exists()) {
			// If resource doesn't exist, ensure it can be created.
			ResourceAttributes parentAttr = resource.getParent().getResourceAttributes();
			if (parentAttr.isReadOnly()) {
				parentAttr.setReadOnly(false);
				resource.getParent().setResourceAttributes(parentAttr);
			}
		} else {
			// If resource exists, ensure it and children are writable
			if (resource instanceof IFile) {
				if (resource.getResourceAttributes().isReadOnly()) {
				    IStatus result = resource.getWorkspace().validateEdit(new IFile[] { (IFile) resource }, null);
				    if (!result.isOK())
				        throw new CoreException(result);
				}
			} else if (resource instanceof IContainer) {
				ResourceAttributes resAttr = resource.getResourceAttributes();
				if (resAttr.isReadOnly()) {
					resAttr.setReadOnly(false);
					resource.setResourceAttributes(resAttr);
				}
				IResource[] members = ((IContainer) resource).members();
				List<IFile> files = new ArrayList<IFile>(members.length);
				for (IResource member : members) {
					if (member instanceof IFile && member.getResourceAttributes().isReadOnly()) {
						files.add((IFile) member);
					}
				}
				if (files.size() > 0) {
					IFile[] filesToValidate = files.toArray(new IFile[files.size()]);
				    IStatus result = resource.getWorkspace().validateEdit(filesToValidate, null);
				    if (!result.isOK())
				        throw new CoreException(result);
				}
			}
		}
	}

}
