/* ************************************************************************
#
#  DivConq
#
#  http://divconq.com/
#
#  Copyright:
#    Copyright 2012 eTimeline, LLC. All rights reserved.
#
#  License:
#    See the license.txt file in the project's top-level directory for details.
#
#  Authors:
#    * Andy White
#
************************************************************************ */
package divconq.xml;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import divconq.lang.Memory;
import divconq.struct.CompositeStruct;
import divconq.struct.CompositeParser;

/**
 * Represents a XML element and contains all the information that the
 * original had. Attributes may be set, accessed and deleted. Child elements may
 * be set accessed or deleted. This can be converted back to XML, along with its
 * children, in a formatted or unformatted string.
 */
public class XElement extends XNode {
	protected String tagName = null;
	protected int line = 0;
	protected int col = 0;

	protected Map<String, String> attributes = null;
	protected List<XNode> children = null;
	protected String comment = null;

	/**
	 * constructor specifying the name with an optional array
	 * of objects to be added as child elements
	 * 
	 * @param tag the name of the tag
	 * @param children
	 *            an array of objects to be added as children
	 */
	public XElement(String tag, Object... children) {
		this.tagName = tag;

		for (int i = 0; i < children.length; i++) {
			Object obj = children[i];

			if (obj instanceof XNode)
				this.add((XNode) obj);
			else if (obj instanceof XAttribute)
				this.setAttribute(((XAttribute) obj).getName(), ((XAttribute) obj).getValue());
			else
				this.add(obj.toString());
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#clone()
	 */
	/*
	 * TODO public Object clone() throws CloneNotSupportedException {
	 * TaggedElement newElement = (TaggedElement)super.clone(); if (attributes
	 * != null) { newElement.attributes = new
	 * HashMap<String,String>(attributes); } if (elements != null) {
	 * newElement.elements = new ArrayList<Element>(); Iterator<Element> it =
	 * elements.iterator(); while (it.hasNext()) {
	 * newElement.elements.add((Element)it.next().clone()); } } return
	 * newElement; }
	 */

	/**
	 * gets the element name 
	 * 
	 * @return the element name 
	 */
	public String getName() {
		return this.tagName;
	}

	/**
	 * sets an attribute of this element
	 * 
	 * @param name
	 *            the name of the attribute to be set
	 * @param value
	 *            the value of the attribute to be set
	 */
	public void setAttribute(String name, String value) {
		if (this.attributes == null)
			this.attributes = new HashMap<String, String>();

		this.attributes.put(name, value);
	}

	/**
	 * gets the specified attribute of this element
	 * 
	 * @param name
	 *            the name of the attribute to get
	 * @return the value of the attribute
	 */
	public String getAttribute(String name) {
		return (this.attributes == null ? null : this.attributes.get(name));
	}

	/**
	 * gets the specified attribute of this element but returns given default
	 * value if the attribute does not exist
	 * 
	 * @param name
	 *            the name of the attribute to get
	 * @param defaultValue
	 *            the value to be returned if the attribute doesn't exist
	 * @return the value of the attribute
	 */
	public String getAttribute(String name, String defaultValue) {
		String result = this.getAttribute(name);
		return result == null ? defaultValue : result;
	}

	/**
	 * finds out whether an attribute exists
	 * 
	 * @param name
	 *            the name of the attribute to look for
	 * @return whether the attribute exists in this element
	 */
	public boolean hasAttribute(String name) {
		return this.attributes == null ? false : this.attributes.containsKey(name);
	}

	/**
	 * finds out whether this element has any attributes
	 * 
	 * @return whether this element has any attributes
	 */
	public boolean hasAttributes() {
		return this.attributes != null && !this.attributes.isEmpty();
	}

	/**
	 * gets the attributes in the form of an indexed table
	 * 
	 * @return the attribute table for this element
	 */
	public Map<String, String> getAttributes() {
		if (this.attributes == null)
			this.attributes = new HashMap<String, String>();

		return this.attributes;
	}

	/**
	 * removes the named attribute
	 *  
	 * @param name Name of attribute to remove
	 */
	public void removeAttribute(String name) {
		if (this.attributes != null)
			this.attributes.remove(name);
	}

	/**
	 * gets the number of child elements this element has
	 * 
	 * @return the number of child elements this element has
	 */
	public int children() {
		return this.children == null ? 0 : this.children.size();
	}

	/**
	 * finds out whether this element has any child elements
	 * 
	 * @return whether this element has any child elements
	 */
	public boolean hasChildren() {
		return this.children() != 0;
	}

	/**
	 * removes the children from this element
	 * 
	 */
	public void clearChildren() {
		if (children != null)
			children.clear();
	}

	/**
	 * adds a child to the end of this element
	 * 
	 * @param element
	 *            the child element to be added
	 */
	public void add(XNode element) {
		this.add(-1, element);
	}

	/**
	 * inserts a child into this element. If the index is out of range, the
	 * child is added at the end
	 * 
	 * @param index
	 *            the location to insert the child
	 * @param element
	 *            the child element to be added
	 */
	public void add(int index, XNode element) {
		if (this.children == null)
			this.children = new ArrayList<XNode>();

		if ((index < 0) || (index >= this.children.size()))
			this.children.add(element);
		else
			this.children.add(index, element);
	}

	/**
	 * adds a child to the end of this element
	 * 
	 * @param string
	 *            the child element to be added
	 */
	public void add(String string) {
		this.add(new XText(string));
	}

	/**
	 * inserts a child into this element If the index is out of range, the child
	 * is added at the end
	 * 
	 * @param index
	 *            the location to insert the child
	 * @param string
	 *            the child element to be added
	 */
	public void add(int index, String string) {
		this.add(index, new XText(string));
	}

	/**
	 * gets a child from the specified place in this element
	 * 
	 * @param i
	 *            the index where the child is to be added
	 * @return the specified child
	 */
	public XNode getChild(int i) {
		if ((i < 0) || (i >= this.children()))
			return null;

		return this.children.get(i);
	}

	/**
	 * replaces a child element with the one given
	 * 
	 * @param index
	 *            the child element number to replace
	 * @param newElement
	 *            the new element to replace the old one
	 */
	public void replace(int index, XNode newElement) {
		if (this.children == null)
			this.children = new ArrayList<XNode>();
		
		if (index >= this.children.size())
			this.children.add(newElement);
		else
			this.children.set(index, newElement);
	}

	/**
	 * Removes a child from this element
	 * 
	 * @param index
	 *            the index of the child to be removed
	 * @return whether the child was found and removed or not
	 */
	public XNode remove(int index) {
		XNode result = null;
		
		if (this.children == null)
			this.children = new ArrayList<XNode>();
		
		if (index >= 0 && index < this.children.size())
			result = this.children.remove(index);
		
		return result;
	}

	/**
	 * Removes a child from this element
	 * 
	 * @param element
	 *            the child to be removed
	 * @return whether the child was found and removed or not
	 */
	public boolean remove(XNode element) {
		if (element == null)
			return true;
		
		return this.children.remove(element);
	}

	/**
	 * Finds a named child tagged element. If there is no such child, null is
	 * returned.
	 * 
	 * @param name
	 *            the name of the child of this TaggedElement to find
	 * @return the name of the found element or null if not found
	 */
	public XElement find(String... name) {
		if (this.children != null)
			for (int i = 0; i < this.children.size(); i++) {
				XNode element = this.children.get(i);
				
				if (element instanceof XElement) {
					for (int n = 0; n < name.length; n++)
						if (((XElement) element).getName().equals(name[n]))
							return (XElement) element;
				}
			}
		
		return null;
	}
	
	/**
	 * A way to select child or sub child elements similar to XPath but lightweight.
	 * Cannot select values or attributes, just elements.  * is supported to match
	 * all elements at a given level.
	 * 
	 * For example: "Toys/Toy" called on "<Person>" means select all Toy elements
	 * inside of the Toys child element (child of Person).
	 * 
	 * Cannot go up levels, or back to root.  Do not start with a slash as in "/People".
	 * 
	 * @param path a backslash delimited string
	 * @return list of all matching elements, or empty list if no match
	 */
	public List<XElement> selectAll(String path) {
		List<XElement> matches = new ArrayList<XElement>();
		this.selectAll(path, matches);
		return matches;
	}
	
	/**
	 * Internal, recursive search used by selectAll
	 * 
	 * @param path a backslash delimited string
	 * @param matches list of all matching elements, or empty list if no match
	 */
	protected void selectAll(String path, List<XElement> matches) {
		if (!this.hasChildren())
			return;
		
		int pos = path.indexOf('/');

		// go back to root not supported
		if (pos == 0)
			return;
		
		String name = null;
		
		if (pos == -1) {
			name = path;
			path = null;
		}
		else { 
			name = path.substring(0, pos);
			path = path.substring(pos + 1);
		}
		
		// TODO add filter per XPath - [@n = f]
		
		for (XNode n : this.children) {
			if (n instanceof XElement) {
				if ("*".equals(name) || ((XElement)n).getName().equals(name)) {
					if (pos == -1) 
						matches.add((XElement)n);
					else  
						((XElement)n).selectAll(path, matches);
				}
			}
		}
	}
	
	/**
	 * A way to select child or sub child elements similar to XPath but lightweight.
	 * Cannot select values or attributes, just elements.  * is supported to match
	 * all elements at a given level.  Returns only the first match.
	 * 
	 * For example: "Toys/Toy" called on "<Person>" means return first Toy element
	 * inside of the Toys child element (child of Person).
	 * 
	 * Cannot go up levels, or back to root.  Do not start with a slash as in "/People".
	 * 
	 * @param path a backslash delimited string
	 * @return first matching element, or null if no match
	 */
	public XElement selectFirst(String path) {
		if (!this.hasChildren())
			return null;
		
		int pos = path.indexOf('/');

		// go back to root not supported
		if (pos == 0)
			return null;
		
		String name = null;
		
		if (pos == -1) {
			name = path;
			path = null;
		}
		else { 
			name = path.substring(0, pos);
			path = path.substring(pos + 1);
		}
		
		// TODO add filter per XPath - [@n = f]
		
		for (XNode n : this.children) {
			if (n instanceof XElement) {
				if ("*".equals(name) || ((XElement)n).getName().equals(name)) {
					if (pos == -1) 
						return (XElement)n;
					else  {
						XElement r = ((XElement)n).selectFirst(path);
						
						if (r != null)
							return r;
					}
				}
			}
		}
		
		return null;
	}

	/**
	 * Finds the index of a named child tagged element. If there is no such
	 * child, -1 is returned.
	 * 
	 * @param name
	 *            the name of the child of this TaggedElement to find
	 * @return the index of the found element or -1 if not found
	 */
	public int findIndex(String name) {
		if (this.children != null)
			for (int i = 0; i < this.children.size(); i++) {
				XNode element = this.children.get(i);
				
				if (element instanceof XElement) 
					if (((XElement) element).getName().equals(name))
						return i;
			}
		
		return -1;
	}

	/**
	 * sets the list of children of this element. This method replaces the
	 * current children.
	 * 
	 * @param elements
	 *            the new list of children
	 */
	public void setElements(List<XNode> elements) {
		this.children = elements;
	}

	/**
	 * gets a list of the children of this element. This method always returns a
	 * List even if it is empty.
	 * 
	 * @return the List containing the children of this element
	 */
	public Collection<XNode> getChildren() {
		if (this.children == null)
			this.children = new ArrayList<XNode>();
		
		return this.children;
	}

	/**
	 * sets the XML source code location information for this element
	 * 
	 * @param line
	 *            the line number of the start tag
	 * @param col
	 *            the column number of the start tag
	 */
	public void setLocation(int line, int col) {
		this.line = line;
		this.col = col;
	}

	/**
	 * gets the XML source code line number where this element was declared.
	 * This number will be 0 if it was never set.
	 * 
	 * @return the XML source code line number of this element's declaration
	 */
	public int getLine() {
		return this.line;
	}

	/**
	 * gets the XML source code cloumn number where this element was declared.
	 * This number will be 0 if it was never set.
	 * 
	 * @return the XML source code column number of this element's declaration
	 */
	public int getCol() {
		return this.col;
	}

	/**
	 * get the comment associated with this element
	 * 
	 * @return this element's comment
	 */
	public String getComment() {
		return this.comment;
	}

	/**
	 * set this element's comment
	 * 
	 * @param comment
	 *            the comment to be stored
	 */
	public void setComment(String comment) {
		this.comment = comment;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return this.toString(true);
	}

	/* (non-Javadoc)
	 * @see divconq.xml.XNode#toString(java.lang.StringBuffer, boolean, int)
	 */
	@Override
	protected StringBuffer toString(StringBuffer sb, boolean formatted, int level) {
		// Add leading newline and spaces, if necessary
		if (formatted && level > 0) {
			sb.append("\r\n");
			for (int i = level; i > 0; i--)
				sb.append("  ");
		}

		// Put the opening tag out
		sb.append("<" + this.tagName);

		// Write the attributes out
		if (this.attributes != null) 
			for (Map.Entry<String, String> entry : this.attributes.entrySet()) {
				sb.append(" " + entry.getKey() + "=");
				sb.append("\"" + XNode.quote(entry.getValue()) + "\"");
			}

		// write out the closing tag or other elements
		boolean formatThis = formatted;
		
		if (!this.hasChildren()) {
			sb.append(" />");
		} 
		else {
			sb.append(">");
			
			for (XNode element : this.children) {
				formatThis = (element instanceof XText) ? false : formatted;
				element.toString(sb, formatThis, level + 1);
			}
			
			// Add leading newline and spaces, if necessary
			if (formatThis && (level >= 0)) {
				sb.append("\r\n");
				
				for (int i = level; i > 0; i--)
					sb.append("\t");
			}
			
			// Now put the closing tag out
			sb.append("</" + this.tagName + ">");
		}
		
		return sb;
	}

	/**
	 * returns just the tag start, not the content or children or the tag end.  
	 * useful for debugging
	 * 
	 * @return tag start in xml syntax
	 */
	public String toLocalString() {
		StringBuffer sb = new StringBuffer();

		// Put the opening tag out
		sb.append("<" + this.tagName);

		// Write the attributes out
		if (this.attributes != null) 
			for (Map.Entry<String, String> entry : this.attributes.entrySet()) {
				sb.append(" " + entry.getKey() + "=");
				sb.append("\"" + XNode.quote(entry.getValue()) + "\"");
			}

		sb.append(">");
		
		return sb.toString();
	}
	
	/**
	 * 
	 * @return the text contained in this element if any, else null
	 */
	public String getText() {
		if (!this.hasChildren())
			return null;
		
		// TODO improve to support multiple CDATA sections in one element
		XNode f = this.children.get(0);
		
		if (f instanceof XText)
			return ((XText)f).getValue();
		
		return null;
	}

	/**
	 * assumes that the text content of this element is escaped xml,
	 * reads and parses the text content and returns the root element
	 * from that content
	 * 
	 * @return root xml element for parsed content or null
	 */
	public XElement toXml() {
		if (!this.hasChildren())
			return null;
		
		XNode f = this.children.get(0);
		
		if (f instanceof XText)
			return XmlReader.parse(((XText)f).getValue());
		
		return null;
	}

	/**
	 * assumes the text content of this element is Json, reads and
	 * parses the text content
	 * 
	 * @return struct or null
	 */
	public CompositeStruct toStruct() {
		if (!this.hasChildren())
			return null;
		
		XNode f = this.children.get(0);
		
		if (f instanceof XText)
			return CompositeParser.parseJson(((XText)f).getValue());
		
		return null;
	}

	/**
	 * 
	 * @return the first child node or null
	 */
	public XNode getFirstChild() {
		if (this.hasChildren())
			return this.children.get(0);
		
		return null;
	}
	
	/* (non-Javadoc)
	 * @see divconq.xml.XNode#toMemory(divconq.lang.Memory, boolean, int)
	 */
	@Override
	protected void toMemory(Memory sb, boolean formatted, int level) {
		// Add leading newline and spaces, if necessary
		if (formatted && level > 0) {
			sb.write("\r\n");
			for (int i = level; i > 0; i--)
				sb.write("  ");
		}

		// Put the opening tag out
		sb.write("<" + this.tagName);

		// Write the attributes out
		if (this.attributes != null) 
			for (Map.Entry<String, String> entry : this.attributes.entrySet()) {
				sb.write(" " + entry.getKey() + "=");
				sb.write("\"" + XNode.quote(entry.getValue()) + "\"");
			}

		// write out the closing tag or other elements
		boolean formatThis = formatted;
		
		if (!this.hasChildren()) {
			sb.write(" />");
		} 
		else {
			sb.write(">");
			
			for (XNode element : this.children) {
				formatThis = (element instanceof XText) ? false : formatted;
				element.toMemory(sb, formatThis, level + 1);
			}
			
			// Add leading newline and spaces, if necessary
			if (formatThis && (level >= 0)) {
				sb.write("\r\n");
				
				for (int i = level; i > 0; i--)
					sb.write("\t");
			}
			
			// Now put the closing tag out
			sb.write("</" + this.tagName + ">");
		}
	}
}
