package ca.ucalgary.seahawk.services;

import org.biomoby.client.util.Base64Coder;
import org.biomoby.shared.*;
import org.biomoby.shared.data.*;
import org.biomoby.shared.parser.MobyTags;

import org.w3c.dom.Document;

import javax.xml.xpath.*;
import javax.xml.parsers.*;
import javax.xml.namespace.NamespaceContext;

import java.util.*;
import java.util.regex.*;

/**
 * Class used by MobyClient to build the MOBY rule database from a configuration file.
 */

public class MobyComplexBuilder{
    public static final String ANON_ARTICLE = "_seahawk_data";
    public static final String INHERITED_MEMBERS_SENTINEL = "_seahawk_member_inheritance";
    public static final String PRIMITIVE_VALUE_SENTINEL = "__prim_field";
    private static XPathFactory xPathFactory;
    private static DocumentBuilder docBuilder;

    private Map<String,String> namespaceRules; // ns, template
    private Map<String,String[]> memberRules;  // member name, [template, whitespace rule, encoding rule]
    private String articleNameRule;
    private MobyDataType mobyDataType;
    private MobyNamespace[] mobyNS;
    private MobyRelationship[] children;
    private MobyClient client;
    private int context = 0;
    private String srcURN = null;

    static{
	//PG temporarily use xalan while Google App Engine has error
        //xPathFactory = XPathFactory.newInstance();
	xPathFactory = new org.apache.xpath.jaxp.XPathFactoryImpl();

	DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
	dbf.setNamespaceAware(true);
	try{
	    docBuilder = dbf.newDocumentBuilder();
	} catch(Exception e){
	    e.printStackTrace();
	}
    }

    public MobyComplexBuilder(String dataType, Map<String,String[]> members, Map<String,String> nameSpaces, MobyClient cl) throws Exception{
	this(dataType, members, nameSpaces, cl, ANON_ARTICLE);
    }

    public MobyComplexBuilder(String dataType, Map<String,String[]> members, Map<String,String> nameSpaces, MobyClient cl, String articleName) throws Exception{
	mobyDataType = MobyDataType.getDataType(dataType, cl.getRegistry());
	// Not an existing Ontology data type
	if(mobyDataType == null){
	    mobyDataType = new MobyDataType(dataType);
	}

	articleNameRule = articleName;
	memberRules = members;
	client = cl;

	if(mobyDataType != null && mobyDataType.getName() != null && 
	   !mobyDataType.getName().equals(MobyTags.MOBYOBJECT)){
	    children = mobyDataType.getAllChildren();
	   
	    // Make sure all fields are defined
	    String[] ruleNames = (String[]) memberRules.keySet().toArray(new String[memberRules.size()]);
	    for(int i = 0; children != null && i < children.length; i++){
		String childName = children[i].getName();
		if(!memberRules.containsKey(childName)){
		    // See if there's an inherited member
		    if(!memberRules.containsKey(INHERITED_MEMBERS_SENTINEL)){
			throw new Exception("Ruleset for object " + mobyDataType.getName() +
					    " does not include a rule for required member '" +
					    childName+"'");
		    }
		    String inheritedRuleName = memberRules.get(INHERITED_MEMBERS_SENTINEL)[0];
		    MobyComplexBuilder inheritedBuilder = client.getBuilder(inheritedRuleName);
		    if(inheritedBuilder.getDataType().getChild(childName) == null){
			throw new Exception("Ruleset for object " + mobyDataType.getName() +
					    " does not include a rule for required member '" +
					    childName+"', nor does the inherited rule '"+
					    inheritedRuleName+"'");
		    }
		    
		}
		for(int j = 0; j < ruleNames.length; j++){
		    if(ruleNames[j] == childName){
			ruleNames[j] = null;  //denote we've processed it
			break;
		    }
		}
	    }

	    if(children != null && memberRules.size() != children.length){
		System.err.println("Ignoring member rules for object type " + 
				   mobyDataType.getName() + " (not in the MOBY ontology):");
		for(int i = 0; i < ruleNames.length; i++){
		    if(ruleNames[i] != null && !ruleNames[i].equals(INHERITED_MEMBERS_SENTINEL)){
			System.err.println("Extra member: " + ruleNames[i]);
		    }
		}
	    }

	    // Check that any casting done on members of the object is kosher
	    // (do this now because we now have the data type of the parent object for sure)
	    for(Map.Entry<String,String[]> member: members.entrySet()){
		if(member.getKey().equals(INHERITED_MEMBERS_SENTINEL)){
		    continue;
		}
		String memberDataTypeSetting = member.getValue()[1];
		if(memberDataTypeSetting != null && memberDataTypeSetting.length() > 0){
		    MobyRelationship child = mobyDataType.getChild(member.getKey());
		    String defaultDataTypeName = child.getDataTypeName();
		    MobyDataType castType = MobyDataType.getDataType(memberDataTypeSetting, 
								     client.getRegistry());
		    MobyDataType origType = MobyDataType.getDataType(defaultDataTypeName, 
								     client.getRegistry());
		    if(castType == null){
			System.err.println("Could not find the data type "+memberDataTypeSetting+
					   " in the ontology, ignoring the data type cast"+
					   " that says it should override " + defaultDataTypeName);
			member.getValue()[1] = null;
		    }
		    else if(origType == null){
			System.err.println("Could not find the data type "+defaultDataTypeName+
					   " in the ontology, ignoring the data type cast" + 
					   " that overrides it to data type (" + memberDataTypeSetting +
					   "), can't tell if the cast is legal or not)");
			member.getValue()[1] = null;
		    }
		    else if(!castType.inheritsFrom(origType)){
			System.err.println("The data type "+memberDataTypeSetting+
					   " does not inherit from the member's type in the ontology (" +
					   defaultDataTypeName+"), ignoring the data type cast");
			member.getValue()[1] = null;
		    }
		    else{
			// It's safe it we got this far, leave it as-is
		    }
		}
	    }
	}

	namespaceRules = nameSpaces;
	if(namespaceRules != null && namespaceRules.size() > 0){
	    mobyNS = new MobyNamespace[namespaceRules.size()];
	    Iterator iter = namespaceRules.keySet().iterator();

	    for(int i = 0; iter.hasNext(); i++){
		String ns = (String) iter.next();

		// Give the namespace a meaningful description from the MOBY registry
		mobyNS[i] = MobyNamespace.getNamespace(ns, client.getRegistry());
		if(mobyNS[i] == null){
		    System.err.println("Namespace '"+ns+"' is not registered in MOBY Central");
		    // Not a registered namespace in MOBY Central's ontology
		    mobyNS[i] = new MobyNamespace(ns);
		}
	    }
	} // if namespace rules
    }

    public MobyDataObject apply(Matcher matcher) throws MobyException{
	return apply(matcher, null);
    }

    public MobyDataObject apply(Matcher matcher, byte[] data) throws MobyException{
	MobyDataObject mobyObj = null;
	
	if(mobyDataType.getName().equals(MobyTags.MOBYOBJECT)){
	    mobyObj = new MobyDataObject("", client.getRegistry());
	}
	else if(mobyDataType.inheritsFrom(MobyDataType.getDataType(MobyDataBytes.BASE64_DATATYPE, client.getRegistry()))){
	    mobyObj = new MobyDataBytes("", (String) null);
	    mobyObj.setDataType(mobyDataType);
	}
	// else ifs for other primitives in the future maybe?
	else{
	    mobyObj = new MobyDataComposite(mobyDataType);
	}

	// Set article name, if available
	if(articleNameRule != null && articleNameRule.length() > 0 && 
	   !articleNameRule.equals(ANON_ARTICLE)){
	    mobyObj.setName(evaluateRule(matcher, 
					 articleNameRule, 
					 MobyClient.WHITESPACE_ATTR_STRIP_FLANKING_VAL,
					 null, 
					 data));
	}

	// Set namespaces if available
	if(namespaceRules != null && namespaceRules.size() > 0){
	    boolean haveID = false;
	    for(int i = 0; mobyNS != null && i < mobyNS.length; i++){	    
		String resultSpec = namespaceRules.get(mobyNS[i].getName());
		String result = evaluateRule(matcher, 
					     resultSpec, 
					     MobyClient.WHITESPACE_ATTR_STRIP_FLANKING_VAL, 
					     null, 
					     data);
		// A non-blank value was the result of the evaluation, or it's the only namespace we have
		if(mobyNS.length == 1 || result.length() > 0){
		    if(!haveID){
			// A namespace that has a value
			mobyObj.setPrimaryNamespace(mobyNS[i]);
			// Note that if there is a conflict in IDs, the first one evaluated will be returned...
			mobyObj.setId(result);
			haveID = true;
		    }
		    else{
			// ... the subsequent ones are saved as cross-references
			mobyObj.addCrossReference(new MobyDataObject(mobyNS[i].getName(), result, client.getRegistry()));
		    }
		}
	    }
	}

	// Done?
	if(!(mobyObj instanceof MobyDataComposite) || children == null){
	    return mobyObj;
	}
	
	// There's another rule to populate members from
	if(memberRules.containsKey(INHERITED_MEMBERS_SENTINEL)){
	    // String[2] = {ruleName, ruleValue}
	    String[] ruleSpec = memberRules.get(INHERITED_MEMBERS_SENTINEL);
	    MobyComplexBuilder inheritedBuilder = client.getBuilder(ruleSpec[0]);
	    String result = evaluateRule(matcher, ruleSpec[1], null, null, data);
	    Matcher submatcher = client.getPattern(ruleSpec[0]).matcher(result);
	    if(!submatcher.find()){
		throw new MobyException("Pattern of inherited rule '"+ruleSpec[0]+
					"' does not match data given: " + result);
	    }
	    MobyDataObject inheritedResult = inheritedBuilder.apply(submatcher, result.getBytes());
	    
	    // Now copy all the fields from the inheritedResults to our object
	    if(inheritedResult instanceof MobyDataComposite){
		((MobyDataComposite) mobyObj).putAll((MobyDataComposite) inheritedResult);
	    }
	}
	
	// Otherwise set members if available and required
	for(int i = 0; i < children.length; i++){
	    String memberName = children[i].getName();
	    String[] resultSpec = memberRules.get(memberName);
	    if(resultSpec == null){
		// Must have been populated by the inherited rule?
		if(!((MobyDataComposite) mobyObj).containsKey(memberName)){
		    throw new MobyException("Member " +memberName+" does not have a rule, nor" +
					    " was not populated by any inherited rule");
		}
		continue;
	    }

	    String dataTypeName = children[i].getDataTypeName();
	    // First: does the rule cast the object to some subtype?
	    if(resultSpec[1] != null && resultSpec[1].length() > 0){			
		// Safe to cast if we got this far (e.g. put String where an Object is specified)
		dataTypeName = resultSpec[1]; 
	    }
	    
	    // Is the field a list rather than a single value?  If so,
	    // we will need to reinterpret the member value specification
	    // as many times as the regex capture group matched...
	    String[] results = new String[1];
	    if(children[i].getRelationshipType() == Central.iHAS){
		// We need to reconstruct the capture group so that we
		// can save each value, not just its last one.
		try{
		    // results may be more than one element in this case
		    results = evaluateIterativeRule(matcher, resultSpec[0], resultSpec[2], resultSpec[3], data);
		} catch(Exception e){
		    e.printStackTrace();
		    throw new MobyException("Error while evaluating HAS (iterative evaluation) rule: " + e);
		}
	    }
	    // else is iHASA, scalar context
	    else{
		// only one result to process and add the to object
		results[0] = evaluateRule(matcher, resultSpec[0], resultSpec[2], resultSpec[3], data);
	    }
	    
	    for(String result: results){
		
		if(PrimitiveTypes.isPrimitive(dataTypeName) ||
		   MobyDataType.getDataType(dataTypeName, client.getRegistry()).inheritsFrom(MobyDataType.getDataType(MobyDataBytes.BASE64_DATATYPE, client.getRegistry()))){
		    ((MobyDataComposite) mobyObj).put(memberName, 
						      MobyDataObject.createInstanceFromString(dataTypeName, result, client.getRegistry()));
		}
		else{
		    // Recursively call the object creator for complex members
		    
		    // Fetch the member's MobyComplexBuilder by name from the rule spec
		    String memberRuleName = resultSpec[4];
		    if(memberRuleName == null || memberRuleName.length() == 0){
			throw new MobyException("Composite member "+memberName+
						" does not have an associated rule to produce it");
		    }
		    MobyComplexBuilder memberBuilder = client.getBuilder(memberRuleName);
		    if(memberBuilder == null){
			throw new MobyException("The object builder for the rule " + memberRuleName +
						" could not be found");
		    }
		    Pattern memberPattern = client.getPattern(memberRuleName);
		    
		    // Now apply the regex for the capture group, so we can use
		    // the results in a recursive call to this method (stop condition
		    // is when all members are primitives).
		    Matcher memberMatcher = memberPattern.matcher(result);
		    
		    if(memberMatcher.find()){
			((MobyDataComposite) mobyObj).put(memberName,
							  memberBuilder.apply(memberMatcher));
		    }
		    else{
			System.err.println("Could not match pattern \"" + memberPattern.pattern() +
					   "\" to string \"" + result + "\", abandoning " +
					   dataTypeName + " object creation");
		    }
		}
	    } //end for results
	}  //end for children

	return mobyObj;
    }

    /**
     * @return a list of rule values to interpret
     */
    public String[] evaluateIterativeRule(Matcher matcher, String result, String whitespaceMode, String encodingMode, byte[] data)
	throws MobyException, Exception{
	Vector<String> capturedValues = new Vector<String>();

	// Figure out which capture group needs to be evaluated iteratively
	Vector<Integer> captureGroups = new Vector<Integer>();
	// This only really works with 10 capture groups or less, otherwise $1 and $10 can get captured, etc.
	for(int captureGroup = 1; captureGroup <= matcher.groupCount(); captureGroup++){
	    if(result.indexOf("$"+captureGroup) != -1){
		//System.err.println("Adding capture group " + captureGroup + 
		//		   " to evaluation list due to its presence in rule " + result);
		captureGroups.add(captureGroup);
	    }
	}
	// No capture group found, so just return single evaluation on whole expression (may use $0)
	if(captureGroups.isEmpty()){
	    return new String[]{evaluateRule(matcher, result, whitespaceMode, encodingMode, data)};
	}
	
	// Pull out the capture group's pattern, by counting parentheses
	Map<Integer,String> captureGroupRegexes = new HashMap<Integer,String>();
	for(Integer captureGroup: captureGroups){
	    captureGroupRegexes.put(captureGroup.intValue(),
				    RegexParser.getCaptureGroupRegex(matcher.pattern(), 
								     captureGroup.intValue()));
	}

	// Find all instances of the capture group, save their value
	Map<Integer,String[]> captureGroupSubvalues = new HashMap<Integer,String[]>();
	for(Map.Entry<Integer,String> entry: captureGroupRegexes.entrySet()){
	    // Why fetch cap group # -1?  Because we auto-encapsulated the cap groups the HAS member
	    // refers to, specifically so we get the whole match for processing, rather than just
	    // the last one, i.e. (\d)+ in a regex rule became ((\d)+) in MobyClient so we can 
	    // find each \d in the whole match and add them indidivually as HAS (0 or more) members
	    // in the object instance.
	    String wholeMatch = matcher.group(entry.getKey().intValue()-1);
	    String regex = entry.getValue();
	    if(regex.length() == 0){
		throw new Exception("Encountered empty regex in capture group " + entry.getKey());
	    }
	    //System.err.println("Applying regex "+regex+" to "+wholeMatch+", whole pattern was "+matcher.pattern().pattern());
	    // If the pattern matches more than once, the matches must be contiguous, hence the \G
	    Pattern captureGroupPattern = Pattern.compile("\\G"+regex, 
							  Pattern.DOTALL | Pattern.COMMENTS);
	    Vector<String> subValues = new Vector<String>();
	    Matcher subMatcher = captureGroupPattern.matcher(wholeMatch);
	    int lastSubMatcherEnd = -1;
	    while(subMatcher.find()){
		subValues.add(subMatcher.group());
		lastSubMatcherEnd = subMatcher.end();
	    }

	    // For safety, we should do a sanity check that there's 
	    // is no unmatched input left over, as we'd expect
	    if(lastSubMatcherEnd != -1 && lastSubMatcherEnd != wholeMatch.length()){
		throw new Exception("The submatcher for capture group "+entry.getKey()+ 
				    " did not match to the last char of \"" + wholeMatch + 
				    "\", should have ended match at index " + (wholeMatch.length()-1) +
				    ", but instead matched until " + (lastSubMatcherEnd-1));
	    }
	    captureGroupSubvalues.put(entry.getKey().intValue(), 
				      (String[]) subValues.toArray(new String[subValues.size()]));
	}

	// Now, iteratively substitute each subvalue from each capture group into the rules 
	IterativeMatchResult iterMatch = new IterativeMatchResult(matcher, captureGroupSubvalues);
	for(int i = 0; i < iterMatch.getNumIterations(); i++){
	    iterMatch.setIteration(i);
	    capturedValues.add(evaluateRule(iterMatch, result, whitespaceMode, encodingMode, data));
	}

	// Return all the values we found
	return (String[]) capturedValues.toArray(new String[capturedValues.size()]);
    }

    // takes a rule and evaluates $# variables and XPath expressions
    private String evaluateRule(MatchResult match, String result, String whitespaceMode, String encodingMode, byte[] data)
	throws MobyException{
	byte[] resultBytes = result.getBytes();
	boolean nonBasic = result.length() != 2 || result.indexOf("$") != 0;
	boolean isBinary = encodingMode != null && encodingMode.equals(MobyClient.ENCODING_ATTR_BASE64_VAL);

	MobyPrefixResolver.MobyNodeList nodeList = new MobyPrefixResolver.MobyNodeList();

	Document doc = null;
	// Only get a DOM instance (for text node creation) if we're going to evaluate an XPath in the end
	if(nonBasic){
	    doc = docBuilder.newDocument();
	}

	// Replace any $0, $1, etc. in the replacement string with the values found in the match
	// Note that this is not perfect: if you had "$1 $2", and $1 had value "$250", you'd get $250$2,
	// then you'd substitute $2's value of "per metre", you'd get "per metre50 per metre" instead of
	// "$250 per metre".  Not sure of a good way around this yet (i.e. when varValue had $k in it where k > j)...
	for(int j = 0; j <= match.groupCount(); j++){
	    // A replaceAll() for binary data
	    if(data != null && isBinary){
		int srcPos = 0;
		for(int varIndex = result.indexOf("$"+j, srcPos); 
		    varIndex != -1; 
		    varIndex = result.indexOf("$"+j, srcPos)){
		    int varValueLength = match.end(j)-match.start(j);
		    int varLen = ("$"+j).getBytes().length;
		    byte[] newResultBytes = new byte[resultBytes.length+varValueLength-varLen];
		    if(varIndex > 0){			
			System.arraycopy(resultBytes, 0, newResultBytes, 0, varIndex);
		    }
		    System.arraycopy(data, match.start(j), newResultBytes, varIndex, varValueLength);
		    int remaining = result.length()-varIndex-varLen;
		    if(remaining > 0){
			System.arraycopy(resultBytes, varIndex+varLen, newResultBytes, varIndex+varValueLength, 
					 result.length()-varIndex-varLen);
		    }
		    srcPos += varIndex + varValueLength + 1;
		    resultBytes = newResultBytes;
		}
	    }
	    // $# substitution in a string, considerably simpler!
	    else if(result.indexOf("$"+j) != -1){
		String varValue = match.group(j);
		if(whitespaceMode == null){
		    // do nothing
		}
		else if(whitespaceMode.equals(MobyClient.WHITESPACE_ATTR_NORMALIZE_VAL)){
		    varValue = varValue.replaceAll("\\s+"," ");
		}
		else if(whitespaceMode.equals(MobyClient.WHITESPACE_ATTR_STRIP_VAL)){
		    varValue = varValue.replaceAll("\\s+","");
		}
		else if(whitespaceMode.equals(MobyClient.WHITESPACE_ATTR_STRIP_FLANKING_VAL)){
		    varValue = varValue.trim(); // removes leading and trailing whitespace
		}
		result = result.replaceAll("\\$"+j+"(?=\\D|\\z)", varValue);
		// Binary data and XPath are incompatible, since many bytes are not allowed in XML
		// so only create the node list if not Base64 encoding
		if(nonBasic){
		    nodeList.add(doc.createTextNode(result));
		}
	    }
	}
	
	if(isBinary){
	    //(new Exception("Base 64 encoding")).printStackTrace();
	    result = new String(Base64Coder.encode(data == null ? result.getBytes() : resultBytes));
	}
	else{
	    //System.err.println("NO BASE64 (" + MobyClient.ENCODING_ATTR_BASE64_VAL + 
	    //		   ") ENCODING for " +memberName+": " + encodingMode);
	}
	
	// If the value contains anything other than $#, treat it as a XPath expression 
	// to be evaluated on a text node containing the result value 
	if(nonBasic && data == null){
	    XPath xpath = xPathFactory.newXPath();
	    try{
		result = xpath.evaluate(result, nodeList);
	    }catch(Exception e){
		e.printStackTrace();
		throw new MobyException("Could not evaluate result of XPath expression " + 
					"('" + result + "'): " + e);
	    }
	}
	return result;
    }

    public MobyDataObject applyXPath(Object result, NamespaceContext nsContext) throws MobyException, XPathExpressionException{
	XPath xpath = xPathFactory.newXPath();
	xpath.setNamespaceContext(nsContext);
	MobyDataObject mobyObj = null;
	
	if(mobyDataType.getName().equals(MobyTags.MOBYOBJECT)){
	    mobyObj = new MobyDataObject("", client.getRegistry());
	}
	else if(mobyDataType.getName().equals(MobyTags.MOBYSTRING)){
	    mobyObj = new MobyDataString("", client.getRegistry());
	}
	// else ifs for other primitives in the future maybe?
	else{
	    mobyObj = new MobyDataComposite(mobyDataType);
	}

	// Set namespaces if available
	if(namespaceRules != null && namespaceRules.size() > 0){
	    boolean haveID = false;
	    for(int i = 0; mobyNS != null && i < mobyNS.length; i++){	    
		String expr = namespaceRules.get(mobyNS[i].getName());
		String value = xpath.evaluate(expr, result);

		// A non-blank value was the result of the evaluation
		if(value != null && value.length() > 0){
		    if(!haveID){
			// A namespace that has a value
			mobyObj.setPrimaryNamespace(mobyNS[i]);
			// Note that if there is a conflict in IDs, the first one evaluated will be returned...
			mobyObj.setId(value);
			haveID = true;
		    }
		    else{
			// ... the subsequent ones are saved as cross-references
			mobyObj.addCrossReference(new MobyDataObject(mobyNS[i].getName(), value, client.getRegistry()));
		    }
		}
	    }
	}

	// Set members if available and required
	if(mobyObj instanceof MobyDataComposite){
	    if(children != null){
		for(int i = 0; i < children.length; i++){
		    String memberName = children[i].getName();
		    String expr = memberRules.get(memberName)[0];
		    String value = xpath.evaluate(expr, result);

		    ((MobyDataComposite) mobyObj).put(memberName, 
						      MobyDataObject.createInstanceFromString(children[i].getDataTypeName(), value, client.getRegistry()));
		}
	    }
	}
	else if(mobyObj instanceof MobyDataString){
	    String expr = memberRules.get(PRIMITIVE_VALUE_SENTINEL)[0];
	    ((MobyDataString) mobyObj).setValue(xpath.evaluate(expr, result));
	}
	// TODO handle integers and other primitives

	return mobyObj;
    }

    /**
     * Reports the data type of the objects returned by matched rules.
     */
    public MobyDataType getDataType(){
	return mobyDataType;
    }

    /**
     * Reports the possible namespaces of the objects returned by matched rules.
     */
    public MobyNamespace[] getNamespaces(){
	return mobyNS;
    }

    /**
     * Reports the URN identifying the rule, or null if it doesn't exist.  Useful for rule-sharing purposes.
     */
    public String getURN(){
	return srcURN;
    }

    /**
     * Call in order to provide a unique ID for the rule, for reuse purposes.
     */
    public void setURN(String urn){
	srcURN = urn;
    }

    /**
     * Call to find out if the result will be a String or NodeList, etc if there's an XPath.
     *
     * @return one of the constants (NODESET, STRING) from javax.xml.xpath.XPathConstants, or 0 if not an XPath rule
     */
    public int requiredXPathContext(){
	return context;
    }
}
