package eu.dnetlib.data.collective.transformation.rulelanguage.visitor;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

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

import eu.dnetlib.data.collective.transformation.rulelanguage.Argument;
import eu.dnetlib.data.collective.transformation.rulelanguage.Condition;
import eu.dnetlib.data.collective.transformation.rulelanguage.IRule;
import eu.dnetlib.data.collective.transformation.rulelanguage.Rules;
import eu.dnetlib.data.collective.transformation.rulelanguage.RulesSet;
import eu.dnetlib.data.collective.transformation.rulelanguage.Argument.Type;
import eu.dnetlib.data.collective.transformation.rulelanguage.parser.ASTMyAssign;
import eu.dnetlib.data.collective.transformation.rulelanguage.parser.ASTMyAttribute;
import eu.dnetlib.data.collective.transformation.rulelanguage.parser.ASTMyCondition;
import eu.dnetlib.data.collective.transformation.rulelanguage.parser.ASTMyCopy;
import eu.dnetlib.data.collective.transformation.rulelanguage.parser.ASTMyEmpty;
import eu.dnetlib.data.collective.transformation.rulelanguage.parser.ASTMyImport;
import eu.dnetlib.data.collective.transformation.rulelanguage.parser.ASTMyNs;
import eu.dnetlib.data.collective.transformation.rulelanguage.parser.ASTMyOp;
import eu.dnetlib.data.collective.transformation.rulelanguage.parser.ASTMyPreprocess;
import eu.dnetlib.data.collective.transformation.rulelanguage.parser.ASTMyScript;
import eu.dnetlib.data.collective.transformation.rulelanguage.parser.ASTMyScript.SCRIPTTYPE;
import eu.dnetlib.data.collective.transformation.rulelanguage.parser.ASTMySet;
import eu.dnetlib.data.collective.transformation.rulelanguage.parser.ASTMySkip;
import eu.dnetlib.data.collective.transformation.rulelanguage.util.Converter;
import eu.dnetlib.data.collective.transformation.rulelanguage.util.FunctionCall;

/**
 * Implementation of the visitor pattern; maps production rules into Java data structures
 * @author jochen
 *
 */
public class RuleLanguageVisitor extends AbstractVisitor{

	private static final Log log = LogFactory.getLog(RuleLanguageVisitor.class);
	
	private String scriptName = "";
	private ASTMyScript.SCRIPTTYPE scriptType;
	private Map<String, Set<IRule>> elementMappingRules = new LinkedHashMap<String, Set<IRule>>();
	private Map<String, IRule> variableMappingRules = new LinkedHashMap<String, IRule>();
	private Map<String, IRule> templateMappingRules = new LinkedHashMap<String, IRule>();
	private List<String> importedScriptList = new LinkedList<String>();
	private List<FunctionCall> functionCallList = new LinkedList<FunctionCall>();
	private Map<String, String> namespaceDeclMap = new HashMap<String, String>();
	private List<Map<String, String>> preprocessingMap = new LinkedList<Map<String,String>>();
	
	
	/**
	 * @return the name of the rule script
	 */
	public String getScriptName(){
		return this.scriptName;
	}
	
	/**
	 * @return the type of the script
	 */
	public SCRIPTTYPE getScriptType(){
		return this.scriptType;
	}
	
	/**
	 * @return the mapping of all rules
	 */
	public Map<String, Set<IRule>> getElementMappingRules(){
		return this.elementMappingRules;
	}
	
	public Map<String, IRule> getVariableMappingRules(){
		return this.variableMappingRules;
	}
	
	public Map<String, IRule> getTemplateMappingRules(){
		return this.templateMappingRules;
	}
	
	/**
	 * @return the list of function calls - this is a subset of the rule mapping
	 */
	public List<FunctionCall> getFunctionCalls(){
		return this.functionCallList;
	}
	
	/**
	 * @return the list of scripts that are declared as import
	 */
	public List<String> getImportedScripts(){
		return this.importedScriptList;
	}
	
	/**
	 * @return the map of name-space declarations made in the script
	 */
	public Map<String, String> getNamespaceDeclarations(){
		return this.namespaceDeclMap;
	}
	
	/**
	 * @return the map of preprocessings (functions, parameters)
	 */
	public List<Map<String, String>> getPreprocessings(){
		return this.preprocessingMap;
	}
	
	public Object visit(ASTMyAssign node, Object data) {
		String attrValue = "";
		String fieldExprValue = "";
		Rules r = node.getRule();
		
		if (node.isFieldExpression()){
			// todo e.g. convert field-expression into a xpath-expression
			fieldExprValue = node.getFieldExpression();
			if (fieldExprValue.startsWith("xpath:")){
				fieldExprValue = Converter.getXpathFromXpathExpr(fieldExprValue);				
				r.setXpath(fieldExprValue);
			}else if (fieldExprValue.startsWith("$") && !fieldExprValue.startsWith("$job.")){
				// variable
				log.debug("ruleLangVisitor: assign variable:" + fieldExprValue);
				r.setAssignmentVariable(fieldExprValue);
			}
		}else if (node.isAttribute()){
			attrValue = node.getValue();
		}else {
			// shouldn't happen
			attrValue = "value not defined: " + node.getValue();
		}
		r.setConstant(attrValue);
		if (r.getUniqueName().trim().length() > 0){
			addRule(r, r.getUniqueName());			
		}
		return null;
	}
	
	@Override
	public Object visit(ASTMyCopy node, Object data) {
		Rules r = node.getRule();
		r.setTemplateMatch(node.getTemplateMatchName());
		r.getProperties().setProperty("applyTemplateSelectExpression", node.getApplyTemplateSelectExpression());
		r.getProperties().setProperty("copySelectExpression", node.getCopySelectExpression());
		addRule(r, r.getUniqueName());
		return null;
	}

	@Override
	public Object visit(ASTMySet aNode, Object aData) {
		log.debug("called method: RuleLanguageVisitor.visit(ASTMySet)");
		// check if the outputfield is the same in rules of this production
		Rules myRule = aNode.getRule();

		if (aNode.isValueExpression()){
			// todo e.g. convert field-expression into a xpath-expression
			String exprValue = aNode.getValueExpression();
			if (exprValue.startsWith("xpath:")){
				exprValue = Converter.getXpathFromXpathExpr(exprValue);				
				myRule.setXpath(exprValue);
			}else if (exprValue.startsWith("$") && !exprValue.startsWith("$job.")){
				// variable
				log.debug("ruleLangVisitor: assign variable:" + exprValue);
				myRule.setAssignmentVariable(exprValue);
			}
		}

		List<Rules> rules = aNode.getRules();
		RulesSet set = new RulesSet();
		//set.setPrimaryRule(rules.get(0));
		myRule.setRulesSet(set);
		log.debug("rulelangvisitor rule name: " + myRule.getUniqueName() + " , hasSet : " + myRule.hasSet());
		set.getPendingRules().addAll(rules);
		addRule(myRule, myRule.getUniqueName());
		return null;
	}

	@Override
	public Object visit(ASTMyEmpty node, Object data) {
		Rules r = node.getRule();
		r.setEmpty(node.isEmpty());
		addRule(r, r.getUniqueName());
		return null;
	}


	/**
	 * vist a production rule that is defined as an operation or external function call 
	 * @see eu.dnetlib.data.collective.transformation.rulelanguage.visitor.AbstractVisitor#visit(eu.dnetlib.data.collective.transformation.rulelanguage.parser.ASTMyOp, java.lang.Object)
	 */
	public Object visit(ASTMyOp node, Object data) {
		Rules r = node.getRule();
		r.setFunctionCall(node.createFunctionCall(r.isStatic()));
		functionCallList.add(r.getFunctionCall());
		log.debug("fc name: " + r.getFunctionCall().getExternalFunctionName());
		for(int i =0; i < node.jjtGetNumChildren(); i++) {
            ASTMyAttribute sn = (ASTMyAttribute)node.jjtGetChild(i);
            Argument arg = null;
            if (sn.getAttributeValue() != null){
            	arg = new Argument(Type.VALUE, sn.getAttributeValue());
            }else if (sn.getAttributeInputField() != null){
            	if (sn.getAttributeInputField().startsWith("xpath:")){
                	arg = new Argument(Type.INPUTFIELD, Converter.getXpathFromXpathExpr(sn.getAttributeInputField()));            		
            	}else if (sn.getAttributeInputField().startsWith("$job.")){
            		// job constant
                	arg = new Argument(Type.JOBCONST, sn.getAttributeInputField());
            	}else{
            		// variable
            		arg = new Argument(Type.VAR, sn.getAttributeInputField());
            	}
            }else{
            	throw new IllegalStateException("Argument with neither value nor inputfield is not allowed.");
            }
            log.debug("argument: " + arg.getArgument());
            r.getFunctionCall().addArgument(arg);
		}
		if (r.getFunctionCall().getParameters() != null){
			Set<String> keys = r.getFunctionCall().getParameters().keySet();
			for (String key: keys){
				log.debug("key: " + key + " , value: " + r.getFunctionCall().getParameters().get(key));
			}			
		}
		log.debug("add rule with declaration: " + r.getRuleDeclaration());
		addRule(r, r.getUniqueName());			

		return null;
	}

	@Override
	public Object visit(ASTMyImport node, Object data) {
		importedScriptList.add(node.getScriptName());
		return null;
	}

	@Override
	public Object visit(ASTMyNs node, Object data) {
		namespaceDeclMap.put(node.getNsPrefix(), node.getNsUri());
		return null;
	}

	@Override
	public Object visit(ASTMyScript node, Object data) {
		this.scriptName = node.getScript();
		this.scriptType = node.getScriptType();
		return null;
	}

	@Override
	public Object visit(ASTMyCondition node, Object data) {
		Condition condition = new Condition();
		if (node.getApplyExpression().length() > 0){
			String applyExpr = Converter.getXpathFromXpathExpr(node.getApplyExpression());
			condition.setApplyExpression(applyExpr);
		}
		
		String conditionalExpr = Converter.getXpathFromXpathExpr(node.getConditionalExpression());
		condition.setConditionExpression(conditionalExpr);
		condition.setPrimaryRule(node.getPrimaryRule());
		node.getPrimaryRule().setCondition(condition);
		condition.setSecondaryRule(node.getSecondaryRule());
		node.getSecondaryRule().setCondition(condition);
		return null;
	}
	
	@Override
	public Object visit(ASTMyPreprocess node, Object data) {
		Map<String, String> functionMap = new HashMap<String, String>();
		functionMap.put(node.getFunctionName(), node.getParameter());
		preprocessingMap.add(functionMap);
		return null;
	}	

	


	private void addRule(IRule rule, String key){
		log.debug("add rule with key: " + key);
		Set<IRule> ruleSet = null;
		if (rule.definesVariable()){
			variableMappingRules.put(key, rule);
		}else if (rule.definesTemplate()){
			templateMappingRules.put(key, rule);
		}else{
			if (elementMappingRules.containsKey(key)){
				ruleSet = elementMappingRules.get(key);
			}else{
				ruleSet = new LinkedHashSet<IRule>();
				elementMappingRules.put(key, ruleSet);
			}			
			ruleSet.add(rule);		
		}
	}

	@Override
	public Object visit(ASTMySkip node, Object data) {
		Rules r = node.getRule();
		r.setSkip(true);
		addRule(r, r.getUniqueName()); // ??? actually no targetField defined
		return null;
	}


}
