/*
 * Copyright (c) 2017 XMLmind Software. All rights reserved.
 *
 * Author: Hussein Shafie
 *
 * This file is part of the XMLmind Assembly Processor project.
 * For conditions of distribution and use, see the accompanying legal.txt file.
 */
package com.xmlmind.domutil;

import java.net.MalformedURLException;
import java.net.URL;
import static javax.xml.XMLConstants.DEFAULT_NS_PREFIX;
import static javax.xml.XMLConstants.XML_NS_URI;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.UserDataHandler;
import org.w3c.dom.Node;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Document;
import org.w3c.dom.ls.DOMImplementationLS;
import com.xmlmind.util.ThrowableUtil;
import com.xmlmind.util.ObjectUtil;
import com.xmlmind.util.URLUtil;
import com.xmlmind.util.XMLText;

public final class DOMUtil {
    private DOMUtil() {}

    // -----------------------------------------------------------------------

    public static Document newDocument() {
        return newDocument(null, null, null);
    }

    public static Document newDocument(String namespaceURI, 
                                       String qualifiedName, 
                                       DocumentType docType) {
        DOMImplementation impl = getDOMImplementation();
        return impl.createDocument(namespaceURI, qualifiedName, docType);
    }

    public static DOMImplementation getDOMImplementation() {
        DOMImplementation impl;
        try {
            DocumentBuilder builder =
                DocumentBuilderFactory.newInstance().newDocumentBuilder();
            impl = builder.getDOMImplementation();

            // Check whether it actually implements DOM3 Load & Save.
            DOMImplementationLS implLS = (DOMImplementationLS) impl;
        } catch (Exception shouldNotHappen) {
            throw new RuntimeException(
                Msg.msg("cannotGetDOMImplementationLS",
                        ThrowableUtil.reason(shouldNotHappen)));
        }
        return impl;
    }

    // -----------------------------------------------------------------------

    public static final class CopyUserData implements UserDataHandler {
        public void handle(short operation, String key, Object data,
                           Node src, Node dst) {
            if (data != null) {
                switch (operation) {
                case UserDataHandler.NODE_IMPORTED:
                case UserDataHandler.NODE_CLONED:
                    dst.setUserData(key, data, this);
                    break;
                }
            }
        }
    }

    public static final CopyUserData COPY_USER_DATA = new CopyUserData();

    public static void removeUserData(Node node, String key) {
        node.setUserData(key, null, null);

        Node child = node.getFirstChild();
        while (child != null) {
            removeUserData(child, key);

            child = child.getNextSibling();
        }
    }

    // -----------------------------------------------------------------------

    public static boolean hasName(Node node, QName name) {
        return hasName(node, getNamespaceURI(name), name.getLocalPart());
    }

    public static String getNamespaceURI(QName name) {
        String nsURI = name.getNamespaceURI();
        if (nsURI != null && nsURI.length() == 0) {
            // For Java, no namespace is "". For W3C DOM, no namespace is null.
            nsURI = null;
        }
        return nsURI;
    }

    public static String getQName(QName name) {
        String prefix = name.getPrefix();
        if (prefix == null || prefix.length() == 0) {
            return name.getLocalPart();
        } else {
            StringBuilder buffer = new StringBuilder();
            buffer.append(prefix);
            buffer.append(':');
            buffer.append(name.getLocalPart());
            return buffer.toString();
        }
    }

    public static boolean hasName(Node node,
                                  String namespace, String localName) {
        String localName2 = node.getLocalName();
        if (localName2 == null) {
            return false;
        }

        return (localName.equals(localName2) &&
                ObjectUtil.equals(namespace, node.getNamespaceURI()));
    }

    // -----------------------------------------------------------------------

    public static String[] parseName(String name) {
        String nsURI = null;
        String localName = null;
        
        name = name.trim();
        if (name.startsWith("xml:")) {
            nsURI = XML_NS_URI;
            localName = name.substring(4);
        } else {
            if (name.startsWith("{")) {
                int pos = name.lastIndexOf('}');
                if (pos < 0) {
                    return null;
                }
                nsURI = name.substring(1, pos).trim();
                if (nsURI.length() == 0) {
                    nsURI = null;
                }

                name = name.substring(pos+1);
            }

            localName = name;
        }
        
        if (!XMLText.isNCName(localName)) {
            return null;
        }

        return new String[] { nsURI, localName };
    }

    // -----------------------------------------------------------------------

    public static String getNonEmptyAttribute(Element element,
                                              String namespace,
                                              String localName) {
        String value = element.getAttributeNS(namespace, localName);
        if (value == null ||
            (value = value.trim()).length() == 0 ||
            // "???" is a placeholder value often used by XMLmind XML Editor.
            "???".equals(value)) {
            value = null;
        }
        return value;
    }

    public static QName getQNameAttribute(Element element,
                                          String namespace,
                                          String localName,
                                          QName fallback) {
        String value = getNonEmptyAttribute(element, namespace, localName);
        if (value == null) {
            return fallback;
        }

        String prefix, localPart;
        int pos = value.indexOf(':');
        if (pos > 0 && pos < value.length()-1) {
            prefix = value.substring(0, pos);
            localPart = value.substring(pos+1);
        } else {
            prefix = null; // Default namespace.
            localPart = value;
        }
        
        String nsURI = element.lookupNamespaceURI(prefix);
        if (nsURI == null) {
            // Prefix not found.
            return null;
        } else {
            return new QName(nsURI, localPart,
                             (prefix == null)? DEFAULT_NS_PREFIX : prefix);
        }
    }

    public static boolean getBooleanAttribute(Element element,
                                              String namespace,
                                              String localName,
                                              boolean fallback) {
        String value = getNonEmptyAttribute(element, namespace, localName);
        if (value == null) {
            return fallback;
        } else if ("true".equals(value)) {
            return true;
        } else if ("false".equals(value)) {
            return false;
        } else {
            return fallback;
        }
    }

    // -----------------------------------------------------------------------

    public static String getXMLLang(Element element) {
        // An empty value for xml:lang is OK.
        return getAttribute(element, XML_NS_URI, "lang");
    }

    public static String getAttribute(Element element,
                                      String namespace, String localName) {
        Attr attr = element.getAttributeNodeNS(namespace, localName);
        return (attr == null)? null : attr.getValue().trim();
    }

    public static String lookupXMLLang(Element element) {
        while (element != null) {
            String value = getXMLLang(element);
            if (value != null) {
                return value;
            }

            element = getParentElement(element);
        }

        return null;
    }

    public static Element getParentElement(Node node) {
        Node parent = node.getParentNode();
        if (parent == null || parent.getNodeType() != Node.ELEMENT_NODE) {
            return null;
        } else {
            return (Element) parent;
        }
    }

    // -----------------------------------------------------------------------

    public static URL getBaseURL(Node tree) {
        URL url = null;

        Node node = tree;
        while (node != null) {
            Node parent = node.getParentNode();
            
            switch (node.getNodeType()) {
            case Node.ELEMENT_NODE:
                {
                    String baseURI = 
                       getNonEmptyAttribute((Element) node, XML_NS_URI, "base");
                    if (baseURI != null) {
                        try {
                            url = URLUtil.createURL(baseURI);
                        } catch (MalformedURLException ignored) {}

                        if (url == null && parent != null) {
                            // May be it is a relative URL.

                            URL parentURL = getBaseURL(parent);
                            if (parentURL != null) {
                                try {
                                    url = URLUtil.createURL(parentURL, baseURI);
                                } catch (MalformedURLException ignored) {}
                            }
                        }
                    }
                }
                break;

            case Node.DOCUMENT_NODE:
                url = getDocumentURL((Document) node);
                break;
            }

            if (url != null) {
                break;
            }

            node = parent;
        }

        return url;
    }

    public static URL getDocumentURL(Document doc) {
        URL url = null;

        String location = doc.getDocumentURI();
        if (location != null) {
            try {
                url = URLUtil.createURL(location);
            } catch (MalformedURLException ignored) {}
        }

        return url;
    }

    // -----------------------------------------------------------------------

    public static Element findElementByName(Node node,
                                            String namespace,
                                            String localName) {
        if (node.getNodeType() == Node.ELEMENT_NODE) {
            Element elem = (Element) node;

            String ns = elem.getNamespaceURI();

            if (elem.getLocalName().equals(localName) &&
                ((namespace == null && ns == null) ||
                 (namespace != null && namespace.equals(ns)))) {
                return elem;
            }
        }

        Node child = node.getFirstChild();
        while (child != null) {
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                Element childElement = (Element) child;

                Element found = 
                    findElementByName(childElement, namespace, localName);
                if (found != null) {
                    return found;
                }
            }

            child = child.getNextSibling();
        }

        return null;
    }

    public static Element getChildElementByName(Node node,
                                                String namespace,
                                                String localName) {
        Node child = node.getFirstChild();
        while (child != null) {
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                Element childElement = (Element) child;

                String ns = childElement.getNamespaceURI();

                if (childElement.getLocalName().equals(localName) &&
                    ((namespace == null && ns == null) ||
                     (namespace != null && namespace.equals(ns)))) {
                    return childElement;
                }
            }

            child = child.getNextSibling();
        }

        return null;
    }

    // -----------------------------------------------------------------------

    public static Element findElementById(Element element, String id) {
        return findElementByAttribute(element, XML_NS_URI, "id", id);
    }

    public static Element findElementByAttribute(Element element, 
                                                 String namespace,
                                                 String localName,
                                                 String value) {
        if (value.equals(getNonEmptyAttribute(element, namespace, localName))) {
            return element;
        }

        Node child = element.getFirstChild();
        while (child != null) {
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                Element found = 
                    findElementByAttribute((Element) child, 
                                           namespace, localName, value);
                if (found != null) {
                    return found;
                }
            }

            child = child.getNextSibling();
        }

        return null;
    }

    // -----------------------------------------------------------------------

    public static Element getNthChildElement(Node node, int index) {
        int i = 0;

        Node child = node.getFirstChild();
        while (child != null) {
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                if (index == i) {
                    return (Element) child;
                }
                ++i;
            }

            child = child.getNextSibling();
        }

        return null;
    }
}
