/***************************************************************************
**
**  This file is part of QGpCoreTools.
**
**  This library is free software; you can redistribute it and/or
**  modify it under the terms of the GNU Lesser General Public
**  License as published by the Free Software Foundation; either
**  version 2.1 of the License, or (at your option) any later version.
**
**  This file 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 Lesser General Public
**  License for more details.
**
**  You should have received a copy of the GNU Lesser General Public
**  License along with this library; if not, write to the Free Software
**  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
**
**  See http://www.geopsy.org for more information.
**
**  Created: 2009-06-07
**  Copyright: 2009-2019
**    Marc Wathelet
**    Marc Wathelet (LGIT, Grenoble, France)
**
***************************************************************************/

#include "XMLParser.h"
#include "CoreApplication.h"
#include "Trace.h"
#include "Global.h"
#include "MemoryChecker.h"

namespace QGpCoreTools {

/*!
  \class XMLParser XMLParser.h
  \brief Brief description of class still missing

  Full description of class still missing
*/

/*!
  Description of constructor still missing
*/
XMLParser::XMLParser(XMLStream * s, XMLContext * context)
    :  _current(context),
    _member(XMLMember::Unknown),
    _stream(s)
{
  _line=1;
}

/*!
  Description of destructor still missing
*/
XMLParser::~XMLParser()
{
  qDeleteAll(_tmpStrings);
}

void XMLParser::finish(XMLClass::Error err)
{
  _error=err;
  if(_error!=XMLClass::NoError) {
    QString fileName;
    if(_stream) {
      fileName=_stream->fileName();
    }
    App::log(XMLClass::message(_error, fileName, _line)+"\n");
  }
  // Set pointer just before the end to force exit.
  while(_ptr->unicode()!='\0') _ptr++;
  _ptr--;
}

XMLClass::Error XMLParser::parse(XMLClass * object)
{
  _current.setObject(object);
//#define XML_DEBUG
#ifdef XML_DEBUG
  printf("Entering context %s\n", _current->xml_tagName().toLatin1().data());
#endif
  _error=XMLClass::NoError;
  // In case restore is called for an XML item with no children, just content
  // For normal situations this should not happen. It is mostly used by GenericItem and XMLDummy
  _content.set(_ptr, 0);
  _specChar=nullptr;
  /* The buffer may start with other data that must be skipped while not inside the right context.
     The buffer is parsed until it finds a root tag corresponding to tagName */
  while(true) {
    switch (_ptr->unicode()) {
    case 0x000A:    // '\n'
      _line++;
      break;
    case 0x0026: { // '&'
        if(!_specChar) _specChar=_ptr; // keep track of first occurence of & in content
        skipSpecChar();
      }
      break;
    case 0x003C: {    // '<'
        _content.setEnd(_ptr);
        setTag();
        switch(_tagType) {
        case Attributes:
          qDeleteAll(_tmpStrings);
          _tmpStrings.clear();
          _attributes.clear();
          setAttributes();
          switch(_tagType) {
          case OpenTagAttributes:
#ifdef XML_DEBUG
            printf("%5i=%5i === <%s att>\n", __LINE__, _line, _tag.toString().toLatin1().data());
#endif
            openContext();
            break;
          case SingleTagAttributes:
#ifdef XML_DEBUG
            printf("%5i=%5i === <%s att/>\n", __LINE__, _line, _tag.toString().toLatin1().data());
#endif
            smallContext();
            break;
          default:
            break;
          }
          break;
        case OpenTagNoAttributes:
          qDeleteAll(_tmpStrings);
          _tmpStrings.clear();
          _attributes.clear();
#ifdef XML_DEBUG
          printf("%5i=%5i === <%s>\n", __LINE__, _line, _tag.toString().toLatin1().data());
#endif
          openContext();
          break;
        case CloseTag:
#ifdef XML_DEBUG
          printf("%5i=%5i === %s</%s>\n", __LINE__, _line, _content.toString().toLatin1().data(),
                                              _tag.toString().toLatin1().data());
#endif
          closeContext();
          break;
        case SingleTagNoAttributes:
          qDeleteAll(_tmpStrings);
          _tmpStrings.clear();
          _attributes.clear();
#ifdef XML_DEBUG
          printf("%5i=%5i === <%s/>\n", __LINE__, _line, _tag.toString().toLatin1().data());
#endif
          smallContext();
          break;
        default:
          break;
        }
      }
      break;
    case 0x0000:    // END
      if(_error!=XMLClass::NoError) {
      } else if(_tag.isEmpty()) {
        finish(XMLClass::ErrorEndTruncatedTag);
      } else if(_tagStack.size() > 2) {
        finish(XMLClass::ErrorEndTruncatedContext);
      } else if(!_current.object()->xml_polished()) {
        finish(XMLClass::ErrorEndTagNotFound);
      }
      return _error;
    default:
      break;
    }
    _ptr++;
  }
}

void XMLParser::openContext()
{
  _member=_current.object()->xml_member(_tag, _attributes, _current.context());
#ifdef XML_DEBUG
  printf("%5i=%5i === xml_member(%s) for '%s': %i, %p\n", __LINE__, _line, _tag.toString().toLatin1().data(),
                                                            _current->xml_tagName().toLatin1().data(),
                                                            _member.memberID(), _member.child());
#endif
  if(_member.isValidMember()) {
    // Does nothing, attributes are already parsed, waiting for content and closing tag.
  } else if(_member.child()) {
    XMLContext * newContext=_member.context(_current.context());
    if(!_member.child()->xml_setAttributes(_attributes, newContext)) {
      finish(XMLClass::ErrorSettingAttributes);
      return;
    }
    _objectStack.push(_current);
    _current=Object(newContext, _member.child());
#ifdef XML_DEBUG
    printf("%5i=%5i =O= Enter object %s\n", __LINE__, _line, _current->xml_tagName().toLatin1().data());
#endif
  } else { // Bad member name... skipping or checking for binDataTag
    if(_tag!=XMLClass::binDataTag && _tag!=XMLClass::binData200510Tag) {
      // Voluntary skipping member, issue no warning
      if(!_member.isSkipMember()) {
        App::log(tr("Unknown tag at line %1 for context %2: %3\n")
          .arg(_line)
          .arg(currentContextTag())
          .arg(_tag.toString()));
      }
      skip();
      return;
    }
  }
  _content.set(_ptr + 1, 0);
  _specChar=nullptr;
  _tagStack.push(Tag(_tag,_member));
}

void XMLParser::closeContext()
{
  if(_tagStack.size() > 0) {
    if(_tag==_tagStack.top().tag()) {
      _member=_tagStack.top().member();
      _tagStack.pop();
    } else {
      App::log(tr("Looking for tag '%1', found '%2'\n")
                    .arg(currentContextTag())
                    .arg(_tag.toString()));
      finish(XMLClass::ErrorUnmatchedTag);
      return;
    }
  } else {
    finish(XMLClass::ErrorEmptyContextStack);
    return;
  }
  // At this point we are sure that open/close tags match
  if(_content.isValid()) {
    if(_member.isValidMember()) {
      if(_specChar) {
        restoreSpecChar();
        if(_error!=XMLClass::NoError) return;
      }
      if(!_current.object()->xml_setProperty(_member.memberID(), _tag, _attributes, _content, _current.context())) {
        finish(XMLClass::ErrorParsingContent);
        return;
      }
      _member.reset();
      _content.setInvalid();
    } else if(_tag==XMLClass::binDataTag && _stream->isMultiFile()) {
      restoreBinaryData();
    } else if(_tag==XMLClass::binData200510Tag) {  // Compatibility with version < 1.2.0 (November 2006)
      restoreBinaryData200510();
    } else {
      // We are in a situation where a "parent" item has in fact no member specified in XML stream.
      // Used by XMLGenericItem, in this case before closing the current context we set properties
#ifdef XML_DEBUG
      printf("%5i=%5i =O= Leaving object %s\n", __LINE__, _line, _current->xml_tagName().toLatin1().data());
#endif
      if(_specChar) {
        restoreSpecChar();
        if(_error!=XMLClass::NoError) return;
      }
      // We ignore return value: for XMLGenericItem, this function returns always true
      // For other objects, e.g. an empty "parent" item: there are good chances that this
      // function will return false (default implementation).
      _current.object()->xml_setProperty(_member.memberID(), _tag, _attributes, _content, _current.context());
      if(!_current.object()->xml_polish(_current.context())) {
        finish(XMLClass::ErrorPolish);
        return;
      }
      Object parent=_objectStack.pop();
      parent.object()->xml_polishChild(_current.object(), _current.context());
      _member.release();
      _current=parent;
      _content.setInvalid();
    }
  } else if(_member.child()) {
    // Usual exit of current context
#ifdef XML_DEBUG
    printf("%5i=%5i =O= Leaving object %s\n", __LINE__, _line, _current->xml_tagName().toLatin1().data());
#endif
    if(!_current.object()->xml_polish(_current.context())) {
      finish(XMLClass::ErrorPolish);
      return;
    }
    Object parent=_objectStack.pop();
    parent.object()->xml_polishChild(_current.object(), _current.context());
    _member.release();
    _current=parent;
    _content.setInvalid();
  } else if(_tagStack.size()==0) {
#ifdef XML_DEBUG
    printf("%5i === Exiting context %s (Success, context stack empty)\n", __LINE__, _current->xml_tagName().toLatin1().data());
#endif
    finish(XMLClass::NoError);
  }
}

void XMLParser::smallContext()
{
#ifdef XML_DEBUG
  printf("%5i === Member <%s/>\n", __LINE__, _tag.toString().toLatin1().data());
#endif
  // The current context is definitively not a just a property with a valid content
  _content.setInvalid();
  _member=_current.object()->xml_member(_tag, _attributes, _current.context());
  XMLContext * newContext=_member.context(_current.context());
  if(_member.isValidMember()) {
    if(!_current.object()->xml_setProperty(_member.memberID(), _tag, _attributes, _content, newContext)) {
      finish(XMLClass::ErrorParsingContent);
    }
  } else if(_member.child()) {
    if(!_member.child()->xml_setAttributes(_attributes, newContext)) {
      finish(XMLClass::ErrorSettingAttributes);
      return;
    }
    _current.object()->xml_polishChild(_member.child(), newContext);
    _member.release();
  }
}

QString XMLParser::currentContextTag()
{
  return _tagStack.size()>0 ? _tagStack.top().tag().toString() : "[top level]";
}

void XMLParser::restoreBinaryData()
{
  if(_specChar) {
    restoreSpecChar();
    if(_error!=XMLClass::NoError) return;
  }
  QByteArray binData;
  if(_stream->file(_content.toString(), binData)) {
    QDataStream binStream(&binData, QIODevice::ReadOnly);
    QString binTag;
    binStream >> binTag;
    if(_current.object()->xml_tagName()==binTag) {
      if(!_current.object()->xml_setBinaryData(binStream, _current.context())) {
        finish(XMLClass::ErrorSettingBinaryData);
      }
    } else {
      App::log(tr("Expecting tag %1, and found %2\n").arg(_current.object()->xml_tagName()).arg(binTag) );
      finish(XMLClass::ErrorWrongBinaryTag);
    }
  } else {
    finish(XMLClass::ErrorBinaryFileNotFound);
  }
}

void XMLParser::restoreBinaryData200510()
{
  if(_specChar) {
    restoreSpecChar();
    if(_error!=XMLClass::NoError) return;
  }
  // Move to pointer specified by XML tag
  qint64 offset=_content.toLongLong();
  QFile f(_stream->fileName()+".bin" );
  if(f.open(QIODevice::ReadOnly)) {
    QDataStream binStream(&f);
    f.seek(offset);
    /* For historical reasons the tag in binary files is set in ASCII encoding
      First try to read it with Unicode and then try with ascii if failed
    */
    QString binTag;
    binStream >> binTag;
    if(_current.object()->xml_tagName()==binTag) {
      if(!_current.object()->xml_setBinaryData(binStream, _current.context())) {
        finish(XMLClass::ErrorSettingBinaryData);
      }
    } else { // Compatibility with preceding version of XMLClass (between Nov 2004 and Oct 2005)
      // Reset file pointer
      f.seek(offset);
      // Read again tag
      int n=_current.object()->xml_tagName().size()+1;
      char * binTag=new char[ n ];
      binStream.readRawData(binTag, sizeof(char) * n);
      if(_current.object()->xml_tagName().toLatin1()!=binTag) {
        finish(XMLClass::ErrorWrongBinaryOffset);
      } else {
        if( !_current.object()->xml_setBinaryData200411(binStream, _current.context())) {
          finish(XMLClass::ErrorSettingBinaryData);
        }
      }
      delete [] binTag;
    }
  } else {
    finish(XMLClass::ErrorBinaryFileNotFound);
  }
}

/*!
  This function is called when special character sequences are encountered.
  It simply checks if the sequence is terminated by a ';' and does not contain
  any LT. Return true if successful, else return false and send a message to
  the log stream.
*/
void XMLParser::skipSpecChar()
{
  int line0=_line;
  while(true) {
    switch (_ptr->unicode()) {
    case 0:
      _line=line0;
      finish(XMLClass::ErrorUnmatchedAmpSemiColon);
      return;
    case 0x000A:    // '\n'
      _line++;
      break;
    case 0x0022:    // '"'
    case 0x0027:    // '\''
      finish(XMLClass::ErrorUnmatchedAmpSemiColon);
      return;
    case 0x003C:    // '\<'
      finish(XMLClass::ErrorUnmatchedAmpSemiColon);
      return;
    case 0x003B:    // ';'
      return;
    }
    _ptr++;
  }
}

/*!
  \a specChar must be null. It contains the pointer to the first occurence of a special character
  in the sting if any.
*/
void XMLParser::skipDoubleQuoteString()
{
  TRACE;
  int line0=_line;
  while(true) {
    switch (_ptr->unicode()) {
    case '\0':
      if(_error!=XMLClass::NoError) {
        _ptr--; // Remain just before the end
        return;
      }
      _line=line0;
      finish(XMLClass::ErrorEndTruncatedString);
      return;
    case 0x005C:    // '\\'
      _ptr++;
      if(_ptr->unicode()=='\0') {
        if(_error!=XMLClass::NoError) {
          _ptr--; // Remain just before the end
          return;
        }
        _line=line0;
        finish(XMLClass::ErrorEndTruncatedString);
      }
      break;
    case 0x000A:    // '\n'
      _line++;
      break;
    case 0x0026: { // '&'
        if( !_specChar) _specChar=_ptr; // keep track of first occurence of & in string
        skipSpecChar();
      }
      break;
    case 0x0022:    // '"'
      return;
    }
    _ptr++;
  }
}

/*!
  \a specChar must be null. It contains the pointer to the first occurence of a special character
  in the sting if any.
*/
void XMLParser::skipSimpleQuoteString()
{
  TRACE;
  int line0=_line;
  while(true) {
    switch (_ptr->unicode()) {
    case '\0':
      if(_error!=XMLClass::NoError) {
        _ptr--; // Remain just before the end
        return;
      }
      _line=line0;
      finish(XMLClass::ErrorEndTruncatedString);
      return;
    case 0x005C:    // '\\'
      _ptr++;
      if(_ptr->unicode()=='\0') {
        if(_error!=XMLClass::NoError) {
          _ptr--; // Remain just before the end
          return;
        }
        _line=line0;
        finish(XMLClass::ErrorEndTruncatedString);
      }
      break;
    case 0x000A:    // '\n'
      _line++;
      break;
    case 0x0026: { // '&'
        if( !_specChar) _specChar=_ptr; // keep track of first occurence of & in string
        skipSpecChar();
      }
      break;
    case 0x0027:    // '\''
      return;
    }
    _ptr++;
  }
}

/*!
  This function is called when at least one special character sequence has been found
  in the content. \a specChar is a pointer to the first occurence, hence *specChar=='&'.

  The general buffer is modified by this function, it is not possible to retrieve
  the original general buffer except by calling saveSpecChar()

  Return ErrorUnknowSpecialCharacter if \a specChar cannot be recognized.
*/
void XMLParser::restoreSpecChar()
{
  //App::log("Content before replacements ---" << content.toString() << "---" << endl;
  StringSection cur(_content.data(), 0);
  StringSection code;
  QString& tmp=*new QString;
  const QChar * ptr=_specChar;
  const QChar * endPtr=_content.data() + _content.size();
  bool ok=true;
  while(ptr < endPtr) {
    switch (ptr->unicode()) {
    case 0x003B:    // ';'
      if(code.isValid()) {
        code.setEnd(ptr);
        tmp+=specialCharacter(code, ok);
        if(!ok) {
          finish(XMLClass::ErrorUnknowSpecialCharacter);
          delete &tmp;
          return;
        }
        cur.set(ptr + 1, 0);
        code.setInvalid();
      }
      break;
    case 0x0026:    // '&'
      cur.setEnd(ptr);
      cur.appendTo(tmp);
      code.set(ptr + 1, 0);
      break;
    default:
      break;
    }
    ptr++;
  }
  cur.setEnd(ptr);
  cur.appendTo(tmp);
  _tmpStrings.append(&tmp);
  _content.set(tmp.data(), tmp.size());
  //App::log("Content after replacements ---" << restoredBuf << "---" << endl;
}

/*!
  _ptr points to first character '<'
*/
void XMLParser::setTag()
{
  _ptr++;
  _tag.set(_ptr, 0);
  while(true) {
    switch (_ptr->unicode()) {
    case '\0':
      finish(XMLClass::ErrorEndTruncatedTag);
      return;
    case '\n':
      _tag.setEnd(_ptr);
      _tagType=Attributes;
      _line++;
      return;
    case '\r':
    case '\t':
    case ' ':
      _tag.setEnd(_ptr);
      _tagType=Attributes;
      return;
    case '?':
    case '!':
      if(_ptr[-1].unicode()=='<') {
        setTag();
        if(_ptr->unicode()=='>') {
          _tagType=QuestionTagNoAttributes;
        } else {
          _tagType=QuestionTagAttributes;
        }
      } else if(_ptr[1].unicode()=='>') {
      } else {
        finish(XMLClass::ErrorEmptyTag);
        _tagType=Unknown;
      }
      return;
    case '/':
      if(_ptr[-1].unicode()=='<') {
        setTag();
        _tagType=CloseTag;
        return;
      }
      if(_ptr[1].unicode()!='>') {
        finish(XMLClass::ErrorEmptyTag);
        _tagType=Unknown;
        return;
      }
      _tag.setEnd(_ptr);
      if(_tag.isEmpty()) finish(XMLClass::ErrorEmptyTag);
      _ptr++;
      _tagType=SingleTagNoAttributes;
      return;
    case '>':
      _tag.setEnd(_ptr);
      if(_tag.isEmpty()) finish(XMLClass::ErrorEmptyTag);
      _tagType=OpenTagNoAttributes;
      return;
    case '<':
      finish(XMLClass::ErrorEmptyTag);
      _tagType=Unknown;
      return;
    default:
      break;
    }
    _ptr++;
  }
}

void XMLParser::setAttributes()
{
  StringSection attributeName;
  attributeName.set(_ptr, 0);
  while(true) {
    switch (_ptr->unicode()) {
    case '\0':
      _tagType=Unknown;
      if(_error!=XMLClass::NoError) {
        _ptr--; // Remain just before the end
        return;
      }
      finish(XMLClass::ErrorEndTruncatedTag);
      return;
    case '\n':
      _line++;
      break;
    case '/':
      if(_ptr[1].unicode()!='>') {
        finish(XMLClass::ErrorParsingAttributes);
        _tagType=Unknown;
        return;
      }
      attributeName.setEnd(_ptr);
      attributeName.trimmed();
      if(!attributeName.isEmpty()) {
        finish(XMLClass::ErrorParsingAttributes);
        _tagType=Unknown;
        return;
      }
      _tagType=SingleTagAttributes;
      return;
    case '>':
      attributeName.setEnd(_ptr);
      attributeName.trimmed();
      if(!attributeName.isEmpty()) {
        finish(XMLClass::ErrorParsingAttributes);
        _tagType=Unknown;
        return;
      }
      _tagType=OpenTagAttributes;
      return;
    case '=':
      attributeName.setEnd(_ptr);
      attributeName.trimmed();
      _ptr++;
      XMLClass::skipBlanks(_ptr, _line);
      switch(_ptr->unicode()) {
      case '"':
        _ptr++;
        _content.set(_ptr, 0);
        _specChar=0;
        skipDoubleQuoteString();
        break;
      case '\'':
        _ptr++;
        _content.set(_ptr, 0);
        _specChar=0;
        skipSimpleQuoteString();
        break;
      default:
        App::log(tr("No value given for attribute %1 at line %2\n").arg(attributeName.toString()).arg(_line) );
        _error=XMLClass::ErrorParsingAttributes;
        _tagType=Unknown;
        return;
      }
      _content.setEnd(_ptr);
      if(_specChar) {
        restoreSpecChar();
      }
      _attributes.insert(attributeName, _content);
      attributeName.set(_ptr + 1, 0);
      break;
    default:
      break;
    }
    _ptr++;
  }
}

/*!
  Table for converting special character codes into QChar. \a ok is set to false in case of error.
*/
QChar XMLParser::specialCharacter(const StringSection& code, bool& ok)
{
  TRACE;
  if(code.size()<2) {
    ok=false;
    return QChar();
  }
  switch(code[0].unicode()) {
  case 'a':
    switch(code[1].unicode()) {
    case 'g':
      if(code=="agrave") return 0x00E0;
      break;
    case 'm':
      if(code=="amp") return '&';
      break;
    case 'p':
      if(code=="apos") return '\'';
      break;
    default:
      break;
    }
    break;
  case 'e':
    switch(code[1].unicode()) {
    case 'a':
      if(code=="eacute") return 0x00E9;
      break;
    }
    break;
  case 'g':
    if(code=="gt") return '>';
    break;
  case 'l':
    if(code=="lt") return '<';
    break;
  case 'q':
    if(code=="quot") return '"';
    break;
  case '#':
    if(code[1]=='x') {
      QString c(code.data()+2, code.size()-2);
      return QChar(c.toInt(&ok, 16));
    } else {
      QString c(code.data()+1, code.size()-1);
      return QChar(c.toInt(&ok, 10));
    }
  default:
    break;
  }
  ok=false;
  return QChar();
}

/*!
  _ptr points to first character '>' (End of an opening tag, no stacked)
*/
void XMLParser::skip()
{
  int skippingLevel=_tagStack.count();
  _tagStack.push(_tag);
#ifdef XML_DEBUG
  printf("%5i === Skipping context %s, context size %i at line %i\n", __LINE__, _tag.toString().toLatin1().data(), skippingLevel, _line);
#endif
  _ptr++;
  while(true) {
    switch (_ptr->unicode()) {
    case 0x000A:    // '\n'
      _line++;
      break;
    case 0x0026:    // '&'
      skipSpecChar();
      break;
    case 0x003C: {    // '<'
        _content.setEnd(_ptr);
        setTag();
        switch(_tagType) {
        case Attributes:
          _attributes.clear();
          qDeleteAll(_tmpStrings);
          _tmpStrings.clear();
          setAttributes();
          switch(_tagType) {
          case OpenTagAttributes:
            _tagStack.push(_tag);
            break;
          case SingleTagAttributes:
            // nothing to do just continue
            break;
          default:
            break;
          }
          break;
        case OpenTagNoAttributes:
          _tagStack.push(_tag);
          break;
        case CloseTag:
          if(_tagStack.size() > 0) {
            if(_tag==_tagStack.top().tag()) {
              _tagStack.pop();
            } else {
              App::log(tr("Looking for tag '%1', found '%2'\n")
                .arg(currentContextTag())
                .arg(_tag.toString()));
              finish(XMLClass::ErrorUnmatchedTag);
              return;
            }
          } else {
            finish(XMLClass::ErrorEmptyContextStack);
            return;
          }
          if(_tagStack.size()==skippingLevel) {
#ifdef XML_DEBUG
            printf("%5i === End skipping context %s, context size %i at line %i\n", __LINE__, _tag.toString().toLatin1().data(), skippingLevel, _line);
#endif
            return;
          }
          break;
        case SingleTagNoAttributes:
          // nothing to do just continue
          break;
        default:
          break;
        }
      }
      break;
    case 0x0000:    // END
      if(_error!=XMLClass::NoError) {
      } else if(_tag.isEmpty()) {
        finish(XMLClass::ErrorEndTruncatedTag);
      } else if(_tagStack.size() > 2) {
        finish(XMLClass::ErrorEndTruncatedContext);
      } else if(!_current.object()->xml_polished()) {
        finish(XMLClass::ErrorEndTagNotFound);
      }
      return;
    default:
      break;
    }
    _ptr++;
  }
}

} // namespace QGpCoreTools
