/* UtilLib - Copyright (c) 2005-2022 Vincent Calame - Exemole
 * Logiciel libre donné sous triple licence :
 * 1) selon les termes de la CeCILL V2
 * 2) selon les termes de l’EUPL V.1.1
 * 3) selon les termes de la GNU GPLv3
 * Voir le fichier licences.txt
 */


package net.mapeadores.util.xml;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
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 net.mapeadores.util.exceptions.ExceptionsUtils;
import net.mapeadores.util.localisation.Lang;
import net.mapeadores.util.text.CleanedString;
import net.mapeadores.util.text.Label;
import net.mapeadores.util.text.Labels;
import net.mapeadores.util.text.LangFilter;
import net.mapeadores.util.text.StringUtils;
import net.mapeadores.util.xml.ns.NameSpace;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentFragment;
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;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;


/**
 * Contients des méthodes statiques
 */
public class XMLUtils {

    public final static DocumentFragmentHolder EMPTY_DOCUMENTFRAGMENTHOLDER = new EmptyDocumentFragmentHolder();
    private final static short BALISE_AUTOFERMANTE = 1;
    private final static short BALISE_FERMANTE = 2;
    private final static short BALISE_OUVRANTE = 3;

    public static void addElement(AppendableXMLWriter xmlWriter, Element element, boolean elementInclude) throws IOException {
        String tagName = element.getTagName();
        if (elementInclude) {
            xmlWriter.startOpenTag(tagName);
            if (element.hasAttributes()) {
                NamedNodeMap map = element.getAttributes();
                int count = map.getLength();
                for (int i = 0; i < count; i++) {
                    Attr attribute = (Attr) map.item(i);
                    xmlWriter.addAttribute(attribute.getName(), attribute.getValue());
                }
            }
        }
        NodeList children = element.getChildNodes();
        if (elementInclude) {
            if (children.getLength() == 0) {
                xmlWriter.closeEmptyTag();
                return;
            } else {
                xmlWriter.endOpenTag();
            }
        }
        int count = children.getLength();
        for (int i = 0; i < count; i++) {
            Node node = children.item(i);
            if (node instanceof Text) {
                xmlWriter.append(((Text) node).getData());
            } else if (node instanceof Element) {
                addElement(xmlWriter, (Element) node, true);
            }
        }
        if (elementInclude) {
            xmlWriter.closeTag(tagName);
        }
    }

    public static void writeDomTree(Document doc, OutputStream outputStream) throws TransformerException {
        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        Transformer transformer = transformerFactory.newTransformer();
        DOMSource source = new DOMSource(doc);
        StreamResult result = new StreamResult(outputStream);
        transformer.transform(source, result);
    }

    public static void writeTabs(Appendable appendable, int tabCount) throws IOException {
        if (tabCount > - 1) {
            appendable.append('\n');
            for (int i = 0; i < tabCount; i++) {
                appendable.append('\t');
            }
        }
    }

    public static String escape(String s) {
        StringBuilder buf = new StringBuilder();
        try {
            writeEscape(buf, s);
        } catch (IOException ioe) {

        }
        return buf.toString();
    }

    public static void writeEscape(Appendable appendable, String s) throws IOException {
        if ((s == null) || (s.length() == 0)) {
            return;
        }
        char[] caracs = s.toCharArray();
        for (int i = 0; i < caracs.length; i++) {
            if (caracs[i] < 32) {
                appendable.append(' ');
            } else {
                switch (caracs[i]) {
                    case '&':
                        appendable.append("&amp;");
                        break;
                    case '"':
                        appendable.append("&quot;");
                        break;
                    case '\'':
                        appendable.append("&apos;");
                        break;
                    case '<':
                        appendable.append("&lt;");
                        break;
                    case '>':
                        appendable.append("&gt;");
                        break;
                    case '\u00A0':
                        appendable.append("&#x00A0;");
                        break;
                    default:
                        appendable.append(caracs[i]);
                }
            }
        }
    }

    public static void writeTechEscape(Appendable appendable, String s) throws IOException {
        if ((s == null) || (s.length() == 0)) {
            return;
        }
        char[] caracs = s.toCharArray();
        for (int i = 0; i < caracs.length; i++) {
            if (caracs[i] < 32) {
                appendable.append(' ');
            } else {
                switch (caracs[i]) {
                    case '&':
                        appendable.append("&amp;");
                        break;
                    case '"':
                        appendable.append("&quot;");
                        break;
                    case '<':
                        appendable.append("&lt;");
                        break;
                    case '>':
                        appendable.append("&gt;");
                        break;
                    case '\u00A0':
                        appendable.append("&#x00A0;");
                        break;
                    default:
                        appendable.append(caracs[i]);
                }
            }
        }
    }

    public static void writeEscape(Appendable appendable, char carac) throws IOException {
        if (carac < 32) {
            appendable.append(' ');
        } else {
            switch (carac) {
                case '&':
                    appendable.append("&amp;");
                    break;
                case '"':
                    appendable.append("&quot;");
                    break;
                case '\'':
                    appendable.append("&apos;");
                    break;
                case '<':
                    appendable.append("&lt;");
                    break;
                case '>':
                    appendable.append("&gt;");
                    break;
                case '\u00A0':
                    appendable.append("&#x00A0;");
                    break;
                default:
                    appendable.append(carac);
            }
        }
    }

    public static String cleanString(String s) {
        return StringUtils.cleanString(s, true);
    }

    public static String cleanString(String s, boolean trim) {
        return StringUtils.cleanString(s, trim);
    }

    public static String getData(Element el) {
        return getData(el, true);
    }

    public static String getData(Element el, boolean trim) {
        String s = getRawData(el);
        return cleanString(s, trim);
    }

    /**
     * Retourne nul si vide
     */
    public static CleanedString toCleanedString(Element el) {
        String s = getRawData(el);
        return CleanedString.newInstance(s);
    }

    public static String getRawData(Element el) {
        StringBuilder buf = new StringBuilder();
        NodeList list = el.getChildNodes();
        for (int i = 0; i < list.getLength(); i++) {
            Node nd = list.item(i);
            short nodeType = nd.getNodeType();
            if ((nodeType == Node.TEXT_NODE) || (nodeType == Node.CDATA_SECTION_NODE)) {
                String data = ((Text) nd).getData();
                if (data != null) {
                    buf.append(data);
                }
            }
        }
        return buf.toString();
    }

    public static Element getFirstElement(Element el) {
        NodeList list = el.getChildNodes();
        for (int i = 0; i < list.getLength(); i++) {
            Node nd = list.item(i);
            if (nd.getNodeType() == Node.ELEMENT_NODE) {
                return (Element) nd;
            }
        }
        return null;
    }

    public static void writeAttribute(Appendable appendable, String attributename, String value) throws IOException {
        if ((value != null) && (value.length() > 0)) {
            appendable.append(' ');
            appendable.append(attributename);
            appendable.append("=\"");
            writeEscape(appendable, value);
            appendable.append('\"');
        }
    }

    public static void writeTag(Appendable appendable, String tagname, String value, int tabCount) throws IOException {
        if ((value != null) && (value.length() > 0)) {
            writeTabs(appendable, tabCount);
            appendable.append("<");
            appendable.append(tagname);
            appendable.append(">");
            writeEscape(appendable, value);
            appendable.append("</");
            appendable.append(tagname);
            appendable.append(">");
        }
    }

    public static XmlProducer toXmlProducer(String xml) {
        return new SimpleXmlProducer(xml);
    }

    public static String testXml(File xmlFile) {
        try {
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder docbuilder = documentBuilderFactory.newDocumentBuilder();
            Document document = docbuilder.parse(xmlFile);
            return null;
        } catch (SAXException saxException) {
            return ExceptionsUtils.getSAXExceptionMessage(saxException);
        } catch (Exception e) {
            return ExceptionsUtils.getMessage(e);
        }
    }

    public static String testXml(String xmlString) {
        try {
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder docbuilder = documentBuilderFactory.newDocumentBuilder();
            InputSource inputSource = new InputSource(new StringReader(xmlString));
            Document document = docbuilder.parse(inputSource);
            return null;
        } catch (SAXException saxException) {
            return ExceptionsUtils.getSAXExceptionMessage(saxException);
        } catch (Exception e) {
            return ExceptionsUtils.getMessage(e);
        }
    }

    public static String testXml(InputStream inputStream) {
        try {
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder docbuilder = documentBuilderFactory.newDocumentBuilder();
            InputSource inputSource = new InputSource(inputStream);
            Document document = docbuilder.parse(inputSource);
            return null;
        } catch (SAXException saxException) {
            return ExceptionsUtils.getSAXExceptionMessage(saxException);
        } catch (Exception e) {
            return ExceptionsUtils.getMessage(e);
        }
    }

    public static void addXmlLangAttribute(XMLWriter xmlWriter, Lang lang) throws IOException {
        if (lang != null) {
            xmlWriter.addAttribute("xml:lang", lang.toString());
        }
    }

    public static void addLibElement(XMLWriter xmlWriter, Lang lang, String lib) throws IOException {
        xmlWriter.startOpenTag("lib");
        if (lang != null) {
            xmlWriter.addAttribute("xml:lang", lang.toString());
        }
        xmlWriter.endOpenTag();
        xmlWriter.addText(lib);
        xmlWriter.closeTag("lib", false);
    }

    public static void addLibElements(XMLWriter xmlWriter, Labels labels) throws IOException {
        addLibElements(xmlWriter, labels, null);
    }

    public static void addLibElements(XMLWriter xmlWriter, Labels labels, LangFilter langFilter) throws IOException {
        for (Label label : labels) {
            if ((langFilter == null) || (langFilter.accept(label.getLang()))) {
                addLibElement(xmlWriter, label.getLang(), label.getLabelString());
            }
        }
    }

    public static AppendableXMLWriter toXMLWriter(Appendable appendable) {
        DefaultXMLWriter xmlWriter = new DefaultXMLWriter();
        xmlWriter.setAppendable(appendable);
        return xmlWriter;
    }

    public static AppendableXMLWriter toXMLWriter(Appendable appendable, int indentLength) {
        DefaultXMLWriter xmlWriter = new DefaultXMLWriter();
        xmlWriter.setAppendable(appendable);
        xmlWriter.setIndentLength(indentLength);
        return xmlWriter;
    }

    public static AppendableXMLWriter toXMLWriter(Appendable appendable, int indentLength, boolean aposEscaped) {
        DefaultXMLWriter xmlWriter = new DefaultXMLWriter();
        xmlWriter.setAppendable(appendable);
        xmlWriter.setIndentLength(indentLength);
        xmlWriter.setAposEscaped(aposEscaped);
        return xmlWriter;
    }

    public static void appendStylesheet(AppendableXMLWriter xmlWriter, String href, String type) throws IOException {
        xmlWriter.appendIndent();
        xmlWriter.append("<?xml-stylesheet href=\"");
        xmlWriter.append(href);
        xmlWriter.append("\" type=\"");
        xmlWriter.append(type);
        xmlWriter.append("\"?>");
    }

    public static void appendDocType(AppendableXMLWriter xmlWriter, String doctype) throws IOException {
        xmlWriter.appendIndent();
        xmlWriter.append("<!DOCTYPE ");
        xmlWriter.append(doctype);
        xmlWriter.append(">");
    }

    public static void appendNameSpaceAttribute(AppendableXMLWriter xmlWriter, NameSpace nameSpace) throws IOException {
        String prefix = nameSpace.getPrefix();
        String name = nameSpace.getName();
        xmlWriter.append(' ');
        xmlWriter.append("xmlns");
        if (prefix.length() > 0) {
            xmlWriter.append(':');
            xmlWriter.append(prefix);
        }
        xmlWriter.append('=');
        xmlWriter.append('\"');
        xmlWriter.addText(name);
        xmlWriter.append('\"');
    }

    public static void appendXmlDeclaration(AppendableXMLWriter xmlWriter) throws IOException {
        xmlWriter.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
    }

    public static String indent(String xml) {
        if (xml.length() < 4) {
            return xml;
        }
        if (xml.startsWith(">")) {
            return xml;
        }
        StringBuilder buf = new StringBuilder();
        buildIndent(buf, xml, 0, 0);
        return buf.toString();
    }

    private static void buildIndent(StringBuilder buf, String xml, int currentIndex, int indentation) {
        int idx = xml.indexOf("> <", currentIndex);
        if (idx == -1) {
            buf.append(xml, currentIndex, xml.length());
            return;
        }
        buf.append(xml, currentIndex, idx);
        buf.append('>');
        buf.append('\n');
        indentation = checkIndentation(xml, idx, indentation);
        for (int i = 0; i < indentation; i++) {
            buf.append("  ");
        }
        buf.append('<');
        buildIndent(buf, xml, idx + 3, indentation);
    }

    private static int checkIndentation(String xml, int idx, int indentation) {
        boolean closingNext = isClosingNext(xml, idx);
        short type = getPreviousType(xml, idx);
        switch (type) {
            case BALISE_AUTOFERMANTE:
                if (closingNext) {
                    indentation--;
                }
                break;
            case BALISE_OUVRANTE:
                if (!closingNext) {
                    indentation++;
                }
                break;
            case BALISE_FERMANTE:
                if (closingNext) {
                    indentation--;
                }
                break;
        }
        if (indentation < 0) {
            indentation = 0;
        }
        return indentation;
    }

    private static boolean isClosingNext(String xml, int idx) {
        idx = idx + 3;
        if (idx < xml.length()) {
            return (xml.charAt(idx) == '/');
        } else {
            return false;
        }
    }

    private static short getPreviousType(String xml, int idx) {
        char prev = xml.charAt(idx - 1);
        if (prev == '/') {
            return BALISE_AUTOFERMANTE;
        }
        for (int i = (idx - 1); i >= 0; i--) {
            char carac = xml.charAt(i);
            if (carac == '<') {
                if (xml.charAt(i + 1) == '/') {
                    return BALISE_FERMANTE;
                } else {
                    return BALISE_OUVRANTE;
                }
            }
        }
        return BALISE_OUVRANTE;

    }


    private static class EmptyDocumentFragmentHolder implements DocumentFragmentHolder {

        private EmptyDocumentFragmentHolder() {
        }

        @Override
        public DocumentFragment getFragment(String fragmentKey) {
            return null;
        }

    }


    private static class SimpleXmlProducer implements XmlProducer {

        private final String xml;

        private SimpleXmlProducer(String xml) {
            this.xml = xml;
        }

        @Override
        public void writeXml(Appendable appendable) throws IOException {
            appendable.append(xml);
        }

    }

}
