/* massXpert - the true massist's program.
   --------------------------------------
   Copyright(C) 2006,2007 Filippo Rusconi

   http://www.massxpert.org/massXpert

   This file is part of the massXpert project.

   The massxpert project is the successor to the "GNU polyxmass"
   project that is an official GNU project package(see
   www.gnu.org). The massXpert project is not endorsed by the GNU
   project, although it is released ---in its entirety--- under the
   GNU General Public License. A huge part of the code in massXpert
   is actually a C++ rewrite of code in GNU polyxmass. As such
   massXpert was started at the Centre National de la Recherche
   Scientifique(FRANCE), that granted me the formal authorization to
   publish it under this Free Software License.

   This software is free software; you can redistribute it and/or
   modify it under the terms of the GNU  General Public
   License version 3, as published by the Free Software Foundation.
   

   This software 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 software; if not, write to the

   Free Software Foundation, Inc.,

   51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
*/


/////////////////////// Local includes
#include "fragSpec.hpp"
#include "polChemDef.hpp"


namespace massXpert
{

  //! 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 MXT_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 = MXT_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 = MXT_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<Atom *> &atomRefList = mp_polChemDef->atomList();

    if (m_name.isEmpty())
      return false;
  
    if (m_fragEnd != MXT_FRAG_END_NONE 
	&& m_fragEnd != MXT_FRAG_END_LEFT
	&& m_fragEnd != MXT_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 = MXT_FRAG_END_NONE;
    else if (child.text() == "LE")
      m_fragEnd = MXT_FRAG_END_LEFT;
    else if (child.text() == "RE")
      m_fragEnd = MXT_FRAG_END_RIGHT;
    else if (child.text() == "BE")
      m_fragEnd = MXT_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 = MXT_FRAG_END_NONE;
    else if (child.text() == "LE")
      m_fragEnd = MXT_FRAG_END_LEFT;
    else if (child.text() == "RE")
      m_fragEnd = MXT_FRAG_END_RIGHT;
    else if (child.text() == "BE")
      m_fragEnd = MXT_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 == MXT_FRAG_END_NONE)
      *string += QString("%1<end>%2</end>\n")
	.arg(lead)
	.arg("NE");
    else if (m_fragEnd == MXT_FRAG_END_BOTH)
      *string += QString("%1<end>%2</end>\n")
	.arg(lead)
	.arg("BE");
    else if (m_fragEnd == MXT_FRAG_END_LEFT)
      *string += QString("%1<end>%2</end>\n")
	.arg(lead)
	.arg("LE");
    else if (m_fragEnd == MXT_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 massXpert
