/***************************************************************************
                          msnobjecttransferp2p.cpp -  description
                             -------------------
    begin                : Fri Nov 26 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 "msnobjecttransferp2p.h"

#include "../../currentaccount.h"
#include "../../emoticonmanager.h"
#include "../../kmessdebug.h"
#include "../../utils/kmessconfig.h"
#include "../../utils/kmessshared.h"
#include "../mimemessage.h"
#include "../p2pmessage.h"

#include <QCryptographicHash>
#include <QFile>
#include <QFileInfo>
#include <QImageReader>
#include <QRegExp>



/**
 * Constructor
 *
 * @param  applicationList  The shared sources for the contact.
 */
MsnObjectTransferP2P::MsnObjectTransferP2P(ApplicationList *applicationList)
    : P2PApplication(applicationList)
    , file_(0)
    , fileName_()
{
}



/**
 * Constructor
 *
 * @param  applicationList  The shared sources for the contact.
 * @param  msnObject        MSNObject identifying the picture to request.
 */
MsnObjectTransferP2P::MsnObjectTransferP2P(ApplicationList *applicationList, const MsnObject &msnObject)
    : P2PApplication(applicationList)
    , file_(0)
    , msnObject_(msnObject)
{
}



/**
 * Destructor, closes the file if it's open.
 */
MsnObjectTransferP2P::~MsnObjectTransferP2P()
{
  if(file_ != 0)
  {
    delete file_;
  }
}



/**
 * Step one of a contact-started chat: the contact invites the user
 *
 * On error, send an 500 Internal Error back. The error will be acked and the application will terminate.
 *
 * @param  message  The invitation message
 */
void MsnObjectTransferP2P::contactStarted1_ContactInvitesUser(const MimeMessage &message)
{
#ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P
  kDebug();
#endif

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

  // Extract the MSNObject from the context field, to determine which picture/emoticon the contact wants

  // Just to be on the safe side, check the buffer size before we start decoding
  if( context.length() <= 24 )
  {
    kWarning() << "MSNObject transfer context field has bad formatting, "
                  "ignoring invite (context=" << context << ", contact=" << getContactHandle() << ").";
    sendCancelMessage( CANCEL_FAILED );
    return;
  }

  // Decode the MSN Object contained in the Context field to a string
  // Decode that string to an MSN Object
  QByteArray decodedContext = QByteArray::fromBase64( context.toLatin1() );
  QString contextString( decodedContext );
  msnObject_ = MsnObject( contextString );

#ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P
  kDebug() << "Got context \"" << contextString << "\".";
// Got context <msnobj Creator="contact@hotmail.com" Size="9442" Type="3" Location="KMess.tmp" Friendly="AA==" SHA1D="FyY3n97RHXDgQujca4FVMv4VIF0=" SHA1C="qsT1Tqmtt7Z0LucXOq1pd9p7cIE="/>
#endif


  switch( msnObject_.getType() )
  {
    case MsnObject::DISPLAYPIC:

      // Test protocol compatibility.
      if( appID != 1 && appID != 12 )    // Display pictures -  AppID 12 is used as of MSN Messenger 7.5
      {
        kDebug() << "Received a request for a display picture, but unexpected appID was set "
                    "(appid=" << appID <<
                    " type=" << msnObject_.getType() <<
                    " contact=" << getContactHandle() << " action=continue).";
      }

      // Continue at separate function
      contactStarted1_gotDisplayPictureRequest();
      return;

    case MsnObject::EMOTICON:

      // Test protocol compatibility.
      if( appID != 1      // HACK: added for compatibility with Messenger for the Mac 6.0.3 and aMsn 0.97rc1
      &&  appID != 11 )   // Custom emoticons
      {
        kDebug() << "Received a request for an emoticon, but unexpected appID was set "
                    "(appid=" << appID <<
                    " type=" << msnObject_.getType() <<
                    " contact=" << getContactHandle() << " action=continue).";
      }

      // Continue at separate function
      contactStarted1_gotEmoticonRequest();
      return;


    // Avoid gcc warnings about missing values.
    // But don't use "default" so the check remains intact.
    case MsnObject::BACKGROUND:
    case MsnObject::DELUXE_DISPLAYPIC:
      break;
    case MsnObject::WINK:
      contactStarted1_gotWinkRequest();
      return;
    default:
      break;
  }

  // Unknown application type
  kWarning() << "Received an invitation for an unexpected object type "
                "(appid=" << appID <<
                " type=" << msnObject_.getType() <<
                " contact=" << getContactHandle() << ").";

  // Abort the contact.
  sendCancelMessage( CANCEL_FAILED );
}



/**
 * Step one continued, the request is for the display picture.
 */
void MsnObjectTransferP2P::contactStarted1_gotDisplayPictureRequest()
{
  // Send our display picture: check if the MSNObject the contact wants is the picture we've got.
  if( msnObject_.hasChanged( CurrentAccount::instance()->getMsnObjectString() ) )
  {
    kWarning() << "Contact " << getContactHandle() << " wants a display picture we don't have!\n"
                  "Requested object: " << msnObject_.objectString() << "\n"
                  "Current object:   " << CurrentAccount::instance()->getMsnObjectString() << "\n"
                  "Aborting invite.";
    sendCancelMessage( CANCEL_FAILED );
    return;
  }

  // The file to send is our picture
  fileName_ = CurrentAccount::instance()->getPicturePath();

  // Reject because there is no file to send
  if( fileName_.isEmpty() )
  {
    kWarning() << "Got an invitation, but we don't have a picture to send.";
    sendCancelMessage( CANCEL_FAILED );
    return;
  }

  // Everything seems OK, accept this message
  contactStarted2_UserAccepts();
}



/**
 * Step one continued, the request is for an emoticon.
 */
void MsnObjectTransferP2P::contactStarted1_gotEmoticonRequest()
{
  QString themePath( EmoticonManager::instance()->getThemePath( true ) );

  // Find if there is a picture named as the one the contact wants
  QFile pictureFile( themePath + msnObject_.getLocation() );
  if( ! pictureFile.open( QIODevice::ReadOnly ) )
  {
#ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P
    kDebug() << "Couldn't open file: " << pictureFile.fileName();
#endif
    sendCancelMessage( CANCEL_FAILED );
    return;
  }

  // Read the picture's data and create a MSNOject of it to test if it really is the requested picture
  QByteArray data = pictureFile.readAll();
  pictureFile.close();

  MsnObject testObject( CurrentAccount::instance()->getHandle(), msnObject_.getLocation(), QString::null, MsnObject::EMOTICON, data );

#ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P
  kDebug() << "msnObject_: " << msnObject_.objectString();
  kDebug() << "testObject: " << testObject.objectString();
#endif

  // Test if they're the same picture
  if( msnObject_.hasChanged( testObject.objectString() ) )
  {
    kWarning() << "Contact " << getContactHandle()
               << " wants a custom emoticon we don't have!"
               << "Requested object: " << msnObject_.objectString()
               << "Aborting invite.";
    sendCancelMessage( CANCEL_FAILED );
    return;
  }

#ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P
  kDebug() << "Picture found, accepting.";
#endif

  // Everything seems OK, accept this message
  fileName_ = pictureFile.fileName();       // Remember the name of the file to send
  contactStarted2_UserAccepts();
}



// Step one continued, the request is for an wink.
void MsnObjectTransferP2P::contactStarted1_gotWinkRequest()
{
  fileName_ = KMessConfig::instance()->getMsnObjectFileName( msnObject_ );

#ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P
  kDebug() << "Requested wink, sending: " + fileName_;
#endif

  if( QFile::exists( fileName_ ) )
  {
    contactStarted2_UserAccepts();
    return;
  }

  // Abort the sending because the file doesn't exist
  sendCancelMessage( CANCEL_FAILED );
}



/**
 * Step two of a contact-started chat: the user accepts.
 */
void MsnObjectTransferP2P::contactStarted2_UserAccepts()
{
#ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P
  kDebug();
#endif

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

  // Now we try to open the file
  file_ = new QFile(fileName_);
  bool success = file_->open(QIODevice::ReadOnly);

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

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

#ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P
    kDebug() << "Cancelling session";
#endif

    // Send 500 Internal Error back if we failed
    // the error will be ACK-ed.
    sendCancelMessage( CANCEL_FAILED );
    return;
  }


#ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P
  kDebug() << "Sending accept message";
#endif

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

  // Send the 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 MsnObjectTransferP2P::contactStarted3_ContactConfirmsAccept(const MimeMessage &/*message*/)
{
#ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P
  kDebug();
#endif

  // Send the data preparation message back.
  // Once this message is ACKed, we can send our code
  sendDataPreparation();
}



/**
 * Step four in a contact-started chat: the contact confirms the data preparation message.
 */
void MsnObjectTransferP2P::contactStarted4_ContactConfirmsPreparation()
{
#ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P
  kDebug();
#endif

  // Send the file, the base class handles everything else here
  sendData(file_, P2P_TYPE_PICTURE);
}



/**
 * Return the application's GUID.
 */
QString MsnObjectTransferP2P::getAppId()
{
  return "{A4268EEC-FEC5-49E5-95C3-F126696BDBF6}";
}



/**
 * Return the msn object of the picture we're transferring
 */
const MsnObject & MsnObjectTransferP2P::getMsnObject() const
{
  return msnObject_;
}



/**
 * Called when data is received.
 * Writes the received data to the output file.
 *
 * @param  message  P2P message with the data.
 */
void MsnObjectTransferP2P::gotData(const P2PMessage &message)
{
  if(file_ == 0)
  {
    kWarning() << "Unable to handle file data: no file open or already closed "
                << "(offset="    << message.getDataOffset()
                << " totalsize=" << message.getTotalSize()
                << " contact="   << getContactHandle() << ")!";

    // 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;
  }

  // Write the data in the file
  // Let the parent class do the heavy lifting, and abort properly.
  bool success = writeP2PDataToFile( message, file_ );
  if( ! success )
  {
    // Close the file
    file_->flush();
    file_->close();
    return;
  }

  // When all data is received, the parent class calls showTransferComplete().
  // That method will test the file and send the msnObjectReceived() signal.
}



/**
 * Indicates a private chat is not required, overwritten from the base class.
 * Returns true by default, unless an emoticon is transferred, or the contact is not in the list.
 *
 * @returns  Returns true if a private chat is required for this application.
 */
bool MsnObjectTransferP2P::isPrivateChatRequired() const
{
  // MsnObject transfer can run in a multi-chat too,
  // The P2P-Dest field of the p2p messages make sure other participants ignore them.

  // For emoticon transfers, a separate private chat is not required,
  // for larger transfers (winks), it's recommended to use a private chat.
  // When the contact is not in our contact list, it may be possible no new chat can be made, so don't enforce this.
  MsnObject::MsnObjectType type = msnObject_.getType();
  bool contactInList = CurrentAccount::instance()->hasContactInList( getContactHandle() );
  return (type != MsnObject::EMOTICON && contactInList);
}



/**
 * Hide standard informative application message (e.g. user invited, cancelled).
 *
 * Avoid annoying messages in the chat windows about "Contact sent something KMess does not support".
 * This is useful for interactive invitations, like file transfer. Since msnobject transfer happen
 * in the background, it's not helping to have empty chat windows popping up with error messages.
 */
void MsnObjectTransferP2P::showEventMessage(const QString &message, const ChatMessage::ContentsClass contents, bool isIncoming )
{
  Q_UNUSED( isIncoming );

  kWarning() << "suppressed message:" << message << "(contact=" << getContactHandle() << ", contentsClass=" << contents << ")";
}



/**
 * Show a message to notify about a system error.
 *
 * Avoid annoying messages in the chat windows. See showEventMessage().
 */
void MsnObjectTransferP2P::showSystemMessage( const QString &message, const ChatMessage::ContentsClass contents, bool isIncoming )
{
  Q_UNUSED( isIncoming );

  kWarning() << "suppressed message:" << message << "(contact=" << getContactHandle() << ", contentsClass=" << contents << ")";
}



/**
 * Called when the transfer is complete.
 * Closes the file, verifies the MsnObject and updates possible listeners.
 */
void MsnObjectTransferP2P::showTransferComplete()
{
#ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P
  kDebug() << "Last data part received, closing file";
#endif

  if( KMESS_NULL(file_) ) return;

  bool success = false;
  QString pictureFileName( file_->fileName() );

  // Don't send an ACK here, it's already ACK-ed
  // (with a special BYE-request ACK)

  // Clean up
  file_->flush();
  file_->close();

  // Check if the received image is valid. Will cost some CPU though.
  QString fileHash( KMessShared::generateFileHash( pictureFileName ).toBase64() );
  success = ( msnObject_.getDataHash() == fileHash );

  // Clean up
  delete file_;
  file_ = 0;

  QFileInfo fileInfo;
  QString ext;

  // Special checks for some types.
  switch( msnObject_.getType() )
  {
    case MsnObject::DISPLAYPIC:
      // Auto-assign the extension for the file
      ext = QImageReader::imageFormat( pictureFileName );
      if( ext.isEmpty() || ! success )
      {
        // If the type of image isn't support or the data is corrupted
        // remove the file
        QFile::remove ( pictureFileName );
        return;
      }

      // Rename the file with the new extension if there is anyone
      fileInfo.setFile( pictureFileName );
      if( fileInfo.suffix().isEmpty() )
      {
        QFile::rename( pictureFileName, pictureFileName + "." + ext );
      }
      break;

    case MsnObject::BACKGROUND:
    case MsnObject::EMOTICON:
      // For pictures, see if it's broken.
      if( QImageReader::imageFormat( pictureFileName ).isEmpty() || ! success )
      {
        kWarning() << "Received image was broken (contact=" << getContactHandle() << ").";
        QFile::remove( pictureFileName );
        return;
      }

      break;
    case MsnObject::WINK:
    {
      QFile file;

      // Write the wink friendly to a file.
      // The file will be used to retrieve the actual wink name, ie "Frog"
      file.setFileName( pictureFileName + ".name" );
      if( file.open( QIODevice::WriteOnly | QIODevice::Text ) )
      {
        QTextStream out( &file );
        out << msnObject_.getFriendly();
        out.flush();
        file.close();
      }

      // Read the stamp value, there is the certificate of the wink
      // This is a X509 certificate, signed by the Microsoft
      // If this value is missing, we can show the wink anyway and be able to sent it to no WLM clients
      const QString& stamp( msnObject_.getAttribute( QString( "stamp" ), msnObject_.objectString() ) );
      if( stamp.isEmpty() )
      {
        break;
      }

      // Write the stamp value to the file. Note that if we don't write this value
      // we can't send the wink to WLM, because it checks for the certificate
      file.setFileName( pictureFileName + ".stamp" );
      if ( ! file.open(QIODevice::WriteOnly | QIODevice::Text ) )
      {
        break;
      }

      QTextStream out(&file);
      out << stamp;
      out.flush();
      file.close();
      break;
    }


    // Avoid gcc warnings.
    default: break;
  }


  // Send an event to the switchboard:
  emit msnObjectReceived( getContactHandle(), msnObject_ );

  // The application should close automatically now,
  // and it sends the BYE message automatically too.
}



/**
 * Hide transfer messages, by overwriting the default method implementation.
 */
void MsnObjectTransferP2P::showTransferMessage( const QString &message )
{
#ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P
  kDebug() << "suppressed message: " << message;
#else
  Q_UNUSED( message ); // Avoid compiler warnings
#endif

  // WLM8 appears to initiate direct connections for msnobject transfers as well.
  // this method hides the connecting-messages by simply overwriting the base method and no nothing instead.
}



/**
 * Step one of a user-started chat: the user invites the contact
 */
void MsnObjectTransferP2P::userStarted1_UserInvitesContact()
{
#ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P
  kDebug() << "requesting display picture";
#endif

  // Set the filename
  fileName_ = KMessConfig::instance()->getMsnObjectFileName(msnObject_);

  // Encode the context
  // The \0 char is added on purpose, this fixes the msnobject transfers with Bot2k3.
  // Sometimes the context string is padded with '=' characters, note this is a feature of BASE64 encoding!
  QString context;
  QByteArray rawObject = msnObject_.objectString().toUtf8();
  rawObject.append( '\0' );  // appears to be sent by the official client, likely unintentional.
  context = rawObject.toBase64();

  int appId;
  MsnObject::MsnObjectType objectType = msnObject_.getType();
  switch( objectType )
  {
    case MsnObject::DISPLAYPIC:
      appId = 12;
      break;

    case MsnObject::WINK:
      appId = 17;
      break;

    case MsnObject::EMOTICON:
      appId = 11;
      break;

    case MsnObject::BACKGROUND:
      appId = 2;

    default:
      appId = 1;  // the old appId for msn object transfers
  }

  // Send the invitation
  sendSlpSessionInvitation( KMessShared::generateID(), getAppId(), appId, context);
}



/**
 * Step two of a user-started chat: the contact accepts
 *
 * @param  message  Accept message of the other contact
 */
void MsnObjectTransferP2P::userStarted2_ContactAccepts(const MimeMessage & /*message*/)
{
#ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P
  kDebug();
#endif

  // We don't need to do anything else here.
  // The contact still needs to send the data preparation.
  // Meanwhile, the base class acks the "SLP/200 OK" message automatically
  // with the session ID we gave in the sendSlpSessionInvitation()
}



/**
 * Step three of a user-started chat: the user prepares for the session.
 */
void MsnObjectTransferP2P::userStarted3_UserPrepares()
{
#ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P
  kDebug();
#endif

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

  if( file_ != 0 )
  {
    // TODO: Quick fix for WML8, this method is called twice because WLM8
    // initializes a Direct connection while it sent the data preparation
#ifdef KMESSDEBUG_MSNOBJECTTRANSFER_P2P
    kWarning() << "this method was already called before.";
#endif
    return;
  }


  file_ = new QFile(fileName_);
  bool success = file_->open(QIODevice::WriteOnly);

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

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

  // Acknowledge the data-preparation message
  // Final step is the gotData() handling..
  sendDataPreparationAck();
}


#include "msnobjecttransferp2p.moc"
