//
// Copyright (c) 2004 Russell E. Gibson
// email: russg@rnstech.com
//
// 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:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// 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.
//

#include <algorithm>
#include <locale>
#include "root.hpp"

using namespace root;

//=============================================================================
// Local functions

namespace
{
    // Called on reading to clear out whitespace and control characters from
    // an XML_TEXT_NODE.  The idea is to get the actual value of an element
    // rather than the whitespace used in formatting the file its in.
    xmlChar* trim(xmlChar* t)
    {
        std::locale l = std::locale();
        while (*t && (std::iscntrl(*t, l) || *t == ' '))
            ++t;
        return t;
    }
   
    // Called on reading to clear out whitespace and control characters from
    // an XML_CDATA_SECTION_NODE.  The idea is to get the actual value of an
    // unless its followed by an end of line first.
    xmlChar* trim_cdata(xmlChar* t)
    {
        std::locale l = std::locale();
        xmlChar* lt = t;
        while (*t)
        {
            // Found end of line?
            if (*t == '\r')
            {
                // Skip it
                ++t;
               
                // Is it followed by linefeed?
                if (*t != '\n')
                    return t;
               
                // Yes, so skip it too
                ++t;
                return t;
            }
           
            // Found linefed (unix eol)?
            if (*t == '\n')
                return ++t;
           
            // Is it not a whitespace or control char?
            if (!std::iscntrl(*t, l) && !std::isspace(*t, l))
                return lt;
            ++t;
        }
        return t;
    }
   
    // Functor to call write on each child element
    template <typename T>
    class writer
    {
    public:
        writer(xmlTextWriterPtr w)
                :
                _w(w)
        {
        }
        void operator()(const T& t)
        {
            t.write(_w);
        }
    private:
        xmlTextWriterPtr _w;
    };
}

//=============================================================================
// Element class Child implementation

Child::Child(const char* f)
{
    // Read in document
    xmlDocPtr d = xmlParseFile(f);
    if (!d)
        throw std::runtime_error("file parse failed");

    try
    {
        // Initialize from root node
        xmlNodePtr r = xmlDocGetRootElement(d);
        if (r)
            read(r);
   
        // Clean up
        xmlFreeDoc(d);
    }
    catch (...)
    {
        xmlFreeDoc(d);
        throw;
    }
}

Child::Child(xmlNodePtr n)
{
    // Initialize from node
    read(n);
}

Child& Child::operator=(const Child& r)
{
    if (this != &r)
    {
        _number = r._number;
        _values = r._values;
    }
    return *this;
}

void Child::read(xmlNodePtr n)
{
    // Make sure node is this element
    if (xmlStrcmp(n->name, reinterpret_cast<const xmlChar*>("child")) != 0)
        throw std::runtime_error("expecting node 'child'");
   
    // Get attributes if provided
    xmlChar* a = xmlGetProp(n, reinterpret_cast<xmlChar*>("number"));
    if (a)
    {
        _number = reinterpret_cast<const char*>(a);
        xmlFree(a);
    }
   
    // Retreive value
    for (xmlNodePtr c = n->children; c; c = c->next)
    {
        switch (c->type)
        {
        case XML_TEXT_NODE:
            {
                xmlChar* t = trim(c->content);
                if (*t)
                {
                    _values.push_back(std::make_pair(false,
                            reinterpret_cast<const char*>(t)));
                }
            }
            break;
       
        case XML_CDATA_SECTION_NODE:
            {
                xmlChar* t = trim_cdata(c->content);
                if (*t)
                {
                    _values.push_back(std::make_pair(false,
                            reinterpret_cast<const char*>(t)));
                }
            }
            break;
       
        case XML_ELEMENT_NODE:
            _values.push_back(std::make_pair(true, std::string()));
            break;
        }
    }
}

void Child::value(std::string& v) const
{
    const VALUES::const_iterator end = _values.end();
    for (VALUES::const_iterator i = _values.begin(); i != end; ++i)
        v += i->second;
}

void Child::write(const char* f, bool i, const char* t) const
{
    // Create a new writer
    xmlTextWriterPtr w = xmlNewTextWriterFilename(f, 0);
    if (!w)
        throw std::runtime_error("unable to create xmlTextWriter");
   
    // Set indentation if desired
    if (i)
    {
        xmlTextWriterSetIndent(w, 1);
        xmlTextWriterSetIndentString(w, reinterpret_cast<xmlChar*>(t ? t : "\t"));
    }
   
    try
    {
        // Create document
        int r = xmlTextWriterStartDocument(w, "1.0", "iso8859-1", "yes");
        if (r < 0)
            throw std::runtime_error("unable to start document");
   
        // Add the child elements
        write(w);
   
        // Close the writer
        xmlFreeTextWriter(w);
    }
    catch (...)
    {
        xmlFreeTextWriter(w);
        throw;
    }
}

void Child::write(xmlTextWriterPtr w) const
{
    // Create element
    int r = xmlTextWriterStartElement(w, reinterpret_cast<const xmlChar*>("child"));
    if (r < 0)
        throw std::runtime_error("creation of element 'child' failed");
   
    // Add attribute number if needed
    if (!_number.empty())
    {
        r = xmlTextWriterWriteAttribute(w, reinterpret_cast<const xmlChar*>("number"),
                reinterpret_cast<const xmlChar*>(_number.c_str()));
        if (r < 0)
        {
            throw std::runtime_error("addition of attribute 'number' "
                    "to element 'child' failed");
        }
    }
   
    // Write value if needed
    std::string v;
    value(v);
    if (!v.empty())
    {
        r = xmlTextWriterWriteString(w, reinterpret_cast<const xmlChar*>(v.c_str()));
        if (r < 0)
            throw std::runtime_error("failed writing 'child' value");
    }
   
    // All done!
    r = xmlTextWriterEndElement(w);
    if (r < 0)
        throw std::runtime_error("failed closing 'child' element");
}

//=============================================================================
// Element class Element implementation

Element::Element(const char* f)
{
    // Read in document
    xmlDocPtr d = xmlParseFile(f);
    if (!d)
        throw std::runtime_error("file parse failed");

    try
    {
        // Initialize from root node
        xmlNodePtr r = xmlDocGetRootElement(d);
        if (r)
            read(r);
   
        // Clean up
        xmlFreeDoc(d);
    }
    catch (...)
    {
        xmlFreeDoc(d);
        throw;
    }
}

Element::Element(xmlNodePtr n)
{
    // Initialize from node
    read(n);
}

Element& Element::operator=(const Element& r)
{
    if (this != &r)
    {
        _childs = r._childs;
        _values = r._values;
    }
    return *this;
}

void Element::read(xmlNodePtr n)
{
    // Make sure node is this element
    if (xmlStrcmp(n->name, reinterpret_cast<const xmlChar*>("element")) != 0)
        throw std::runtime_error("expecting node 'element'");
   
    // Read the child elements
    for (xmlNodePtr s = n->children; s; s = s->next)
    {
        // Ignore non-element children
        if (s->type != XML_ELEMENT_NODE)
            continue;
   
        if (xmlStrcmp(s->name, reinterpret_cast<const xmlChar*>("child")) == 0)
        {
            _childs.push_back(Child(s));
            continue;
        }
    }
   
    // Retreive value
    for (xmlNodePtr c = n->children; c; c = c->next)
    {
        switch (c->type)
        {
        case XML_TEXT_NODE:
            {
                xmlChar* t = trim(c->content);
                if (*t)
                {
                    _values.push_back(std::make_pair(false,
                            reinterpret_cast<const char*>(t)));
                }
            }
            break;
       
        case XML_CDATA_SECTION_NODE:
            {
                xmlChar* t = trim_cdata(c->content);
                if (*t)
                {
                    _values.push_back(std::make_pair(false,
                            reinterpret_cast<const char*>(t)));
                }
            }
            break;
       
        case XML_ELEMENT_NODE:
            _values.push_back(std::make_pair(true, std::string()));
            break;
        }
    }
}

void Element::value(std::string& v) const
{
    const VALUES::const_iterator end = _values.end();
    for (VALUES::const_iterator i = _values.begin(); i != end; ++i)
        v += i->second;
}

void Element::write(const char* f, bool i, const char* t) const
{
    // Create a new writer
    xmlTextWriterPtr w = xmlNewTextWriterFilename(f, 0);
    if (!w)
        throw std::runtime_error("unable to create xmlTextWriter");
   
    // Set indentation if desired
    if (i)
    {
        xmlTextWriterSetIndent(w, 1);
        xmlTextWriterSetIndentString(w, reinterpret_cast<xmlChar*>(t ? t : "\t"));
    }
   
    try
    {
        // Create document
        int r = xmlTextWriterStartDocument(w, "1.0", "iso8859-1", "yes");
        if (r < 0)
            throw std::runtime_error("unable to start document");
   
        // Add the child elements
        write(w);
   
        // Close the writer
        xmlFreeTextWriter(w);
    }
    catch (...)
    {
        xmlFreeTextWriter(w);
        throw;
    }
}

void Element::write(xmlTextWriterPtr w) const
{
    // Create element
    int r = xmlTextWriterStartElement(w, reinterpret_cast<const xmlChar*>("element"));
    if (r < 0)
        throw std::runtime_error("creation of element 'element' failed");
   
    // Write value if needed
    std::string v;
    value(v);
    if (!v.empty())
    {
        r = xmlTextWriterWriteString(w, reinterpret_cast<const xmlChar*>(v.c_str()));
        if (r < 0)
            throw std::runtime_error("failed writing 'element' value");
    }
   
    // Add child elements
    std::for_each(_childs.begin(), _childs.end(), writer<Child>(w));
   
    // All done!
    r = xmlTextWriterEndElement(w);
    if (r < 0)
        throw std::runtime_error("failed closing 'element' element");
}

//=============================================================================
// Element class Root implementation

Root::Root(const char* f)
{
    // Read in document
    xmlDocPtr d = xmlParseFile(f);
    if (!d)
        throw std::runtime_error("file parse failed");

    try
    {
        // Initialize from root node
        xmlNodePtr r = xmlDocGetRootElement(d);
        if (r)
            read(r);
   
        // Clean up
        xmlFreeDoc(d);
    }
    catch (...)
    {
        xmlFreeDoc(d);
        throw;
    }
}

Root::Root(xmlNodePtr n)
{
    // Initialize from node
    read(n);
}

Root& Root::operator=(const Root& r)
{
    if (this != &r)
    {
        _elements = r._elements;
        _values = r._values;
    }
    return *this;
}

void Root::read(xmlNodePtr n)
{
    // Make sure node is this element
    if (xmlStrcmp(n->name, reinterpret_cast<const xmlChar*>("root")) != 0)
        throw std::runtime_error("expecting node 'root'");
   
    // Read the child elements
    for (xmlNodePtr s = n->children; s; s = s->next)
    {
        // Ignore non-element children
        if (s->type != XML_ELEMENT_NODE)
            continue;
   
        if (xmlStrcmp(s->name, reinterpret_cast<const xmlChar*>("element")) == 0)
        {
            _elements.push_back(Element(s));
            continue;
        }
    }
   
    // Retreive value
    for (xmlNodePtr c = n->children; c; c = c->next)
    {
        switch (c->type)
        {
        case XML_TEXT_NODE:
            {
                xmlChar* t = trim(c->content);
                if (*t)
                {
                    _values.push_back(std::make_pair(false,
                            reinterpret_cast<const char*>(t)));
                }
            }
            break;
       
        case XML_CDATA_SECTION_NODE:
            {
                xmlChar* t = trim_cdata(c->content);
                if (*t)
                {
                    _values.push_back(std::make_pair(false,
                            reinterpret_cast<const char*>(t)));
                }
            }
            break;
       
        case XML_ELEMENT_NODE:
            _values.push_back(std::make_pair(true, std::string()));
            break;
        }
    }
}

void Root::value(std::string& v) const
{
    const VALUES::const_iterator end = _values.end();
    for (VALUES::const_iterator i = _values.begin(); i != end; ++i)
        v += i->second;
}

void Root::write(const char* f, bool i, const char* t) const
{
    // Create a new writer
    xmlTextWriterPtr w = xmlNewTextWriterFilename(f, 0);
    if (!w)
        throw std::runtime_error("unable to create xmlTextWriter");
   
    // Set indentation if desired
    if (i)
    {
        xmlTextWriterSetIndent(w, 1);
        xmlTextWriterSetIndentString(w, reinterpret_cast<xmlChar*>(t ? t : "\t"));
    }
   
    try
    {
        // Create document
        int r = xmlTextWriterStartDocument(w, "1.0", "iso8859-1", "yes");
        if (r < 0)
            throw std::runtime_error("unable to start document");
   
        // Add the child elements
        write(w);
   
        // Close the writer
        xmlFreeTextWriter(w);
    }
    catch (...)
    {
        xmlFreeTextWriter(w);
        throw;
    }
}

void Root::write(xmlTextWriterPtr w) const
{
    // Create element
    int r = xmlTextWriterStartElement(w, reinterpret_cast<const xmlChar*>("root"));
    if (r < 0)
        throw std::runtime_error("creation of element 'root' failed");
   
    // Write value if needed
    std::string v;
    value(v);
    if (!v.empty())
    {
        r = xmlTextWriterWriteString(w, reinterpret_cast<const xmlChar*>(v.c_str()));
        if (r < 0)
            throw std::runtime_error("failed writing 'root' value");
    }
   
    // Add child elements
    std::for_each(_elements.begin(), _elements.end(), writer<Element>(w));
   
    // All done!
    r = xmlTextWriterEndElement(w);
    if (r < 0)
        throw std::runtime_error("failed closing 'root' element");
}