/* 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/>.
 *
 * *****************************************************************************
 * This specific code is a port to C++ of the Envemind Python code by Radzinski
 * and colleagues of IsoSpec fame (Lacki, Startek and company :-)
 *
 * See https://github.com/PiotrRadzinski/envemind.
 * *****************************************************************************
 *
 * END software license
 */


#include <QDebug>
#include <QtNetwork>

#include "MsXpS/libXpertMassCore/MassDataClient.hpp"

namespace MsXpS
{
namespace libXpertMassCore
{

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

\brief The MassDataClient class provides a network client.
*/

/*!
\variable MsXpS::libXpertMassCore::MassDataClient::m_inStream

\brief The data stream in input into this MassDataClient instance.
*/

/*!
\variable MsXpS::libXpertMassCore::MassDataClient::m_data

\brief The data being transported.
*/

/*!
\variable MsXpS::libXpertMassCore::MassDataClient::m_ipAddress

\brief The server IP address to which the connection must be made.
*/

/*!
\variable MsXpS::libXpertMassCore::MassDataClient::m_portNumber

\brief The port number to which to attach during the connection.
*/

/*!
\variable MsXpS::libXpertMassCore::MassDataClient::mp_tcpSocket

\brief The socket that is used for the connection to the server.
*/

/*!
 * \variable MsXpS::libXpertMassCore::MassDataClient::m_forcefulDisconnection
 *
 * \brief Tells if the disconnection was forceful.
 */

/*!
\brief Constructs a MassDataClient instance.

\list
\li \a ip_address: the server (\l{MassDataServer}) IP address at which the
client needs to try a connection.
\li \a port_number: the port number for the connection.
\li \a parent: the parent QObject.
\endlist
*/
MassDataClient::MassDataClient(const QString &ip_address,
                               int port_number,
                               QObject *parent)
  : QObject(parent), m_ipAddress(ip_address), m_portNumber(port_number)
{
  mp_tcpSocket = new QTcpSocket(parent);

  if(mp_tcpSocket == nullptr)
    qFatal("Failed to allocate a socket.");

  // When the connection below (see call mpa_tcpSocket->connectToHost())
  // will actually work, this signal will be emitted and we respond
  // by sending to the socket the READY string so that the server
  // know we are ready to receive data.
  connect(mp_tcpSocket, &QTcpSocket::connected, [this]() {
    mp_tcpSocket->write("READY\n");
    mp_tcpSocket->flush();
    emit connectedSignal();
  });

  connect(mp_tcpSocket, &QTcpSocket::hostFound, [this]() {
    emit hostFoundSignal();
  });

  connect(mp_tcpSocket,
          &QAbstractSocket::errorOccurred,
          this,
          &MassDataClient::reportError);

  connect(mp_tcpSocket, &QTcpSocket::disconnected, [this]() {
    // This signal can be sent when the user forcefully
    // disconnects from the host. In this case, the socket
    // is destroyed. So we need to ensure we only try to
    // reconnect if the disconnection was not voluntary.
    if(mp_tcpSocket != nullptr)
      {
        qDebug() << "Lost connection, the socket is not destroyed."
                 << QDateTime::currentDateTime();

        if(m_forcefulDisconnection)
          {
            qDebug() << "The disconnection was forceful. Do not restart."
                     << QDateTime::currentDateTime();
          }
        else
          {
            qDebug() << "The disconnection was not forceful, retrying "
                        "connection in 5s..."
                     << QDateTime::currentDateTime() << mp_tcpSocket;
            QTimer::singleShot(5000, [this]() {
              mp_tcpSocket->connectToHost(m_ipAddress, m_portNumber);
            });
          }
      }
    else
      {
        qDebug() << "The socket was destroyed already. Doing nothing."
                 << QDateTime::currentDateTime()
                 << QDateTime::currentDateTime();
      }

    qDebug() << "Now emitting disconnectedSignal() for the user of this client."
             << QDateTime::currentDateTime();

    emit disconnectedSignal();
  });

  // When data will be ready to read from the server, they well be served
  // in this m_inStream.
  m_inStream.setDevice(mp_tcpSocket);
  m_inStream.setVersion(QDataStream::Qt_5_0);

  connect(mp_tcpSocket, &QIODevice::readyRead, this, &MassDataClient::readData);

  mp_tcpSocket->connectToHost(ip_address, port_number);
}

/*!
\brief Destructs this MassDataClient instance.
*/
MassDataClient::~MassDataClient()
{
}

/*!
\brief Reads the data into the QByteArray m_data member using the QDataStream
m_inStream member.

Emits the newDataSignal() signal with m_data;.
*/
void
MassDataClient::readData()
{
  // qDebug() << "Reading data.";

  // We need to have the same version, FIXME, this should go as a macro
  // somewhere.

  // qDebug() << "The number of bytes available in this readData call:"
  //<< mp_tcpSocket->bytesAvailable() << "at"
  //<< QDateTime::currentDateTime().toString();

  // This version uses the readAll() function.

  // QByteArray byte_array = mp_tcpSocket->readAll();

  // QDataStream stream(byte_array);
  // stream.setVersion(QDataStream::Qt_5_0);
  // stream.startTransaction();
  // stream >> m_data;

  // if(!stream.commitTransaction())
  //{
  // qDebug() << "Failed to commit the data read transaction.";

  // return ;
  //}

  // This version uses the stream operator.
  // Seems to work equivalently to the version above.
  // stream.setVersion(QDataStream::Qt_5_0);
  m_inStream.startTransaction();

  m_inStream >> m_data;

  // qDebug() << "Atomically read" << m_data.size();

  if(!m_inStream.commitTransaction())
    {
      qDebug() << "Could NOT commit the transaction fine.";
      return;
    }
  // else
  // qDebug() << "Could commit the transaction fine.";

  emit newDataSignal(m_data);

  // qDebug() << "Got these data:" << QString(m_data);
}

/*!
\brief Reports the error \a socket_error.
*/
void
MassDataClient::reportError(QAbstractSocket::SocketError socket_error)
{
  switch(socket_error)
    {
      case QAbstractSocket::RemoteHostClosedError:
        // qDebug() << "Error: QAbstractSocket::RemoteHostClosedError.";
        // emit reportErrorSignal("RemoteHostClosedError");
        break;
      case QAbstractSocket::HostNotFoundError:
        // qDebug() << "Error: QAbstractSocket::HostNotFoundError.";
        // emit reportErrorSignal("HostNotFoundError");
        break;
      case QAbstractSocket::ConnectionRefusedError:
        qDebug() << "Error: QAbstractSocket::ConnectionRefusedError.";
        emit reportErrorSignal("ConnectionRefusedError");
        break;
      default:
        // qDebug() << "Error:" << mp_tcpSocket->errorString();
        // emit reportErrorSignal(mp_tcpSocket->errorString());
        break;
    }
}

void
MassDataClient::forcefullyDisconnect()
{
  qDebug() << "Forcefully disconnecting client."
           << QDateTime::currentDateTime();

  m_forcefulDisconnection = true;

  if(mp_tcpSocket != nullptr)
    {
      // Maybe we do not need this step, deleting the socket will
      // emit the disconnection signal.
      // mp_tcpSocket->disconnectFromHost();

      qDebug() << "Deleting the socket that was disconnected"
               << QDateTime::currentDateTime() << mp_tcpSocket;

      delete mp_tcpSocket;
      mp_tcpSocket = nullptr;

      qDebug() << "Deletedg the socket that was disconnected"
               << QDateTime::currentDateTime() << mp_tcpSocket;

      // This will emit the disconnected() signal that we have
      // connected to the emission of disconnectedSignal().
      // The user of this MassDataClient instance should connect
      // a slot this disconnectedSignal() and handle the destruction of
      // this very instance.
    }
}

QString
MassDataClient::getIpAddress() const
{
  return m_ipAddress;
}

int
MassDataClient::getPortNumber() const
{
  return m_portNumber;
}

/*!
\brief Returns a string display the connection details.
*/
QString
MassDataClient::getStatus()
{
  return QString("Doing well, connection should be to IP address: %1, port %2.")
    .arg(m_ipAddress)
    .arg(m_portNumber);
}

} // namespace libXpertMassCore

} // namespace MsXpS
