/**
 * Copyright (c) 2011-2012 Tim Roes
 * <p>
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
 * associated documentation files (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge, publish, distribute,
 * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * <p>
 * The above copyright notice and this permission notice shall be included in all copies or
 * substantial portions of the Software.
 * <p>
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
 * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package com.satikey.tools.supervisord;

import com.satikey.tools.supervisord.exceptions.SupervisordException;
import com.satikey.tools.supervisord.exceptions.XMLRPCException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
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;

/**
 * The ResponseParser parses the response of an XMLRPC server to an object.
 *
 * @author Tim Roes {#link https://github.com/gturri/aXMLRPC}
 * @since 1.6
 */
class ResponseParser {
    private enum TYPE {
        I4, INT, STRING, BOOLEAN, DOUBLE, STRUCT, ARRAY
    }

    private static final String FAULT_CODE = "faultCode";
    private static final String FAULT_STRING = "faultString";

    private static final String METHOD_RESPONSE = "methodResponse";
    private static final String PARAMS = "params";
    private static final String PARAM = "param";
    public static final String VALUE = "value";
    private static final String FAULT = "fault";
    private static final String METHOD_CALL = "methodCall";
    private static final String METHOD_NAME = "methodName";
    private static final String STRUCT_MEMBER = "member";

    Object parse(InputStream xmlin)
            throws XMLRPCException, SupervisordException {
        try {

            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setNamespaceAware(true);
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document dom = builder.parse(xmlin);

            Element e = dom.getDocumentElement();

            // Check for root tag
            if (!e.getNodeName().equals(METHOD_RESPONSE)) {
                throw new XMLRPCException("MethodResponse root tag is missing.");
            }
            //ROOT
            e = getOnlyChildElement(e.getChildNodes());

            if (e.getNodeName().equals(PARAMS)) {
                //PARAMS
                e = getOnlyChildElement(e.getChildNodes());
                if (!e.getNodeName().equals(PARAM)) {
                    throw new XMLRPCException("The params tag must contain a param tag.");
                }
                //parse value
                return getReturnValueFromElement(e);
            } else if (e.getNodeName().equals(FAULT)) {

                @SuppressWarnings("unchecked")
                Map<String, Object> o = (Map<String, Object>) getReturnValueFromElement(e);
                Integer faultCode = (Integer) o.get(FAULT_CODE);
                throw SupervisordException.create(SupervisordException.Code.get(faultCode));

            }

            throw new XMLRPCException("The methodResponse tag must contain a fault or params tag.");

        } catch (XMLRPCException ex) {
            throw ex;
        } catch (SupervisordException sdex) {
            throw sdex;
        } catch (Exception ex) {
            throw new XMLRPCException("Error getting result from server.", ex);
        }
    }

    public static void printDocument(Document doc, OutputStream out)
            throws IOException, TransformerException {
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer transformer = tf.newTransformer();
        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
        transformer.setOutputProperty(OutputKeys.METHOD, "xml");
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");

        transformer.transform(new DOMSource(doc),
                              new StreamResult(new OutputStreamWriter(out, "UTF-8")));
    }

    /**
     * This method takes an element (must be a param or fault element) and returns the deserialized
     * object of this param tag.
     *
     * @param element An param element.
     * @return The deserialized object within the given param element.
     * @throws XMLRPCException Will be thrown when the structure of the document doesn't match the
     *                         XML-RPC specification.
     */
    private Object getReturnValueFromElement(Element element) {
        Element childElement = getOnlyChildElement(element.getChildNodes());
        return extract(childElement);
    }

    private Object extract(Element element) {
        Element childElement = getOnlyChildElement(element.getChildNodes());
        String nodeName = childElement.getNodeName();
        TYPE type = TYPE.valueOf(nodeName.toUpperCase());
        switch (type) {
            case INT:
                return extractInt(childElement);
            case BOOLEAN:
                return true;
            case STRING:
                return extractString(childElement);
            case STRUCT:
                return extractStruct(childElement);
            case ARRAY:
                return extractArray(childElement);
            default:
                return 0;
        }
    }

    private String extractString(Element content)
            throws XMLRPCException {
        String text = getOnlyTextContent(content.getChildNodes());
        text = text.replaceAll("&lt;", "<").replaceAll("&amp;", "&");
        return text;
    }

    private int extractInt(Element content) {
        return Integer.parseInt(getOnlyTextContent(content.getChildNodes()));
    }

    private boolean extractBoolean(Element content) {
        return getOnlyTextContent(content.getChildNodes()).equals("1")
                ? Boolean.TRUE : Boolean.FALSE;
    }

    private Object extractStruct(Element content) {
        final String STRUCT_MEMBER = "member";
        final String STRUCT_NAME = "name";
        final String STRUCT_VALUE = "value";

        Map<String, Object> map = new HashMap<String, Object>();

        Node n, m;
        String s;
        Object o;
        for (int i = 0; i < content.getChildNodes().getLength(); i++) {

            n = content.getChildNodes().item(i);

            // Strip only whitespace text elements and comments
            if ((n.getNodeType() == Node.TEXT_NODE
                    && n.getNodeValue().trim().length() <= 0)
                    || n.getNodeType() == Node.COMMENT_NODE)
                continue;

            if (n.getNodeType() != Node.ELEMENT_NODE
                    || !STRUCT_MEMBER.equals(n.getNodeName())) {
                throw new XMLRPCException("Only struct members allowed within a struct.");
            }

            // Grep name and value from member
            s = null;
            o = null;
            for (int j = 0; j < n.getChildNodes().getLength(); j++) {
                m = n.getChildNodes().item(j);

                // Strip only whitespace text elements and comments
                if ((m.getNodeType() == Node.TEXT_NODE
                        && m.getNodeValue().trim().length() <= 0)
                        || m.getNodeType() == Node.COMMENT_NODE)
                    continue;

                if (STRUCT_NAME.equals(m.getNodeName())) {
                    if (s != null) {
                        throw new XMLRPCException("Name of a struct member cannot be set twice.");
                    } else {
                        s = getOnlyTextContent(m.getChildNodes());
                    }
                } else if (m.getNodeType() == Node.ELEMENT_NODE &&
                        STRUCT_VALUE.equals(m.getNodeName())) {
                    if (o != null) {
                        throw new XMLRPCException("Value of a struct member cannot be set twice.");
                    } else {
                        o = extract((Element) m);
                    }
                } else {
                    throw new XMLRPCException("A struct member must only contain one name and one value.");
                }

            }

            map.put(s, o);

        }

        return map;
    }

    private Object[] extractArray(Element content) {
        final String ARRAY_DATA = "data";
        final String ARRAY_VALUE = "value";
        List<Object> list = new ArrayList<Object>();

        Element data = getOnlyChildElement(content.getChildNodes());

        if (!ARRAY_DATA.equals(data.getNodeName())) {
            throw new XMLRPCException("The array must contain one data tag.");
        }

        // Deserialize every array element
        Node value;
        for (int i = 0; i < data.getChildNodes().getLength(); i++) {

            value = data.getChildNodes().item(i);

            // Strip only whitespace text elements and comments
            if (value == null || (value.getNodeType() == Node.TEXT_NODE
                    && value.getNodeValue().trim().length() <= 0)
                    || value.getNodeType() == Node.COMMENT_NODE)
                continue;

            if (value.getNodeType() != Node.ELEMENT_NODE) {
                throw new XMLRPCException("Wrong element inside of array.");
            }

            list.add(extract((Element) value));

        }

        return list.toArray();
    }

    /**
     * Returns the only child element in a given NodeList. Will throw an error if there is more then
     * one child element or any other child that is not an element or an empty text string
     * (whitespace are normal).
     *
     * @param list A NodeList of children nodes.
     * @return The only child element in the given node list.
     * @throws XMLRPCException Will be thrown if there is more then one child element except empty
     *                         text nodes.
     */
    private static Element getOnlyChildElement(NodeList list)
            throws XMLRPCException {

        Element e = null;
        Node n;
        for (int i = 0; i < list.getLength(); i++) {
            n = list.item(i);
            // Strip only whitespace text elements and comments
            if ((n.getNodeType() == Node.TEXT_NODE
                    && n.getNodeValue().trim().length() <= 0)
                    || n.getNodeType() == Node.COMMENT_NODE)
                continue;

            // Check if there is anything else than an element node.
            if (n.getNodeType() != Node.ELEMENT_NODE) {
                throw new XMLRPCException("Only element nodes allowed.");
            }

            // If there was already an element, throw exception.
            if (e != null) {
                throw new XMLRPCException("Element has more than one children.");
            }

            e = (Element) n;

        }

        return e;

    }

    /**
     * Returns the text node from a given NodeList. If the list contains more then just text nodes,
     * an exception will be thrown.
     *
     * @param list The given list of nodes.
     * @return The text of the given node list.
     * @throws XMLRPCException Will be thrown if there is more than just one text node within the
     *                         list.
     */
    private static String getOnlyTextContent(NodeList list)
            throws XMLRPCException {

        StringBuilder builder = new StringBuilder();
        Node n;

        for (int i = 0; i < list.getLength(); i++) {
            n = list.item(i);

            // Skip comments inside text tag.
            if (n.getNodeType() == Node.COMMENT_NODE) {
                continue;
            }

            if (n.getNodeType() != Node.TEXT_NODE) {
                throw new XMLRPCException("Element must contain only text elements.");
            }

            builder.append(n.getNodeValue());

        }

        return builder.toString();

    }

    /**
     * Checks if the given {@link NodeList} contains a child element.
     *
     * @param list The {@link NodeList} to check.
     * @return Whether the {@link NodeList} contains children.
     */
    public static boolean hasChildElement(NodeList list) {

        Node n;

        for (int i = 0; i < list.getLength(); i++) {
            n = list.item(i);

            if (n.getNodeType() == Node.ELEMENT_NODE) {
                return true;
            }
        }

        return false;

    }
}