/**
 * File:    CustomXSLFunctions.java
 * Created: 21.03.2008
 *
 *
/*******************************************************************************
 * Copyright (c) 2008-2010  Open Wide (www.openwide.fr)
 *
 * 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:
 *    Guillaume Decarnin, Open Wide, Lyon, France
 *      - Creation and implementation
 *    Mickael Istria, Open Wide, Lyon, France
 *      - Refactoring to include it in common XSLT tools plugin
 *    Marc Dutoo, Open Wide, Lyon, France
 *      - #327474 - made all aspect properties available within XSLT
 *******************************************************************************/
package org.eclipse.jwt.transformations.xslt.tools;


import java.io.StringReader;
import java.io.StringWriter;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
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 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.apache.xalan.processor.TransformerFactoryImpl;
import org.eclipse.jwt.transformations.api.TransformationException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import com.ibm.icu.text.SimpleDateFormat;

/**
 * This utility class contains methods that are useful for introspection
 * of workflow files. It is useful to resolve EMF paths & co...
 * 
 * It is typically useful when using an XSL Transformation.
 * 
 * @author Guillaume Decarnin - Open Wide
 * @since 0.6
 */
@SuppressWarnings("nls")
public class CustomXslFunctions {

	private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	
    private static final String beforeOp = "^([^=<> ]+) ?"; // all chars without operator and space
    private static final String afterOp = " *([^&|\\(\\) ]+)([|&\\(\\) ]*)(.*)$"; // optional spaces, letters, operateurs, and all chars until the end
    
    private static final Pattern patternVar       = Pattern.compile("^(.*)([^\\.]+)\\.\\2(.*)$"); // *isApproved.isApproved*
    private static final Pattern patternEqual     = Pattern.compile(beforeOp + "==" + afterOp);
    private static final Pattern patternDifferent = Pattern.compile(beforeOp + "!=" + afterOp);
    private static final String ACTIVITY_NAME_PREFIX = "Activity";
    private static final String ROUTE_NAME_PREFIX = "Route";
    private static final String BLOCK_NAME_PREFIX = "Block";
    private static final String FINAL_NAME_PREFIX = "BonitaEnd-";

    private static HashSet<String> names = new HashSet<String>();

	//private static ThreadLocal<Model> JWT_MODEL = new ThreadLocal<Model>();
	private static ThreadLocal<Properties> PROPERTIES = new ThreadLocal<Properties>();
	private static ThreadLocal<Map<String, String>> RESOURCES = new ThreadLocal<Map<String, String>>();

	public static void setContext(/*Model jwtModel, */Properties properties, Map<String, String> resources) {
		//JWT_MODEL.set(jwtModel);
		PROPERTIES.set(properties);
		RESOURCES.set(resources);
	}
	
    
    public static String declareName(String name) {
    	names.add(name);
    	return "";
    }

    public static String generateActivityName() {
    	return generateName(ACTIVITY_NAME_PREFIX);
    }

    public static String generateFinalName() {
    	return generateName(FINAL_NAME_PREFIX);
    }
    
    public static String generateRouteName() {
    	return generateName(ROUTE_NAME_PREFIX);
    }

    public static String generateBlockName() {
    	return generateName(BLOCK_NAME_PREFIX);
    }

    private static String generateName(String prefix) {
    	String name;
    	int n = 1;
    	
    	do {
        	name = prefix + n;
        	n++;
    	} while (names.contains(name));
    	
    	declareName(name);
    	
    	return name;
    }
    
    public static String getDate() {
    	return dateFormat.format(new Date());
    }

	public static void clear() {
		names.clear();
	}

    public static String getBlockId(Node structuredActivityNode) throws TransformationException {
    	// if there is a "name" attribute, use it
    	Node nameNode = structuredActivityNode.getAttributes().getNamedItem("name");
    	if (nameNode != null) {
    		String name = nameNode.getTextContent();
    		if (name != null && name.length() > 0) {
    			return name;
    		}
    	}
    	
    	// else throw TransformationException
    	throw new TransformationException("The embedded subprocess must have a name.");
    }

    /**
     * "//@elements.0/@nodes.1 //@elements.1/@nodes.3" =>
     * <res>
     * 	 <...>   // Copy of //@elements.0/@nodes.1
     *   <...>   // Copy of //@elements.1/@nodes.3
     * </res>
     * 
     * 
     * @param nodesEmfPaths
     * @param root
     * @return the list of nodes that are referenced by this list of paths
     * @throws TransformationException 
     */
    public static Node getNodes(String nodesEmfPaths, Node root) throws TransformationException {
    	String[] emfNodes = nodesEmfPaths.split(" ");
    	Document doc = null;
    	try {
    		doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
    	} catch (Exception ex) {
    		throw new TransformationException("Could not create 'node' element", ex);
    	}
    	Node nodes = doc.createElement("res");
    	for (int i = 0; i < emfNodes.length; i++) {
    		nodes.appendChild(doc.importNode(getNode(emfNodes[i], root), true));
    	}
    	return nodes;
    }
    
    
    
    /**
     * @param emfPath the path of a node in EMF path language
     * @param root 
     * @return The {@link Node} described by the EMF path
     * @throws TransformationException 
     */
    private static Node getNode(String emfPath, Node root) throws TransformationException {
    	try {
    		String doc = createStringFromNode(root.getOwnerDocument());
        	XPath xPath = getXPath();
        	XPathExpression expression = xPath.compile(generateNodeXPath(emfPath).toString());
        	Node res = (Node) expression.evaluate(new InputSource(new StringReader(doc)), XPathConstants.NODE);
    		return res;
    	} catch (Exception ex) {
			throw new TransformationException("Could not process node", ex);
    	}
	}

	/**
     * "//@elements.0/@nodes.1/" -> "//elements[1]/nodes[2]/@name"
     * 
     * @param nodePath
     * @param root
     * @return XML values/value:
     * 		   <values>
     * 			   <value>@name</value>
     * 			   <value>...</value>
     * 			   <value>...</value>
     * 		   </values>
     */
    public static Node getValuesFromPath(Node nodePath, Node root) throws Exception {
    	return getValuesFromPath(nodePath, root, "name");
    }
     
    
    /**
     * "//@elements.0/@nodes.1/" -> "//elements[1]/nodes[2]/@attribute"
     * 
     * @param nodePath
     * @param root
     * @return XML values/value:
     * 		   <values>
     * 			   <value>@attribute</value>
     * 			   <value>...</value>
     * 			   <value>...</value>
     * 		   </values>
     */
    public static Node getValuesFromPath(Node nodePath, Node root, String attribute) throws Exception {
    	Node values = null;
    	
    	// create root node
    	DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    	Document doc = factory.newDocumentBuilder().newDocument();
    	values = doc.createElement("values");
    	
    	String[] originalPaths = nodePath.getTextContent().split(" ");
    	for (int i = 0; i < originalPaths.length; i++) {
    		String originalPath = originalPaths[i];
    		if (originalPath == null || originalPath.trim().length() == 0) {
    			continue;
    		}
    		
    		String value = getValueFromPath(originalPath, root, attribute);
    		
    		Node valueNode = doc.createElement("value");
    		valueNode.setTextContent(value);
    		values.appendChild(valueNode);				
    	}
    	
    	return values;
    }
    
    /**
     * "//@elements.0/@nodes.1" -> "//elements[1]/nodes[2]/@name"
     * 
     * @param nodePath The context node
     * @param root The xpdl root node
     * @return the value for the 'name' attribute of the context node,
     * or ERROR([cause]) if an exception occured 
     */
    public static String getValueFromPath(Node nodePath, Node root) throws Exception {
        return getValueFromPath(nodePath, root, "name");
    }
    
    /**
     * "//@elements.0/@nodes.1" -> "//elements[1]/nodes[2]/@attribute"
     * 
     * @param nodePath The context node
     * @param root The xpdl root node
     * @param attribute The attribute to lookup
     * @return the value for the attribute of the context node,
     * or ERROR([cause]) if an exception occured 
     */
    public static String getValueFromPath(Node nodePath, Node root, String attribute) throws Exception {
    	try {
	    	String value = "";
	    	
	    	String text = nodePath.getTextContent();
	    	if (text == null && nodePath.getFirstChild() != null &&
	    			nodePath.getFirstChild().getLocalName() != null && nodePath.getFirstChild().getLocalName().length() == 0)
	    		text = nodePath.getFirstChild().getTextContent();
	    		
	    	if (text == null)
	    		throw new TransformationException("Could not get text for node [" + nodePath.getLocalName() + "]");
	    	
			String[] originalPaths = text.split(" ");
	    	if (originalPaths.length > 1) {
	    		throw new Exception("There are " + originalPaths.length + " paths: use getValuesFromPath for \"" + text + "\"");
	    	}
	    	
	        String originalPath = originalPaths[0];
	        value = getValueFromPath(originalPath, root, attribute);
	            
	        return value;
    	} catch (Exception ex) {
    		return "ERROR(" + ex.getMessage() + ")";
    	}
    }
    

    private static String getValueFromPath(String originalPath, Node root, String attribute) throws XPathExpressionException, TransformerException, TransformationException {
   		String value;
		StringBuilder xPathExpression = generateNodeXPath(originalPath);

    	String xPathExpressionName = xPathExpression.toString() + "/@" + attribute;
    	XPath xPath = getXPath();
    	XPathExpression expression = xPath.compile(xPathExpressionName);
    	String document = createStringFromNode(root);
    	InputSource inputSource = new InputSource(new StringReader(document));
    	value = expression.evaluate(inputSource);
    	return value;
    }

	private static XPath getXPath() throws TransformationException {
		XPathFactory xPathFactory = null;
		try {
			xPathFactory = XPathFactory.newInstance();
		} catch (Exception e) {
			throw new TransformationException("Wrong xpath/xslt configuration", e);
		}
    	XPath xPath = xPathFactory.newXPath();
		HashMap<String, String> namespaces = getNamespaces();
    	xPath.setNamespaceContext(new CustomNamespaceContext(namespaces));
		return xPath;
	}

	private static HashMap<String, String> getNamespaces() {
		HashMap<String, String> namespaces = new HashMap<String, String>();
    	namespaces.put("xsi", "http://www.w3.org/2001/XMLSchema-instance");
    	namespaces.put("core", "org.eclipse.jwt/core");
    	namespaces.put("data", "org.eclipse.jwt/data");
    	namespaces.put("processes", "org.eclipse.jwt/processes");
		return namespaces;
	}

    /**
     * Generates the XPath condition that match the EMF node path
     * @param emfPath the EMF node path
     * @return the XPath for this EMF
     */
	private static StringBuilder generateNodeXPath(String emfPath) {
    	/*
    	 * building xPathExpression :
    	 * 1. start with "//@elements.0/@nodes.1"
    	 * 2. remove all "@" : "//elements.0/nodes.1"
    	 * 3. replace "//" by "/core:Model/"
    	 * 4. remove ".", add "[ ]" and incremente indexes : "//elements[1]/nodes[2]"
    	 * 5. append "/@attribute" : "//elements[1]/nodes[2]/@attribute"
    	 */
    	StringBuilder xPathExpression = new StringBuilder();

    	String path = emfPath;
    	path = path.replaceAll("@", "");
    	path = path.replaceAll("//", "/core:Model/");

    	Pattern pattern = Pattern.compile("([^\\.]*)\\.(\\d+)([^/]*)"); // (any char).(integer)(any char)/
    	Matcher matcher = pattern.matcher(path);
    	while (matcher.find()) {
    		/* 
    		 * exemple:
    		 * path://@elements.0/@nodes.1
    		 * 
    		 * iteration 1:
    		 * groupe0://@elements.0
    		 * groupe1://@elements
    		 * groupe2:0
    		 * groupe3:
    		 * 
    		 * iteration 2:
    		 * groupe0:/@nodes.1
    		 * groupe1:/@nodes
    		 * groupe2:1
    		 * groupe3:
    		 */
    		//for (int i = 0; i <= matcher.groupCount(); i++) {
    		//    System.out.println("groupe" + i + ":" + matcher.group(i));
    		//}

    		int index = Integer.parseInt(matcher.group(2));
    		index++;

    		xPathExpression.append(matcher.group(1));
    		xPathExpression.append('[');
    		xPathExpression.append(index);
    		xPathExpression.append(']');
    	}
		return xPathExpression;
	}
    
    private static String applyPatternVar(String condition) {
        Pattern pattern = patternVar;
        Matcher matcher = pattern.matcher(condition);
        while (matcher.matches()) {
            condition = matcher.group(1) + matcher.group(2) + matcher.group(3);
            matcher = pattern.matcher(condition);
        }
        return condition;
    }
    
    private static String applyPatternsOp(String condition) {
        condition = applyPatternEqual(condition);
        condition = applyPatternDifferent(condition);
        return condition;
    }
    
    private static String applyPatternEqual(String condition) {
        StringBuilder result = new StringBuilder();
        
        Pattern pattern = patternEqual;
        Matcher matcher = pattern.matcher(condition);
        if (matcher.matches()) {
//            for (int i = 1; i <= matcher.groupCount(); i++) {
//                System.out.println(i + ":" + matcher.group(i));
//            }
            
            result.append(matcher.group(1));
            result.append(".toString().equals(\"");
            result.append(matcher.group(2));
            result.append("\")");
            result.append(matcher.group(3));
            // recursif call for next groups
            result.append(applyPatternsOp(matcher.group(4)));
        }
        else {
            result.append(condition);
        }
        return result.toString();
    }
    
    private static String applyPatternDifferent(String condition) {
        StringBuilder result = new StringBuilder();
        
        Pattern pattern = patternDifferent;
        Matcher matcher = pattern.matcher(condition);
        if (matcher.matches()) {
            result.append("!");
            result.append(matcher.group(1));
            result.append(".toString().equals(\"");
            result.append(matcher.group(2));
            result.append("\")");
            result.append(matcher.group(3));
            // recursif call for next groups
            result.append(applyPatternsOp(matcher.group(4)));
        }
        else {
            result.append(condition);
        }
        
        return result.toString();
    }
    
    /**
     * in:  (Branche.Branche == tests &amp;&amp; isApproved.isApproved != true) || Reisemittel.Reisemittel == ok
     * out: (Branche.equals("tests") &amp;&amp; !isApproved.equals("true")) || Reisemittel.equals("ok")
     * 
     * guard:
     * <guard name="Approve" textualdescription="Approve" shortdescription="isApproved.isApproved == true">
     *     <detailedSpecification data="//@subpackages.2/@subpackages.1/@elements.4" attribute="isApproved" value="true"/>
     * </guard>
     *   
     * @param jwtCondition
     * @param guard
     * @param root
     * @return
     */
    public static String getCondition(final String jwtCondition, Node guard, Node root) {
//        System.out.println("jwtCondition:" + jwtCondition);
        String condition = jwtCondition;
        
        condition = condition.replaceAll("&amp;", "&");
        condition = applyPatternVar(condition);
        condition = applyPatternsOp(condition);
        
        return condition;
    }

    /**
     * Create a String result from DOM NodeList
     *
     * @param nodes
     *            the DOM Nodes. Must not be null
     * @return a String representation of the DOM Document
     * @throws TransformerException
     */
    public static String createStringFromNode(Node node) throws TransformerException {
        final StringWriter out = new StringWriter();
        final Result resultStream = new StreamResult(out);
        final TransformerFactory tFactory = TransformerFactoryImpl.newInstance();
        Transformer transformer;
        transformer = tFactory.newTransformer();
        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
        
//        node.normalize();
        final Source source = new DOMSource(node);
        transformer.transform(source, resultStream);
        
        return out.toString();
    }

    /**
     * Create a String result from DOM NodeList
     *
     * @param nodes
     *            the DOM Nodes. Must not be null
     * @return a String representation of the DOM Document
     * @throws TransformerException
     */
    public static String createStringFromNodeList(NodeList nodes) throws TransformerException {
        final StringWriter out = new StringWriter();
        final Result resultStream = new StreamResult(out);
        final TransformerFactory tFactory = TransformerFactory.newInstance();
        Transformer transformer;
        transformer = tFactory.newTransformer();
        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
        
        for (int i = 0; i < nodes.getLength(); i++) {
            Node node = nodes.item(i);
//            node.normalize();
            final Source source = new DOMSource(node);
            transformer.transform(source, resultStream);
        }
        
        return out.toString();
    }
    
    public static String getUpperCaseValueFromPath(Node nodePath, Node root) throws Exception {
    	return getValueFromPath(nodePath, root).toUpperCase();
    }
    
    public static String getUpperCaseValueFromPath(Node nodePath, Node root, String attribute) throws Exception {
    	return getValueFromPath(nodePath, root, attribute).toUpperCase();
    }
	
	/**
	 * Returns the corresponding aspect property
	 * (see ExtractPropertyService)
	 * TODO maybe better as an extension point ?
	 * @param propName
	 * @return
	 */
	public static String getPropertyValue(String propName) {
		return PROPERTIES.get().getProperty(propName);
	}


}
