package cz.moneta.test.harness.support.xml;

import org.apache.commons.lang3.tuple.Triple;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.IntStream;

public final class XmlUtils {

    private XmlUtils() {
    }

    public static String resolveTemplate(String path, Map<String, String> params, Triple<String, String, Map<String, String>>... nodesToInsert) {

        try (InputStream documentStream = XmlUtils.class.getClassLoader().getResourceAsStream(path)) {

            DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
            Document doc = dBuilder.parse(documentStream);
            doc.getDocumentElement().normalize();

            doReplacements(doc, params);

            Arrays.stream(nodesToInsert)
                    .forEach(t -> getParentNode(doc, t.getLeft())
                            .appendChild(getChildNode(doc, t.getMiddle(), t.getRight())));

            DOMSource domSource = new DOMSource(doc);
            StringWriter writer = new StringWriter();
            StreamResult result = new StreamResult(writer);
            TransformerFactory tf = TransformerFactory.newInstance();
            tf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
            Transformer transformer = tf.newTransformer();
            transformer.setOutputProperty(OutputKeys.INDENT, "no");
            transformer.transform(domSource, result);

            return writer.toString();
        } catch (IOException
                | ParserConfigurationException
                | SAXException
                | TransformerException e) {
            throw new AssertionError("failed to resolve definition for " + path, e);
        }
    }

    private static Node getChildNode(Document parentDoc, String nodeFile, Map<String, String> params) {

        try (InputStream documentStream = XmlUtils.class.getClassLoader().getResourceAsStream(nodeFile)) {

            DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
            Document doc = dBuilder.parse(documentStream);
            doc.getDocumentElement().normalize();

            doReplacements(doc, params);

            return parentDoc.importNode(doc.getDocumentElement(), true);
        } catch (IOException
                | ParserConfigurationException
                | SAXException e) {
            throw new AssertionError("failed to resolve child node definition for " + nodeFile, e);
        }
    }

    private static void doReplacements(Document doc, Map<String, String> params) {
        NodeList tags = doc.getElementsByTagName("*");

        IntStream.range(0, tags.getLength())
                .mapToObj(tags::item)
                .filter(t -> t.getTextContent() != null && params.containsKey(t.getTextContent()))
                .forEach(t -> t.setTextContent(params.get(t.getTextContent())));
    }

    private static Node getParentNode(Document doc, String parentTagName) {
        NodeList tags = doc.getElementsByTagName(parentTagName);
        if (tags.getLength() != 1) {
            throw new IllegalArgumentException("Did not find exactly one matching parent element");
        }
        return tags.item(0);
    }

    public static boolean documentsEqual(String document1, String document2) {
        try {
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            dbf.setNamespaceAware(true);
            dbf.setCoalescing(true);
            dbf.setIgnoringElementContentWhitespace(true);
            dbf.setIgnoringComments(true);
            DocumentBuilder db = dbf.newDocumentBuilder();

            Document doc1 = db.parse(new ByteArrayInputStream(document1.getBytes(StandardCharsets.UTF_8)));
            doc1.normalizeDocument();
            Document doc2 = db.parse(new ByteArrayInputStream(document2.getBytes(StandardCharsets.UTF_8)));
            doc2.normalizeDocument();

            return doc1.isEqualNode(doc2);
        } catch (IOException
                | ParserConfigurationException
                | SAXException e) {
            throw new AssertionError("failed to compare xml documents", e);
        }
    }
}
