package eu.dnetlib.data.collective.transformation.engine.core;

import java.io.StringReader;
import java.io.StringWriter;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.Set;

import javax.xml.namespace.NamespaceContext;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import eu.dnetlib.data.collective.transformation.core.schema.SchemaElement;
import eu.dnetlib.data.collective.transformation.core.schema.SchemaInspector;
import eu.dnetlib.data.collective.transformation.core.xsl.XslConstructor;
import eu.dnetlib.data.collective.transformation.core.xsl.XsltConstants;
import eu.dnetlib.data.collective.transformation.core.xsl.XslElement;
import eu.dnetlib.data.collective.transformation.rulelanguage.IRule;
import eu.dnetlib.data.collective.transformation.rulelanguage.RuleLanguageParser;
import eu.dnetlib.data.collective.transformation.rulelanguage.Rules;
import eu.dnetlib.data.collective.transformation.rulelanguage.util.Converter;

/**
 * @author jochen
 *
 */
public class StylesheetBuilder {

	private static final Log log = LogFactory.getLog(StylesheetBuilder.class);
	private SchemaInspector schemaInspector;
	private RuleLanguageParser ruleLanguageParser;
	private NamespaceContext namespaceContext;
	// implicit rule for deleted records
	private final String elementNameIndicatingDeletedRecords = "header";
	private final String attributeNameIndicatingDeletedRecords = "status";
	private final String attributeValueIndicatingDeletedRecords = "deleted";
	private final String elementNameAbout = "about";

	public String createTemplate(){
		if (schemaInspector == null || ruleLanguageParser == null || namespaceContext == null){
			throw new IllegalStateException("StylesheetBuidler is not initialized with schemaInspector or ruleLanguageParser or namespaceContext.");
		}
		if (!schemaInspector.isInspected()){
			throw new IllegalStateException("schemaInspector must first inspect in order to create a stylesheet.");
		}
		StringBuilder builder = new StringBuilder();
		XslElement templateRoot = new XslElement("templateroot");
		templateRoot.addBoundPrefix(XsltConstants.nsXsl);
		templateRoot.addAllBoundPrefixes(Converter.getBoundPrefixes(this.ruleLanguageParser.getNamespaceDeclarations()));
		XslElement template = new XslElement(XsltConstants.template);
		template.addAttribute("match", "/");
		Map<String, Set<IRule>> ruleMapping = this.ruleLanguageParser.getElementMappingRules();
		Map<String, IRule> variableRuleMapping = this.ruleLanguageParser.getVariableMappingRules();
		Map<String, IRule> templateRuleMapping = this.ruleLanguageParser.getTemplateMappingRules();
		Queue<String> templateQueue = new LinkedList<String>();
		XslElement rootField = new XslElement(schemaInspector.getRootElement());
		XslConstructor xslConstructor = new XslConstructor();
		
		/**
		 * 		 * 
		 */
//		Iterator<String> keyIterator = ruleMapping.keySet().iterator();
//		while(keyIterator.hasNext()){
//			System.out.println("stylesheetbuilder: key: " + keyIterator.next());
//		}
		/*
		 * 
		 */
		
		int templateCounter = 1;
		int standaloneTemplateCounter = 1;
		// write variables at the beginning of the templateRoot
		for (String variable: variableRuleMapping.keySet()){
			IRule currentVariableRule = variableRuleMapping.get(variable);
			templateRoot.addEnclosedElements(xslConstructor.writeOutVariableRule((Rules)currentVariableRule));
		}
		Map<String, String> targetFieldTemplateMap = new LinkedHashMap<String, String>();
		for (String keyTemplate: templateRuleMapping.keySet()){
			IRule currentTemplateRule = templateRuleMapping.get(keyTemplate);
			targetFieldTemplateMap.put(((Rules)currentTemplateRule).getFunctionCall().getParameters().get("elementName"), "templName" + standaloneTemplateCounter);
			templateRoot.addEnclosedElements(xslConstructor.writeOutRecursiveTemplate((Rules)currentTemplateRule , "templName" + (standaloneTemplateCounter++), this.ruleLanguageParser.getNamespaceDeclarations()).asXml());
		}
		XslElement chooseField = new XslElement(XsltConstants.choose);
		XslElement whenField = new XslElement(XsltConstants.when, 
				"test", "//" + this.elementNameIndicatingDeletedRecords + "/@" + this.attributeNameIndicatingDeletedRecords + "='" + this.attributeValueIndicatingDeletedRecords + "'");
		XslElement otherwiseField = new XslElement(XsltConstants.otherwise);
		String templateAboutName = "applyAbout";

		// write schema elements
		for (SchemaElement element: schemaInspector.getChildElements()){
			if (!element.containsSimpleType()){
				String complexTypeTemplateName = "apply" + (templateCounter++);
				XslElement complexTypeTemplate = new XslElement(XsltConstants.template, "name", complexTypeTemplateName);
				// will contain only other elements			
				XslElement childField = new XslElement(XsltConstants.element, "name", element.getName());
				if (element.getName().equals(this.elementNameIndicatingDeletedRecords)){
					XslElement ifField = new XslElement(XsltConstants.ifCondition, "test", "//" + this.elementNameIndicatingDeletedRecords + "/@" + this.attributeNameIndicatingDeletedRecords);
					XslElement attributeField = new XslElement(XsltConstants.attribute, "name", this.attributeNameIndicatingDeletedRecords);
					XslElement valueofField = new XslElement(XsltConstants.valueOf, "select", "//" + this.elementNameIndicatingDeletedRecords + "/@" + this.attributeNameIndicatingDeletedRecords);
					attributeField.addEnclosedElements(valueofField.asXml());
					ifField.addEnclosedElements(attributeField.asXml());
					childField.addEnclosedElements(ifField.asXml());
				}

				if (element.getName().equals(this.elementNameAbout)){
					XslElement templateAbout = new XslElement(XsltConstants.template, "name", templateAboutName);
					templateAbout.addEnclosedElements(xslConstructor.writeOutApplyAbout());
					templateQueue.add(templateAbout.asXml());
				}
				
				for (SchemaElement childElement: element.getChildList()){
					String currentKey = getPrefixedElementName(childElement);
					if (childElement.containsSimpleType()){
						log.debug("currentKey: " + currentKey);
						if (ruleMapping.containsKey(currentKey)){
							for (IRule currentRule: ruleMapping.get(currentKey)){
								if (currentRule instanceof Rules){
									log.debug(" has Set? " + ((Rules)currentRule).hasSet() );
									log.debug(" has Condition? " + ((Rules)currentRule).hasCondition() );
									
									if ( !((Rules)currentRule).hasCondition()){
										if ( !((Rules)currentRule).hasSet() ){
											if (currentRule.definesTemplateMatch()){
												String templateName = ((Rules)currentRule).getTemplateMatch();
												XslElement subTemplate = new XslElement(XsltConstants.template, "match", templateName);
												subTemplate.addEnclosedElements(xslConstructor.writeOutRuleCopy((Rules)currentRule, currentKey));
												templateQueue.add(subTemplate.asXml());
												childField.addEnclosedElements(xslConstructor.writeOutApplyTemplates(((Rules)currentRule).getProperties().getProperty("applyTemplateSelectExpression")));
											}else{
												childField.addEnclosedElements(xslConstructor.writeOutRule((Rules)currentRule, currentKey));												
											}
										}else{
											childField.addEnclosedElements(xslConstructor.writeOutRuleComplex((Rules)currentRule, currentKey));
										}
									}else{
										// has condition										

										if ( ((Rules)currentRule).getCondition().isPrimary((Rules)currentRule) &&
												((Rules)currentRule).getUniqueName().equals( ((Rules)currentRule).getCondition().getSecondaryRule().getUniqueName() ) ){
											
											if ( ((Rules)currentRule).getCondition().getApplyExpression() != null ){
												String templateName = "apply" + (templateCounter++);
												XslElement subTemplate = new XslElement(XsltConstants.template, "name", templateName);
												subTemplate.addEnclosedElements(xslConstructor.writeOutApplyConditionalTemplateChoose((Rules)currentRule));
												templateQueue.add(subTemplate.asXml());
												childField.addEnclosedElements(xslConstructor.writeOutCallTemplate(templateName));												
											}else{
												// a condition with alternative rules for the same output elements
												childField.addEnclosedElements(xslConstructor.writeOutConditionalChoose((Rules) currentRule));												
											}											
										}else if ( ! ((Rules)currentRule).getCondition().getPrimaryRule().getUniqueName().equals( ((Rules)currentRule).getCondition().getSecondaryRule().getUniqueName() )  ){
											// a condition with alternative rules for distinct output elements											
											if ( ((Rules)currentRule).getCondition().getApplyExpression() != null ){
												// has apply expression
												String templateName = "apply" + (templateCounter++);
												XslElement subTemplate = new XslElement(XsltConstants.template, "name", templateName);
												subTemplate.addEnclosedElements(xslConstructor.writeOutApplyConditionalTemplateIf((Rules)currentRule, false));
												templateQueue.add(subTemplate.asXml());
												childField.addEnclosedElements(xslConstructor.writeOutCallTemplate(templateName));
											}else{
												childField.addEnclosedElements(xslConstructor.writeOutConditionalIf((Rules) currentRule));
											}
										}
									}
								}else{
									// only Rules instances are supported
								}
							}
						}else if (targetFieldTemplateMap.containsKey(currentKey)){
							childField.addEnclosedElements(xslConstructor.writeOutCallTemplate(targetFieldTemplateMap.get(currentKey)));
						}else{
						
							// no rule defined, check if element is mandatory
							if (childElement.isMandatory()){
								XslElement emptyField = new XslElement(currentKey);
								childField.addEnclosedElements(emptyField.asXml());
							}
						}
					}else{
						// complex-type elements
												
						if (ruleMapping.containsKey(currentKey)){
							for (IRule currentRule: ruleMapping.get(currentKey)){
								if (currentRule instanceof Rules){
									if ( !((Rules)currentRule).hasCondition() ){
										log.debug("stylesheetbuilder.complexType NO CONDITION: " + childElement.getName());
										childField.addEnclosedElements(xslConstructor.writeOutRuleComplex((Rules)currentRule, currentKey));
									}else{
										// log.debug("stylesheetbuilder.complexType HAS CONDITION: ONLY PARTLY IMPLEMENTED !!!!!!!!!!");
										
										if ( ((Rules)currentRule).getCondition().isPrimary((Rules)currentRule) &&
												((Rules)currentRule).getUniqueName().equals( ((Rules)currentRule).getCondition().getSecondaryRule().getUniqueName() ) ){
										//	log.debug("condition: complex rule with same output elements");
											if ( ((Rules)currentRule).getCondition().getApplyExpression() != null ){
												log.debug("APPLY expression rules for complex-type elements NOT YET SUPPORTED");
											//	log.debug("complex rule with apply expression: NOT YET IMPLEMENTED !!!!!!!!!!!!");
											}else{
												// a condition with alternative rules for the same output elements
												childField.addEnclosedElements(xslConstructor.writeOutConditionalChooseComplex((Rules) currentRule));
											}
										}else if ( ! ((Rules)currentRule).getCondition().getPrimaryRule().getUniqueName().equals( 
												((Rules)currentRule).getCondition().getSecondaryRule().getUniqueName() )  ){
											log.debug("CURRENTLY UNSUPPORTED!!!");
											// a condition with alternative rules for distinct output elements											
											if ( ((Rules)currentRule).getCondition().getApplyExpression() != null ){
												log.debug("APPLY expression rules for complex-type elements NOT YET SUPPORTED");

												String templateName = "apply" + (templateCounter++);
												XslElement subTemplate = new XslElement(XsltConstants.template, "name", templateName);
												subTemplate.addEnclosedElements(xslConstructor.writeOutApplyConditionalTemplateIf((Rules)currentRule, true));
												templateQueue.add(subTemplate.asXml());
												childField.addEnclosedElements(xslConstructor.writeOutCallTemplate(templateName));
											
//												// has apply expression
//												String templateName = "apply" + (templateCounter++);
//												XslElement subTemplate = new XslElement(XsltConstants.template, "name", templateName);
//												subTemplate.addEnclosedElements(xslConstructor.writeOutApplyConditionalTemplateIf((Rules)currentRule));
//												templateQueue.add(subTemplate.asXml());
//												childField.addEnclosedElements(xslConstructor.writeOutCallTemplate(templateName));
											}else{
												childField.addEnclosedElements(xslConstructor.writeOutConditionalIfComplex((Rules) currentRule));
											}
										}
									}
								}
							}
						}
					}
				}
				if ( !(childField.isEmpty() && !element.isMandatory()) ){
					complexTypeTemplate.addEnclosedElements(childField.asXml());
					templateQueue.add(complexTypeTemplate.asXml());
					
					if (element.getName().equals(this.elementNameIndicatingDeletedRecords)){
						whenField.addEnclosedElements(xslConstructor.writeOutCallTemplate(complexTypeTemplateName));
					}
					otherwiseField.addEnclosedElements(xslConstructor.writeOutCallTemplate(complexTypeTemplateName));

//					rootField.addEnclosedElements(childField.asXml());						
				}
				
			}
		}
		
		chooseField.addEnclosedElements(whenField.asXml());
		otherwiseField.addEnclosedElements(xslConstructor.writeOutCallTemplate(templateAboutName));
		chooseField.addEnclosedElements(otherwiseField.asXml());
		rootField.addEnclosedElements(chooseField.asXml());
		template.addEnclosedElements(rootField.asXml());

		templateRoot.addEnclosedElements(template.asXml());
		// add sub-templates from queue
		for (String templateCode: templateQueue){
			templateRoot.addEnclosedElements(templateCode);
		}
		builder.append(templateRoot.asXml());
		log.debug(dumpStylesheetTemplate(builder.toString()));
		return builder.toString();
	}

	String dumpStylesheetTemplate(String aTemplate){
		StringWriter w = new StringWriter();
		Source s = new StreamSource(new StringReader(aTemplate));
		Result r = new StreamResult(w);
		Transformer t;
		try {
			t = TransformerFactory.newInstance().newTransformer();
			t.setOutputProperty(OutputKeys.METHOD, "xml");
			t.setOutputProperty(OutputKeys.INDENT, "yes");
			t.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
			t.transform(s,  r);
		} catch (Exception e) {
			log.fatal(e);
		}
		return w.toString();
	}
	
	/**
	 * @param schemaInspector the schemaInspector to set
	 */
	public void setSchemaInspector(SchemaInspector schemaInspector) {
		this.schemaInspector = schemaInspector;
	}
	
	/**
	 * @return the schemaInspector
	 */
	public SchemaInspector getSchemaInspector() {
		return schemaInspector;
	}

	/**
	 * @return the ruleLanguageParser
	 */
	public RuleLanguageParser getRuleLanguageParser() {
		return ruleLanguageParser;
	}

	/**
	 * @param ruleLanguageParser the ruleLanguageParser to set
	 */
	public void setRuleLanguageParser(RuleLanguageParser ruleLanguageParser) {
		this.ruleLanguageParser = ruleLanguageParser;
	}
	
	/**
	 * @param namespaceContext the namespaceContext to set
	 */
	public void setNamespaceContext(NamespaceContext namespaceContext) {
		this.namespaceContext = namespaceContext;
	}

	/**
	 * @return the namespaceContext
	 */
	public NamespaceContext getNamespaceContext() {
		return namespaceContext;
	}


	private String getPrefixedElementName(SchemaElement aElement){
		return ( this.namespaceContext.getPrefix(aElement.getTargetNamespace()) + ":" + aElement.getName() );
	}
	
}
