package eu.dnetlib.data.collective.transformation.core.xsl.ext;

import java.io.StringReader;
import java.io.StringWriter;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Result;
import javax.xml.transform.Transformer;
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.XPathFactory;

import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.svenson.JSONParser;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.InputSource;

import eu.dnetlib.data.collective.transformation.engine.FunctionResults;
import eu.dnetlib.data.collective.transformation.engine.functions.Convert;
import eu.dnetlib.data.collective.transformation.engine.functions.IdentifierExtract;
import eu.dnetlib.data.collective.transformation.engine.functions.LookupRecord;
import eu.dnetlib.data.collective.transformation.engine.functions.ProcessingException;
import eu.dnetlib.data.collective.transformation.engine.functions.RegularExpression;
import eu.dnetlib.data.collective.transformation.engine.functions.Split;

/**
 * The class implements external XSLT functions
 * @author jochen
 *
 */
public class TransformationFunctionProxy {

	@SuppressWarnings("unused")
	private static final Log log = LogFactory.getLog(TransformationFunctionProxy.class);
	private static TransformationFunctionProxy tf;
	private RegularExpression regExprFunction = new RegularExpression();
	private Convert convertFunction;
	private IdentifierExtract identifierExtractFunction = new IdentifierExtract();
	private static DocumentBuilder docBuilder;
	private static Transformer transformer;
	private Split split = new Split();
	private Map<String, FunctionResults> mapOfResults = new HashMap<String, FunctionResults>();
	private LookupRecord lookupRecord;
	private static XPath xpath = XPathFactory.newInstance().newXPath();
	
	/**
	 * @return the transformationFunctionProxy instance
	 */
	public static TransformationFunctionProxy getInstance(){
		if ( tf == null ){
			tf = new TransformationFunctionProxy();
			try {
				DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
				dbf.setNamespaceAware(true);
				docBuilder = dbf.newDocumentBuilder();
				transformer  = TransformerFactory.newInstance().newTransformer();
				xpath.setNamespaceContext(new NamespaceContext() {
					
					@Override
					public Iterator getPrefixes(String namespaceURI) {
						// TODO Auto-generated method stub
						return null;
					}
					
					@Override
					public String getPrefix(String namespaceURI) {
						// TODO Auto-generated method stub
						return null;
					}
					
					@Override
					public String getNamespaceURI(String aPrefix) {
						if (aPrefix == null){
							throw new IllegalArgumentException("No prefix provided!");
						}else if (aPrefix.equals(XMLConstants.DEFAULT_NS_PREFIX)){
							return "http://namespace.openaire.eu";
						}else if (aPrefix.equals("dc")){
							return "http://purl.org/dc/elements/1.1/";
						}else{
							return XMLConstants.NULL_NS_URI;
						}
					}
				});

			} catch (Exception e) {
				log.fatal("error while instantiating DocumentBuilderFactory, Transfomer, Xpath.namespacecontext", e);
				throw new IllegalStateException(e);
			}
		}
		return tf;
	}
	
	/**
	 * @param uniqueKey
	 * @param i
	 * @return
	 */
	public String getValue(String uniqueKey, int i){
		if ( !mapOfResults.containsKey(uniqueKey)){
			throw new IllegalStateException("unknown key: " + uniqueKey);
		}
		return mapOfResults.get(uniqueKey).get(i);
	}
	
	/**
	 * @param uniqueKey
	 * @param i
	 * @return
	 * @deprecated
	 */
//	public String convert(String uniqueKey, int i){
//		if (mapOfResults == null){
//			return "transformationFunctionProxy_convert not initialized";
//		}else{
//			if (!mapOfResults.containsKey(uniqueKey)){
//				throw new IllegalStateException("unknown key: " + uniqueKey);
//			}
//			return mapOfResults.get(uniqueKey).get(i);			
//		}
//	}
	
	/**
	 * @param uniqueKey
	 * @param i
	 * @param aPos
	 * @return
	 * @deprecated
	 */
//	public String convert(String uniqueKey, int i, int aPos){
//		if (mapOfResults == null){
//			return "transformationFunctionProxy_convert not initialized";
//		}else{
//			if (!mapOfResults.containsKey(uniqueKey)){
//				throw new IllegalStateException("unknown key: " + uniqueKey);
//			}
//			return mapOfResults.get(uniqueKey).get(i, aPos);
//		}
//	}
	
	/**
	 * @param uniqueKey
	 * @param i
	 * @return
	 */
	public String extract(String uniqueKey, int i){
		if (mapOfResults == null){
			return "transformationFunctionProxy_extract not initialized";
		}else{
			if (!mapOfResults.containsKey(uniqueKey)){
				throw new IllegalStateException("unknown key: " + uniqueKey);
			}
			return mapOfResults.get(uniqueKey).get(i);			
		}		
	}
	
	/**
 	 * normalize values given as an input value by using a vocabulary 
	 * @param aInput - the value as a String
	 * @param aVocabularyName - the name of the vocabulary, which must be known for the vocabulary registry
	 * @return
	 */
	public synchronized String convertString(String aInput, String aVocabularyName){
		List<String> values = new LinkedList<String>();
		values.add(aInput);
		try {
			log.debug("conversion input: " + aInput);
			String conversionResult = convertFunction.executeSingleValue(aVocabularyName, values);
			log.debug("conversion result: " + conversionResult);
			return conversionResult;
		} catch (ProcessingException e) {
			log.fatal("convert failed for args 'input': " + aInput + " , 'vocabularyName': " + aVocabularyName, e);
			throw new IllegalStateException(e);
		}
	}

	/**
	 * normalize values given as a NodeList by using a vocabulary 
	 * @param aInput - the input values as NodeList
	 * @param aVocabularyName - the name of the vocabulary, which must be known for the vocabulary registry
	 * @return
	 */
	public synchronized String convert(NodeList aInput, String aVocabularyName){
		List<String> values = new LinkedList<String>();
		getTextFromNodeList(aInput, values);
		try {
			return convertFunction.executeSingleValue(aVocabularyName, values);
		} catch (ProcessingException e) {
			throw new IllegalStateException(e);
		}
	}
	
	public synchronized String convert(NodeList aInput, String aVocabularyName, String aDefaultPattern, String aFunction){
		List<String> values = new LinkedList<String>();
		getTextFromNodeList(aInput, values);
		try {
			List<String> results = convertFunction.executeFilterByParams(aVocabularyName, values, aDefaultPattern, aFunction);
			if (results.size() > 0)
				return results.get(0);
			else
				return "";
		} catch (ProcessingException e) {
			throw new IllegalStateException(e);
		}
	}
	
	private void getTextFromNodeList(NodeList aNodeList, List<String> aTextvalues){
		for (int i = 0; i < aNodeList.getLength(); i++){
			Node n = aNodeList.item(i);
			if (n.getNodeType() == Node.ELEMENT_NODE)
				getTextFromNodeList(n.getChildNodes(), aTextvalues);
			else if (n instanceof Text)
				aTextvalues.add(n.getNodeValue());
		}
	}

	/**
	 * substitutes using regular expression
	 * @param aInput
	 * @param aReplacement
	 * @param aRegularExpression
	 * @return
	 */
	public synchronized String regExpr(String aInput, String aReplacement, String aRegularExpression){
		try {
			int lastSlash = aRegularExpression.lastIndexOf("/");
			String trailingOptions = aRegularExpression.substring(lastSlash);
//			log.debug("trailingOptions: " + trailingOptions);
			int replacementSlash = aRegularExpression.substring(0, lastSlash).lastIndexOf("/");
			String replacementFromExpression = aRegularExpression.substring(replacementSlash + 1, lastSlash);
//			log.debug("replacementFromExpr lengt: " + replacementFromExpression.length() + ", value: " + replacementFromExpression);
			String newRegExpr = aRegularExpression.substring(0, replacementSlash + 1) + aReplacement + replacementFromExpression + trailingOptions;
//			log.debug("newRegExpr: " + newRegExpr);
			return regExprFunction.executeSingleValue(newRegExpr, aInput, aReplacement);
		} catch (ProcessingException e) {
			throw new IllegalStateException(e);
		}
	}
	
	public String lookup(String aIdentifier, String aPropertyKey){
		log.debug("functionProxy.lookup: param identifier: " + aIdentifier + " , key: " + aPropertyKey);
		return this.lookupRecord.getPropertyValue(aIdentifier, aPropertyKey);
	}

	public synchronized Collection<String> split(NodeList aInput, String aRegularExpression, String aCallId){
		try {
			List<String> textValues = new LinkedList<String>();
			getTextFromNodeList(aInput, textValues);
			return split.executeAllValues(textValues, aRegularExpression);
			//return split.executeSingleValue(textValues, aRegularExpression, aCallId);
		}catch (ProcessingException e){
			throw new IllegalStateException(e);
		}
	}
	
	public synchronized String split(String aCallId){
		try {
			return split.executeSingleValue(aCallId);
		}catch (ProcessingException e){
			throw new IllegalStateException(e);
		}		
	}
	
	/**
	 * extract content that match pattern given by a regular expression from a given node
	 * @param aXpathExprJson
	 * @param aInput
	 * @param aRegExpression
	 * @return nodeList
	 */
	public synchronized NodeList identifierExtract(String aXpathExprJson, Node aInput, String aRegExpression){
		String xpathExprJson = StringEscapeUtils.unescapeXml(aXpathExprJson);
		log.debug("unescape xpathExprJson: " + xpathExprJson);
		String regExpression = StringEscapeUtils.unescapeXml(aRegExpression);
		log.debug("unescape regExpr" + regExpression);

		try{
			List<String> xpathExprList = JSONParser.defaultJSONParser().parse(List.class, xpathExprJson);
			
			// workaround: rewrap, why ?
			DOMSource s = new DOMSource(aInput);
			StringWriter w = new StringWriter();
			Result r = new StreamResult(w);
			transformer.transform(s, r);
			Document doc = docBuilder.parse(new InputSource(new StringReader(w.toString())));
			
			return identifierExtractFunction.extract(xpathExprList, doc, regExpression, docBuilder.newDocument(), xpath);
		}catch(Exception e){
			log.fatal("identifierExtract failed for node value: " + aInput.getNodeValue(), e);
			throw new IllegalStateException(e.getMessage());
		}
	}
		
	/**
	 * @param key
	 * @param resultsFunction_getvalue
	 */
	public void setResults(String key, FunctionResults resultsFunction_getvalue) {
		mapOfResults.put(key, resultsFunction_getvalue);
	}

	/**
	 * @param convertFunction the convertFunction to set
	 */
	public void setConvertFunction(Convert convertFunction) {
		this.convertFunction = convertFunction;
	}

	/**
	 * @return the convertFunction
	 */
	public Convert getConvertFunction() {
		return convertFunction;
	}

	/**
	 * @return the lookupRecord
	 */
	public LookupRecord getLookupRecord() {
		return lookupRecord;
	}

	/**
	 * @param lookupRecord the lookupRecord to set
	 */
	public void setLookupRecord(LookupRecord lookupRecord) {
		this.lookupRecord = lookupRecord;
	}

}
