/**
 * \file pappsomspp/processing/tandemwrapper/tandemwrapperrun.cpp
 * \date 25/01/2020
 * \author Olivier Langella
 * \brief actually does really run tandem directly on Bruker's data
 */

/*******************************************************************************
 * Copyright (c) 2020 Olivier Langella <Olivier.Langella@u-psud.fr>.
 *
 * This file is part of PAPPSOms-tools.
 *
 *     PAPPSOms-tools 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.
 *
 *     PAPPSOms-tools 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 PAPPSOms-tools.  If not, see <http://www.gnu.org/licenses/>.
 *
 ******************************************************************************/

#include "tandemwrapperrun.h"
#include <QDebug>
#include <QFileInfo>
#include <QSettings>
#include <QThread>
#include <QThreadPool>
#include <QRegularExpression>
#include "../../exception/exceptioninterrupted.h"
#include "../../msfile/msfileaccessor.h"
#include "../../msrun/private/timsmsrunreaderms2.h"
#include "../../processing/filters/filtertriangle.h"
#include "../../processing/filters/filterchargedeconvolution.h"
#include "../../msrun/output/mzxmloutput.h"
#include "wraptandemresults.h"
#include "xtandempresetreader.h"
#include "wraptandeminput.h"

namespace pappso
{


TandemWrapperRun::TandemWrapperRun(const QString &tandem_binary,
                                   const QString &tmp_dir)
{

  setTandemBinaryPath(tandem_binary);

  if(!tmp_dir.isEmpty())
    {
      mpa_temporaryDirectory = new QTemporaryDir(tmp_dir + "/xtpwrp");
    }
  else
    {
      mpa_temporaryDirectory = new QTemporaryDir(QDir::tempPath() + "/xtpwrp");
    }
  mpa_temporaryDirectory->setAutoRemove(true);
  if(!mpa_temporaryDirectory->isValid())
    {
      throw pappso::PappsoException(
        QObject::tr("ERROR: unable to create temporary directory %1\n Please "
                    "check file system permissions")
          .arg(mpa_temporaryDirectory->path()));
    }
}

TandemWrapperRun::~TandemWrapperRun()
{
  if(mpa_temporaryDirectory != nullptr)
    {
      delete mpa_temporaryDirectory;
    }

  if(m_xtProcess != nullptr)
    {
      m_xtProcess->deleteLater();
    }
}

void
TandemWrapperRun::setTandemBinaryPath(const QString &tandem_binary_path)
{


  m_tandemBinary = tandem_binary_path;
  QSettings settings;
  if(m_tandemBinary.isEmpty())
    {
      m_tandemBinary =
        settings.value("path/tandem_binary", "/usr/bin/tandem").toString();
    }
  // check for tandem executable
  m_tandemVersion = checkXtandemVersion(m_tandemBinary);

  qDebug() << m_tandemVersion;
  settings.setValue("path/tandem_binary", m_tandemBinary);
}


const QString
TandemWrapperRun::checkXtandemVersion(const QString &tandem_bin_path)
{
  qDebug();
  // check tandem path
  QFileInfo tandem_exe(tandem_bin_path);
  if(!tandem_exe.exists())
    {
      // dir.path() returns the unique directory path
      throw pappso::PappsoException(
        QObject::tr(
          "X!Tandem software not found at %1.\nPlease check the X!Tandem "
          "installation on your computer and set tandem.exe path.")
          .arg(tandem_exe.absoluteFilePath()));
    }
  if(!tandem_exe.isReadable())
    {
      // dir.path() returns the unique directory path
      throw pappso::PappsoException(
        QObject::tr("Please check permissions on X!Tandem software found at %1 "
                    "(file not readable).")
          .arg(tandem_exe.absoluteFilePath()));
    }
  if(!tandem_exe.isExecutable())
    {
      // dir.path() returns the unique directory path
      throw pappso::PappsoException(
        QObject::tr("Please check permissions on X!Tandem software found at %1 "
                    "(file not executable).")
          .arg(tandem_exe.absoluteFilePath()));
    }


  QString version_return;
  QStringList arguments;

  arguments << "-v";

  QProcess *xt_process = new QProcess();
  // hk_process->setWorkingDirectory(QFileInfo(_hardklor_exe).absolutePath());

  xt_process->start(tandem_bin_path, arguments);

  if(!xt_process->waitForStarted())
    {
      throw pappso::PappsoException(
        QObject::tr("X!Tandem %1 process failed to start")
          .arg(m_tandemVersion));
    }

  while(xt_process->waitForReadyRead(1000))
    {
    }
  /*
  if (!xt_process->waitForFinished(_max_xt_time_ms)) {
      throw pappso::PappsoException(QObject::tr("can't wait for X!Tandem process
  to finish : timeout at %1").arg(_max_xt_time_ms));
  }
  */
  QByteArray result = xt_process->readAll();


  qDebug() << result.constData();

  // X! TANDEM Jackhammer TPP (2013.06.15.1 - LabKey, Insilicos, ISB)

  QRegularExpression parse_version(
    "(.*) TANDEM ([A-Z,a-z, ]+) \\(([^ ,^\\)]*)(.*)");
  qDebug() << parse_version;
  // Pattern patt = Pattern.compile("X! TANDEM [A-Z]+ \\((.*)\\)",
  //			Pattern.CASE_INSENSITIVE);
  QRegularExpressionMatch match_parse_version =
    parse_version.match(result.constData());
  if(match_parse_version.hasMatch())
    {
      version_return = QString("X!Tandem %1 %2")
                         .arg(match_parse_version.captured(2))
                         .arg(match_parse_version.captured(3)); //.join(" ");
    }
  else
    {
      throw pappso::PappsoException(
        QObject::tr("This executable %1 may not be a valid X!Tandem software. "
                    "Please check your X!Tandem installation.")
          .arg(tandem_bin_path));
    }

  QProcess::ExitStatus Status = xt_process->exitStatus();
  delete xt_process;
  if(Status != 0)
    {
      // != QProcess::NormalExit
      throw pappso::PappsoException(
        QObject::tr("error executing X!Tandem Status != 0 : %1 %2\n%3")
          .arg(tandem_bin_path)
          .arg(arguments.join(" ").arg(result.data())));
    }
  qDebug();
  return version_return;
}

void
TandemWrapperRun::readyReadStandardOutput()
{
  QString message(m_xtProcess->readAllStandardOutput());
  mp_monitor->appendText(message);

  if(message.toLower().contains("error"))
    {
      throw pappso::XtandemError(message);
    }

  if(mp_monitor->shouldIstop())
    {
      m_xtProcess->kill();
      delete m_xtProcess;
      m_xtProcess = nullptr;
      throw pappso::ExceptionInterrupted(
        QObject::tr("X!Tandem stopped by the user"));
    }
}

void
TandemWrapperRun::readyReadStandardError()
{
  mp_monitor->appendText(m_xtProcess->readAllStandardError());
  if(mp_monitor->shouldIstop())
    {
      m_xtProcess->kill();
      delete m_xtProcess;
      m_xtProcess = nullptr;
      throw pappso::ExceptionInterrupted(
        QObject::tr("X!Tandem stopped by the user"));
    }
}

void
TandemWrapperRun::writeFinalTandemOutput(
  const QString &tmp_tandem_output,
  const QString &final_tandem_output,
  const QString &original_msdata_file_name)
{
  mp_monitor->setStatus(QObject::tr("Rewriting X!Tandem XML result file"));

  WrapTandemResults wrap_output(final_tandem_output, original_msdata_file_name);

  wrap_output.setInputParameters("spectrum, timstof MS2 filters",
                                 getMs2FilterSuiteString());
  wrap_output.setInputParameters("spectrum, mzFormat",
                                 QString("%1").arg((int)m_mzFormat));

  if(m_convertMzDataUsingSpectrumIndex)
    {
      wrap_output.setInputParameters("output, spectrum index", "true");
    }
  else
    {
    }

  if(m_conversionTime != 0)
    {
      wrap_output.setInputParameters(
        "timing, tandemwrapper conversion time (sec)",
        QString("%1").arg(m_conversionTime / 1000));
    }

  if(wrap_output.readFile(tmp_tandem_output))
    {
    }
  else
    {
      throw pappso::PappsoException(
        QObject::tr("Error reading %1 X!Tandem output file :\n %2")
          .arg(tmp_tandem_output)
          .arg(wrap_output.errorString()));
    }
}

void
TandemWrapperRun::readTandemPresetFile(const QString &tandem_preset_file)
{
  // get number of threads and centroid parameters from tandem preset

  XtandemPresetReader preset_handler;


  if(preset_handler.readFile(tandem_preset_file))
    {

      int ideal_number_of_thread = QThread::idealThreadCount();
      int cpu_number             = preset_handler.getNumberOfThreads();
      qDebug() << " cpu_number=" << cpu_number;
      // QThreadPool::globalInstance()->setMaxThreadCount(1);
      if(cpu_number > ideal_number_of_thread)
        {
          cpu_number = ideal_number_of_thread;
        }
      else
        {
          if(cpu_number > 0)
            {
              QThreadPool::globalInstance()->setMaxThreadCount(cpu_number);

              qDebug() << " maxThreadCount="
                       << QThreadPool::globalInstance()->maxThreadCount();
            }
        }

      QString ms2_filters_str = preset_handler.getMs2FiltersOptions();
      if(!ms2_filters_str.isEmpty())
        {
          msp_ms2FilterSuiteString =
            std::make_shared<pappso::FilterSuiteString>(ms2_filters_str);
        }
      else
        {
          msp_ms2FilterSuiteString =
            std::make_shared<pappso::FilterSuiteString>(
              "chargeDeconvolution|0.02dalton mzExclusion|0.01dalton");
        }
    }
  else
    {
      throw pappso::PappsoException(
        QObject::tr("Error reading %1 X!Tandem preset file :\n %2")
          .arg(tandem_preset_file)
          .arg(preset_handler.errorString()));
    }
}


void
TandemWrapperRun::wrapTandemInputFile(const QString &tandem_input_file)
{
  // read original tandem input file
  // store original ms data file name
  // create new mzXML data file in temporary directory
  // create new tandem input file based on new mzXML file


  QString mzxml_data_file_name =
    mpa_temporaryDirectory->filePath("msdata.mzxml");
  QString wrapped_tandem_input =
    mpa_temporaryDirectory->filePath("input_tandem.xml");
  QString wrapped_tandem_output =
    mpa_temporaryDirectory->filePath("output_tandem.xml");

  WrapTandemInput wrap_tandem_input(
    mzxml_data_file_name, wrapped_tandem_input, wrapped_tandem_output);


  if(wrap_tandem_input.readFile(tandem_input_file))
    {
    }
  else
    {
      throw pappso::PappsoException(
        QObject::tr("Error reading %1 X!Tandem input file :\n %2")
          .arg(tandem_input_file)
          .arg(wrap_tandem_input.errorString()));
    }


  if(m_tandemBinary.endsWith("tandemng") ||
     m_tandemBinary.endsWith("tandemng.exe") ||
     m_tandemBinary.endsWith("tandemng2015") ||
     m_tandemBinary.endsWith("tandemng2015.exe") ||
     m_tandemBinary.endsWith("tandemng2015p") ||
     m_tandemBinary.endsWith("tandemng2015p.exe"))
    {
      // no wrapper
      //  launch tandem on original file
      runTandem(tandem_input_file);
    }
  else
    {
      /*
       *
      XtandemInputSaxHandler wrap_input(
        mzxml_data_file_name, wrapped_tandem_input, wrapped_tandem_output);
      QFile qfile(tandem_input_file);
      if(!qfile.exists())
        {
          throw pappso::PappsoException(
            QObject::tr("Tandem input file %1 does not exists")
              .arg(QFileInfo(tandem_input_file).absoluteFilePath()));
        }
      QXmlInputSource xmlInputSource(&qfile);
      QXmlSimpleReader simplereader;
      simplereader.setContentHandler(&wrap_input);
      simplereader.setErrorHandler(&wrap_input);

      if(simplereader.parse(xmlInputSource))
        {
        }
      else
        {
          throw pappso::PappsoException(
            QObject::tr("Error reading %1 X!Tandem input file :\n %2")
              .arg(tandem_input_file)
              .arg(wrap_input.errorString()));
        }
    */
      // get number of threads and centroid parameters from tandem preset
      readTandemPresetFile(wrap_tandem_input.getOriginalTandemPresetFileName());


      // convert to mzXML
      QString original_msdata_file_name =
        wrap_tandem_input.getOriginalMsDataFileName();
      if(convertOrginalMsData2mzXmlData(original_msdata_file_name,
                                        mzxml_data_file_name))
        {


          // launch tandem
          runTandem(wrapped_tandem_input);

          // rewrite tandem result file
          writeFinalTandemOutput(
            wrapped_tandem_output,
            wrap_tandem_input.getOriginalTandemOutputFileName(),
            original_msdata_file_name);
        }
      else
        {
          // launch tandem on original file
          runTandem(tandem_input_file);
        }
    }
}

bool
TandemWrapperRun::convertOrginalMsData2mzXmlData(const QString &origin,
                                                 const QString &target)
{
  qDebug();
  pappso::MsFileAccessor origin_access(origin, "runa1");
  origin_access.setPreferredFileReaderType(pappso::MsDataFormat::brukerTims,
                                           pappso::FileReaderType::tims_ms2);
  origin_access.getMsRunIds();
  m_mzFormat = origin_access.getFileFormat();
  if(m_mzFormat == pappso::MsDataFormat::unknown)
    {
      throw pappso::PappsoException(
        QObject::tr("%1 file format not known").arg(origin));
    }

  if(origin_access.getFileFormat() == pappso::MsDataFormat::brukerTims)
    {
      m_convertMzDataUsingSpectrumIndex = true;
    }

  if((origin_access.getFileFormat() == pappso::MsDataFormat::mzML) ||
     (origin_access.getFileFormat() == pappso::MsDataFormat::brukerTims))
    {
      mp_monitor->setStatus(
        QObject::tr("Converting %1 to mzXML %2").arg(origin).arg(target));
      pappso::MsRunReaderSPtr p_reader;
      p_reader =
        origin_access.msRunReaderSPtr(origin_access.getMsRunIds().front());

      pappso::TimsMsRunReaderMs2 *tims2_reader =
        dynamic_cast<pappso::TimsMsRunReaderMs2 *>(p_reader.get());
      if(tims2_reader != nullptr)
        {
          qDebug();
          tims2_reader->setMs2BuiltinCentroid(true);

          if(msp_ms2FilterSuiteString != nullptr)
            {
              tims2_reader->setMs2FilterCstSPtr(msp_ms2FilterSuiteString);
            }
          qDebug();
        }


      pappso::MzxmlOutput *p_mzxml_output;
      QFile output_file(target);
      // qDebug() << " TsvDirectoryWriter::writeSheet " <<
      // QFileInfo(*_p_ofile).absoluteFilePath();
      if(output_file.open(QIODevice::WriteOnly))
        {
          QElapsedTimer timer;
          m_conversionTime = 0;
          timer.start();
          p_mzxml_output = new pappso::MzxmlOutput(
            *mp_monitor, QTextStream(&output_file).device());

          p_mzxml_output->maskMs1(true);

          p_mzxml_output->setReadAhead(true);

          p_mzxml_output->write(p_reader.get());

          p_mzxml_output->close();

          delete p_mzxml_output;
          m_conversionTime = timer.elapsed();

          mp_monitor->setStatus(QObject::tr("Conversion finished in %1 seconds")
                                  .arg(m_conversionTime / 1000));
        }
      else
        {
          throw pappso::PappsoException(
            QObject::tr("unable to write into %1 mzXML output file")
              .arg(target));
        }

      qDebug();
      return true;
    }
  else
    { // other mz data formats
      return false;
    }
  return true;
}

void
TandemWrapperRun::run(UiMonitorInterface &monitor,
                      const QString &tandem_input_file)
{
  mp_monitor = &monitor;

  wrapTandemInputFile(tandem_input_file);
  mp_monitor = nullptr;
}
void
TandemWrapperRun::runTandem(const QString &tandem_input_file)
{
  if(mp_monitor->shouldIstop())
    {
      throw pappso::ExceptionInterrupted(
        QObject::tr("X!Tandem stopped by the user processing on file %1")
          .arg(tandem_input_file));
    }
  m_xtProcess = new QProcess();
  QStringList arguments;

  qDebug() << m_tandemBinary << " " << m_xtProcess->arguments();

  arguments << tandem_input_file;
  // hk_process->setWorkingDirectory(QFileInfo(_hardklor_exe).absolutePath());
  m_xtProcess->start(m_tandemBinary, arguments);

  qDebug() << m_tandemBinary << " " << m_xtProcess->arguments();

  connect(m_xtProcess,
          &QProcess::readyReadStandardOutput,
          this,
          &TandemWrapperRun::readyReadStandardOutput);
  connect(m_xtProcess,
          &QProcess::readyReadStandardError,
          this,
          &TandemWrapperRun::readyReadStandardError);


  qDebug() << m_tandemBinary << " " << m_xtProcess->arguments();

  mp_monitor->setStatus(QObject::tr("Running X!Tandem"));

  if(!m_xtProcess->waitForStarted())
    {
      throw pappso::PappsoException(
        QObject::tr("X!Tandem process failed to start"));
    }

  qDebug() << m_tandemBinary << " " << m_xtProcess->arguments();
  while(m_xtProcess->waitForFinished(m_maxTandemRunTimeMs) == false)
    {
      //_p_monitor->appendText(xt_process->readAll().data());
      // data.append(xt_process->readAll());
      if(mp_monitor->shouldIstop())
        {
          m_xtProcess->kill();
          delete m_xtProcess;
          m_xtProcess = nullptr;
          throw pappso::ExceptionInterrupted(
            QObject::tr("X!Tandem stopped by the user processing on file %1")
              .arg(tandem_input_file));
        }
    }

  QProcess::ExitStatus Status = m_xtProcess->exitStatus();

  delete m_xtProcess;
  if(Status != QProcess::ExitStatus::NormalExit)
    {
      // != QProcess::NormalExit
      throw pappso::PappsoException(
        QObject::tr("error executing X!Tandem Status != 0 : %1")
          .arg(m_tandemBinary));
    }
  m_xtProcess = nullptr;
}

QString
TandemWrapperRun::getMs2FilterSuiteString() const
{
  if(msp_ms2FilterSuiteString == nullptr)
    return "";
  return msp_ms2FilterSuiteString.get()->toString();
}

} // namespace pappso
