// Copyright (C) 2010, Guy Barrand. All rights reserved.
// See the file tools.license for terms.

#ifndef toolx_xml_loader
#define toolx_xml_loader

#include <tools/xml/tree>
#include <tools/file>
#include <tools/fileis>
#include <tools/mnmx>

#include <cstdio>
#include <cctype> //iscntrl

#include <expat.h>

namespace toolx {
namespace xml {

class loader {
public:
  loader(tools::xml::factory& a_factory,
         std::ostream& a_out,bool a_verbose = false)
  :m_factory(a_factory)
  ,m_out(a_out)
  ,m_verbose(a_verbose)
  ,m_take_cntrl(false)

  ,m_errors(0)
  ,m_top(0) // Used to cleanup in case XML parsing failed.
  ,m_current(0)
  ,m_compressed_reader(0)
  ,m_depth(0)
  ,m_abort(false)
  {
  }

  virtual ~loader(){
    delete m_compressed_reader;
    clear();
  }

protected:
  loader(const loader& a_from)
  :m_factory(a_from.m_factory)
  ,m_out(a_from.m_out)
  ,m_verbose(a_from.m_verbose)
  ,m_take_cntrl(a_from.m_take_cntrl)

  ,m_errors(0)
  ,m_top(0) // Used to cleanup in case XML parsing failed.
  ,m_current(0)
  ,m_compressed_reader(0)
  ,m_depth(0)
  ,m_abort(false)
  {
  }
  loader& operator=(const loader& a_from){
    if(&a_from==this) return *this;

    m_verbose = a_from.m_verbose;
    m_take_cntrl = a_from.m_take_cntrl;

    m_errors = 0;
    m_top = 0;
    m_current = 0;
    m_compressed_reader = 0;
    m_depth = 0;
    m_abort = false;

    return *this;
  }

public:
  virtual bool visit_end_element(tools::xml::tree&,bool& a_keep) {
    a_keep = true;
    return true;
  }

public:
  void set_take_cntrl_chars(bool a_value) {m_take_cntrl = a_value;}

  std::ostream& out() const {return m_out;}
  void set_compressed_reader(tools::file::reader* aReader){
    delete m_compressed_reader;
    m_compressed_reader = aReader; //take ownership.
  }

  unsigned int errors() const {return m_errors;}

  void set_tags(const std::vector<std::string>& a_tags){m_tags=a_tags;}
  void add_tag(const std::string& a_tag){m_tags.push_back(a_tag);}

  bool load_file(const std::string& a_file,bool a_compressed) {
    clear();
    if(!parse_file(a_file,
                    (XML_StartElementHandler)start_element,
                    (XML_EndElementHandler)end_element,
                    this,a_compressed)) {
      clear();
      return false;
    }
    if(m_current) m_current->set_file(a_file);
    return true;
  }

  bool load_string(const std::string& a_string){
    clear();
    if(!parse_buffer(a_string.size(),a_string.c_str(),
                    (XML_StartElementHandler)start_element,
                    (XML_EndElementHandler)end_element,
                    this)) {
      clear();
      return false;
    }
    return true;
  }

  bool load_buffer(size_t aSize,const char* aBuffer){
    clear();
    if(!parse_buffer(aSize,aBuffer,
                    (XML_StartElementHandler)start_element,
                    (XML_EndElementHandler)end_element,
                    this)) {
      clear();
      return false;
    }
    return true;
  }

  const tools::xml::tree* top_item() const {return m_current;}

  tools::xml::tree* top_item(){return m_current;}

  void empty(){m_top = 0;m_current = 0;}

  bool is_tag(const std::string& a_string) const {
    size_t number = m_tags.size();
    for(size_t index=0;index<number;index++) {
      if(a_string==m_tags[index]) return true;
    }
    return false;
  }

protected:
  void clear(){
    // In case of problem, deleting m_current is not sufficient.
    delete m_top;
    m_top = 0;
    m_current = 0;
  }

  bool parse_buffer(size_t aSize,const char* aBuffer,
                    XML_StartElementHandler a_start,XML_EndElementHandler a_end,
                    void* a_tag){
    m_errors = 0;
    if(!aSize) return true; //nothing to do.
    m_depth = 0;
    m_abort = false;

    XML_Parser _parser = XML_ParserCreate(NULL);

    XML_SetUserData(_parser,a_tag);
    XML_SetElementHandler(_parser,a_start,a_end);
    XML_SetCharacterDataHandler(_parser,(XML_CharacterDataHandler)character_data_handler);
    char* buf = (char*)aBuffer;
    size_t l = aSize;
    int done = 0;
    do {
      size_t len = tools::min_of<size_t>(l,BUFSIZ); //BUFSIZ in cstdio
      done = len < BUFSIZ ? 1 : 0;
      if(XML_Parse(_parser, buf, (int)len, done)==XML_STATUS_ERROR) {
        m_out << "parse_buffer :"
            << " " << XML_ErrorString(XML_GetErrorCode(_parser))
            << " at line " << (int)XML_GetCurrentLineNumber(_parser)
            << " at byte index " << (int)XML_GetCurrentByteIndex(_parser)
            << std::endl;
       {XML_Index pos = XML_GetCurrentByteIndex(_parser);
        XML_Index pmn = tools::max_of<XML_Index>(pos-10,0);
        XML_Index pmx = tools::min_of<XML_Index>(pos+10,XML_Index(aSize)-1);
        std::string c = " ";
       {for(XML_Index p=pmn;p<=pmx;p++) {c[0] = *(aBuffer+p);m_out << c;}
        m_out << std::endl;}
       {for(XML_Index p=pmn;p<pos;p++) m_out << " ";
        m_out << "^" << std::endl;}}
        XML_ParserFree(_parser);
        return false;
      }
      if(m_abort) {
        XML_ParserFree(_parser);
        return false;
      }
      buf += len;
      l -= len;
    } while (!done);
    XML_ParserFree(_parser);
    return true;
  }

  bool parse_file(const std::string& a_file,
                  XML_StartElementHandler a_start,XML_EndElementHandler a_end,
                  void* a_tag,bool a_compressed){
    if(m_verbose) {
      m_out << "parse_file :"
            << " parse file " << tools::sout(a_file) << "..." << std::endl;
    }
    m_errors = 0;

    bool use_zlib = false;
    if(a_compressed) {
      if(m_verbose) {
        m_out << "parse_file :"
              << " uncompress requested for file "
              << tools::sout(a_file) << "."
              << std::endl;
      }
      use_zlib = true;
    } else {
      // may be compressed anyway :
      bool compressed;
      if(!tools::file::is_gzip(a_file,compressed)) {
        m_out << "parse_file :"
              << " tools::file::is_gzip() failed for " << a_file << "."
              << std::endl;
        return false;
      }
      if(compressed) use_zlib = true;
    }

    tools::file::reader* freader = 0;
    bool delete_freader = false;
    if(use_zlib) {
      if(!m_compressed_reader) {
        m_out << "parse_file :"
              << " no compressed reader given."
              << std::endl;
        return false;
      }
      freader = m_compressed_reader;
    } else {
      freader = new tools::FILE_reader();
      delete_freader = true;
    }
    if(!freader->open(a_file)) {
      m_out << "parse_file :"
            << " can't open file " << a_file << std::endl;
      if(delete_freader) delete freader;
      return false;
    }

    m_depth = 0;
    m_abort = false;

    XML_Parser _parser = XML_ParserCreate(NULL);

    XML_SetUserData(_parser,a_tag);
    XML_SetElementHandler(_parser,a_start,a_end);
    XML_SetCharacterDataHandler(_parser,(XML_CharacterDataHandler)character_data_handler);
    char buf[BUFSIZ];
    int done = 0;
    do {
      size_t len;
      if(!freader->read(buf,sizeof(buf),len)) {
        XML_ParserFree(_parser);
        freader->close();
        if(delete_freader) delete freader;
        return false;
      }
      done = len < sizeof(buf) ? 1 : 0;
      if(XML_Parse(_parser, buf, (int)len, done)==XML_STATUS_ERROR) {
        m_out << "parse_file :"
            << " in file " << tools::sout(a_file)
            << " " << XML_ErrorString(XML_GetErrorCode(_parser))
            << " at line " << (int)XML_GetCurrentLineNumber(_parser)
            << std::endl;
        XML_ParserFree(_parser);
        freader->close();
        if(delete_freader) delete freader;
        return false;
      }
      if(m_abort) {
        XML_ParserFree(_parser);
        freader->close();
        if(delete_freader) delete freader;
        return false;
      }
    } while (!done);
    XML_ParserFree(_parser);
    freader->close();
    if(m_verbose) {
      m_out << "parse_file :"
          << " parse file " << tools::sout(a_file) << " done." << std::endl;
    }
    if(delete_freader) delete freader;
    return true;
  }

protected:
  static void character_data_handler(void* aUserData,const XML_Char* a_string,int aLength){
    loader* This = (loader*)aUserData;
    std::string _s;
    _s.resize(aLength);
    size_t count = 0;
    char* p = (char*)a_string;
    for (int i = 0; i < aLength; i++, p++) {
      if(This->m_take_cntrl || (!iscntrl(*p))) {
        _s[count] = *p;
        count++;
      }
    }
    if(count) {
      _s.resize(count);
      This->m_value += _s;
    }
  }

  static void start_element(void* aUserData,const XML_Char* a_name,const XML_Char** a_atbs){
    loader* This = (loader*)aUserData;
    if(This->m_abort) return; //Do nothing.

    This->m_depth++;
    This->m_value = "";

    std::string name = a_name; //Can't be empty
    if(This->is_tag(name)) {

      if(!This->m_current) {
        if(This->m_depth==1) {
          // Ok. Head.
        } else {
          This->m_out << "start_element :"
              << " no tag with a depth of " << This->m_depth
              << std::endl;
          This->m_abort = true;
          return;
        }
      } else {
        int delta = This->m_current->depth() - This->m_depth;
        if(delta>=1) {
          This->m_out << "start_element :"
              << " for element " << tools::sout(name)
              << " tag with a delta depth of " << delta
              << std::endl;
          This->m_abort = true;
          return;
        }
      }

      std::vector<tools::xml::tree::atb> atbs;
     {const XML_Char** a_atts = a_atbs;
      while((*a_atts)&&(*(a_atts+1))) {
        atbs.push_back(tools::xml::tree::atb(*a_atts,*(a_atts+1)));
        a_atts+=2;
      }}

      tools::xml::tree* parent = This->m_current;
      tools::xml::tree* _tree = This->m_factory.create(name,atbs,parent);
      if(!_tree) {
        This->m_out << "start_element :"
            << " can't create a tree for tag " << tools::sout(name)
            << std::endl;
        This->m_abort = true;
        return;
      }

      if(parent) parent->add_child(_tree);

      This->m_current = _tree;
      _tree->set_depth(This->m_depth); // Internal only.

      if(!This->m_top) This->m_top = _tree;

    } else {

      if(!This->m_current) {

        // Can't be in a non-tag without a tag !
        This->m_out << "start_element :"
            << " for element " << tools::sout(name)
            << " non-tag without some parent tag."
            << std::endl;
        This->m_abort = true;
        return;

      } else {

        int delta =  This->m_depth - This->m_current->depth();
        if(delta>1) {

          This->m_out << "start_element :"
              << " for element " << tools::sout(name)
              << " grand child of a tag."
              << std::endl;
          This->m_abort = true;
          return;

        } else if(delta==1) { //ok

          This->m_atbs.clear();
         {const XML_Char** a_atts = a_atbs;
          while((*a_atts)&&(*(a_atts+1))) {
            This->m_atbs.push_back(tools::xml::tree::atb(*a_atts,*(a_atts+1)));
            a_atts+=2;
          }}

        } else {

          This->m_out << "start_element :"
              << " for element " << tools::sout(name)
              << " non-tag with a delta depth of " << delta
              << std::endl;
          This->m_abort = true;
          return;

        }
      }

    }
  }


  static void end_element(void* aUserData,const XML_Char* a_name){
    loader* This = (loader*)aUserData;
    if(This->m_abort) return; //Do nothing.

    if(This->m_current) {

      tools::xml::tree* tr = This->m_current;
      int delta = This->m_depth - tr->depth();
      if(delta==0) { //Back to a tag :

        tools::xml::tree* parent = tr->parent();

        bool keep = false;
        bool cont = This->visit_end_element(*tr,keep);
        if(keep) {
          if(parent) {
            This->m_current = parent;
          }
        } else {
          //FIXME : the top could be recreated !
          if(This->m_top==tr) This->m_top = 0;

          if(parent) {
            parent->remove_child(tr); //delete the tr
          } else {
            delete tr;
          }

          This->m_current = parent; //parent could be 0 : ok.
        }

        if(!cont) This->m_abort = true;

      } else if(delta==1) { //Back to a child of tag :

        //FIXME : correct m_value ? (Can we pick the one of a sub item ?)
        tr->add_element(std::string(a_name),This->m_atbs,This->m_value);

      } else {

        This->m_out << "end_element :"
            << " problem for element " << tools::sout(std::string(a_name))
            << " : delta depth of " << delta
            << std::endl;
        This->m_abort = true;

      }

    }


    This->m_depth--;
  }

protected:
  tools::xml::factory& m_factory;
  std::ostream& m_out;
protected:
  bool m_verbose;
  bool m_take_cntrl;
  unsigned int m_errors;
  std::vector<std::string> m_tags;
  tools::xml::tree* m_top;
  tools::xml::tree* m_current;
  //std::vector<tools::xml::tree::atb> m_atbs;
  std::vector< std::pair<std::string,std::string> > m_atbs;
  std::string m_value;
  tools::file::reader* m_compressed_reader;
  unsigned int m_depth;
  bool m_abort;
};

}}

#endif
