/*

 BabbleMud - A MUD server

 Author:  Nick Gammon 
          http://www.gammon.com.au/ 

(C) Copyright Nick Gammon 2004. Permission to copy, use, modify, sell and
distribute this software is granted provided this copyright notice appears
in all copies. This software is provided "as is" without express or implied
warranty, and with no claim as to its suitability for any purpose.
 
*/

#include <map>
#include <list>
#include <string>
#include <iostream>
#include <algorithm>
#include <iostream>
#include <stdexcept>

using namespace std;

//
// BTW - CDATA sections look like this:

//  <![CDATA[ ... ]]> 

using namespace std;

#include "xmltree.h"
#include "xmlerrors.h"
#include "message.h"
#include "strings.h"
#include "entities.h"
#include "compose.h"    // for compose function

#define ISEQ(container) (container).begin (), (container).end ()

#define MAX_XML_DOCUMENT_SIZE (1000 * 1024)   // max XML file length - 1,000 Kb
#define MAX_XML_NAME_LENGTH 255        // max name of element (eg. <trigger> )

const char NAME_CHARS [] =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"  
    "abcdefghijklmnopqrstuvwxyz"  
    "0123456789"  
    "-._";

const char NON_PRINTABLE [] =
    "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F" 
    "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F"; 

const char NON_PRINTABLE_EXCEPT_CR_LF [] =
    "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0B\x0C\x0E\x0F" 
    "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F"; 

const char CDATA_START    [] = "<![CDATA[";
const char CDATA_END      [] = "]]>";
const char COMMENT_START  [] = "<!--";
const char COMMENT_END    [] = "-->";

// function object for finding the end of a name
::find_first_not_of fo_find_not_name (NAME_CHARS);

::find_first_of fo_find_unprintable (NON_PRINTABLE);
::find_first_of fo_find_unprintable_except_cr_lf (NON_PRINTABLE_EXCEPT_CR_LF);


void XmlParser::SkipSpaces ()
  {

  xmlPosition_ = xmlBuffer_.find_first_not_of (SPACES, xmlPosition_);

  } // end of XmlParser::SkipSpaces


void XmlParser::SkipComments (const bool bAndSpaces)
  {

  while (true)  // loop in case consecutive comments, or comment - space - comment
    {

    if (bAndSpaces)
      SkipSpaces ();

    // comment?

    if (!strPrefix (xmlBuffer_, xmlPosition_, COMMENT_START))
      return;

    // skip comment
    xmlPosition_ += strlen (COMMENT_START);

    string::size_type newpos = xmlBuffer_.find (COMMENT_END, xmlPosition_);

    if (newpos == string::npos)
       throw runtime_error (XML_ERROR_UNTERMINATED_COMMENT);

    // comments cannot have -- in the middle of them
    if (xmlBuffer_.substr (xmlPosition_, newpos - xmlPosition_).find ("--") != string::npos)
       throw runtime_error (XML_ERROR_INVALID_COMMENT);
            
    // here for end of comment

    xmlPosition_ = newpos + strlen (COMMENT_END); // skip -->

    };

  } // end of SkipComments

void XmlParser::Parse ()
  {

  // in case we re-parse, set things back to new
  xmlPosition_ = 0; // back at start
  root_.Initialise ();

  // can hardly be XML if empty file
  if (xmlBuffer_.empty ())
    throw runtime_error (XML_ERROR_ZERO_LENGTH_FILE);

  // convert tabs to spaces, we don't want tabs in our data
  replace_if (xmlBuffer_.begin (), xmlBuffer_.end (),
              bind2nd (equal_to<char> (), '\t'),
              ' ');

  ProcessNode (root_);  // process root node

  }   // end of XmlParser::Parse

/*

  Process all siblings in a node

  Special cases: 

  <?xml version="1.0" ?>
  <!DOCTYPE blah >

  Other expected cases:
   
  <node> ... contents ... </node>
  <node a='1' b='2'> ... contents </node>
  <node />
  <node a='1' b='2' />

*/


string XmlParser::GetName (const string & sType)
  {
string::size_type iStart;
string::size_type iNameLen;

  // in case spaces before next thing
  SkipSpaces ();
    
  iStart = xmlPosition_; // start of name

  if (xmlBuffer_ [xmlPosition_] == 0)
    THROW_ERROR (XML_ERROR_UNEXPECTED_EOF_1, sType);

  // name must start with letter or underscore
  if (!isalpha (xmlBuffer_ [xmlPosition_]) && xmlBuffer_ [xmlPosition_]  != '_')
      THROW_ERROR (XML_ERROR_BAD_NAME, sType << string (1, xmlBuffer_ [xmlPosition_]));

  // build up name

  const char * p = fo_find_not_name (&xmlBuffer_ [xmlPosition_]);

  if (!p)
    THROW_ERROR (XML_ERROR_UNEXPECTED_EOF_1, sType);

  xmlPosition_ = p - &xmlBuffer_ [0];

  // name length
  iNameLen = xmlPosition_ - iStart;

  // sanity check - make sure name not ridiculous
  if (iNameLen > MAX_XML_NAME_LENGTH)
    THROW_ERROR (XML_ERROR_NAME_TOO_LONG,
              sType
              << (int) iNameLen
              << MAX_XML_NAME_LENGTH);

  // in case spaces before next thing
  SkipSpaces ();

  // return name 
  return xmlBuffer_.substr (iStart, iNameLen);

  } // end of XmlParser::GetName

string XmlParser::GetValue (const string & sType, 
                            const string sName, 
                            string::size_type & iStart)
  {
char cQuote;

  // in case spaces before the value
  SkipSpaces ();

  // find what sort of quote they are using
  cQuote = xmlBuffer_ [xmlPosition_];

  if (cQuote == 0)
    THROW_ERROR (XML_ERROR_UNEXPECTED_EOF_2, sType << sName);

  if (cQuote != '\'' && cQuote != '\"')
    {
    // value starting with > is really an omitted value
    if (cQuote == '>')
      THROW_ERROR (XML_ERROR_NO_VALUE, sType);
    else
      {
      // detect a comment here as a special case
      if (strPrefix (xmlBuffer_, xmlPosition_, COMMENT_START))
        throw runtime_error (XML_ERROR_COMMENT_NOT_VALID);
      else
        THROW_ERROR (XML_ERROR_NO_VALUE, sType << sName);
      } // end of not a '>' found

    } // end of not single or double quote found

  ++xmlPosition_; // get past quote

  iStart = xmlPosition_; // start of value

  // find closing quote
  xmlPosition_ = xmlBuffer_.find (cQuote, iStart);

  // check for no closing quote
  if (xmlPosition_ == string::npos)
     THROW_ERROR (XML_ERROR_VALUE_NOT_TERMINATED, sType << sName);

  // get quoted string
  string sValue = xmlBuffer_.substr (iStart, xmlPosition_ - iStart);

  // look for non-printables - note - values *can* span newlines
  const char * p = fo_find_unprintable_except_cr_lf (sValue.c_str ());
  if (p)
      THROW_ERROR (XML_ERROR_NON_PRINTABLE_VALUE_1,
                            (int) (unsigned char) *p
                            << sType
                            << sName);
  
  // look for CDATA terminator
  if (sValue.find (CDATA_END) != string::npos)
     THROW_ERROR (XML_ERROR_NO_CDATA_TERMINATOR_1, sType << sName);

  ++xmlPosition_;   // get past closing quote

  SkipSpaces ();

  // construct value from buffer
  return sValue;

  } // end of XmlParser::GetValue 

/*

A node is <blah> (content1) <child> (content2) </child> (content3) </blah>

Thus, nodes are processed recursively.

We will store contents as blank entries in the childen list.

When we reach here we already have <blah> so we are looking for:

   * contents
   * children
   * </blah>

The name of the terminating element (</blah>) will be the name of the parent,
excepting the top-level node which will have an empty name.


*/

void XmlParser::ProcessNode (XmlNode & parent)
  {

string  strName,
        strValue,
        sContent,
        sParentName = parent.GetName ();

string::size_type iNodeStart;

  while (xmlBuffer_ [xmlPosition_])
    {

    // remember where this part starts
    iNodeStart = xmlPosition_;

    // find child (or terminating element)
    string::size_type iContent = xmlBuffer_.find ("<", xmlPosition_);
      
    // save between previous one and now as content
    if (iContent == string::npos)
      // rest of buffer is contents
      sContent = xmlBuffer_.substr (xmlPosition_);
    else
      sContent = xmlBuffer_.substr (xmlPosition_, iContent - xmlPosition_);

    // in case spaces before next thing
    SkipSpaces ();
    
    if (!sContent.empty ())
      {

      // look for non-printables
      const char * p = fo_find_unprintable_except_cr_lf (sContent.c_str ());
      if (p)
          THROW_ERROR (XML_ERROR_NON_PRINTABLE_VALUE_2,
                                (int) (unsigned char) *p);

      // look for CDATA terminator
      if (sContent.find (CDATA_END) != string::npos)
         throw runtime_error (XML_ERROR_NO_CDATA_TERMINATOR_2);
      
      parent.children_.push_back (XmlNode ("", xmlPosition_, ReplaceEntities (sContent)));
      }   // end of non-empty content
      
    // no more < - must be end of XML
    if (iContent == string::npos)
      {
      if (!sParentName.empty ())
        THROW_ERROR (XML_ERROR_UNEXPECTED_EOF_3, sParentName);
      return;      
      }

    // advance up to the <
    xmlPosition_ = iContent;

    if (strPrefix (xmlBuffer_, xmlPosition_, COMMENT_START))
      {
      SkipComments (false);
      continue;
      }   // end of comment


    if (strPrefix (xmlBuffer_, xmlPosition_, CDATA_START))
      {

      // skip CDATA_START
      xmlPosition_ += strlen (CDATA_START);

      string::size_type newpos = xmlBuffer_.find (CDATA_END, xmlPosition_);

      if (newpos == string::npos)
         throw runtime_error (XML_ERROR_UNEXPECTED_EOF_4);

      sContent = xmlBuffer_.substr (xmlPosition_, newpos - xmlPosition_);

      // look for non-printables
      const char * p = fo_find_unprintable_except_cr_lf (sContent.c_str ());
      if (p)
          THROW_ERROR (XML_ERROR_NON_PRINTABLE_VALUE_2,
                                (int) (unsigned char) *p);

      parent.children_.push_back (XmlNode ("", xmlPosition_, sContent));   // add to parent's list of children
      
      // here for end of CDATA

      xmlPosition_ = newpos + strlen (CDATA_END); // skip -->
      
      continue;
      } // end of CDATA

    ++xmlPosition_;  // skip <

    switch (xmlBuffer_ [xmlPosition_])
      {
      // check for sequence </blah>
      case '/':
        ++xmlPosition_;    // bypass /
        strName = GetName ("Element");
        if (xmlBuffer_ [xmlPosition_] != '>')
          if (xmlBuffer_ [xmlPosition_] == 0)
            THROW_ERROR (XML_ERROR_UNEXPECTED_EOF_5, strName);
          else
            THROW_ERROR (XML_ERROR_SURPLUS_CHARACTERS,
                         strName);

        if (strName != sParentName)
          if (sParentName.empty ())  // can't go up a level here :)
            THROW_ERROR (XML_ERROR_UNEXPECTED_CLOSE_TAG, strName);

          else
            THROW_ERROR (XML_ERROR_BAD_TERMINATION_SEQUENCE,
                                  sParentName
                                  << strName);

        ++xmlPosition_; // skip terminating >

        return;    // terminated this node

      case '!':
        ++xmlPosition_;    // bypass !
        strName = GetName ("Element");
        ProcessDeclaration (strName);   // process declaration, eg. <!DOCTYPE
        break;


      case '?':
        {
        string::size_type iPosition = xmlPosition_;
        ++xmlPosition_;    // bypass ?
        strName = GetName ("Element");
        XmlNode pElement = XmlNode (strName, iPosition);
        ProcessAttributes (string ("?") + strName, pElement); 
        parent.children_.push_back (pElement);   // add to parent's list of children
        }
        break;

      default:
        {
        string::size_type iPosition = xmlPosition_;
        strName = GetName ("Element");
        XmlNode pElement = XmlNode (strName, iPosition);

        // OK, we have a new sibling for our parent, let's see if it has any attributes ...

        ProcessAttributes (strName, pElement);  // process its attributes

        // hmm - we have now got something like <blah> ...
        // drop down to find children for it - unless 'empty' element

        if (!pElement.IsEmpty ())
          ProcessNode (pElement);

        parent.children_.push_back (pElement);   // add to parent's list of children
        }

        break;

      } // end of switch on cPrefix


    } // end of processing buffer

  }   // end of XmlParser::ProcessNode 


void XmlParser::ProcessAttributes (const string sNodeName, XmlNode & node)
  {
string strName;
string strValue;

  while (xmlBuffer_ [xmlPosition_] != '>')
    {
    // look for "empty element" terminator: />
    if (xmlBuffer_ [xmlPosition_] == '/' && xmlBuffer_ [xmlPosition_ + 1] == '>')
      {
      node.MarkEmpty ();
      ++xmlPosition_; // skip '/' symbol
      break;
      }

    // look for terminator ?> at end of <?xml tag
    if (strPrefix (xmlBuffer_, xmlPosition_, "?>") &&
        sNodeName [0] == '?'
        )
      {
      node.MarkEmpty ();
      ++xmlPosition_; // skip '?' symbol
      break;
      }

    // shouldn't have end-of-file before the >
    if (xmlBuffer_ [xmlPosition_] == 0)
      THROW_ERROR (XML_ERROR_UNEXPECTED_EOF_6, sNodeName);

    // we really should have a name now ...

    strName = GetName ("Attribute");

    // already there? error

    StringMap::const_iterator iter = node.attributes_.find (strName);
    if (iter != node.attributes_.end ())
      THROW_ERROR (XML_ERROR_DUPLICATE_ATTRIBUTE, strName);

    // in case spaces before next thing
    SkipSpaces ();

    if (xmlBuffer_ [xmlPosition_] != '=')
      THROW_ERROR (XML_ERROR_NO_EQUALS, strName);

    ++xmlPosition_;    // skip the = sign

    string::size_type iPos;

    strValue = GetValue ("attribute", strName, iPos);

     // add to map
    node.attributes_ [strName] = ReplaceEntities (strValue);

    } // end of looking for attributes

  ++xmlPosition_; // skip '>' symbol


  }   // end of XmlParser::ProcessAttributes

void XmlParser::ProcessDeclaration (const string strDeclaration)
  {

  if (strDeclaration == "DOCTYPE")
    ProcessDoctype ();
  else
    THROW_ERROR (XML_ERROR_BAD_DECLARATION, strDeclaration);
  } // end of XmlParser::ProcessDeclaration

void XmlParser::ProcessDoctype ()
  {

  documentName_ = GetName ("Root document");

  // document name may be followed by SYSTEM or PUBLIC

  if (isalpha (xmlBuffer_ [xmlPosition_]))
    {
    string strName = GetName ("External ID");
    
    string::size_type iPos;

    // we'll get and ignore these for future compatibility
    if (strName == "SYSTEM")
      GetValue ("SYSTEM", "DTD name", iPos);     // eg. SYSTEM "c:\blah\my.dtd"
    else
    if (strName == "PUBLIC")
      {
      GetValue ("PUBLIC", "Public ID", iPos);    // eg. PUBLIC "blah" "c:\blah\my.dtd"
      GetValue ("PUBLIC", "DTD name", iPos);
      }
    else
      THROW_ERROR (XML_ERROR_NOT_IMPLEMENTED_1, strName);


    } // end of SYSTEM/PUBLIC

  if (xmlBuffer_ [xmlPosition_] == '[')
    ProcessDTD ();
  
  if (xmlBuffer_ [xmlPosition_] != '>')
    {

    // shouldn't have end-of-file before the >
    if (xmlBuffer_ [xmlPosition_] == 0)
      throw runtime_error (XML_ERROR_UNEXPECTED_EOF_7);

    throw runtime_error (XML_ERROR_NOT_IMPLEMENTED_2);

    } // end of looking for attributes

  ++xmlPosition_; // skip '>' symbol


  }   // end of XmlParser::ProcessDoctype

void XmlParser::ProcessDTD ()
  {
string strName;

  ++xmlPosition_;    // skip the [ symbol

  SkipComments (true);

  while (xmlBuffer_ [xmlPosition_] != ']')
    {

    // shouldn't have end-of-file before the >
    if (xmlBuffer_ [xmlPosition_] == 0)
      throw runtime_error (XML_ERROR_UNEXPECTED_EOF_8);

    // look for declaration start

    if (!strPrefix (xmlBuffer_, xmlPosition_, "<!"))
      THROW_ERROR (XML_ERROR_BAD_DTD_DECLARATION,
                   xmlBuffer_ [xmlPosition_]);

    xmlPosition_ += 2;  // skip <!

    strName = GetName ("DTD Declaration");

    // we only support <!ENTITY ... > right now
    if (strName == "ENTITY")
      ProcessEntity ();
    else
      THROW_ERROR (XML_ERROR_NOT_IMPLEMENTED_3, strName);

    } // end of looking for declarations

  ++xmlPosition_; // skip ']' symbol

  SkipComments (true);

  } // end of XmlParser::ProcessDTD

void XmlParser::ProcessEntity ()
  {
string strName;
string strValue;
string strTemp;

  strName = GetName ("entity");

  // get first 3 characters
  if (ciStringEqual (strName.substr (0, 3), "XML"))
    throw runtime_error (XML_ERROR_NAME_RESERVED);

  string::size_type iPos;

  strValue = GetValue ("entity", strName, iPos);

  customEntities_ [strName] = ReplaceEntities (strValue);

  if (xmlBuffer_ [xmlPosition_] != '>')
    THROW_ERROR (XML_ERROR_EXPECTED_GREATER_SIGN, xmlBuffer_ [xmlPosition_]);

  ++xmlPosition_;   // skip >

  // bypass spaces and comments

  SkipComments (true);

  } // end of XmlParser::ProcessEntity 

// find the first named node in a list of them
list<XmlNode>::const_iterator FindNode (const list<XmlNode> & nodelist, 
                                        const string & name)
  {
  return find_if (ISEQ (nodelist),
                  boost::compose_f_gx (bind2nd (equal_to<string> (), name), 
                                      mem_fun_ref ( &XmlNode::GetName ))
                  );
  }
  
// must find the first named node in a list of them
list<XmlNode>::const_iterator NeedNode (const list<XmlNode> & nodelist, 
                                        const string & name)
  {
  list<XmlNode>::const_iterator iter = FindNode (nodelist, name);
  if (iter == nodelist.end ())
     throw runtime_error (
        MAKE_STRING ("Unable to find required XML node <" << name << ">"));
  return iter;
  }
  
void ShowAttribute (const pair<const string, const string> & attribute)

  {
  cout << "Attribute: " <<  attribute.first << " = " 
       << attribute.second << endl;

  } // end of ShowAttribute
  
void ShowElement (const XmlNode & element)
  {

  if  (!element.GetName ().empty ())
    cout << "Element: " << element.GetName () << endl;

  if (!trim (element.contents_).empty ())
    cout << "  -- contents: " << element.contents_ << endl;

  // show all attributes for this element
  for_each (ISEQ (element.attributes_), ShowAttribute);
   
  // now recurse to show children
  for_each (ISEQ (element.children_), ShowElement);

  } // end of ShowElement

 
