package ca.ucalgary.seahawk.services;

import ca.ucalgary.seahawk.util.SeahawkOptions;

import org.biomoby.registry.meta.*;
import org.biomoby.shared.*;
import org.biomoby.shared.data.MobyDataInstance;

import org.w3c.dom.*;

import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.*;

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

/**
 * This class uses XSLT rules to transform MOBY XML data representation
 * (during service invocation) to plain text.  It can be thought of as the
 * opposite of the MobyClient, which generates MOBY objects from text using 
 * regular expressions.
 */
public class TextClient{

    public static final String DATA_MAPPING_XSLT_RESOURCE = "ca/ucalgary/seahawk/resources/mobyRules.xsl";
    public static final String RESOURCE_SYSTEM_PROPERTY = "seahawk.textrules";

    public final static String XSLT_NS = "http://www.w3.org/1999/XSL/Transform";
    public final static String XSLT_MODE_VAR = "selectedACDTypeTarget";

    private Vector<Transformer> moby2textConverters;  // XSLT engine
    private TransformerFactory transFactory;
    private DocumentBuilder docBuilder;
    private URL dataMappingXSLTURL;
    //private Map<String,Transformer> id; //todo: unique xslt doc id (e.g. LSID or URL) -> transformer

    private Map<String, Vector<String>> typeTemplates;  //mobytype -> Vector(xsltrule1, xsltrule2,...)
    private Map<String, Vector<MobyNamespace>> typeNsRestrictions;  //mobytype -> Vector(mobynamespace1, ...)
    private Map<String, String> templateMode;
    private Map<String,String> templateIds; // template name -> URN
    private Registry registry;

    private static Logger logger = Logger.getLogger(TextClient.class.getName());

    public TextClient() throws Exception{
	this(SeahawkOptions.getRegistry() == null ? 
	     RegistryCache.getDefaultRegistry() :
	     SeahawkOptions.getRegistry());
    }

    public TextClient(Registry reg) throws Exception{
	registry = reg;

        // Now setup the XSLT to transform MOBY XML into text for use in
	// non-XML aware applications.
        transFactory = TransformerFactory.newInstance();

	// For reading in the XSLT, we need data on template names, mode, etc.
	DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
        docFactory.setNamespaceAware(true);
	docBuilder = docFactory.newDocumentBuilder();

	moby2textConverters = new Vector<Transformer>();
	typeTemplates = new HashMap<String, Vector<String>>();
	typeNsRestrictions = new HashMap<String, Vector<MobyNamespace>>();
	templateMode = new HashMap<String, String>();
	templateIds = new HashMap<String, String>();

	ClassLoader cl = getClass().getClassLoader();
	if(cl == null){
	    cl = ClassLoader.getSystemClassLoader();
	}
        String rulesResource = System.getProperty(RESOURCE_SYSTEM_PROPERTY);
        if(rulesResource == null){
	  dataMappingXSLTURL = cl.getResource(DATA_MAPPING_XSLT_RESOURCE);
        }
	else if(rulesResource.length() != 0){
	    // See if it's a URL
	    try{
		dataMappingXSLTURL = new URL(rulesResource);
	    }
	    catch(Exception e){
		dataMappingXSLTURL = cl.getResource(rulesResource);
	    }
	}
	if(dataMappingXSLTURL == null){
	    if(rulesResource.length() != 0){ // if not left intentionally blank
		logger.log(Level.WARNING, "Could not find MOBY to text mapping resource '"+
				          rulesResource+"'");
	    }
	}
	else{
	    try{
		addMappingsFromURL(dataMappingXSLTURL);
	    }
	    catch(Exception e){
		logger.log(Level.SEVERE, "Error loading default data mapping rules ("+dataMappingXSLTURL+")", e);
                e.printStackTrace();
	    }
	}
    }

    public String[] getPossibleTextTypes(MobyPrimaryData dataTemplate, boolean mustHaveURN){
	Vector<String> possibleTypes = new Vector<String>();
	// Search for templates matching the given type or one of its parent types,
	// and see if they create the given text type (indicated by the template's mode attribute)
	for(MobyDataType type = dataTemplate.getDataType();
	    type != null;
	    type = type.getParent()){
	    if(typeTemplates.containsKey(type.getName())){
		// These two vectors should be of the same length!
		Vector<String> templateNames = typeTemplates.get(type.getName());
		Vector<MobyNamespace> nsRestrictions = typeNsRestrictions.get(type.getName());
		for(int i = 0; i < templateNames.size(); i++){
		    String templateName = templateNames.elementAt(i);
		    if(mustHaveURN && getTemplateURN(templateName) == null){
			continue;
		    }

		    // the template input moby datatype is the same type as we have an instance of
		    String textType = templateMode.get(templateName); // mode and text type name are one and the same
		    // does set-ness match?
		    if(dataTemplate instanceof MobyPrimaryDataSet){
			if(templateName.startsWith("Collection-")){
			    possibleTypes.add(textType);	
			}
		    }
		    else{
			if(!templateName.startsWith("Collection-")){
			    possibleTypes.add(textType);
			}
		    }
		    // Is there a namespace restriction on the transformation rule?
		    MobyNamespace ns = nsRestrictions.elementAt(i);
		    if(ns != null){
			for(MobyNamespace n: dataTemplate.getNamespaces()){
			    if(ns.equals(n)){
				possibleTypes.add(textType);
			    }
			}
		    }
		}
	    }
	}
	return possibleTypes.toArray(new String[possibleTypes.size()]);
    }

    public String getTemplateURN(String templateName){
	return templateIds.get(templateName);
    }

    /**
     * @return the data format the xslt creates (the last template mode attribute in the file) 
     */
    public synchronized String addMappingsFromURL(URL xsltURL) throws Exception{
	// We actually need to read in the XSLT file and keep track of the template
	// names, modes, etc..  Currently, this will not follow links that
	// import other stylesheets into the one being examined.
        Element xsltDOMRoot = null;
	Document domDoc = docBuilder.parse(xsltURL.openStream());
	xsltDOMRoot = domDoc.getDocumentElement();
        if(xsltDOMRoot == null){
            throw new Exception("Error: Could not get XSLT document as DOM from source URL " 
				+ xsltURL + " (empty or malformed document?)");
        }

	String mode = null; // returned in the end

	// Lines look something like below
	// <xsl:template match="moby:GenericSequence | GenericSequence" name="Collection-GenericSequence.1" mode="seq">
	NodeList templates = xsltDOMRoot.getElementsByTagNameNS(XSLT_NS, "template");
	for(int i = 0; i < templates.getLength(); i++){
	    Element template = (Element) templates.item(i);
	    if(!template.hasAttribute("name") || !template.hasAttribute("mode")){
		continue;
	    }
	    String templateName = template.getAttribute("name");

	    templateIds.put(templateName, getId(template));

	    String m = template.getAttribute("mode");
	    if(m != null && m.trim().length() > 0){
		mode = template.getAttribute("mode");
	    }
	    templateMode.put(templateName, m);

	    //System.err.println("Processing template " + templateName + ", mode " + template.getAttribute("mode"));

	    // Keep track of the list of templates using the given MOBY object type
	    String[] templateNameParts = templateName.split("\\.");
	    // if(templateNameParts.length == 1){
// 		System.err.println("Ignoring template without \"DataType.1\" format for its name attribute (" + 
// 				   templateName);
// 		continue;
// 	    }

	    if(templateNameParts[0].startsWith("Collection-")){
		if(templateNameParts[0].length() == 11){
		    throw new Exception("The name for one of the templates (" + templateName + 
					") did not specify a Moby data type after the Collection prefix");
		}
		templateNameParts[0] = templateNameParts[0].substring(11);
	    }

	    // See if the type exists in the ontology
	    MobyDataType type = MobyDataType.getDataType(templateNameParts[0], getRegistry());
	    MobyNamespace ns = null;  //no ns restriction by default
	    if(type == null){
		// See if there is a namespace restriction e.g. Object-EC
		if(templateNameParts[0].contains("-") && !templateNameParts[0].endsWith("-")){
		    // Assumes no dashes in the namespace label itself
		    String namespace = templateNameParts[0].substring(templateNameParts[0].lastIndexOf("-")+1);
		    //System.err.println("Found a dash in "+templateNameParts[0] + ", assuming ns is " + namespace);
		    ns = MobyNamespace.getNamespace(namespace, getRegistry());
		    // If it can be parsed as a real namespace, treat the stuff before the "-NS" as the data type
		    if(ns != null){
			templateNameParts[0] = templateNameParts[0].substring(0, templateNameParts[0].lastIndexOf("-"));
			type = MobyDataType.getDataType(templateNameParts[0], getRegistry());
		    }
		    //System.err.println("Resolved ns is " + type);
		}
		if(type == null){
		    System.err.println("Ignoring template whose name attribute (" + templateName + 
				       ") uses a non-existent datatype (" + templateNameParts[0] + ")");		    
		    continue;
		}
	    }

	    if(!typeTemplates.containsKey(templateNameParts[0])){
		typeTemplates.put(templateNameParts[0], new Vector<String>());
		typeNsRestrictions.put(templateNameParts[0], new Vector<MobyNamespace>());
	    }
	    // Later templates have higher priority than earlier declarations
	    typeTemplates.get(templateNameParts[0]).insertElementAt(templateName, 0);
	    typeNsRestrictions.get(templateNameParts[0]).insertElementAt(ns, 0);	    
	}

        DOMSource stylesheet = new DOMSource(domDoc);
	// Prepend to list, so later rules can override ones specified earlier
	moby2textConverters.insertElementAt(transFactory.newTransformer(stylesheet), 0);

	//System.err.println("Mode returned by " + xsltURL + " is " + mode);
	return mode; // return the last mode value in the file (useful when mapping XSLT -> data format)
    }

    // Looks for Dublin Core metadata
    private String getId(Element xsltDOMRoot){
	NodeList idElements = xsltDOMRoot.getElementsByTagNameNS(MobyPrefixResolver.DUBLIN_CORE_NAMESPACE,
								 "identifier");
	if(idElements.getLength() == 0){
	     idElements = xsltDOMRoot.getElementsByTagNameNS(MobyPrefixResolver.DUBLIN_CORE_NAMESPACE,
							     "source");
	}
	if(idElements.getLength() == 0){
	    return null;
	}
	if(idElements.getLength() != 1){
	    System.err.println("More than one Dublin Core identifier was found, cannot disambiguate.");
	    return null;
	}
	String id = idElements.item(0).getTextContent();
	// remove child so id doesn't show up in the xslt output!
	idElements.item(0).getParentNode().removeChild(idElements.item(0));
	System.err.println("Found and removed "+id);
	return id;
    }

   /**
     * Report whether a rule exists in the provided XSLT that converts the given MOBY
     * data to the given text type.
     */
    public boolean canProduceTextTypeFromMoby(String textType, MobyPrimaryData dataTemplate){
	if(textType == null){
	    return false;
	}
	// Spaces are not allowed in mode names
	textType = textType.replace(' ', '-');

	// Search for templates matching the given type or one of its parent types,
	// and see if they create the given text type (indicated by the template's mode attribute)
	for(MobyDataType type = dataTemplate.getDataType();
	    type != null;
	    type = type.getParent()){
	    if(typeTemplates.containsKey(type.getName())){
		// These two vectors should be of the same length!
		Vector<String> templateNames = typeTemplates.get(type.getName());
		Vector<MobyNamespace> nsRestrictions = typeNsRestrictions.get(type.getName());
		for(int i = 0; i < templateNames.size(); i++){
		    String templateName = templateNames.elementAt(i);
		    // the template input moby datatype is the same type as we have an instance of
		    if(textType.equals(templateMode.get(templateName))){  
			// Input Moby type and output text type match: does set-ness?
			if(dataTemplate instanceof MobyPrimaryDataSet){
			    if(templateName.startsWith("Collection-")){
				return true;
			    }
			}
			else{
			    if(!templateName.startsWith("Collection-")){
				return true;
			    }
			}
			// Is there a namespace restriction on the transformation rule?
			MobyNamespace ns = nsRestrictions.elementAt(i);
			if(ns != null){
			    for(MobyNamespace n: dataTemplate.getNamespaces()){
				if(ns.equals(n)){
				    return true;
				}
			    }
			}
		    }
		}
	    }
	}

	return false;
    }

    /**
     * Because XSLT works on XPath, and we don't know the MOBY tag name (it 
     * may be a subclass of the object type we have a rule for), we iteratively
     * try representing the data in simpler top level objects until we get a rule 
     * that produces something.
     *
     * @param mobyData must be an instance of a MobyPrimaryData that will be transformed
     *
     * @return null if the moby data or target text type is null, null if data is not primary, null if no templates apply, or a string representing the transformation if successful
     */
    public String getText(MobyDataInstance mobyData, String targetTextType) throws Exception{
	if(targetTextType == null || mobyData == null || !(mobyData instanceof MobyPrimaryData)){
	    return null;
	}
	// Spaces are not allowed in mode names
	targetTextType = targetTextType.replace(' ', '-');

	if(!(mobyData instanceof MobyPrimaryData)){
	    throw new IllegalArgumentException("Require a MobyPrimaryData instance for arg 0 but " +
					       "was passed a "+mobyData.getClass().getName() + " instead.");
	}

	Vector<MobyDataType> candidateTypes = new Vector<MobyDataType>();
	for(MobyDataType type = ((MobyPrimaryData) mobyData).getDataType();
	    type != null;
	    type = type.getParent()){
	    if(typeTemplates.containsKey(type.getName())){
		// These two vectors should be of the same length!
		Vector<String> templateNames = typeTemplates.get(type.getName());
		Vector<MobyNamespace> nsRestrictions = typeNsRestrictions.get(type.getName());
		for(int i = 0; i < templateNames.size(); i++){
		    String templateName = templateNames.elementAt(i);
		    // the template input moby datatype is the same type as we have an instance of
		    if(targetTextType.equals(templateMode.get(templateName))){  
			// Is there a namespace restriction on the transformation rule?
			MobyNamespace ns = nsRestrictions.elementAt(i);
			if(ns != null){
			    boolean nsMatch = false;
			    for(MobyNamespace n: ((MobyPrimaryData) mobyData).getNamespaces()){
				if(ns.equals(n)){
				    nsMatch = true;
				    break;
				}
			    }
			    if(!nsMatch){
				continue; // ns doesn't match, don't bother checking set-ness
			    }
			}

			// Input Moby type and output text type match, we'll try the template here
			if(mobyData instanceof MobyPrimaryDataSet){
			    if(templateName.startsWith("Collection-")){
				candidateTypes.add(type);
				break;
			    }
			}
			else{
			    if(!templateName.startsWith("Collection-")){
				candidateTypes.add(type);
				break;
			    }
			}
		    }
		}
	    }
	}

	for(MobyDataType type = ((MobyPrimaryData) mobyData).getDataType();
	    type != null;
	    type = type.getParent()){
	    if(typeTemplates.containsKey(type.getName())){
		for(String templateName: typeTemplates.get(type.getName())){
		    if(targetTextType.equals(templateMode.get(templateName))){
			// Input Moby type and output text type match, we'll try the template here
			if(mobyData instanceof MobyPrimaryDataSet){
			    if(templateName.startsWith("Collection-")){
				candidateTypes.add(type);
				break;
			    }
			}
			else{
			    if(!templateName.startsWith("Collection-")){
				candidateTypes.add(type);
				break;
			    }
			}
		    }
		}
	    }
	}

	int oldXMLMode = mobyData.getXmlMode();
	mobyData.setXmlMode(MobyDataInstance.SERVICE_XML_MODE);
	String xmlFormattedMobyObject = "<?xml version='1.0'?>\n"+mobyData.toXML();
	mobyData.setXmlMode(oldXMLMode);
	String origDataTypeName = ((MobyPrimaryData) mobyData).getDataType().getName();

	for(MobyDataType dataType: candidateTypes){
	    String dataTypeName = dataType.getName();
	    String mobyCastXML = xmlFormattedMobyObject;
	    mobyCastXML = mobyCastXML.replaceAll("<"+origDataTypeName+"\\s", "<"+dataTypeName+" ");
	    mobyCastXML = mobyCastXML.replaceAll("</"+origDataTypeName+"\\s*>", "</"+dataTypeName+">");
	    //System.err.println("Try to transform ("+dataTypeName+"):\n"+mobyCastXML);

	    for(Transformer transformer: moby2textConverters){
		//System.err.println("Setting XSLT var "+XSLT_MODE_VAR+": " +targetTextType);
		transformer.setParameter(XSLT_MODE_VAR, targetTextType);
		//transformer.setParameter(XSLT_ELNAME_VAR, targetName);
		//transformer.setParameter(XSLT_ELNS_VAR, targetNS);

		// Do the actual transformation
		StringWriter stringWriter = new StringWriter(1000);
		try{
		    transformer.transform(new StreamSource(new StringReader(mobyCastXML)),
					  new StreamResult(stringWriter));
		}
		catch(TransformerException te){
		    throw new Exception("Sorry! Could not transform the MOBY data into text form: " + te);
		}

		if(stringWriter.getBuffer().length() > 0){
		    //System.err.println("Result of transformation: " + stringWriter.toString());
		    return stringWriter.toString();
		}
	    }
	}

	return null;
    }

    /**
     * Convenience method to backtrack from a mapping rule to the Moby datatype it consumes.
     *
     * @param ruleSource where the rule should be loaded from
     * @param ruleURI a unique ID for the rule, in the source XML using Dublin Core
     *
     * @return a template object with the (minimal) datatype and namespaces consumed by the rule
     */
    public static MobyPrimaryDataSimple getObjectConsumed(URL ruleSource, String ruleURI, Registry reg) throws Exception{
	MobyPrimaryDataSimple template = null;
	TextClient client = null;
	try{
	    client = new TextClient(reg);
	    client.addMappingsFromURL(ruleSource);
	} catch(Exception e){
	    throw new Exception("Internal error: Could not create TextClient and load the rule (from " + 
				ruleSource+"): "+e.getMessage(), 
				e);
	}	 
	
	if(client.templateIds.isEmpty()){
	    throw new Exception("Internal error: loaded the transformation rule (URI " + ruleURI + 
				") from " + ruleSource + " but could not retrieve it from TextClient " +
				"using the URI.  Make sure the transformation rule contains a Dublin Core " +
				"identifier element specifying this URI.");
	}
	else if(!client.templateIds.containsValue(ruleURI)){
	    throw new Exception("Internal error: loaded the transformation rule (URI " + ruleURI + 
				") from " + ruleSource + " but could not retrieve it from TextClient " +
				"using the URI.  The given Dublin Core info for the rule actually loaded " +
				"was " + client.templateIds.values().iterator().next());
	}
	else{
	    template = new MobyPrimaryDataSimple("templateRuleReturnedObject");
	    // extract the datatype from typeTemplates, there should only be one...
	    Set<String> dataTypeNames = client.typeTemplates.keySet();
	    if(dataTypeNames.size() == 0){
		throw new Exception("No TextClient rules were loaded from " + ruleSource +
				    " - make sure the XSLT follows the naming conventions described at " +
				    "http://biomoby.open-bio.org/CVS_CONTENT/moby-live/Java/docs/demRules.html");
	    }
	    if(dataTypeNames.size() > 1){
		throw new Exception("Multiple TextClient rules were loaded from " + ruleSource +
				    ", please separate them out into separate XSLT files to ensure " +
				    "they can be referenced indivudually and unambiguously.");
	    }
	    MobyDataType type = MobyDataType.getDataType(dataTypeNames.toArray()[0].toString(), client.getRegistry());
	    template.setDataType(type);
	    // extract the ns from typeNsRestrictions
	    Vector<MobyNamespace> nsRestrictions = client.typeNsRestrictions.get(type.getName());
	    template.setNamespaces(nsRestrictions.toArray(new MobyNamespace[nsRestrictions.size()]));
	 }
	return template;
    }
    
    public Registry getRegistry(){
	return registry;
    }     
}
