/**
 *  Copyright 2005-2016 Red Hat, Inc.
 *
 *  Red Hat licenses this file to you under the Apache License, version
 *  2.0 (the "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 *  implied.  See the License for the specific language governing
 *  permissions and limitations under the License.
 */
package io.fabric8.common.util;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * XML related utilities.
 */
public class XmlUtils {

    private XmlUtils() {
        // utility class
    }

    public static Document parseDoc(File xmlFile)
            throws ParserConfigurationException,
            SAXException,
            IOException {
        return parseDoc(new FileInputStream(xmlFile));
    }

    public static Document parseDoc(final InputStream is)
            throws ParserConfigurationException,
            SAXException,
            IOException {
        try {
            BufferedInputStream in = new BufferedInputStream(is);
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
            factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
            factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
            DocumentBuilder builder = factory.newDocumentBuilder();
            InputSource source = new InputSource(in);
            return builder.parse(source);
        } finally {
            is.close();
        }
    }

    public static Element getElement(final Document doc, final String path) {
        NullArgumentException.validateNotNull(doc, "XML document");
        return getElement(doc.getDocumentElement(), path);
    }

    public static Element getElement(final Element element, final String path) {
        NullArgumentException.validateNotNull(element, "Element ");
        NullArgumentException.validateNotNull(path, "Element path");

        Element current = element;
        StringTokenizer st = new StringTokenizer(path, "/", false);
        while (st.hasMoreTokens() && current != null) {
            final String token = st.nextToken();
            final NodeList childs = current.getChildNodes();
            current = null;
            for (int i = 0; i < childs.getLength(); i++) {
                final Node child = childs.item(i);
                if (child instanceof Element && child.getNodeName().equals(token)) {
                    current = (Element) child;
                }
            }
        }
        return current;
    }

    public static List<Element> getElements(final Document doc, final String path) {
        NullArgumentException.validateNotNull(doc, "Document");
        return getElements(doc.getDocumentElement(), path);
    }

    public static List<Element> getElements(final Element element, final String path) {
        NullArgumentException.validateNotNull(element, "Element");
        NullArgumentException.validateNotNull(path, "Element path");
        String lastElement;
        Element parent;
        if (path.contains("/")) {
            parent = getElement(element, path.substring(0, path.lastIndexOf("/")));
            lastElement = path.substring(path.lastIndexOf("/") + 1);
        } else {
            parent = element;
            lastElement = path;
        }
        List<Element> elements = null;
        if (parent != null) {
            NodeList nodeList = parent.getElementsByTagName(lastElement);
            if (nodeList != null) {
                elements = new ArrayList<Element>();
                for (int i = 0; i < nodeList.getLength(); i++) {
                    elements.add((Element) nodeList.item(i));
                }
            }
        }
        return elements;
    }

    public static List<Element> getChildElements(final Element element) {
        final List<Element> elements = new ArrayList<Element>();
        final NodeList childs = element.getChildNodes();
        for (int i = 0; i < childs.getLength(); i++) {
            Node child = childs.item(i);
            if (child instanceof Element) {
                elements.add((Element) child);
            }
        }
        return elements;
    }

    public static String getTextContentOfElement(final Document doc, final String path) {
        NullArgumentException.validateNotNull(doc, "XML document");
        return getTextContentOfElement(doc.getDocumentElement(), path);
    }

    public static String getTextContentOfElement(final Element element, final String path) {
        NullArgumentException.validateNotNull(element, "Element");
        NullArgumentException.validateNotNull("Element path", path);

        StringTokenizer st = new StringTokenizer(path, "/", false);
        Element currentElement = element;
        while (st.hasMoreTokens()) {
            final String childName = st.nextToken();
            if (childName.endsWith("]")) {
                int startPos = childName.indexOf("[");
                int endPos = childName.indexOf("]");
                NodeList children = currentElement.getElementsByTagName(childName.substring(0, startPos));
                int numChildren = children.getLength();
                int index;
                String numbers = childName.substring(startPos + 1, endPos);
                if ("last".equals(numbers)) {
                    index = numChildren - 1;
                } else {
                    index = Integer.parseInt(numbers);
                }
                if (index > numChildren) {
                    throw new IllegalArgumentException(
                            "index of " + index + " is larger than the number of child nodes (" + numChildren + ")"
                    );
                }
                currentElement = (Element) children.item(index);
            } else {
                final NodeList parent = currentElement.getElementsByTagName(childName);
                if (parent != null) {
                    currentElement = (Element) parent.item(0);
                }
            }
            if (null == currentElement) {
                return null;
            }
        }
        return getTextContent(currentElement);
    }

    public static String getTextContent(final Node node) {
        switch (node.getNodeType()) {
            case Node.ELEMENT_NODE:
            case Node.ATTRIBUTE_NODE:
            case Node.ENTITY_NODE:
            case Node.ENTITY_REFERENCE_NODE:
            case Node.DOCUMENT_FRAGMENT_NODE:
                return mergeTextContent(node.getChildNodes());
            case Node.TEXT_NODE:
            case Node.CDATA_SECTION_NODE:
            case Node.COMMENT_NODE:
            case Node.PROCESSING_INSTRUCTION_NODE:
                return node.getNodeValue();
            case Node.DOCUMENT_NODE:
            case Node.DOCUMENT_TYPE_NODE:
            case Node.NOTATION_NODE:
            default:
                return null;
        }
    }

    private static String mergeTextContent(final NodeList nodes) {
        StringBuffer buf = new StringBuffer();
        for (int i = 0; i < nodes.getLength(); i++) {
            Node n = nodes.item(i);
            final String text;

            switch (n.getNodeType()) {
                case Node.COMMENT_NODE:
                case Node.PROCESSING_INSTRUCTION_NODE:
                    // ignore comments when merging
                    text = null;
                    break;
                default:
                    text = getTextContent(n);
                    break;
            }

            if (text != null) {
                buf.append(text);
            }
        }
        return buf.toString();
    }

}
