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


#include "qmath.h"

/////////////////////// Qt includes
#include <QIODevice>
#include <QDebug>
#include <QFile>


/////////////////////// Local includes
#include <minexpert/nongui/SqlMassDataSlicer.hpp>


namespace msXpSmineXpert
{

int sliceSpecifMetaTypeId =
  qRegisterMetaType<msXpSmineXpert::SliceSpecif>("msXpSmineXpert::SliceSpecif");

int sqlMassDataSlicerMetaTypeId =
  qRegisterMetaType<msXpSmineXpert::SqlMassDataSlicer>(
    "msXpSmineXpert::SqlMassDataSlicer");
int sqlMassDataSlicerPtrMetaTypeId =
  qRegisterMetaType<msXpSmineXpert::SqlMassDataSlicer *>(
    "msXpSmineXpert::SqlMassDataSlicer*");

QMap<int, QString> sliceTypeMap = {
  {SliceType::SLICE_NOT_SET, "NOT_SET"},
  {SliceType::SLICE_ZOOMED_REGION, "ZOOMED_REGION"},
  {SliceType::SLICE_BY_COUNT, "BY_COUNT"},
  {SliceType::SLICE_BY_SIZE, "BY_SIZE"}};


///////////////////////// PRIVATE ////////////////////
///////////////////////// PRIVATE ////////////////////
///////////////////////// PRIVATE ////////////////////

//! Deletes from memory all the SliceSpecif instances.
/*!

  The \c m_sliceSpecifList is emptied and each instance is deleted.

*/
void
SqlMassDataSlicer::clearSliceSpecifications()
{
  while(!m_sliceSpecifList.isEmpty())
    delete m_sliceSpecifList.takeFirst();
}


///////////////////////// PUBLIC ////////////////////
///////////////////////// PUBLIC ////////////////////
///////////////////////// PUBLIC ////////////////////


//! Construct a SqlMassDataSlicer instance
SqlMassDataSlicer::SqlMassDataSlicer()
{
}


//! Construct a SqlMassDataSlicer instance on the basis of \p other.
/*!

  The copying of the \p other data into \c this instance is deep with a copy
  of all the \c m_sliceSpecifList items.

  Note that the value of \p other.mp_sqlHandler cannot be nullptr and that
  pointer is copied as is in \c this instance.

  \sa SqlMassDataSlicer().

*/
SqlMassDataSlicer::SqlMassDataSlicer(const SqlMassDataSlicer &other)
  : m_history{other.m_history},
    m_srcFileName{other.m_srcFileName},
    m_formatString{other.m_formatString},
    m_sliceType{other.m_sliceType},
    m_rangeStart{other.m_rangeStart},
    m_rangeEnd{other.m_rangeEnd},
    m_sliceCount{other.m_sliceCount},
    m_sliceSize{other.m_sliceSize}
{
  for(int iter = 0; iter < other.m_sliceSpecifList.size(); ++iter)
    {
      SliceSpecif *specif = new SliceSpecif;

      specif->fileName = other.m_sliceSpecifList.at(iter)->fileName;
      specif->start    = other.m_sliceSpecifList.at(iter)->start;
      specif->end      = other.m_sliceSpecifList.at(iter)->end;
      specif->number   = other.m_sliceSpecifList.at(iter)->number;

      m_sliceSpecifList.append(specif);
    }
}


//! Construct a SqlMassDataSlicer instance.
/*!

  The construction involves the initialization of member data based on the
  parameters given by the function caller.

  \param history History instance to be used to characterize the data that
  should be sliced.

  \param rangeStart value representing the left border of the initial data
  range to be sliced.

  \param rangeEnd value representing the right border of the initial data
  range to be sliced.

  \sa SqlMassDataSlicer().

*/
SqlMassDataSlicer::SqlMassDataSlicer(History history,
                                     double rangeStart,
                                     double rangeEnd)
  : m_history{history}, m_rangeStart{rangeStart}, m_rangeEnd{rangeEnd}
{
}


//! Destruct \c this SqlMassDataSlicer instance.
/*!

  The SliceSpecif instances allocated and stored in \c m_sliceSpecifList are
  deleted.

*/
SqlMassDataSlicer::~SqlMassDataSlicer()
{
  // We do not own mp_sqlHandler.

  clearSliceSpecifications();
}


//! Initialize the initial member data
SqlMassDataSlicer &
SqlMassDataSlicer::initialize(QString fileName,
                              double rangeStart,
                              double rangeEnd,
                              const History &history)
{
  if(!QFileInfo(fileName).isAbsolute())
    qFatal(
      "Fatal error at %s@%d -- %s(). "
      "The file path name must be absolute."
      "Program aborted.",
      __FILE__,
      __LINE__,
      __FUNCTION__);

  setSrcFileName(fileName);
  setRangeStart(rangeStart);
  setRangeEnd(rangeEnd);
  setHistory(history);

  return *this;
}


//! Initialize \c this Assignment operator.
/*!

  \c this instance is made identical to \p other. The copying of the \p other
  instance data into \c this instance is deep.

  \param other reference to another OASqlMassDataSlicer instance to be used
  for the initialization of \c this instance.

  \return a reference to \c this instance.

  \sa SqlMassDataSlicer(const SqlMassDataSlicer &other).

*/
SqlMassDataSlicer &
SqlMassDataSlicer::initialize(const SqlMassDataSlicer &other)
{
  if(&other == this)
    return *this;

  // Do not copy the pointer to the MassSpecSqlite3Handler!

  m_history      = other.m_history;
  m_rangeStart   = other.m_rangeStart;
  m_rangeEnd     = other.m_rangeEnd;
  m_srcFileName  = other.m_srcFileName;
  m_formatString = other.m_formatString;
  m_sliceType    = other.m_sliceType;
  m_rangeStart   = other.m_rangeStart;
  m_rangeEnd     = other.m_rangeEnd;
  m_sliceCount   = other.m_sliceCount;
  m_sliceSize    = other.m_sliceSize;

  for(int iter = 0; iter < other.m_sliceSpecifList.size(); ++iter)
    {
      SliceSpecif *specif = new SliceSpecif;

      specif->fileName = other.m_sliceSpecifList.at(iter)->fileName;
      specif->start    = other.m_sliceSpecifList.at(iter)->start;
      specif->end      = other.m_sliceSpecifList.at(iter)->end;
      specif->number   = other.m_sliceSpecifList.at(iter)->number;

      m_sliceSpecifList.append(specif);
    }

  return *this;
}


//! Assignment operator.
/*!

  \c this instance is made identical to \p other. The copying of the \p other
  instance data into \c this instance is deep.

  \param other reference to another OASqlMassDataSlicer instance to be used
  for the initialization of \c this instance.

  \return a reference to \c this instance.

  \sa SqlMassDataSlicer(const SqlMassDataSlicer &other).
  \sa SqlMassDataSlicer::initialize(const SqlMassDataSlicer &other).

*/
SqlMassDataSlicer &
SqlMassDataSlicer::operator=(const SqlMassDataSlicer &other)
{
  return initialize(other);
}


//! Reset \c this SqlMassDataSlicer instance.
/*!

  Empties this instance (See also) and resets variables to default values.

  \sa clearSliceSpecifications().
  */
void
SqlMassDataSlicer::reset()
{
  m_sliceType = SliceType::SLICE_NOT_SET;
  m_formatString.clear();
  clearSliceSpecifications();
  m_sliceCount = 0;
  m_sliceSize  = 0;
}


//! Configure a number of the slicing operation parameters.
/*!

  The parameters, like the number of slices or their size, are computed
  depending on the value of some of the member of the instance.

  \param errors pointer to a string in which to record errors encountered
  during the configuration of the specifications. Cannot be nullptr.

  \return true upon success, false otherwise.

*/
bool
SqlMassDataSlicer::configureSpecifs(QString *errors)
{
  if(errors == Q_NULLPTR)
    qFatal("Fatal error at %s@%d. Program aborted.", __FILE__, __LINE__);

  QString errorString;
  int errorCount = 0;

  if(m_formatString.isEmpty())
    {
      errorString += "The format string is empty.\n";
      ++errorCount;
    }

  // We have the [start-end] data range to slice by a slice size.
  double origDataRange = m_rangeEnd - m_rangeStart;

  if(m_sliceType == SliceType::SLICE_ZOOMED_REGION)
    {
      // This is an easy situation. All we need is to set m_sliceCount to 1.
      m_sliceCount = 1;

      // qDebug() << __FILE__ << __LINE__ << __FUNCTION__ << "()"
      //<< "Setting m_sliceCount to 1 because SliceType::SLICE_ZOOMED_REGION"
      //<< "with m_rangeStart:" << m_rangeStart << "and m_rangeEnd:" <<
      //m_rangeEnd;
    }
  else if(m_sliceType == SliceType::SLICE_BY_SIZE)
    {
      if(m_sliceSize == 0)
        {
          errorString += "The slice size is 0.\n";
          ++errorCount;
        }

      // We want a round number of slices
      m_sliceCount = ceil(origDataRange / m_sliceSize);

      // qDebug() << __FILE__ << __LINE__
      //<< "m_sliceSize is" << m_sliceSize
      //<< "Setting m_sliceCount to" << m_sliceCount
      //<< "because SliceType::SLICE_BY_SIZE";
    }
  else
    {
      if(m_sliceType != SliceType::SLICE_BY_COUNT)
        {
          errorString += "The slice type should be SLICE_BY_COUNT.\n";
          ++errorCount;
        }

      if(m_sliceCount == 0)
        {
          errorString += "The slice count is 0.\n";
          ++errorCount;
        }

      // We have the [start-end] data range to slice by a slice count
      m_sliceSize = origDataRange / m_sliceCount;

      // qDebug() << __FILE__ << __LINE__
      //<< "Setting m_sliceSize to" << m_sliceSize;
    }

  // qDebug() << __FILE__ << __LINE__
  //<< "Before making the slicing, SqlMassDataSlicer.asText():" << asText();

  // At this point, m_sliceCount and m_sliceRange will have been filled-up
  // with proper values.

  for(int iter = 0; iter < m_sliceCount; ++iter)
    {
      SliceSpecif *sliceSpecif = new SliceSpecif;

      // Set the number of the slice that will be required for the crafting of
      // the fileName.
      sliceSpecif->number = iter + 1;

      sliceSpecif->start = m_rangeStart + (iter * m_sliceSize);

      if(iter == (m_sliceCount - 1))
        sliceSpecif->end = m_rangeEnd;
      else
        sliceSpecif->end = sliceSpecif->start + m_sliceSize;

      QString fileName = craftSliceFileName(sliceSpecif);
      if(fileName.isEmpty())
        {
          errorString += "Failed to craft a file name for specif:.\n";
          errorString += sliceSpecifAsText(sliceSpecif);
          errorString += "\n";

          ++errorCount;
        }

      // At this point, the name can go to the specif:
      sliceSpecif->fileName = fileName;

      // qDebug() << __FILE__ << __LINE__ << "Slice specif:"
      //<< "[" << sliceSpecif->start << "-" << sliceSpecif->end
      //<< "] ; " << sliceSpecif->fileName;

      // Finally append the sliceSpecif to the list.
      m_sliceSpecifList.append(sliceSpecif);
    }

  if(errorCount > 0)
    {
      *errors = errorString;

      return false;
    }

  return true;
}


//! Set the smallest value of the range to be sliced off
void
SqlMassDataSlicer::setRangeStart(double value)
{
  m_rangeStart = value;
}


//! Return the smallest value of the range to be sliced off
double
SqlMassDataSlicer::rangeStart() const
{
  return m_rangeStart;
}


//! Set the greatest value of the range to be sliced off
void
SqlMassDataSlicer::setRangeEnd(double value)
{
  m_rangeEnd = value;
}


//! Return the greatest value of the range to be sliced off
double
SqlMassDataSlicer::rangeEnd() const
{
  return m_rangeEnd;
}


//! Set the values of the range to be sliced off
void
SqlMassDataSlicer::setRange(double start, double end)
{
  setRangeStart(start);
  setRangeEnd(end);
}


//! Set the caller widget's History
void
SqlMassDataSlicer::setHistory(const History &history)
{
  m_history = history;
}


//! Set the type of slicing method
void
SqlMassDataSlicer::setSliceType(int type)
{
  m_sliceType = type;
}


//! Set the size of the slices
void
SqlMassDataSlicer::setSliceSize(double size)
{
  m_sliceSize = size;
}

//! Set the number of slices
void
SqlMassDataSlicer::setSliceCount(int count)
{
  m_sliceCount = count;
}


//! Set the directory where the files have to be created
void
SqlMassDataSlicer::setDestDirectory(QString dir)
{
  m_destDirectory = dir;
}


//! Return the directory where the files have to be created
QString
SqlMassDataSlicer::destDirectory() const
{
  return m_destDirectory;
}


//! Set the source data file name
void
SqlMassDataSlicer::setSrcFileName(QString fileName)
{
  m_srcFileName = fileName;
  // qDebug() << __FILE__ << __LINE__ << __FUNCTION__ << "()"
  //<< "m_srcFileName:" << m_srcFileName;
}


//! Return the source data file name
QString
SqlMassDataSlicer::srcFileName() const
{
  return m_srcFileName;
}


//! Set the format string to be used to craft filenames
void
SqlMassDataSlicer::setFormatString(QString format)
{
  m_formatString = format;
}

//! Return the format string to be used to craft filenames
QString
SqlMassDataSlicer::formatString() const
{
  return m_formatString;
}


//! Craft the name of the slice file according to the \p sliceSpecif.
/*!

  Depending on the contents of the \p sliceSpecif SliceSpecif instance, the
  name of the slice file would change because \c m_formatString might contain
  the values of the sliced data range, for example. This function parses \c
  m_formatString and uses the \p sliceSpecif contents to craft the file name
  for the slice.

  Note that if \c m_formatString is empty, an empty string is returned.

  \param sliceSpecif pointer to a SliceSpecif instance containing the
  specifications for the slice of which the file name is to be crafted.

  \return a string with the crafted slice file name.

*/
QString
SqlMassDataSlicer::craftSliceFileName(SliceSpecif *sliceSpecif) const
{
  if(m_formatString.isEmpty())
    return QString();

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

  QString fileName;
  QChar prevChar = ' ';

  for(int iter = 0; iter < m_formatString.size(); ++iter)
    {
      QChar curChar = m_formatString.at(iter);

      // qDebug() << __FILE__ << __LINE__
      // << "Current char:" << curChar;

      if(curChar == '\\')
        {
          if(prevChar == '\\')
            {
              fileName += '\\';
              prevChar = ' ';
              continue;
            }
          else
            {
              prevChar = '\\';
              continue;
            }
        }

      if(curChar == '%')
        {
          if(prevChar == '\\')
            {
              fileName += '%';
              prevChar = ' ';
              continue;
            }
          else
            {
              prevChar = '%';
              continue;
            }
        }

      if(prevChar == '%')
        {
          // The current character might have a specific signification.
          if(curChar == 'f')
            {
              // Insert the source data file name but without the extension.
              QFileInfo fileInfo(m_srcFileName);
              fileName += fileInfo.completeBaseName();
              prevChar = ' ';
              continue;
            }
          if(curChar == 's')
            {
              // Insert the slice range start
              fileName += QString("%1").arg(sliceSpecif->start);
              prevChar = ' ';
              continue;
            }
          if(curChar == 'e')
            {
              // Insert the slice range end
              fileName += QString("%1").arg(sliceSpecif->end);
              prevChar = ' ';
              continue;
            }
          // Insert the current slice number
          if(curChar == 'n')
            {
              fileName += QString("%1").arg(sliceSpecif->number);

              // qDebug() << __FILE__ << __LINE__ << "fileName: " << fileName;

              prevChar = ' ';
              continue;
            }
          if(curChar == 'c')
            {
              // Inser the total slice count
              fileName += QString("%1").arg(m_sliceCount);
              prevChar = ' ';
              continue;
            }

          // At this point the '%' is not followed by any special character
          // above, so we skip them both from the text. If the '%' is to be
          // printed, then it needs to be escaped.

          continue;
        }
      // End of
      // if(prevChar == '%')

      // The character prior this current one was not '%' so we just append
      // the current character.
      fileName += curChar;
    }
  // End of
  // for (int iter = 0; iter < pattern.size(); ++iter)

  QFileInfo fileInfo(fileName);
  if(!fileInfo.isAbsolute())
    fileName = m_destDirectory + "/" + fileName;

  return fileName;
}


//! Concatenate all the fileName members of all the slice specifications.
/*!

  The \c m_sliceSpecifList is iterated over and the \c fileName member of each
  SliceSpecif instance is appended to a local string that is then returned.
  The file names are separated by a newline characters "\n".

  \return A string corresponding to the concatenation of all the \c fileName
  members of all the SliceSpecif instances in the member list.

*/
QString
SqlMassDataSlicer::allFileNamesAsString() const
{
  QString fileNames;
  int listSize = m_sliceSpecifList.size();

  for(int iter = 0; iter < listSize; ++iter)
    {
      fileNames += m_sliceSpecifList.at(iter)->fileName;
      fileNames += "\n";
    }

  return fileNames;
}


//! Slice the initial mass data into smaller chunk.
/*!

  Each smaller chunk goes to a new SQLite3-based formatted mass data file. The
  slicing operation is carried over according to the values of the various
  members of \c this instance. The final data that are accounted for are the
  various SliceSpecif instances allocated, configured and stored in \c
  m_sliceSpecifList. There is one SliceSpecif instance per slice file.

  It is the responsibility of the programmer to make sure this function is
  called when all the input data have been validated. That means that the list
  of slice specifications must contain at least one element and that the
  element(s)' data are correct.

  Note that if a slice is configured by the \c fileName member of its
  corresponding SliceSpecif instance to be stored in a file that exists
  already, the file is "silently" overwritten with only a debugging message
  sent to the terminal window.

  \param mp_sqlHandler pointer to the MassSpecSqlite3Handler to use for the
  slicing operation.

  \return true if the slicing operation succeeded, false otherwise.

*/
QString
SqlMassDataSlicer::slice(MassSpecSqlite3Handler *mp_sqlHandler)
{
  if(mp_sqlHandler == Q_NULLPTR)
    qFatal(
      "Fatal error at %s@%d -- %s(). "
      "Cannot be that the pointer to the SQLite3 data handler is nullptr."
      "Program aborted.",
      __FILE__,
      __LINE__,
      __FUNCTION__);

  int sliceSpecifListSize = m_sliceSpecifList.size();

  // Set aside a string to collect the errors and return them.
  QString errors;

  if(sliceSpecifListSize == 0)
    {
      errors += QString("%1 %2 %3 %4")
                  .arg(__FILE__)
                  .arg(__LINE__)
                  .arg(__FUNCTION__)
                  .arg("There are no slice specifications in the list.");

      return errors;
    }

  // qDebug() << __FILE__ << __LINE__ << __FUNCTION__ << "()"
  //<< "allSliceSpecifsAsText:" << allSliceSpecifsAsText();

  for(int iter = 0; iter < sliceSpecifListSize; ++iter)
    {
      SliceSpecif *sliceSpecif = m_sliceSpecifList.at(iter);

      if(sliceSpecif->fileName.isEmpty())
        {
          errors += QString("%1 %2 %3 %4")
                      .arg(__FILE__)
                      .arg(__LINE__)
                      .arg(__FUNCTION__)
                      .arg("The file name is empty.");

          return errors;
        }

      QFile file(sliceSpecif->fileName);
      if(file.exists())
        {
          qDebug() << __FILE__ << __LINE__ << "File" << sliceSpecif->fileName
                   << "exists already, overwriting it.";

          file.resize(0);
          file.close();
        }

      emit updateSliceFileName(sliceSpecif->fileName);

      // qDebug() << __FILE__ << __LINE__ << __FUNCTION__
      //<< "Now handling slice:" << sliceSpecifAsText(sliceSpecif);

      // We need to update *INSIDE, that is IN PLACE* the member History the
      // last item such that the slicer will have the sliceSpecif range values
      // for current slice.

      HistoryItem *item = Q_NULLPTR;

      IntegrationRange *integrationRange = Q_NULLPTR;

      item = m_history.lastHistoryItem(IntegrationType::RT_TO_XXX,
                                       &integrationRange);
      if(item == Q_NULLPTR)
        item = m_history.lastHistoryItem(IntegrationType::MZ_TO_XXX,
                                         &integrationRange);
      if(item == Q_NULLPTR)
        item = m_history.lastHistoryItem(IntegrationType::DT_TO_XXX,
                                         &integrationRange);
      if(item == Q_NULLPTR)
        {
          errors +=
            QString("%1 %2 %3 %4")
              .arg(__FILE__)
              .arg(__LINE__)
              .arg(__FUNCTION__)
              .arg("There is no XXX_TO_YY IntegrationType history item.");

          return errors;
        }

      // Modify *IN PLACE* the integration ranges to match the current slice
      // limits.

      integrationRange->start = sliceSpecif->start;
      integrationRange->end   = sliceSpecif->end;

      // qDebug() << __FILE__ << __LINE__ << __FUNCTION__
      //<< "m_history as transmitted to the sql handler:" << m_history.asText();

      errors = mp_sqlHandler->exportData(
        m_srcFileName, sliceSpecif->fileName, m_history);

      // If there were errors, then return immediately.
      if(!errors.isEmpty())
        return errors;
    }
  // End of
  // for(int iter = 0; iter < sliceSpecifListSize; ++iter)

  return errors;
}


//! Create a textual representation of \c this SqlMassDataSlicer instance
//! contents.
/*!

  \return a string containing a textual representation of \c this
  SqlMassDataSlicer instance contents.

*/
QString
SqlMassDataSlicer::asText() const
{
  QString text;

  text += QString(
            "History:\n%1\n\nSource file name: %2\n"
            "Format string: %3\n"
            "m_rangeStart: %4\n"
            "m_rangeEnd: %5\n"
            "m_sliceCount: %6\n"
            "m_sliceSize: %7\n"
            "Splice specifications:\n%8\n")
            .arg(m_history.asText())
            .arg(m_srcFileName)
            .arg(m_formatString)
            .arg(m_rangeStart)
            .arg(m_rangeEnd)
            .arg(m_sliceCount)
            .arg(m_sliceSize)
            .arg(allSliceSpecifsAsText());

  return text;
}


//! Create a textual representation of \c specif.
/*!

  \return a string containing a textual representation of \c this
  SqlMassDataSlicer instance contents.

*/
QString
SqlMassDataSlicer::sliceSpecifAsText(SliceSpecif *specif) const
{
  if(specif == Q_NULLPTR)
    qFatal("Fatal error at %s@%d. Program aborted.", __FILE__, __LINE__);

  QString text;

  text += QString("%1 : [%2-%3]\n")
            .arg(specif->fileName)
            .arg(specif->start, 0, 'f', 6)
            .arg(specif->end, 0, 'f', 6);

  return text;
}


//! Create a textual representation of all the SliceSpecif instances.
/*!

  The SliceSpecif instances stored in \c m_sliceSpecifList are iterated over
  and for each one a string is created (See also) and concatenated to a local
  string. That local string is then returned.

  \return a string containing a textual representation of all the SliceSpecif
  instances stored in \c m_sliceSpecifList.

*/
QString
SqlMassDataSlicer::allSliceSpecifsAsText() const
{
  QString text;

  for(int iter = 0; iter < m_sliceSpecifList.size(); ++iter)
    {
      text += sliceSpecifAsText(m_sliceSpecifList.at(iter));
    }

  return text;
}


} // namespace msXpSmineXpert
