/***************************************************************************
**
**  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: 2004-10-20
**  Copyright: 2004-2019
**    Marc Wathelet
**    Marc Wathelet (ULg, Liège, Belgium)
**    Marc Wathelet (LGIT, Grenoble, France)
**
***************************************************************************/

#include "XMLClass.h"
#include "XMLParser.h"
#include "CoreApplication.h"
#include "QGpCoreToolsVersion.h"
#include "Version.h"
#include "MemoryChecker.h"

namespace QGpCoreTools {

const QString XMLClass::openBeginTagStr="<";
const QString XMLClass::closeBeginTagStr="</";
const QString XMLClass::endTagStr=">";
const QString XMLClass::closeEndTagStr="/>";
const QString XMLClass::trueStr="true";
const QString XMLClass::falseStr="false";
int XMLClass::fileIndex=1000;
const QString XMLClass::binDataTag="binDataFile";
const QString XMLClass::binData200510Tag="binDataOffset";

const XMLSaveAttributes XMLClass::nullSaveAttributes;

/*!
  \class XMLMember XMLClass.h
  \brief XMLMember is used by XMLClass to receive the type of tag to parse

  XMLMember is used only in re-implemetations of
  XMLClass::xml_member(StringSection& tag, XMLRestoreAttributes& map)
*/

/*!
  \fn XMLMember::XMLMember(XMLMember::Unknown)

  Return an XMLMember constructed with this constructor if the member is not valid
*/

/*!
  \fn XMLMember::XMLMember(int memberID)

  Return an XMLMember constructed with this constructor if the member is a property
*/

/*!
  \fn XMLMember::XMLMember(XMLClass * child, bool temp)

  Return an XMLMember constructed with this constructor if the member is another XMLClass object (child).

  If \a temp is true the \a child is automatically destroyed once it is restored. \a temp is false by default.
*/

/*!
  \class XMLClass XMLClass.h
  \brief XMLClass is a parser for a simplified XML syntax
 
*/

/*!
  \fn void XMLClass::xml_writeBinaryData(QDataStream& s) const

  This function must be re-implemented in all classes dealing with binary data
  that cannot be saved in an ASCII xml file (e.g. due to the amount of data).

  The way binary data is stored drastically changed in November 2006 with the
  introduction of tar.gz structures for xml files. Each class willing to store
  binary data can automatically generate a new file (with an automatic file name) in the
  .tar.gz structure by sending bytes to s.

  See also xml_setBinaryData().

  For the arguments of this function use Macro XML_WRITEBINARYDATA_ARGS.
*/

/*!
  \fn bool XMLClass::xml_setBinaryData(QDataStream& s)

  This function must be re-implemented in all classes dealing with binary data
  that cannot be saved in an ASCII xml file (e.g. due to the amount of data).

  See also xml_writeBinaryData().

  The difference between xml_setBinaryData() and xml_setBinaryData200410()
  is detected by the type of tag at the beginning of each block if it can be read with
  QString ==> 200510, else try with a normal C string, if it match the current tag
  then execute xml_setBinaryData200411().

  See also xml_setBinaryData200411() to maintain compatibility
  with previous versions of xml storages.

  For the arguments of this function use Macro XML_SETBINARYDATA_ARGS.
*/

/*!
  \fn bool XMLClass::xml_setBinaryData200411(QDataStream& s)

  This function must be re-implemented in all classes dealing with binary data
  that cannot be saved in an ASCII xml file (e.g. due to the amount of data).

  See also xml_setBinaryData().

  For the arguments of this function use Macro XML_SETBINARYDATA_ARGS.

  This function must not used in new codes. This is only kept for compatibility
  reasons, to be able to read all xml.bin files generated by releases before
  November 2006.
*/

/*!
  \fn XMLMember XMLClass::xml_member(StringSection& tag, XMLRestoreAttributes& map)

  Re-implement this function to offer XML restore (children and properties) support to your class.

  From \a tag and \a map (with contains the attibute value) return a unique identifier under
  the format of a XMLMember. XMLMember is initialized with 3 types of contructors:

  \li An integer: id number of a property
  \li A XMLClass * : a child of this object identified by \a tag
  \li Default constructor: error, unknow child or property

  Map of attributes can be inspected in this way (can be achived also in xml_setProperty()):

  \code
    static const QString tmp("childrenName");
    XMLRestoreAttributeIterator it=map.find(tmp);
    if(it!=map.end()) {
      // found attribute "childrenName"
    }
  \endcode

  If the map of attributes is not used:

  \code
    Q_UNUSED(attributes)
    if(tag=="x1") return XMLMember(0);
    else if(tag=="y1") return XMLMember(1);
    else if(tag=="x2") return XMLMember(2);
    else if(tag=="y2") return XMLMember(3);
    else return XMLMember(XMLMember::Unknown);
  \endcode

  Arithmetic operations + and - apply to XMLMember to avoid confusion of property id numbers
  between inherited objects. Offset 3 corresponds to the number of properties defined in this object.

  \code
    if(tag=="anInteger") return XMLMember(0);
    else if(tag=="aString") return XMLMember(1);
    else if(tag=="aDouble") return XMLMember(2);
    return AbstractLine::xml_member(tag, attributes, context)+3;
  \endcode

  For the arguments of this function use Macro XML_MEMBER_ARGS.
*/

/*!
  \fn XMLMember XMLClass::xml_setProperty(int memberID, XMLRestoreAttributes& map, StringSection& content)

  Re-implement this function to offer XML restore properties support to your class.

  From \a memberID set the corresponding property with value \a content. The \a map of attributes
  is given as a supplementary information (not useful in all cases).

  For a general case:

  \code
  Q_UNUSED(attributes)
  double val=content.toDouble();
  switch (memberID) {
  case 0:
    _x1=val;
    return true;
  case 1:
    _y1=val;
    return true;
  case 2:
    _x2=val;
    return true;
  case 3:
    _y2=val;
    return true;
  default:
    return false;
  }
  \endcode

  For classes inheriting other classes (see also xml_member())
  
  \code
  switch (memberID) {
  case 0:
    _anInteger=content.toString();
    return true;
  case 1:
    _aString=content.toInt();
    return true;
  case 2:
    _aDouble=content.toDouble();
    return true;
  default:
    return AbstractLine::xml_setProperty(memberID-3, map, content);
  \endcode

  For the arguments of this function use Macro XML_SETPROPERTY_ARGS.
*/

IMPL_XML_WRITE_PROPERTY(const char *, saveSpecChar(content))
IMPL_XML_WRITE_PROPERTY(QString, saveSpecChar(content))
IMPL_XML_WRITE_PROPERTY(QChar, saveSpecChar(content))
IMPL_XML_WRITE_PROPERTY(int, QString::number(content))
IMPL_XML_WRITE_PROPERTY(uint, QString::number(content))
IMPL_XML_WRITE_PROPERTY(qint64, QString::number(content))
IMPL_XML_WRITE_PROPERTY(double, QString::number(content, 'g', 20))
IMPL_XML_WRITE_PROPERTY(Complex, content.toString('g', 20))
IMPL_XML_WRITE_PROPERTY(bool, content ? trueStr : falseStr)
IMPL_XML_WRITE_PROPERTY_ATT(const char *, saveSpecChar(content))
IMPL_XML_WRITE_PROPERTY_ATT(QString, saveSpecChar(content))
IMPL_XML_WRITE_PROPERTY_ATT(QChar, saveSpecChar(content))
IMPL_XML_WRITE_PROPERTY_ATT(int, QString::number(content))
IMPL_XML_WRITE_PROPERTY_ATT(uint, QString::number(content))
IMPL_XML_WRITE_PROPERTY_ATT(qint64, QString::number(content))
IMPL_XML_WRITE_PROPERTY_ATT(double, QString::number(content, 'g', 20))
IMPL_XML_WRITE_PROPERTY_ATT(Complex, content.toString('g', 20))
IMPL_XML_WRITE_PROPERTY_ATT(bool, content ? trueStr : falseStr)

void XMLClass::writeProperty(XMLStream& s, QString name, const XMLSaveAttributes& attributes)
{
  TRACE;
  QString tmp=s.indent();
  tmp+=openBeginTagStr;
  tmp+=name;
  tmp+=attributes.toEncodedString();
  tmp+=closeEndTagStr;
  tmp+="\n";
  s << tmp;
}

/*!
  Convenient function to "manually" write some XML content
*/
void XMLClass::writeChildren(XMLStream& s, QString name, const XMLSaveAttributes& attributes, QString children)
{
  TRACE;
  QString tmp=s.indent();
  tmp+=openBeginTagStr;
  tmp+=name;
  tmp+=attributes.toEncodedString();
  tmp+=endTagStr;
  tmp+="\n";
  tmp+=children;
  tmp+=s.indent() + closeBeginTagStr;
  tmp+=name;
  tmp+=endTagStr;
  tmp+="\n";
  s << tmp;
}

/*!
  May be useful for classes that do not inherit QObject.
  Must be re-implemented in sub-classes
*/
bool XMLClass::xml_inherits(const QString& tagName) const
{
  return tagName=="XMLClass";
}

/*!
  \fn XMLClass::xml_save(XMLStream &s, QString attributes=QString()) const
  All output must execute this function that add header and footer to the XML
*/

/*!
  Called by each child to export its data into the XML stream.

  \li s            current active XML stream
  \li attributes   any string with format keyword1="value1" keyword2="value2" ..., may be null.

  Do not call this function directly with an empty QTextStream: the produced XML will not contain the header.
*/
void XMLClass::xml_save(XMLStream& s, XMLContext * context, const XMLSaveAttributes& attributes) const
{
  TRACE;
  QString tmp=s.indent();
  tmp+=openBeginTagStr;
  tmp+=xml_tagName();
  tmp+=attributes.toEncodedString();
  tmp+=endTagStr;
  tmp+="\n";
  s << tmp;

  s.incrementIndent();
  xml_writeProperties(s, context);
  xml_writeChildren(s, context);
  s.decrementIndent();

  tmp=s.indent();
  tmp+=closeBeginTagStr;
  tmp+=xml_tagName();
  tmp+=endTagStr;
  tmp+="\n";
  s << tmp;
}

XMLClass::Error XMLClass::restore(const QChar *& ptr, int& line, XMLStream * s, XMLContext * context)
{
  XMLParser parser(s, context);
  parser.setLine(line);
  parser.setPointer(ptr);
  return parser.parse(this);
}

void XMLClass::skipBlanks(const QChar *& ptr, int& line)
{
  TRACE;
  while( *ptr!=0) {
    switch (ptr->unicode()) {
    case 0x000A:    // '\n'
      line++;
    case 0x0020:    // ' '
    case 0x0009:    // '\t'
    case 0x000D:    // '\r'
      break;
    default:
      return ;
    }
    ptr++;
  }
}

/*!
  This function is called when special characters are suspected. The
  following special characters are replaced by XML sequences (see
  http://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references for
  a list XML predefined entities):

  Replace:
  \li '&amp;' by '\&amp;'
  \li '&lt;' by '\&lt;'
  \li '&gt;' by '\&gt;'
  \li '&quot;' by '\&quot;'
  \li '&apos;' by '\&apos;'

  Replacements are inlined due the small number of special characters to replace
  and to avoid time losses.
*/
QString XMLClass::saveSpecChar(QString content)
{
  TRACE;
  QString dest;
  const QChar * ptr=content.data();
  StringSection cur (ptr, 0);
  while(true) {
    switch (ptr->unicode()) {
    case 0x0000:
      if(cur.data()==content.data()) { // no special character found
        return content;
      } else {
        cur.setEnd(ptr);
        cur.appendTo(dest);
        //App::log("Content before replacements ---" << content << "---" << endl;
        //App::log("Content after replacements ---" << dest << "---" << endl;
        return dest;
      }
    case 0x0026:    // '&'
      cur.setEnd(ptr);
      cur.appendTo(dest);
      dest += "&amp;";
      cur.set(ptr + 1, 0);
      break;
    case 0x003C:    // '<'
      cur.setEnd(ptr);
      cur.appendTo(dest);
      dest += "&lt;";
      cur.set(ptr + 1, 0);
      break;
    case 0x003E:    // '>'
      cur.setEnd(ptr);
      cur.appendTo(dest);
      dest += "&gt;";
      cur.set(ptr + 1, 0);
      break;
    case 0x0027:    // '''
      cur.setEnd(ptr);
      cur.appendTo(dest);
      dest += "&apos;";
      cur.set(ptr + 1, 0);
      break;
    case 0x0022:    // '"'
      cur.setEnd(ptr);
      cur.appendTo(dest);
      dest += "&quot;";
      cur.set(ptr + 1, 0);
      break;
    default:
      break;
    }
    ptr++;
  }
}

void XMLClass::writeBinaryData(XMLStream & s, XMLContext * context) const
{
  TRACE;
  if(s.isMultiFile()) {
    QString fileName=QString("bin_data_%1").arg(fileIndex++);
    writeProperty(s, binDataTag, fileName);
    QByteArray binArray;
    QDataStream binStream(&binArray, QIODevice::WriteOnly);
    binStream << xml_tagName();
    xml_writeBinaryData(binStream, context);
    s.addFile(fileName, binArray);
  }
}

/*!
  Reports errors to user.
*/
QString XMLClass::message(Error err, const QString& file, int line)
{
  TRACE;
  QString msg;
  switch (err) {
  case NoError:
    return QString();
  case ErrorFileNotOpen:
    msg=tr("Cannot open file. Check file permissions");
    break;
  case ErrorWritingFile:
    msg=tr("Cannot write to file. Check disk space");
    break;
  case ErrorNoDocType:
    msg=tr("No doc type found at the beginning of the XML");
    break;
  case ErrorNoVersion:
    msg=tr("No version found at the beginning of the XML");
    break;
  case ErrorUnmatchedAmpSemiColon:
    msg=tr("Unmatched ampersand and semi colon for XML special characters");
    break;
  case ErrorUnknowSpecialCharacter:
    msg=tr("Unknwown XML special character");
    break;
  case ErrorEmptyTag:
    msg=tr("Empty XML tag");
    break;
  case ErrorUnmatchedTag:
    msg=tr("Unmatched XML tag");
    break;
  case ErrorUnmatchedTopLevelTag:
    msg=tr("Unmatched top level XML tag");
    break;
  case ErrorEmptyContextStack:
    msg=tr("Empty context stack");
    break;
  case ErrorParsingContent:
    msg=tr("Error parsing tag content");
    break;
  case ErrorParsingAttributes:
    msg=tr("Error parsing tag attributes");
    break;
  case ErrorSettingAttributes:
    msg=tr("Error setting tag attributes");
    break;
  case ErrorSettingBinaryData:
    msg=tr("Error setting binary data");
    break;
  case ErrorWrongBinaryOffset:
    msg=tr("Wrong offset in binary file");
    break;
  case ErrorWrongBinaryTag:
    msg=tr("Wrong binary tag");
    break;
  case ErrorBinaryFileNotFound:
    msg=tr("Binary file not found");
    break;
  case ErrorEndTruncatedTag:
    msg=tr("Truncated tag at the end of XML");
    break;
  case ErrorEndTruncatedString:
    msg=tr("Truncated string at the end of XML");
    break;
  case ErrorEndTruncatedContext:
    msg=tr("Truncated context at the end of XML");
    break;
  case ErrorEndStillInside:
    msg=tr("Object not completely restored at the end of XML");
    break;
  case ErrorEndTagNotFound:
    msg=tr("Object not found in XML");
    break;
  case ErrorPolish:
    msg=tr("Error while polishing the object");
    break;
  }
  if(!file.isEmpty()) {
    if(line>0) {
      msg+=tr(" (%1 at line %2).").arg(file).arg(line);
    } else {
      msg+=tr(" (%1).").arg(file);
    }
  } else if(line>0) {
    msg+=tr(" (at line %1).").arg(line);
  } else {
    msg+=".";
  }
  return msg;
}

/*!
  This function is designed to save as XML properties all QObject properties.
*/
void XMLClass::qobject_writeProperties(const QObject * o, const XMLClass * xmlo, XML_WRITEPROPERTIES_ARGS)
{
  TRACE;
  Q_UNUSED(context)
  const QMetaObject * meta=o->metaObject();
  // Ignore property with index 0 (objectName from QObject, replaced by objectName of the first base class from  library)
  int n=meta->propertyCount();
  for(int i=meta->indexOfProperty("objectName"); i<n; i++) {
    QMetaProperty prop=meta ->property(i);
    if(prop.isStored()) {
      QVariant val=prop.read(o);
      if(val.type()==QVariant::String) {
        xmlo->writeProperty(s, prop.name(), val.toString());
      } else {
        xmlo->writeProperty(s, prop.name(), val.toString());
      }
    }
  }
}

/*!
  This function is designed to restore from XML properties to QObject properties.
*/
XMLMember XMLClass::qobject_member(QObject * o, XML_MEMBER_ARGS)
{
  TRACE;
  Q_UNUSED(attributes)
  Q_UNUSED(context)
  const QMetaObject * meta=o->metaObject();
  int id=meta->indexOfProperty(tag.toString().toLatin1().data());
  if(id==-1) {
    return XMLMember(XMLMember::Unknown);
  } else {
    return XMLMember(id);
  }
}

/*!
  This function is designed to restore from XML properties to QObject properties.
*/
bool XMLClass::qobject_setProperty(QObject * o, XML_SETPROPERTY_ARGS)
{
  TRACE;
  Q_UNUSED(tag)
  Q_UNUSED(attributes)
  Q_UNUSED(context)
  const QMetaObject * meta=o->metaObject();
  QMetaProperty p=meta->property(memberID);
  if(!p.isValid()) {
    return false;
  }
  if(!p.isWritable()) {
    App::log(tr("read-only property : %1\n").arg(p.name()));
    return false;
  }
  if(p.type()==QVariant::Bool) {
    QVariant v(content=="true" );
    return p.write(o, v);
  } else {
    QVariant v(content.toString());
    return p.write(o, v);
  }
}

} // namespace QGpCoreTools
