package eu.dnetlib.miscutils.functional.xml;

import java.io.*;
import java.nio.charset.Charset;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.lang.CharSetUtils;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import com.sun.org.apache.xml.internal.serialize.OutputFormat;
import com.sun.org.apache.xml.internal.serialize.XMLSerializer;

import eu.dnetlib.miscutils.functional.UnaryFunction;

/**
 * The Class IndentXmlString.
 */
public class IndentXmlString implements UnaryFunction<String, String> {

	/*
	 * (non-Javadoc)
	 * 
	 * @see eu.dnetlib.miscutils.functional.UnaryFunction#evaluate(java.lang.Object)
	 */
	@Override
	public String evaluate(final String unformattedXml) {
		try {
			return doIndent(unformattedXml);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Do indent.
	 * 
	 * @param unformattedXml
	 *            the unformatted xml
	 * @return the string
	 * @throws IOException
	 *             Signals that an I/O exception has occurred.
	 */
	protected String doIndent(final String unformattedXml) throws IOException {
		final Document document = parseXmlString(unformattedXml);

		OutputFormat format = new OutputFormat(document);
		format.setIndenting(true);
		format.setIndent(2);
		Writer out = new StringWriter();
		XMLSerializer serializer = new XMLSerializer(out, format);
		serializer.serialize(document);

		return out.toString();
	}

	/**
	 * Parses the xml string.
	 * 
	 * @param in
	 *            the in
	 * @return the document
	 */
	protected Document parseXmlString(final String in) {
		try {
			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
			DocumentBuilder db = dbf.newDocumentBuilder();
			db.setErrorHandler(null);
			InputSource is = new InputSource(new StringReader(in));
			return db.parse(is);
		} catch (ParserConfigurationException e) {
			throw new RuntimeException(e);
		} catch (SAXException e) {
			throw new RuntimeException(e);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	private String doIndent(final Document document) throws TransformerException, UnsupportedEncodingException {

		final Transformer transformer = getTransformer();
		final ByteArrayOutputStream out = new ByteArrayOutputStream();
		transformer.transform(new DOMSource(document), new StreamResult(out));

		return out.toString("utf-8");
	}

	private Transformer getTransformer() throws TransformerConfigurationException {
		TransformerFactory tf = TransformerFactory.newInstance();
		Transformer transformer = tf.newTransformer();
		transformer.setOutputProperty(OutputKeys.INDENT, "yes");
		transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "yes");
		transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
		return transformer;
	}

	private Document doIndentDocument(final Document document) throws TransformerException, UnsupportedEncodingException {

		final Transformer transformer = getTransformer();

		final DOMResult out = new DOMResult();
		transformer.transform(new DOMSource(document), out);

		return (Document) out.getNode();
	}

	/**
	 * Static helper Apply.
	 * 
	 * @param xml
	 *            the xml
	 * @return the indented xml string
	 */
	public static String apply(final String xml) {
		return new IndentXmlString().evaluate(xml);
	}

	/**
	 * Static helper Apply.
	 *
	 * @param xml
	 *            the xml
	 * @return the indented xml string
	 */
	public static Document apply(final Document xml) throws TransformerException, UnsupportedEncodingException {
		return new IndentXmlString().doIndentDocument(xml);
	}

}
