/*
 * utils - SimpleXML.java - Copyright © 2006-2009 David Roden
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package net.pterodactylus.util.xml;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import net.pterodactylus.util.logging.Logging;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;

/**
 * SimpleXML is a helper class to construct XML trees in a fast and simple way.
 * Construct a new XML tree by calling {@link #SimpleXML(String)} and append new
 * nodes by calling {@link #append(String)}.
 *
 * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
 */
public class SimpleXML {

	/** Logger. */
	private static final Logger logger = Logging.getLogger(SimpleXML.class.getName());

	/**
	 * A {@link List} containing all child nodes of this node.
	 */
	private List<SimpleXML> children = new ArrayList<SimpleXML>();

	/**
	 * The name of this node.
	 */
	private String name = null;

	/**
	 * The value of this node.
	 */
	private String value = null;

	/** Attributes of the element. */
	private Map<String, String> attributes = null;

	/**
	 * Constructs a new XML node without a name.
	 */
	public SimpleXML() {
		super();
	}

	/**
	 * Constructs a new XML node with the specified name.
	 *
	 * @param name
	 *            The name of the new node
	 */
	public SimpleXML(String name) {
		this(name, (String[]) null, (String[]) null);
	}

	/**
	 * Constructs a new XML node with the specified name and a single attribute.
	 *
	 * @param name
	 *            The name of the node
	 * @param attributeName
	 *            The name of the attribute
	 * @param attributeValue
	 *            The value of the attribute
	 */
	public SimpleXML(String name, String attributeName, String attributeValue) {
		this(name, new String[] { attributeName }, new String[] { attributeValue });
	}

	/**
	 * Constructs a new XML node with the specified name and attributes.
	 *
	 * @param name
	 *            The name of the node
	 * @param attributeNames
	 *            The names of the attribute
	 * @param attributeValues
	 *            The values of the attribute
	 */
	public SimpleXML(String name, String[] attributeNames, String[] attributeValues) {
		this.name = name;
		attributes = new HashMap<String, String>();
		if ((attributeNames != null) && (attributeValues != null) && (attributeNames.length == attributeValues.length)) {
			for (int index = 0, size = attributeNames.length; index < size; index++) {
				attributes.put(attributeNames[index], attributeValues[index]);
			}
		}
	}

	/**
	 * Returns all attributes’ names. The array is not sorted.
	 *
	 * @return The names of all attributes
	 */
	public String[] getAttributeNames() {
		return attributes.keySet().toArray(new String[attributes.size()]);
	}

	/**
	 * Returns the value of the attribute with the given name.
	 *
	 * @param attributeName
	 *            The name of the attribute to look up
	 * @return The value of the attribute
	 */
	public String getAttribute(String attributeName) {
		return getAttribute(attributeName, null);
	}

	/**
	 * Returns the value of the attribute with the given name.
	 *
	 * @param attributeName
	 *            The name of the attribute to look up
	 * @param defaultValue
	 *            The value to return if there is no attribute with the given
	 *            name
	 * @return The value of the attribute
	 */
	public String getAttribute(String attributeName, String defaultValue) {
		if (!attributes.containsKey(attributeName)) {
			return defaultValue;
		}
		return attributes.get(attributeName);
	}

	/**
	 * Sets the value of an attribute.
	 *
	 * @param attributeName
	 *            The name of the attribute to set
	 * @param attributeValue
	 *            The value of the attribute
	 */
	public void setAttribute(String attributeName, String attributeValue) {
		attributes.put(attributeName, attributeValue);
	}

	/**
	 * Removes the attribute with the given name, returning its previous value.
	 *
	 * @param attributeName
	 *            The name of the attribute to remove
	 * @return The value of the attribute before removing it
	 */
	public String removeAttribute(String attributeName) {
		return attributes.remove(attributeName);
	}

	/**
	 * Checks whether this node contains the attribute with the given name.
	 *
	 * @param attributeName
	 *            The name of the attribute
	 * @return <code>true</code> if this node has an attribute with the given
	 *         name, <code>false</code> otherwise
	 */
	public boolean hasAttribute(String attributeName) {
		return attributes.containsKey(attributeName);
	}

	/**
	 * Returns whether this node has any child nodes.
	 *
	 * @return {@code true} if this node has child nodes, {@code false}
	 *         otherwise
	 */
	public boolean hasNodes() {
		return !children.isEmpty();
	}

	/**
	 * Checks if this object has a child with the specified name.
	 *
	 * @param nodeName
	 *            The name of the child node to check for
	 * @return <code>true</code> if this node has at least one child with the
	 *         specified name, <code>false</code> otherwise
	 */
	public boolean hasNode(String nodeName) {
		return getNode(nodeName) != null;
	}

	/**
	 * Returns the child node of this node with the specified name. If there are
	 * several child nodes with the specified name only the first node is
	 * returned.
	 *
	 * @param nodeName
	 *            The name of the child node
	 * @return The child node, or <code>null</code> if there is no child node
	 *         with the specified name
	 */
	public SimpleXML getNode(String nodeName) {
		for (int index = 0, count = children.size(); index < count; index++) {
			if (children.get(index).name.equals(nodeName)) {
				return children.get(index);
			}
		}
		return null;
	}

	/**
	 * Returns the child node that is specified by the names. The first element
	 * of <code>nodeNames</code> is the name of the child node of this node, the
	 * second element of <code>nodeNames</code> is the name of a child node's
	 * child node, and so on. By using this method you can descend into an XML
	 * tree pretty fast.
	 *
	 * <pre>
	 * <code>
	 * SimpleXML deepNode = topNode.getNodes(new String[] { "person", "address", "number" });
	 * </code>
	 * </pre>
	 *
	 * @param nodeNames
	 *            The names of the nodes
	 * @return A node that is a deep child of this node, or <code>null</code> if
	 *         the specified node does not eixst
	 */
	public SimpleXML getNode(String[] nodeNames) {
		SimpleXML node = this;
		for (String nodeName : nodeNames) {
			node = node.getNode(nodeName);
		}
		return node;
	}

	/**
	 * Returns all child nodes of this node.
	 *
	 * @return All child nodes of this node
	 */
	public SimpleXML[] getNodes() {
		return getNodes(null);
	}

	/**
	 * Returns all child nodes of this node with the specified name. If there
	 * are no child nodes with the specified name an empty array is returned.
	 *
	 * @param nodeName
	 *            The name of the nodes to retrieve, or <code>null</code> to
	 *            retrieve all nodes
	 * @return All child nodes with the specified name
	 */
	public SimpleXML[] getNodes(String nodeName) {
		List<SimpleXML> resultList = new ArrayList<SimpleXML>();
		for (SimpleXML child : children) {
			if ((nodeName == null) || child.name.equals(nodeName)) {
				resultList.add(child);
			}
		}
		return resultList.toArray(new SimpleXML[resultList.size()]);
	}

	/**
	 * Appends a new XML node with the specified name and returns the new node.
	 * With this method you can create deep structures very fast.
	 *
	 * <pre>
	 * <code>
	 * SimpleXML mouseNode = topNode.append("computer").append("bus").append("usb").append("mouse");
	 * </code>
	 * </pre>
	 *
	 * @param nodeName
	 *            The name of the node to append as a child to this node
	 * @return The new node
	 */
	public SimpleXML append(String nodeName) {
		return append(new SimpleXML(nodeName));
	}

	/**
	 * Appends a new XML node with the specified name and value and returns the
	 * new node.
	 *
	 * @param nodeName
	 *            The name of the node to append
	 * @param nodeValue
	 *            The value of the node to append
	 * @return The newly appended node
	 */
	public SimpleXML append(String nodeName, String nodeValue) {
		return append(nodeName).setValue(nodeValue);
	}

	/**
	 * Appends the node with all its child nodes to this node and returns the
	 * child node.
	 *
	 * @param newChild
	 *            The node to append as a child
	 * @return The child node that was appended
	 */
	public SimpleXML append(SimpleXML newChild) {
		children.add(newChild);
		return newChild;
	}

	/**
	 * Removes the specified child from this node.
	 *
	 * @param child
	 *            The child to remove
	 */
	public void remove(SimpleXML child) {
		children.remove(child);
	}

	/**
	 * Removes the child with the specified name from this node. If more than
	 * one children have the same name only the first is removed.
	 *
	 * @param childName
	 *            The name of the child node to remove
	 */
	public void remove(String childName) {
		SimpleXML child = getNode(childName);
		if (child != null) {
			remove(child);
		}
	}

	/**
	 * Replace the child node with the specified name by a new node with the
	 * specified content.
	 *
	 * @param childName
	 *            The name of the child to replace
	 * @param value
	 *            The node child's value
	 */
	public void replace(String childName, String value) {
		remove(childName);
		append(childName, value);
	}

	/**
	 * Replaces the child node that has the same name as the given node by the
	 * given node.
	 *
	 * @param childNode
	 *            The node to replace the previous child node with the same name
	 */
	public void replace(SimpleXML childNode) {
		remove(childNode.getName());
		append(childNode);
	}

	/**
	 * Removes all children from this node.
	 */
	public void removeAll() {
		children.clear();
	}

	/**
	 * Sets the value of this node.
	 *
	 * @param nodeValue
	 *            The new value of this node
	 * @return This node
	 */
	public SimpleXML setValue(String nodeValue) {
		value = nodeValue;
		return this;
	}

	/**
	 * Returns the name of this node.
	 *
	 * @return The name of this node
	 */
	public String getName() {
		return name;
	}

	/**
	 * Returns the value of this node.
	 *
	 * @return The value of this node
	 */
	public String getValue() {
		return value;
	}

	/**
	 * Returns the value of the first child node with the specified name.
	 *
	 * @param childName
	 *            The name of the child node
	 * @return The value of the child node
	 * @throws NullPointerException
	 *             if the child node does not exist
	 */
	public String getValue(String childName) {
		return getNode(childName).getValue();
	}

	/**
	 * Returns the value of the first child node with the specified name, or the
	 * default value if there is no child node with the given name.
	 *
	 * @param childName
	 *            The name of the child node
	 * @param defaultValue
	 *            The default value to return if there is no child node with the
	 *            given name
	 * @return The value of the child node
	 * @throws NullPointerException
	 *             if the child node does not exist
	 */
	public String getValue(String childName, String defaultValue) {
		SimpleXML childNode = getNode(childName);
		if (childNode == null) {
			return defaultValue;
		}
		return childNode.getValue();
	}

	/**
	 * Creates a {@link Document} from this node and all its child nodes.
	 *
	 * @return The {@link Document} created from this node
	 */
	public Document getDocument() {
		DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
		try {
			DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
			Document document = documentBuilder.newDocument();
			Element rootElement = document.createElement(name);
			for (Entry<String, String> attributeEntry : attributes.entrySet()) {
				rootElement.setAttribute(attributeEntry.getKey(), attributeEntry.getValue());
			}
			document.appendChild(rootElement);
			addChildren(rootElement);
			return document;
		} catch (ParserConfigurationException e) {
			/* ignore. */
		}
		return null;
	}

	/**
	 * Appends all children of this node to the specified {@link Element}. If a
	 * node has a value that is not <code>null</code> the value is appended as a
	 * text node.
	 *
	 * @param rootElement
	 *            The element to attach this node's children to
	 */
	private void addChildren(Element rootElement) {
		for (SimpleXML child : children) {
			Element childElement = rootElement.getOwnerDocument().createElement(child.name);
			for (Entry<String, String> attributeEntry : child.attributes.entrySet()) {
				childElement.setAttribute(attributeEntry.getKey(), attributeEntry.getValue());
			}
			rootElement.appendChild(childElement);
			if (child.value != null) {
				Text childText = rootElement.getOwnerDocument().createTextNode(child.value);
				childElement.appendChild(childText);
			} else {
				child.addChildren(childElement);
			}
		}
	}

	/**
	 * Creates a SimpleXML node from the specified {@link Document}. The
	 * SimpleXML node of the document's top-level node is returned.
	 *
	 * @param document
	 *            The {@link Document} to create a SimpleXML node from
	 * @return The SimpleXML node created from the document's top-level node
	 */
	public static SimpleXML fromDocument(Document document) {
		SimpleXML xmlDocument = new SimpleXML(document.getFirstChild().getNodeName());
		NamedNodeMap attributes = document.getFirstChild().getAttributes();
		for (int attributeIndex = 0, attributeCount = attributes.getLength(); attributeIndex < attributeCount; attributeIndex++) {
			Node attribute = attributes.item(attributeIndex);
			logger.log(Level.FINER, "adding attribute: " + attribute.getNodeName() + " = " + attribute.getNodeValue());
			xmlDocument.setAttribute(attribute.getNodeName(), attribute.getNodeValue());
		}
		document.normalizeDocument();
		/* look for first non-comment node */
		Node firstChild = null;
		NodeList children = document.getChildNodes();
		for (int index = 0, count = children.getLength(); index < count; index++) {
			Node child = children.item(index);
			if ((child.getNodeType() != Node.COMMENT_NODE) && (child.getNodeType() != Node.PROCESSING_INSTRUCTION_NODE)) {
				firstChild = child;
				break;
			}
		}
		return addDocumentChildren(xmlDocument, firstChild);
	}

	/**
	 * Appends the child nodes of the specified {@link Document} to this node.
	 * Text nodes are converted into a node's value.
	 *
	 * @param xmlDocument
	 *            The SimpleXML node to append the child nodes to
	 * @param document
	 *            The document whose child nodes to append
	 * @return The SimpleXML node the child nodes were appended to
	 */
	private static SimpleXML addDocumentChildren(SimpleXML xmlDocument, Node document) {
		NodeList childNodes = document.getChildNodes();
		for (int childIndex = 0, childCount = childNodes.getLength(); childIndex < childCount; childIndex++) {
			Node childNode = childNodes.item(childIndex);
			if ((childNode.getChildNodes().getLength() == 1) && (childNode.getFirstChild().getNodeName().equals("#text"))) {
				SimpleXML newXML = xmlDocument.append(childNode.getNodeName(), childNode.getFirstChild().getNodeValue());
				NamedNodeMap childNodeAttributes = childNode.getAttributes();
				for (int attributeIndex = 0, attributeCount = childNodeAttributes.getLength(); attributeIndex < attributeCount; attributeIndex++) {
					Node attribute = childNodeAttributes.item(attributeIndex);
					logger.log(Level.FINER, "adding attribute: " + attribute.getNodeName() + " = " + attribute.getNodeValue());
					newXML.setAttribute(attribute.getNodeName(), attribute.getNodeValue());
				}
			} else {
				if ((childNode.getNodeType() == Node.ELEMENT_NODE) || (childNode.getChildNodes().getLength() != 0)) {
					SimpleXML newXML = xmlDocument.append(childNode.getNodeName());
					NamedNodeMap childNodeAttributes = childNode.getAttributes();
					for (int attributeIndex = 0, attributeCount = childNodeAttributes.getLength(); attributeIndex < attributeCount; attributeIndex++) {
						Node attribute = childNodeAttributes.item(attributeIndex);
						logger.log(Level.FINER, "adding attribute: " + attribute.getNodeName() + " = " + attribute.getNodeValue());
						newXML.setAttribute(attribute.getNodeName(), attribute.getNodeValue());
					}
					addDocumentChildren(newXML, childNode);
				}
			}
		}
		return xmlDocument;
	}
}