/**
 * Scott's XML Editor - Swing and JDOM XML Editor
 * @author Scott Hurring - scott at hurring dot com
 * @version beta1
 * @license GPL
 * For most recent version, go to this url:
 * http://hurring.com/code/java/xmleditor/
 */

import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.util.List;

import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.JTree;
import javax.swing.event.ChangeEvent;
import javax.swing.table.DefaultTableModel;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;

import org.jdom.Attribute;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;

/**
 * XMLEditor - handles editing the XML
 */
public class XMLEditor
{
	private ScottEditor parent;

	// The Tree representing the XML Document
	public XMLEditorTree xmltree;
	// Element attributes table
	private XMLEditorAttributeTable attribtable;

	// Currently selected node in the XMLEditorTree
	private XMLEditorTreeNode xmlnode;
	// Currently selected Element
	public Element e;
	// Currently selected row number in XMLEditorTree
	private int selectedRow = 0;

	private JTextField elementNameText;
	private JTextField elementValueText;
	private JButton applyButton;
	private JButton newChildButton;
	private JButton newAttribButton;
	
	public XMLEditor(ScottEditor parent) {
		this.parent = parent;
	}
	
/*-------------------------------------------------------------------
 * GUI Stuff
 */
	
	public JPanel createGUI() {
		JPanel p = new JPanel(new BorderLayout());
		GridLayout grid = new GridLayout(1,3);
	
		JSplitPane jpp = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true);
		jpp.setOneTouchExpandable(true);
		
		jpp.add(new JScrollPane(createDetailsPanel()));
		jpp.add(new JScrollPane(createAttributesTable()));
		
		
		JSplitPane jp = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true);
		jp.setOneTouchExpandable(true);
		jp.setAlignmentX(JSplitPane.RIGHT_ALIGNMENT);
		jp.add(new JScrollPane(createXMLTree()));
		jp.add(jpp);
		
		jp.setDividerLocation(450);
		jpp.setDividerLocation(175);
		
		p.add(jp, BorderLayout.CENTER);
		return p;
	}
	
	private JPanel createXMLTree() {
		JPanel p = new JPanel(new BorderLayout(5,5));
		xmltree = new XMLEditorTree();
		xmltree.addMouseListener(new MouseAdapter() {
			public void mousePressed(MouseEvent e) {
				selectXMLTreeElement((JTree) e.getComponent());
			}
		});
		p.add(new JLabel("XML Structure Tree"), BorderLayout.NORTH);
		p.add(xmltree, BorderLayout.CENTER);		
		return p;
	}
	
	private JPanel createAttributesTable() {
		attribtable = new XMLEditorAttributeTable(this);
		attribtable.setModel(getAttribModel());
		attribtable.setRowSelectionAllowed(false);

		JPanel p = new JPanel(new BorderLayout(5,5));		
		p.add(new JLabel("Attributes of the selected element"), BorderLayout.NORTH);

		JPanel ap = new JPanel(new BorderLayout());
		ap.add(attribtable.getTableHeader(), BorderLayout.NORTH);
		ap.add(attribtable, BorderLayout.CENTER);
		
		p.add(ap, BorderLayout.CENTER);
		return p;
	}
	
	private JPanel createDetailsPanel() {
		JPanel p = new JPanel(new BorderLayout(5,5));
		p.add(new JLabel("Element details"), BorderLayout.NORTH);
		
		JPanel dp = new JPanel(new GridLayout(0,2));

		elementNameText = new JTextField();
		elementNameText.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				applyChanges();
			}
		});

		elementValueText = new JTextField();
		elementValueText.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				applyChanges();
			}
		});

		dp.add(new JLabel("Name:"));	dp.add(elementNameText);
		dp.add(new JLabel("Value:"));	dp.add(elementValueText);
		dp.add(new JLabel());			dp.add(createApplyChangesButton());
		dp.add(createNewAttributeButton());
		dp.add(createNewChildButton());
		
		disableDetails();
		
		JPanel dpp = new JPanel(new BorderLayout());
		dpp.add(dp, BorderLayout.NORTH);
		p.add(dpp, BorderLayout.WEST);
		return p;
	}
	
	private JButton createApplyChangesButton() {
		applyButton = new JButton("Apply Changes");
		applyButton.addMouseListener(new MouseAdapter() {
			public void mousePressed(MouseEvent e) {
				applyChanges();
			}
		});
		return applyButton;
	}

	private JButton createNewChildButton() {
		newChildButton = new JButton("New Child");
		newChildButton.addMouseListener(new MouseAdapter() {
			public void mousePressed(MouseEvent e) {
				addNewChild();
				updateXMLTree();
			}
		});
		return newChildButton;
	}
	
	private JButton createNewAttributeButton() {
		newAttribButton = new JButton("New Attribute");
		newAttribButton.addMouseListener(new MouseAdapter() {
			public void mousePressed(MouseEvent evt) {
				addNewAttribute();
				showAttributes(e);
			}
		});
		return newAttribButton;
	}

/*-----------------------------------------------------------------*/

	/**
	 * Create the attribute model for attribute table
	 */
	private DefaultTableModel getAttribModel() {
		DefaultTableModel attribmodel = new DefaultTableModel();
		attribmodel.addColumn("Name");
		attribmodel.addColumn("Value");
		return attribmodel;
	}

	/**
	 * Disable details when there's no Tree node selection
	 */
	private void disableDetails() {
		elementNameText.setEnabled(false);
		elementValueText.setEnabled(false);
		applyButton.setEnabled(false);
		newChildButton.setEnabled(false);
		newAttribButton.setEnabled(false);
	}
	
	/**
	 * Enable details when there's a Tree node selection
	 */
	private void enableDetails() {
		elementNameText.setEnabled(true);
		elementValueText.setEnabled(true);
		applyButton.setEnabled(true);
		newChildButton.setEnabled(true);
		newAttribButton.setEnabled(true);
		
		// Editing the value of an elem with subtags screws up the subtags
		if (!xmlnode.isLeaf()) {
			elementValueText.setEnabled(false);
			elementValueText.setText("");
		}
	}

	/**
	 * Apply the changes you made to the element
	 */
	protected void applyChanges() {
		parent.debug("Applying changes");
		
		try {
			e.setName(elementNameText.getText());
			xmlnode.setName(elementNameText.getText());
		} catch(Exception e) {
			parent.debug("Error! "+ e.getMessage());
		}
		
		if (xmlnode.isLeaf()) {
			e.setText(elementValueText.getText());
		}
		
		updateXMLTree();
	}
	
	/**
	 * Add a new child to the currently selected element.
	 */
	protected void addNewChild() {
		parent.debug("Adding new child to: "+ e.getName());
		e.addContent(new Element("empty"));
	}

	/**
	 * Add a blank new attribute to the currently selected element.
	 */
	protected void addNewAttribute() {
		parent.debug("Adding new attribute to: "+ e.getName());
		e.setAttribute("empty", "empty");
	}
	
	/**
	 * Called from the attribute table when an attribute is edited.
	 */
	public void editAttribute(String name, String value, String oldname, String oldvalue) 
	throws Exception
	{
		try {
			e.setAttribute(name, value);
			if (!oldname.equals(name)) {
				e.removeAttribute(oldname);
			}
		} catch(Exception ex) {
			parent.debug("Error! "+ ex.getMessage());
			parent.debug("Reverting to: "+ oldname +"="+ oldvalue);
			throw new Exception();
		}
	}

	/**
	 * Called from the Tree when a node is selected.
	 */
	public void selectXMLTreeElement(JTree t) {
		TreePath tp = t.getSelectionPath();	
		if (tp == null) return ;
		
		//parent.debug("> "+ tp);
		selectedRow = xmltree.getRowForPath(tp);
		//parent.debug("  selected row: "+ selectedRow);
	
		try {
			xmlnode = (XMLEditorTreeNode)tp.getLastPathComponent();
		} catch (NullPointerException e) { return; }
		
		e = (Element)xmlnode.getValue();
		//parent.debug("Selected element: "+ e.getName());

		showElement(e);
		showAttributes(e);

		enableDetails();
	}

	/**
	 * Show details of the element
	 */
	public void showElement(Element e)  {
		//parent.debug("> show details: "+ e);
		elementNameText.setText(e.getName());
		elementValueText.setText(e.getValue());
	}

	/**
	 * Show all attributes of the element
	 */
	public void showAttributes(Element e) {
		//parent.debug("> show attribs: "+ e);
		List attribs = e.getAttributes();
		DefaultTableModel model = getAttribModel();
		
		for (int i=0; i<attribs.size(); i++) {
			Attribute a = (Attribute)attribs.get(i);
			model.addRow(new Object[]{a.getName(), a.getValue() });
			//parent.debug("  attrib: "+ a.getName() +"="+ a.getValue());
		}

		attribtable.setModel(model);
	}

	/**
	 * Build a JDOM Document from the current file.
	 */
	public void buildXMLDocument() {
		SAXBuilder sb = new SAXBuilder();
		parent.doc = new Document(new Element("root"));
		disableDetails();
	
		// New file
		if (parent.file.getName() == "") {
			updateXMLTree();
			return ;	
		}
		
		//parent.debug("> Building XML Document: "+ parent.file.getAbsolutePath());
		try {			
			parent.doc = sb.build(parent.file);
		} catch (JDOMException e) {
			parent.debug("JDOM Error! "+ e.getMessage());
			//parent.debug("> JDOMException:"+ e.getMessage());
		} catch (IOException e) {
			parent.debug("IO Error! "+ e.getMessage());
			//parent.debug("> IOException:"+ e.getMessage());
		}
		finally {
			updateXMLTree();
		}
	}
	
	/**
	 * Rebuild the Tree from the JDOM Document
	 */
	public void updateXMLTree() {
		//parent.debug("> update tree");
		xmltree.setModel(buildXMLTree());
		xmltree.expandAll();	
		xmltree.setSelectionRow(selectedRow);
		selectXMLTreeElement(xmltree);
	}

	/**
	 * Build a TreeModel that represents the current XML document
	 * The model is used to populate the Tree.
	 */
	private DefaultTreeModel buildXMLTree() {
		XMLEditorTreeNode root = new XMLEditorTreeNode("", "");
		DefaultTreeModel model = new DefaultTreeModel(root);
		
		try {
			Element e = parent.doc.getRootElement();
			root.add(buildChildren(e));
		} catch(NullPointerException e) {}

		return model;
		//new JTree(model);
	}
	
	/**
	 * Construct a tree node containing an element and all its children.
	 */
	private XMLEditorTreeNode buildChildren(Element e) {
		XMLEditorTreeNode me = new XMLEditorTreeNode(e.getName(), e);
		XMLEditorTreeNode child;
		
		List c = e.getChildren();
		for (int i=0; i<c.size(); i++) {
			//System.out.println(c.get(i));			
			Element ec = (Element)c.get(i);
			child = buildChildren(ec);
			me.add(child);
		}

		return me;
	}
	
}


/**
 * Supporting classes
 */


/**
 * Attribute Table shows a table of all element attributes
 * Allows in-place editing
 */
class XMLEditorAttributeTable extends JTable 
{
	XMLEditor xmleditor;

	public XMLEditorAttributeTable(XMLEditor xmleditor) {
		this.xmleditor = xmleditor;	
	}
	
	/**
	 * User finished editing an attribute, save the new value
	 */
	public void editingStopped(ChangeEvent evt) {
		//parent.debug("> editingStopped: "+ e);	

		int row = getEditingRow();
		String oldname = getValueAt(row, 0).toString();
		String oldvalue = getValueAt(row, 1).toString();
		super.editingStopped(evt);
		String name = getValueAt(row, 0).toString();
		String value = getValueAt(row, 1).toString();

		try {
			xmleditor.editAttribute(name, value, oldname, oldvalue);
		} catch(Exception e) {
			setValueAt(oldname, row, 0);
			setValueAt(oldvalue, row, 1);
		}
	}

}	

/**
 * The XML Document is represented to the user by this JTree.
 * The JDOM Elements of the Document should be tied to each node.
 */
class XMLEditorTree extends JTree 
{
	public XMLEditorTree()  {
		super();
		setRootVisible(false);
		setExpandsSelectedPaths(true);
		setShowsRootHandles(true);
		setExpandsSelectedPaths(true);
		setScrollsOnExpand(true);
	}
	
	public void expandAll() { 
    	for (int i=0; i < getRowCount(); i++) {
    		expandRow(i);
    	}
	}

	public void expandUpTo(int row) {
    	for (int i=0; i < row; i++) {
    		expandRow(i);
    	}
	}
	
	public void collapseAll() {
    	for (int i=getRowCount()-1; i >= 0; i--) {
    		collapseRow(i);
    	}
	}	
}

/**
 * This is a single node in the Tree that represents a JDOM Document
 * I keep the Element buried inside of a tree node to make it very easy
 * to operate on the "currently selected" Element of the Tree.  
 */
class XMLEditorTreeNode extends DefaultMutableTreeNode 
{
	public String _name;
	public Object _value;
	public XMLEditorTreeNode(String name, Object value) { _name = name; _value = value; }
	public String getName() { return _name; }
	public void setName(String name) { _name = name; }
	public Object getValue() { return _value; }
	public void setValue(Object value) { _value = value; }
	public String toString() { return _name; }
};
