// XMLTree.java
//
// Created: February 2006
//
// This file is a component of the BioMoby project.
// Copyright Martin Senger (martin.senger@gmail.com).
//

package org.biomoby.service.dashboard.renderers;

import org.biomoby.shared.MobyException;

import org.tulsoft.tools.gui.SwingUtils;

import javax.swing.Icon;
import javax.swing.JTree;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;

import org.jdom.Attribute;
import org.jdom.Content;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.Parent;
import org.jdom.Text;
import org.jdom.input.SAXBuilder;

import java.awt.Component;
import java.awt.Color;

import java.io.StringReader;
import java.util.Enumeration;
import java.util.Iterator;

/**
 * Dislaying XML data in a JTree structure. <p>
 *
 * This was taken (with small changes) from the <a
 * href="http://taverna.sf.net">Taverna</a> project (see the original
 * authors below). There it was derived from original code by Kyle
 * Gabhart from
 * http://www.devx.com/gethelpon/10MinuteSolution/16694/0/page/1. The
 * Taverna authors say to it: "...then subsequently heavily rewritten
 * to move to JDOM, and moved lots of the setup code to the renderer
 * to cut down initialisation time. Added text node size limit as
 * well. Displaying large gene sequences as base64 encoded text in a
 * single node really, really hurts performance.". <p>
 * 
 * @author Kyle Gabhart
 * @author Tom Oinn
 * @author Kevin Glover
 * @author <A HREF="mailto:martin.senger@gmail.com">Martin Senger</A>
 * @version $Id: XMLTree.java,v 1.2 2006/02/20 05:51:11 senger Exp $
 */

public class XMLTree
    extends JTree {

//    private static org.apache.commons.logging.Log log =
//	org.apache.commons.logging.LogFactory.getLog (XMLTree.class);

    public static final int MAX_TEXT_SIZE = 1000;

    int textSizeLimit = MAX_TEXT_SIZE;
    Element rootElement = null;

    // shared icons
    static Icon nodeIcon, leafIcon;

    /*********************************************************************
     * Build a new XMLTree from the supplied String containing
     * XML. The size of text nodes is limited to
     * {@link #MAX_TEXT_SIZE}.
     *
     * @param text to be rendered
     * @throws MobyException if there is a XML parsing or I/O error
     ********************************************************************/
    public XMLTree (String text)
	throws MobyException {
	super();
	init (createDocument (text).getRootElement());
	revalidate();
    }

    /*********************************************************************
     * Build a new XMLTree from the supplied String containing XML. <p>
     *
     * @param text to be rendered
     * @param textSizeLimit is the maximum length of the text element to be
     * displayed; use -1, or <tt>Integer.MAX_VALUE</tt> for unlimited
     * length
     * @throws MobyException if there is a XML parsing or I/O error
     ********************************************************************/
    public XMLTree (String text, int textSizeLimit)
	throws MobyException {
	this.textSizeLimit = textSizeLimit;
	init (createDocument (text).getRootElement());
	revalidate();
    }

    /*********************************************************************
     * Build a new XMLTree from the supplied XML document. <p>
     *
     * @param document to be rendered
     ********************************************************************/
    public XMLTree (Document document) {
	super();
	init (document.getRootElement());
	revalidate();
    }

    /*********************************************************************
     * Build a new XMLTree from the supplied XML document. <p>
     *
     * @param document to be rendered
     * @param textSizeLimit is the maximum length of the text element to be
     * displayed; use -1, or <tt>Integer.MAX_VALUE</tt> for unlimited
     * length
     ********************************************************************/
    public XMLTree (Document document, int textSizeLimit) {
	super();
	this.textSizeLimit = textSizeLimit;
	init (document.getRootElement());
	revalidate();
    }

    /*********************************************************************
     * Load all icons.
     ********************************************************************/
    protected void loadIcons() {
	if (nodeIcon == null)
 	    nodeIcon = SwingUtils.createIcon ("images/xml_node.gif", XMLTree.class);
	if (leafIcon == null)
	    leafIcon = SwingUtils.createIcon ("images/leaf.gif", XMLTree.class);
    }

    /*********************************************************************
     *
     ********************************************************************/
    protected Document createDocument (String text)
	throws MobyException {
	try {
	    return new SAXBuilder (false)
		.build (new StringReader (text));
	} catch (Exception e) {
	    throw new MobyException (e.toString());
	}
    }

    /*********************************************************************
     *
     ********************************************************************/
    private void init (Content content) {
	loadIcons();
	rootElement = (Element)content;
	// Fix for platforms other than metal which can't otherwise
	// cope with arbitrary size rows
	setRowHeight(0);
	getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
	setShowsRootHandles(true);
	setEditable(false);
	setModel(new DefaultTreeModel(createTreeNode(content)));
	XMLTreeCellRenderer tRend = new XMLTreeCellRenderer();
	tRend.setLeafIcon (leafIcon);
	setCellRenderer (tRend);
	setAllNodesExpanded();
    }

    public void setAllNodesExpanded() {
	synchronized (this.getModel()) {
	    expandAll(this, new TreePath(this.getModel().getRoot()), true);
	}
    }
    
    private void expandAll (JTree tree, TreePath parent, boolean expand) {
	synchronized (this.getModel()) {
	    // Traverse children
	    // Ignores nodes who's userObject is a Processor type to
	    // avoid overloading the UI with nodes at startup.
	    TreeNode node = (TreeNode) parent.getLastPathComponent();
	    if (node.getChildCount() >= 0) {
		for (Enumeration e = node.children(); e.hasMoreElements();) {
		    TreeNode n = (TreeNode) e.nextElement();
		    TreePath path = parent.pathByAddingChild(n);
		    expandAll(tree, path, expand);
		}
	    }
	    // Expansion or collapse must be done bottom-up
	    if (expand)
		tree.expandPath(parent);
	    else
		tree.collapsePath(parent);
	}
    }
    
    private XMLNode createTreeNode(Content content) {
	XMLNode node = new XMLNode(content);
	if (content instanceof Parent) {
	    Parent parent = (Parent) content;
	    Iterator children = parent.getContent().iterator();
	    while (children.hasNext()) {
		Object child = children.next();
		if (child instanceof Element) {
		    node.add(createTreeNode((Content) child));
		} else if (textSizeLimit != 0 && child instanceof Text) {
		    Text text = (Text) child;
		    if (!text.getTextNormalize().equals("")) {
			node.add(createTreeNode(text));
		    }
		}
	    }
	}
	return node;
    }

    /*********************************************************************
     *
     * Class for a node...
     *
     ********************************************************************/
    private class XMLNode extends DefaultMutableTreeNode {
	public XMLNode (Content userObject) {
	    super (userObject);
	}
    }

    /*********************************************************************
     *
     * Tree renderer...
     *
     ********************************************************************/
    private class XMLTreeCellRenderer
	extends DefaultTreeCellRenderer {

	public Color getBackgroundNonSelectionColor() {
	    return null;
	}

	public Color getBackground() {
	    return null;
	}

	public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel,
						      boolean expanded, boolean leaf,
						      int row, boolean hasFocus) {
	    super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
	    setOpaque(false);
	    if (value instanceof XMLNode) {
		XMLNode node = (XMLNode) value;
		if (node.getUserObject() instanceof Element) {
 		    setIcon (nodeIcon);
		    Element element = (Element) node.getUserObject();
		    StringBuffer nameBuffer = new StringBuffer("<html>"
							       + element.getQualifiedName());
		    boolean addedAnAttribute = false;
		    // Bit of a quick and dirty hack here to try to ensure
		    // that the element namespace is shown. There appears no
		    // way to get the actual xmlns declarations that are
		    // part of an element through jdom. Also, please note,
		    // theres no namespace handling at all for attributes...
		    if (element.getParent() instanceof Element) {
			Element parent = (Element) element.getParent();
			if (parent.getNamespace(element.getNamespacePrefix()) == null) {
			    nameBuffer.append(" <font color=\"purple\">xmlns:"
					      + element.getNamespacePrefix()
					      + "</font>=\"<font color=\"green\">"
					      + element.getNamespaceURI() + "</font>\"");
			}
		    } else {
			nameBuffer.append(" <font color=\"purple\">xmlns:"
					  + element.getNamespacePrefix()
					  + "</font>=\"<font color=\"green\">"
					  + element.getNamespaceURI() + "</font>\"");
		    }
		    
		    Iterator attributes = element.getAttributes().iterator();
		    while (attributes.hasNext()) {
			Attribute attribute = (Attribute) attributes.next();
			String name = attribute.getName().trim();
			String attributeValue = attribute.getValue().trim();
			if (attributeValue != null) {
			    if (attributeValue.length() > 0) {
				if (addedAnAttribute) {
				    nameBuffer.append(",");
				}
				addedAnAttribute = true;
				nameBuffer.append(" <font color=\"purple\">" + name
						  + "</font>=\"<font color=\"green\">" + attributeValue
						  + "</font>\"");
			    }
			}
		    }
		    
		    nameBuffer.append("</html>");
		    setText(nameBuffer.toString());

		} else if (node.getUserObject() instanceof Text) {
		    Text text = (Text) node.getUserObject();
		    String name = text.getText();
		    if (textSizeLimit > -1 && name.length() > textSizeLimit) {
			name = name.substring(0, textSizeLimit) + "...";
		    }
		    setText("<html><pre><font color=\"blue\">"
			    + name.replaceAll("<br>", "\n").replaceAll("<", "&lt;")
			    + "</font></pre></html>");
		}
	    }
	    setBackground(new Color(0,0,0,0));
	    return this;
	}
    }

}
