/***************************************************************************
                          msnftpconnection.cpp -  description
                             -------------------
    begin                : Tue 06 30 2005
    copyright            : (C) 2003 by Mike K. Bennett
                           (C) 2005 by Diederik van der Boor
    email                : mkb137b@hotmail.com
                           vdboor --at-- codingdomain.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 "msnftpconnection.h"

#include "../../kmessdebug.h"
#include "../../currentaccount.h"

#include <QFile>
#include <QStringList>


#ifdef KMESSDEBUG_MSNFTP
  #define KMESSDEBUG_MSNFTP_GENERAL
  // note that enabling KMESSDEBUG_MSNFTP_RECEIVING makes file transfer slow and inresponsive.
  #define KMESSDEBUG_MSNFTP_RECEIVING
#endif



// The constructor
MsnFtpConnection::MsnFtpConnection( const QString authHandle, const QString authCookie )
  : DirectConnectionBase()
  , authCookie_( authCookie )
  , authHandle_( authHandle )
  , fileBytesRemaining_(0)
  , fileSize_(0)
  , inputStream_(0)
  , mode_(WAIT)
  , outputStream_(0)
  , remainingBlockBytes_(0)
  , userCancelled_(false)
{
  setObjectName("MsnFtpConnection[" + authHandle + "]");

}



// The destructor
MsnFtpConnection::~MsnFtpConnection()
{

}



// Cancel the transfer
void MsnFtpConnection::cancelTransfer(bool userCancelled)
{
#ifdef KMESSDEBUG_MSNFTP_GENERAL
  kmDebug() << "Cancelling.";
#endif
#ifdef KMESSTEST
  KMESS_ASSERT( isConnected() );
#endif

  userCancelled_ = userCancelled;

  if(mode_ == SEND_DATA)
  {
    // Fail the transfer
    mode_ = SEND_CANCEL;
  }
  else if(mode_ == RECEIVE_DATA)
  {
    mode_ = SEND_CCL;
  }
  else
  {
    mode_ = SEND_CCL;
  }
}



// Close the connection
void MsnFtpConnection::closeConnection()
{
#ifdef KMESSDEBUG_MSNFTP_GENERAL
  kmDebug() << "MsnFtpConnection - closing sockets";
#endif

  // Reset the current mode
  mode_ = WAIT;

  // Close and delete sockets
  DirectConnectionBase::closeConnection();
}



// Emit a cancel message
void MsnFtpConnection::emitCancelStatusMessage(bool contactCancelled)
{
#ifdef KMESSTEST
  // Assume that this is only used from slotWriteMessage()
  KMESS_ASSERT( contactCancelled || mode_ == SEND_CANCEL || mode_ == SEND_CCL );
#endif

  if(userCancelled_)
  {
    // The user pressed the cancel button
    emit statusMessage( ki18n("You have cancelled the transfer of file &quot;%1&quot;."), STATUS_CHAT_FAILED   );
    emit statusMessage( i18n("Cancelled"),                                                STATUS_DIALOG_FAILED );
  }
  else if(contactCancelled)
  {
    // The contact pressed the cancel button
    emit statusMessage( ki18n("The contact has cancelled the transfer of file &quot;%1&quot;."), STATUS_CHAT_FAILED   );
    emit statusMessage( i18n("Cancelled"),                                                       STATUS_DIALOG_FAILED );
  }
  else
  {
    // This class sent the "CCL" message in a response, because the contact sent bad data.
    emit statusMessage( i18n("The file transfer invitation was cancelled. Bad data was received."), STATUS_CHAT_FAILED   );
    emit statusMessage( i18n("Failed"),                                                             STATUS_DIALOG_FAILED );
  }
}



// Parse a command received from the contact
void MsnFtpConnection::parseCommand(const QStringList &command)
{
  if(command[0] == "VER")
  {
    if(mode_ == WAIT_VER)
    {
      // Server: Send version confirmation
      mode_ = SEND_VER2;
    }
    else
    {
      // Client: Got version confirmation
      mode_ = SEND_USR;
    }
  }
  else if(command[0] == "FIL")
  {
    // Client: Get file size
    fileSize_           = command[1].toULong();
    fileBytesRemaining_ = fileSize_;
    mode_               = SEND_TFR;

    // Mark connection as authorized, this command is received in response to our USR.
    setAuthorized(true);
#ifdef KMESSDEBUG_MSNFTP_GENERAL
    kmDebug() << "There are " << fileBytesRemaining_ << " bytes to be received.";
#endif
  }
  else if(command[0] == "USR")
  {
    // Server: Accept user login
    if(// command[1] == authHandle_ &&  // TODO: send the correct handle to MimeApplication classes (msnswitchboardconnection problem)
       command[2] == authCookie_)
    {
      // Send file size with next write
      mode_ = SEND_FIL;
      setAuthorized(true);

      // We also know we have a good connection now.
      // Close the server so we can re-use the port later again.
      // (or we should keep it open all the time, and assign sockets to the correct msnftpconnection)
      closeServerSocket();
    }
    else
    {
      kmWarning() << "user authorisation was incorrect.";
#ifdef KMESSDEBUG_MSNFTP_GENERAL
      kmDebug() << "expected " << authHandle_ << "/" << authCookie_
                                      << " got " << command[1]  << "/" << command[2] << "." << endl;
#endif
      cancelTransfer(false);
      // TODO: test it
    }
  }
  else if(command[0] == "TFR")
  {
    // Server: Initiate transfer
    if(mode_ == WAIT_TFR)
    {
      // Client was authorized,
      // WAIT_TFR is set set after SEND_FIL
      mode_ = SEND_DATA;

      // Update status
      emit statusMessage( ki18n("Sending file %1"), STATUS_DIALOG );
    }
    else
    {
      // Never passed authorisation test
      kmWarning() << "user authorisation was skipped by contact.";
      cancelTransfer(false);
      // TODO: test it
    }
  }
  else if(command[0] == "BYE")
  {
    // Server: Got BYE from client
    emit transferComplete();
    closeConnection();
  }
  else if(command[0] == "CCL")
  {
    // Client/server: Got cancel from contact
    emitCancelStatusMessage(true);  // contact cancelled
    emit transferFailed();   // emit first, so base class see we failed after the connection was made.
    closeConnection();
  }
  else
  {
    kmDebug() << "File transfer got unhandled command: " << command[0] << ".";
    // Cancel, session is of no use now.
    cancelTransfer(false);
  }
}



// Handle the received file data
void MsnFtpConnection::parseReceivedFileData()
{
#ifdef KMESSTEST
  KMESS_ASSERT( outputStream_ != 0);
#endif

  // After we give the TFR signal, the contact (server) sends all file data
  // Each data block has a 3-byte header, and could be received in fragments

  char          rawBlock[2050];
  unsigned char code;
  unsigned char byte1;
  unsigned char byte2;
  qint64        blockSize;
  qint64        noBytesRead;


  // Make sure we read all available bytes from the socket before returning
  do   // while(getAvailableBytes() > 0);
  {
    // If no block was started
    if(remainingBlockBytes_ <= 0)
    {
      // Start with a new data block
      QByteArray controlBlock( 3, 0x00 );
      readBlock( controlBlock, 3);

#ifdef KMESS_NETWORK_WINDOW
      KMESS_NET_RECEIVED( this, controlBlock );
#endif

      code  = controlBlock[0];  // First byte: 0=data, 1=control
      byte1 = controlBlock[1];  // Next two bytes contain
      byte2 = controlBlock[2];  // the data size

      if(code == 0)
      {
        // A data block was received

        // Merge the two bytes as integer
        // This is the block-size to expect
        remainingBlockBytes_ = (byte1 | byte2 << 8);

#ifdef KMESSDEBUG_MSNFTP_RECEIVING
        kmDebug() << "Receiving new block, size = " << remainingBlockBytes_ << ". "
                 << "socket bytes available: " << getAvailableBytes() << "." << endl;
#endif
      }
      else if(code == 1)
      {
        // A control block was received
        if( byte1 == 0 && byte2 == 0 )
        {
          // The transfer was terminated by the sender.
          // TODO: better messages for KMess 1.5 (like "aborted by receiver" for the dialog, "the contact aborted the file transfer")
          emitCancelStatusMessage(true);
          mode_ = SEND_BYE;  // emits transferFailed()
          return;
        }
      }
      else
      {
        // Contact did not sent a valid block
        // At this point, the official client displays "corrupt file received".
        cancelTransfer(false);
        emit transferFailed();
        return;
      }
    }

    // If there is a block waiting
    if(remainingBlockBytes_ > 0)
    {
      // Determine max size to read (avoid buffer overflows!)
      blockSize = remainingBlockBytes_;
      if(blockSize > ((int) sizeof(rawBlock))) blockSize = sizeof(rawBlock);

      // Read the data block (usually 2045 bytes)
      noBytesRead = readBlock( rawBlock, blockSize );

#ifdef KMESS_NETWORK_WINDOW
      QByteArray wrapper = QByteArray::fromRawData( rawBlock, (int)noBytesRead );
      KMESS_NET_RECEIVED( this, wrapper );
#endif

      if(noBytesRead <= 0)
      {
#ifdef KMESSDEBUG_MSNFTP_GENERAL
        kmWarning() << "read error (returned " << noBytesRead << " block size=" << blockSize << ")";
#endif
        // Socket error, exit
        emit transferFailed();
//        cancelTransfer(false);
        closeConnection();
        return;
      }
      else
      {
        // Update the progress dialog
        fileBytesRemaining_  -= noBytesRead;
        remainingBlockBytes_ -= noBytesRead;
        emit transferProgess( (qint32)( fileSize_ - fileBytesRemaining_ ) );

        // Add data to file
        outputStream_->write( rawBlock, noBytesRead );

#ifdef KMESSDEBUG_MSNFTP_RECEIVING
        kmDebug() << noBytesRead << " bytes read.  "
                 << fileBytesRemaining_ << " bytes remaining." << endl;
#endif
      }


      // See if all data is received
      if(fileBytesRemaining_ <= 0)
      {
#ifdef KMESSDEBUG_MSNFTP_GENERAL
        kmDebug() << "Done receiving file data.";
#endif
        mode_ = SEND_BYE;
        return;
      }
    }
  }
  while(getAvailableBytes() > 0);
}



// Send a file
bool MsnFtpConnection::sendFile(QFile *inputFile)
{
#ifdef KMESSTEST
  KMESS_ASSERT( authHandle_.length() > 0 );
  KMESS_ASSERT( authCookie_.length() > 0 );
  KMESS_ASSERT( inputFile != 0);
  KMESS_ASSERT( inputFile->isOpen() && inputFile->isReadable() );
#endif

  // Prepare
  inputStream_        = inputFile;
  fileSize_           = inputStream_->size();
  fileBytesRemaining_ = fileSize_;
  mode_               = SEND_VER;

  // Start listing for the connection
  bool listening = openServerPort();
  if(listening)
  {
    // Success!
    QString externIp( CurrentAccount::instance()->getExternalIp() );
    emit statusMessage( i18n( "Awaiting connection at %1, port %2",
                              externIp, // display the external ip submitted to the contact, not our local lan ip.
                              QString::number( getLocalServerPort() ) ),
                              STATUS_DIALOG
                      );
  }
  else
  {
    // Failed!
    emit statusMessage( ki18n("The transfer of %1 failed. Could not open a local port."), STATUS_CHAT_FAILED);
    emit statusMessage( i18n("Could not open a local port."),                             STATUS_DIALOG_FAILED);
    emit transferFailed();
  }

  // The return code only indicates whether this object is listening,
  // it can be used to send network commands, but status messages are delivered with the signals
  return listening;
}



// Retrieve a file
bool MsnFtpConnection::retrieveFile(QFile *outputFile, const QString &ipAddress, const quint16 port)
{
#ifdef KMESSTEST
  KMESS_ASSERT( authHandle_.length() > 0 );
  KMESS_ASSERT( authCookie_.length() > 0 );
  KMESS_ASSERT( outputFile != 0 );
  KMESS_ASSERT( outputFile->isOpen() && outputFile->isWritable() );
#endif

  // Open the file
  outputStream_       = outputFile;
  fileSize_           = 0;    // is set by FIL command
  fileBytesRemaining_ = 999;  // dummy value for FIL command
  mode_               = SEND_VER;

  // Open the connection
  bool socketCreated = openConnection(ipAddress, port);
  if( ! socketCreated )
  {
    // Tell the user a connection could not be made.
    slotConnectionFailed();
    return false;
  }
  else
  {
    // Wait for slotConnectionEstablished() or slotSocketError()
    return true;
  }
}



// This is called when a connection is established.
void MsnFtpConnection::slotConnectionEstablished()
{
#ifdef KMESS_NETWORK_WINDOW
  KMESS_NET_INIT(this, "FTP " + getRemoteIp());
#endif

  // Connect the write handler. This can't be done earlier,
  // as the parent socket_ object is not created before.
  // Messages are sent each time the socket is ready for writing.
  connectWriteHandler(this, SLOT(slotWriteData()));

  // Signal we've got a connection
  emit statusMessage( i18n("Initiating file transfer"), STATUS_DIALOG );
}



// This is called when the connection could not be made.
void MsnFtpConnection::slotConnectionFailed()
{
#ifdef KMESSTEST
  KMESS_ASSERT( ! isServer() );  // slot only applies to connections initialized with openConnection()
#endif

  // Failed!
  emit statusMessage( ki18n("The transfer of %1 failed. A connection could not be made."), STATUS_CHAT_FAILED );
  emit statusMessage( i18n("Unable to make a connection."),                               STATUS_DIALOG_FAILED );
  emit transferFailed();
}



// This is called when data is received from the socket.
void MsnFtpConnection::slotSocketDataReceived()
{
  // This function either delegates the control
  // to parseCommand() or parseReceivedFileData()

  char          rawBlock[255];
  qint64        blockSize;
  qint64        noBytesRead;
  QString       commandLine;
  QStringList   command;

  if(mode_ == RECEIVE_DATA)
  {
    // Session is in the data transfer stage
    parseReceivedFileData();
  }
  else
  {
    // In the command stage, parse the commands

    // Get number of available bytes
    blockSize = 500; // getAvailableBytes() is always 0
    if( blockSize >= (int) sizeof(rawBlock) )
    {
      // Safety net, shouldn't happen though
      blockSize = sizeof(rawBlock);
    }

    // Read all received data without buffering
    // This can't hurt, because MSNFTP is a stop-and-wait protocol.
    // It's not possible to receive multiple lines at once.
    noBytesRead = readBlock( rawBlock, blockSize );
    if(noBytesRead <= 0)
    {
      // Error code or buffer empty.
      return;
    }

    // Convert data block to UTF8 string
    commandLine = QString::fromUtf8( rawBlock, (int)noBytesRead );

#ifdef KMESS_NETWORK_WINDOW
    KMESS_NET_RECEIVED( this, commandLine.toUtf8() );
#endif

    // Strip the newline character
    if(commandLine.contains("\r\n"))
    {
      commandLine = commandLine.left(commandLine.indexOf("\r\n"));
    }

    // Split command into separate fields
    command = commandLine.split( " " );


#ifdef KMESSDEBUG_MSNFTP_GENERAL
    kmDebug() << "<<< " << commandLine;
#endif

    // Parse the command
    parseCommand( command );
  }
}



// The socket is ready for writing.  Write any outstanding commands.
void MsnFtpConnection::slotWriteData()
{
  switch(mode_)
  {
    case WAIT :
    {
      // Do nothing.  Wait for slotSocketDataReceived()
      break;
    }
    case SEND_VER :
    {
      // Client: Send the version
      writeMessage("VER MSNFTP\r\n");
      mode_ = WAIT;  // Wait for server response (VER)
      break;
    }
    case SEND_USR :
    {
      // Client: Send authorisation
      writeMessage("USR " + authHandle_ + " " + authCookie_ + "\r\n");
      mode_ = WAIT;  // Wait for server response (FIL)
      break;
    }
    case SEND_TFR :
    {
      // Client: Send signal to start the transfer
      writeMessage("TFR\r\n");
      mode_ = RECEIVE_DATA;

      // Update the status
      emit statusMessage( ki18n("Receiving file %1"), STATUS_DIALOG );

      break;
    }
    case RECEIVE_DATA :
    {
      // Client: Receiving data with slotSocketDataReceived()
      break;
    }
    case SEND_BYE :
    {
      // Client: Send the bye
      QString message;
      writeMessage("BYE 16777989\r\n");
      mode_ = WAIT;

      // Update the status
      if(this->fileBytesRemaining_ == 0)
      {
        emit transferComplete();
      }
      else
      {
        emit transferFailed();
      }
      break;
    }
    case WAIT_VER :
    {
      // Server: waiting for client to send version, do nothing
      break;
    }
    case SEND_VER2 :
    {
      // Server: Waiting for VER, send VER back
      writeMessage("VER MSNFTP\r\n");
      mode_ = WAIT;  // Wait for client response (USR)
      break;
    }
    case SEND_FIL :
    {
      // Server: Waiting for USR, send FIL back
      writeMessage("FIL " + QString::number(fileSize_) + "\r\n");
      mode_ = WAIT_TFR;  // Wait for client response (TFR)
      break;
    }
    case WAIT_TFR :
    {
      // Server: Wait for TFR, do nothing
      break;
    }
    case SEND_DATA :
    {
      // Server: Send data
      writeFileData();
      break;
    }
    case SEND_CANCEL :
    {
      // Server: Cancel the transfer
      writeCancelData();

      // Update the status
      emitCancelStatusMessage(false);   // we send cancel, contact didn't

      // wait for contact to send BYE (and close afterwards)
      // TODO: use a timeout here
      break;
    }
    case SEND_CCL :
    {
      // Server/client: Send a CCL message to abort
      writeMessage("CCL\r\n");

      // Update the status
      emitCancelStatusMessage(false);   // we send cancel, contact didn't

      // contact should close the connection now.
      // TODO: use a timeout here
      mode_ = WAIT;
      emit transferFailed();   // for now, close manually
      break;
    }
  }
}



// Write a cancel data block
void MsnFtpConnection::writeCancelData()
{
#ifdef KMESSDEBUG_MSNFTP_GENERAL
  kmDebug() << "Cancelling transfer";
#endif

  // Send a new data block
  char out[3];
  out[0] = 0x01;
  out[1] = 0x00;
  out[2] = 0x00;
  writeBlock( out, 3 );
  mode_ = WAIT;
}



// Write more file data to the socket
void MsnFtpConnection::writeFileData()
{
#ifdef KMESSTEST
  KMESS_ASSERT( inputStream_ != 0 );
#endif

  qint64 noBytesRead;
  char   rawBlock[2048];

  // Read a block from the file.
  noBytesRead = inputStream_->read( rawBlock + 3, sizeof(rawBlock) - 3 );

  if(noBytesRead < 0)
  {
    // File read error
    writeCancelData();

    // Add status message
    userCancelled_ = false;
    emitCancelStatusMessage(false);

    // Wait for contact to quit
    mode_ = WAIT;
    return;
  }

#ifdef KMESSDEBUG_MSNFTP_GENERAL
  kmDebug() << noBytesRead         << " bytes written.  "
           << fileBytesRemaining_ << " bytes remaining."
           << " connected=" << isConnected() << endl;
#endif

  // Set the header
  rawBlock[0] = 0;
  rawBlock[1] = (char)  ( noBytesRead & 0x00ff );
  rawBlock[2] = (char)( ( noBytesRead & 0xff00 ) >> 8 );

  // Update the progress
  fileBytesRemaining_ -= noBytesRead;
  emit transferProgess( (qint32)( fileSize_ - fileBytesRemaining_ ) );

  // Write the data to the socket_
  writeBlock( rawBlock, noBytesRead + 3 );

  // See if we're done
  if(fileBytesRemaining_ <= 0 )
  {
    kmDebug() << "done transferring file.";
    mode_ = WAIT;  // Wait for BYE
  }
}



// Write a message to the socket.
void MsnFtpConnection::writeMessage(QString message)
{
  QByteArray messageUtf8 = message.toUtf8();
  writeBlock( messageUtf8 );

#ifdef KMESSDEBUG_MSNFTP_GENERAL
  kmDebug() << "mode is " << mode_;
  kmDebug() << "MsnFtpConnection >>> " << message;
#endif
}


#include "msnftpconnection.moc"
