/* BEGIN software license
 *
 * msXpertSuite - mass spectrometry software suite
 * -----------------------------------------------
 * Copyright(C) 2009,...,2018 Filippo Rusconi
 *
 * http://www.msxpertsuite.org
 *
 * This file is part of the msXpertSuite project.
 *
 * The msXpertSuite project is the successor of the massXpert project. This
 * project now includes various independent modules:
 *
 * - massXpert, model polymer chemistries and simulate mass spectrometric data;
 * - mineXpert, a powerful TIC chromatogram/mass spectrum viewer/miner;
 *
 * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
 *
 * END software license
 */


/////////////////////// Local includes
#include <globals/globals.hpp>
#include <massxpert/nongui/Modif.hpp>
#include <massxpert/nongui/PolChemDef.hpp>
#include <massxpert/nongui/Monomer.hpp>


namespace msXpSmassXpert
{

//! Constructs a modification.
/*!

  \param polChemDef Polymer chemistry definition. Cannot be 0.

  \param name Name. Cannot be empty.

  \param formula formula. Defaults to the null string.
  */
Modif::Modif(const PolChemDef *polChemDef, QString name, QString formula)
  : PolChemDefEntity(polChemDef, name),
    Formula(formula),
    Ponderable(0, 0),
    m_targets("*"),
    m_maxCount(1)
{
  // By default a modification will target any monomer.  By default
  // the modification will be settable only once at maximum to a
  // given monomer entity.
}


//! Constructs a copy of \p other.
/*!  \param other modifications to be used as a mold.
 */
Modif::Modif(const Modif &other)
  : PolChemDefEntity(other),
    Formula(other),
    Ponderable(other),
    m_targets(other.m_targets),
    m_maxCount(other.m_maxCount)
{
}


Modif::~Modif()
{
}


//! Creates a new modification.
/*! The newly created modification is initialized using \c this.  The
  property list is not initialized, which means that the
  initialization is shallow.

  \return The newly created modification, which should be deleted when
  no longer used.
  */
Modif *
Modif::clone() const
{
  Modif *other = new Modif(*this);

  return other;
}


//! Modifies \p other to be identical to \p this.
/*!  \param other modification.
 */
void
Modif::clone(Modif *other) const
{
  Q_ASSERT(other);

  other->m_targets  = m_targets;
  other->m_maxCount = m_maxCount;

  PolChemDefEntity::clone(other);
  Formula::clone(other);
  Ponderable::clone(other);
}


//! Modifies \p this  to be identical to \p other.
/*!  \param other modification to be used as a mold.
 */
void
Modif::mold(const Modif &other)
{
  if(&other == this)
    return;

  m_targets  = other.m_targets;
  m_maxCount = other.m_maxCount;

  PolChemDefEntity::mold(other);
  Formula::mold(other);
  Ponderable::mold(other);
}


//! Assigns other to \p this modification.
/*! \param other modification used as the mold to set
  values to \p this instance.

  \return a reference to \p this modification.
  */
Modif &
Modif::operator=(const Modif &other)
{
  if(&other != this)
    mold(other);

  return *this;
}


//! Reset the modification to an empty object.
void
Modif::reset()
{
  m_name = "NOT_SET";

  m_formula.clear();
  m_plusFormula.clear();
  m_minusFormula.clear();

  // When m_targets is empty, the modification cannot modify
  // anything. Useful for testing purposes.
  m_targets.clear();
  m_maxCount = 1;

  while(!m_atomCountList.isEmpty())
    delete m_atomCountList.takeFirst();

  m_mono = 0;
  m_avg  = 0;

  while(!m_propList.isEmpty())
    delete m_propList.takeFirst();
}


QString &
Modif::setTargets(QString targets)
{
  if(targets.isEmpty())
    {
      m_targets.clear();
      return m_targets;
    }

  //   qDebug() << __FILE__ << __LINE__
  // 	    << "Before unspacification" << targets;

  // Remove any space from 'targets'.
  m_targets = msXpS::unspacifyString(targets);

  //   qDebug() << __FILE__ << __LINE__
  // 	    << "After unspacification" << m_targets;

  // Validate and simplify: true is by default the bool param.
  if(!validateTargets())
    m_targets = QString();

  return m_targets;
}


QString
Modif::targets() const
{
  return m_targets;
}


int
Modif::targets(QStringList &stringList) const
{
  // Return a string list after splitting at ';'.

  stringList.clear();

  stringList = m_targets.split(';', QString::SkipEmptyParts, Qt::CaseSensitive);
  return stringList.size();
}


bool
Modif::hasMonomerTarget(QString code) const
{
  if(m_targets == "*")
    return true;

  if(m_targets == "!")
    return false;

  QString delimitedCode = QString(";%1;").arg(code);

  // The m_targets string is in the form ";code;code;code;".

  return m_targets.contains(delimitedCode, Qt::CaseSensitive);
}


bool
Modif::validateTargets(bool simplify)
{
  // A targets string cannot contain both a '*' and a '!'
  // character. We check that immediately.
  if(m_targets.contains('*') && m_targets.contains('!'))
    return false;

  QList<Monomer *> monomerList = mp_polChemDef->monomerList();

  QString result;

  // If the m_targets is empty, this is an error, because we cannot
  // know what's to be done with the modification.

  if(m_targets.isEmpty())
    {
      return false;
    }

  // A targets string looks like "Ser ; Thr ; Tyr".
  QStringList stringList =
    m_targets.split(';', QString::SkipEmptyParts, Qt::CaseSensitive);

  for(int iter = 0; iter < stringList.size(); ++iter)
    {
      QString currentString = stringList.at(iter);

      // There are two character that might be encountered: '*' is the
      // equivalent of "all the monomers in the definition"; '!' is
      // equivalent to "none of the monomers in the definition". But
      // it is not possible that both * and ! be present in the same
      // targets string.

      if(currentString == "*" || currentString == "!")
        {
          // Simplification asked: if '*' is found then it can be
          // there alone. Same for '!'. '*' means that any monomer in
          // the definition might be modified with this modification,
          // '!' means that none of the monomers might be modified.

          if(simplify)
            {
              m_targets = currentString;

              return true;
            }
          else
            {
              result.append(QString("%1;").arg(currentString));
            }

          continue;
        }

      // At this point, we have something to check as a monomer code:

      if(Monomer::isCodeInList(currentString, monomerList) == -1)
        {
          qDebug() << __FILE__ << __LINE__
                   << "Monomer code is not known:" << currentString;

          m_targets = result;

          return false;
        }
      else
        {
          // Want the string to be ;code;code;code;(notice the first
          // and last ';'), so that we can later ask easily if ;Lys;
          // is found in the targets string, without risking to also
          // match ;Ly;.
          if(!result.size())
            result.append(QString(";%1;").arg(currentString));
          else
            result.append(QString("%1;").arg(currentString));
        }
    }

  if(result.isEmpty())
    return false;

  m_targets = result;

  return true;
}


void
Modif::setMaxCount(int value)
{
  Q_ASSERT(value > 0);

  m_maxCount = value;
}


int
Modif::maxCount()
{
  return m_maxCount;
}


QString
Modif::formula() const
{
  return Formula::text();
}


//! Tests equality.
/*!

  \param other modification to be compared with \p this.

  \return true if the modifications are identical, false otherwise.
  */
bool
Modif::operator==(const Modif &other)
{
  int tests = 0;

  tests += (m_targets == other.m_targets);
  tests += PolChemDefEntity::operator==(other);
  tests += Formula::operator==(other);
  tests += Ponderable::operator==(other);

  if(tests < 4)
    return false;

  return true;
}


//! Tests inequality.
/*!

  \param other modification to be compared with \p this.

  \return true if the modifications differ, false otherwise.
  */
bool
Modif::operator!=(const Modif &other)
{
  int tests = 0;

  tests += (m_targets != other.m_targets);
  tests += PolChemDefEntity::operator!=(other);
  tests += Formula::operator!=(other);
  tests += Ponderable::operator!=(other);

  if(tests > 0)
    return true;

  return false;
}


//! Searches \p this modification in the reference list.
/*!  \return the index of the found modification or -1 if none is
  found or if the name is empty.
  */
int
Modif::isNameKnown()
{
  const QList<Modif *> &refList = mp_polChemDef->modifList();

  if(m_name.isEmpty())
    return -1;

  for(int iter = 0; iter < refList.size(); ++iter)
    {
      if(refList.at(iter)->m_name == m_name)
        return iter;
    }

  return -1;
}


//! Searches a modification in a modification list.
/*! Searches for a modification having a name identical to argument \p
  str in \p refList. If such modification is found, and if \p other is
  non-0, the found modification's data are copied into \p other.

  \param name name.

  \param refList list of modifications.

  \param other modification to be updated with data in the found
  modification.  Defaults to 0, in which case no update occurs.

  \return the int index of the found modification or -1 if none is
  found or if \p str is empty.
  */
int
Modif::isNameInList(const QString &name,
                    const QList<Modif *> &refList,
                    Modif *other)
{
  Modif *modif = 0;

  if(name.isEmpty())
    return -1;

  for(int iter = 0; iter < refList.size(); ++iter)
    {
      modif = refList.at(iter);
      Q_ASSERT(modif);

      if(modif->m_name == name)
        {
          if(other)
            modif->clone(other);

          return iter;
        }
    }

  return -1;
}


bool
Modif::validate()
{
  if(!mp_polChemDef)
    return false;

  if(m_name.isEmpty())
    return false;

  Formula formula(m_formula);
  const QList<msXpSlibmass::Atom *> &refList = mp_polChemDef->atomList();

  if(!formula.validate(refList))
    return false;

  if(!validateTargets())
    return false;

  if(m_maxCount <= 0)
    return false;

  return true;
}


//! Calculates the net masses.
/*! Calculation of the net masses of this modification is performed by
  accounting the formula masses in the member monoisotopic and average
  mass members.

  Note that the list of reference atoms member datum must point to a
  valid list.

  \return true if successful, false otherwise.
  */
bool
Modif::calculateMasses()
{
  const QList<msXpSlibmass::Atom *> &refList = mp_polChemDef->atomList();

  m_mono = 0;
  m_avg  = 0;

  if(!Formula::accountMasses(refList, &m_mono, &m_avg))
    return false;

  return true;
}


//! Increments the masses in the arguments.
/*! The values pointed to by the first two arguments are updated by
  incrementation using the masses(monoisotopic and average)
  compounded by the \p times argument.

  \param mono monoisotopic mass to update. Defaults to 0, in which
  case this mass is not updated.

  \param avg average mass to update. Defaults to 0, in which case this
  mass is not updated.

  \param times times that the increment should be performed. Defaults
  to 1.

  \return always true.
  */
bool
Modif::accountMasses(double *mono, double *avg, int times)
{
  if(mono)
    *mono += m_mono * times;

  if(avg)
    *avg += m_avg * times;

  return true;
}


//! Renders a modification XML element specifically for \p version.
/*! Renders a modification XML element by delegating to a function
  specific for \p version of the polymer chemistry definition.

  As of last version of the polymer chemistry definition, the DTD is

  <!ELEMENT mdf(name,formula,targets)>

  Typical element structure:

  \verbatim
  <mdf>
  <name>Phosphorylation</name>
  <formula>-H+H2PO3</formula>
  <targets>S;T;Y</targets>
  </mdf>
  \endverbatim


  \param element XML element to be parsed and rendered.

  \param version version of the XML polymer chemistry definition for
  which this modification element is to be rendered.

  \return true if parsing was successful, false otherwise.
  */
bool
Modif::renderXmlMdfElement(const QDomElement &element, int version)
{
  QDomElement child;

  if(element.tagName() != "mdf")
    return false;

  if(version < 2)
    return renderXmlMdfElement(element);
  else if(version >= 2 && version <= 4)
    return renderXmlMdfElementV2(element);
  else if(version >= 5)
    return renderXmlMdfElementV3(element);
  return false;
}


bool
Modif::renderXmlMdfElement(const QDomElement &element)
{
  QDomElement child;

  if(element.tagName() != "mdf")
    return false;

  child = element.firstChildElement("name");

  if(child.isNull())
    return false;

  m_name = child.text();
  if(m_name.isEmpty())
    return false;

  child = child.nextSiblingElement("formula");

  if(child.isNull())
    return false;

  if(!Formula::renderXmlFormulaElement(child))
    return false;

  if(!calculateMasses())
    {
      qDebug() << __FILE__ << __LINE__
               << "Failed to calculate masses for modification" << m_name;
      return false;
    }

  // Fake the existence of the <targets> element by postulating that
  // the modification can apply on any monomer. Alert the user.

  m_targets = "*";

  //   qDebug() << __FILE__ << __LINE__
  // 	    << "Attention, the modification will be applied to any "
  //     "monomer without limitation";

  if(!validate())
    return false;

  return true;
}


bool
Modif::renderXmlMdfElementV2(const QDomElement &element)
{
  QDomElement child;

  if(element.tagName() != "mdf")
    return false;

  child = element.firstChildElement("name");

  if(child.isNull())
    return false;

  m_name = child.text();
  if(m_name.isEmpty())
    return false;

  child = child.nextSiblingElement("formula");

  if(child.isNull())
    return false;

  if(!Formula::renderXmlFormulaElement(child))
    return false;

  if(!calculateMasses())
    {
      qDebug() << __FILE__ << __LINE__
               << "Failed to calculate masses for modification" << m_name;
      return false;
    }

  child = child.nextSiblingElement("targets");

  if(child.isNull())
    return false;

  m_targets = child.text();

  // The m_maxCount value is set in the constructor, no need to set
  // it manually. Let it at 1, as for this version of the function
  // the <maxcount> element does not exist.  m_maxCount = 1;

  // The validation will take care of checking that the <targets>
  // element did have correct text inside.

  if(!validate())
    return false;

  return true;
}


bool
Modif::renderXmlMdfElementV3(const QDomElement &element)
{
  QDomElement child;

  if(element.tagName() != "mdf")
    return false;

  child = element.firstChildElement("name");

  if(child.isNull())
    return false;

  m_name = child.text();
  if(m_name.isEmpty())
    return false;

  child = child.nextSiblingElement("formula");

  if(child.isNull())
    return false;

  if(!Formula::renderXmlFormulaElement(child))
    return false;

  if(!calculateMasses())
    {
      qDebug() << __FILE__ << __LINE__
               << "Failed to calculate masses for modification" << m_name;
      return false;
    }

  child = child.nextSiblingElement("targets");

  if(child.isNull())
    return false;

  m_targets = child.text();

  child = child.nextSiblingElement("maxcount");

  if(child.isNull())
    return false;

  bool ok = false;

  m_maxCount = child.text().toInt(&ok);

  if(!m_maxCount && !ok)
    return false;

  // The validation will take care of checking that the <targets>
  // element did have correct text inside and that <maxcount> be
  // correct also.

  if(!validate())
    return false;

  return true;
}


//! Formats a string suitable to use as an XML element.
/*! Formats a string suitable to be used as an XML element in a
  polymer chemistry definition file. The typical modification element
  that is generated in this function looks like this:

  \verbatim
  <mdf>
  <name>Phosphorylation</name>
  <formula>-H+H2PO3</formula>
  <targets>Ser;Thr;Try</targets>
  </mdf>
  \endverbatim

  \param offset times the \p indent string must be used as a lead in the
  formatting of elements.

  \param indent string used to create the leading space that is placed
  at the beginning of indented XML elements inside the XML
  element. Defaults to two spaces(QString(" ")).

  \return a dynamically allocated string that needs to be freed after
  use.
  */
QString *
Modif::formatXmlMdfElement(int offset, const QString &indent)
{
  int newOffset;
  int iter = 0;

  QString lead("");
  QString *string = new QString();

  // Prepare the lead.
  newOffset = offset;
  while(iter < newOffset)
    {
      lead += indent;
      ++iter;
    }

  /* We are willing to create an <modif> node that should look like this:
   *
   <mdf>
   <name>Phosphorylation</name>
   <formula>-H+H2PO3</formula>
   <targets>S;T;Y</targets>
   <maxcount>1</maxcount>
   </mdf>
   *
   */

  *string += QString("%1<mdf>\n").arg(lead);

  // Prepare the lead.
  ++newOffset;
  lead.clear();
  iter = 0;
  while(iter < newOffset)
    {
      lead += indent;
      ++iter;
    }

  // Continue with indented elements.

  *string += QString("%1<name>%2</name>\n").arg(lead).arg(m_name);

  *string += QString("%1<formula>%2</formula>\n").arg(lead).arg(m_formula);

  *string += QString("%1<targets>%2</targets>\n").arg(lead).arg(m_targets);

  *string += QString("%1<maxcount>%2</maxcount>\n").arg(lead).arg(m_maxCount);

  // Prepare the lead for the closing element.
  --newOffset;
  lead.clear();
  iter = 0;
  while(iter < newOffset)
    {
      lead += indent;
      ++iter;
    }

  *string += QString("%1</mdf>\n").arg(lead);

  return string;
}


//! Outputs a string to the standard error console.
/*! Outputs a string describing the modification. Used for debugging
  purposes.

*/
void
Modif::debugPutStdErr()
{
  qDebug() << __FILE__ << __LINE__ << m_name << m_formula << m_targets;
}


//////////////////////// ModifProp ////////////////////////
ModifProp::ModifProp(Modif *modif)
{
  m_name = "MODIF";

  mpa_data = static_cast<void *>(modif);
}


ModifProp::~ModifProp()
{
  deleteData();
}


void
ModifProp::deleteData()
{
  if(mpa_data)
    {
      delete static_cast<Modif *>(mpa_data);
      mpa_data = 0;
    }
}


void *
ModifProp::clone() const
{
  ModifProp *prop = 0;

  if(mpa_data)
    prop = new ModifProp(static_cast<Modif *>(mpa_data));

  return static_cast<void *>(prop);
}


void
ModifProp::cloneOut(void *other) const
{
  Q_ASSERT(other);

  ModifProp *otherProp = static_cast<ModifProp *>(other);

  if(otherProp->mpa_data)
    otherProp->deleteData();

  otherProp->m_name = m_name;

  Modif *modif = 0;

  if(mpa_data)
    {
      modif = static_cast<Modif *>(mpa_data);

      otherProp->mpa_data = static_cast<void *>(new Modif(*modif));
    }
  else
    otherProp->mpa_data = 0;
}


void
ModifProp::cloneIn(const void *other)
{
  Q_ASSERT(other);

  const ModifProp *otherProp = static_cast<const ModifProp *>(other);

  if(mpa_data)
    deleteData();

  m_name = otherProp->m_name;

  Modif *modif = 0;

  if(otherProp->mpa_data)
    {
      modif = static_cast<Modif *>(otherProp->mpa_data);

      mpa_data = static_cast<void *>(new Modif(*modif));
    }
  else
    mpa_data = 0;
}


bool
ModifProp::renderXmlElement(const QDomElement &element, int version)
{
  QDomElement child;

  if(element.tagName() != "prop")
    return false;

  if(version < 2)
    return renderXmlElement(element);
  else if(version >= 2)
    return renderXmlElementV2(element);

  return false;
}


bool
ModifProp::renderXmlElement(const QDomElement &element)
{
  QDomElement child;

  /* This is what we expect.
   *   <prop>
   *     <name>MODIF</name>
   *     <data>acetylation</data>
   *   </prop>
   */

  if(element.tagName() != "prop")
    return false;

  child = element.firstChildElement("name");

  if(child.isNull())
    return false;

  m_name = child.text();

  if(m_name != "MODIF")
    return false;

  // And now we have to manage the prop's data element. When this
  // ModifProp object was allocated, one Modif was allocated and
  // set as the data of the Prop. Get to it.

  Modif *modif                 = static_cast<Modif *>(mpa_data);
  const PolChemDef *polChemDef = modif->polChemDef();

  child = child.nextSiblingElement("data");

  if(child.isNull())
    return false;

  // What is the name of the modification ?
  QString modifName = child.text();

  // And now search for that modification in the list of modifs in the
  // polymer chemistry definition and put all its data in this prop's
  // modif object.

  if(!polChemDef->modif(modifName, modif))
    return false;

  // At this point, the modif object inside this ModifProp is
  // completely identical to the reference one stored in the polymer
  // chemistry definition's list of modifications. Our task is
  // completed.

  return true;
}


bool
ModifProp::renderXmlElementV2(const QDomElement &element)
{
  QDomElement child;

  // The element looks like this:
  //
  // <prop>
  //   <name>MODIF</name>
  //   <data>Phosphorylation</data> // That is the Modif name
  //   <data>-H+H2PO3</data>        // -------------------- formula
  //   <data>S;T;Y</data>           // -------------------- targets
  // </prop>


  if(element.tagName() != "prop")
    return false;

  child = element.firstChildElement("name");

  if(child.isNull())
    return false;

  m_name = child.text();

  if(m_name != "MODIF")
    return false;

  // And now we have to manage the prop's data elements. When this
  // ModifProp object was allocated, one Modif was allocated and
  // set as the data of the Prop. Get to it.

  Modif *modif = static_cast<Modif *>(mpa_data);

  // Next sibling is the modif's name data.

  child = child.nextSiblingElement("data");

  if(child.isNull())
    return false;

  modif->setName(child.text());

  // Next sibling is the modif's formula data.

  child = child.nextSiblingElement("data");

  if(child.isNull())
    return false;

  modif->setFormula(child.text());

  if(!modif->calculateMasses())
    {
      qDebug() << __FILE__ << __LINE__
               << "Failed to calculate masses for modification"
               << modif->name();
      return false;
    }

  // Next sibling is the modif's targets data.

  child = child.nextSiblingElement("data");

  if(child.isNull())
    return false;

  modif->setTargets(child.text());

  // The validation will take care of checking that the <targets>
  // element did have correct text inside.

  if(!modif->validate())
    return false;

  return true;
}


QString *
ModifProp::formatXmlElement(int offset, const QString &indent)
{
  int newOffset;
  int iter = 0;

  QString lead("");
  QString *string = new QString();

  // Prepare the lead.
  newOffset = offset;
  while(iter < newOffset)
    {
      lead += indent;
      ++iter;
    }

  // The property has its data member that points to a Modif. Thus
  // the formatting of the element should produce something like this:
  //
  // <prop>
  //   <name>MODIF</name>
  //   <data>Phosphorylation</data> // That is the Modif name
  //   <data>-H+H2PO3</data>        // -------------------- formula
  //   <data>S;T;Y</data>           // -------------------- targets
  // </prop>

  *string += QString("%1<prop>\n").arg(lead);

  // Prepare the lead.
  ++newOffset;
  lead.clear();
  iter = 0;
  while(iter < newOffset)
    {
      lead += indent;
      ++iter;
    }

  // Continue with indented elements.

  *string += QString("%1<name>%2</name>\n").arg(lead).arg(m_name);

  *string += QString("%1<data>%2</data>\n")
               .arg(lead)
               .arg(static_cast<Modif *>(mpa_data)->name());

  *string += QString("%1<data>%2</data>\n")
               .arg(lead)
               .arg(static_cast<Modif *>(mpa_data)->formula());

  *string += QString("%1<data>%2</data>\n")
               .arg(lead)
               .arg(static_cast<Modif *>(mpa_data)->targets());

  // Prepare the lead for the closing element.
  --newOffset;
  lead.clear();
  iter = 0;
  while(iter < newOffset)
    {
      lead += indent;
      ++iter;
    }

  *string += QString("%1</prop>\n").arg(lead);

  return string;
}

} // namespace msXpSmassXpert
