package org.eclipse.jwt.we.conf.model.resource.internal;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.Path;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EClassifier;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.jwt.we.conf.model.plugin.ConfPlugin;
import org.eclipse.jwt.we.conf.model.plugin.internal.EMFUtils;
import org.eclipse.jwt.we.conf.model.resource.ConfResourceException;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;


/**
 * Allows to patch ConfModels that have unknown Aspects
 * by backuping the file and then removing them.
 * 
 * @author mdutoo
 *
 */
public class ConfModelPatchingContext {
	
	private static DocumentBuilder db;
	private static XPathExpression profileXpathExpr;
	private static XPathExpression aspectInstanceETypeXpathExpr;
	private static XPathExpression aspectInstanceXpathExpr;
	private static TransformerFactory tf;
	
	static {
		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		try {
			db = dbf.newDocumentBuilder();
		} catch (ParserConfigurationException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		XPathFactory xpf = XPathFactory.newInstance();
		try {
			profileXpathExpr = xpf.newXPath().compile("/ConfModel/profiles");
			aspectInstanceETypeXpathExpr = xpf.newXPath().compile("aspects/aspectInstanceEType");
			aspectInstanceXpathExpr = xpf.newXPath().compile("/ConfModel/aspectInstances");
		} catch (XPathExpressionException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		tf = TransformerFactory.newInstance();
		// NB. Transformer is not thread safe so can't be static
	}
	
	private URI confModelUri;
	private ResourceSet modelResourceSet;
	
	private String confModelAbsoluteFilePath = null;
	private String confModelRelativePlatformPath = null;
	private IFile confModelIFile;
	private Document confModelDoc;
	private ArrayList<Node> nokProfileNodes = new ArrayList<Node>(3);
	private HashSet<String> nokProfileNameSet = new HashSet<String>(3);
	private HashSet<String> nokAspectIdSet = new HashSet<String>(3);
	
	
	public ConfModelPatchingContext(URI confModelUri, ResourceSet modelResourceSet) {
		this.confModelUri = confModelUri;
		this.modelResourceSet = modelResourceSet;
	}
	
	/**
	 * 
	 * @return
	 */
	public HashSet<String> getNokProfileNameSet() {
		return nokProfileNameSet;
	}
	public HashSet<String> getNokAspectIdSet() {
		return nokAspectIdSet;
	}
	
	/*
	 * Returns a transformer that can be used everywhere
	 */
	private static Transformer getTransformer() throws TransformerConfigurationException {
		// NB. Transformer is not thread safe so can't be static (TODO ThreadLocal ?)
		return tf.newTransformer();
	}

	
	/**
	 * Looks for unknown Aspect extension problems
	 */
	public void auditConfModel() throws ConfResourceException {
		// getting conf model file and path
		if (this.confModelUri.isPlatformResource()) {
			// workspace resource
			this.confModelRelativePlatformPath = confModelUri.toPlatformString(false); // no need to decode since Eclipse-born resource
			this.confModelIFile = ConfPlugin.getWorkspaceRoot().getFile(new Path(this.confModelRelativePlatformPath));
			File workspaceFile = ConfPlugin.getWorkspaceRoot().getLocation().toFile();
			File confModelFile = new File(workspaceFile, this.confModelRelativePlatformPath);
			this.confModelAbsoluteFilePath = confModelFile.getAbsolutePath();
		} else if (this.confModelUri.isFile()) {
			// resource outside the workspace ; NB. patches bug #298043
			this.confModelAbsoluteFilePath = new File(confModelUri.toFileString()).getAbsolutePath();
			this.confModelIFile = null;
		} else {
			throw new ConfResourceException("Unsupported URI type for " + this.confModelUri);
		}

		InputStream confModelIs = null;
		try {
			confModelIs = new BufferedInputStream(new FileInputStream(this.confModelAbsoluteFilePath));
			this.confModelDoc = db.parse(confModelIs);
		} catch (FileNotFoundException cex) {
			throw new ConfResourceException("Unable to audit confModel "
					+ this.confModelAbsoluteFilePath
					+ " : file not found", cex);
		} catch (IOException ioex) {
			throw new ConfResourceException("Unable to audit confModel "
					+ this.confModelAbsoluteFilePath
					+ " : IO error loading conf", ioex);
		} catch (SAXException sex) {
			throw new ConfResourceException("Unable to audit confModel "
					+ this.confModelAbsoluteFilePath
					+ " : error loading conf XML", sex);
		} finally {
			try {
				if (confModelIs != null) {
					confModelIs.close();
				}
			} catch (IOException ioex) {
				// no log
			}
		}
		
		// now checking for invalid aspects and profiles
		try {
			checkForInvalidAspectsAndProfiles();
		} catch (XPathExpressionException xpeex) {
			// should not happen
			throw new ConfResourceException("Unable to audit confModel "
					+ this.confModelAbsoluteFilePath
					+ " : bad xpath", xpeex);
		}
	}

	
	/**
	 * If wished for, backs up _conf file to _conf.bak and then patches it
	 * @throws ConfResourceException 
	 */
	public void patchConfModel() throws ConfResourceException {
		// backing up the _conf file
		try {
			backupConfModel();
		} catch (CoreException cex) {
			throw new ConfResourceException("Unable to patch confModel "
					+ this.confModelAbsoluteFilePath
					+ " : error backuping it", cex);
		}
		
		// patching the _conf file by removing invalid Profiles and Aspect instances
		try {
			removeInvalidProfilesAndAspectInstances();
		} catch (XPathExpressionException xpeex) {
			// should not happen
			throw new ConfResourceException("Unable to patch confModel "
					+ this.confModelAbsoluteFilePath
					+ " : bad xpath", xpeex);
		}
		
		// write the patched conf model
		DOMSource source = new DOMSource(this.confModelDoc);
		StreamResult result = new StreamResult(confModelAbsoluteFilePath);
		try {
			getTransformer().transform(source, result);
		} catch (TransformerConfigurationException tcex) {
			throw new ConfResourceException("Unable to patch confModel "
					+ this.confModelAbsoluteFilePath
					+ " : error writing patched XML", tcex);
		} catch (TransformerException tex) {
			throw new ConfResourceException("Unable to patch confModel "
					+ this.confModelAbsoluteFilePath
					+ " : error writing patched XML", tex);
		} 

		// cleaning and refreshing (helps future reload of resource)
		try {
			if (this.confModelIFile != null) {
				// refreshing the EResource, else out of sync
				this.confModelIFile.refreshLocal(IFile.DEPTH_ZERO, null); // no ProgressMonitor
			}
			// have to clean resourceset
			// let's remove the duplicated resource in the resource set by unload and reload confModel :
			EMFResourceUtils.removeResourceFromResourceSet(confModelUri, modelResourceSet);
			if (this.confModelIFile != null) {
				// have to refresh else not in sync, though the conf model would work
				ConfPlugin.getWorkspaceRoot().getFile(new Path(this.confModelRelativePlatformPath))
					.refreshLocal(IFile.DEPTH_ZERO, null); // no decode since Eclipse born URI, no progressMonitor
			}
		} catch (CoreException cex) {
			// should not happen
			throw new ConfResourceException("Unable to patch confModel "
					+ this.confModelAbsoluteFilePath
					+ " : error refreshing resource", cex);
		}
	}


	/**
	 * Checks for invalid aspects and profiles
	 * @throws XPathExpressionException
	 */
	private void checkForInvalidAspectsAndProfiles() throws XPathExpressionException {
		NodeList profileNodes = (NodeList) profileXpathExpr.evaluate(confModelDoc, XPathConstants.NODESET);
		//ArrayList<Node> okProfileNodes = new ArrayList<Node>(profileNodes.getLength());
		for (int profileInd = 0; profileInd < profileNodes.getLength(); profileInd++) {
			Node profileNode = profileNodes.item(profileInd);
			NodeList aspectInstanceETypeNodes = (NodeList) aspectInstanceETypeXpathExpr.evaluate(profileNode, XPathConstants.NODESET);
			boolean isProfileOk = true;
			for (int eTypeNodeInd = 0; eTypeNodeInd < aspectInstanceETypeNodes.getLength(); eTypeNodeInd++) {
				Node aspectInstanceETypeNode = (Node) aspectInstanceETypeNodes.item(eTypeNodeInd);
				String aspectInstanceEType = ((Attr) aspectInstanceETypeNode.getAttributes().getNamedItem("href")).getValue();
				URI aspectInstanceETypeUri = URI.createPlatformResourceURI(aspectInstanceEType, false); // don't encode, else # is escaped
				EClassifier aspectInstanceEClassifier = null;
				try {
					// trying to load it
					aspectInstanceEClassifier = EMFUtils.getETypeEClassifier(aspectInstanceETypeUri);
				} catch (Throwable t) {
					// TODO could display for debug
					// TODO doesn't happen ?!
				}
				if (aspectInstanceEClassifier == null) {
					isProfileOk = false;
					String nokAspectId = ((Attr) aspectInstanceETypeNode.getParentNode().getAttributes().getNamedItem("id")).getValue();
					this.nokAspectIdSet.add(nokAspectId);
				}
			}
			if (isProfileOk) {
				//okProfileNodes.add(profileNode);
			} else {
				this.nokProfileNodes.add(profileNode);
				this.nokProfileNameSet.add(((Attr) profileNode.getAttributes().getNamedItem("name")).getValue());
				// TODO could display for debug
			}
		}
		
		if (nokAspectIdSet.isEmpty()) {
			// no problem, nothing to do
			return;
		}
	}


	/**
	 * Backs up the _conf file
	 * @throws CoreException
	 */
	private void backupConfModel() throws CoreException, ConfResourceException {
		if (this.confModelIFile != null) {
			backupConfModelInWorkspace();
		} else {
			backupConfModelOutsideWorkspace(); // NB. Patches bug #298043
		}
	}
	
	/**
	 * Backs up the _conf file when it is within the workspace
	 * @throws CoreException
	 * @throws ConfResourceException
	 */
	private void backupConfModelInWorkspace() throws CoreException, ConfResourceException {
		Path confModelBackupPlatformPath = new Path(this.confModelRelativePlatformPath + ".bak");
		// first have to delete using Java IO API the existing backup file if any,
		// because copying with "force" is sometimes not enough (when not in sync)
		IFile confModelBackupIFile = ConfPlugin.getWorkspaceRoot().getFile(confModelBackupPlatformPath);
		confModelBackupIFile.getRawLocation().toFile().delete();
		confModelBackupIFile.delete(true, null); // force, no progressMonitor
		this.confModelIFile.copy(confModelBackupPlatformPath, true, null); // force, no progressMonitor ; TODO IProgressMonitor ?
	}

	/**
	 * Backs up the _conf file when it is outside the workspace
	 * Patches bug #298043
	 * @throws CoreException
	 * @throws ConfResourceException
	 */
	private void backupConfModelOutsideWorkspace() throws CoreException, ConfResourceException {
		String confModelBackupAbsoluteFilePath = this.confModelAbsoluteFilePath + ".bak";
		File confModelBackupFile = new File(confModelBackupAbsoluteFilePath);
		if (confModelBackupFile.exists()) {
			throw new ConfResourceException("Can't write over existing  file at " + confModelBackupAbsoluteFilePath);
		}
		BufferedInputStream confModelBis = null;
		BufferedOutputStream confModelBackupBos = null;
		try {
			if (!confModelBackupFile.createNewFile()) {
				throw new ConfResourceException("Can't write over existing  file at " + confModelBackupAbsoluteFilePath);
			}
			confModelBis = new BufferedInputStream(new FileInputStream(this.confModelAbsoluteFilePath));
			confModelBackupBos = new BufferedOutputStream(new FileOutputStream(confModelBackupFile));
			byte[] buf = new byte[1024];
			int nbRead;
			while ((nbRead = confModelBis.read(buf)) != -1) {
				confModelBackupBos.write(buf, 0, nbRead);
			}
			confModelBackupBos.flush();
			
		} catch (IOException ioex) {
			throw new ConfResourceException("Error backing up conf model to "
					+ confModelBackupAbsoluteFilePath, ioex);
		} finally {
			try { if (confModelBis != null) confModelBis.close(); } catch (Exception e) {}
			try { if (confModelBackupBos != null) confModelBackupBos.close(); } catch (Exception e) {}
		}
	}


	/**
	 * Patches the _conf file by removing invalid Profiles and Aspect instances
	 * @throws XPathExpressionException 
	 */
	private void removeInvalidProfilesAndAspectInstances() throws XPathExpressionException {
		// first profiles
		Element confModelConfNode = this.confModelDoc.getDocumentElement(); // TODO or below ?
		for (Node nokProfileNode : this.nokProfileNodes) {
			confModelConfNode.removeChild(nokProfileNode);
		}
		
		// now aspectInstances
		NodeList aspectInstanceNodes = (NodeList) aspectInstanceXpathExpr.evaluate(confModelDoc, XPathConstants.NODESET);
		ArrayList<Node> nokAspectInstanceNodes = new ArrayList<Node>(3);
		for (int aspectInstanceInd = 0; aspectInstanceInd < aspectInstanceNodes.getLength(); aspectInstanceInd++) {
			Node aspectInstanceNode = aspectInstanceNodes.item(aspectInstanceInd);
			String nokAspectId = ((Attr) aspectInstanceNode.getAttributes().getNamedItem("id")).getValue();
			if (this.nokAspectIdSet.contains(nokAspectId)) {
				nokAspectInstanceNodes.add(aspectInstanceNode);
				confModelConfNode.removeChild(aspectInstanceNode);
			}
		}
	}

}
