/***************************************************************************
                          filetransfer.cpp  -  description
                             -------------------
    begin                : Mon Mar 24 2003
    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 "filetransfer.h"

#include <qfile.h>

#include <kdebug.h>
#include <kextsock.h>
#include <klocale.h>

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

// It wouldn't hurt if these GUI specific features are removed here.
// That would make this class GUI-independant and only emit signals for possible GUI actions.
#include <kfiledialog.h>
#include "../../dialogs/transferwindow.h"
#include "../../dialogs/transferentryinterface.h"
#include "../../dialogs/transferentry.h"

#ifdef KMESSDEBUG_FILETRANSFER
#define KMESSDEBUG_FILETRANSFER_GENERAL
#endif

// The constructor
FileTransfer::FileTransfer(const QString& authHandle, const QString& localIp)
  : MimeApplication(localIp)
  , authHandle_(authHandle)
  , connectivity_('?')
  , file_(0)
  , msnFtpConnection_(0)
  , transferPanel_(0)
{

}



// The destructor
FileTransfer::~FileTransfer()
{
  // Make sure the transfer panel displays the cancel state
  if(transferPanel_ != 0)
  {
    // The object can be deleted by the user
    transferPanel_->failTransfer();
    transferPanel_ = 0;
  }

  // Stop the ftp connection
  delete msnFtpConnection_;

  // Close the file nicely
  if(file_ != 0)
  {
    delete file_;
  }
}



// Connect signals of the MsnFtpConnection object
void FileTransfer::connectMsnFtpConnection()
{
#ifdef KMESSTEST
  ASSERT( msnFtpConnection_ != 0 );
#endif

  connect(msnFtpConnection_, SIGNAL(              statusMessage(QString,int)   ) ,  // Display a message in the dialog/chat window
          this,                SLOT(    slotMsnFtpStatusMessage(QString,int)   ) );
  connect(msnFtpConnection_, SIGNAL(           transferComplete()              ) ,  // Signal that the transfer is complete
          this,                SLOT( slotMsnFtpTransferComplete()              ) );
  connect(msnFtpConnection_, SIGNAL(             transferFailed()              ) ,  // Signal that the transfer failed (and clean up)
          this,                SLOT(   slotMsnFtpTransferFailed()              ) );
  connect(msnFtpConnection_, SIGNAL(            transferProgess(unsigned long) ) ,  // Signal that the transfer made progress
          this,                SLOT(  slotMsnFtpTransferProgess(unsigned long) ) );
}



// Step one of a contact-started chat: the contact invites the user
void FileTransfer::contactStarted1_ContactInvitesUser(const MimeMessage& message)
{
#ifdef KMESSDEBUG_FILETRANSFER_GENERAL
  kdDebug() << "FileTransfer - contactStarted1_ContactInvitesUser" << endl;
#endif
  QString html;

  // Get the file name and size from the message
  shortFileName_ = message.getValue("Application-File");
  fileSize_      = message.getValue("Application-FileSize").toInt();
  connectivity_  = message.getValue("Connectivity").at(0).latin1();

  // Send the message to the chat window.
  html = i18n("Do you want to accept the file: %1 (%2 bytes)")
         .arg( "<font color=red>" + shortFileName_ + "</font>" )
         .arg( fileSize_ );

  offerAcceptOrReject( html );
}



// Step two of a contact-started chat: the user accepts
void FileTransfer::contactStarted2_UserAccepts()
{
#ifdef KMESSDEBUG_FILETRANSFER_GENERAL
  kdDebug() << "ReceiveFile - contactStarted2_UserAccepts" << endl;
#endif
#ifdef KMESSTEST
  ASSERT( file_ == 0 );
#endif

  QString recentFolderTag = ":filedownload"; // Make it a QString()
  KURL startDir;
  bool open;

  // Get an initial path to the file
  startDir = KFileDialog::getStartURL(QString::null, recentFolderTag);
  startDir.addPath(shortFileName_);

  // Open a file dialog so that the user can
  // save the file to a particular directory and name.
  // Give the last path + received short name as suggestion.
  // This is one before the actual accept message is sent,
  // so we can respond to the "cancel" button early.
  fileName_ = KFileDialog::getSaveFileName( startDir.url() );

  if(fileName_.isNull())
  {
    // Dialog cancelled, cancel afterall
    userRejected();
    return;
  }
  else
  {
    // Create a file object, open for writing
    file_ = new QFile(fileName_);
    open  = file_->open(IO_WriteOnly);
    if(! open)
    {
      // Important: undo what we created
      delete file_;
      file_     = 0;
      fileName_ = QString::null;

      // Cancel invitation afterall
      sendCancelMessage(CANCEL_FAILED);
      slotMsnFtpStatusMessage( i18n("The transfer of %1 failed.  Couldn't open file."), MsnFtpConnection::STATUS_CHAT );
      endApplication();
      return;
    }
    else
    {
      // All passed. Send the accept message
      MimeMessage message;
      message.addField( "Invitation-Command", "ACCEPT"         );
      message.addField( "Invitation-Cookie",  getCookie()      );
      message.addField( "Launch-Application", "FALSE"          );
      message.addField( "Request-Data",       "IP-Address:"    );

      sendMessage( message );
    }
  }
}



// Step three of a contact-started chat: the contact confirms the accept
void FileTransfer::contactStarted3_ContactConfirmsAccept(const MimeMessage& message)
{
#ifdef KMESSDEBUG_FILETRANSFER_GENERAL
  kdDebug() << "FileTransfer - contactStarted3_ContactConfirmsAccept" << endl;
#endif
#ifdef KMESSTEST
  ASSERT( ! fileName_.isEmpty()      );
  ASSERT( ! shortFileName_.isEmpty() );
  ASSERT( file_             != 0     );
  ASSERT( msnFtpConnection_ == 0     );
#endif

  QString ip;
  QString ipInternal;
  int     port;
  int     portXInternal;
  int     portX;

  // Pull the IP, port, and authorization info from the message
  ip            = message.getValue( "IP-Address"          );
  ipInternal    = message.getValue( "IP-Address-Internal" );          // As of MSN5
  portXInternal = message.getValue( "PortX-Internal"      ).toInt();  // As of MSN5
  port          = message.getValue( "Port"                ).toInt();
  portX         = message.getValue( "PortX"               ).toInt();  // As of MSN5
  authCookie_   = message.getValue( "AuthCookie"          );

/*
 * This is an attempt to get file transfers
 * between two home systems working.. (both behind the same NAT router)
 * I didn't discover any reference when I started this.
 *
 * TODO: Implement MSN5-compatible transfers using http://www.hypothetic.org/docs/msn/phorum/read.php?f=1&i=3435&t=3372#reply_3435
 *       A full implementation also requires UPnP handling.
 *
 * When receiving an invite with a "Connectivity: N" field,
 * and the client it's not being translated by a NAT, set a "Sender-Connect: TRUE" field,
 * and include addressing information for both internal and external addresses, and start a server.
 * Otherwise, start a client.
 *
 * Presumably, a client can try three connections when attempting to connect to a server:
 * - To IP-Address on Port
 * - To IP-Address on PortX
 * - To IP-Address-Internal on PortX-Internal.
 * Using IP-Address on Port and IP-Address-Internal on PortX-Internal should be sufficient.
 */

  QString externalIP = getExternalIp();
  if(ip == externalIP)
  {
    // The other client must be at the same network..!
    // Try to connect internally.

    // Just to be sure the values are set...
    if(! ipInternal.isEmpty())
    {
      ip   = ipInternal;
      //port = internPort;
      port = 6891; // Only works if there is one transfer, but it's better then nothing.
    }
    // else: the file transfer fails anyway
  }


#ifdef KMESSDEBUG_FILETRANSFER_GENERAL
  kdDebug() << "FileTransfer - IP: " << ip << " Port: " << port << " AuthCookie: " << authCookie_ << endl;
#endif

  // Initialize the MSNFTP connection
  msnFtpConnection_ = new MsnFtpConnection();
  msnFtpConnection_->setAuthInfo( CurrentAccount::instance()->getHandle(), authCookie_ );
  connectMsnFtpConnection();

  // Initialize the progess dialog
  initializeProgressDialog(true);
  slotMsnFtpStatusMessage( i18n("Connecting to %1, port %2").arg(ip).arg(port), MsnFtpConnection::STATUS_DIALOG );

  // Start the transfer
  bool connected = msnFtpConnection_->retrieveFile(file_, ip, port);

  // Signal when we're failed to connect,
  // don't display a status message or destroy the object.
  // The signals of msnftpconnection do this already.
  if(! connected)
  {
    sendCancelMessage(CANCEL_FAILED);
  }
}



// Return the application's GUID
QString FileTransfer::getAppId()
{
  return "{5D3E02AB-6190-11d3-BBBB-00C04F795683}";
}



// Return a cancel message to the user
QString FileTransfer::getCancelMessage() const
{
  // Application::getCancelMessage() returns "You have cancelled the session".
  return i18n("The transfer was cancelled");
}



// Create and initilize the progress dialog.
void FileTransfer::initializeProgressDialog(bool incoming)
{
#ifdef KMESSTEST
  ASSERT( transferPanel_     == 0 );
  ASSERT( fileSize_          != 0 );
  ASSERT( fileName_.length()  > 0 );
#endif

  // Create a new entry in the tranfer window
  TransferWindow *transferWindow = TransferWindow::instance();
  transferPanel_ = transferWindow->addEntry( fileName_, fileSize_, incoming );

  // Connect the dialog so that if the user closes it, it's deleted.
  connect( transferPanel_,    SIGNAL( cancelTransfer()     ) ,
           this,              SLOT  ( slotCancelTransfer() ) );

  transferWindow->show();
}



// Cancelled the file transfer from the TransferWindow
void FileTransfer::slotCancelTransfer()
{
#ifdef KMESSDEBUG_FILETRANSFER_GENERAL
  kdDebug() << "FileTransfer - slotCancelTransfer" << endl;
#endif

  // This method is activated from the transferPanel.
  // cancelClicked() will already call failTransfer()
  // make sure we don't (or call updateProcess() by accident)
  transferPanel_ = 0;

  // Call userAborted() which cleans up this object correctly
  userAborted();
}



// Display a status message
void FileTransfer::slotMsnFtpStatusMessage(QString message, int statusType)
{
#ifdef KMESSDEBUG_FILETRANSFER_GENERAL
  kdDebug() << "FileTransfer - Status message (type="    << statusType
                                          << " message=" << message
                                          << " panel="   << (transferPanel_ != 0) << ")." << endl;
#endif

  switch(statusType)
  {
    // A message for the transfer dialog
    case MsnFtpConnection::STATUS_DIALOG:
    {
      if(transferPanel_ != 0)
      {
        // Parse %1 placeholder for filename, and display
        if(message.contains("%1")) message = message.arg(fileName_);
        transferPanel_->setStatusMessage( message );
      }
      break;
    }
    case MsnFtpConnection::STATUS_DIALOG_FAILED:
    {
      if(transferPanel_ != 0)
      {
        if(message.contains("%1")) message = message.arg(fileName_);
        transferPanel_->failTransfer( message );
        transferPanel_ = 0;
      }
      break;
    }

    // A message to display in the chat window
    case MsnFtpConnection::STATUS_CHAT_FAILED:
    {
      if(message.contains("%1")) message = message.arg("<font color=red>" + shortFileName_ + "</font>");
      emit appMessage( message );
      break;
    }
    case MsnFtpConnection::STATUS_CHAT:
    default:
    {
      if(message.contains("%1")) message = message.arg("<font color=blue>" + shortFileName_ + "</font>");
      emit appMessage( message );
      break;
    }
  }
}



// Signal that the transfer was succesful
void FileTransfer::slotMsnFtpTransferComplete()
{
#ifdef KMESSDEBUG_FILETRANSFER_GENERAL
  kdDebug() << "FileTransfer - Successful Transfer" << endl;
#endif

  // Display a message to the user
  if( transferPanel_ != 0 )
  {
    transferPanel_->finishTransfer();
    transferPanel_ = 0;
  }
  emit fileTransferred(fileName_);

  // Terminate this application
  endApplication();
}



// Signal that the transfer failed
void FileTransfer::slotMsnFtpTransferFailed()
{
#ifdef KMESSDEBUG_FILETRANSFER_GENERAL
  kdDebug() << "FileTransfer - Transfer Failed" << endl;
#endif

  // If MsnFtpConnection did not emit a statusMessage(.. STATUS_DIALOG_FAILED) signal
  // the dialog will be terminated with a default message here.
  if( transferPanel_ != 0 )
  {
    transferPanel_->failTransfer();
    transferPanel_ = 0;
  }

  // Terminate this application
  endApplication();
}



// Update the progress dialog with the number of bytes transferred.
void FileTransfer::slotMsnFtpTransferProgess(unsigned long bytesReceived)
{
  if ( transferPanel_ != 0 )
  {
    transferPanel_->updateProgress( bytesReceived );
  }
}



// The user cancelled the session
void FileTransfer::userAborted()
{
#ifdef KMESSDEBUG_FILETRANSFER_GENERAL
  kdDebug() << "FileTransfer - userAborted" << endl;
#endif

  // Make sure the transfer panel displays the cancel state
  if(transferPanel_ != 0)
  {
    // The object can be deleted by the user
    transferPanel_->failTransfer();
    transferPanel_ = 0;
  }

  // Then teardown the active transfer
  if(msnFtpConnection_ != 0)
  {
    // We have a MsnFtpConnection listening
    if(msnFtpConnection_->isConnected())
    {
      // It is already sending the file
      // Initiate connection teardown

      // Disconnect signals
      // We are not interested in status messages, they are displayed here already
      // slotWriteCommand() calls slotMsnFtpTransferFailed() which deletes this object, we do this here already
      disconnect(msnFtpConnection_, SIGNAL(statusMessage(QString,int)), this, SLOT(slotMsnFtpStatusMessage(QString,int)));
      disconnect(msnFtpConnection_, SIGNAL(transferFailed()),           this, SLOT(slotMsnFtpTransferFailed()));

      // Tell MsnFtpConnection to stop transferring. This also signals the other client we cancelled
      // delete once all signals are processed (since we detached slotMsnFtpTransferFailed())
      msnFtpConnection_->cancelTransfer(true);
      msnFtpConnection_->deleteLater();
      msnFtpConnection_ = 0;

      // we don't have to send a cancel message anymore.
      // MsnFtpConnection already does the same thing.
    }
    else
    {
      // No transfer, but close the connection ASAP
      msnFtpConnection_->closeConnection();
      msnFtpConnection_->deleteLater();
      msnFtpConnection_ = 0;

      // The transfer was not initiated.
      // Send the cancel-session message (same as Application::userAborted())
      sendCancelMessage(CANCEL_SESSION);
    }
  }
  else
  {
    // Same as above
    sendCancelMessage(CANCEL_SESSION);
  }


  // Finally delete this object
  endApplication( getCancelMessage() );
}



// Step one of a user-started chat: the user invites the contact
void FileTransfer::userStarted1_UserInvitesContact()
{
#ifdef KMESSDEBUG_FILETRANSFER_GENERAL
  kdDebug() << "FileTransfer - userStarted1_UserInvitesContact" << endl;
#endif
  QString     sizeString;
  MimeMessage message;

  // Show a dialog to get a file path from the user.
  fileName_ = KFileDialog::getOpenFileName( );
  if(fileName_.isEmpty()) return;   // user pressed cancel

  // Get a short file name 
  shortFileName_ = fileName_.right( fileName_.length() - fileName_.findRev('/') - 1 );

  // Check if the file truly exists
  if(! QFile::exists(fileName_))
  {
    // Fake an event
    // No need to show a progress-dialog message, isn't not created here yet
    slotMsnFtpStatusMessage( i18n("The transfer of %1 failed.  The file doesn't exist."), MsnFtpConnection::STATUS_CHAT );
    endApplication();
    return;
  }


  // Create a file object
  file_ = new QFile(fileName_);

  // Open the file for reading
  bool open = file_->open(IO_ReadOnly);
  if ( !open )
  {
    // Important: undo what we created
    delete file_;
    file_     = 0;
    fileName_ = QString::null;

    // Leave
    slotMsnFtpStatusMessage( i18n("The transfer of %1 failed.  Couldn't open file."), MsnFtpConnection::STATUS_CHAT );
    endApplication();
    return;
  }


  // Get the file parameters for the message
  fileSize_      = file_->size();
  sizeString     = QString::number(fileSize_);

  // Create the invitation message
  //  connectivity_ = 'U';   // Or 'N' if the port is mapped with NAT
  message.addField( "Application-Name",     "File Transfer" );
  message.addField( "Application-GUID",     getAppId()      );
  message.addField( "Invitation-Command",   "INVITE"        );
  message.addField( "Invitation-Cookie",    getCookie()     );
  message.addField( "Application-File",     shortFileName_  );
  message.addField( "Application-FileSize", sizeString      );
//  message.addField( "Connectivity",         connectivity_   );

  sendMessage( message );

  // Give the user the option of cancelling the transfer
  offerCancel( i18n("Sending file %1").arg("<font color=blue>" + shortFileName_ + "</font>") );
}



// Step two of a user-started chat: the contact accepts
void FileTransfer::userStarted2_ContactAccepts(const MimeMessage& /*message*/)
{
#ifdef KMESSDEBUG_FILETRANSFER_GENERAL
  kdDebug() << "FileTransfer - userStarted2_ContactAccepts" << endl;
#endif
#ifdef KMESSTEST
  ASSERT( msnFtpConnection_ == 0 );
#endif

  QString html;
  QString portString;

  // Create a message showing that the transfer was accepted.
  emit appMessage( i18n("Transfer accepted.") );

  // Get the authorisation cookie to use
  authCookie_ = generateCookie();

  // Initialize the msnftp connection class
  msnFtpConnection_ = new MsnFtpConnection();
  msnFtpConnection_->setAuthInfo(authHandle_, authCookie_);
  connectMsnFtpConnection();

  // Get the IP,port to use
  portString  = QString::number( msnFtpConnection_->getLocalServerPort() );

  // Initialize the progress dialog
  initializeProgressDialog();

  // Set the first status message.
  // TODO: replace this mesage with something like "Negotiating session parameters"
  slotMsnFtpStatusMessage( i18n("File transfer dialog message", "Negotiating file transfer mode"), MsnFtpConnection::STATUS_DIALOG );

  // Create the invitation message
  // TODO: PortX-Internal and PortX should be another port.
  //       We should be listening to port 11178 as well for internal transfers
  MimeMessage response;
  response.addField( "Invitation-Command",  "ACCEPT"         );
  response.addField( "Invitation-Cookie",   getCookie()      );
  response.addField( "IP-Address",          getExternalIp()  );
//  response.addField( "IP-Address-Internal", getLocalIp()     );
//  response.addField( "PortX-Internal",      portString       );
  response.addField( "Port",                portString       );
//  response.addField( "PortX",               portString       );
  response.addField( "Launch-Application",  "FALSE"          );
  response.addField( "AuthCookie",          authCookie_      );

  sendMessage( response );
}



// Connect to the port and start listening for the transfer.
void FileTransfer::userStarted3_UserPrepares()
{
#ifdef KMESSTEST
  ASSERT( msnFtpConnection_ != 0 );
  ASSERT( file_             != 0 );
#endif

  if(file_ != 0)
  {
    bool listening = msnFtpConnection_->sendFile(file_);

    // Signal when we're failed to listen,
    // don't display a status message or destroy the object.
    // The signals of msnftpconnection do this already.
    if(! listening)
    {
      sendCancelMessage(CANCEL_FAILED);
    }
  }
  else
  {
#ifdef KMESSDEBUG_FILETRANSFER_GENERAL
    kdDebug() << "FileTransfer::userStarted3_UserPrepares: file_ is a null pointer!" << endl;
#endif
    slotMsnFtpTransferFailed();
  }
}


#include "filetransfer.moc"
