/* -*- mia-c++  -*-
 *
 * This file is part of MIA - a toolbox for medical image analysis
 * Copyright (c) Leipzig, Madrid 1999-2017 Gert Wollny
 *
 * MIA is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with MIA; if not, see <http://www.gnu.org/licenses/>.
 *
 */

#include <libxml/parser.h>
#include <libxml/tree.h>
#include <sstream>
#include <cstdarg>
#include <map>
#include <stack>

#include <string>

#include <mia/core/xmlinterface.hh>
#include <mia/core/msgstream.hh>
#include <mia/core/errormacro.hh>

NS_MIA_BEGIN

using std::string;
using std::stringstream;
using std::make_shared;
using std::vector;
using std::map;
using std::runtime_error;
using std::stack;
using std::pair;
using std::make_pair;
using std::unique_ptr;

struct CXMLElementImpl {
       CXMLElementImpl(const char *name);
       ~CXMLElementImpl();

       string m_name;
       std::string m_content;
       map<std::string, std::string> m_attributes;
       vector<CXMLElement::Pointer> m_children;

       xmlNodePtr write_to_node();
};


struct CXMLErrorHandler {
       CXMLErrorHandler(const std::string& context, string& last_error);
       ~CXMLErrorHandler();
       static void error_handler(void *ctx, const char *msg, ...);
private:
       string m_context;
       string& m_last_error;

};


CXMLErrorHandler::CXMLErrorHandler(const std::string& context, string& error_msg):
       m_context(context),
       m_last_error(error_msg)
{
       xmlSetGenericErrorFunc(this, (xmlGenericErrorFunc)CXMLErrorHandler::error_handler);
}

CXMLErrorHandler::~CXMLErrorHandler()
{
       xmlSetGenericErrorFunc(NULL, NULL);
}


void CXMLErrorHandler::error_handler(void *ctx, const char *msg, ...)
{
       CXMLErrorHandler *self = reinterpret_cast<CXMLErrorHandler *>(ctx);
       const int TMP_BUF_SIZE = 1024;
       char s[TMP_BUF_SIZE];
       va_list arg_ptr;
       va_start(arg_ptr, msg);
       vsnprintf(s, TMP_BUF_SIZE, msg, arg_ptr);
       va_end(arg_ptr);
       stringstream error_msg;
       error_msg << self->m_context << ":" << s;
       self->m_last_error = error_msg.str();
}


CXMLElementImpl::CXMLElementImpl(const char *name):
       m_name(name)
{
}

CXMLElementImpl::~CXMLElementImpl()
{
}

xmlNodePtr CXMLElementImpl::write_to_node()
{
       auto this_node = xmlNewNode(nullptr, BAD_CAST m_name.c_str());

       // add attributes
       for (auto a : m_attributes)
              xmlNewProp(this_node, BAD_CAST a.first.c_str(),  BAD_CAST a.second.c_str());

       // add content
       // missing: add encoding special chars
       if (!m_content.empty()) {
              xmlNodePtr text = xmlNewText(BAD_CAST m_content.c_str());
              xmlAddChild(this_node, text);
       }

       // add children
       for (auto child : m_children) {
              auto child_node = child->impl->write_to_node();
              xmlAddChild(this_node, child_node );
       }

       return this_node;
}


CXMLElement::CXMLElement(const char *name):
       impl(new CXMLElementImpl(name))
{
}

CXMLElement::~CXMLElement()
{
       delete impl;
}


CXMLElement::Pointer CXMLElement::add_child(const char *name)
{
       Pointer result = make_shared<CXMLElement>(name);
       impl->m_children.push_back(result);
       return result;
}

void CXMLElement::set_child_text(const std::string& value)
{
       assert(impl);
       impl->m_content += value;
}

void CXMLElement::set_attribute(const char *name, const std::string& value)
{
       assert(impl);
       impl->m_attributes[name] = value;
}

const string& CXMLElement::get_name() const
{
       assert(impl);
       return impl->m_name;
}

const string CXMLElement::get_attribute(const string& name) const
{
       auto ip = impl->m_attributes.find(name);

       if (ip != impl->m_attributes.end()) {
              return ip->second;
       }

       return string();
}

const vector<CXMLElement::Pointer>& CXMLElement::get_all_children() const
{
       assert(impl);
       return impl->m_children;
}

vector<CXMLElement::Pointer> CXMLElement::get_children(const char *name) const
{
       assert(impl);
       vector<CXMLElement::Pointer> result;

       for (auto c :  impl->m_children) {
              if (c->get_name() == name)
                     result.push_back(c);
       }

       return result;
}

const std::string& CXMLElement::get_content() const
{
       return impl->m_content;
}



CXMLElement::Pointer CXMLDocument::create_root_node(const char *name)
{
       m_root = CXMLElement::Pointer(new CXMLElement(name));
       return m_root;
}

CXMLElement::Pointer CXMLDocument::get_root_node() const
{
       return m_root;
}


CXMLDocument::CXMLDocument()
{
}

CXMLDocument::CXMLDocument(const char *init)
{
       if (!read_from_string(init)) {
              throw runtime_error(m_last_error);
       }
}


struct xmlDocDeleter {
       void operator() (xmlDoc *p)
       {
              xmlFreeDoc(p);
       }
};

bool CXMLDocument::read_from_string(const char *init)
{
       CXMLErrorHandler error_handler("XML Reader", m_last_error);
       unique_ptr<xmlDoc,  xmlDocDeleter> doc(xmlParseDoc(reinterpret_cast<const xmlChar *>(init)));

       if (!doc)
              return false;

       auto root = xmlDocGetRootElement(doc.get());

       if (!root)
              return false;

       m_root  = make_shared<CXMLElement>(reinterpret_cast<const char *>(root->name));
       {
              xmlChar *content = xmlNodeGetContent(root);

              if (content) {
                     const char *c = reinterpret_cast<const char *>(content);
                     m_root->set_child_text(c);
                     xmlFree(content);
              }

              // read attributes
              xmlAttr *attribute = root->properties;

              while (attribute) {
                     const char *name = reinterpret_cast<const char *>(attribute->name);
                     xmlChar *value = xmlNodeListGetString(root->doc, attribute->children, 1);
                     const char *v = reinterpret_cast<const char *>(value);
                     m_root->set_attribute(name, v);
                     xmlFree(value);
                     attribute = attribute->next;
              }
       }
       std::stack<pair<CXMLElement::Pointer, xmlNodePtr>> s;
       s.push(make_pair(m_root, root->children));
       cvdebug() << "Parse document\n";

       while (!s.empty() && m_last_error.empty()) {
              auto current_level = s.top();
              s.pop();

              for (auto n = current_level.second; n; n = n->next) {
                     if (n->type != XML_ELEMENT_NODE)
                            continue;

                     cvdebug() << s.size() << "  Got node '" << n->name
                               << "' of type " << n->type << "\n";
                     auto name = reinterpret_cast<const char *>(n->name);
                     auto node = current_level.first->add_child(name);
                     cvdebug() << s.size() << "Add child node to "
                               << current_level.first->get_name() << ": " << name << "\n";
                     // read content
                     xmlChar *content = xmlNodeGetContent(n);

                     if (content) {
                            const char *c = reinterpret_cast<const char *>(content);
                            cvdebug() << s.size() << "  Got content '" << c << "'\n";
                            node->set_child_text(c);
                            xmlFree(content);
                     }

                     // read attributes
                     xmlAttr *attribute = n->properties;

                     while (attribute) {
                            const char *name = reinterpret_cast<const char *>(attribute->name);
                            xmlChar *value = xmlNodeListGetString(n->doc, attribute->children, 1);
                            const char *v = reinterpret_cast<const char *>(value);
                            cvdebug() << s.size() << "  Got attribute '" << name
                                      << "', value='" << value << "'\n";
                            node->set_attribute(name, v);
                            xmlFree(value);
                            attribute = attribute->next;
                     }

                     if (n->children) {
                            s.push(make_pair(node, n->children));
                     }
              }
       }

       return m_last_error.empty();
}


std::string CXMLDocument::write_to_string(const char *encoding, bool formatted) const
{
       string error_msg;
       CXMLErrorHandler error_handler("XML Writer", error_msg);
       xmlChar *doc_txt_ptr = NULL;
       int doc_txt_len = 0;
       unique_ptr<xmlDoc,  xmlDocDeleter> doc(xmlNewDoc(BAD_CAST "1.0"));
       xmlNodePtr root = m_root->impl->write_to_node();
       xmlDocSetRootElement(doc.get(), root);
       xmlKeepBlanksDefault(0);

       if (encoding)
              xmlDocDumpFormatMemoryEnc(doc.get(),
                                        &doc_txt_ptr,
                                        &doc_txt_len,
                                        encoding,
                                        formatted ? 1 : 0);
       else
              xmlDocDumpFormatMemory(doc.get(),
                                     &doc_txt_ptr,
                                     &doc_txt_len,
                                     formatted ? 1 : 0);

       stringstream out_string;
       out_string << doc_txt_ptr;
       free(doc_txt_ptr);

       if (!error_msg.empty())
              throw runtime_error(error_msg);

       return out_string.str();
}

NS_MIA_END
