/***************************************************************************
                          msnsockettcp.cpp  -  description
                             -------------------
    begin                : Thu Jan 23 2003
    copyright            : (C) 2003 by Mike K. Bennett
    email                : mkb137b@hotmail.com
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

#include "msnsockettcp.h"

#include <QTimer>
#include <QTcpSocket>
#include <QHostAddress>

#include <KLocale>

#include "../utils/kmessshared.h"
#include "../currentaccount.h"
#include "../kmessapplication.h"
#include "../kmessdebug.h"

#ifdef KMESSDEBUG_CONNECTION // Area-specific debug statements
  #define KMESSDEBUG_CONNECTION_SOCKET_TCP
#endif

/**
 * @brief Login timer for DNS name resolution
 *
 * In milliseconds, the time to wait for the msn server name resolution to complete.
 */
#define LOGIN_TIMER_DNS_RESOLVE   10000

/**
 * @brief Login timer for actual server connection
 *
 * In milliseconds, the time to wait for the connection to be established.
 */
#define LOGIN_TIMER_CONNECTION     5000


#define MSN_TCP_HOST "messenger.hotmail.com"
#define MSN_TCP_PORT 1863



/**
 * @brief The constructor
 *
 * Initializes the sockets, buffers and ping timer.
 */
MsnSocketTcp::MsnSocketTcp( ServerType serverType )
: MsnSocketBase( serverType )
, missedPings_( 0 )
, nextPayloadSize_( 0 )
, sendPings_( false )
{
  setObjectName( "MsnSocketTcp" );

  // Create the socket
  socket_ = new QTcpSocket( this );

  // TODO: socket timeout settings..

  // Forward the socket's raw event signals for further interpretation
  connect( socket_, SIGNAL(              connected() ),
           this,    SLOT  (          slotConnected() ) );
  connect( socket_, SIGNAL(           disconnected() ),
           this,    SLOT  (       slotDisconnected() ) );
  // Connect the socket's signals to parse them internally
  connect( socket_, SIGNAL(              hostFound() ),
           this,    SLOT  (          slotHostFound() ) );
  connect( socket_, SIGNAL(              readyRead() ),
           this,    SLOT  ( slotSocketDataReceived() ) );
  connect( socket_, SIGNAL(           error(QAbstractSocket::SocketError) ),
           this,    SLOT  ( slotSocketError(QAbstractSocket::SocketError) ) );
  connect( socket_, SIGNAL( proxyAuthenticationRequired(const QNetworkProxy&,QAuthenticator*) ),
           this,    SLOT  (           proxyAuthenticate(const QNetworkProxy&,QAuthenticator*) ) );

  // Connect the ping timer to the sendPing slot
  pingTimer_.stop();
  connect( &pingTimer_,       SIGNAL(         timeout() ),
           this,              SLOT  (    slotSendPing() ) );

  // Attach a timer to watch over the connection process
  connectionTimer_.stop();
  connectionTimer_.setSingleShot( true );
  connect( &connectionTimer_, SIGNAL(         timeout() ),
           this,              SLOT  ( slotLoginFailed() ) );

#ifdef KMESSTEST
  KMESS_ASSERT( socket_ != 0 );
#endif
}



/**
 * @brief The destructor
 *
 * Closes the connection, and deletes the sockets.
 */
MsnSocketTcp::~MsnSocketTcp()
{
  // Disconnect from the server on exit
  if( isConnected() )
  {
    disconnectFromServer();
  }

  // Delete the socket
  delete socket_;

#ifdef KMESSDEBUG_CONNECTION_SOCKET_TCP
  kmDebug() << "DESTROYED";
#endif
}



/**
 * @brief Connect to the given server via the socket
 *
 * The connection attempt is asynchronous.
 * When the connection is made, connectionSuccess() is called,
 * on error connectionFailed() is called.
 * Connection timeouts are not detected here yet.
 *
 * @param  server  The server hostname or IP address.
 * @param  port    The port to connect to.
 */
void MsnSocketTcp::connectToServer( const QString& server, const quint16 port )
{
  quint16 desiredPort;
  QString desiredServer;

  if( server.isEmpty() && port == 0 )
  {
    desiredServer = MSN_TCP_HOST;
    desiredPort   = MSN_TCP_PORT;
  }
  else
  {
    desiredServer = server;
    desiredPort   = port;
  }

  // Redirect to another server if started with --server
  if( static_cast<KMessApplication*>( kapp )->getUseTestServer() )
  {
    desiredServer = static_cast<KMessApplication*>( kapp )->getTestServer();
  }

#ifdef KMESSTEST
  KMESS_ASSERT( desiredPort < 32768 );
#endif

#ifdef KMESSDEBUG_CONNECTION_SOCKET_TCP
  kmDebug() << "Connecting to server at " << desiredServer << ":" << desiredPort << ".";
  kmDebug() << "Socket state is " << socket_->state() << ".";
#endif

  // Flush any pending data if the socket is closing a previous connection
  socket_->blockSignals( true );
  socket_->flush();

  // Check if we are disconnected (the socket is idle)
  if( socket_->state() != QAbstractSocket::UnconnectedState )
  {
    kmWarning() << "Socket is not disconnected. Already connected? The socket is in state" << socket_->state() << ".";
  }

  // Prepare state vars for the new connection
  connected_       = true;
  nextPayloadSize_ = 0;

  // Close any previous connection (without firing disconnection signals)
  socket_->close();

  socket_->blockSignals( false );

  // Start connecting
  socket_->connectToHost( desiredServer, desiredPort );

  // Start the connection timer to watch over the server name resolution
  connectionTimer_.start( LOGIN_TIMER_DNS_RESOLVE );
}



/**
 * @brief Disconnect from the server
 *
 * This function is called after closeConnection()
 * It empties all buffers, and resets the sockets.
 *
 * @param  isTransfer  When set, a disconnected() signal won't be fired.
 *                     This is used to handle transfers to another server
 *                     without noticing a disconnect.
 */
void MsnSocketTcp::disconnectFromServer( bool isTransfer )
{
  // Stop pinging
  setSendPings( false );

  // Stop the connection timer in case the user closed the connection during login
  connectionTimer_.stop();

#ifdef KMESSDEBUG_CONNECTION_SOCKET_TCP
  kmDebug() << "Close the socket.";
#endif

  // Disconnect the socket
  socket_->blockSignals( true );
  socket_->disconnectFromHost();

  // Just clean up if we are disconnected already
  if( ! connected_ )
  {
    return;
  }

  connected_ = false;

  // Do not signal a disconnection when we are transferring to another gateway
  if( isTransfer )
  {
    return;
  }

#ifdef KMESSDEBUG_CONNECTION_SOCKET_TCP
  kmDebug() << "Emitting disconnection signal.";
#endif

  emit disconnected();
}



/**
 * @brief Return the local IP address of the socket.
 *
 * This is typically a LAN address, unless the system
 * is directly connected to the Internet.
 *
 * @returns The IP address the socket uses at the system.
 */
QString MsnSocketTcp::getLocalIp() const
{
  QHostAddress address = socket_->localAddress();

  return address.toString();
}



/**
 * @brief Return whether or the socket is connected.
 *
 * @returns True when connected, false otherwise.
 */
bool MsnSocketTcp::isConnected() const
{
  return socket_->isValid() && socket_->state() == QAbstractSocket::ConnectedState;
}



// Set whether we're sending pings or not (also resets ping timer)
void MsnSocketTcp::setSendPings( bool sendPings )
{
  sendPings_ = sendPings;
  missedPings_ = 0;

  pingReceived_ = true;

  if ( sendPings_ )
  {
    slotSendPing();

    pingTimer_.stop();
    pingTimer_.start( 30000 );  // 30s ping timer
  }
  else
  {
    pingTimer_.stop();
  }
}



/**
 * @brief Called when the connection has been successfully opened
 */
void MsnSocketTcp::slotConnected()
{
  // Stop the connection timer, we're done
  connectionTimer_.stop();

  // Notify about the operation result
  emit connected();
}



/**
 * @brief Called when the connection has been closed
 *
 * This method is not called when using disconnectFromServer(), because
 * its signals were blocked.
 */
void MsnSocketTcp::slotDisconnected()
{
  // Stop the connection timer, in case the user had stopped the connection process
  connectionTimer_.stop();

  // Notify about what's happening
  if( connected_ )
  {
    emit disconnected();
  }
}



/**
 * @brief Called when the server's IP address has been found
 *
 * This method is used to prolong the connection timer, when the DNS query for
 * the MSN Server has been successfully completed.
 */
void MsnSocketTcp::slotHostFound()
{
#ifdef KMESSDEBUG_CONNECTION_SOCKET_TCP
  kmDebug() << "Server host address found.";
#endif

  // Restart the connection timer, to watch over the actual connection establishment.
  connectionTimer_.stop();
  connectionTimer_.start( LOGIN_TIMER_CONNECTION );
}



/**
 * @brief Called when the connection did not complete within a time limit
 *
 * This method is used to prolong the connection timer, when the DNS query for
 * the MSN Server has been successfully completed.
 */
void MsnSocketTcp::slotLoginFailed()
{
#ifdef KMESSDEBUG_CONNECTION_SOCKET_TCP
  kmDebug() << "Connection not successful within time limit.";
#endif

  // Bounce back to the unified error handler to give the user a message
  emit error( i18n( "Connection time limit exceeded" ), MsnSocketBase::ERROR_CONNECTION_TIME_LIMIT );
}



// Send a "ping" to the server
void MsnSocketTcp::slotSendPing()
{
  QString message;

  if ( pingReceived_ == false )
  {
    missedPings_ ++;

    message += i18np( "1 ping lost", "%1 pings lost", missedPings_ );

    emit statusMessage( message, false );
  }
  else
  {
    emit statusMessage( i18n( "Connected" ), false );
  }

  pingReceived_ = false;

  // Assume the connection was lost when 3 pings are missed.
  if ( missedPings_ == 3 )
  {
    // Bounce back to the unified error handler to disconnect and give the user a message
    emit error( i18n( "The connection to the server was lost" ), MsnSocketBase::ERROR_DROP );
  }

  writeData( "PNG\r\n" );

  emit pingSent();
}



/**
 * @brief Called when data is received from the server.
 *
 * This method parses the incoming raw data for the syntax used by the MSN protocol.
 * When a command is only partially received, it's buffered until the whole command is received by subsequent dataReceived() calls.
 *
 * Messages are then signalled back to MsnConnection which perform the parsing.
 * Only <code>PNG</code> commands are handled internally.
 */
void MsnSocketTcp::slotSocketDataReceived()
{
  bool        gotPayloadCommand  = false;
  bool        hasCompletePayload = false;
  QByteArray  rawCommandLine;
  QByteArray  payloadData;
  QStringList command;
  QString     commandLine;

  while( true )
  {
    // Read the next command if we're not waiting for a payload.
    if( nextPayloadSize_ == 0 )
    {
      // Stop when socket didn't receive a full line yet (Qt does the buffering, thanks Qt4!)
      if( ! socket_->canReadLine() )
      {
        break;
      }

      // Read the next line.
      rawCommandLine = socket_->readLine();

      // Parse the command, split in separate fields
      commandLine = QString::fromUtf8( rawCommandLine.data(), rawCommandLine.size() );
      command     = commandLine.trimmed().split(" ");

      // See if it's a payload command or an error
      if( isErrorCommand( command[0] ) )
      {
        // If the command contains a payload length parameter, also parse it
        gotPayloadCommand = ( command.size() > 2 );
      }
      else
      {
        gotPayloadCommand = isPayloadCommand( command[0] );
      }

      if( gotPayloadCommand )
      {
        nextPayloadSize_    = command[ command.count() - 1 ].toInt();  // Last arg is payload length.
        nextPayloadCommand_ = command;
      }
    }


    // See if we can read the payload
    if( nextPayloadSize_ > 0 )
    {
      // Check if the full payload is received.
      // Otherwise, just wait until "dataReceived" is called again.
      hasCompletePayload = ( socket_->bytesAvailable() >= nextPayloadSize_ );
      if( ! hasCompletePayload )
      {
        break;
      }

      // Full payload received.
      payloadData = socket_->read( nextPayloadSize_ );

      // Copy state from previous loop if needed.
      if( ! gotPayloadCommand )
      {
        gotPayloadCommand = true;
        command           = nextPayloadCommand_;
      }

      // Reset state for next loop.
      nextPayloadSize_ = 0;
    }
    else
    {
      payloadData.clear();
    }


    // Response from the internal ping timer.
    if( command[0] == "QNG" )
    {
      pingReceived_ = true;
      missedPings_  = 0;
    }
    else // All other data
    {
      emit dataReceived( command, payloadData );
    }
  }
}



/**
 * @brief Detect socket errors
 *
 * Closes the connection, and displays
 * the "could not connect to the MSN Messenger service." message box.
 *
 * @param  errorCode  The system error.
 */
void MsnSocketTcp::slotSocketError( QAbstractSocket::SocketError errorCode )
{
#ifdef KMESSDEBUG_CONNECTION_SOCKET_TCP
  kmWarning() << "Received error" << errorCode << "from the socket"
                "(socket state=" << socket_->state() << ").";
#endif

  ErrorType type;

  // Find the most appropriate kind of error to report
  switch( errorCode )
  {
    case QAbstractSocket::ConnectionRefusedError:
    case QAbstractSocket::HostNotFoundError:
      type = ERROR_CONNECTING;
      break;

    case QAbstractSocket::RemoteHostClosedError:
    case QAbstractSocket::NetworkError:
      type = ERROR_DROP;
      break;

    case QAbstractSocket::DatagramTooLargeError:
    case QAbstractSocket::UnknownSocketError:
      type = ERROR_DATA;
      break;

    case QAbstractSocket::ProxyConnectionRefusedError:
    case QAbstractSocket::ProxyConnectionClosedError:
    case QAbstractSocket::ProxyConnectionTimeoutError:
    case QAbstractSocket::ProxyNotFoundError:
    case QAbstractSocket::ProxyProtocolError:
      type = ERROR_PROXY;
      break;

    default:
      type = ERROR_UNKNOWN;
      break;
  }

  // Ignore errors when disconnected: avoids duplicate error dialogs
  if( connected_ )
  {
    emit error( socket_->errorString(), type );
  }
}



// Write data to the socket without conversions
qint64 MsnSocketTcp::writeBinaryData( const QByteArray &data )
{
  qint64 noBytesWritten;

  if( ! isConnected() )
  {
    kmWarning() << "Attempted to write data to a disconnected socket, aborting.";
    return -1;
  }

  // Write to the socket
  noBytesWritten = socket_->write( data.data(), data.size() );
  if ( noBytesWritten != data.size() )
  {
    kmWarning() << "Wanted to write " << data.size() << " bytes to the socket, wrote " << noBytesWritten << ".";
  }

  return noBytesWritten;
}



// Write data to the socket
qint64 MsnSocketTcp::writeData( const QString &data )
{
  qint64 noBytesWritten;

  if( ! isConnected() )
  {
    kmWarning() << "Attempted to write data to a disconnected socket, aborting.";
    return -1;
  }

  // Write to the socket
  QByteArray binData = data.toUtf8();
  noBytesWritten = socket_->write( binData.data(), binData.length() );
  if( noBytesWritten != binData.size() )
  {
    kmWarning() << "Wanted to write " << binData.size() << " bytes to the socket, wrote " << noBytesWritten << ".";
  }

  return noBytesWritten;
}



#include "msnsockettcp.moc"
