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


namespace msXpSmassXpert
{

//! Constructs a fragmentation specification.
/*!

  \param polChemDef Polymer chemistry definition. Cannot be 0.

  \param name Name. Cannot be empty.

  \param formula Formula. Defaults to the null string.

  \param fragEnd Fragmentation end. Defaults to FRAG_END_NONE.

  \param comment Comment. Defaults to the null string.
*/
FragSpec::FragSpec(const PolChemDef *polChemDef,
                   QString name,
                   QString formula,
                   MxtFragEnd fragEnd,
                   const QString &comment)
  : PolChemDefEntity(polChemDef, name),
    Formula(formula),
    m_fragEnd(fragEnd),
    m_comment(comment)
{
  m_fragEnd = FRAG_END_NONE;

  m_monomerContribution = 0;
}


//! Constructs a fragmentation specification.
/*!

  \param polChemDef Polymer chemistry definition. Cannot be 0.

  \param name Name. Cannot be empty.

  \param formula Formula.
*/
FragSpec::FragSpec(const PolChemDef *polChemDef, QString name, QString formula)
  : PolChemDefEntity(polChemDef, name), Formula(formula)
{
  m_fragEnd = FRAG_END_NONE;

  m_monomerContribution = 0;
}


//! Constructs a copy of \p other.
/*!  \param other fragmentation specification to be used as a mold.
 */
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(mp_polChemDef, "NOT_SET");

      other.m_ruleList.at(iter)->clone(fragRule);

      m_ruleList.append(fragRule);
    }
}


//! Destroys the fragmentation specification.
FragSpec::~FragSpec()
{
  while(!m_ruleList.isEmpty())
    delete m_ruleList.takeFirst();
}


//! Creates a new fragmentation specification.
/*! The newly created fragmentation specification is initialized
  according to \c this. The list of rules is initialized also.

  \return The newly created fragmentation specification, which must be
  deleted when no longer in use.
*/
FragSpec *
FragSpec::clone() const
{
  FragSpec *other = new FragSpec(*this);

  return other;
}


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

  if(other == this)
    return;

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

  other->m_fragEnd             = m_fragEnd;
  other->m_monomerContribution = m_monomerContribution;
  other->m_comment             = m_comment;

  while(!other->m_ruleList.isEmpty())
    delete other->m_ruleList.takeFirst();

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

      other->m_ruleList.append(fragRule);
    }
}


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

  PolChemDefEntity::mold(other);
  Formula::mold(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);
    }
}


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

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

  return *this;
}


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


//! Append fragmentation rule to the list.
/*!

  \param rule Rule to be appended to the list of rules. Cannot be 0.
*/
void
FragSpec::appendRule(FragRule *rule)
{
  Q_ASSERT(rule);

  m_ruleList.append(rule);
}


//! Insert fragmentation rule to the list at index \p index.
/*!

  \param index Index at which the rule is to be inserted. No bound
  checking is performed.

  \param rule Rule to be appended to the list of rules. Cannot be 0.
*/
void
FragSpec::insertRuleAt(int index, FragRule *rule)
{
  Q_ASSERT(rule);

  m_ruleList.insert(index, rule);
}


//! Remove fragmentation rule from the list at index \p index.
/*!

  \param index Index at which the rule is to be removed. No
  bound-checking is performed.
*/
void
FragSpec::removeRuleAt(int index)
{
  m_ruleList.removeAt(index);
}


//! Sets the fragmentation end.
/*!  \param fragEnd new fragmentation end.
 */
void
FragSpec::setFragEnd(MxtFragEnd fragEnd)
{
  m_fragEnd = fragEnd;
}


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


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


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


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


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


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


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

  \param str name.

  \param refList list of fragmentation specifications.

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

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

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

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

      if(fragSpec->m_name == str)
        {
          if(other)
            fragSpec->clone(other);

          return iter;
        }
    }

  return -1;
}


//! Validates the fragmentation specification.
/*! All the fragmentation rules(if any) are validated also.
  \return true upon success, false otherwise.
*/
bool
FragSpec::validate()
{
  const QList<msXpSlibmass::Atom *> &atomRefList = mp_polChemDef->atomList();

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

  if(!Formula::validate(atomRefList))
    return false;

  return true;
}


//! Parses a fragmentation specification XML element.
/*! Parses the fragmentation specification XML element passed as
  argument and sets its data to \p this fragmentation specification
 (this is called XML rendering).

  \param element XML element to be parsed and rendered.

  \return true if parsing and formula validation were successful,
  false otherwise.

  \sa formatXmlFgsElement(int offset, const QString &indent).
*/
bool
FragSpec::renderXmlFgsElement(const QDomElement &element, int version)
{
  // For the time being the version is not necessary here. As of
  // version up to 2, the current function works ok.

  if(version >= 4)
    return renderXmlFgsElementV2(element);

  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 file format does not prescript the <sidechaincontrib>
  // element, but we have to set the corresponding value to 0.

  m_monomerContribution = 0;

  // 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(mp_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;
}


bool
FragSpec::renderXmlFgsElementV2(const QDomElement &element)
{
  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(mp_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;
}


//! 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 fragmentation
  specification element that is generated in this function looks like
  this:

  \verbatim
  <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>
  \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.

  \sa renderXmlFgsElement(const QDomElement &element).
*/
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 msXpSmassXpert
