package lu.tudor.santec.gecamed.importexport.utils;

import java.io.StringReader;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

/**
 * This Class mimics the private XPathAPI of Java but uses the public methods
 * and objects. It can be used as a drop in replacement where the internal api
 * is used.
 * 
 * @author Matthias Kutscheid
 * 
 */
public class XPathAPI {

	/** A cache for already generated XPath expressions */
	private static final Map<String, XPathExpression> CACHE = new HashMap<String, XPathExpression>();
	private static final Map<String, XPathExpression> UNCHECKED_CACHE = new HashMap<String, XPathExpression>();
	/**
	 * The defined namespaces. By default only the gecamed import namespace is
	 * defined with an empty prefix. This can be extended as needed
	 */
	private static final NamespaceContext NAMESPACES = new NamespaceContext() {

		public Iterator<?> getPrefixes(String arg0) {
			return null;
		}

		public String getPrefix(String arg0) {
			return null;
		}

		public String getNamespaceURI(String prefix) {
			if (prefix.equals("")) {
				// the default namespace
				return "http://gecamed.lu/schemas/import";
			}
			return null;
		}
	};
	/** The XPath compiler that is used through this class */
	private static final XPath xpath = XPathFactory.newInstance().newXPath();
	static {
		// set the namespace context on the static XPath compiler
		xpath.setNamespaceContext(NAMESPACES);
	}

	/**
	 * Get the compiled expression for the given xpath from the cache if
	 * possible. If there is nothing in the cache a new XPathExpression will be
	 * compiled and put into the said cache.
	 * 
	 * @param xPathToChild
	 *            The xpath to be compiled
	 * @return A precompiled xpath expression for the given String
	 * @throws XPathExpressionException
	 *             If the given string was not a valid xpath expression.
	 */
	private static XPathExpression getExpressionForXPath(String xPathToChild, boolean checkXPath) throws XPathExpressionException {
		XPathExpression expression;
		if (checkXPath) {
			expression = CACHE.get(xPathToChild);
		} else {
			expression = UNCHECKED_CACHE.get(xPathToChild);
		}
		
		if (expression == null) {
			String modifiedXPath = xPathToChild;
			if (checkXPath) {
				// adjust the xpath in several steps
				if (!modifiedXPath.equals(".")) {
					if (!modifiedXPath.startsWith("/")) {
						// add the child identifier if nothing is given
						modifiedXPath = "./" + modifiedXPath;
					}
					if (modifiedXPath.contains("/")) {
						modifiedXPath = modifiedXPath.replaceAll("/([^\\./:])", "/:$1").replaceAll("@", "@:");
					}
				}
			}
			expression = xpath.compile(modifiedXPath);
			if (checkXPath) {
				CACHE.put(xPathToChild, expression);
			} else {
				UNCHECKED_CACHE.put(xPathToChild, expression);
			}
		}
		return expression;
	}
	
	/**
	 * Select a single node from another node
	 * @param parent The parent to be used as the context node
	 * @param xPathToChild The xpath to the child. Its context is the parent node.
	 * @return The Node that has been found under the given xpath or null if none existed.
	 * @throws XPathExpressionException If the given XPath was not correct
	 */
	public static Node selectSingleNode(Node parent, String xPathToChild) throws XPathExpressionException {
		XPathExpression expression = getExpressionForXPath(xPathToChild, true);
		return (Node) expression.evaluate(parent, XPathConstants.NODE);
	}
	
	/**
	 * Select a single node from another node
	 * @param parent The parent to be used as the context node
	 * @param xPathToChild The xpath to the child. Its context is the parent node.
	 * @param checkXPath This is for the labo module, where you cannot access the XML document in the corrected form
	 * @return The Node that has been found under the given xpath or null if none existed.
	 * @throws XPathExpressionException If the given XPath was not correct
	 */
	public static Node selectSingleNode(Node parent, String xPathToChild, boolean checkXPath) throws XPathExpressionException {
		XPathExpression expression = getExpressionForXPath(xPathToChild, checkXPath);
		return (Node) expression.evaluate(parent, XPathConstants.NODE);
	}
	
	
	/**
	 * Select a single node of the XML String
	 * @param xmlData The XML document as String
	 * @param xPathToChild The xpath to the child. Its context is the parent node.
	 * @param checkXPath This is for the labo module, where you cannot access the XML document in the corrected form
	 * @return The Node that has been found under the given xpath or null if none existed.
	 * @throws Exception If the given XPath or the XML String was not correct
	 */
	public static Node selectSingleNode(String xmlData, String xPathToChild, boolean checkXPath) throws Exception 
	{
		Document document =  DocumentBuilderFactory.newInstance().newDocumentBuilder()
				.parse(new InputSource(new StringReader(xmlData)));
		return selectSingleNode(document, xPathToChild, checkXPath);
	}
	
	
	/**
	 * Select several nodes from another node
	 * @param parent The parent to be used as the context node
	 * @param xPathToChild The xpath to the child. Its context is the parent node.
	 * @return The nodes that have been found under the given xpath
	 * @throws XPathExpressionException If the given XPath was not correct
	 */
	public static NodeList selectNodeIterator(Node parent, String xPathToChild) throws XPathExpressionException {
		return selectNodeIterator(parent, xPathToChild, true);
	}
	
	/**
	 * Select several nodes from another node
	 * @param parent The parent to be used as the context node
	 * @param xPathToChild The xpath to the child. Its context is the parent node.
	 * @param checkXPath This is for the labo module, where you cannot access the XML document in the corrected form
	 * @return The nodes that have been found under the given xpath
	 * @throws XPathExpressionException If the given XPath was not correct
	 */
	public static NodeList selectNodeIterator(Node parent, String xPathToChild, boolean checkXPath) throws XPathExpressionException {
		XPathExpression expression = getExpressionForXPath(xPathToChild, checkXPath);
		return (NodeList) expression.evaluate(parent, XPathConstants.NODESET);
	}
	
	
	/**
	 * Select several nodes from the XML String
	 * @param xmlData The XML document as String
	 * @param xPathToChild The xpath to the child. Its context is the parent node.
	 * @param checkXPath This is for the labo module, where you cannot access the XML document in the corrected form
	 * @return The nodes that have been found under the given xpath
	 * @throws Exception If the given XPath or XML String was not correct
	 */
	public static NodeList selectNodeIterator (String xmlData, String xPathToChild, boolean checkXPath) throws Exception 
	{
		Document document =  DocumentBuilderFactory.newInstance().newDocumentBuilder()
				.parse(new InputSource(new StringReader(xmlData)));
		return selectNodeIterator(document, xPathToChild, checkXPath);
	}
	
	
	/**
	 * Get the value of a single node selected by the xpath
	 * 
	 * @param parent
	 *            The parent to be used as the context node
	 * @param xPathToChild
	 *            The xpath to the child. Its context is the parent node.
	 * @param checkXPath
	 *            This is for the labo module, where you cannot access the XML document in the corrected form
	 * @return The value of the Node that has been found under the given xpath <b>or null</b> if none existed.
	 * @throws XPathExpressionException
	 *             If the given XPath was not correct
	 */
	public static String selectNodeValue(Node parent, String xPathToChild, boolean checkXPath) throws XPathExpressionException {
		Node node = selectSingleNode(parent, xPathToChild, checkXPath);
		if (node != null) {
			return node.getNodeValue();
		} else {
			return null;
		}
	}
	
	
	/**
	 * Get the value of a single node selected by the xpath as String
	 * @param parent The parent to be used as the context node
	 * @param xPathToChild The xpath to the child. Its context is the parent node.
	 * @param checkXPath This is for the labo module, where you cannot access the XML document in the corrected form
	 * @return The value of the Node that has been found under the given xpath <b>or empty string</b> if none existed.
	 * @throws XPathExpressionException If the given XPath was not correct
	 */
	public static String selectNodeString (Node parent, String xPathToChild, boolean checkXPath) throws XPathExpressionException 
	{
		Node node = selectSingleNode(parent, xPathToChild, checkXPath);
		if (node != null)
		{
			return node.getNodeValue();
		}
		else
		{
			return "";
		}
	}
}
