package ca.ucalgary.services.util;

import org.biomoby.shared.LSIDResolver;
import org.biomoby.shared.parser.MobyTags;

import org.apache.commons.httpclient.*;
import org.apache.commons.httpclient.methods.*;

import org.w3c.dom.*;

import javax.xml.xpath.*;

import java.io.*;
import java.net.URL;
import java.util.*;
import java.util.regex.*;

/**
 * Contains utility functions to create and store MOB and DEM rules (for Sehawk's MobyClient and TextClient)
 */
public class RuleCreator{

    private static Pattern attrValSelectionXPathPattern;
    private static Pattern attrNameSelectionXPathPattern;
    private static Pattern elNameSelectionXPathPattern;
    public final static String RULESTORE_URL = "http://bioxml.info/authority/store.cgi/";
    public final static String LIFTING_SCHEMA_PATH = "mobyLiftingSchemaMapping";
    public final static String LOWERING_SCHEMA_PATH = "mobyLoweringSchemaMapping";
    public final static String RULE_CONTENTS_PARAM = "contents";
    public final static String DATA_CONTENTS_PARAM = "data";
    public final static String METADATA_CONTENTS_PARAM = "metadata";

    static{
	attrValSelectionXPathPattern = Pattern.compile("([^/]+)\\[@(.+)=\"(.*)\"\\]");
	attrNameSelectionXPathPattern = Pattern.compile("([^/]+)\\[@(.+)\\]");
	elNameSelectionXPathPattern = Pattern.compile("([^/]+)$");
    }

    /**
     * Creates a new MOB rule based on the input xpaths, then uploads that rule to an LSID server.
     *
     * @return uri of the registered rule
     */
    public static String createLiftingRuleAndMakeAvailable(String discoveryXPath,
							   String cleanXPath, // with no prefixes
							   String dataType,
							   Map<String,String> nsXPaths, 
							   Map<String,String> memberXPaths) throws Exception{

	StringBuilder rule = new StringBuilder();
	rule.append("<?xml version=\"1.0\"?>\n\n" +
		    "<object xmlns=\"http://bioxml.info/MOB\"\n" +
		    "        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
		    "        xsi:schemaLocation=\"http://bioxml.info/MOB http://bioxml.info/xsd/MOB.xsd\"\n" +
		    "        xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n");
	rule.append(" <xpath>"+discoveryXPath+"</xpath>\n");
	if(dataType != null && !dataType.equals(MobyTags.MOBYOBJECT)){
	    rule.append(" <datatype value=\""+dataType+"\"/>");
	}
	if(!nsXPaths.isEmpty()){
	    rule.append(" <namespace>\n");
	    for(Map.Entry<String,String> ns: nsXPaths.entrySet()){
		rule.append("  <ns value=\""+ns.getKey()+"\">"+ns.getValue()+"</ns>\n");
	    }
	    rule.append(" </namespace>\n");
	}
	for(Map.Entry<String,String> member: memberXPaths.entrySet()){
	    rule.append(" <member value=\""+member.getKey()+"\">"+member.getValue()+"</member>\n"); 
	}

	// Create a reasonable identifier by doing a GET on the datastore
	URL u = new URL(RULESTORE_URL+LIFTING_SCHEMA_PATH+cleanXPath);
	LineNumberReader reader = new LineNumberReader(new InputStreamReader(u.openStream()));
	String lsid = reader.readLine();
	if(!LSIDResolver.isLSID(lsid)){
	    throw new Exception("Could not get an LSID assignment from the rulestore ("+
				RULESTORE_URL+") got '"+ lsid + "'");
	}
	rule.append(" <dc:identifier>" + lsid + "</dc:identifier>\n");
	rule.append("</object>");

	// Now store the data using a POST
	PostMethod registrationRequest = new PostMethod(RULESTORE_URL);
	registrationRequest.setParameter(RULE_CONTENTS_PARAM, rule.toString());
	registrationRequest.setParameter(DATA_CONTENTS_PARAM, "1"); // as opposed to saying we're posting metadata
	HttpClient client = new HttpClient();
	int returnCode = client.executeMethod(registrationRequest);
	    
	if(returnCode >= 300 && returnCode <= 400){
	    lsid = registrationRequest.getResponseBodyAsString();
	}
	else if(returnCode >= 400){
	    throw new Exception("Error registering new rule @ " + 
				registrationRequest.getURI() + " (return code is " + 
				returnCode + "): " + registrationRequest.getStatusLine() +
				"\n" + registrationRequest.getResponseBodyAsString());
	}

	return lsid.replaceAll("\n","");
    }

    // Based on the selection criteria, certain values are more likely to be the info to extract than others
    public static Vector<String> suggestValueXPathsFromSelectionXPath(String selectionXpath, Node selectionNode){
	Vector<String> suggestions = new Vector<String>();
	
	Matcher byAttrValMatcher = attrValSelectionXPathPattern.matcher(selectionXpath);
	Matcher byAttrNameMatcher = attrNameSelectionXPathPattern.matcher(selectionXpath);
	Matcher byElemNameMatcher = elNameSelectionXPathPattern.matcher(selectionXpath);
	// element selection was based on a given attribute having a specific value
	if(byAttrValMatcher.find()){
	    String elName = byAttrValMatcher.group(1);
	    String attrName = byAttrValMatcher.group(2);
	    String attrVal = byAttrValMatcher.group(3);

	    //text()
	    suggestions.add("text() -- Text between the "+elName+" open and close tags");

	    //@otherattr
	    NamedNodeMap attrs = selectionNode.getAttributes();
	    for(int i = 0; i < attrs.getLength(); i++){
		String otherAttrName = attrs.item(i).getLocalName();
		if(attrName.equals(otherAttrName)){
		    continue;  //will go last, as punishment
		}
		suggestions.add("@"+otherAttrName+" -- The value of attribute "+otherAttrName);
	    }
	    //attr
	    suggestions.add("{-- @"+attrName+" not available --} -- Click the "+
			    attrName+" link, not the "+attrVal+" link in results XML to use it");
	    // todo: place attr in selection last
	    // suggest sibling nodes too
	    NodeList siblings = selectionNode.getParentNode().getChildNodes();
	    for(int i = 0;  siblings.getLength() < 50 && i < siblings.getLength(); i++){
		Node sibling = siblings.item(i);
		if(sibling == selectionNode || !(sibling instanceof Element)){
		    continue;
		}
		if(((Element) sibling).getTextContent().length() > 0){
		    String suggestion = "../"+sibling.getNodeName()+"/text() -- The sibling node's text contents";
		    if(!suggestions.contains(suggestion)){
			suggestions.add(suggestion);
		    }		    
		}
		NamedNodeMap sibAttrs = ((Element) sibling).getAttributes();
		for(int j = 0; j < sibAttrs.getLength(); j++){
		    String otherAttrName = sibAttrs.item(i).getLocalName();
		    String suggestion = "../"+sibling.getNodeName()+"/@"+otherAttrName+
			" -- The value of sibling node's attribute "+otherAttrName;
		    if(!suggestions.contains(suggestion)){
			suggestions.add(suggestion);
		    }
		}
	    }
	}
	// element selection was based on a given attribute being present (excludes case above by logic, not regex)
	else if(byAttrNameMatcher.find()){
	    String elName = byAttrNameMatcher.group(1);
	    String attrName = byAttrNameMatcher.group(2);

	    //@attr
	    suggestions.add("@"+attrName+" -- The value of the selected attribute ("+attrName+")");
	    //text()
	    suggestions.add("text() -- Text between the "+elName+" open and close tags");
	    //@otherattr
	    NamedNodeMap attrs = selectionNode.getAttributes();
	    for(int i = 0; i < attrs.getLength(); i++){
		String otherAttrName = attrs.item(i).getLocalName();
		if(attrName.equals(otherAttrName)){
		    continue; //already suggested
		}
		suggestions.add("@"+otherAttrName+" -- The value of attribute "+otherAttrName);
	    }
	}
	else if(byElemNameMatcher.find()){  // custom or based on element name
	    String elName = byElemNameMatcher.group(1);
	    //text()
	    suggestions.add("text() -- Text between the " + elName +" open and close tags");
	    if(selectionNode != null){
		NamedNodeMap attrs = selectionNode.getAttributes();
		for(int i = 0; i < attrs.getLength(); i++){
		    String otherAttrName = attrs.item(i).getLocalName();
		    suggestions.add("@"+otherAttrName+" -- The value of attribute "+otherAttrName);
		}
		NodeList children = selectionNode.getChildNodes();
		Map<String,Boolean> alreadySuggested = new HashMap<String,Boolean>();
		for(int i = 0; i < children.getLength(); i++){
		    Node child = children.item(i);
		    if(child instanceof Element){
			if(!alreadySuggested.containsKey(child.getNodeName())){
			    suggestions.add(child.getNodeName()+"/text() -- The text inside child elements called "+
					    child.getNodeName());
			    alreadySuggested.put(child.getNodeName(), Boolean.TRUE);
			}
		    }
		}
	    }
	}
	suggestions.add("string() -- The XML string value of the selected node");

	return suggestions;
    }

    /**
     * Turns a complex xpath-based parameter name into something a moby user won't be afraid of
     * Since the XPath is passd between the PBE system and the Daggoo registration engine, they should both
     * cal this method to coordinate on Moby parameter names.
     */
    public static String simplifyParamName(String originalName, Map<String,String> namesInUse) throws Exception{

	// Figure out an appropriate name ourselves
	String newName = originalName;
	int slashPos = originalName.indexOf("/");
	if(slashPos == -1){
	   // if there's no XPath nesting, it's not bad...just use the tag name as is
	}
	// sub the single slash with a dash
	if(originalName.matches("[^/]+/[^/]+")){
	    newName = originalName.substring(0, slashPos)+"-"+originalName.substring(slashPos+1);
	}
	// sub all slashses with dashes if the whole thing it's very long
	else if(originalName.length() < 25){
	    newName = originalName.replaceAll("/", "-");
	}
	else{
	    // we need to shorten it: find a "natural" breakpoint in the last 25 letters
	    newName = originalName.substring(originalName.length()-25);
	    if(newName.indexOf("/") < 12){  //slash near the start, just remove prefix
		newName = newName.substring(newName.indexOf("/")+1).replaceAll("/","-");
	    }
	}
	newName = newName.replaceFirst("\\[(\\d+)\\]", "-part$1")
	    .replace("/text()", "")
	    .replaceAll("[^A-Za-z0-9\\-_]","_");
	if(namesInUse != null && namesInUse.containsKey(newName)){
	    throw new Exception("There are more than one field that maps to the " +
				"same Moby name ("+newName+") by default...can't continue.");
	}
	return newName;
    }
}
