/***************************************************************************
                          filetransferp2p.cpp -  description
                             -------------------
    begin                : Sun 12 19 2004
    copyright            : (C) 2004 by Diederik van der Boor
    email                : 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 "filetransferp2p.h"

#include "../mimemessage.h"
#include "../p2pmessage.h"
#include "../../msnobject.h"
#include "../../kmessdebug.h"
#include "../../dialogs/transferentryinterface.h"
#include "../../dialogs/transferentry.h"
#include "../../dialogs/transferwindow.h"

#include <qfile.h>
#include <qregexp.h>
#include <kdebug.h>
#include <kmdcodec.h>
#include <klocale.h>
#include <kurl.h>
#include <kfiledialog.h>



/**
 * The constructor for the FileTransferP2P class, without filename (sufficient for incoming sessions)
 *
 * @param  localIP        The IP-Address of the SwitchBoard socket.
 * @param  contactHandle  Person to send P2P messages to.
 */
FileTransferP2P::FileTransferP2P(const QString &localIP, const QString &contactHandle)
  : P2PApplication(localIP, contactHandle),
  file_(0),
  fileSize_(0),
  transferPanel_(0)
{
}



/**
 * The constructor for the FileTransferP2P, with filename to start a session
 *
 * @param  localIP        The IP-Address of the SwitchBoard socket.
 * @param  contactHandle  Person to send P2P messages to.
 * @param  filename       Filename of the file to send.
 */
FileTransferP2P::FileTransferP2P(const QString &localIP, const QString &contactHandle, const QString &filename)
  : P2PApplication(localIP, contactHandle),
  file_(0),
  fileName_(filename),
  fileSize_(0),
  transferPanel_(0)
{
}



/**
 * Destructor, closes the file if it's open.
 */
FileTransferP2P::~FileTransferP2P()
{
  // Make sure the transfer panel displays the cancel state
  if(transferPanel_ != 0)
  {
    // The object can be deleted by the user
    transferPanel_->failTransfer();
    transferPanel_ = 0;
  }

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



/**
 * Step one of a contact-started chat: the contact invites the user
 *
 * @param  message  The invitation message
 */
void FileTransferP2P::contactStarted1_ContactInvitesUser(const MimeMessage &message)
{
#ifdef KMESSDEBUG_FILETRANSFER_P2P
  kdDebug() << "FileTransferP2P - contactStarted1_ContactInvitesUser" << endl;
#endif

  // Get the content type
  QString contentType = getInvitationContentType();

  // Determine the content-type, and read those fields as well:
  if(contentType == "application/x-msnmsgr-sessionreqbody")
  {
#ifdef KMESSTEST
    ASSERT( transferPanel_ == 0 );
#endif

    // First stage of the file transfer: initiate the session

    // Read the values from the message
    unsigned long int appID   = message.getValue("AppID").toUInt();
    QString           context = message.getValue("Context");

    if(appID != 2)
    {
      kdDebug() << "FileTransferP2P: WARNING - Received unexpected AppID: " << appID << "." << endl;

      // Wouldn't know what to do if the AppID is not 2, so send an 500 Internal Error back:
      sendCancelMessage(CANCEL_SESSION);
      emit appInitMessage( i18n("The file transfer invitation was cancelled.  Bad data was received.") );

      // Don't QUIT, this is the accept stage. The contact should ACK back.
      return;
    }


    // The context field contains file transfer data.
    QByteArray encodedContext;
    QByteArray decodedContext;
    encodedContext.duplicate(context.data(), context.length());
    KCodecs::base64Decode(encodedContext, decodedContext);

    // Just to be on the safe side, check the buffer size before we start copying.
    if(context.length() <= 24)
    {
      kdDebug() << "KMess: File transfer context field has bad formatting, ignoring invite (context=" << context << ")." << endl;
      sendCancelMessage(CANCEL_SESSION);
      P2PApplication::endApplication( i18n("The file transfer invitation was cancelled.  Bad data was received.") );
      return;
    }

    // Extract the simple fields from the context string.
    unsigned int  length     = P2PMessage::extractBytes(decodedContext,  0);  // field 1: length of fields 1-6
    unsigned int  unknown    = P2PMessage::extractBytes(decodedContext,  4);  // field 2: unknown. (usually 2)
                  fileSize_  = P2PMessage::extractBytes(decodedContext,  8);  // field 3: file size
    unsigned int  noPreview  = P2PMessage::extractBytes(decodedContext, 16);  // field 4: 1 if NO preview available.

    // Again, I don't want KMess to crash or being exploited.
    if(length > decodedContext.size() || length < 24)
    {
      kdDebug() << "KMess: File transfer context field has bad formatting, rejecting invite (length=" << length << ")" << endl;
      sendCancelMessage(CANCEL_SESSION);
      emit appInitMessage( i18n("The file transfer invitation was cancelled.  Bad data was received.") );
      return;
    }


      // FIXME: The length argument doesn't really seam to be correct...

/*
      // field 6: most likely a splitter mark between the file name and preview fields. It's always 0xFFFFFFFF

    unsigned int splitter = (unsigned int) decodedContext[length - 4];
    if(splitter != 0xFFFFFFFF)
    {
      kdDebug() << "KMess: File transfer context field has bad formatting, ignoring invite (splitter not found)." << endl;
      //return;
    }

*/

    // field 5: the file name
    unsigned int filenameLength = (length - 40); // 24 bytes = 4 dwords, 1 qword (fields 1-4 and 6.)

    void    *pointer   = decodedContext.data() + 20;
    suggestedFileName_ = QString::fromUcs2((unsigned short*) pointer);

#ifdef KMESSDEBUG_FILETRANSFER_P2P
      kdDebug() << "FileTransferP2P: File to transfer is " << suggestedFileName_ << ", waiting for user to accept." << endl;
#endif


    // Everything seams OK, allow the user to accept the message:
    QString html = i18n("Do you want to accept the file: %1 (%2 bytes)")
                   .arg( "<font color=red>" + suggestedFileName_ + "</font>" )
                   .arg( fileSize_ );

    offerAcceptOrReject( html );
  }
  else if(contentType == "application/x-msnmsgr-transreqbody")
  {
    // Second stage of the file transfer: initiate the connection
    // (the progress dialog is also initialized now)

#ifdef KMESSTEST
    ASSERT(   file_ != 0          );
    ASSERT( ! fileName_.isEmpty() );
    ASSERT(   transferPanel_ != 0 );
#endif

#ifdef KMESSDEBUG_FILETRANSFER_P2P
    kdDebug() << "FileTransferP2P: Received the file transfer-negotiation." << endl;
#endif

    // File should be opened in the contactStarted2_UserAccepts() step
    if(file_ == 0)
    {
      // File opening failed (file_ == 0)
#ifdef KMESSDEBUG_FILETRANSFER_P2P
      kdDebug() << "FileTransferP2P: Cannot open file: " << fileName_ << endl;
#endif

      // Reject the second invitation.
      sendCancelMessage( CANCEL_SESSION );

      // Tell the user about it
      emit appInitMessage( i18n("The transfer of %1 failed.  Couldn't open file.")
          .arg("<font color=red>" + fileName_ + "</font>") );
      if(transferPanel_ != 0)
      {
        transferPanel_->setStatusMessage( i18n("File transfer dialog message", "Couldn't open file.") );
      }
      return;
    }


    // This kind of message is always accepted, and we skip the
    // contactStarted2_UserAccepts() step because it would be a little
    // difficult otherwise.

    // Read the data from the message
    QString bridges        = message.getValue("Bridges");       // Transport layers the client supports
    int     netID          = message.getValue("NetID").toInt(); // Some ID
    QString connectionType = message.getValue("Conn-Type");     // Suggested connection type
    QString hasUPnPNat     = message.getValue("UPnPNat");       // uses UPnP-enabled NAT router
    QString hasICF         = message.getValue("ICF");           // uses Internet Connection Firewall


#ifdef KMESSDEBUG_FILETRANSFER_P2P
    kdDebug() << "FileTransferP2P: Begin of transfer " << file_->name() << endl;
#endif

    // Examine the network configuration of the other client
    if(connectionType == "Direct-Connect")
    {
      // Same IP, same port
    }
    else if(connectionType == "IP-Restrict-NAT")
    {
      // Different IP, same port
    }
    else if(connectionType == "Port-Restrict-NAT")
    {
      // Same IP, different port
    }
    else if(connectionType == "Symmetric-NAT" || connectionType == "Unknown-Connect")
    {
      // Different IP, different port
    }


    // Based on the contact's network configuration,
    // we choose how we want to send the file:

    // This configuration sends each file over the switchboard:
    MimeMessage acceptMessage;
    acceptMessage.addField( "Bridge",    "TCPv1" );
    acceptMessage.addField( "Listening", "false" );
    acceptMessage.addField( "Nonce",     "{00000000-0000-0000-0000-000000000000}");

    // Confirm the session
    sendSlpOkMessage( acceptMessage );

    // Update the progress dialog
    if(transferPanel_ != 0)
    {
      transferPanel_->setStatusMessage( i18n("File transfer dialog message",
                                              "Using the switchboard for file transfer (this could be slow).") );
    }
  }
  else if( contentType == "application/x-msnmsgr-transrespbody" )
  {
    kdWarning() << "FileTransferP2P: ignoring unsupported request to make a direct connection." << endl;
    sendCancelMessage( CANCEL_INVALID_SLP_CONTENT_TYPE );
    return;
  }
  else
  {
    kdWarning() << "FileTransferP2P: Received unexpected Content-Type: " << contentType << "." << endl;

    // Indicate we don't like that content-type:
    sendCancelMessage( CANCEL_INVALID_SLP_CONTENT_TYPE );

    // Tell the user about it
    if(transferPanel_ != 0)
    {
      transferPanel_->failTransfer( i18n("File transfer dialog message", "Failed") );
      transferPanel_ = 0;
    }
    emit appInitMessage( i18n("The transfer failed.") + "  " +
                         i18n("The contact sent bad data, or KMess doesn't support it.") );

    // Don't quit yet, the SLP error message will be ACKed by the contact.
  }
}



/**
 * Step two of a contact-started chat: the user accepts.
 */
void FileTransferP2P::contactStarted2_UserAccepts()
{
#ifdef KMESSDEBUG_FILETRANSFER_P2P
  kdDebug() << "FileTransferP2P - contactStarted2_UserAccepts" << endl;
#endif

#ifdef KMESSTEST
  ASSERT( file_ == 0          );
  ASSERT( fileName_.isEmpty() );
  ASSERT( transferPanel_ == 0 );
#endif

  bool success;

  // Ask the user for a file
  QString initialFilePath;
  QString recentFolderTag = ":filedownload";

  // Set an initial path to the file
  KURL startDir = KFileDialog::getStartURL(QString::null, recentFolderTag);
  startDir.addPath(suggestedFileName_);
  initialFilePath = startDir.url();

  // Open a file dialog so that the user can save the file to a particular directory and name.
  fileName_ = KFileDialog::getSaveFileName(initialFilePath);

  if ( fileName_ == "" )
  {
#ifdef KMESSDEBUG_FILETRANSFER_P2P
    kdDebug() << "FileTransferP2P::contactStarted2_UserAccepts: User cancelled in file save dialog" << endl;
#endif

    sendCancelMessage( CANCEL_INVITATION ); // Pressed cancel, I consider this a reject
    // Don't quit yet, the SLP error message will be ACKed by the contact.
    return;
  }



  // Now we try to open the file
  file_   = new QFile(fileName_);
  success = (file_ != 0) && file_->open(IO_WriteOnly);

  if( ! success )
  {
#ifdef KMESSDEBUG_FILETRANSFER_P2P
    kdDebug() << "FileTransferP2P::contactStarted2_UserAccepts: Cancelling session" << endl;
#endif

    // Notify the user, even if debug mode is not enabled.
    kdDebug() << "WARNING - Unable to open file: " << fileName_ << "!" << endl;

    // Close the file (also causes gotData() to fail)
    delete file_;
    file_ = 0;

    // Tell the user about it
    emit appInitMessage( i18n("The transfer of %1 failed.  Couldn't open file")
                         .arg("<font color=red>" + fileName_ + "</font>") );

    // Send 500 Internal Error back if we failed (this is still the accept stage)
    sendCancelMessage( CANCEL_SESSION );

    // Don't quit yet, the SLP error message will be ACKed by the contact.
    return;
  }


#ifdef KMESSDEBUG_FILETRANSFER_P2P
  kdDebug() << "FileTransferP2P::contactStarted2_UserAccepts: Sending accept message" << endl;
#endif

  // Initialize the progress dialog
  initializeProgressDialog(true, fileSize_);
  if(transferPanel_ != 0)
  {
    // TODO: replace this mesage with something like "Negotiating session parameters"
    transferPanel_->setStatusMessage( i18n("File transfer dialog message", "Negotiating file transfer mode") );
  }

  // Create the message
  MimeMessage message;
  message.addField( "SessionID", QString::number( getInvitationSessionID() ) );

  // Send the ACCEPT message
  sendSlpOkMessage(message);
}



/**
 * Step three of a contact-started chat: the contact confirms the accept
 *
 * @param  message  The message of the other contact, not usefull in P2P sessions because it's an ACK.
 */
void FileTransferP2P::contactStarted3_ContactConfirmsAccept(const MimeMessage &/*message*/)
{
#ifdef KMESSDEBUG_FILETRANSFER_P2P
  kdDebug() << "FileTransferP2P - contactStarted3_ContactConfirmsAccept" << endl;
#endif

  // When the 200 OK message is accepted, the contact sends
  // another invitation with a different content-type, and the
  // contactStarted1_ContactInvitesUser() is called again.

  // The second time, the contact initiates the file transfer based
  // on the fields we sent in the second 200 OK message.
  // Once that message is received, the gotData() handles the rest.
}



/**
 * Step four in a contact-started chat: the contact confirms the data preparation message.
 */
void FileTransferP2P::contactStarted4_ContactConfirmsPreparation()
{
#ifdef KMESSDEBUG_FILETRANSFER_P2P
  kdDebug() << "FileTransferP2P - contactStarted4_ContactConfirmsPreparation" << endl;
#endif

}



/**
 * End the application with another message in the file transfer dialog as well.
 */
void FileTransferP2P::endApplication(const QString &reason, const QString &transferDialogMessage)
{
  QString html;

#ifdef KMESSDEBUG_TRANSFERFILE
  kdDebug() << "FileTransferP2P::endApplication: " << reason << endl;
#endif
#ifdef KMESSTEST
    ASSERT( transferPanel_ != 0 );
#endif

  // Display a message to the user, and terminate this application
  QString message = reason.arg("<font color=red>" + fileName_ + "</font>");

  if( transferPanel_ != 0)
  {
    transferPanel_->failTransfer(transferDialogMessage);
    transferPanel_ = 0;
  }

  P2PApplication::endApplication(message);
}



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



/**
 * Called when data is received.
 * Once all data is received, the SLP BYE message will be sent.
 *
 * @param  message  P2P message with the data.
 */
void FileTransferP2P::gotData(const P2PMessage &message)
{
  if(file_ == 0)
  {
#ifdef KMESSDEBUG_FILETRANSFER_P2P
    kdDebug() << "FileTransferP2P: Unable to handle file data: no file open!" << endl;
#endif

    // Cancel if we can't receive it.
    // If this happens we're dealing with a very stubborn client,
    // because we already rejected the data-preparation message.
    sendCancelMessage(CANCEL_FAILED);
    return;
  }

  
#ifdef KMESSTEST
  ASSERT( transferPanel_ != 0 );
#endif

  unsigned int  dataMessageSize = message.getDataSize();
  unsigned int  dataOffset      = message.getDataOffset();

  // Update the progress bar
  if(transferPanel_ != 0 )
  {
    transferPanel_->updateProgress( dataOffset + dataMessageSize );
  }


  // Write the data in the file
  Q_LONG status = file_->writeBlock( message.getData(), message.getDataSize() );

  // Check whether the write failed
  if(status == -1)
  {
    kdDebug() << "FileTransferP2P: Failed to write the datablock in the file" << endl;

    // Close the file
    file_->flush();
    file_->close();
    delete file_;
    file_ = 0;

    if(transferPanel_ != 0 )
    {
      transferPanel_->failTransfer( i18n("File transfer dialog message", "File could not be written") );
      transferPanel_ = 0;
    }

    sendCancelMessage(CANCEL_FAILED);
    // endApplication();  // I expect the other client sends more data.. :-(
    return;
  }


  // is the file complete:
  if( message.isLastFragment() )
  {
#ifdef KMESSDEBUG_FILETRANSFER_P2P
    kdDebug() << "FileTransferP2P: Last data part received, closing file" << endl;
#endif

    // Clean up
    file_->flush();
    file_->close();
    delete file_;
    file_ = 0;

    if(transferPanel_ != 0 )
    {
      transferPanel_->finishTransfer();
      transferPanel_ = 0;
    }

    // Send an event to the switchboard:
    emit fileTransferred(fileName_);

    // The base class will wait automatically for the BYE and close the app.
  }
}



/**
 * Create and initilize the progress dialog.
 *
 * @param incoming  Set to indicate whether this is an incoming file transfer or not.
 * @param filename  Name of the file to transfer.
 * @param filesize  Size of the file to transfer.
 */
void FileTransferP2P::initializeProgressDialog(bool incoming, uint filesize)
{
  // 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 FileTransferP2P::slotCancelTransfer()
{
  // This method is activated from the transferPanel,
  // it will delete itself.
  // make sure we never call updateProcess() on it again.
  transferPanel_ = 0;

  // Send the reject message
  sendCancelMessage(CANCEL_SESSION);

  // Now we wait for the contact to send the session
}



/**
 * Step one of a user-started chat: the user invites the contact
 */
void FileTransferP2P::userStarted1_UserInvitesContact()
{
#ifdef KMESSDEBUG_FILETRANSFER_P2P
  kdDebug() << "FileTransferP2P - userStarted1_UserInvitesContact - starting file transfer" << endl;
#endif


  // TODO: This invitation doesn't work yet:
  //       - The context numbers have the wrong endian.
  //       - The invitation message needs to be split at ~1200 bytes (in P2PApplication off course).

  /*
   * Many things to Siebe Tolsma for providing this documentation:
   * http://siebe.bot2k3.net/docs/
   */

  file_ = new QFile(fileName_);

  // Filename parameters
  fileSize_               = file_->size();
  QString shortName       = fileName_.right( fileName_.length() - fileName_.findRev( QRegExp("/") ) - 1 );
  uint    shortNameLength = shortName.length();

  // Preview
  uint previewLength = 0;
  int  flags         = (previewLength == 0 ? 1 : 0);

  // Size
  uint length         = (shortNameLength * 2) + 24; // Utf16 string + 24 bytes for header. TODO: MSN6 sends a different size??
  uint totalLength    = 574 + previewLength;        // MSN6 always uses 550 for filename length, length of fields 1-6 = 24

  // Fill the context parameter
  QByteArray context(totalLength);
  context.fill(0x00);

  P2PMessage::insertBytes(context, length,    0);  // Field 1: Length of fields 1-6
  P2PMessage::insertBytes(context, 2,         4);  // Field 2: somehow this is always 2
  P2PMessage::insertBytes(context, fileSize_, 8);  // Field 3: file size (QWord)
  P2PMessage::insertBytes(context, flags,    16);  // Field 4: 1 if NO preview data

  // Field 5: the file name
  const unsigned short * utf16Name = shortName.ucs2();
  int offset = 20;
  for(uint i = 0; i < shortNameLength; ++i)
  {
    P2PMessage::insertShortBytes(context, utf16Name[i], offset);
    offset += 2;
  }

  P2PMessage::insertBytes(context, 0xFFFFFFFF, 570); // Field 6: some splitter field.


  // Encode the context
  QByteArray encodedContext;
  KCodecs::base64Encode(context, encodedContext);
  QString base64Context = QString::fromUtf8(encodedContext);

  // Create a session ID
  uint sessionID = P2PApplication::generateID();

  // Create the message
  MimeMessage message;
  message.addField( "EUF-GUID",  getAppId()                 );
  message.addField( "SessionID", QString::number(sessionID) );
  message.addField( "AppID",     "2"                        );
  message.addField( "Context",   context                    );

  // Send the message
  sendSlpInvitation(sessionID, "application/x-msnmsgr-sessionreqbody", message);
}



/**
 * Step two of a user-started chat: the contact accepts
 *
 * @param  message  Accept message of the other contact
 */
void FileTransferP2P::userStarted2_ContactAccepts(const MimeMessage &message)
{
#ifdef KMESSDEBUG_FILETRANSFER_P2P
  kdDebug() << "FileTransferP2P - userStarted2_ContactAccepts" << endl;
#endif

#ifdef KMESSTEST
  MimeMessage slpContent(message.getBody());
  ASSERT( message.getValue("Content-Type") == "application/x-msnmsgr-sessionreqbody" );
  ASSERT( slpContent.getValue("SessionID").toULong() == getSessionID() );
#endif

  // We don't need to do anything else here, the P2PApplication base class
  // already ACK-ed with the session ID we gave in the sendSlpInvitation()
}



/**
 * Step three of a user-started chat: the user prepares for the session.
 */
void FileTransferP2P::userStarted3_UserPrepares()
{
#ifdef KMESSDEBUG_FILETRANSFER_P2P
  kdDebug() << "FileTransferP2P - userStarted3_UserPrepares" << endl;
#endif

#ifdef KMESSTEST
  ASSERT(   file_ == 0          );
  ASSERT( ! fileName_.isEmpty() );
#endif

  file_ = new QFile(fileName_);
  bool success = file_->open(IO_WriteOnly);

  if( ! success )
  {
    // Notify the user, even if debug mode is not enabled.
    kdDebug() << "WARNING - Unable to open file: " << fileName_ << "!" << endl;

    delete file_;
    file_ = 0;
  }
  else
  {
    // Transfer the file..?
  }
}


#include "filetransferp2p.moc"
