package com.purpletech.xml;

import com.purpletech.util.*;
import java.util.*;
import java.io.*;
import org.w3c.dom.*;
import org.xbeans.*;

// todo: make changes propagate to DOM node
/**
 * An EntryList that can parse a DOM node to set its values. <p>
 *
 * Contiguous text nodes in the tree get concatenated, and stored as an
 * entry named "#text". It has complicated rules for parsing text
 * nodes, but they do what you'd expect them to do in most cases. <p>
 *
 * Supports XBeans events. 
 **/
public class XMLEntryList extends EntryList implements DOMListener, DOMSource, XMLPrinter
{
    static protected XMLEntryListFactory defaultfactory = new XMLEntryListFactory();
    
    protected List attributes = new ArrayList();
    protected String listName;
    protected XMLEntryListFactory factory = defaultfactory;
    protected Node node;
    
    public XMLEntryList() {}

    public XMLEntryList(Node node) throws XMLException {
	this.node = node;
	setListName(node.getNodeName());
	parseNode();
    }

    public XMLEntryList(String listName, XMLEntryListFactory factory) {
	setListName(listName);
	setFactory(factory);
    }

    public XMLEntryList(String listName, XMLEntryListFactory factory,
			Node node) throws XMLException
    {
	setListName(listName);
	setFactory(factory);
	this.node = node;
	parseNode();
    }

    public Object get(Object key)
    {
	if (key.equals("")) key = "#text";
	return super.get(key);
    }

    protected void parseNode() throws XMLException {
	if (factory.parse(node, this) != this) {
	    throw new XMLException("inappropriate node");
	}
    }	
    
    public void setFactory(XMLEntryListFactory factory) {
	if (factory != null)
	    this.factory = factory;
    }
    
    protected XMLEntryListFactory getFactory() {
	return factory;
    }
    
    public void setAttribute(String attname) {
	if (!attributes.contains(attname))
	    attributes.add(attname);
    }

    public void setListName(String listName) {
	this.listName = listName;
    }
    
    public String getListName() {
	return listName;
    }

    /** does NOT parse -- must call parseNode() immediately **/
    public void setNode(Node node) {
	this.node = node;
    }

    /**
     * returns this, so you can say "new XMLEntryList().parseNode(node)"
     **/
    public XMLEntryList parseNode(Node node) throws XMLException {
	this.node = node;
	parseNode();
	return this;
    }
    
    public Node getNode() {
	return node;
    }
    
    public void printXml(PrintStream out)
    { printXML(out); }
    
    public void printXML(PrintStream out)
    {
	PrintWriter w = new PrintWriter(out);
	printXml(w,0);
	w.flush();
    }

    public void printXml(PrintWriter out, int indent)
    { printXML(out,indent); }
    
    /**
     * If it has a name, then it'll print itself as an XML element,
     * including tag name and attributes. If its name is null, then it
     * just prints its elements.
     **/
    public void printXML(PrintWriter out, int indent)
    {
	Iterator i;
	// print tag
	if (listName != null) {
	    if (indent > 0) {
		out.println();
		Utils.printIndent(out, indent);
	    }
	    printOpenTag(out, listName, attributes);
	}
	
	if (this.get("#text") == null) {
	    // has no text, so indent prettily
	    indent += 2;
	    printElements(out, indent);
	    indent -= 2;
	    out.println();
	    Utils.printIndent(out, indent);
	}
	else {
	    // has some text, so jam it onto one line
	    printElements(out,0);
	}

	if (listName != null)
	    printCloseTag(out, listName);
    }

    public void printElements(PrintWriter out, int indent)
    {
	// print elements
	Iterator i = iterator();
	while (i.hasNext()) {
	    Map.Entry e = (Map.Entry)i.next();
	    printElement(out, indent, e);
	}
    } // printElements

    protected void printElement(PrintWriter out, int indent, Map.Entry entry)
    {
	if (entry.getValue() instanceof XMLPrinter) {
	    ((XMLPrinter)entry.getValue()).printXML(out, indent);
	}
	else if (listName == null || !attributes.contains(entry.getKey()))
	{
	    if (entry.getValue() != null) // skip null values ???
	    {
		if (indent > 0 ) {
		    out.println();
		    Utils.printIndent(out, indent);
		}

		String tag  = (String)entry.getKey();
		printOpenTag(out, tag, null);		

		// if (entry.getValue() != null) 

		out.print(fix(entry.getValue().toString()));

		printCloseTag(out, tag);
		
	    }
	}
    } // printElement

    protected void printOpenTag(PrintWriter out, String tag, List attributes) {
	boolean isText = (tag.equals("#text"));
	boolean isComment = (tag.equals("#comment"));
	if (isText)
	{
	}
	else if (isComment)
	{
	    out.print("<!--");
	}
	else
	{
	    out.print("<");
	    out.print(tag);

	    if (attributes != null) {
		Iterator i = attributes.iterator();
		while (i.hasNext()) {
		    String att = (String)i.next();
		    if (get(att) != null) {
			out.print(" ");
			out.print(att);
			out.print("='");
			out.print(get(att));
			out.print("'");
		    }
		}
	    }
	    out.print(">");
	}
    }    

    protected void printCloseTag(PrintWriter out, String tag)
    {
	boolean isText = (tag.equals("#text"));
	boolean isComment = (tag.equals("#comment"));
	
	if (isText)
	{
	}
	else if (isComment)
	{
	    out.print("-->");
	}
	else
	{
	    out.print("</");
	    out.print(tag);
	    out.print(">");
	}	
    }
    
    /**
     * return a single string representing all sub-elements, including
     * tags
     **/
    public String flatten() {
	StringWriter sw = new StringWriter();
	PrintWriter pw = new PrintWriter(sw);
	printElements(pw, 0);
	pw.close();
	return sw.toString();
    }

    /**
     * Squoosh all text children. Turns strings of all-whitespace
     * nodes into nothing. That is, <pre>
     * "#text" = ""
     * "b" = [some other object]
     * "#text" = ""
     * "#text" = "yo mama"
     * "#text" = ""
     * </pre> turns into <pre>
     * "b" = [some other object]
     * "#text" = "yo mama"
     * </pre>
     **/
    public void squoosh() {
	String s = "";
	int i = 0; 	// jdk1.2 lets us reuse i
	while (i<this.size())
	{
	    Map.Entry entry = (Map.Entry)get(i);
	    if (entry.getKey().equals("#text"))
	    {
		if (!(entry.getValue() == null ||
		      Utils.isWhitespace((String)entry.getValue())))
		    s += entry.getValue();
		this.remove(i);
		// don't inc - stay on (next) value
	    }
	    else
	    {
		if (!s.equals("")) {
		    add(i, new EntryList.Entry("#text", s));
		    s = "";
		    i++;  	// insert and inc to current item
		}
		i++;	// inc to next item
	    }
	}
	if (!s.equals("")) {
	    this.add(new EntryList.Entry("#text", s));
	}
    }

    // xbeans support

    protected DOMListener DOMListener;

    // The next 2 methods implement operations in the DomSource interface.

    // Sets the listener of DOM events, i.e. the next Xbean on the chain.
    public void setDOMListener(DOMListener newDomListener) {
        DOMListener = newDomListener;
    }

    // Returns the listener of DOM events.
    public DOMListener getDOMListener(){
        return DOMListener;
    }

    /*
        The following method implements the DomListener interface.
        This is where code that processes the DOM goes.  (In this sample
        bean no processing is done.) Once the processing
        is complete, the method calls the next Xbean.
    */

    public void documentReady(DOMEvent evt) throws XbeansException {
        if (DOMListener==null) {
            throw new XbeansException(
                        evt.getDocument().getNodeName(),
                        "ItemBean",
                        "next component not established",
                        "The component needs to be configured."
                        );
        }

        // Code to process the incoming DOM document goes here

	// should we clear this item first? i think this will only be
	// called on a fresh, empty bean, so we shouldn't have to...
	setNode(evt.getDocument());
	try {
	    parseNode();
	    
	    // Pass the document on to the next Xbean.  It is required that all
	    // source Xbeans call the next Xbean.
	    DOMListener.documentReady(evt);
	}
	catch (XMLException e) {
	    throw new XbeansException(evt.getDocument().getNodeName(), this.toString(), e.getMessage(), "");
	}
    }


    public String toString() {
	if (listName != null)
	    return listName + ":" + super.toString();
	else
	    return super.toString();
    }    

    public static void main(String[] args) {
	XMLEntryList list = new XMLEntryList();
	list.add("foo", "bar");
	list.add("#text", "some text");

	PrintWriter out = new PrintWriter(System.out);
	list.printXml(out);
	out.println("flattened:");
	out.println("'" + list.flatten() + "'");
	out.flush();
    }
}
