/* Author: Jeff Dalton <J.Dalton@ed.ac.uk>
 * Updated: Fri Nov 30 13:38:07 2001 by Jeff Dalton
 * Copyright: (c) 2001, AIAI, University of Edinburgh
 */

package ix.iface.util;

import java.util.*;

import java.io.IOException;
import java.io.StringReader;

// Imports for using JDOM
// import java.io.IOException;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.Attribute;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jdom.output.XMLOutputter;

import ix.icore.Issue;
import ix.icore.BasicIssue;
import ix.icore.Report;
import ix.icore.process.StatusValues;

import ix.ichat.ChatMessage;

import ix.util.lisp.*;
import ix.util.*;

/**
 * A class that encapsulates the XML knowledge needed for
 * sending and receiving Issues and Reports and that contains
 * other useful static XML utilities. <p>
 * 
 * Example issue:
 * <pre>
 *    &lt;issue sender-id="ILEED" ref="ILEED-IDEEL-1" report-back="no"&gt;
 *       &lt;priority&gt;high&lt;/priority&gt;
 *       &lt;statement&gt;avoid elephants laki_safari_park&lt;/statement&gt;
 *    &lt;/issue&gt;
 * </pre>
 *
 * Example report:
 * <pre>
 *    &lt;report sender-id="MCA" ref="MCA-IDEEL-1"&gt;
 *       done - no conflicts
 *    &lt;/report&gt;
 * </pre>
 */

/* Those examples again:
 *
 * <issue sender-id="ILEED" ref="ILEED-IDEEL-1" report-back="no">
 *    <priority>high</priority>
 *    <statement>avoid elephants laki_safari_park</statement>
 * </issue>
 *
 * <report sender-id="MCA" ref="MCA-IDEEL-1">
 *    done - no conflicts
 * </report>
 */

public class XML {

    private XML() { }		// don't allow instantiation

    /**
     * Thrown to indicate a problem converting to or from XML.
     */
    public static class XMLException extends RuntimeException {
	public XMLException(String message) { super(message); }
    }

    /*
     * XML Parsing
     */

    /**
     * Converts a string of XML to an object such as an Issue or Report.
     */
    public static Object objectFromXML(String text) {
	Document doc = parseXML(text);
	Element root = doc.getRootElement();
	String rootName = root.getName();
	if (rootName.equals("issue"))
	    return issueFromXML(root);
	else if (rootName.equals("report"))
	    return reportFromXML(root);
	else if (rootName.equals("service-address"))
	    return serviceAddressFromXML(root);
	else if (rootName.equals("chat-message"))
	    return chatMessageFromXML(root);
	else
	    throw new XMLException("Can't handle " + root);
    }

    /**
     * A Set containing the attribute names that are valid in "issue"
     * elements.
     */
    public static Set issueAttributes
	= new HashSet(Lisp.list("sender-id", "ref", "report-back"));

    /**
     * Converts a JDOM Element to an Issue.
     */
    public static Issue issueFromXML(Element issueElt) {
	int priority = -1;
	String statement = null;
	for (Iterator i = issueElt.getChildren().iterator(); i.hasNext();) {
	    Element child = (Element)i.next();
	    if (child.getName().equals("priority"))
		priority = priorityFromXML(child);
	    else if (child.getName().equals("statement"))
		statement = statementFromXML(child);
	    else
		throw new XMLException("Illegal within issue: " + child);
	}
	if (statement == null)
	    throw new XMLException("Need a statement in " + issueElt);

	Issue issue = new BasicIssue(statement);
	if (priority != -1)
	    issue.setPriority(priority);
	issue.setProperties
	    (propertiesFromAttributes(issueElt, issueAttributes));
	return issue;
    }

    public static int priorityFromXML(Element priorityElt) {
	String name = priorityElt.getText();
	return ViewColor.priorityValue(name);
    }

    public static String statementFromXML(Element statementElt) {
	return statementElt.getText();
    }

    /**
     * A Set containing the attribute names that are valid in "report"
     * elements.
     */
    // report-type = success, failure, or progress
    public static Set reportAttributes
	= new HashSet(Lisp.list("sender-id", "ref", "report-type"));

    /**
     * Converts a JDOM Element to a Report.
     */
    public static Report reportFromXML(Element reportElt) {
	Report report = new Report(reportElt.getText());
	report.setProperties
	    (propertiesFromAttributes(reportElt, reportAttributes));
	return report;
    }

    /**
     * Converts a JDOM Element to an IPC.ServiceAddress.
     */
    public static IPC.ServiceAddress serviceAddressFromXML(Element addrElt) {
	// This is messy but should work.  /\/
	Map props = propertiesFromAttributes(addrElt,
					     serviceAddressAttributes);
	String host = (String)props.get("host");
	Long port = (Long)Lisp.readFromString((String)props.get("port"));
	return new IPC.ServiceAddress(host, port.intValue());
    }

    public static Set serviceAddressAttributes
	= new HashSet(Lisp.list("host", "port"));

    /**
     * Converts a JDOM Element to a ChatMessage
     */
    public static ChatMessage chatMessageFromXML(Element elt) {
	Map props = propertiesFromAttributes(elt, chatMessageAttributes);
	String senderId = (String)props.get("sender-id");
	String text = elt.getText();
	return new ChatMessage(text, senderId);
    }

    public static Set chatMessageAttributes
        = new HashSet(Lisp.list("sender-id"));

    /**
     * Converts XML attributes to a Map.
     */
    public static Map propertiesFromAttributes(Element elt, Set validNames) {
	Map map = new HashMap(5);
	List attributes = elt.getAttributes();
	for (Iterator i = attributes.iterator(); i.hasNext();) {
	    Attribute attr = (Attribute)i.next();
	    String name = attr.getName();
	    if (validNames.contains(name))
		map.put(name, attr.getValue());
	    else
		throw new XMLException
		    ("Invalid attribute " + Util.quote(name)
		     + " in " + elt);
	}
	return map;
    }

    /**
     * The class of the SAX parser used by the <code>parseXML</code>
     * method.
     */
    static String SAXDriverClass = "org.apache.xerces.parsers.SAXParser";

    /**
     * Converts a string of XML to a JDOM Document.
     */
    public static Document parseXML(String text) {
	StringReader reader = new StringReader(text);
	try {
	    SAXBuilder builder = new SAXBuilder(SAXDriverClass);
	    Document doc = builder.build(reader);
	    return doc;		// for now /\/
	}
	catch (Exception e) {
	    Debug.noteln("While parsing", e);
	    // Debug.noteException(e);
	    // Throw because we have nothing to return
	    // and stray nulls are a pain.
	    throw new XMLException("Can't parse because " + e);
	}
    }

    /*
     * XML Generation
     */

    /**
     * Converts an object, such as an Issue or Report, to a string of XML.
     */
    public static String objectToXML(Object contents) {
	if (contents instanceof Issue) {
	    return issueToXML((Issue)contents);
	}
	else if (contents instanceof Report) {
	    return reportToXML((Report)contents);
	}
	else if (contents instanceof IPC.ServiceAddress) {
	    return serviceAddressToXML((IPC.ServiceAddress)contents);
	}
	else if (contents instanceof ChatMessage) {
	    return chatMessageToXML((ChatMessage)contents);
	}
	else {
	    throw new XMLException("Can't convert to XML " + contents);
	}
    }

    public static String issueToXML(Issue issue) {
	// Issues should make it easier to get a description by
	// providing an appropriate method.  /\/
	return
	    "<issue" + propertiesToAttributes(issue.getProperties(),
					      issueAttributes) + ">" +
	        priorityToXML(issue.getPriority()) +
	        statementToXML(issueDescription(issue)) +
	    "</issue>";
    }

    public static String priorityToXML(int priority) {
	if (priority == StatusValues.PRIORITY_NONE)
	    return "";
	else {
	    String p = ViewColor.priorityName[priority].toLowerCase();
	    return "<priority>" + p + "</priority>";
	}
    }

    public static String statementToXML(String statement) {
	return "<statement>" + statement + "</statement>";
    }

    public static String issueDescription(Issue i) {
	LList pattern = Lisp.cons(i.getVerb(), (LList)i.getParameters());
	return Lisp.elementsToString(pattern);
    }

    public static String reportToXML(Report report) {
	return
	    "<report" + propertiesToAttributes(report.getProperties(),
                                               reportAttributes) + ">" +
	        report.getText() +
	    "</report>";
    }

    public static String propertiesToAttributes(Map map, Set validNames) {
	StringBuffer result = new StringBuffer(64);
	for (Iterator i = map.entrySet().iterator(); i.hasNext();) {
	    Map.Entry e = (Map.Entry)i.next();
	    String name = e.getKey().toString();
	    String value = e.getValue().toString();
	    if (validNames.contains(name))
		result
		    .append(" ")
		    .append(name).append("=\"").append(value).append("\"");
	    else
		;		// ignore
	}
	return result.toString();
    }

    public static String serviceAddressToXML(IPC.ServiceAddress addr) {
	return
	    "<service-address" +
	    " host=" + Util.quote(addr.getHost()) +
	    " port=" + Util.quote("" + addr.getPort()) +
	    "/>";
    }

    public static String chatMessageToXML(ChatMessage message) {
	return
	    "<chat-message sender-id=" + Util.quote(message.getSenderId()) +
	    ">" +
	    message.getText() +
	    "</chat-message>";
    }


    /**
     * An XML outputter created by makePrettyXMLOutputter().
     */
    public static XMLOutputter prettyXMLOutputter = makePrettyXMLOutputter();

    /**
     * Constructs a JDOM XMLOutputter that outputs a JDOM Document
     * in a nicely indented fashion.  For example:
     * <pre>
     *    XMLOutputter outputter = makePrettyXMLOutputter()
     *    outputter.output(doc, System.out);
     *    System.out.flush();
     * </pre>
     */
    public static XMLOutputter makePrettyXMLOutputter() {
	// Create an outputter with 3 space indent and newlines=true
	XMLOutputter outputter = new XMLOutputter("   ", true);
	outputter.setTrimText(true);
	return outputter;
    }

}
