/* BEGIN software license
 *
 * MsXpertSuite - mass spectrometry software suite
 * -----------------------------------------------
 * Copyright (C) 2009--2020 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
 */


/////////////////////// Std lib includes


/////////////////////// Qt includes
#include <QDebug>
#include <QFileInfo>
#include <QThread>

/////////////////////// IsoSpec


/////////////////////// Local includes
#include "MsXpS/libXpertMassCore/MassDataCborMassSpectrumHandler.hpp"

namespace MsXpS
{

namespace libXpertMassCore
{


/*!
\class MsXpS::libXpertMassCore::MassDataCborMassSpectrumHandler
\inmodule libXpertMassCore
\ingroup XpertMassCoreUtilities
\inheaderfile MassDataCborMassSpectrumHandler.hpp

\brief The MassDataCborMassSpectrumHandler class provides features to handle
mass spectrum data using the CBOR (Concise Binary Object Representation)
container streaming classes.

The data transported using CBOR is first qualified using the
\l{Enums::MassDataType::MASS_SPECTRUM} value. The data are packed following that
mass data type specification.

The format for this kind of CBOR mass spectrum data is the following:

The whole file is actually a map that contains the key/value
pairs below.

All the text string values below are described in this way in the CBOR Qt
implementation specification: "Major type 3:  a text string, specifically a
string of Unicode characters that is encoded as UTF-8 (that is, not a
QByteArray, but a QString)".

The containers, map, strings and byte arrays dot not need the reader's next()
call.

The simple value, like integers, do need the reader's next() call.

In this format, the whole data set in the CBOR container is a map with the
following key/value pairs:

\e{start_map}

\list

\li "DATA_TYPE" / value (quint64) : this quint64
(QCborStreamReader::UnsignedInteger) value is only got as the very first data
bit in the file and represents the Enums::MassDataType::MASS_SPECTRUM value
describing the kind of data encoded in the CBOR container.

\li "TITLE" / value (text string): the title of the mass spectrum that should be
used to identify it later in a mass data visualization/exploration program like
MineXpert.

\li "TRACE_COLOR" / value (QByteArray)

\li "X_LABEL" / value (text string): the label of the x abscissae axis.

\li "X_DATA" / value (base64 QByteArray): the array of Base64-encoded m/z values
(x axis)

\li "Y_LABEL" / value (text string): the label of the y ordinates axis.

\li "Y_DATA" / value (base64 QByteArray): the array of Base64-encoded intensity
values (y axis)
\endlist

\e{end_map}

All the data above fully qualify a mass spectrum in order to display it as a
trace (that is, a graph) in a mass spectrometric data visualization/exploration
program like MineXpert, from \l{http://www.msxpertsuite.org/}.
*/


/*!
 \brief Constructs a MassDataCborMassSpectrumHandler instance setting parent to
 \a parent_p.
 */
MassDataCborMassSpectrumHandler::MassDataCborMassSpectrumHandler(
  QObject *parent_p)
  : MassDataCborBaseHandler(parent_p)
{
  qDebug() << "Current thread:" << QThread::currentThread()
           << "is main thread:" << QThread::isMainThread();
}

/*!
\brief Constructs a MassDataCborMassSpectrumHandler instance initializing the
member trace to \a trace and setting parent to \a parent_p.

\note The \a trace is considered the mass spectrum data to be streamed to a file
as a CBOR container.
*/
MassDataCborMassSpectrumHandler::MassDataCborMassSpectrumHandler(
  QObject *parent_p, const pappso::Trace &trace)
  : MassDataCborBaseHandler(parent_p), m_trace(trace)
{
}

/*!
 \brief Destructs the MassDataCborMassSpectrumHandler instance.
 */
MassDataCborMassSpectrumHandler::~MassDataCborMassSpectrumHandler()
{
}

/*!
 \brief Decodes the data stream from \a reader_sp.

 The CBOR container consists in a succession of map key/value pairs.
 The very first key/value pair is
 "DATA_TYPE"/Enums::MassDataType::MASS_SPECTRUM. The following key/value pairs
 contain data like the title of the mass spectrum, the color for tracing the
 mass spectrum, the x (m/z) data and the y (intensity) data. See the class
 documentation for details.

 Data decoded from the \a reader_sp stream are set to member data for later
 consumption.

 Returns true if the decoding was successful, false otherwise.

 \sa writeFile()
 */
bool
MassDataCborMassSpectrumHandler::decodeStream(QCborStreamReaderSPtr &reader_sp)
{
  // See QDoc class description above

  while(reader_sp->lastError() == QCborError::NoError && reader_sp->hasNext())
    {
      QCborStreamReader::Type type = reader_sp->type();
      // qDebug() << "Type is:" << type;

      if(type == QCborStreamReader::UnsignedInteger)
        {
          // In this format, the QCborStreamReader::UnsignedInteger datum is
          // only got as the very first data bit in the file. This bit of
          // information is a quint64 representing
          // Enums::MassDataType::MASS_SPECTRUM.

          // Now check that the read value actually corresponds to the expected
          // mass data type for which this object is working!

          // The m_currentKey value was set in a previous loop iteration below
          // where the type of data read from the stream was the string key
          // "DATA_TYPE" and we now, for this loop iteratation get the value
          // for that key.
          if(m_currentKey == "DATA_TYPE")
            {
              // quint64 data_type = 0;
              // data_type = reader_sp->toUnsignedInteger();
              // qDebug() << "The mass data type:" << data_type;

              if(static_cast<Enums::MassDataType>(
                   reader_sp->toUnsignedInteger()) !=
                 Enums::MassDataType::MASS_SPECTRUM)
                {
                  qDebug()
                    << "The expected DATA_TYPE::MASS_SPECTRUM was not found.";
                  return false;
                }

              m_massDataType = Enums::MassDataType::MASS_SPECTRUM;

              m_currentKey = QString();
            }

          // qDebug() << "At this point, check if it has next:"
          //<< reader_sp->hasNext();

          // We had what we wanted, go to next.
          reader_sp->next();
        }
      // End of
      // if(type == QCborStreamReader::UnsignedInteger)
      else if(type == QCborStreamReader::String)
        {
          // Read a CBOR string, concatenating all
          // the chunks into a single string.
          QString str;
          auto chunk = reader_sp->readString();
          while(chunk.status == QCborStreamReader::Ok)
            {
              str   += chunk.data;
              chunk  = reader_sp->readString();
            }

          if(chunk.status == QCborStreamReader::Error)
            {
              // handle error condition
              qDebug() << "There was an error reading string chunk.";
              str.clear();
            }

          // qDebug() << "The string that was read:" << str;

          // If the current key is empty, this string value is a new key or tag.
          // Otherwise, it's a value.

          if(m_currentKey.isEmpty())
            {
              // qDebug() << "Setting m_currentKey:" << str;
              m_currentKey = str;
            }
          else
            {
              // Depending on what we had already, we will set the proper member
              // datum.
              if(m_currentKey == "TITLE")
                {
                  // qDebug() << "Setting m_title:" << str;
                  m_title = str;
                }

              if(m_currentKey == "X_LABEL")
                {
                  // qDebug() << "Setting m_xLabel:" << str;

                  m_xLabel = str;
                }

              if(m_currentKey == "Y_LABEL")
                {
                  // qDebug() << "Setting m_yLabel:" << str;
                  m_yLabel = str;
                }

              // At this point we can reset m_currentKey.
              m_currentKey = QString();
            }
        }

      // The ByteArray data for X_DATA and Y_DATA.
      // They are preceded by the keys "X_DATA" and "Y_DATA" respectively.

      else if(type == QCborStreamReader::ByteArray)
        {
          // Read a byte array. That could be either the X_DATA or the Y_DATA.

          QByteArray array;

          auto chunk = reader_sp->readByteArray();

          while(chunk.status == QCborStreamReader::Ok)
            {
              array.append(chunk.data);
              chunk = reader_sp->readByteArray();
            }

          if(m_currentKey == "X_DATA")
            {
              m_xBase64Data = array;
            }
          else if(m_currentKey == "Y_DATA")
            {
              m_yBase64Data = array;
            }
          else if(m_currentKey == "TRACE_COLOR")
            {
              m_colorByteArray = array;
            }
          else
            {
              qDebug() << "Error in the data read from file.";
              return false;
            }

          m_currentKey = QString();
        }
      else if(type == QCborStreamReader::Map)
        {

          reader_sp->enterContainer();

          // Read elements until end of map is reached
          bool res = decodeStream(reader_sp);
          if(res)
            reader_sp->leaveContainer();
          else
            return false;
        }
      else
        {
          // Ignore all other types, go to the next element
          reader_sp->next();
        }
    }

  if(reader_sp->lastError() != QCborError::NoError)
    qDebug() << "There was an error: " << reader_sp->lastError()
             << "Returning false.";

  // Return true if there were no errors
  // qDebug() << "Returning: " << !reader_sp->lastError();

  return !reader_sp->lastError();
}

/*!
 \brief Writes member data to file \a output_file_name.

 \note The member data are packed in a CBOR container and streamed to the file
 as a set of key/value pairs that form the CBOR map being streamed to the file.

 Returns true if the operation was successful, false, otherwise.
 */
bool
MassDataCborMassSpectrumHandler::writeFile(const QString &output_file_name)
{
  QString local_file_name = output_file_name;

  if(local_file_name.isEmpty())
    local_file_name = m_outputFileName;

  QFile file(local_file_name);

  bool res = file.open(QIODevice::WriteOnly);

  if(!res)
    {
      qDebug() << "Failed to open the file for write.";
      return false;
    }

  msp_writer = std::make_shared<QCborStreamWriter>(&file);

  // qDebug() << "Now writing data to file:" << local_file_name;

  // Start of array containing all mapped items
  msp_writer->startMap(7);

  // First the Enums::MassDataType: this is the very *first* data bit in the
  // data stream.
  msp_writer->append("DATA_TYPE");
  msp_writer->append(static_cast<quint64>(Enums::MassDataType::MASS_SPECTRUM));

  msp_writer->append("TITLE");
  msp_writer->append(m_title);

  msp_writer->append("TRACE_COLOR");
  msp_writer->append(m_colorByteArray);

  msp_writer->append("X_LABEL");
  msp_writer->append("m/z");

  msp_writer->append("X_DATA");
  msp_writer->append(m_trace.xAsBase64Encoded());

  msp_writer->append("Y_LABEL");
  msp_writer->append("intensity");

  msp_writer->append("Y_DATA");
  msp_writer->append(m_trace.yAsBase64Encoded());

  msp_writer->endMap();
  // Close the map now.

  file.close();

  return res;
}

/*!
 \brief Writes member data to byte array \a byte_array.

 \note The member data are packed in a CBOR container and streamed to the byte
 array as a set of key/value pairs that form the CBOR map being streamed to the
 byte array.
 */
void
MassDataCborMassSpectrumHandler::writeByteArray(QByteArray &byte_array)
{
  msp_writer = std::make_shared<QCborStreamWriter>(&byte_array);

  // qDebug() << "Now writing data byte array.";

  // Start of array containing all mapped items
  msp_writer->startMap(7);

  // First the Enums::MassDataType: this is the very *first* data bit in the
  // data stream.
  msp_writer->append("DATA_TYPE");
  msp_writer->append(static_cast<quint64>(Enums::MassDataType::MASS_SPECTRUM));

  msp_writer->append("TITLE");
  msp_writer->append(m_title);

  msp_writer->append("TRACE_COLOR");
  msp_writer->append(m_colorByteArray);

  msp_writer->append("X_LABEL");
  msp_writer->append("m/z");

  msp_writer->append("X_DATA");
  msp_writer->append(m_trace.xAsBase64Encoded());

  msp_writer->append("Y_LABEL");
  msp_writer->append("intensity");

  msp_writer->append("Y_DATA");
  msp_writer->append(m_trace.yAsBase64Encoded());

  msp_writer->endMap();
  // Close the map now.
}

/*!
 \brief Reads data from the byte array \a byte_array.

 The data in \a byte_array are packed in a CBOR container. These data have
 typically travelled over the network and the receiving end needs to unpack the
 data in that \a byte_array. This is what this function does.

 The data unpacked from \a byte_array are stored in this object member variables
 for later consumption by the user of this function.

A CBOR stream reader is allocated and set to decode \a byte_array. The actual
decoding occurs in \l{decodeStream()}.

Returns true if the operations was successful, false otherwise.
 */
bool
MassDataCborMassSpectrumHandler::readByteArray(const QByteArray &byte_array)
{
  msp_reader = std::make_shared<QCborStreamReader>(byte_array);

  bool res = decodeStream(msp_reader);

  // qDebug() << "After finishing the read, m_title is:" << m_title
  //<< "and mass data type is:" << static_cast<quint64>(m_massDataType);

  // At this point we need to decode the arrays and with the data initialize the
  // pappso::Trace.

  QByteArray x_array;
  QByteArray y_array;

  QByteArray::FromBase64Result decoding_result = QByteArray::fromBase64Encoding(
    m_xBase64Data, QByteArray::Base64Encoding | QByteArray::OmitTrailingEquals);

  if(decoding_result.decodingStatus == QByteArray::Base64DecodingStatus::Ok)
    x_array = decoding_result.decoded;
  else
    {
      qDebug() << "Failed to decode the " << m_xLabel << "data";
      return false;
    }

  decoding_result = QByteArray::fromBase64Encoding(
    m_yBase64Data, QByteArray::Base64Encoding | QByteArray::OmitTrailingEquals);

  if(decoding_result.decodingStatus == QByteArray::Base64DecodingStatus::Ok)
    y_array = decoding_result.decoded;
  else
    {
      qDebug() << "Failed to decode the " << m_yLabel << "data";
      return false;
    }

  m_trace.clear();

  m_trace.initialize(QString(x_array), QString(y_array));

  return res;
}

/*!
 \brief Reads the data in file \a input_file_name by decoding the data stream
 read from it.
 This function first decodes the data in the CBOR container stored in the
 file. Then it decodes the Base64-encoded x and y axis data into the member
 pappso::Trace object.

 Returns true if the operation was successful, false, otherwise.

 \sa decodeStream(), readByteArray()
 */
bool
MassDataCborMassSpectrumHandler::readFile(const QString &input_file_name)
{
  // The format for this kind of CBOR pappso::Trace data is the following:

  // First the quint64 that represents Enums::MassDataType. In our specific
  // case, that must be Enums::MassDataType::MASS_SPECTRUM.

  // Then there is the title of data, which is according to the specification:
  //
  // Major type 3:  a text string, specifically a string of Unicode characters
  // that is encoded as UTF-8 (that is, not a QByteArray, but a QString).

  // Then there is a MAP with the following key/value pairs:

  // start_map

  // "X_LABEL" / value (text string)
  // "Y_LABEL" / value (text string)
  //
  // "X_DATA" / value (base64 ByteArray)
  // "Y_DATA" / value (base64 ByteArray)

  // end_map


  QString local_file_name = input_file_name;

  if(local_file_name.isEmpty())
    local_file_name = m_inputFileName;

  QFileInfo file_info(local_file_name);

  if(!file_info.exists())
    {
      qDebug() << "File not found.";
      return false;
    }

  QFile file(local_file_name);

  bool res = file.open(QIODevice::ReadOnly);

  if(!res)
    {
      qDebug() << "Failed to open the file for read.";
      return false;
    }

  // qDebug() << "Now starting the CBOR data read.";

  msp_reader = std::make_shared<QCborStreamReader>(&file);

  res = decodeStream(msp_reader);

  file.close();

  // qDebug() << "After finishing the read, m_title is:" << m_title
  //<< "and mass data type is:" << static_cast<quint64>(m_massDataType);

  // At this point we need to decode the arrays and with the data initialize the
  // pappso::Trace.

  QByteArray x_array;
  QByteArray y_array;

  QByteArray::FromBase64Result decoding_result = QByteArray::fromBase64Encoding(
    m_xBase64Data, QByteArray::Base64Encoding | QByteArray::OmitTrailingEquals);

  if(decoding_result.decodingStatus == QByteArray::Base64DecodingStatus::Ok)
    x_array = decoding_result.decoded;
  else
    {
      qDebug() << "Failed to decode the " << m_xLabel << "data";
      return false;
    }

  decoding_result = QByteArray::fromBase64Encoding(
    m_yBase64Data, QByteArray::Base64Encoding | QByteArray::OmitTrailingEquals);

  if(decoding_result.decodingStatus == QByteArray::Base64DecodingStatus::Ok)
    y_array = decoding_result.decoded;
  else
    {
      qDebug() << "Failed to decode the " << m_yLabel << "data";
      return false;
    }

  m_trace.clear();

  m_trace.initialize(QString(x_array), QString(y_array));

  return res;
}

/*!
\brief Sets the abscissae \a label for the mass spectrum data.
*/
void
MassDataCborMassSpectrumHandler::setXLabel(const QString &label)
{
  m_xLabel = label;
}

/*!
\brief Returns the abscissae label for the mass spectrum data.
*/
QString
MassDataCborMassSpectrumHandler::getXLabel() const
{
  return m_xLabel;
}

/*!
\brief Sets the ordinates \a label for the mass spectrum data.
*/
void
MassDataCborMassSpectrumHandler::setYLabel(const QString &label)
{
  m_yLabel = label;
}

/*!
\brief Returns the ordinates label for the mass spectrum data.
*/
QString
MassDataCborMassSpectrumHandler::getYLabel() const
{
  return m_yLabel;
}

/*!
\brief Sets the mass spectrum data to \a trace.
*/
void
MassDataCborMassSpectrumHandler::setTrace(const pappso::Trace &trace)
{
  m_trace = trace;
}

/*!
\brief Returns the mass spectrum data.
*/
pappso::Trace
MassDataCborMassSpectrumHandler::getTrace() const
{
  return m_trace;
};

/*!
\brief Clears the mass spectrum data.
*/
void
MassDataCborMassSpectrumHandler::clearTrace()
{
  m_trace.clear();
}

/*!
\brief Sets the color with which the mass spectrum data must be displayed. The
color i encoded as the \a color_byte_array byte array.
*/
void
MassDataCborMassSpectrumHandler::setTraceColor(
  const QByteArray &color_byte_array)
{
  m_colorByteArray = color_byte_array;
}

/*!
\brief Returns the color with which the mass spectrum data must be displayed.
*/
QByteArray
MassDataCborMassSpectrumHandler::getTraceColor() const
{
  return m_colorByteArray;
}


} // namespace libXpertMassCore


} // namespace MsXpS
