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


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

/////////////////////// IsoSpec
#include <IsoSpec++/isoSpec++.h>
#include <IsoSpec++/element_tables.h>


/////////////////////// Local includes
#include "MsXpS/libXpertMassCore/globals.hpp"
#include "MsXpS/libXpertMassCore/IsotopicDataLibraryHandler.hpp"

namespace MsXpS
{

namespace libXpertMassCore
{


/*!
\class MsXpS::libXpertMassCore::IsotopicDataLibraryHandler
\inmodule libXpertMassCore
\ingroup PolChemDefBuildingdBlocks
\inheaderfile IsotopicDataLibraryHandler.hpp

\brief The IsotopicDataLibraryHandler class handles \l{IsotopicData} from the
IsoSpec element data tables directly from the library's data. These are the
reference, pristine, \e{unmodified}, isotopic data.

The IsoSpec element data tables that are used in libXpertMassCore are the
following:

\list

\li elem_table_element
\li elem_table_symbol
\li elem_table_mass
\li elem_table_probability

\endlist

The data tables are all of the same length and the data in each row of a
given table matches the contents of that same row in all the other tables. For
example, the first two rows of table elem_table_ID are:

1

1

These two rows match the same rows in elem_table_mass:

1.00782503227

2.01410177819

and the the same rows in elem_table_element:

"hydrogen"

"hydrogen"

By reading, row-by-row, the data from the same row number in each one of the
tables, one constructs a fully qualified \l{Isotope}.

\sa IsotopicDataUserConfigHandler, IsotopicDataManualConfigHandler
*/

/*!
\typedef MsXpS::libXpertMassCore::IsotopicDataLibraryHandlerSPtr
\relates IsotopicDataLibraryHandler

Synonym for std::shared_ptr<IsotopicDataLibraryHandler>.
*/

/*!
\typedef MsXpS::libXpertMassCore::IsotopicDataLibraryHandlerCstSPtr
\relates IsotopicDataLibraryHandler

Synonym for std::shared_ptr<const IsotopicDataLibraryHandler>.
*/


/*!
\brief Constructs the \l{IsotopicDataLibraryHandler}.

The instance will have empty member data.
*/
IsotopicDataLibraryHandler::IsotopicDataLibraryHandler()
  : IsotopicDataBaseHandler()
{
}

/*!
\brief Constructs the \l{IsotopicDataLibraryHandler}.

The instance will have its isotopic data member pointing to \a
isotopic_data_sp.
*/
IsotopicDataLibraryHandler::IsotopicDataLibraryHandler(
  IsotopicDataSPtr isotopic_data_sp)
  : IsotopicDataBaseHandler(isotopic_data_sp)
{
}

/*!
\brief Destructs the \l{IsotopicDataLibraryHandler}.

Nothing is explicitely deleted in the destructor.
*/
IsotopicDataLibraryHandler::~IsotopicDataLibraryHandler()
{
  // qDebug();
}

/*!
\brief Loads isotopic data directly from IsoSpec library' element data tables.

The member isotopic data are cleared before setting new data read from the
library's element data tables.

The code iterates, row-by-row, in the all the tables and extracts
the data to fill in the Isotope data:

\code
IsotopeQSPtr isotope_qsp =
std::make_shared<Isotope>(QString(IsoSpec::elem_table_element[iter]),
QString(IsoSpec::elem_table_symbol[iter]),
IsoSpec::elem_table_mass[iter],
IsoSpec::elem_table_probability[iter]);
\endcode

Note that some rows in the table are populated with non-isotope data, like
electron, missing_electron or protonation (twice). When these rows are
encountered, the \a count_non_isotope_skipped_items is incremented.

Returns the count of \l{Isotope}s that were allocated and stored in the
msp_isotopicData member.
*/
qsizetype
IsotopicDataLibraryHandler::loadData(qsizetype &count_non_isotope_skipped_items)
{

  // We need to allocate one Isotope instance for each element
  // in the various arrays in the IsoSpec++ source code header file.

  // extern const char* elem_table_element[ISOSPEC_NUMBER_OF_ISOTOPIC_ENTRIES];
  // extern const char* elem_table_symbol[ISOSPEC_NUMBER_OF_ISOTOPIC_ENTRIES];
  // extern const double elem_table_mass[ISOSPEC_NUMBER_OF_ISOTOPIC_ENTRIES];
  // extern const double
  // elem_table_probability[ISOSPEC_NUMBER_OF_ISOTOPIC_ENTRIES];

  // Big sanity check, all the arrays must be the same length!
  std::size_t array_length = checkConsistency();
  if(array_length < 1)
    return false;

  // qDebug() << "The size of the tables:" << array_length;

  // Clear all the data, since this function might be called multiple times.
  msp_isotopicData->clear();

  std::size_t loaded_isotopes = 0;
  for(std::size_t iter = 0; iter < array_length; ++iter)
    {
      QString elem_element = QString(IsoSpec::elem_table_element[iter]);

      // These are the last items in the various tables. We do not handle them
      // at the moment.
      if(elem_element == "electron" || elem_element == "missing electron" ||
         elem_element == "protonation" /* occurs twice */)
        {
          ++count_non_isotope_skipped_items;
          continue;
        }

      IsotopeQSPtr isotope_qsp = QSharedPointer<Isotope>::create(
        QString(IsoSpec::elem_table_element[iter]),
        QString(IsoSpec::elem_table_symbol[iter]),
        IsoSpec::elem_table_mass[iter],
        IsoSpec::elem_table_probability[iter]);

      // We do not want to update the mono/avg maps each time we load an
      // isotope. We'll call the relevant function later.
      msp_isotopicData->appendNewIsotope(isotope_qsp, /* update maps*/ false);
      ++loaded_isotopes;
    }

  // Sanity check
  if((loaded_isotopes + count_non_isotope_skipped_items) !=
     IsoSpec::isospec_number_of_isotopic_entries)
    qFatal(
      "Programming error. Error loading the isotopic data from IsoSpec++'s "
      "tables.");

  // Now ask that the mono/avg mass maps be updated.
  if(!msp_isotopicData->updateMassMaps())
    qFatal("Programming error. Failed to update the mass maps.");

  // qDebug() << "Done loading data with :" << msp_isotopicData->size()
  //          << "isotopes in the isotopic data.";

  return msp_isotopicData->size();
}

/*!
\brief Loads isotopic data directly from IsoSpec library' element data tables.

The member isotopic data are cleared before setting new data read from the
library's element data tables.

The code iterates, row-by-row, in the all the tables and extracts
the data to fill in the Isotope data:

\code
IsotopeQSPtr isotope_qsp =
std::make_shared<Isotope>(QString(IsoSpec::elem_table_element[iter]),
QString(IsoSpec::elem_table_symbol[iter]),
IsoSpec::elem_table_mass[iter],
IsoSpec::elem_table_probability[iter]);
\endcode

Note that some rows in the table are populated with non-isotope data, like
electron, missing_electron or protonation (twice). When these rows are
encountered, they are skipped. See overloaded function that takes
a parameter to store the count of such non-isotopic items.

Returns the count of \l{Isotope}s that were allocated and stored in the
msp_isotopicData member.
*/
qsizetype
IsotopicDataLibraryHandler::loadData()
{
  // The user does not care of the count of non isotopic data items.
  qsizetype count_non_isotope_skipped_items = 0;
  return (loadData(count_non_isotope_skipped_items));
}

/*!
\brief Write all the IsotopicData to \a file_name.

If \a file_name is empty, m_fileName is tried. If both are empty, the
function returns 0. If any one of the file names are correct (file_name takes
precedence over m_fileName), then m_fileName is set to that file name.

The format of the file consists in a single line of data per \l{Isotope} as
created using the Isotope::toString() function and

Isotope::Isotope(const QString &text, QObject *parent). Each isotope is output
to its own line.

Returns the count of \l{Isotope::Isotope}s written to file or 0 if the file does
not exist or is not readable.

\sa Isotope::Isotope and overloads
*/
qsizetype
IsotopicDataLibraryHandler::writeData(const QString &file_name)
{
  // Although the isotopic data were loaded from the IsoSpec library tables, we
  // might be willing to store these data to a file.

  if(file_name.isEmpty() && m_fileName.isEmpty())
    return 0;

  QString temp_file_name;

  // The passed filename takes precedence over the member datum. So copy
  // that file name to the member datum.

  if(!file_name.isEmpty())
    temp_file_name = file_name;
  else
    temp_file_name = m_fileName;

  QFile file(temp_file_name);

  // qDebug() << "File name to write to:" << temp_file_name;

  if(!file.open(QIODevice::WriteOnly | QIODevice::Text))
    {
      qDebug("Failed to open file for writing.");
      return 0;
    }

  QTextStream out(&file);

  out
    << "# This file contains isotopic data in a format that can accommodate\n";
  out
    << "# comments in the form of lines beginning with the '#' character.\n\n";

  std::size_t isotope_count = 0;

  for(auto isotope_qsp : msp_isotopicData->m_isotopes)
    {
      out << isotope_qsp->toString();
      // We need to add it because toString() does not terminate the line with
      // a new line character.
      out << "\n";

      ++isotope_count;
    }

  out.flush();

  file.close();

  // Now we know that temp_file_name is fine. Store into m_fileName.
  m_fileName = temp_file_name;

  return isotope_count;
}

/*!
\brief Checks the consistency in all the IsoSpec library's different isotopic
data tables.

This function essentially verifies that each table has the same row count as
all the other ones.

Returns the count of isotopes in the isotopic data.
*/
qsizetype
IsotopicDataLibraryHandler::checkConsistency()
{
  qsizetype array_length = sizeof(IsoSpec::elem_table_atomicNo) /
                           sizeof(IsoSpec::elem_table_atomicNo[0]);

  // qDebug() << "The array length is:" << array_length;

  // All the tables in the header file of the IsoSpec library must
  // have exactly the same size.

  if(static_cast<qsizetype>(IsoSpec::isospec_number_of_isotopic_entries) !=
     array_length)
    {
      qFatal(
        "Found corruption: the size of IsoSpec arrays is not like expected.");
    }

  // Now test each table one by one.
  qsizetype tested_length = sizeof(IsoSpec::elem_table_probability) /
                            sizeof(IsoSpec::elem_table_probability[0]);
  if(tested_length != array_length)
    {
      qDebug()
        << "Found corruption: at least two arrays are not of the same length."
        << "tested_length:" << tested_length;

      return 0;
    }

  tested_length =
    sizeof(IsoSpec::elem_table_mass) / sizeof(IsoSpec::elem_table_mass[0]);
  if(tested_length != array_length)
    {
      qDebug()
        << "Found corruption: at least two arrays are not of the same length."
        << "tested_length:" << tested_length;

      return 0;
    }

  tested_length =
    sizeof(IsoSpec::elem_table_massNo) / sizeof(IsoSpec::elem_table_massNo[0]);
  if(tested_length != array_length)
    {
      qDebug()
        << "Found corruption: at least two arrays are not of the same length."
        << "tested_length:" << tested_length;

      return 0;
    }

  tested_length = sizeof(IsoSpec::elem_table_extraNeutrons) /
                  sizeof(IsoSpec::elem_table_extraNeutrons[0]);
  if(tested_length != array_length)
    {
      qDebug()
        << "Found corruption: at least two arrays are not of the same length."
        << "tested_length:" << tested_length;

      return 0;
    }

  tested_length = sizeof(IsoSpec::elem_table_element) /
                  sizeof(IsoSpec::elem_table_element[0]);
  if(tested_length != array_length)
    {
      qDebug()
        << "Found corruption: at least two arrays are not of the same length."
        << "tested_length:" << tested_length;

      return 0;
    }

  tested_length =
    sizeof(IsoSpec::elem_table_symbol) / sizeof(IsoSpec::elem_table_symbol[0]);
  if(tested_length != array_length)
    {
      qDebug()
        << "Found corruption: at least two arrays are not of the same length."
        << "tested_length:" << tested_length;

      return 0;
    }

  tested_length = sizeof(IsoSpec::elem_table_Radioactive) /
                  sizeof(IsoSpec::elem_table_Radioactive[0]);
  if(tested_length != array_length)
    {
      qDebug()
        << "Found corruption: at least two arrays are not of the same length."
        << "tested_length:" << tested_length;

      return 0;
    }

  tested_length = sizeof(IsoSpec::elem_table_log_probability) /
                  sizeof(IsoSpec::elem_table_log_probability[0]);
  if(tested_length != array_length)
    {
      qDebug()
        << "Found corruption: at least two arrays are not of the same length."
        << "tested_length:" << tested_length;

      return 0;
    }

  return tested_length;
}


} // namespace libXpertMassCore

} // namespace MsXpS


#if 0

Example from IsoSpec.

const int elementNumber = 2;
const int isotopeNumbers[2] = {2,3};

const int atomCounts[2] = {2,1};


const double hydrogen_masses[2] = {1.00782503207, 2.0141017778};
const double oxygen_masses[3] = {15.99491461956, 16.99913170, 17.9991610};

const double* isotope_masses[2] = {hydrogen_masses, oxygen_masses};

const double hydrogen_probs[2] = {0.5, 0.5};
const double oxygen_probs[3] = {0.5, 0.3, 0.2};

const double* probs[2] = {hydrogen_probs, oxygen_probs};

IsoLayeredGenerator iso(Iso(elementNumber, isotopeNumbers, atomCounts,
isotope_masses, probs), 0.99);

#endif
