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


namespace MsXpS
{

namespace libXpertMass
{


/*!
\class MsXpS::libXpertMass::FragSpec
\inmodule libXpertMass
\ingroup PolChemDefGasPhaseChemicalReactions
\inheaderfile FragSpec.hpp

\brief The FragSpec class provides a model for specifying gas phase
fragmentations of \l{Oligomer} \l{Sequence}s.

The FragSpec class provides a fragmentation specification. Fragmentation
specifications determine the chemical reaction that governs the fragmentation
of the polymer in the gas-phase. The chemical reaction is embodied by a
formula. The side of the polymer (left or right) that makes the fragment after
the fragmentation has occurred is described by a fragmentation-end enumeration.

A fragmentation specification might not be enough information to determine the
manner in which a polymer fragments in the gas-phase. Fragmentation rules
(\l{FragRule}s) might be required to refine the specification.  A fragmentation
specification might hold as many \l{FragRule}s as required.
*/

/*!
\enum MsXpS::libXpertMass::FragEnd

This enum specifies the end of the \l Oligomer that will be contained in the
fragment resulting from its fragmentation. In protein chemistry, for example,
a,b and x fragments are N-terminal fragments, that is, they contain the left
end of the framgented Oligomer's sequence.

\value FRAG_END_NONE
       The value is not set.
\value FRAG_END_LEFT
       The generated fragment contains the left end of the oligomer
\value FRAG_END_RIGHT
       The generated fragment contains the right end of the oligomer
\value FRAG_END_BOTH
       The generated fragment contains both the left and the right end of the
oligomer (that is, is all the oligomer)
*/


/*!
\brief Constructs a FragSpec instance.

  \a pol_chem_def_csp Polymer chemistry definition. Cannot be nullptr.

  \a name Name. Cannot be empty.

  \a formula Formula. Defaults to the null string.

  \a frag_end Fragmentation end. Defaults to FRAG_END_NONE.

  \a comment Comment. Defaults to the null string.
*/
FragSpec::FragSpec(PolChemDefCstSPtr pol_chem_def_csp,
                   QString name,
                   QString formula,
                   FragEnd frag_end,
                   const QString &comment)
  : PolChemDefEntity(pol_chem_def_csp, name),
    Formula(formula),
    m_fragEnd(frag_end),
    m_comment(comment)
{
  m_fragEnd = FRAG_END_NONE;

  m_monomerContribution = 0;
}


/*!
\brief Constructs a fragmentation specification.

  \a pol_chem_def_csp Polymer chemistry definition. Cannot be nullptr.

  \a name Name. Cannot be empty.

  \a formula Formula.
*/
FragSpec::FragSpec(PolChemDefCstSPtr pol_chem_def_csp,
                   QString name,
                   QString formula)
  : PolChemDefEntity(pol_chem_def_csp, name), Formula(formula)
{
  m_fragEnd = FRAG_END_NONE;

  m_monomerContribution = 0;
}


/*!
\brief Constructs a FragSpec instance as a copy of \a other.
 */
FragSpec::FragSpec(const FragSpec &other)
  : PolChemDefEntity(other),
    Formula(other),
    m_fragEnd(other.m_fragEnd),
    m_monomerContribution(other.m_monomerContribution),
    m_comment(other.m_comment)
{
  for(int iter = 0; iter < other.m_ruleList.size(); ++iter)
    {
      FragRule *fragRule = new FragRule(*other.m_ruleList.at(iter));
      m_ruleList.append(fragRule);
    }
}


/*!
\brief Destroys this FragSpec instance.
*/
FragSpec::~FragSpec()
{
  while(!m_ruleList.isEmpty())
    delete m_ruleList.takeFirst();
}


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

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

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

  m_fragEnd             = other.m_fragEnd;
  m_monomerContribution = other.m_monomerContribution;
  m_comment             = other.m_comment;

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

  for(int iter = 0; iter < other.m_ruleList.size(); ++iter)
    {
      FragRule *fragRule = new FragRule(*other.m_ruleList.at(iter));
      m_ruleList.append(fragRule);
    }

  return *this;
}


/*!
\brief Returns the list of FragRule instances.
*/
QList<FragRule *> &
FragSpec::ruleList()
{
  return m_ruleList;
}


/*!
\brief Adds the \a frag_rule FragRule instance to the member list of FragRule
instances.
*/
void
FragSpec::appendRule(FragRule *frag_rule)
{
  Q_ASSERT(frag_rule);

  m_ruleList.append(frag_rule);
}


/*!
\brief Inserts in the member list of FragRule instances at \a index the \a
frag_rule FragRule instance.
*/
void
FragSpec::insertRuleAt(int index, FragRule *frag_rule)
{
  Q_ASSERT(frag_rule);

  m_ruleList.insert(index, frag_rule);
}


/*!
\brief Removes from the member list of FragRule instances the item at index \a
index.
*/
void
FragSpec::removeRuleAt(int index)
{
  m_ruleList.removeAt(index);
}


/*!
\brief Sets the fragmentation end to \a frag_end.
*/
void
FragSpec::setFragEnd(FragEnd frag_end)
{
  m_fragEnd = frag_end;
}


/*!
\brief Returns the fragmentation end.
 */
FragEnd
FragSpec::fragEnd() const
{
  return m_fragEnd;
}


void
FragSpec::setMonomerContribution(int value)
{
  m_monomerContribution = value;
}


int
FragSpec::monomerContribution()
{
  return m_monomerContribution;
}


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


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


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


/*!
\brief Searches by \a name for a FragSpec in the \a frag_spec_list.

If such fragmentation specification is found, and if \a other is non-0, the
found fragmentation specification's data are copied into \a other.

Returns the index of the found FragSpec instance or -1 if none is found or if
\a name is empty.
*/
int
FragSpec::isNameInList(const QString &name,
                       const QList<FragSpec *> &frag_spec_list,
                       FragSpec *other)
{
  FragSpec *fragSpec = 0;

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

  for(int iter = 0; iter < frag_spec_list.size(); ++iter)
    {
      fragSpec = frag_spec_list.at(iter);
      Q_ASSERT(fragSpec);

      if(fragSpec->m_name == name)
        {
          if(other)
            *other = *fragSpec;

          return iter;
        }
    }

  return -1;
}


/*!
\brief  Validates the FragSpec.

The validation involves checking that:

\list
\li The name is not empty.
\li The fragmentation end must have been set.
\li The FragRule instances (if any) are valid.
\li The Formula is valid.
\endlist

Returns true upon success, false otherwise.
*/
bool
FragSpec::validate()
{
  IsotopicDataCstSPtr isotopic_data_csp =
    mcsp_polChemDef->getIsotopicDataCstSPtr();

  if(m_name.isEmpty())
    return false;

  if(m_fragEnd != FRAG_END_NONE && m_fragEnd != FRAG_END_LEFT &&
     m_fragEnd != FRAG_END_RIGHT)
    return false;

  for(int iter = 0; iter < m_ruleList.size(); ++iter)
    {
      if(!m_ruleList.at(iter)->validate())
        return false;
    }

  return Formula::validate(isotopic_data_csp);
}


/*!
\brief Parses a fragmentation specification XML \a element using a
\a{version}ed function.

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

Returns true if parsing and formula validation were successful, false otherwise.
*/
bool
FragSpec::renderXmlFgsElement(const QDomElement &element, int version)
{
  if(version == 1)
  {
  //no-op

  version = 1;
  }

  QDomElement child;
  QDomElement childRule;

  FragRule *fragRule = 0;

  bool commentParsed = false;

  /* The xml node we are in is structured this way:
   *
   * <fgs>
   *   <name>a</name>
   *   <end>LE</end>
   *   <formula>-C1O1</formula>
   *   <comment>opt_comment</comment>
   *   <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>
   *   other fgr allowed, none possible also
   * </fgs>
   *
   * And the element parameter points to the
   *
   * <fgs> element tag:
   *  ^
   *  |
   *  +----- here we are right now.
   *
   * Which means that element.tagName() == "fgs" 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 says this:
   * <!ELEMENT fgs(name,end,formula,comment?,fgr*)>
   */

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

  child = element.firstChildElement("name");

  if(child.isNull())
    return false;

  m_name = child.text();

  child = child.nextSiblingElement();

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

  if(child.text() == "NE")
    m_fragEnd = FRAG_END_NONE;
  else if(child.text() == "LE")
    m_fragEnd = FRAG_END_LEFT;
  else if(child.text() == "RE")
    m_fragEnd = FRAG_END_RIGHT;
  else if(child.text() == "BE")
    m_fragEnd = FRAG_END_BOTH;
  else
    return false;

  child = child.nextSiblingElement();

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

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

  // The next element must be <sidechaincontrib>
  child = child.nextSiblingElement();

  if(child.tagName() != "sidechaincontrib")
    return false;

  QString text = child.text();

  bool ok = false;

  m_monomerContribution = text.toInt(&ok);

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

  // The next element might be either comment or(none, one or more)
  // fgr.
  child = child.nextSiblingElement();

  while(!child.isNull())
    {
      // Is it a comment or the first of one|more <fgr> elements ?
      // Remember: <!ELEMENT fgs(name,end,formula,comment?,fgr*)>

      if(child.tagName() == "comment")
        {
          if(commentParsed)
            return false;

          m_comment     = child.text();
          commentParsed = true;

          child = child.nextSiblingElement();
          continue;
        }

      // At this point, yes, if there is still a sibling, then it
      // has to be one <fgr>, either alone or the first of multiple.

      while(!child.isNull())
        {
          fragRule = new FragRule(mcsp_polChemDef, "NOT_SET");

          if(!fragRule->renderXmlFgrElement(child))
            {
              delete fragRule;
              return false;
            }
          else
            m_ruleList.append(fragRule);

          child = child.nextSiblingElement();
        }
    }

  if(!validate())
    return false;

  return true;
}


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

The typical fragmentation
  specification element that is generated in this function looks like
  this:

  \code
  <fgs>
  <name>a</name>
  <end>LE</end>
  <formula>-C1O1</formula>
  <fgr>
  <name>a-fgr-1</name>
  <formula>+H200</formula>
  <prev-mnm-code>E</prev-mnm-code>
  <curr-mnm-code>D</curr-mnm-code>
  <next-mnm-code>F</next-mnm-code>
  <comment>comment here!</comment>
  </fgr>
  <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>
  </fgs>
  \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 *
FragSpec::formatXmlFgsElement(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;
    }

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

  if(m_fragEnd == FRAG_END_NONE)
    *string += QString("%1<end>%2</end>\n").arg(lead).arg("NE");
  else if(m_fragEnd == FRAG_END_BOTH)
    *string += QString("%1<end>%2</end>\n").arg(lead).arg("BE");
  else if(m_fragEnd == FRAG_END_LEFT)
    *string += QString("%1<end>%2</end>\n").arg(lead).arg("LE");
  else if(m_fragEnd == FRAG_END_RIGHT)
    *string += QString("%1<end>%2</end>\n").arg(lead).arg("RE");

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

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

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

  for(int iter = 0; iter < m_ruleList.size(); ++iter)
    {
      QString *ruleString = m_ruleList.at(iter)->formatXmlFgrElement(newOffset);

      *string += *ruleString;

      delete ruleString;
    }

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

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

  return string;
}

} // namespace libXpertMass

} // namespace MsXpS
