/* BEGIN software license
 *
 * MsXpertSuite - mass spectrometry software suite
 * -----------------------------------------------
 * Copyright 2009--2026 by 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 "MsXpS/libXpertMassCore/FragmentationRule.hpp"
#include "MsXpS/libXpertMassCore/PolChemDef.hpp"

namespace MsXpS
{

namespace libXpertMassCore
{


/*!
\class MsXpS::libXpertMassCore::FragmentationRule
\inmodule libXpertMassCore
\ingroup PolChemDefGasPhaseChemicalReactions
\inheaderfile FragmentationRule.hpp

\brief The FragmentationRule class provides a model for specifying gas phase
fragmentation rules for refining fragmentation pathway specifications (\l
FragmentationPathway) of \l{Oligomer} \l{Sequence}s.

Fragmentation rules characterize in more detail the chemical reaction that
governs the fragmentation of the polymer in the gas-phase. The rule is a
conditional rule. Its logic is based on the presence of specified monomers
right at the place of the fragmentation and before or after that precise
location.

In saccharide chemistry, fragmentations are a very complex topic. This is
because a given monomer will fragment according to a given chemistry if it is
preceded in the sequence by a monomer of a given identity and according to
another chemistry if its direct environment is different.

This paradigm is implemented using a sequence environment logic based on
conditions that can be formulated thanks to three monomer codes:

\list
\li The monomer at which the fragmentation takes place (current code);
\li The monomer preceeding the current code (previous code);
\li The monomer following the current code (next code);
\endlist

The use of these codes is typically according to this kind of logic:

\e{If current monomer is Glu and previous monomer is Gly and
next monomer is Arg, then fragmentation should occur according
to this formula : "-H2O"}.

\sa FragmentationPathway
*/


/*!
\variable MsXpS::libXpertMassCore::FragmentationRule::mcsp_polChemDef

\brief The PolChemDef (polymer chemistry definition) that is the context in
which the Oligomer being fragmented exists.
*/

/*!
\variable MsXpS::libXpertMassCore::FragmentationRule::m_name

\brief The name of the FragmentationRule.
*/

/*!
\variable MsXpS::libXpertMassCore::FragmentationRule::m_prevCode

\brief The \l Monomer code of the Monomer located before the actual
fragmentation site.
*/


/*!
\variable MsXpS::libXpertMassCore::FragmentationRule::m_currCode

\brief The \l Monomer code of the Monomer at the actual fragmentation site.
*/


/*!
\variable MsXpS::libXpertMassCore::FragmentationRule::m_nextCode

\brief The \l Monomer code of the Monomer located after the actual fragmentation
site.
*/


/*!
\variable MsXpS::libXpertMassCore::FragmentationRule::m_comment

\brief A comment associated to the FragmentationRule.
*/

/*!
\variable MsXpS::libXpertMassCore::FragmentationRule::m_formula

\brief A \l{Formula} instance describing the fragmentation reaction occurring on
the Monomer at which the fragmentation occurs.
*/

/*!
\variable MsXpS::libXpertMassCore::FragmentationRule::m_isValid

\brief The validity status of this FragmentationRule instance.
*/


/*!
\brief Constructs a fragmentation rule with a number of parameters.

\list
\li \a pol_chem_def_csp Polymer chemistry definition. Cannot be nullptr.

\li \a name Name. Cannot be empty.

\li \a prev_code Previous monomer code. Defaults to the null string.

\li \a current_code Current monomer code. Defaults to the null string.

\li \a next_code Next monomer code. Defaults to the null string.

\li \a comment Comment. Defaults to the null string.

\li \a formula_string Formula. Defaults to the null string.
\endlist

Upon setting all the member data, this instance is validated and the member
m_isValid is set to the result.
*/
FragmentationRule::FragmentationRule(PolChemDefCstSPtr pol_chem_def_csp,
                                     const QString &name,
                                     const QString &prev_code,
                                     const QString &current_code,
                                     const QString &next_code,
                                     const QString &comment,
                                     const QString &formula_string)
  : mcsp_polChemDef(pol_chem_def_csp),
    m_name(name),
    m_prevCode(prev_code),
    m_currCode(current_code),
    m_nextCode(next_code),
    m_comment(comment),
    m_formula(formula_string)
{
  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug()
      << "Upon construction of FragmentationRule, the instance failed to "
         "validate with errors:"
      << Utils::joinErrorList(error_list, ", ");
}

/*!
\brief Constructs a FragmentationRule instance starting from an XML <fgr> \a
element according to \a version and using the reference  \a pol_chem_def_csp
polymer chemistry definition.

This is the current format (FAKE fgr):
\code

<fgr>
<name>a-fgr-2</name>
<formula>+H100</formula>
<prev-mnm-code>F</prev-mnm-code>
<curr-mnm-code>D</curr-mnm-code>
<next-mnm-code>E</next-mnm-code>
<comment>comment here!</comment>
</fgr>

\endcode

The rendering of the CleavageRule instances requires that the PolChemDef be
available.

\sa renderXmlFgrElement()
*/
FragmentationRule::FragmentationRule(PolChemDefCstSPtr pol_chem_def_csp,
                                     const QDomElement &element,
                                     [[maybe_unused]] int version)
  : mcsp_polChemDef(pol_chem_def_csp)
{
  if(mcsp_polChemDef == nullptr || mcsp_polChemDef.get() == nullptr)
    qDebug() << "Constructing FragmentationRule with no PolChemDef.";

  if(!renderXmlFgrElement(element))
    qDebug()
      << "Failed to fully render or validate the FragmentationRule XML element "
         "for construction of FragmentationRule instance.";
}

/*!
\brief Constructs a FragmentationRule instance as a copy of \a other.

Upon setting all the member data, this instance is validated and the member
m_isValid is set to the result.
*/
FragmentationRule::FragmentationRule(const FragmentationRule &other)
  : mcsp_polChemDef(other.mcsp_polChemDef),
    m_name(other.m_name),
    m_prevCode(other.m_prevCode),
    m_currCode(other.m_currCode),
    m_nextCode(other.m_nextCode),
    m_comment(other.m_comment),
    m_formula(other.m_formula)
{
  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug()
      << "Upon construction of FragmentationRule, the instance failed to "
         "validate with errors:"
      << Utils::joinErrorList(error_list, ", ");
}

/*!
\brief Destructs the fragmentation rule.
*/
FragmentationRule::~FragmentationRule()
{
}

/*!
\brief Sets the PolChemDef to \a pol_chem_def_csp.
*/
void
FragmentationRule::setPolchemDefCstSPtr(PolChemDefCstSPtr pol_chem_def_csp)
{
  mcsp_polChemDef = pol_chem_def_csp;

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug()
      << "Upon setting PolChemDef of FragmentationRule, the instance failed to "
         "validate with errors:"
      << Utils::joinErrorList(error_list, ", ");
}

/*!
\brief Returns the PolChemDef.
*/
PolChemDefCstSPtr
FragmentationRule::getPolchemDefCstSPtr() const
{
  return mcsp_polChemDef;
}

/*!
\brief Sets the name to \a name.
*/
void
FragmentationRule::setName(const QString &name)
{
  m_name = name;

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug()
      << "Upon setting name of FragmentationRule, the instance failed to "
         "validate with errors:"
      << Utils::joinErrorList(error_list, ", ");
}

/*!
\brief Returns the name.
*/
const QString &
FragmentationRule::getName()
{
  return m_name;
}

/*!
\brief Sets the previous monomer \a code.

Upon setting the member data, this instance is validated and the member
m_isValid is set to the result.
*/
void
FragmentationRule::setPrevCode(const QString &code)
{
  m_prevCode = code;

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug() << "Upon setting previous code of FragmentationRule, the "
                "instance failed to "
                "validate with errors:"
             << Utils::joinErrorList(error_list, ", ");
}

/*!
\brief Returns the previous monomer code.
*/
const QString &
FragmentationRule::getPrevCode() const
{
  return m_prevCode;
}

/*!
\brief Sets the current monomer \a code.

Upon setting the member data, this instance is validated and the member
m_isValid is set to the result.
*/
void
FragmentationRule::setCurrCode(const QString &code)
{
  m_currCode = code;

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug() << "Upon setting current code of FragmentationRule, the "
                "instance failed to "
                "validate with errors:"
             << Utils::joinErrorList(error_list, ", ");
}

/*!
\brief Returns the current monomer code.
*/
const QString &
FragmentationRule::getCurrCode() const
{
  return m_currCode;
}

/*!
\brief Sets the next monomer \a code.

Upon setting the member data, this instance is validated and the member
m_isValid is set to the result.
*/
void
FragmentationRule::setNextCode(const QString &code)
{
  m_nextCode = code;

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug()
      << "Upon setting next code of FragmentationRule, the instance failed to "
         "validate with errors:"
      << Utils::joinErrorList(error_list, ", ");
}

/*!
\brief Returns the next monomer code.
*/
const QString &
FragmentationRule::getNextCode() const
{
  return m_nextCode;
}

/*!
\brief Sets the \a comment.
*/
void
FragmentationRule::setComment(const QString &comment)
{
  m_comment = comment;
}

/*!
\brief Returns the comment.
*/
const QString &
FragmentationRule::getComment() const
{
  return m_comment;
}

/*!
\brief Sets the \a formula.

Upon setting the member data, this instance is validated and the member
m_isValid is set to the result.
*/
void
FragmentationRule::setFormula(const Formula &formula)
{
  m_formula = formula;

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug()
      << "Upon setting Formula of FragmentationRule, the instance failed to "
         "validate with errors:"
      << Utils::joinErrorList(error_list, ", ");
}

/*!
\brief Sets the member Formula using \a formula_string.

Upon setting the member data, this instance is validated and the member
m_isValid is set to the result.
*/
void
FragmentationRule::setFormula(const QString &formula_string)
{
  m_formula = Formula(formula_string);

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug()
      << "Upon setting Formula of FragmentationRule, the instance failed to "
         "validate with errors:"
      << Utils::joinErrorList(error_list, ", ");
}

/*!
\brief Returns a const reference to the member \l Formula.
*/
const Formula &
FragmentationRule::getFormulaCstRef() const
{
  return m_formula;
}

/*!
\brief Returns a reference to the member \l Formula.
*/
Formula &
FragmentationRule::getFormulaRef()
{
  return m_formula;
}

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

Upon setting the member data, this instance is validated and the member
m_isValid is set to the result.

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

  mcsp_polChemDef = other.mcsp_polChemDef;
  m_name          = other.m_name;
  m_prevCode      = other.m_prevCode;
  m_currCode      = other.m_currCode;
  m_nextCode      = other.m_nextCode;
  m_comment       = other.m_comment;
  m_formula       = other.m_formula;

  ErrorList error_list;
  m_isValid = validate(&error_list);

  if(!m_isValid)
    qDebug() << "Upon assignment of FragmentationRule, the instance failed to "
                "validate with errors:"
             << Utils::joinErrorList(error_list, ", ");

  return *this;
}

/*!
\brief Returns true if \c this instance and \a other are identical.
*/
bool
FragmentationRule::operator==(const FragmentationRule &other) const
{
  if(&other == this)
    return true;

  return mcsp_polChemDef == other.mcsp_polChemDef && m_name == other.m_name &&
         m_prevCode == other.m_prevCode && m_currCode == other.m_currCode &&
         m_nextCode == other.m_nextCode && m_comment == other.m_comment &&
         m_formula == other.m_formula;
}

/*!
\brief Returns true if \c this and \a other are different.

Returns the negated result of operator==().
*/
bool
FragmentationRule::operator!=(const FragmentationRule &other) const
{
  if(&other == this)
    return false;

  return !operator==(other);
}

//////////////// VALIDATIONS /////////////////////
/*!
\brief Validates the FragmentationRule, recording any error as a message in \a
error_list_p.

The validation involves checking that:

\list
\li The member PolChemDef cannot be nullptr.

\li The name is not empty.

\li The previous code is valid if non empty.

\li The current code is valid if non empty.

\li The next code is valid if non empty.

\li The formula validates successfully if non empty.
\endlist

The m_isValid member is set to true upon success, false otherwise and returned.
*/
bool
FragmentationRule::validate(ErrorList *error_list_p) const
{
  qsizetype error_count = error_list_p->size();

  if(mcsp_polChemDef == nullptr || mcsp_polChemDef.get() == nullptr)
    {
      qDebug() << "A FragmentationRule with no PolChemDef available cannot "
                  "validate successfully.";
      error_list_p->push_back(
        "A FragmentationRule with no PolChemDef available cannot validate "
        "successfully");
    }

  if(m_name.isEmpty())
    {
      qDebug()
        << "A FragmentationRule with no name cannot validate successfully.";
      error_list_p->push_back(
        "A FragmentationRule with no name cannot validate successfully");
    }

  if(m_prevCode.isEmpty() && m_currCode.isEmpty() && m_nextCode.isEmpty())
    {
      qDebug() << "A FragmentationRule with not a single Monomer code "
                  "cannot validate successfully.";
      error_list_p->push_back(
        "A FragmentationRule with not a single Monomer code cannot validate "
        "successfully");
    }

  if(mcsp_polChemDef != nullptr && mcsp_polChemDef.get() != nullptr &&
     !m_prevCode.isEmpty())
    {
      if(mcsp_polChemDef->getMonomerCstSPtrByCode(m_prevCode) == nullptr)
        {
          qDebug()
            << "A FragmentationRule with an unknown previous monomer code "
               "cannot validate successfully.";
          error_list_p->push_back(
            "A FragmentationRule with an unknown previous monomer code cannot "
            "validate "
            "successfully");
        }
    }

  if(mcsp_polChemDef != nullptr && mcsp_polChemDef.get() != nullptr &&
     !m_currCode.isEmpty())
    {
      if(mcsp_polChemDef->getMonomerCstSPtrByCode(m_currCode) == nullptr)
        {
          qDebug()
            << "A FragmentationRule with an unknown current monomer code "
               "cannot validate successfully.";
          error_list_p->push_back(
            "A FragmentationRule with an unknown current monomer code cannot "
            "validate "
            "successfully");
        }
    }

  if(mcsp_polChemDef != nullptr && mcsp_polChemDef.get() != nullptr &&
     !m_nextCode.isEmpty())
    {
      if(mcsp_polChemDef->getMonomerCstSPtrByCode(m_nextCode) == nullptr)
        {
          qDebug() << "A FragmentationRule with an unknown next monomer code "
                      "cannot validate successfully.";
          error_list_p->push_back(
            "A FragmentationRule with an unknown next monomer code cannot "
            "validate "
            "successfully");
        }
    }

  if(m_formula.getActionFormula().isEmpty())
    {
      qDebug() << "A FragmentationRule with no Formula does not make sense and "
                  "cannot validate successfully.";
      error_list_p->push_back(
        "A FragmentationRule with no Formula does not make sense and cannot "
        "validate "
        "successfully");
    }

  if(mcsp_polChemDef != nullptr && mcsp_polChemDef.get() != nullptr &&
     !m_formula.validate(mcsp_polChemDef->getIsotopicDataCstSPtr(),
                         error_list_p))
    {
      qDebug() << "A FragmentationRule with a non-empty invalid Formula "
                  "cannot validate successfully.";
      error_list_p->push_back(
        "A FragmentationRule with a non-empty invalid Formula cannot validate "
        "successfully");
    }

  m_isValid = error_list_p->size() > error_count ? false : true;

  return m_isValid;
}

/*!
\brief Returns the validity status of this FragmentationRule.
*/
bool
FragmentationRule::isValid() const
{
  return m_isValid;
}

//////////////// UTILITIES /////////////////////
/*!
\brief Returns a string with the textual representation of this
FragmentationRule instance.
*/
QString
FragmentationRule::toString() const
{
  QString text = "Fragmentation rule:\n";

  text += QString(
            "name: %1 - prev code: %2 - curr code: %3 - next code: %4 - "
            "formula: %5 - comment: %6 - is valid? %7\n")
            .arg(m_name)
            .arg(m_prevCode)
            .arg(m_currCode)
            .arg(m_nextCode)
            .arg(m_formula.getActionFormula(/*with_title*/ true))
            .arg(m_comment)
            .arg(m_isValid ? "true" : "false");

  return text;
}

/*!
\brief Parses the FragmentationRule XML \a element.

Upon parsing and validation of the parsed data, the member data are updated,
thus essentially initializing this FragmentationRule instance.

Returns true if parsing and formula validation were successful, false otherwise.
*/
bool
FragmentationRule::renderXmlFgrElement(const QDomElement &element)
{
  QDomElement child;

  bool prevSet    = false;
  bool currSet    = false;
  bool nextSet    = false;
  bool commentSet = false;

  /* The xml node we are in is structured this way:
   *
   * <fgr>
   *  <name>one_rule</name>
   *  <formula>+H2O</formula>
   *  <prev-mnm-code>M</prev-mnm-code>
   *  <this-mnm-code>Y</this-mnm-code>
   *  <next-mnm-code>T</next-mnm-code>
   *  <comment>opt_comment</comment>
   * </fgr>
   *
   * And the element parameter points to the
   *
   * <fgr> element tag:
   * ^
   * |
   * +----- here we are right now.
   *
   * Which means that element.tagName() == "fgr" and that
   * we'll have to go one step down to the first child of the
   * current node in order to get to the <name> element.
   *
   * The DTD:
   *  <!ELEMENT fgr(name, formula, prev-mnm-code?,
   *          this-mnm-code?, next-mnm-code?, comment?)>
   */

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

  child = element.firstChildElement();

  if(child.isNull() || child.tagName() != "name")
    return false;

  m_name = child.text();

  child = child.nextSiblingElement();

  if(child.isNull() || child.tagName() != "formula")
    return false;

  if(!m_formula.renderXmlFormulaElement(child))
    return false;

  // Since the following items are not obligatory, we have to while()
  // until we have no more items...

  child = child.nextSiblingElement();

  while(!child.isNull())
    {
      if(child.tagName() == "prev-mnm-code")
        {
          if(prevSet)
            return false;
          else
            {
              m_prevCode = child.text();
              prevSet    = true;
            }
        }
      else if(child.tagName() == "curr-mnm-code")
        {
          if(currSet)
            return false;
          else
            {
              m_currCode = child.text();
              currSet    = true;
            }
        }
      else if(child.tagName() == "next-mnm-code")
        {
          if(nextSet)
            return false;
          else
            {
              m_nextCode = child.text();
              nextSet    = true;
            }
        }
      else if(child.tagName() == "comment")
        {
          if(commentSet)
            return false;
          else
            {
              m_comment  = child.text();
              commentSet = true;
            }
        }

      child = child.nextSiblingElement();
    }

  ErrorList error_list;
  m_isValid = validate(&error_list);
  if(!m_isValid)
    {
      qDebug()
        << "The FragmentationRule rendered from <clr> element is invalid.";
    }

  return m_isValid;
}

/*!
\brief Formats a string suitable to use as an XML element.


The string is suitable to be used as an XML element in a polymer chemistry
definition file. The typical fragmentation rule element that is generated in
this function looks like this:

\code
<fgr>
<name>a-fgr-2</name>
<formula>+H100</formula>
<prev-mnm-code>F</prev-mnm-code>
<curr-mnm-code>D</curr-mnm-code>
<next-mnm-code>E</next-mnm-code>
<comment>comment here!</comment>
</fgr>
\endcode

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
FragmentationRule::formatXmlFgrElement(int offset, const QString &indent)
{

  int newOffset;
  int iter = 0;

  QString lead("");
  QString text;


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

  text += QString("%1<fgr>\n").arg(lead);

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

  // Continue with indented elements.

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

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

  if(!m_prevCode.isEmpty())
    text += QString("%1<prev-mnm-code>%2</prev-mnm-code>\n")
              .arg(lead)
              .arg(m_prevCode);

  if(!m_currCode.isEmpty())
    text += QString("%1<curr-mnm-code>%2</curr-mnm-code>\n")
              .arg(lead)
              .arg(m_currCode);

  if(!m_nextCode.isEmpty())
    text += QString("%1<next-mnm-code>%2</next-mnm-code>\n")
              .arg(lead)
              .arg(m_nextCode);

  if(!m_comment.isEmpty())
    text += QString("%1<comment>%2</comment>\n").arg(lead).arg(m_comment);

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

  text += QString("%1</fgr>\n").arg(lead);

  return text;
}

} // namespace libXpertMassCore

} // namespace MsXpS
