/*
 * Copyright (c) 2011-2015, Intel Corporation
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation and/or
 * other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its contributors
 * may be used to endorse or promote products derived from this software without
 * specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "XmlDocSource.h"
#include "AlwaysAssert.hpp"
#include <libxml/tree.h>
#include <libxml/xmlschemas.h>
#include <libxml/parser.h>
#include <libxml/xinclude.h>
#include <libxml/uri.h>
#include <memory>
#include <stdexcept>

using std::string;
using xml_unique_ptr = std::unique_ptr<xmlChar, decltype(xmlFree)>;

CXmlDocSource::CXmlDocSource(_xmlDoc *pDoc, bool bValidateWithSchema, _xmlNode *pRootNode)
    : _pDoc(pDoc), _pRootNode(pRootNode), _strRootElementType(""), _strRootElementName(""),
      _strNameAttributeName(""), _bValidateWithSchema(bValidateWithSchema)
{
}

CXmlDocSource::CXmlDocSource(_xmlDoc *pDoc, bool bValidateWithSchema,
                             const string &strRootElementType, const string &strRootElementName,
                             const string &strNameAttributeName)
    : _pDoc(pDoc), _pRootNode(xmlDocGetRootElement(pDoc)), _strRootElementType(strRootElementType),
      _strRootElementName(strRootElementName), _strNameAttributeName(strNameAttributeName),
      _bValidateWithSchema(bValidateWithSchema)
{
}

CXmlDocSource::~CXmlDocSource()
{
    if (_pDoc) {
        // Free XML doc
        xmlFreeDoc(_pDoc);
        _pDoc = nullptr;
    }
}

void CXmlDocSource::getRootElement(CXmlElement &xmlRootElement) const
{
    xmlRootElement.setXmlElement(_pRootNode);
}

string CXmlDocSource::getRootElementName() const
{
    return (const char *)_pRootNode->name;
}

string CXmlDocSource::getRootElementAttributeString(const string &strAttributeName) const
{
    CXmlElement topMostElement(_pRootNode);

    string attribute;
    topMostElement.getAttribute(strAttributeName, attribute);
    return attribute;
}

void CXmlDocSource::setSchemaBaseUri(const string &uri)
{
    _schemaBaseUri = uri;
}

string CXmlDocSource::getSchemaBaseUri()
{
    return _schemaBaseUri;
}

string CXmlDocSource::getSchemaUri() const
{
    // Adding a trailing '/' is a bit dirty but works fine on both Linux and
    // Windows in order to make sure that libxml2's URI handling methods
    // interpret the base URI as a folder.
    return mkUri(_schemaBaseUri + "/", getRootElementName() + ".xsd");
}

_xmlDoc *CXmlDocSource::getDoc() const
{
    return _pDoc;
}

bool CXmlDocSource::isParsable() const
{
    // Check that the doc has been created
    return _pDoc != nullptr;
}

bool CXmlDocSource::populate(CXmlSerializingContext &serializingContext)
{
    // Check that the doc has been created
    if (!_pDoc) {

        serializingContext.setError("Could not parse document ");

        return false;
    }

    // Validate if necessary
    if (_bValidateWithSchema) {
        if (!isInstanceDocumentValid()) {

            serializingContext.setError("Document is not valid");

            return false;
        }
    }

    // Check Root element type
    if (getRootElementName() != _strRootElementType) {

        serializingContext.setError("Error: Wrong XML structure document ");
        serializingContext.appendLineToError("Root Element " + getRootElementName() +
                                             " mismatches expected type " + _strRootElementType);

        return false;
    }

    if (!_strNameAttributeName.empty()) {

        string strRootElementNameCheck = getRootElementAttributeString(_strNameAttributeName);

        // Check Root element name attribute (if any)
        if (!_strRootElementName.empty() && strRootElementNameCheck != _strRootElementName) {

            serializingContext.setError("Error: Wrong XML structure document ");
            serializingContext.appendLineToError(
                _strRootElementType + " element " + _strRootElementName + " mismatches expected " +
                _strRootElementType + " type " + strRootElementNameCheck);

            return false;
        }
    }

    return true;
}

bool CXmlDocSource::isInstanceDocumentValid()
{
#ifdef LIBXML_SCHEMAS_ENABLED
    string schemaUri = getSchemaUri();

    xmlDocPtr pSchemaDoc = xmlReadFile(schemaUri.c_str(), nullptr, XML_PARSE_NONET);

    if (!pSchemaDoc) {
        // Unable to load Schema
        return false;
    }

    xmlSchemaParserCtxtPtr pParserCtxt = xmlSchemaNewDocParserCtxt(pSchemaDoc);

    if (!pParserCtxt) {

        // Unable to create schema context
        xmlFreeDoc(pSchemaDoc);
        return false;
    }

    // Get Schema
    xmlSchemaPtr pSchema = xmlSchemaParse(pParserCtxt);

    if (!pSchema) {

        // Invalid Schema
        xmlSchemaFreeParserCtxt(pParserCtxt);
        xmlFreeDoc(pSchemaDoc);
        return false;
    }
    xmlSchemaValidCtxtPtr pValidationCtxt = xmlSchemaNewValidCtxt(pSchema);

    if (!pValidationCtxt) {

        // Unable to create validation context
        xmlSchemaFree(pSchema);
        xmlSchemaFreeParserCtxt(pParserCtxt);
        xmlFreeDoc(pSchemaDoc);
        return false;
    }

    bool isDocValid = xmlSchemaValidateDoc(pValidationCtxt, _pDoc) == 0;

    xmlSchemaFreeValidCtxt(pValidationCtxt);
    xmlSchemaFree(pSchema);
    xmlSchemaFreeParserCtxt(pParserCtxt);
    xmlFreeDoc(pSchemaDoc);

    return isDocValid;
#else
    return true;
#endif
}

std::string CXmlDocSource::mkUri(const std::string &base, const std::string &relative)
{
    xml_unique_ptr baseUri(xmlPathToURI((const xmlChar *)base.c_str()), xmlFree);
    xml_unique_ptr relativeUri(xmlPathToURI((const xmlChar *)relative.c_str()), xmlFree);
    /* return null pointer if baseUri or relativeUri are null pointer  */
    xml_unique_ptr xmlUri(xmlBuildURI(relativeUri.get(), baseUri.get()), xmlFree);

    ALWAYS_ASSERT(xmlUri != nullptr, "unable to make URI from: \"" << base << "\" and \""
                                                                   << relative << "\"");

    return (const char *)xmlUri.get();
}

_xmlDoc *CXmlDocSource::mkXmlDoc(const string &source, bool fromFile, bool xincludes,
                                 CXmlSerializingContext &serializingContext)
{
    _xmlDoc *doc = nullptr;
    if (fromFile) {
        doc = xmlReadFile(source.c_str(), nullptr, 0);
    } else {
        doc = xmlReadMemory(source.c_str(), (int)source.size(), "", nullptr, 0);
    }

    if (doc == nullptr) {
        string errorMsg = "libxml failed to read";
        if (fromFile) {
            errorMsg += " \"" + source + "\"";
        }
        serializingContext.appendLineToError(errorMsg);

        return nullptr;
    }

    if (xincludes and (xmlXIncludeProcess(doc) < 0)) {
        serializingContext.appendLineToError("libxml failed to resolve XIncludes");

        xmlFreeDoc(doc);
        doc = nullptr;
    }

    return doc;
}