/*
 * Copyright (C) 2018 Fondazione Istituto Italiano di Tecnologia
 *
 * Author: Francesco Romano - Google LLC
 *
 * Licensed under either the GNU Lesser General Public License v3.0 :
 * https://www.gnu.org/licenses/lgpl-3.0.html
 * or the GNU Lesser General Public License v2.1 :
 * https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
 * at your option.
 */

#include "XMLParser.h"

#include "XMLElement.h"
#include "XMLAttribute.h"
#include "XMLDocument.h"

#include <iDynTree/Core/Utils.h>

#include <libxml/SAX2.h>
#include <libxml/xmlschemas.h>

#include <cassert>
#include <iostream>
#include <mutex>
#include <stack>
#include <vector>
#include <cstdarg>
#include <cstdio>
#include <cstring>

static const std::string stringFromFormattedCString(const char *message, va_list arguments)
{
    va_list tempArguments;
    va_copy(tempArguments, arguments);
    // We have to get the size of the final string.
    // We use vsnprintf with an empty buffer
    // (the function returns the number of characters that didn't fit the buffer)
    va_copy(tempArguments, arguments);
    int length = std::vsnprintf(NULL, 0, message, tempArguments);
    va_end(tempArguments);

    // We now have the size. We can create the real string.
    std::vector<char> buffer(length + 1);
    std::vsnprintf(buffer.data(), buffer.size(), message, arguments);

    return std::string(buffer.begin(), buffer.end());
}

namespace iDynTree {
    
    //MARK: - XMLParserPimpl definition
    
    class XMLParser::XMLParserPimpl {
        
        xmlSAXHandler m_callbackHandler;
        
    public:
        std::stack<std::shared_ptr<XMLElement>> m_parsedTrace;
        std::function<std::shared_ptr<XMLDocument>(XMLParserState&)> f_documentFactory;

        std::shared_ptr<XMLDocument> m_document;

        std::string m_schemaLocation;
        bool m_performValidation;

        bool m_logParsing;

        bool m_keepInMemory;
        std::vector<std::string> m_packageDirs;

        XMLParserState m_parserState;

    public:
        XMLParserPimpl() {
            // ???: SAX2 structure initialization is kept in PIMPL constructor
            // All the other initializations are left to the main class
            
            // xmlSAX2InitDefaultSAXHandler(&m_callbackHandler, 0);
            // xmlSAXVersion(&m_callbackHandler, 2);
            // TODO: check what init does as the program crashes in that case
            // I think it initialize some callbacks that are missing
            memset(&m_callbackHandler, 0, sizeof(xmlSAXHandler));
            m_callbackHandler.initialized = XML_SAX2_MAGIC;
            
            m_callbackHandler.startElementNs = &parserCallbackStartTag;
            m_callbackHandler.endElementNs = &parserCallbackEndTag;
            m_callbackHandler.startDocument = &parserCallbackStartDocument;
            m_callbackHandler.endDocument = &parserCallbackEndDocument;
            m_callbackHandler.characters = &parserCallbackCharacters;
            m_callbackHandler.error = &parserErrorMessageCallback;
            m_callbackHandler.warning = &parserWarningMessageCallback;

        }
        
        xmlSAXHandlerPtr callbackHandler() { return &m_callbackHandler; }
        //TODO: should we handle entities (i.e. &amp; &lg; etc).
//        static xmlEntityPtr
//        my_getEntity(void *user_data, const xmlChar *name) {
//            return xmlGetPredefinedEntity(name);
//        }

    private:
        
        static const std::string stringFromXMLCharPtr(const xmlChar* xmlString);
        static const std::unordered_map<std::string, std::shared_ptr<XMLAttribute>> attributesFromArray(const xmlChar ** attributes,
                                                                                       int nb_attributes,
                                                                                       int nb_defaulted);
        
        static void parserCallbackStartDocument(void* context);
        static void parserCallbackEndDocument(void* context);
        static void parserCallbackStartTag(void* context,
                                           const xmlChar * localname,
                                           const xmlChar * prefix,
                                           const xmlChar * URI,
                                           int nb_namespaces,
                                           const xmlChar ** namespaces,
                                           int nb_attributes,
                                           int nb_defaulted,
                                           const xmlChar ** attributes);
        static void parserCallbackEndTag(void* context,
                                         const xmlChar * localname,
                                         const xmlChar * prefix,
                                         const xmlChar * URI);
        static void parserCallbackCharacters(void * ctx,
                                             const xmlChar * ch,
                                             int len);

        static void parserErrorMessageCallback(void * ctx, const char * msg, ...);
        static void parserWarningMessageCallback(void * ctx, const char * msg, ...);
    };
    
    const std::string XMLParser::XMLParserPimpl::stringFromXMLCharPtr(const xmlChar* xmlString)
    {
        return std::string(reinterpret_cast<const char*>(xmlString), xmlStrlen(xmlString));
    }
    
    const std::unordered_map<std::string, std::shared_ptr<XMLAttribute>> XMLParser::XMLParserPimpl::attributesFromArray(const xmlChar ** attributes,
                                                                                                       int nb_attributes,
                                                                                                       int nb_defaulted)
    {
        // TODO: how to handle the nb_defaulted parameter
        const int fields = 5;
        std::unordered_map<std::string, std::shared_ptr<XMLAttribute>> mappedAttributes;
        // Array of array. The inner array contains: localname/prefix/URI/value/end
        for (int attributeIndex = 0; attributeIndex < nb_attributes; ++attributeIndex) {
            std::string name = XMLParser::XMLParserPimpl::stringFromXMLCharPtr(attributes[attributeIndex * fields + 0]);
            std::string prefix = XMLParser::XMLParserPimpl::stringFromXMLCharPtr(attributes[attributeIndex * fields + 1]);
            std::string uri = XMLParser::XMLParserPimpl::stringFromXMLCharPtr(attributes[attributeIndex * fields + 2]);
            const xmlChar *value_start = attributes[attributeIndex * fields + 3];
            const xmlChar *value_end = attributes[attributeIndex * fields + 4];
            size_t len = value_end - value_start;
            std::string value(reinterpret_cast<const char*>(value_start), len);
            
            mappedAttributes.emplace(name, std::make_shared<XMLAttribute>(name, value, prefix, uri));
        }
        return mappedAttributes;
    }
    
    void XMLParser::XMLParserPimpl::parserCallbackStartDocument(void* context)
    {
        XMLParser *state = static_cast<XMLParser*>(context);
        if (state->m_pimpl->m_logParsing) {
            reportInfo("XMLParser", "parserCallbackStartDocument", "Start document");
        }
        // clear stack
        state->m_pimpl->m_parsedTrace = std::stack<std::shared_ptr<XMLElement>>();
        // create a Document type
        state->m_pimpl->m_document = std::shared_ptr<XMLDocument>(
            state->m_pimpl->f_documentFactory(state->m_pimpl->m_parserState));
    }
    
    void XMLParser::XMLParserPimpl::parserCallbackEndDocument(void* context)
    {
        XMLParser *state = static_cast<XMLParser*>(context);
        if (state->m_pimpl->m_logParsing) {
            reportInfo("XMLParser", "parserCallbackEndDocument", "End document");
        }

        if (!state->m_pimpl->m_document->documentHasBeenParsed()) {
            reportError("XMLParser", "parserCallbackEndDocument", "Document final callback failed processing");
        }
        
        if (!state->m_pimpl->m_parsedTrace.empty()) {
            // This is an error. Reset the document as it is invalid
            state->m_pimpl->m_document.reset();
            reportError("XMLParser", "parserCallbackEndDocument", "Unbalanced tags in the document");
        }
        
    }
    void XMLParser::XMLParserPimpl::parserCallbackStartTag(void* context,
                                                           const xmlChar * localname,
                                                           const xmlChar * prefix,
                                                           const xmlChar * URI,
                                                           int nb_namespaces,
                                                           const xmlChar ** namespaces,
                                                           int nb_attributes,
                                                           int nb_defaulted,
                                                           const xmlChar ** attributes)
    {
        // ask to the top of the stack if it can push this element
        // TODO: handle namespaces and prefix
        XMLParser *state = static_cast<XMLParser*>(context);
        const std::string localNameString = XMLParser::XMLParserPimpl::stringFromXMLCharPtr(localname);
        if (state->m_pimpl->m_logParsing) {
            // TODO: reportXXX should either accept a format + var arguments or something else
            std::string message = std::string("Start of tag <") + localNameString + (">");
            reportInfo("XMLParser", "parserCallbackStartTag", message.c_str());
        }
        
        // get attributes
        std::unordered_map<std::string, std::shared_ptr<XMLAttribute>> parsedAttributes = XMLParser::XMLParserPimpl::attributesFromArray(attributes, nb_attributes, nb_defaulted);
        
        if (state->m_pimpl->m_logParsing) {
            for (auto pair : parsedAttributes) {
                // TODO: reportXXX should either accept a format + var arguments or something else
                std::string message = std::string("Attribute found: ") + pair.second->description();
                reportInfo("XMLParser", "parserCallbackStartTag", message.c_str());
            }
        }
        
        std::shared_ptr<XMLElement> nextElement;
        // The start tag can be the root or not. If root, we have to ask the children to the
        // Document object
        if (state->m_pimpl->m_parsedTrace.empty()) {
            // it is the root
            nextElement = state->m_pimpl->m_document->rootElementForName(localNameString,
                                                                         state->m_pimpl->m_packageDirs);
            if (state->m_pimpl->m_keepInMemory) {
                state->m_pimpl->m_document->setRootElement(nextElement);
            }
        } else {
            nextElement = state->m_pimpl->m_parsedTrace.top()->childElementForName(localNameString);
            if (state->m_pimpl->m_keepInMemory) {
                state->m_pimpl->m_parsedTrace.top()->addChildElement(nextElement);
            }
        }
        
        // additionally configure element
        if (!nextElement->setAttributes(parsedAttributes)) {
            // Error
        }
        state->m_pimpl->m_parsedTrace.push(nextElement);
        
    }
    
    void XMLParser::XMLParserPimpl::parserCallbackEndTag(void* context,
                                                         const xmlChar * localname,
                                                         const xmlChar * prefix,
                                                         const xmlChar * URI)
    {
        // TODO: use the prefix and uri, or remove them from the parameters        
        XMLParser *state = static_cast<XMLParser*>(context);
        std::shared_ptr<XMLElement> element = state->m_pimpl->m_parsedTrace.top();

        if (state->m_pimpl->m_logParsing) {
            // Add here the optional text content
            std::string message = std::string("Content of tag: ") + element->getParsedTextContent();
            reportInfo("XMLParser", "parserCallbackEndTag", message.c_str());
            
            // TODO: reportXXX should either accept a format + var arguments or something else
            message = std::string("End of tag <") + element->name() + (">");
            reportInfo("XMLParser", "parserCallbackEndTag", message.c_str());
        }
        element->exitElementScope();
        state->m_pimpl->m_parsedTrace.pop();
        // Get a callback also to the parent, if any
        if (!state->m_pimpl->m_parsedTrace.empty()) {
            state->m_pimpl->m_parsedTrace.top()->childHasBeenParsed(element);
        }
    }
    
    void XMLParser::XMLParserPimpl::parserCallbackCharacters(void *context, const xmlChar *ch, int len)
    {
        //TODO: manage better the characters.. whitespace characters & c
        XMLParser *state = static_cast<XMLParser*>(context);
        std::shared_ptr<XMLElement> element = state->m_pimpl->m_parsedTrace.top();
        std::string parsedString = std::string(reinterpret_cast<const char*>(ch), len);
        if (state->m_pimpl->m_logParsing) {
            std::cerr << "Ch:(" << len << ") __" << parsedString << "__" << std::endl;
        }
        element->parsedCharacters(parsedString);
    }

    void XMLParser::XMLParserPimpl::parserErrorMessageCallback(void * /*context*/,
                                                               const char * message, ...)
    {
        va_list arguments;
        va_start(arguments, message);
        std::string errorMessage = stringFromFormattedCString(message, arguments);
        va_end(arguments);
        reportError("XMLParser", "[Parsing]", errorMessage.c_str());
    }

    void XMLParser::XMLParserPimpl::parserWarningMessageCallback(void * /*context*/,
                                                                 const char * message, ...)
    {
        va_list arguments;
        va_start(arguments, message);
        std::string errorMessage = stringFromFormattedCString(message, arguments);
        va_end(arguments);
        reportWarning("XMLParser", "[Parsing]", errorMessage.c_str());
    }
    
    //MARK: - XMLParser definition
    
    XMLParser::XMLParser()
    : m_pimpl(new XMLParserPimpl())
    {
        assert(m_pimpl);
        m_pimpl->f_documentFactory = [](XMLParserState& state){ return std::shared_ptr<XMLDocument>(new XMLDocument(state)); };
        m_pimpl->m_performValidation = false;
        m_pimpl->m_logParsing = false;
        m_pimpl->m_keepInMemory = false;
    }
    
    XMLParser::~XMLParser() {}
    
    void XMLParser::setDocumentFactory(std::function<std::shared_ptr<XMLDocument>(XMLParserState& state)> factory)
    {
        assert(m_pimpl);
        if (factory) {
            m_pimpl->f_documentFactory = factory;
        } else {
            // Restore the default function
            m_pimpl->f_documentFactory = [](XMLParserState& state){ return std::shared_ptr<XMLDocument>(new XMLDocument(state)); };
        }
    }

    bool XMLParser::parseXMLFile(std::string absoluteFileName)
    {
        assert(m_pimpl);
        LIBXML_TEST_VERSION

        // Reset the failure state.
        m_pimpl->m_parserState.resetState();
        
        if (m_pimpl->m_performValidation) {
            if (m_pimpl->m_schemaLocation.empty()) {
                reportError("XMLParser", "parseXMLFile", "Validation requested, but no schema has been specified");
                return false;
            }
            // Create necessary structures for the validator
            xmlSchemaParserCtxtPtr schemaParserContext = xmlSchemaNewParserCtxt(m_pimpl->m_schemaLocation.c_str());
            xmlSchemaPtr xmlSchema = xmlSchemaParse(schemaParserContext);
            xmlSchemaValidCtxtPtr validator = xmlSchemaNewValidCtxt(xmlSchema);
            
            // This is the actual validation. Note that there is the possibility to use
            // xmlSchemaSAXPlug which should perform the validation while parsing.
            // The code remains cleared in this way though, as the validation is optional.

            int validation = xmlSchemaValidateFile(validator, absoluteFileName.c_str(), 0);

            xmlSchemaFreeValidCtxt(validator);
            xmlSchemaFree(xmlSchema);
            xmlSchemaFreeParserCtxt(schemaParserContext);
            if (validation) {
                std::string message = std::string("Failed to validate ") + absoluteFileName + " for schema " + m_pimpl->m_schemaLocation;
                reportError("XMLParser", "parseXMLFile", message.c_str());
                return false;
            }
    
        }

//        xmlParserCtxtPtr parserContext = xmlCreatePushParserCtxt(m_pimpl->callbackHandler(),
//                                                                 this, NULL, 0, absoluteFileName.c_str());

        int result = xmlSAXUserParseFile(m_pimpl->callbackHandler(), this, absoluteFileName.c_str());
        return result == 0 && !m_pimpl->m_parserState.getParsingErrorState();
    }

    bool XMLParser::parseXMLString(std::string xmlString)
    {
        assert(m_pimpl);
        LIBXML_TEST_VERSION

        // Reset the failure state.
        m_pimpl->m_parserState.resetState();

        // For now we do not support validation on the fly

        int result = xmlSAXUserParseMemory(m_pimpl->callbackHandler(), this, xmlString.c_str(), xmlString.length());
        return result == 0 && !m_pimpl->m_parserState.getParsingErrorState();
    }
    
    std::shared_ptr<const XMLDocument> XMLParser::document() const
    {
        assert(m_pimpl);
        return m_pimpl->m_document;
    }
    
    bool XMLParser::validateXML() const { return m_pimpl->m_performValidation; }
    void XMLParser::setValidateXML(bool validate) { m_pimpl->m_performValidation = validate; }
    
    std::string XMLParser::schemaLocation() const { return m_pimpl->m_schemaLocation; }
    void XMLParser::setSchemaLocation(std::string schemaLocation) { m_pimpl->m_schemaLocation = schemaLocation; }
    
    bool XMLParser::logParsing() const { return m_pimpl->m_logParsing; }
    void XMLParser::setLogParsing(bool enableLogging) { m_pimpl->m_logParsing = enableLogging; }
    
    bool XMLParser::keepTreeInMemory() const { return m_pimpl->m_keepInMemory; }
    void XMLParser::setKeepTreeInMemory(bool keepTreeInMemory) { m_pimpl->m_keepInMemory = keepTreeInMemory; }

    void XMLParser::setPackageDirs(const std::vector<std::string>& packageDirs) { m_pimpl->m_packageDirs = packageDirs; }
    const std::vector<std::string>& XMLParser::packageDirs() const { return m_pimpl->m_packageDirs; }

    void XMLParserState::setParsingError() {
        std::lock_guard<std::mutex> lock(m_mutex);
        m_parsing_error = true;
    }

    void XMLParserState::resetState() {
        std::lock_guard<std::mutex> lock(m_mutex);
        m_parsing_error = false;
    }

    bool XMLParserState::getParsingErrorState() {
        std::lock_guard<std::mutex> lock(m_mutex);
        return m_parsing_error;
    }
}

