/*******************************************************************************
 * Copyright (c) 2004, 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 - Initial API and implementation
 * Anton Leherbauer (Wind River Systems)
 *******************************************************************************/
package org.eclipse.cdt.make.internal.core.scannerconfig;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.Map;

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.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.eclipse.cdt.make.core.MakeCorePlugin;
import org.eclipse.cdt.make.core.scannerconfig.IDiscoveredPathManager.IDiscoveredScannerInfoSerializable;
import org.eclipse.cdt.make.core.scannerconfig.InfoContext;
import org.eclipse.cdt.make.internal.core.MakeMessages;
import org.eclipse.cdt.make.internal.core.scannerconfig2.PerProjectSICollector;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.core.runtime.Status;
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;
import org.xml.sax.SAXException;

/**
 * Discovered scanner info persistance store
 *
 * @author vhirsl
 */
public final class DiscoveredScannerInfoStore {
	private static final QualifiedName dscFileNameProperty = new
            QualifiedName(MakeCorePlugin.getUniqueIdentifier(), "discoveredScannerConfigFileName"); //$NON-NLS-1$
	private static final String CDESCRIPTOR_ID = MakeCorePlugin.getUniqueIdentifier() + ".discoveredScannerInfo"; //$NON-NLS-1$
    public static final String SCD_STORE_VERSION = "scdStore"; //$NON-NLS-1$
	public static final String SI_ELEM = "scannerInfo"; //$NON-NLS-1$
	public static final String COLLECTOR_ELEM = "collector"; //$NON-NLS-1$
	public static final String ID_ATTR = "id"; //$NON-NLS-1$

	private static final String INSTANCE_ELEM = "instance";  //$NON-NLS-1$

	private static DiscoveredScannerInfoStore instance;

	/**
	 * Caches scanner config XML Documents per project using soft references.
	 */
	private final Map<IProject, Reference<Document>> fDocumentCache = new HashMap<IProject, Reference<Document>>();

	public static DiscoveredScannerInfoStore getInstance() {
		if (instance == null) {
			instance = new DiscoveredScannerInfoStore();
		}
		return instance;
	}
	/**
	 *
	 */
	private DiscoveredScannerInfoStore() {
	}

	public void loadDiscoveredScannerInfoFromState(IProject project, IDiscoveredScannerInfoSerializable serializable)
			throws CoreException {
		loadDiscoveredScannerInfoFromState(project, new InfoContext(project), serializable);
	}

	public void loadDiscoveredScannerInfoFromState(IProject project, InfoContext context, IDiscoveredScannerInfoSerializable serializable)
			throws CoreException {
		// Get the document
		Element rootElem = getRootElement(project, context, serializable);

		if(rootElem != null){
	        // get the collector element
	        NodeList collectorList = rootElem.getElementsByTagName(COLLECTOR_ELEM);
	        if (collectorList.getLength() > 0) {
	        	// find the collector element
	        	for (int i = 0; i < collectorList.getLength(); ++i) {
	        		Element collectorElem = (Element) collectorList.item(i);
	        		String collectorId = collectorElem.getAttribute(ID_ATTR);
	        		if (serializable.getCollectorId().equals(collectorId)) {
	    		        serializable.deserialize(collectorElem);
	        			break;
	        		}
	        	}
	        }
		}
	}

	public boolean hasInfo(IProject project, InfoContext context, IDiscoveredScannerInfoSerializable serializable){
		try {
			if(getRootElement(project, context, serializable) != null)
				return true;
		} catch (CoreException e) {
			MakeCorePlugin.log(e);
		}
		return false;
	}

	private Element getRootElement(IProject project, InfoContext context, IDiscoveredScannerInfoSerializable serializable) throws CoreException{
		if(serializable == null)
			return null;

		Document document = getDocument(project);
		Element rootElem = null;
		if (document != null) {
			NodeList rootList = document.getElementsByTagName(SI_ELEM);
			if (rootList.getLength() > 0) {
				rootElem = (Element) rootList.item(0);

				if(!context.isDefaultContext()){
					String instanceId = context.getInstanceId();

		        	Element instanceElem = findChild(rootElem, INSTANCE_ELEM, ID_ATTR, instanceId);

		        	rootElem = instanceElem;
				}
			}
		}

		return rootElem;
	}

	private Document getDocument(IProject project) throws CoreException {
		// Get the document
		Reference<Document> ref= fDocumentCache.get(project);
		Document document = ref != null ? ref.get() : null;
		if (document == null) {
		    try {
		        DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
		        IPath path = getDiscoveredScannerConfigStore(project);
		        if (path.toFile().exists()) {
		            // read form file
		            FileInputStream file = new FileInputStream(path.toFile());
		            document = builder.parse(file);
		            Node rootElem = document.getFirstChild();
		            if (rootElem.getNodeType() != Node.PROCESSING_INSTRUCTION_NODE) {
		                // no version info; upgrade
		                upgradeDocument(document, project);
		            }
		        }
		        else {
		            // create new document
		            document = builder.newDocument();
		            ProcessingInstruction pi = document.createProcessingInstruction(SCD_STORE_VERSION, "version=\"2\""); //$NON-NLS-1$
		            document.appendChild(pi);
		            Element rootElement = document.createElement(SI_ELEM);
		            rootElement.setAttribute(ID_ATTR, CDESCRIPTOR_ID);
		            document.appendChild(rootElement);
		        }
		        fDocumentCache.put(project, new SoftReference<Document>(document));
		    }
		    catch (IOException e) {
		        MakeCorePlugin.log(e);
		        throw new CoreException(new Status(IStatus.ERROR, MakeCorePlugin.getUniqueIdentifier(), -1,
		                MakeMessages.getString("DiscoveredPathManager.File_Error_Message"), e)); //$NON-NLS-1$
		    }
		    catch (ParserConfigurationException e) {
		        MakeCorePlugin.log(e);
		        throw new CoreException(new Status(IStatus.ERROR, MakeCorePlugin.getUniqueIdentifier(), -1,
		                MakeMessages.getString("DiscoveredPathManager.File_Error_Message"), e)); //$NON-NLS-1$
		    }
		    catch (SAXException e) {
		        MakeCorePlugin.log(e);
		        throw new CoreException(new Status(IStatus.ERROR, MakeCorePlugin.getUniqueIdentifier(), -1,
		                MakeMessages.getString("DiscoveredPathManager.File_Error_Message"), e)); //$NON-NLS-1$
		    }
		}
		return document;
	}

	private void upgradeDocument(Document document, IProject project) {
		Element rootElem = (Element) document.getElementsByTagName(SI_ELEM).item(0);
		ProcessingInstruction pi = document.createProcessingInstruction(SCD_STORE_VERSION, "version=\"2.0\""); //$NON-NLS-1$
		document.insertBefore(pi, rootElem);

		Element collectorElem = document.createElement(COLLECTOR_ELEM);
		collectorElem.setAttribute(ID_ATTR, PerProjectSICollector.COLLECTOR_ID);
		for (Node child = rootElem.getFirstChild(); child != null; child = rootElem.getFirstChild()) {
			collectorElem.appendChild(rootElem.removeChild(child));
		}
		rootElem.appendChild(collectorElem);
	}

	private Element findChild(Element parentElem, String name, String attr, String attrValue){
        Element cfgElem = null;
        NodeList cfgList = parentElem.getElementsByTagName(name);
        if (cfgList.getLength() > 0) {
        	// find per file collector element and remove children
        	for (int i = 0; i < cfgList.getLength(); ++i) {
        		Element cElem = (Element) cfgList.item(i);
        		String value = cElem.getAttribute(attr);
        		if (value.equals(attrValue)) {
        			cfgElem = cElem;
        			break;
        		}
        	}
        }

        return cfgElem;
	}

	private void saveDiscoveredScannerInfo(InfoContext context, IDiscoveredScannerInfoSerializable serializable, Document doc) {
		NodeList rootList = doc.getElementsByTagName(SI_ELEM);
		if (rootList.getLength() > 0) {
			Element rootElem = (Element) rootList.item(0);

			// get the collector element
			if(!context.isDefaultContext()){
				String instanceId = context.getInstanceId();

				Element instanceElem = findChild(rootElem, INSTANCE_ELEM, ID_ATTR, instanceId);

				if(instanceElem == null){
		        	instanceElem = doc.createElement(INSTANCE_ELEM);
		        	instanceElem.setAttribute(ID_ATTR, instanceId);
		        	rootElem.appendChild(instanceElem);
		        }

	        	rootElem = instanceElem;
			}

			// get the collector element
	        Element collectorElem = null;
	        NodeList collectorList = rootElem.getElementsByTagName(COLLECTOR_ELEM);
	        if (collectorList.getLength() > 0) {
	        	// find per file collector element and remove children
	        	for (int i = 0; i < collectorList.getLength(); ++i) {
	        		Element cElem = (Element) collectorList.item(i);
	        		String collectorId = cElem.getAttribute(ID_ATTR);
	        		if (serializable.getCollectorId().equals(collectorId)) {
	        			for (Node child = cElem.getFirstChild(); child != null;
	        					child = cElem.getFirstChild()) {
	        				cElem.removeChild(child);
	        			}
	        			collectorElem = cElem;
	        			break;
	        		}
	        	}
	        }
	        if (collectorElem == null) {
	        	// create per profile element
	        	collectorElem = doc.createElement(COLLECTOR_ELEM);
	        	collectorElem.setAttribute(ID_ATTR, serializable.getCollectorId());
	        	rootElem.appendChild(collectorElem);
	        }

			// Save the discovered scanner info
			serializable.serialize(collectorElem);
		}
	}
	public void saveDiscoveredScannerInfoToState(IProject project, IDiscoveredScannerInfoSerializable serializable) throws CoreException {
		saveDiscoveredScannerInfoToState(project, new InfoContext(project), serializable);
	}

	public void saveDiscoveredScannerInfoToState(IProject project, InfoContext context, IDiscoveredScannerInfoSerializable serializable) throws CoreException {
		Document document = getDocument(project);
		// Create document
		try {
			saveDiscoveredScannerInfo(context, serializable, document);

			// Transform the document to something we can save in a file
			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(document);
			StreamResult result = new StreamResult(stream);
			transformer.transform(source, result);

			// Save the document
			try {
				IPath path = getDiscoveredScannerConfigStore(project);
				FileOutputStream file = new FileOutputStream(path.toFile());
				file.write(stream.toByteArray());
				file.close();
			} catch (IOException e) {
				throw new CoreException(new Status(IStatus.ERROR, MakeCorePlugin.getUniqueIdentifier(), -1,
						MakeMessages.getString("DiscoveredPathManager.File_Error_Message"), e)); //$NON-NLS-1$
			}

			// Close the streams
			stream.close();
		} catch (TransformerException e) {
			MakeCorePlugin.log(e);
			throw new CoreException(new Status(IStatus.ERROR, MakeCorePlugin.getUniqueIdentifier(), -1,
					MakeMessages.getString("DiscoveredPathManager.File_Error_Message"), e)); //$NON-NLS-1$
		} catch (IOException e) {
			MakeCorePlugin.log(e);
			throw new CoreException(new Status(IStatus.ERROR, MakeCorePlugin.getUniqueIdentifier(), -1,
					MakeMessages.getString("DiscoveredPathManager.File_Error_Message"), e)); //$NON-NLS-1$
		}
	}

    public IPath getDiscoveredScannerConfigStore(IProject project) {
        String fileName = project.getName() + ".sc"; //$NON-NLS-1$
        String storedFileName = null;
        try {
            storedFileName = project.getPersistentProperty(dscFileNameProperty);
        } catch (CoreException e) {
            MakeCorePlugin.log(e.getStatus());
        }
        if (storedFileName != null && !storedFileName.equals(fileName)) {
            // try to move 2.x file name format to 3.x file name format
            movePluginStateFile(storedFileName, fileName);
        }
        try {
            project.setPersistentProperty(dscFileNameProperty, fileName);
        } catch (CoreException e) {
            MakeCorePlugin.log(e.getStatus());
        }

        return MakeCorePlugin.getWorkingDirectory().append(fileName);
    }

    public void updateScannerConfigStore(IResourceDelta delta) {
        try {
            delta.accept(new IResourceDeltaVisitor() {

                @Override
				public boolean visit(IResourceDelta delta) throws CoreException {
                    IResource resource = delta.getResource();
                    if (resource instanceof IProject) {
                        IProject project = (IProject) resource;
                        int kind = delta.getKind();
                        switch (kind) {
                        case IResourceDelta.REMOVED:
                            if ((delta.getFlags() & IResourceDelta.MOVED_TO) != 0) {
                                // project renamed
                                IPath newPath = delta.getMovedToPath();
                                IProject newProject = delta.getResource().getWorkspace().
                                        getRoot().getProject(newPath.toString());
                                scProjectRenamed(project, newProject);
                            }
                            else {
                                // project deleted
                                scProjectDeleted(project);
                            }
                            // remove from cache
                            fDocumentCache.remove(project);
                        }
                        return false;
                    }
                    return true;
                }

            });
        }
        catch (CoreException e) {
            MakeCorePlugin.log(e);
        }
    }

    private void scProjectDeleted(IProject project) {
        String scFileName = project.getName() + ".sc"; //$NON-NLS-1$
        deletePluginStateFile(scFileName);
    }

    private void deletePluginStateFile(String scFileName) {
        IPath path = MakeCorePlugin.getWorkingDirectory().append(scFileName);
        File file = path.toFile();
        if (file.exists()) {
            file.delete();
        }
    }

    private void scProjectRenamed(IProject project, IProject newProject) {
        String scOldFileName = project.getName() + ".sc"; //$NON-NLS-1$
        String scNewFileName = newProject.getName() + ".sc"; //$NON-NLS-1$
        movePluginStateFile(scOldFileName, scNewFileName);
        try {
            newProject.setPersistentProperty(dscFileNameProperty, scNewFileName);
        }
        catch (CoreException e) {
            MakeCorePlugin.log(e);
        }
    }

    private void movePluginStateFile(String oldFileName, String newFileName) {
        IPath oldPath = MakeCorePlugin.getWorkingDirectory().append(oldFileName);
        IPath newPath = MakeCorePlugin.getWorkingDirectory().append(newFileName);
        File oldFile = oldPath.toFile();
        File newFile = newPath.toFile();
        if (oldFile.exists()) {
            oldFile.renameTo(newFile);
        }
    }

}
