/* 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 "CrossLinker.hpp"
#include "PolChemDef.hpp"


namespace MsXpS
{

namespace libXpertMass
{


/*!
\class MsXpS::libXpertMass::CrossLinker
\inmodule libXpertMass
\ingroup PolChemDefAqueousChemicalReactions
\inheaderfile CrossLinker.hpp

\brief The CrossLinker class provides abstractions to define the chemical basis
of a cross-linking reaction.

The notion of a cross-linker is that it is the description of the chemical
reaction that is carried out by \l{Monomer}s in a \l Polymer or in an \l
Oligomer sequence to form a \l CrossLink.

There are two different kinds of chemical entities potentially involved in the
description of a CrossLinker:

\list
\a Modif instances that are applied to \l Monomer instances.
\a the \l Formula base class that completes the chemical reactions described by
the \l Modif instances.
\endlist
*/

/*!
\variable MsXpS::libXpertMass::CrossLinker::m_modifList

\brief The list of \l Modif instances that describe the reaction (or the
reactions) involved in the formation of a CrossLink between \l{Monomer}s of a
\l Polymer (or of an \l Oligomer) sequence.
*/

/*!
\brief Constructs a CrossLinker instance.

\list
\li \a pol_chem_def_csp: the polymer chemistry definition (\l PolChemDef).
\li \a name: the name of this CrossLinker.
\li \a formula: the \l Formula that potentially complements the description of
the reaction that is the basis of this CrossLinker (used to initialize the
Formula base class).
\endlist
*/
CrossLinker::CrossLinker(PolChemDefCstSPtr pol_chem_def_csp,
                         const QString &name,
                         const QString &formula)
  : PolChemDefEntity(pol_chem_def_csp, name), Formula(formula)
{
}

/*!
\brief Constructs a CrossLinker instance as a copy of \a other.
*/
CrossLinker::CrossLinker(const CrossLinker &other)
  : PolChemDefEntity(other), Formula(other), Ponderable(other)
{
  for(int iter = 0; iter < other.m_modifList.size(); ++iter)
    m_modifList.append(other.m_modifList.at(iter));
}


/*!
\brief Destructs this CrossLinker instance
*/
CrossLinker::~CrossLinker()
{
  // We do not own the modifications in m_modifList!
}

/*!
\brief Sets the Modif at index \a index to \a modif.

\a index cannot be out-of-bounds.

Returns true.
*/
bool
CrossLinker::setModifAt(Modif *modif, int index)
{
  Q_ASSERT(modif);
  Q_ASSERT(index >= 0 && index < m_modifList.size());

  m_modifList.replace(index, modif);

  return true;
}


/*!
\brief Adds \a modif to this CrossLinker instance.

Returns true.
*/
bool
CrossLinker::appendModif(Modif *modif)
{
  Q_ASSERT(modif);
  m_modifList.append(modif);

  return true;
}


/*!
\brief Returns the \l Modif instance at \a index.

\a index cannot be out-of-bounds.
*/
const Modif *
CrossLinker::modifAt(int index) const
{
  Q_ASSERT(index >= 0 && index < m_modifList.size());

  return m_modifList.at(index);
}


/*!
\brief Removes the \l Modif instance at \a index.

\a index cannot be out-of-bounds.

Returns true.
*/
bool
CrossLinker::removeModifAt(int index)
{
  Q_ASSERT(index < m_modifList.size());

  m_modifList.removeAt(index);

  return true;
}


/*!
\brief Returns the \l Formula.
*/
QString
CrossLinker::formula() const
{
  return Formula::toString();
}


/*!
\brief Returns the list of \l Modif instances.
*/
QList<Modif *> &
CrossLinker::modifList()
{
  return m_modifList;
}


/*!
\brief Returns true if a \l Modif instance by \a modif_name is in the list of
Modif instances, false otherwise.
*/
int
CrossLinker::hasModif(const QString &modif_name)
{
  // Iterate in the list of modifications, and check if one of these
  // has the name passed as argument. If so return the index of the
  // found item in the list, otherwise return -1.

  for(int iter = 0; iter < m_modifList.size(); ++iter)
    {
      Modif *modif = m_modifList.at(iter);

      if(modif->name() == modif_name)
        return iter;
    }

  return -1;
}

/*!
\brief Assigns \a other to this CrossLinker instance.

Returns a reference to this CrossLinker instance.
*/
CrossLinker &
CrossLinker::operator=(const CrossLinker &other)
{
  if(&other == this)
    return *this;

  PolChemDefEntity::operator=(other);
  Formula::operator         =(other);
  Ponderable::operator      =(other);

  while(!m_modifList.isEmpty())
    m_modifList.removeFirst();

  for(int iter = 0; iter < other.m_modifList.size(); ++iter)
    m_modifList.append(other.m_modifList.at(iter));

  return *this;
}

/*!
\brief Returns true if this CrossLinker and \a other are identical.
*/
bool
CrossLinker::operator==(const CrossLinker &other) const
{
  int tests = 0;

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

  if(m_modifList.size() != other.m_modifList.size())
    return false;

  // FIXME: at written now, we compare pointers, not objects.
  // There should be an indirection.
  for(int iter = 0; iter < m_modifList.size(); ++iter)
    {
      if(m_modifList.at(iter) != other.m_modifList.at(iter))
        return false;
    }

  if(tests < 3)
    return false;
  else
    return true;
}

/*!
\brief Returns true if this CrossLinker instance is found in the member polymer
chemistry definition, false otherwise.
*/
int
CrossLinker::isNameKnown()
{
  const QList<CrossLinker *> &refList = mcsp_polChemDef->crossLinkerList();

  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;
}

/*!
\brief Returns the index of a CrossLinker found in the \a cross_linker_list
list of CrossLinker instances by name \a name, -1 otherwise.

If \a other is non-nullptr, the found CrossLinker instance is copied into it.
*/
int
CrossLinker::isNameInList(const QString &name,
                          const QList<CrossLinker *> &cross_linker_list,
                          CrossLinker *other)
{
  CrossLinker *crossLinker = 0;

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

  for(int iter = 0; iter < cross_linker_list.size(); ++iter)
    {
      crossLinker = cross_linker_list.at(iter);
      Q_ASSERT(crossLinker);

      if(crossLinker->m_name == name)
        {
          if(other != nullptr)
            *other = *crossLinker;

          return iter;
        }
    }

  return -1;
}

/*!
\brief Returns true if this CrossLinker instance validates successfully,
false otherwise.

The validation involves the following:

\list
\li The member polymer chemistry definition must exist.
\li The name cannot be empty.
\li The formula (if not empty) must validate successfully agains the member
polymer chemistry definition.
\li The list of \l Modif instances must contain either no or more than two
Modif instances. In the latter case, the Modif instances must be found in the
member polymer chemistry definition and must validate successfully.
\endlist
*/
bool
CrossLinker::validate()
{
  if(!mcsp_polChemDef)
    return false;

  if(m_name.isEmpty())
    return false;

  // Remember that the formula of the crosslinker might be empty because the
  // crosslinker might be fully accounted for by the modifications (below).
  if(!m_formula.isEmpty())
    {
      Formula formula(m_formula);

      IsotopicDataCstSPtr isotopic_data_csp =
        mcsp_polChemDef->getIsotopicDataCstSPtr();

      if(!formula.validate(isotopic_data_csp))
        return false;
    }

  // This is mainly a sanity check, as the pointers to Modif in the
  // list all point to modification objects in the polymer chemistry
  // definition, which have been validated already...

  // The validation actually is simple, it might be that there are NO
  // modifs, or if this is not the case there must be at least
  // 2. Indeed, either none of the monomers in the crosslink get
  // modified, or each one has to be(otherwise we cannot know which
  // modif goes to which monomer).

  int size = m_modifList.size();

  if(size > 0 && size < 2)
    return false;

  for(int iter = 0; iter < size; ++iter)
    {
      // Make sure the modification is known to the polymer chemistry
      // definition. This check is not performed by the modif's
      // validation function.

      if(!mcsp_polChemDef->referenceModifByName(m_modifList.at(iter)->name()))
        return false;

      if(!m_modifList.at(iter)->validate())
        return false;
    }

  return true;
}

/*!
\brief Returns true if the mass calculations for this CrossLinker instance are
performed without error, false otherwise.

The calculation involved accounting masses for the Modifs (if any) and for the
Formula (if non-empty). The member masses (\l Ponderable base class) are
updated.
*/
bool
CrossLinker::calculateMasses()
{
  // qDebug() << "Calculating masses for the cross-link";

  m_mono = 0;
  m_avg  = 0;

  // Account the masses of the formula parent class.
  IsotopicDataCstSPtr isotopic_data_csp =
    mcsp_polChemDef->getIsotopicDataCstSPtr();

  // Here is a special case because the formula for the crosslinker might be
  // empty because the crosslinker might be accounted for only but the
  // modifications below.

  if(!m_formula.isEmpty())
    {
      // qDebug() << "The formula of the crosslinker is:" << m_formula
      //<< "accounting for its masses.";

      if(!Formula::accountMasses(isotopic_data_csp, &m_mono, &m_avg))
        return false;
    }
  else
    {
      // qDebug() << "The formula of the crosslinker is empty.";
    }
  // Now, for each modif in the crossLinker, have to account their
  // mass.

  for(int iter = 0; iter < m_modifList.size(); iter++)
    {
      Modif *modif = m_modifList.at(iter);

      // qDebug() << "Accounting for crosslinker modif's" << modif->name()
      //<< "masses";

      if(!modif->accountMasses(&m_mono, &m_avg))
        {
          qDebug()
            << "Failed to account for masses of the cross-linker's modif.";
          return false;
        }
    }

  // qDebug() << "At this point, the masses of the CrossLinker are:" << m_mono
  //<< "/" << m_avg;

  return true;
}

/*!
\brief Copies the member masses (\l Ponderable base class) to \a mono and \a
avg if these are non-nullptr.

Upon copying, the masses are multiplied by the compounding factor \a
times.

Returns true.
*/
bool
CrossLinker::accountMasses(double *mono, double *avg, int times)
{
  // qDebug() << "Accounting masses for crossliker -- mono:" << m_mono
  //<< "avg:" << m_avg;

  if(mono)
    *mono += m_mono * times;

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

  return true;
}

/*!
\brief Copies the member masses (\l Ponderable base class) to \a ponderable.

\a ponderable cannot be nullptr.

Upon copying, the masses are multiplied by the compounding factor \a
times.

Returns true.
*/
bool
CrossLinker::accountMasses(Ponderable *ponderable, int times)
{
  Q_ASSERT(ponderable);

  ponderable->rmono() += m_mono * times;
  ponderable->ravg() += m_avg * times;

  return true;
}

/*!
\brief Returns true if parsing of XML \a element, using a \a{version}ed
function was successful, false otherwise.

The data in \a element are set to this CrossLinker instance if they validated
successfully, thus essentially initializing this CrossLinker instance.
*/
bool
CrossLinker::renderXmlClkElement(const QDomElement &element, int version)
{
  if(element.tagName() != "clk")
    return false;

  if(version == 1)
    {
      // no-op

      version = 1;
    }

  QDomElement child;

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

  child = element.firstChildElement("name");

  if(child.isNull())
    return false;

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

  // qDebug() << "The crosslinker name:" << m_name;

  child = child.nextSiblingElement("formula");

  // Here, it is possible that the formula element be empty because the
  // crosslinker might be accounted for by using the modifications in it.
  if(!child.isNull())
    {
      QString formula = child.text();

      if(!formula.isEmpty())
        {
          if(!Formula::renderXmlFormulaElement(child))
            return false;
        }
      // else
      // qDebug() << "The formula element of crosslinker is empty.";
    }
  else
    qDebug() << "The formula element of crosslinker is null.";

  const QList<Modif *> &refList = mcsp_polChemDef->modifList();

  // At this point there might be 0, 1 or more "modifname" elements.
  child = child.nextSiblingElement("modifname");

  while(!child.isNull())
    {
      // qDebug() << "Now handling CrossLinker modif:" << child.text();

      int index = Modif::isNameInList(child.text(), refList);

      if(index == -1)
        {
          qDebug() << "Failed to parse one modification of the crosslink:"
                   << m_name;

          return false;
        }
      else
        {
          // qDebug()
          //<< "Found the CrossLinker modification in the reference list:"
          //<< m_name;
        }

      // Modif *modif = mcsp_polChemDef->modifList().at(index);
      // qDebug() << "The found modif has name:" << modif->name()
      //<< "and masses:" << modif->mono() << "/" << modif->avg();

      m_modifList.append(mcsp_polChemDef->modifList().at(index));

      child = child.nextSiblingElement("modifname");
    }

  if(!calculateMasses())
    {
      qDebug() << __FILE__ << __LINE__
               << "Failed to calculate masses for crossLinker" << m_name;

      return false;
    }

  if(!validate())
    return false;

  return true;
}

/*!
\brief Formats this CrossLinker instance in a heap-allocated string to be used
as an XML element in the polymer chemistry definition.

The formatting of the XML element takes into account \a offset and \a
indent by prepending the string with \a offset * \a indent character substring.

\a indent defaults to two spaces.

Returns a dynamically allocated string that needs to be freed after use.
*/
QString *
CrossLinker::formatXmlClkElement(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:
   *
   <clk>
   <name>Phosphorylation</name>
   <formula>-H+H2PO3</formula>
   <modifname>Phosphorylation</modifname>
   <modifname>Acetylation</modifname>
   </clk>
   *
   */

  *string += QString("%1<clk>\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);

  for(int iter = 0; iter < m_modifList.size(); ++iter)
    {
      *string += QString("%1<modifname>%2</modifname>\n")
                   .arg(lead)
                   .arg(m_modifList.at(iter)->name());
    }

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

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

  return string;
}

} // namespace libXpertMass

} // namespace MsXpS
