/* 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 <massxpert/gui/SequencePurificationDlg.hpp>


namespace msXpSmassXpert
{

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

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

  mpa_origSequence = new 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);

  // 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.
  Sequence sequence(document.toPlainText());

  if(sequence.makeMonomerList(
       mp_editorWnd->polymer()->polChemDef(), 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)
    {
      QRegExp regExp("\\d+", Qt::CaseInsensitive, QRegExp::RegExp2);

      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)
    {
      QRegExp regExp("\\s+", Qt::CaseInsensitive, QRegExp::RegExp2);

      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)
    {
      QRegExp regExp(
        "@|~|&|\\(|\\)|\\{|\\}|\\[|\\]|=|<|>|#|="
        "|\\\\|/|%|\\+|_|-|;|:|\\.|'|!|\\?|\\\"|\\|",
        Qt::CaseInsensitive,
        QRegExp::RegExp2);

      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)
    {
      QRegExp regExp("[a-z]", Qt::CaseSensitive, QRegExp::RegExp2);

      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)
    {
      QRegExp regExp("[A-Z]", Qt::CaseSensitive, QRegExp::RegExp2);

      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())
    {
      QRegExp regExp(others, Qt::CaseSensitive, QRegExp::RegExp2);

      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 msXpSmassXpert
