/* 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
 */


/////////////////////// Qt includes
#include <QMessageBox>
#include <QTextDocumentFragment>
#include <QProgressDialog>


/////////////////////// Local includes
#include "SequencePurificationDlg.hpp"


namespace MsXpS
{

namespace massxpert
{


  enum
  {
    NONE  = 0 << 1,
    LEFT  = 1 << 1,
    RIGHT = 2 << 1,
  };

  SequencePurificationDlg::SequencePurificationDlg(
    SequenceEditorWnd *editorWnd,
    /* no libXpertMass::Polymer **/
    /* no libXpertMass::PolChemDef **/
    const QString &settingsFilePath,
    const QString &applicationName,
    const QString &description,
    libXpertMass::Sequence *sequence,
    QList<int> *errorList)
    : AbstractSeqEdWndDependentDlg(editorWnd,
                                   0 /*polymer **/,
                                   0 /*polChemDef **/,
                                   settingsFilePath,
                                   "SequencePurificationDlg",
                                   applicationName,
                                   description)
  {
    if(!sequence || !errorList)
      qFatal("Fatal error at %s@%d. Program aborted.", __FILE__, __LINE__);

    mpa_origSequence = new libXpertMass::Sequence(*sequence->monomerText());
    m_errorList      = *errorList;
    m_firstRound     = true;

    if(!initialize())
      qFatal("Fatal error at %s@%d. Program aborted.", __FILE__, __LINE__);
  }


  SequencePurificationDlg::~SequencePurificationDlg()
  {
    delete mpa_origSequence;
  }


  bool
  SequencePurificationDlg::initialize()
  {
    m_ui.setupUi(this);

    // Update the window title because the window title element in m_ui might be
    // either erroneous or empty.
    setWindowTitle(
      QString("%1 - %2").arg(m_applicationName).arg(m_windowDescription));

    // Make the connections between QTextEdits and QTextDocuments.
    setupDocuments();

    // Seed the system by putting the original sequence text into the
    // left QTextEdit.
    setOrigText();

    connect(m_ui.purifyPushButton, SIGNAL(clicked()), this, SLOT(purify()));

    connect(m_ui.removeTaggedPushButton,
            SIGNAL(clicked()),
            this,
            SLOT(removeTagged()));

    connect(m_ui.testPushButton, SIGNAL(clicked()), this, SLOT(test()));

    connect(m_ui.resetPushButton, SIGNAL(clicked()), this, SLOT(reset()));

    return true;
  }


  void
  SequencePurificationDlg::setupDocuments()
  {
    m_leftTextDocument.setParent(this);
    m_rightTextDocument.setParent(this);

    m_ui.leftSequenceTextEdit->setDocument(&m_leftTextDocument);
    m_ui.rightSequenceTextEdit->setDocument(&m_rightTextDocument);
  }


  void
  SequencePurificationDlg::setOrigText()
  {
    const QString *text = mpa_origSequence->monomerText();

    bool res = text->isEmpty();

    if(res)
      qFatal("Fatal error at %s@%d. Program aborted.", __FILE__, __LINE__);

    m_leftTextDocument.setPlainText(*text);

    // And now make the highlighting of the invalid characters, using
    // the list of indices of invalid chars.
    setCursor(Qt::WaitCursor);

    doTest(m_leftTextDocument);

    setCursor(Qt::ArrowCursor);

    m_firstRound = true;
  }


  void
  SequencePurificationDlg::doTest(QTextDocument &document)
  {
    if(document.isEmpty())
      return;

    QTextCursor cursor(&document);

    QTextCharFormat normalFormat;
    normalFormat.setFontUnderline(false);

    QTextCharFormat underlinedFormat;
    underlinedFormat.setFontUnderline(true);

    // List of int indices where errors were found.
    QList<int> errorList;

    // Create a sequence with the document text.
    libXpertMass::Sequence sequence(document.toPlainText());

    if(sequence.makeMonomerList(
         mp_editorWnd->polymer()->getPolChemDefCstSPtr(), true, &errorList) != -1)
      {
        // No errors were found, we can set all the text in normal font.
        cursor.select(QTextCursor::Document);

        cursor.setCharFormat(normalFormat);

        return;
      }

    // Errors were found, format all single erroneous positions.

    QProgressDialog progress(
      tr("Task already performed..."), tr("Abort"), 0, errorList.size(), this);

    progress.setWindowModality(Qt::WindowModal);
    progress.setWindowTitle(tr("Formatting progression"));

    int listSize = errorList.size();

    for(int iter = 0; iter < listSize; ++iter)
      {
        progress.setValue(iter);
        qApp->processEvents();
        if(progress.wasCanceled())
          break;

        // There should be room for optimization by looking for
        // continuous stretches of invalid characters.  For example,
        // imagine we have the following indices in the errorList:

        // 1 5 6 7 8 9 12 16 17 18 19 20

        // We may joing [5-9] in one move position. Same applies to
        // [16-20].

        int endIndex = 0;

        // This optimization brought an increase in performance of a
        // factor 10 !!! The other solution was to only perform task
        // in the other else block.

        if(findContinuum(errorList, iter, endIndex))
          {
            int span = errorList.at(endIndex) - errorList.at(iter) + 1;

            cursor.setPosition(errorList.at(iter), QTextCursor::MoveAnchor);

            cursor.movePosition(
              QTextCursor::NextCharacter, QTextCursor::KeepAnchor, span);

            cursor.setCharFormat(underlinedFormat);

            iter = endIndex;

            continue;
          }
        else
          {
            cursor.setPosition(errorList.at(iter), QTextCursor::MoveAnchor);

            cursor.movePosition(QTextCursor::NextCharacter,
                                QTextCursor::KeepAnchor);

            cursor.setCharFormat(underlinedFormat);
          }
      }

    progress.setValue(errorList.size());
  }


  void
  SequencePurificationDlg::purify()
  {
    if(m_firstRound)
      {
        // This is the first round of purification. Which means, the
        // original text is in the left QTextEdit(see
        // setOrigText()). We copy that text in m_rightTextDocument and
        // purify that last copy and put the resulting text into the
        // right QTextEdit.

        // Select all the text in the left text document.
        QTextCursor leftCursor(&m_leftTextDocument);
        leftCursor.select(QTextCursor::Document);

        // Make a fragment out of that selected text.
        QTextDocumentFragment fragment(leftCursor.selection());

        // Copy that fragment into the right text document, after having
        // removed all its initial contents.

        // Select all the text in the right text document. And remove all the
        // selection, so that the documents ends empty.
        QTextCursor rightCursor(&m_rightTextDocument);
        rightCursor.select(QTextCursor::Document);
        rightCursor.removeSelectedText();

        // And now copy the fragment from the left text document into the
        // right text document.
        rightCursor.insertFragment(fragment);
      }
    else
      {
        // This is not the first round of purification. That means that we
        // first have to copy the right text in the left text. Then, we'll
        // modify the right text.

        // Select all the text in the right text document.
        QTextCursor rightCursor(&m_rightTextDocument);
        rightCursor.select(QTextCursor::Document);

        // Make a fragment out of that selected text.
        QTextDocumentFragment fragment(rightCursor.selection());

        // Copy that fragment into the left text document, after having
        // removed all its initial contents.

        // Select all the text in the left text document. And remove all the
        // selection, so that the documents ends empty.
        QTextCursor leftCursor(&m_leftTextDocument);
        leftCursor.select(QTextCursor::Document);
        leftCursor.removeSelectedText();

        // And now copy the fragment from the right text document into the
        // left text document.
        leftCursor.insertFragment(fragment);
      }

    if(m_rightTextDocument.isEmpty())
      return;

    // And now we can perform the purification in the right text
    // document.

    bool numerals    = m_ui.numeralsCheckBox->checkState();
    bool punctuation = m_ui.punctuationCheckBox->checkState();
    bool spaces      = m_ui.spacesCheckBox->checkState();
    bool lowercase   = m_ui.lowercaseCheckBox->checkState();
    bool uppercase   = m_ui.uppercaseCheckBox->checkState();
    QString others   = m_ui.othersLineEdit->text();

    QTextCursor cursor(&m_rightTextDocument);

    if(numerals)
      {
        QRegularExpression regExp("\\d+",
                                  QRegularExpression::CaseInsensitiveOption);

        cursor = m_rightTextDocument.find(regExp, cursor);

        while(!cursor.isNull())
          {
            cursor.deleteChar();
            cursor = m_rightTextDocument.find(regExp, cursor);
          }
      }

    cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);

    if(spaces)
      {
        QRegularExpression regExp("\\s+",
                                  QRegularExpression::CaseInsensitiveOption);

        cursor = m_rightTextDocument.find(regExp, cursor);

        while(!cursor.isNull())
          {
            cursor.deleteChar();
            cursor = m_rightTextDocument.find(regExp, cursor);
          }
      }

    cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);

    if(punctuation)
      {
        QRegularExpression regExp(
          "@|~|&|\\(|\\)|\\{|\\}|\\[|\\]|=|<|>|#|="
          "|\\\\|/|%|\\+|_|-|;|:|\\.|'|!|\\?|\\\"|\\|",
          QRegularExpression::CaseInsensitiveOption);

        cursor = m_rightTextDocument.find(regExp, cursor);

        while(!cursor.isNull())
          {
            cursor.deleteChar();
            cursor = m_rightTextDocument.find(regExp, cursor);
          }
      }

    cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);

    if(lowercase)
      {
        QRegularExpression regExp("[a-z]",
                                  QRegularExpression::CaseInsensitiveOption);

        cursor = m_rightTextDocument.find(regExp, cursor);

        while(!cursor.isNull())
          {
            cursor.deleteChar();
            cursor = m_rightTextDocument.find(regExp, cursor);
          }
      }

    cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);

    if(uppercase)
      {
        QRegularExpression regExp("[A-Z]",
                                  QRegularExpression::CaseInsensitiveOption);

        cursor = m_rightTextDocument.find(regExp, cursor);

        while(!cursor.isNull())
          {
            cursor.deleteChar();
            cursor = m_rightTextDocument.find(regExp, cursor);
          }
      }

    cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);

    if(!others.isEmpty())
      {
        QRegularExpression regExp(others,
                                  QRegularExpression::CaseInsensitiveOption);

        if(regExp.isValid())
          {
            cursor = m_rightTextDocument.find(regExp, cursor);

            while(!cursor.isNull())
              {
                cursor.deleteChar();
                cursor = m_rightTextDocument.find(regExp, cursor);
              }
          }
        else
          qDebug() << "RegExp:" << others << "is not valid";
      }

    m_firstRound = false;

    return;
  }


  void
  SequencePurificationDlg::removeTagged()
  {
    // The original text is in the left QTextEdit(see
    // setOrigText()). We copy that text in m_rightTextDocument and
    // purify that last copy and put the resulting text into the right
    // QTextEdit.

    // Select all the text in the left text document.
    QTextCursor leftCursor(&m_leftTextDocument);
    leftCursor.select(QTextCursor::Document);

    // Make a fragment out of that selected text.
    QTextDocumentFragment fragment(leftCursor.selection());

    // Copy that fragment into the right text document, after having
    // removed all its initial contents.

    // Select all the text in the right text document. And remove all the
    // selection, so that the documents ends empty.
    QTextCursor rightCursor(&m_rightTextDocument);
    rightCursor.select(QTextCursor::Document);
    rightCursor.removeSelectedText();

    // And now copy the fragment from the left text document into the
    // right text document.
    rightCursor.insertFragment(fragment);

    if(m_rightTextDocument.isEmpty())
      return;

    // And now we can perform the purification in the right text
    // document. Note that this work is extremely resource-intensive. We
    // better give progress feedback to the user... and allow her to
    // abort the process!

    QTextCursor cursor(&m_rightTextDocument);

    bool ret =
      cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::MoveAnchor);

    int position = cursor.position();

    QProgressDialog progress(
      tr("Task remaining to perform..."), tr("Abort"), 0, position, this);

    progress.setWindowModality(Qt::WindowModal);
    progress.setWindowTitle(tr("Formatting progression"));

    while(ret)
      {
        progress.setValue(position);
        qApp->processEvents();
        if(progress.wasCanceled())
          break;

        // Check char format of char right before the cursor.

        QTextCharFormat format = cursor.charFormat();

        if(format.fontUnderline())
          {
            cursor.deletePreviousChar();

            continue;
          }

        ret = cursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor);

        position = cursor.position();

        if(!position)
          break;
      }

    progress.setValue(position);
  }


  void
  SequencePurificationDlg::test()
  {
    doTest(m_rightTextDocument);
  }


  void
  SequencePurificationDlg::reset()
  {
    setOrigText();
    m_ui.rightSequenceTextEdit->clear();
  }


  bool
  SequencePurificationDlg::findContinuum(QList<int> &errorList,
                                         int firstIndex,
                                         int &secondIndex)
  {
    int listSize = errorList.size();

    Q_ASSERT(firstIndex >= 0 && firstIndex < listSize);

    if(firstIndex == listSize - 2)
      return false;

    int currentValue  = 0;
    int previousValue = errorList.at(firstIndex);

    int startIndex = firstIndex + 1;
    int iter       = 0;

    for(iter = startIndex; iter < listSize; ++iter)
      {
        currentValue = errorList.at(iter);

        if(currentValue == previousValue + 1)
          {
            previousValue = currentValue;
            continue;
          }
        else
          {
            --iter;
            break;
          }
      }

    if(iter == listSize)
      // We got to the end of the list. Decrement iter by one fake run.
      --iter;

    if(iter - startIndex >= 1)
      {
        secondIndex = iter;
        return true;
      }

    return false;
  }

} // namespace massxpert

} // namespace MsXpS
