/*******************************************************************************
 * Copyright (c) 2010 Robert "Unlogic" Olofsson (unlogic@unlogic.se).
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser Public License v3
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/lgpl-3.0-standalone.html
 ******************************************************************************/
package se.unlogic.standardutils.xml;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URI;
import java.util.Collection;
import java.util.List;
import java.util.Map.Entry;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

public class XMLUtils {

	private static DocumentBuilder documentBuilder;
	private static DocumentBuilder namespaceAwareDocumentBuilder;

	static {
		try {
			DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();

			documentBuilder = documentBuilderFactory.newDocumentBuilder();

			documentBuilderFactory.setNamespaceAware(true);

			namespaceAwareDocumentBuilder = documentBuilderFactory.newDocumentBuilder();
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		}
	}

	public static Document createDomDocument() {

		return documentBuilder.newDocument();
	}

	public static Document createNamespaceAwareDomDocument() {

		return namespaceAwareDocumentBuilder.newDocument();
	}

	public static String toString(Document doc, String encoding, boolean indent) throws TransformerFactoryConfigurationError, TransformerException {
		Source source = new DOMSource(doc);
		StringWriter sw = new StringWriter();
		Result result = new StreamResult(sw);

		Transformer xformer = TransformerFactory.newInstance().newTransformer();
		xformer.setOutputProperty(OutputKeys.ENCODING, encoding);

		if (indent) {
			xformer.setOutputProperty(OutputKeys.INDENT, "yes");
		}

		xformer.transform(source, result);

		return sw.getBuffer().toString();
	}

	public static void toString(Document doc, String encoding, Writer w, boolean indent) throws TransformerFactoryConfigurationError, TransformerException {
		Source source = new DOMSource(doc);
		Result result = new StreamResult(w);

		Transformer xformer = TransformerFactory.newInstance().newTransformer();
		xformer.setOutputProperty(OutputKeys.ENCODING, encoding);

		if (indent) {
			xformer.setOutputProperty(OutputKeys.INDENT, "yes");
		}

		xformer.transform(source, result);
	}

	public static Document parseXmlFile(String filename, boolean validating, boolean namespaceAware) throws SAXException, IOException, ParserConfigurationException {

		return parseXmlFile(new File(filename), validating, namespaceAware);
	}

	public static Document parseXmlFile(File file, boolean validating, boolean namespaceAware) throws SAXException, IOException, ParserConfigurationException {

		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

		factory.setNamespaceAware(namespaceAware);
		factory.setValidating(validating);

		Document doc = factory.newDocumentBuilder().parse(file);

		return doc;
	}

	public static Document parseXmlFile(URI uri, boolean validating) throws SAXException, IOException, ParserConfigurationException {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		factory.setValidating(validating);
		Document doc = factory.newDocumentBuilder().parse(uri.toString());
		return doc;
	}

	public static Document parseXmlFile(File f, boolean validating) throws SAXException, IOException, ParserConfigurationException {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		factory.setValidating(validating);
		Document doc = factory.newDocumentBuilder().parse(f);
		return doc;
	}

	public static Document parseXmlFile(InputStream stream, boolean validating) throws SAXException, IOException, ParserConfigurationException {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		factory.setValidating(validating);
		Document doc = factory.newDocumentBuilder().parse(stream);
		return doc;
	}

	public static Element createElement(String name, Object value, Document doc) {
		Element element = doc.createElement(name);
		element.appendChild(doc.createTextNode(value.toString()));
		return element;
	}

	public static Element createCDATAElement(String name, Object value, Document doc) {
		Element element = doc.createElement(name);
		element.appendChild(doc.createCDATASection(value.toString()));
		return element;
	}

	public static void writeXmlFile(Document doc, File file, boolean indent, String encoding) throws TransformerFactoryConfigurationError, TransformerException {
		// Prepare the DOM document for writing
		Source source = new DOMSource(doc);

		// Prepare the output file
		Result result = new StreamResult(file);

		Transformer xformer = TransformerFactory.newInstance().newTransformer();

		xformer.setOutputProperty(OutputKeys.ENCODING, encoding);
		
		if (indent) {
			xformer.setOutputProperty(OutputKeys.INDENT, "yes");
		}

		xformer.transform(source, result);

	}

	public static void writeXmlFile(Document doc, File file, Entry<String,String>... outputKeys) throws TransformerFactoryConfigurationError, TransformerException {
		// Prepare the DOM document for writing
		Source source = new DOMSource(doc);

		// Prepare the output file
		Result result = new StreamResult(file);

		// Write the DOM document to the file
		Transformer xformer = TransformerFactory.newInstance().newTransformer();

		if(outputKeys != null){

			for(Entry<String,String> entry : outputKeys){

				xformer.setOutputProperty(entry.getKey(), entry.getValue());
			}
		}

		xformer.transform(source, result);

	}

	public static void writeXmlFile(Document doc, String filename, boolean indent, String encoding) throws TransformerFactoryConfigurationError, TransformerException {

		// Prepare the output file
		File file = new File(filename);


		writeXmlFile(doc, file, indent, encoding);

	}

	public static void append(Document doc, Element targetElement, Collection<? extends XMLable> beans) {

		if (beans != null && !beans.isEmpty()) {

			for (XMLable xmlable : beans) {
				targetElement.appendChild(xmlable.toXML(doc));
			}
		}
	}

	public static void append(Document doc, Element targetElement, String elementName, String subElementsName, Object[] values) {

		if(values != null){
			
			Element subElement = doc.createElement(elementName);
			targetElement.appendChild(subElement);

			for (Object value : values) {
				
				appendNewCDATAElement(doc, subElement, subElementsName, value);
			}
		}
	}
	
	public static void append(Document doc, Element targetElement, String elementName, String subElementsName, List<? extends Object> values) {

		if(values != null){
			
			Element subElement = doc.createElement(elementName);
			targetElement.appendChild(subElement);

			for (Object value : values) {
				
				appendNewCDATAElement(doc, subElement, subElementsName, value);
			}
		}
	}	
	
	public static void append(Document doc, Element targetElement, String elementName, Collection<? extends XMLable> beans) {

		if (beans != null && !beans.isEmpty()) {

			Element subElement = doc.createElement(elementName);
			targetElement.appendChild(subElement);

			for (XMLable xmlable : beans) {
				subElement.appendChild(xmlable.toXML(doc));
			}
		}
	}

	public static void appendNewCDATAElement(Document doc, Element targetElement, String elementName, String value) {

		if(value != null){
			targetElement.appendChild(createCDATAElement(elementName, value, doc));
		}
	}

	public static void appendNewElement(Document doc, Element targetElement, String elementName, String value) {

		if(value != null){
			targetElement.appendChild(createElement(elementName, value, doc));
		}
	}

	public static void appendNewCDATAElement(Document doc, Element targetElement, String elementName, Object value) {

		if(value != null){
			appendNewCDATAElement(doc,targetElement,elementName,value.toString());
		}
	}

	public static void appendNewElement(Document doc, Element targetElement, String elementName, Object value) {

		if(value != null){
			appendNewElement(doc,targetElement,elementName,value.toString());
		}

	}

	/**
	 * Adds or replaces node in parent.
	 * @param parent
	 * @param node
	 * @throws Exception - Node cannot exist more than once,
	 * i.e. multiple nodes with the same name cannot exist in parent.
	 */
	public static void replaceSingleNode(Element parent, final Node node) throws RuntimeException {
		
		NodeList nodes = parent.getElementsByTagName(node.getNodeName());

		if(nodes.getLength() > 1) {
			throw new RuntimeException("Parent element contains multiple nodes with the name " + node.getNodeName());
		}
		if(nodes.getLength() == 0) {
			parent.appendChild(node);
		} else {
			parent.replaceChild(node, nodes.item(0));
		}
	}
	
	public enum TimeUnit {
		HOUR,
		MINUTE,
		SECOND;
	}
	
	public static Element getTimeUnits(Document doc, TimeUnit timeUnit) {
		switch(timeUnit) {
		case HOUR:
			Element hoursElement = doc.createElement("hours");
			Element hourElement;
			for(int i=0;i<10;++i) {
				hourElement = doc.createElement("hour");
				XMLUtils.appendNewElement(doc, hourElement, "value", "0"+i);
				hoursElement.appendChild(hourElement);
			}
			for(int i=10;i<24;++i) {
				hourElement = doc.createElement("hour");
				XMLUtils.appendNewElement(doc, hourElement, "value", i);
				hoursElement.appendChild(hourElement);
			}
			return hoursElement;
		case MINUTE:
			Element minutesElement = doc.createElement("minutes");
			Element minuteElement;
			for(int i=0;i<10;++i) {
				minuteElement = doc.createElement("minute");
				XMLUtils.appendNewElement(doc, minuteElement, "value", "0"+i);
				minutesElement.appendChild(minuteElement);
			}
			for(int i=10;i<60;++i) {
				minuteElement = doc.createElement("minute");
				XMLUtils.appendNewElement(doc, minuteElement, "value", i);
				minutesElement.appendChild(minuteElement);
			}
			return minutesElement;
		case SECOND:
			Element secondsElement = doc.createElement("seconds");
			Element secondElement;
			for(int i=0;i<10;++i) {
				secondElement = doc.createElement("second");
				XMLUtils.appendNewElement(doc, secondElement, "value", "0"+i);
				secondsElement.appendChild(secondElement);
			}
			for(int i=10;i<60;++i) {
				secondElement = doc.createElement("second");
				XMLUtils.appendNewElement(doc, secondElement, "value", i);
				secondsElement.appendChild(secondElement);
			}
			return secondsElement;
		}
		return null;
	}

}
