package org.texttechnologylab.project.gruppe_05_1.util;


import org.w3c.dom.*;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;

public abstract class XmlUtils {

    public static Document loadXmlDocumentFromRemoteZip(ZipEntry zipEntry,
                                                        InputStream xmlInputStream,
                                                        InputStream dtdInputStream) {

        Document document = null;

        // Set up a DocumentBuilderFactory and enable DTD validation
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setValidating(true);
        DocumentBuilder builder = null;
        try {
            builder = factory.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            throw new RuntimeException(e);
        }
        builder.setEntityResolver((publicId, systemId) -> {
            if (systemId != null && systemId.endsWith(".DTD")) {
                return new InputSource(dtdInputStream);
            }
            return null;
        });
        try {
            document = builder.parse(xmlInputStream);

            // TODO: Error handling...
        } catch (SAXException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        document.getDocumentElement().normalize();
        return document;
    }


    /**
     * Read an XML document from a given path
     * @param filePath      (example: src/main/resources/myData.xml)
     * @return
     */
    public static Document loadXmlDocument(String filePath) {

        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder;
        try {
            builder = dbf.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            throw new RuntimeException(e);
        }

        Document doc = null;
        try {
            doc = builder.parse(new File(filePath));    // systemID = "MDB_STAMMDATEN.DTD"
        } catch (SAXException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            Logger.error(String.valueOf(e));
            Logger.error("\n");
            Logger.error(e.getLocalizedMessage());
            throw new RuntimeException(e);
        }
        doc.getDocumentElement().normalize();

        return doc;
    }


    /**
     * Read an XML document and return its root element
     * @param filePath
     * @return the root element of document
     */
    public static Element getRootDocument(String filePath) {
        Document doc = loadXmlDocument(filePath);
        return doc.getDocumentElement();
    }


    /**
     * Get a list of children nodes
     * @param origin
     * @return list of children nodes
     */
    public static List<Node> getChildren(Element origin) {

        NodeList nl = origin.getChildNodes();
        for (int i=0 ; i< nl.getLength() ; i++) {
            Node n = nl.item(i);
            Logger.info(String.valueOf(nl.item(i)));
        }

        List<Node> list = new ArrayList<>();
        Node current = origin.getFirstChild();
        while (null != current) {
            list.add(current);
            current = current.getNextSibling();
        }

        return list;
    }

    /**
     * Get a list of children nodes which have the specified name
     * @param origin
     * @param name
     * @return list of children nodes with a specified name
     */
    public static List<Node> getChildrenByName(Node origin, String name) {
        List<Node> list = new ArrayList<>();

        Node current = origin.getFirstChild();
        while (null != current) {
            if (current.getNodeName().equals(name)) {
                list.add(current);
            }
            current = current.getNextSibling();
        }

        return list;
    }

    /**
     * Get first child nodes which has the specified name
     * @param origin
     * @param name
     * @return first child node with a specified name
     */
    public static Node getFirstChildByName(Node origin, String name) {

        Node current = origin.getFirstChild();
        while (null != current) {
            if (current.getNodeName().equals(name)) {
                return current;
            }
            current = current.getNextSibling();
        }

        return null;
    }

    /**
     *
     * @param node
     * @return attributes of the given node
     */
    public static Map<String, String> getNodeAttributes(Node node) {
        NamedNodeMap attributes = node.getAttributes();
        Map<String, String> attributesList = new HashMap<>();
        for (int i=0 ; i< attributes.getLength() ; i++) {
            Node attributeNode = attributes.item(i);
            attributesList.put(attributeNode.getNodeName(), attributeNode.getNodeValue());
        }
        return attributesList;
    }

    /**
     *
     * @param node
     * @param attributeName
     * @return
     */
    public static String getNodeAttributeByName(Node node, String attributeName) {
        NamedNodeMap attributes = node.getAttributes();
        for (int i=0 ; i< attributes.getLength() ; i++) {
            Node attributeNode = attributes.item(i);
            if (attributeNode.getNodeName().equals(attributeName)) return attributeNode.getNodeValue();
        }
        return null;
    }

    /**
     * Return the content of a Node as raw text
     * @param node
     * @return String
     */
    public static String getRawText(Node node) {
        return getRawText(node, 0);
    }

    private static String getRawText(Node node, int depth) {
        StringBuilder sb = new StringBuilder();

        String padding = "  ".repeat(2 * depth);
        switch (node.getNodeType()) {
            case Node.TEXT_NODE:    // 3
                if ( ! node.getNodeValue().trim().isBlank()) {
                    sb.append(node.getNodeValue());
                }
                break;

            case Node.ELEMENT_NODE: // 1
                // Print tag
                sb.append('\n').append(padding).append('<').append(node.getNodeName());

                // Print attributes, if any
                Map<String, String> attributes = getNodeAttributes(node);
                if (attributes.isEmpty()) {
                    sb.append('>');
                } else {
                    for (Map.Entry<String, String> entry : attributes.entrySet()) {
                        sb.append(' ').append(entry.getKey()).append("=\"").append(entry.getValue()).append('"');
                    }
                    sb.append('>');
                }

                // Visit all Children
                NodeList children = node.getChildNodes();
                for (int i=0 ; i < children.getLength() ; i++) {
                    Node child = children.item(i);
                    sb.append(getRawText(child, depth+1));
                }

                // Print closing tag
                sb.append('\n').append(padding).append("</").append(node.getNodeName()).append('>');
                break;

            default:
                Logger.warn("Unknown type in getRawText(), depth " + depth
                        +"Type = " + node.getNodeType()
                        + " / Name = '" + node.getNodeName()
                        + "' / Value = '" + node.getNodeValue()
                        + "' / Context = '" + node.getTextContent()
                        + "'");
        }

        return sb.toString();
    }

}
