/***************************************************************************
                          msnswitchboardconnection.cpp  -  description
                             -------------------
    begin                : Fri Jan 24 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 "msnswitchboardconnection.h"

#include <qptrlist.h>
#include <kdebug.h>
#include <kextendedsocket.h>
#include <kfiledialog.h>
#include <klocale.h>
#include <kmessagebox.h>
#include <qcstring.h>
#include <qregexp.h>
#include <qfile.h>

#include "../contact/contact.h"
#include "../contact/contactlist.h"
#include "../currentaccount.h"
#include "../kmessapplication.h"
#include "../kmessdebug.h"
#include "../msnobject.h"
#include "msnnotificationconnection.h"
#include "applications/mimeapplication.h"
#include "applications/p2papplication.h"
#include "applications/gnomemeeting.h"
#include "applications/msnremotedesktop.h"
#include "applications/filetransfer.h"
#include "applications/voiceconversation.h"
#include "applications/picturetransferp2p.h"
#include "applications/filetransferp2p.h"
#include "applications/webapplicationp2p.h"
#include "chatinformation.h"
#include "mimemessage.h"
#include "p2pmessage.h"

#ifdef KMESSDEBUG_SWITCHBOARD
  #define KMESSDEBUG_SWITCHBOARD_GENERAL
  #define KMESSDEBUG_SWITCHBOARD_MIMEAPP
  #define KMESSDEBUG_SWITCHBOARD_P2PAPP
#endif



// The constructor
MsnSwitchboardConnection::MsnSwitchboardConnection()
 : MsnConnection("MsnSwitchboardConnection"),
   abortingApplications_(false),
   autoDeleteLater_(false),
   chatInfo_(0),
   closingConnection_(false),
   contactsConnected_(false),
   initialized_(false)
{
  pendingMessages_.setAutoDelete(true);
}



// The destructor
MsnSwitchboardConnection::~MsnSwitchboardConnection()
{
  // Close the connection
  closeConnection();

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "DESTROYED Switchboard" << endl;
#endif
}



// Abort the applications in a proper way
void MsnSwitchboardConnection::abortApplications()
{
  // TODO: start using this in some way
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "Switchboard::abortApplications - aborting all running Applications" << endl;
#endif

  if(abortingApplications_)
  {
    kdWarning() << "Switchboard is already aborting applications." << endl;
    return;
  }


  abortingApplications_ = true;

  // Tell each application to stop
  QPtrListIterator<MimeApplication>  appIterator(mimeApplications_);
  while( appIterator.current() != 0 )
  {
    appIterator.current()->userAborted();  // emits deleteMe()
    ++appIterator;
  }

  QPtrListIterator<P2PApplication>  p2pIterator(p2pApplications_);
  while( p2pIterator.current() != 0 )
  {
    p2pIterator.current()->userAborted();  // emits deleteMe()
    ++p2pIterator;
  }
}



// Close the connection
void MsnSwitchboardConnection::closeConnection()
{
  // Delete all applications, they're useles once the connection is closed.
  // If they're not aborted, its not possible to do so here.
  // Closing applications requires some signal/slot interaction
  // before this method can send the BYE message
  if(mimeApplications_.count() > 0 ||
     p2pApplications_.count()  > 0)
  {
    kdWarning() << "Switchboard::closeConnection: unable to terminate remaining applications nicely." << endl;
    mimeApplications_.setAutoDelete(true);
    mimeApplications_.clear();
    mimeApplications_.setAutoDelete(false);  // and reset again!
    p2pApplications_.setAutoDelete(true);
    p2pApplications_.clear();
    p2pApplications_.setAutoDelete(false);  // and reset again!
  }

  // Detach contacts, they can't use this switchboard anymore
  detachContacts();

  if( isConnected() )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "Switchboard::closeConnection: still connected, sending BYE." << endl;
#endif
    // Send a "bye"
    sendCommand( "BYE", "\r\n");
    disconnectFromServer();
  }

  // The contactsInChat_ list is not cleared, for the isExclusiveChatWithContact() check
}



// Clean up, close the connection, destroy this object
void MsnSwitchboardConnection::closeConnectionLater(bool autoDelete)
{
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "Switchboard::closeConnectionLater: aborting applications nicely and closing connection." << endl;
#endif

  if(mimeApplications_.count() > 0 ||
     p2pApplications_.count()  > 0)
  {
    // Set variables to use in terminateApplication()
    closingConnection_ = true;
    autoDeleteLater_   = autoDelete;

    // Abort running applications first
    abortApplications();
  }
  else
  {
    // Close the connection directly
    closeConnection();

    // Automatically delete ourself
    if(autoDelete)
    {
      this->deleteLater();
    }
  }
}



// Make the neccessary MimeApplication connections.
void MsnSwitchboardConnection::connectApplication(MimeApplication *app)
{
#ifdef KMESSDEBUG_SWITCHBOARD_MIMEAPP
  kdDebug() << "Switchboard: Connecting MimeApplication signals" << endl;
#endif

  // Common signals:
  connect( app,  SIGNAL(                 deleteMe(Application *) ),    // Request for removal
           this,   SLOT( terminateMimeApplication(Application *) ) );
  connect( app,  SIGNAL(           appInitMessage(QString)       ),    // Initialization message
           this,   SLOT(    forwardAppInitMessage(QString)       ) );
  connect( app,  SIGNAL(               appMessage(QString)       ),    // Activity message
           this,   SLOT(        forwardAppMessage(QString)       ) );
  connect( app,  SIGNAL(          fileTransferred(QString)       ),    // File transferred
           this,   SLOT(          fileTransferred(QString)       ) );
  connect( app,  SIGNAL(            systemMessage(QString)       ),    // Error message
           this,   SLOT(     forwardSystemMessage(QString)       ) );
  connect( app,  SIGNAL(                   putMsg(const MimeMessage &)   ),    // Request to send message
           this,   SLOT(    putMimeApplicationMsg(const MimeMessage &)   ) );

  mimeApplications_.append( app );
}



// Make the neccessary P2PApplication connections.
void MsnSwitchboardConnection::connectApplication(P2PApplication *app)
{
#ifdef KMESSDEBUG_SWITCHBOARD_P2PAPP
  kdDebug() << "Switchboard: Connecting P2PApplication signals" << endl;
#endif

  // Common signals:
  connect( app,  SIGNAL(                 deleteMe(Application *) ),   // Request for removal
           this,   SLOT(  terminateP2PApplication(Application *) ) );
  connect( app,  SIGNAL(           appInitMessage(QString)       ),   // Initialization message
           this,   SLOT(    forwardAppInitMessage(QString)       ) );
  connect( app,  SIGNAL(               appMessage(QString)       ),   // Activity message
           this,   SLOT(        forwardAppMessage(QString)       ) );
  connect( app,  SIGNAL(          fileTransferred(QString)       ),    // File transferred
           this,   SLOT(          fileTransferred(QString)       ) );
  connect( app,  SIGNAL(            systemMessage(QString)       ),   // Error message
           this,   SLOT(     forwardSystemMessage(QString)       ) );
  connect( app,  SIGNAL(                   putMsg(const MimeMessage &)   ),   // Request to send p2p message
           this,   SLOT(     putP2PApplicationMsg(const MimeMessage &)   ) );

  p2pApplications_.append(app);
}



// The socket connected, so send the version command.
void MsnSwitchboardConnection::connectionSuccess()
{
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "Switchboard: Connected Asynchronously, so send VER." << endl;
#endif
  if ( chatInfo_->getUserStartedChat() )
  { // This is a user-initiated chat
    // Set the usr information to the server.
    sendCommand( "USR", currentAccount_->getHandle() + " " + chatInfo_->getAuthorization() + "\r\n" );
  }
  else
  { // This is a contact-initiated chat
    // Answer the chat with the authorization
    sendCommand( "ANS", currentAccount_->getHandle() + " " + chatInfo_->getAuthorization() + " " + chatInfo_->getChatId() + "\r\n" );
  }
}



// A contact changed their msn object
void MsnSwitchboardConnection::contactChangedMsnObject(Contact *contact)
{
#ifdef KMESSTEST
  ASSERT( isContactInChat(contact->getHandle()) );
#endif

  // Last sanity check
  if( isContactInChat(contact->getHandle()) )
  {
    startPictureDownload(contact);
  }
}



// Do what's required when a contact joined
void MsnSwitchboardConnection::contactJoined(QString& handle, QString& friendlyName)
{
  contactsConnected_ = true;
  removePercents( friendlyName );

  // Add the contact to the list if the contact isn't there already
  if ( !isContactInChat( handle ) )
  {
    contactsInChat_.append( handle );
  }

  // Send all pending messages
  sendPendingMessages();

  // Download the display picture of the contact if he/she has an msnobject
  // and the capabilities field indicates it supports MSNC1
  Contact   *contact   = chatInfo_->getContactList()->getContactByHandle(handle);

  if(contact != 0)
  {
    // If the contact is capable of transferring pictures,
    if(contact->getCapabilities() >= Contact::MSN_CAP_MSN60)
    {
      // Handle updates
      connect(contact, SIGNAL(        changedMsnObject(Contact*) ) ,
              this,      SLOT( contactChangedMsnObject(Contact*) ) );

      // Download the current picture the contact has one
      const MsnObject *msnObject = contact->getMsnObject();
      if(msnObject != 0)
      {
        startPictureDownload( contact );
      }
    }
  }

  // Emit the signal to the Chat Window.
  emit contactJoinedChat( handle, friendlyName );
}



// Remove a contact from the list of contacts in the chat
void MsnSwitchboardConnection::contactLeft(const QString& handle)
{
  QValueList<QString>::iterator it;

  // Check if the contact is in the chat..
  if ( isContactInChat( handle ) )
  {
    // Leave at least one contact in the chat
    if ( contactsInChat_.count() > 1 )
    {
      // Find the contact in the chat
      for ( it = contactsInChat_.begin(); it != contactsInChat_.end(); ++it )
      {
        if ( (*it) == handle )
        {
          // Remove from the chat
          contactsInChat_.remove( it );
          break;
        }
      }
    }
    else
    {
      // There is only one contact in the chat and it won't removed in order to have a default.
      contactsConnected_ = false;
    }
  }


  // Unregister the contact
  Contact *contact = chatInfo_->getContactList()->getContactByHandle(handle);
  if(contact != 0)
  {
    // Disconnect the update signal
    disconnect(contact, SIGNAL(        changedMsnObject(Contact*) ) ,
               this,      SLOT( contactChangedMsnObject(Contact*) ) );


    // Are we the switchboard that transferred the contact picture?
    if(contact->getMsnObjectChat() == this->chatInfo_)
    {
      // Unregister this chat, allow others to take ownership
      contact->setMsnObjectChat(0);
    }
  }

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "MsnSwitchboardConnection: emitting that '" << handle << "' left chat." << endl;
#endif

  // Emit the signal to the Chat Window.
  emit contactLeftChat( handle );


  // Check if all contacts went away
  if(! contactsConnected_)
  {
 #ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "MsnSwitchboardConnection: last contact left chat, closing connection." << endl;
#endif
    // No contacts left in the chat, close connection since it is of little use.
    // - the connection can still be used to 'CAL' the last contact again
    // - when the contact resumes the connection, it uses a different server,
    //   so startChat() needs to reconnect.
    closeConnection();
  }
}



// Convert an html format (#RRGGBB) color to an msn format (BBGGRR) color
void MsnSwitchboardConnection::convertHtmlColorToMsnColor(QString &color) const
{
#ifdef KMESSTEST
  ASSERT( color.length() == 7 );
#endif
  // Get the color components
  QString red   = color.mid(1, 2);
  QString green = color.mid(3, 2);
  QString blue  = color.mid(5, 2);

  // Reassemble the color
  if ( blue != "00" )
  {
    color = blue + green + red;
  }
  else if ( green != "00" )
  {
    color = green + red;
  }
  else if ( red != "00" )
  {
    color = red;
  }
  else
  {
    color = "0";
  }
#ifdef KMESSTEST
  ASSERT( color.length() < 7 );
#endif
}



// Convert and msn format color (BBGGRR) to an html format (#RRGGBB) color
void MsnSwitchboardConnection::convertMsnColorToHtmlColor(QString &color) const
{
#ifdef KMESSTEST
  ASSERT( color.length() < 7 );
#endif
  if ( color == "0" )
  {
    color = "#000000";
  }
  else
  {
    // Fill the color out to six characters
    while ( color.length() < 6 )
    {
      color = "0" + color;
    }
    // Get the color components
    QString blue  = color.mid(0, 2);
    QString green = color.mid(2, 2);
    QString red   = color.mid(4, 2);
    // Reassemble the components
    color = "#" + red + green + blue;
  }
#ifdef KMESSTEST
  ASSERT( color.length() == 7 );
#endif
}



// Create an application of the given type
Application* MsnSwitchboardConnection::createApplication(const QString &/*sendingContact*/, const MimeMessage &message)
{
  MimeApplication *app = 0;
  QString type;

  // Get the application GUID
  type = message.getValue("Application-GUID");


  // Determine what application to start based on the GUID
  if ( type == GnomeMeeting::getAppId() )
  {
    app = new GnomeMeeting( getLocalIp() );
  }
  else if ( type == MsnRemoteDesktop::getAppId() )
  {
    app = new MsnRemoteDesktop( getLocalIp() );
  }
  else if ( type == FileTransfer::getAppId() )
  {
    app = new FileTransfer( CurrentAccount::instance()->getHandle(), getLocalIp() );
  }
  else if ( type == VoiceConversation::getAppId() )
  {
    // Check if we are already in voice conversation with someone else
    if(getVoiceConversation() != 0)
    {
      // Tell the user it already has a voice conversation
      QString warning = i18n("The contact is inviting you to start an audio conversation, but you can't have more then one audio conversation at the same time.");
      emit systemMessage( warning );

      // Reject the invitation
      rejectMimeApplication( message.getValue("Invitation-Cookie") );
      return 0;
    }

    app = new VoiceConversation( getLocalIp() );
  }
  else
  {
    // Warn the user we received an unknown invitation
    QString warning = i18n("The contact is inviting you for '%1', but this is not implemented yet.")
                      .arg( message.getValue("Application-Name") );
    emit systemMessage( warning );

    // Reject the application, it's not installed
    rejectMimeApplication( message.getValue("Invitation-Cookie"), true );
    return 0;
  }


  // Connect up the application
  if( app != 0 )
  {
    connectApplication(app);
  }

  return app;
}



/**
 * A function to initialize the correct P2PApplication subtype.
 *
 * @param  sendingContact  Contact who sent the message.
 * @param  message         The P2PMessage sent by the contact.
 */
P2PApplication * MsnSwitchboardConnection::createApplication(const QString &sendingContact, const P2PMessage &message)
{
#ifdef KMESSTEST
  ASSERT( message.getSessionID() == 0 );
#endif

#ifdef KMESSDEBUG_SWITCHBOARD_P2PAPP
  kdDebug() << "SwitchBoard: Creating new P2PApplication instance to parse the P2P message." << endl;
#endif

  P2PApplication *app = 0;
  QRegExp guidRE("EUF-GUID: (.+)\r\n");
  QString slpMessage;
  QString eufGuid;
  int     guidPos;

  // Extract the SLP data message
  slpMessage = QString::fromUtf8( message.getData(), message.getDataSize() );

  if(! slpMessage.startsWith("INVITE"))
  {
    // Not an invite message
    // this belonds to another session, but parseP2PAppMessage() couldn't find it.
    // Likely a P2PApplication object terminated prematurely.
    QString preamble = slpMessage.left( QMIN(slpMessage.find(":"), 20) );
    kdWarning() << "SwitchBoard: P2P message can't be handled, "
                << "P2PApplication terminated already (slp-preamble=" << preamble << " contact=" << sendingContact << ")." << endl;
    return 0;
  }


  // Find the EUF-GUID in the data
  guidRE.setMinimal(true);
  guidPos    = guidRE.search(slpMessage);

  // If no EUF-GUID is found:
  if( guidPos == -1 )
  {
    // Notify the user:
    kdWarning() << "SwitchBoard: P2P message can't be handled, "
                << "EUF-GUID not found. (message dump follows)" << endl;
    kdDebug() << slpMessage << endl;
    return 0;
  }

  // Get the GUID
  eufGuid = guidRE.cap(1);

  // Determine what application to start based on the EUF-GUID
  if( eufGuid == PictureTransferP2P::getAppId() )
  {
    app = new PictureTransferP2P( getLocalIp(), sendingContact );
  }
  else if( eufGuid == FileTransferP2P::getAppId() )
  {
    app = new FileTransferP2P( getLocalIp(), sendingContact );
  }
  else if( eufGuid == WebApplicationP2P::getAppId() )
  {
    app = new WebApplicationP2P( getLocalIp(), sendingContact );
  }
  else
  {
    // Display a warning to the user
    QString warning = i18n("The contact initiated a MSN6 feature KMess can't handle yet.");
    emit systemMessage( warning );

    // Nevertheless, create an application instance which will reject the invitation correctly.
    app = new P2PApplication( getLocalIp(), sendingContact );

    // Application::contactStarted1 rejects correct invitatations automatically,
    // so don't set app->setMode(APP_MODE_ERROR_HANDLER) here..!
  }


  // Connect up the application
  if( app != 0 )
  {
    connectApplication(app);
  }

  return app;
}



// Detach all contacts from this switchboard, so others can take ownership
void MsnSwitchboardConnection::detachContacts()
{
  if(contactsInChat_.size() == 0) return;

  // KMessApplication also defines sessionSaving() for KDE 3.1
  KMessApplication *app = static_cast<KMessApplication *>(kapp);

  // Don't call contact->setMsnObjectChat,
  // contact may be deleted already
  if(app->quitSelected() ||
     app->sessionSaving())
  {
    return;
  }

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "Switchboard::detachContacts: releasing all contacts." << endl;
#endif

  // startPictureDownload() assigns the contact with one switchboard.
  // this ensures only one switchboard will download the contact picture.
  // once the picture download is complete, the contact will be freed automatically
  // this method ensures it also happens in other situations, like an unexpected quit.

  // Unregister all contacts
  QValueListConstIterator<QString> it;
  Contact *contact;

  for( it = contactsInChat_.begin(); it != contactsInChat_.end(); ++it )
  {
    // Get the contact object
    contact = chatInfo_->getContactList()->getContactByHandle( *it );

    // Are we the switchboard that transferred the contact picture?
    if(contact != 0 && contact->getMsnObjectChat() == this->chatInfo_)
    {
      // Unregister this chat, allow others to take ownership
      contact->setMsnObjectChat(0);
    }
  }
}



// A file was received by an application
void MsnSwitchboardConnection::fileTransferred(QString filename)
{
  QString shortName = filename.right( filename.length() - filename.findRev( QRegExp("/") ) - 1 );
  QString fileURL   = "<a href=\"file:/" + filename + "\">" + shortName + "</a>";

  forwardAppInitMessage( i18n("Successfully transferred file: %1")
                         .arg("<font color=blue>" + fileURL + "</font>") );
}



// Forward an application message by reemitting it as a signal.
void MsnSwitchboardConnection::forwardAppInitMessage(QString message)
{
  if(autoDeleteLater_)
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "MsnSwitchboardConnection::forwardAppInitMessage: ignoring application message while closing connection"
              << " (message=" << message << ")." << endl;
#endif
    return;
  }
  emit showAppMessage( "<font color=blue>" + message + "</font>" );
}



// Forward an application message by reemitting it as a signal.
void MsnSwitchboardConnection::forwardAppMessage(QString message)
{
  if(autoDeleteLater_)
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "MsnSwitchboardConnection::forwardAppMessage: ignoring application message while closing connection"
              << " (message=" << message << ")." << endl;
#endif
    return;
  }
  emit showAppMessage( "<font color=purple>" + message + "</font>" );
}



// Forward an system message
void MsnSwitchboardConnection::forwardSystemMessage(QString message)
{
  if(autoDeleteLater_)
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "MsnSwitchboardConnection::forwardSystemMessage: ignoring application message while closing connection"
              << " (message=" << message << ")." << endl;
#endif
    return;
  }
  emit systemMessage(message);
}



// Return the application with the given cookie.
Application* MsnSwitchboardConnection::getApplication(QString cookie) const
{
#ifdef KMESSDEBUG_SWITCHBOARD_MIMEAPP
  kdDebug() << "Switchboard::getApplication( cookie = " << cookie << " )" << endl;
#endif
  Application                       *application = 0;
  QPtrListIterator<MimeApplication>  mimeIterator(mimeApplications_);
  QPtrListIterator<P2PApplication>   p2pIterator(p2pApplications_);

  while( mimeIterator.current() != 0 )
  {
    if( mimeIterator.current()->getCookie() == cookie )
    {
      application = mimeIterator.current();
      break;
    }
    ++mimeIterator;
  }

  // Also check P2P if the application wasn't found yet.
  if( application == 0 )
  {
    while ( p2pIterator.current() != 0 )
    {
      if ( p2pIterator.current()->getCookie() == cookie )
      {
        application = p2pIterator.current();
        break;
      }
      ++p2pIterator;
    }
  }

  return application;
}



// Get the switchboard to make a list of the contacts in the chat
QString MsnSwitchboardConnection::getCaption()
{
  QString      caption;
  QString     contactString;
  ContactList *contactList;

  if ( chatInfo_ != 0 )
  {
    contactList = chatInfo_->getContactList();
    if ( contactList != 0 )
    {
      // Make a list of the contacts in the chat
      if ( contactsInChat_.count() == 1 )
      {
        return i18n("Chat - %1")
               .arg( contactList->getContactFriendlyNameByHandle(contactsInChat_[0]) );
      }
      else if ( contactsInChat_.count() == 2 )
      {
        return i18n("Chat - %1 and %2")
               .arg( contactList->getContactFriendlyNameByHandle(contactsInChat_[0]) )
               .arg( contactList->getContactFriendlyNameByHandle(contactsInChat_[1]) );
      }
      else if ( contactsInChat_.count() > 2 )
      {
        return i18n("Chat - %1 et al.")
               .arg( contactList->getContactFriendlyNameByHandle(contactsInChat_[0]) );
      }
    }
  }
  return i18n("Chat");
}



// Get a contact's friendly name from the contact list
QString MsnSwitchboardConnection::getContactFriendlyNameByHandle(const QString &handle)
{
  if ( chatInfo_ == 0 )
  {
    kdWarning() << "Switchboard::getContactFriendlyNameByHandle(" << handle <<") - Chat info is null." << endl;
    return QString::null;
  }

  ContactList *contactList = chatInfo_->getContactList();
  if ( contactList == 0 )
  {
    kdWarning() << "Switchboard::getContactFriendlyNameByHandle(" << handle <<") - Contact list is null." << endl;
    return QString::null;
  }

  // Get the friendly name
  return contactList->getContactFriendlyNameByHandle( handle );
}



// Make a list of the contacts in the chat
QStringList MsnSwitchboardConnection::getContactList() const
{
  QStringList                       contacts;
  QValueListConstIterator<QString> it;

  for ( it = contactsInChat_.begin(); it != contactsInChat_.end(); ++it )
  {
    contacts << (*it);
  }

  return contacts;
}



// Get the font and color from a message
void MsnSwitchboardConnection::getFontAndColorFromMessage(QFont &font, QString &color, const MimeMessage &message) const
{
  // Get the font from the format string
  getFontFromMessageFormat( font, message );
  // Get the font color from the message format
  getFontColorFromMessageFormat( color, message );
}



// Get a font from the message
void MsnSwitchboardConnection::getFontFromMessageFormat(QFont &font, const MimeMessage& message) const
{
  QString family  = message.getSubValue("X-MMS-IM-Format", "FN");
  QString effects = message.getSubValue("X-MMS-IM-Format", "EF");

  removePercents( family );
  font.setFamily( family );
  if ( effects.contains("B") )
    font.setBold(true);
  else
    font.setBold(false);
  if ( effects.contains("I") )
    font.setItalic(true);
  else
    font.setItalic(false);
  if ( effects.contains("U") )
    font.setUnderline(true);
  else
    font.setUnderline(false);
}



// Get an html font color from the message
void MsnSwitchboardConnection::getFontColorFromMessageFormat(QString &color, const MimeMessage& message) const
{
  color = message.getSubValue("X-MMS-IM-Format", "CO");
  convertMsnColorToHtmlColor( color );
}



// Extract some information from a font message (i.e. "EF=UIB;" extract the "UIB" )
QString MsnSwitchboardConnection::getFontValueFromMessageFormat(const QString& field, const QString& message) const
{
#ifdef KMESSTEST
  ASSERT( message.contains( field ) );
#endif
  // Look for "field=" in the message...
  int index = message.find( field );
  if ( index < 0 )
  {
    // The field wasn't found
    return "";
  }
  // Move the index ahead past the field and the = that should follow it
  index += field.length() + 1;
  // Get everything to the right of the index
  QString value = message.right( message.length() - index );
  // Find the first ';' in the value, which should mark then end of the value
  value = value.left( value.find(";") );
  return value;
}



// Find an voice conversation object from the application list and return it.
VoiceConversation *MsnSwitchboardConnection::getVoiceConversation() const
{
  QPtrListIterator<MimeApplication>  it(mimeApplications_);
  VoiceConversation *voiceApplication = 0;

  while ( it.current() != 0 )
  {
    voiceApplication = dynamic_cast<VoiceConversation *>(it.current());
    if (voiceApplication != 0) return voiceApplication;
    ++it;
  }

  return 0;
}



// Give the application with the given cookie the command.
void MsnSwitchboardConnection::giveAppCommand(QString cookie, QString command)
{
#ifdef KMESSDEBUG_SWITCHBOARD_MIMEAPP
  kdDebug() << "Switchboard: Give app cookie = " << cookie << " command = " << command << "." << endl;
#endif
  Application *application;
  // Find the application indicated by the cookie
  application = getApplication( cookie );
  // If the application wasn't found
  if ( application != 0 )
  {
    application->gotCommand( command );
  }
  else
  {
    kdWarning() << "SwitchBoard: Application '" << cookie
                << "' wasn't found to give command '" << command << "' to." << endl;
  }
}



// Received a positive delivery message.
void MsnSwitchboardConnection::gotAck(const QStringList& command)
{
#ifdef KMESSTEST
  ASSERT( acks_.count() == messages_.count() );
  uint oldAckCount = acks_.count();
  uint oldMessageCount = messages_.count();
#endif
  QValueList<QString>::iterator ackIt, messageIt;

  QString ack = command[1];

#ifdef KMESSTEST
  bool goodNumber;
  ack.toUInt( &goodNumber );
  ASSERT( goodNumber );
#endif
  messageIt = messages_.begin();
  for ( ackIt = acks_.begin(); ackIt != acks_.end(); ++ackIt )
  {
    if ( (*ackIt) == ack )
    {
      acks_.remove( ackIt );
      messages_.remove( messageIt );
      break;
    }
    ++messageIt;
  }
#ifdef KMESSTEST
  ASSERT( acks_.count() == (oldAckCount - 1) );
  ASSERT( messages_.count() == (oldMessageCount - 1) );
#endif
}



// Received notification that a contact is no longer in session.
void MsnSwitchboardConnection::gotBye(const QStringList& command)
{
  contactLeft( command[1] );
}



// Received the initial roster information for new contacts joining a session.
void MsnSwitchboardConnection::gotIro(const QStringList& command)
{
  QString handle       = command[4];
  QString friendlyName = command[5];
  removePercents( friendlyName );

  if ( chatInfo_ == 0 )
  {
    kdWarning() << "Switchboard - gotIro - ChatInfo is null!" << endl;
  }
  else
  {
    QString altFriendlyName = chatInfo_->getContactList()->getContactFriendlyNameByHandle( handle );
    if ( altFriendlyName != "" )
    {
      friendlyName = altFriendlyName;
    }
  }
  contactJoined( handle, friendlyName );
}



// Received notification of a new client in the session.
void MsnSwitchboardConnection::gotJoi(const QStringList& command)
{
  QString handle       = command[1];
  QString friendlyName = command[2];
  removePercents( friendlyName );

  if ( chatInfo_ == 0 )
  {
    kdWarning() << "Switchboard - gotJoi - ChatInfo is null!" << endl;
  }
  else
  {
    QString altFriendlyName = chatInfo_->getContactList()->getContactFriendlyNameByHandle( handle );
    if ( altFriendlyName != "" )
    {
      friendlyName = altFriendlyName;
    }
  }
  contactJoined( handle, friendlyName );
}



// Received a negative acknowledgement of the receipt of a message.
void MsnSwitchboardConnection::gotNak(const QStringList& command)
{
#ifdef KMESSTEST
  ASSERT( acks_.count() == messages_.count() );
  uint oldAckCount = acks_.count();
  uint oldMessageCount = messages_.count();
#endif
  QValueList<QString>::iterator ackIt, messageIt;

  QString ack = command[1];

#ifdef KMESSTEST
  bool goodNumber;
  ack.toUInt( &goodNumber );
  ASSERT( goodNumber );
#endif
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "Switchboard - NAK number is " << ack << endl;
#endif

  messageIt = messages_.begin();
  for ( ackIt = acks_.begin(); ackIt != acks_.end(); ++ackIt )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "Switchboard - Have stored ack of " << (*ackIt) << endl;
#endif

    if ( (*ackIt) == ack )
    {
      // Tell the user a certain message was not received
      if(*messageIt != QString::null)
      {
        QString message = i18n("The message \"%1\" was not received").arg( *messageIt );

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
        kdDebug() << "Switchboard - Give the message: " << message << endl;
#endif
        emit systemMessage( message );
      }

      acks_.remove( ackIt );
      messages_.remove( messageIt );
      break;
    }
    ++messageIt;
  }
#ifdef KMESSTEST
  ASSERT( acks_.count() == (oldAckCount - 1) );
  ASSERT( messages_.count() == (oldMessageCount - 1) );
#endif
}



// Received notification of the termination of a client-server session.
void MsnSwitchboardConnection::gotOut(const QStringList& /*command*/)
{
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "Switchboard - got OUT." << endl;
#endif

  closeConnection();
}



// Received a client-server authentication message.
void MsnSwitchboardConnection::gotUsr(const QStringList& command)
{
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "Switchboard - got USR." << endl;
#endif
#ifdef KMESSTEST
  ASSERT( chatInfo_ != 0 );
#endif
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "Switchboard - Contact handle is " << chatInfo_->getContactHandle() << endl;
#endif
  // This should just be a confirmation
  if ( command[2] != "OK" )
  {
    kdWarning() << "MsnSwitchboardConnection: Switchboard authentication failed" << endl;
    return;
  }
  if ( chatInfo_ == 0 )
  {
    kdWarning() << "Switchboard - gotUsr - ChatInfo is null!" << endl;
  }
  else
  {
    // Now call the other user to the conversation.
    sendCommand( "CAL", chatInfo_->getContactHandle() + "\r\n" );
  }
}



// Initialize the object
bool MsnSwitchboardConnection::initialize()
{
  if ( initialized_ )
  {
    kdDebug() << "Switchboard Connection already initialized!" << endl;
    return false;
  }
  if ( !MsnConnection::initialize() )
  {
    kdDebug() << "Switchboard Connection: Couldn't initialize parent." << endl;
    return false;
  }
  initialized_ = true;
  return initialized_;
}



// Invite a contact into the chat
void MsnSwitchboardConnection::inviteContact(QString handle)
{
  if ( handle != "" )
  {
    sendCommand("CAL", handle + "\r\n");
  }
}



// Check if a certain contact is in the chat
bool MsnSwitchboardConnection::isContactInChat(const QString& handle) const
{
  QValueListConstIterator<QString> it;

  for ( it = contactsInChat_.begin(); it != contactsInChat_.end(); ++it )
  {
    if ( (*it) == handle )
    {
      return true;
    }
  }
  return false;
}



// Check if all contacts left
bool MsnSwitchboardConnection::isEmpty() const
{
  return (! contactsConnected_);
}


// Check if only the given contact is in the chat
bool MsnSwitchboardConnection::isExclusiveChatWithContact(const QString& handle) const
{
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "Switchboard - Checking if chat is exclusive with " << handle << "." << endl;
  kdDebug() << "Switchboard - There are " << contactsInChat_.count() << " contacts in the chat." << endl;
#endif

  return (contactsInChat_.count() == 1 && contactsInChat_[0] == handle);
}



// Parse a regular command
void MsnSwitchboardConnection::parseCommand(const QStringList& command)
{
  if ( command[0] == "ACK" )
  {
    gotAck( command );
  }
  else if ( command[0] == "ANS" )
  {
    // Do nothing.
  }
  else if ( command[0] == "BYE" )
  {
    gotBye( command );
  }
  else if ( command[0] == "CAL" )
  {
    // Do nothing
  }
  else if ( command[0] == "IRO" )
  {
    gotIro( command );
  }
  else if ( command[0] == "JOI" )
  {
    gotJoi( command );
  }
  else if ( command[0] == "NAK" )
  {
    gotNak( command );
  }
  else if ( command[0] == "OUT" )
  {
    gotOut( command );
  }
  else if ( command[0] == "USR" )
  {
    gotUsr( command );
  }
  else if ( command[0] == "216" )
  {
    emit systemMessage( i18n( "This person is online, but he or she is blocking you." ) );
  }
  else if ( command[0] == "217" )
  {
    emit systemMessage( i18n( "This person is offline or invisible." ) );
  }
  else if ( command[0] == "282" )
  {
    // Got it once when I sent a bad P2P message or something.
    kdDebug() << "Switchboard got unknown 282 error response." << endl;
  }
  else
  {
    kdDebug() << "Switchboard got unhandled command " << command[0] << "." << endl;
  }
}



// Parse a message command
void MsnSwitchboardConnection::parseMessage(const QStringList& command, const MimeMessage &message)
{
  // Get the message type from the head
  QString contentType = message.getSubValue( "Content-Type" );
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "Switchboard: Message type is '" << contentType << "'." << endl;
#endif

  // Get the sender's handle and friendly name
  QString contactHandle;
  QString friendlyName;
  contactHandle = command[1];

  // Check the contact list for a more current friendly name
  friendlyName = getContactFriendlyNameByHandle( contactHandle );
  if ( friendlyName == "" )
  {
    // There was no current friendly name, so get one from the message
    friendlyName = command[2];
    removePercents( friendlyName );
  }

  if ( contentType == "text/plain" )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "Switchboard: Message is:" << endl;
    message.print();
#endif
    // This is a regular text message to the user
    QFont    font;
    QString color;
    getFontAndColorFromMessage( font, color, message );

    // Emit the message
    emit chatMessage( contactHandle, friendlyName, message.getBody(), font, color, "#D60000" );
  }
  else if ( contentType == "text/x-msmsgscontrol" )
  {
    QString typingUser = message.getValue( "TypingUser" );
    // Check the contact list for a more current friendly name
    QString typingName = getContactFriendlyNameByHandle( typingUser );
    if ( typingName == "" )
    {
      typingName = typingUser;
    }
    emit contactTyping( typingUser, typingName );
  }
  else if ( contentType == "text/x-msmsgsinvite" )
  {
    // This is an application message
    MimeMessage subMessage( message.getBody() );
    parseMimeAppMessage( contactHandle, subMessage );
  }
  else if ( contentType == "application/x-msnmsgrp2p" )
  {
    // This is an p2p message
    P2PMessage subMessage( message );
    parseP2PAppMessage( contactHandle, subMessage );
  }
  else if ( contentType == "text/x-clientcaps" )
  {
    // This is a message exchanged by a lot of third party clients.
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "Switchboard: Got third-party client info message. (message dump follows)" << endl;
    message.print();
    // Client-Name: Client-Name/Version-Major.Version-Minor
    // Chat-Logging: Y (Nonsecure logging is enabled), S (log is encrypted), N (client is not logging conversation)
#endif
  }
  else
  {
    kdDebug() << "SwitchBoard got unhandled message type " << contentType << "." << endl;
  }
}



// Parse an application message
void MsnSwitchboardConnection::parseMimeAppMessage(const QString &sendingContact, const MimeMessage &message)
{
#ifdef KMESSDEBUG_SWITCHBOARD_MIMEAPP
  kdDebug() << "Switchboard: Parsing Mime appliation message." << endl;
  message.print();
#endif
  Application *app = 0;

  // If this is an invite message, an application need to be created
  if ( message.getValue("Invitation-Command") == "INVITE" )
  {
    // Create the application based on the type
    app = createApplication( sendingContact, message );
  }
  else
  {
    // Find the application based on the app cookie
    app = getApplication( message.getValue("Invitation-Cookie")  );
  }


  // Give the message to the application
  if ( app != 0 )
  {
    app->gotMessage( message );
  }
}



// Handle the incoming P2P messages.
void MsnSwitchboardConnection::parseP2PAppMessage(const QString &sendingContact, const P2PMessage &message)
{
  // First see if the message was direct to us
  QString p2pDest = message.getValue("P2P-Dest");

  // Received a message with all values left empty, (e.g. To: <msnmsgr:>)
  if(p2pDest.isEmpty())
  {
    kdWarning() << "SwitchBoard: Unable to handle P2P message, "
                << "message values are empty, switchboard noticed an error in our message." << endl;
    return;
  }

  // P2P dest is empty if we produce an error when a session is not initiated yet
  if(p2pDest != currentAccount_->getHandle())
  {
#ifdef KMESSDEBUG_SWITCHBOARD_P2PAPP
    kdDebug() << "SwitchBoard: Received a P2P message, but it's for '" << p2pDest << "'." << endl;
#endif
    return;
  }


  // Parse the message
#ifdef KMESSDEBUG_SWITCHBOARD_P2PAPP
  kdDebug() << "SwitchBoard: Parsing P2P:"
            << " SID="         << message.getSessionID()
            << " MID="         << message.getMessageID()
            << " AckID="       << message.getAckMessageID()
            << " ACKUID="      << message.getAckUniqueID()
            << " flags="       << message.getFlags()
            << " size="        << message.getDataSize()
            << " no.sessions=" << p2pApplications_.count() << endl;
#endif

  QPtrListIterator<P2PApplication> p2pIterator(p2pApplications_);
  P2PApplication *p2pApplication = 0;

  unsigned long sessionID    = message.getSessionID();
  unsigned long messageID    = message.getMessageID();
  unsigned long ackUniqueID  = message.getAckUniqueID();
  unsigned long ackMessageID = message.getAckMessageID();

  int callPos = -1;
  QString slpMessage;
  QString callID;
  QRegExp callRE("Call-ID: (.+)\r\n"); // HACK: match everything because MSN6 accepts anything.
  callRE.setMinimal(true);


  if( sessionID == 0 )
  {
    // Session ID is 0 if clients negotiate.
    // (unfortunately for both INVITE and BYE messages!)

    if( message.getFlags() == 0
     && message.getDataSize() > 0)  // HACK: added for Bot2K3 4.1 (has no flags with first ack message).
    {
      if( message.getDataOffset() > 0 )
      {
        // This is another fragment of the SLP message.
        // Find the correct application to deliver this message to.
        while( p2pIterator.current() != 0 )
        {
#ifdef KMESSDEBUG_SWITCHBOARD_P2PAPP
          unsigned long debugMsgId = p2pIterator.current()->getLastContactMessageID();
          kdDebug() << "SwitchBoard: Demultiplexing P2P message"
                    << "    (fragment MessageID " << debugMsgId << " == " << messageID << " ?)" << endl;
#endif

          if(p2pIterator.current()->getLastContactMessageID() == messageID)
          {
            // HACK: Not tested for "&& getLastContactAckMessageID() == ackMessageID" to support non-standard implementations.

            // Found it, save reference
            p2pApplication = p2pIterator.current();
            break;
          }

          ++p2pIterator;
        }

      }
      else
      {
        // No offset, start of SLP message

        // NOTE: Assuming that the Call-ID will be found in the first part..
        slpMessage = QString::fromUtf8( message.getData(), message.getDataSize() );
        callPos    = callRE.search(slpMessage);

        if( callPos == -1 )
        {
          kdWarning() << "SwitchBoard: Unable to handle P2P message, "
                      << "CallID not found. (message dump follows)" << endl;
          kdDebug() << slpMessage << endl;
        }
        else
        {
          // first try to find an existing session. (for 200 OK and BYE messages)
          callID = callRE.cap(1);

          while( p2pIterator.current() != 0 )
          {
#ifdef KMESSDEBUG_SWITCHBOARD_P2PAPP
            kdDebug() << "SwitchBoard: Demultiplexing P2P message"
                      << "    (CallID " << p2pIterator.current()->getCallID() << " == " << callID  << " ?)" << endl;
#endif

            if( p2pIterator.current()->getCallID() == callID )
            {
              // Found it, save reference
              p2pApplication = p2pIterator.current();
              break;
            }

            ++p2pIterator;
          }

          // No existing session found..?
          if( p2pApplication == 0 )
          {
#ifdef KMESSDEBUG_SWITCHBOARD_P2PAPP
            kdDebug() << "SwitchBoard: No existing P2P session found, must be a new session invitation." << endl;
#endif

            // Initialize an application class to handle the invitation.
            p2pApplication = createApplication( sendingContact, message );
          }
        }
      }
    }
    else if( message.isAck() || message.isWaiting() || message.isClosingAck() || message.isError() )
    {
      // Both the ACK-Message-ID and ACK-Unique-ID fields
      // should resolve the orginal session it was sent from.

      while( p2pIterator.current() != 0 )
      {
#ifdef KMESSDEBUG_SWITCHBOARD_P2PAPP
        kdDebug() << "SwitchBoard: Demultiplexing P2P message"
                  << "    (UniqueID "  << p2pIterator.current()->getLastUserUniqueID()  << " == " << ackUniqueID  << " ?)"
                  << " or (UniqueID "  << p2pIterator.current()->getLastUserUniqueID()  << " == " << ackMessageID << " ?)"
                  << " or (MessageID " << p2pIterator.current()->getLastUserMessageID() << " == " << ackMessageID << " ?)" << endl;
#endif

        if(  p2pIterator.current()->getLastUserUniqueID()  == ackUniqueID   // This works with MSN 6.2
        ||  (p2pIterator.current()->getLastUserUniqueID()  == ackMessageID && ackUniqueID == 0) )  // For type-8 error messages
        {
          // Found it, save reference
          p2pApplication = p2pIterator.current();
          break;
        }

        if( ackUniqueID == 0 && p2pIterator.current()->getLastUserMessageID() == ackMessageID )
        {
          // HACK: added for GAIM 1.5 (does not return UniqueID)
          kdWarning() << "SwitchBoard: Got P2P message with matching ackMessageID and no ackUniqueID set (delivering anyway, contact=" << sendingContact << ")!" << endl;
          p2pApplication = p2pIterator.current();
          break;
        }

        ++p2pIterator;
      }

      // No existing session found..?
      if(p2pApplication == 0)
      {
        kdWarning() << "SwitchBoard: P2P message can't be handled, "
                    << "ACK-Message-ID not found (flags=" << message.getFlags() << " size=" << message.getDataSize() << " contact=" << sendingContact << ")." << endl;
        return;
      }
    }
    else if( message.getFlags() == 0 && message.getDataSize() == 0 && ackUniqueID == 0 )
    {
      // HACK: added bot Bot2k3 4.1 (sends first ack without UniqueID, flags or any other context).
      kdWarning() << "SwitchBoard P2P message has no ACK fields set at all (assuming base identifier message, contact=" << sendingContact << ")!" << endl;
      return;
    }
    else
    {
      // Didn't reconize the flag type.
      kdWarning() << "SwitchBoard: P2P message can't be handled, "
                  << "unknown flag type found (flags=" << message.getFlags() << " size="  << message.getDataSize() << " contact=" << sendingContact << ")." << endl;

    }
  }
  else
  {
    // The message has a SessionID set

    // Find the correct session object.
    while( p2pIterator.current() != 0 )
    {
#ifdef KMESSDEBUG_SWITCHBOARD_P2PAPP
      kdDebug() << "SwitchBoard: Demultiplexing P2P message"
                << "    (SessionID " << p2pIterator.current()->getSessionID() << " == " << sessionID << " ?)" << endl;
#endif
      if( p2pIterator.current()->getSessionID() == sessionID )
      {
        // Found the session
        p2pApplication = p2pIterator.current();
        break;
      }

      ++p2pIterator;
    }

    // Notify the user if the message was invalid (send error-response later)
    if(p2pApplication == 0)
    {
      kdWarning() << "SwitchBoard: Unable to handle P2P message, "
                  << "Session-ID not found (flags="   << message.getFlags() << " size=" << message.getDataSize() << " contact=" << sendingContact << ")." << endl;
    }
  }


  // If the application was found, deliver the message
  if( p2pApplication != 0 )
  {
    // Deliver the message
    p2pApplication->gotMessage( reinterpret_cast<const MimeMessage&>(message) );
  }
  else
  {
    // If we didn't find the application, the message had bad fields
    // or we don't support the invitation type:

    if( message.isAck() || message.isWaiting() || message.isClosingAck() || message.isError() )
    {
#ifdef KMESSDEBUG_SWITCHBOARD_P2PAPP
      kdDebug() << "SwitchBoard: Not sending an P2P Error response for error-message "
                << "(flags=" << message.getFlags() << ")." << endl;
#endif
    }
    else
    {
#ifdef KMESSDEBUG_SWITCHBOARD_P2PAPP
    kdDebug() << "SwitchBoard: Creating new P2PApplication instance to send error response." << endl;
#endif

      // Create a default application to send the correct error response (also for unknown invitation types)
      p2pApplication = new P2PApplication( getLocalIp(), sendingContact );
      p2pApplication->setMode(Application::APP_MODE_ERROR_HANDLER);
      p2pApplication->gotMessage( reinterpret_cast<const MimeMessage&>(message) );
    }
  }
}



// Slot, activated if picture is received
void MsnSwitchboardConnection::pictureReceived(const QString &handle, const QString &msnObjectString)
{
#ifdef KMESSDEBUG_SWITCHBOARD_P2PAPP
  kdDebug() << "SwitchBoard: Received contact picture from " << handle << "." << endl;
#endif

  // Extract data from the MsnObject
  MsnObject msnObject(msnObjectString);
  QString   fileName;

  // Get the contact
  // Note I don't use msnObject.getCreator, because I don't like to trust the other client!
  Contact *contact = chatInfo_->getContactList()->getContactByHandle(handle);
#ifdef KMESSTEST
  ASSERT( contact != 0 );
#endif

  if(contact != 0)
  {
    // Handle the picture
    MsnObject::MsnObjectType type = msnObject.getType();
    // fileName = PictureTransferP2P::getPictureFileName(msnObject);

    if(type == MsnObject::DISPLAYPIC)
    {
      // Don't use the fileName from the MsnObject, use the most recent
      // maybe the contact changed their picture while we were still transferring the old one
      fileName = PictureTransferP2P::getPictureFileName(* (contact->getMsnObject()));
      contact->getExtension()->setContactPicturePath( fileName );

      // Are we the switchboard that transferred the contact picture?
      if(contact->getMsnObjectChat() == this->chatInfo_)
      {
        // Unregister this chat, allow others to take ownership
        contact->setMsnObjectChat(0);
      }
    }
    else
    {
      kdWarning() << "SwitchBoard: Unable to handle MsnObject pictures of type '" << type << "'." << endl;
    }
  }
  else
  {
    kdWarning() << "MsnSwitchboardConnection::pictureReceived() - Contact is null!" << endl;
  }
}



// Send an application message to the server.
void MsnSwitchboardConnection::putMimeApplicationMsg(const MimeMessage &message)
{
  // Simply send the message on to putMsg.  As long as the applications can
  // be trusted to send correctly formatted data, no checking needs to be done.

#ifdef KMESSDEBUG_SWITCHBOARD_MIMEAPP
  kdDebug() << "Switchboard: Putting app message" << endl;
#endif

  sendMimeMessageWhenReady(ACK_NAK_ONLY, message);
}



// Send an p2p message to the server.
void MsnSwitchboardConnection::putP2PApplicationMsg(const MimeMessage &message)
{
#ifdef KMESSDEBUG_SWITCHBOARD_P2PAPP
  kdDebug() << "Switchboard: Putting p2p message" << endl;
#endif

  // Send the message and update the ACK-register
  sendMimeMessageWhenReady(ACK_ALWAYS_P2P, message);
}



// Send a reject message for the given application
void MsnSwitchboardConnection::rejectMimeApplication(const QString& cookie, bool notInstalled)
{
  QString command, message;
  message = "MIME-Version: 1.0\r\n"
            "Content-Type: text/x-msmsgsinvite; charset=UTF-8\r\n\r\n"
            "Invitation-Command: CANCEL\r\n"
            "Invitation-Cookie: " + cookie + "\r\n"
            "Cancel-Code: " + (notInstalled ? "REJECT_NOT_INSTALLED" : "REJECT");  // sendcommand adds a \r\n

  command.sprintf( "N %u\r\n", message.utf8().length() );
  sendCommand( "MSG", command + message );
}



// Send a message to the contact(s)
void MsnSwitchboardConnection::sendChatMessage(QString text)
{
  if ( currentAccount_ == 0 )
  {
    kdWarning() << "MsnSwitchboardConnection::sendChatMessage - currentAccount_ is null!" << endl;
    return;
  }

  // Get parameters
  QFont   font       = currentAccount_->getFont();
  QString fontFamily = font.family();
  QString color      = currentAccount_->getFontColor();

  // Convert data
  addPercents( fontFamily );
  convertHtmlColorToMsnColor( color );

  // Determine effects
  QString effects    = "";
  if ( font.bold() )      effects += "B";
  if ( font.italic() )    effects += "I";
  if ( font.underline() ) effects += "U";

  // Create the message
  MimeMessage message;
  message.addField("MIME-Version",    "1.0");
  message.addField("Content-Type",    "text/plain; charset=UTF-8");
  message.addField("X-MMS-IM-Format", "FN=" + fontFamily + "; EF=" + effects + "; CO=" + color + "; CS=0; PF=0");
  message.setBody(text);

  // Send the message
  sendMimeMessageWhenReady(ACK_NAK_ONLY, message);
}



// Send a message to the contact(s), or leave it pending until a connection is restored
void MsnSwitchboardConnection::sendMimeMessageWhenReady(AckType ackType, const MimeMessage &message)
{
  if(chatInfo_ == 0) return;

  // If the connection is ready, send the message
  if( isConnected() && contactsConnected_ )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "Switchboard - Sending message." << endl;
#endif

    int ack = sendMimeMessage(ackType, message);
    storeMessageForAcknowledgement(ack, ackType, message);
  }
  else
  {
    // otherwise, store as pending message

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "Switchboard - Storing pending message to send later." << endl;
#endif

    // Store the message
    pendingMessages_.append( new QPair<AckType,MimeMessage>(ackType, message) );

    // The connection was closed.
    // It's not possible to re-open it, because
    // the authcookie for the USR command expired
    if(! isConnected())
    {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
      kdDebug() << "Switchboard - connection is closed, asking notification connection for a new switchboard connection!" << endl;
#endif

      // Request a new switchboard session
      // TODO: test what happens in a multi-chat, when isExclusiveChatWithContact() fails
      chatInfo_->getNotificationConnection()->requestChat( contactsInChat_.first() );
    }
    else if(! contactsConnected_)
    {
      // Call the first (should be only) contact in the "contacts in chat" list.
      if( contactsInChat_.count() > 0)
      {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
        kdDebug() << "Switchboard - No contacts are presently in the chat - call the one kept." << endl;
#endif
        sendCommand( "CAL", contactsInChat_.first() + "\r\n");
      }
      // else: first contact didn't join the chat yet, just be patient
    }
  }
}



// Send messages that weren't sent because a contact had to be re-called
void MsnSwitchboardConnection::sendPendingMessages()
{
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  if ( pendingMessages_.count() > 0 )
  {
    kdDebug() << "Switchboard - Sending " << pendingMessages_.count() << " pending messages." << endl;
  }
#endif

  QPair<AckType,MimeMessage> *pendingMessage;


  // A contact should be connected, but check to make sure
  if ( contactsConnected_ )
  {
    // Send all messages
    pendingMessage = pendingMessages_.first();
    while( pendingMessage != 0 )
    {
      // Send the message
      int ack = sendMimeMessage( pendingMessage->first, pendingMessage->second );
      storeMessageForAcknowledgement( ack, pendingMessage->first, pendingMessage->second );

      // get next message
      pendingMessage = pendingMessages_.next();
    }

    // Clear the pending messages
    pendingMessages_.setAutoDelete(true);
    pendingMessages_.clear();
  }
}



// The user is typing so send a typing message
void MsnSwitchboardConnection::sendTypingMessage()
{
  // Build the typing notification message
  MimeMessage message;
  message.addField("MIME-Version", "1.0");
  message.addField("Content-Type", "text/x-msmsgscontrol");
  message.addField("TypingUser",   currentAccount_->getHandle());

  // if disconnected, reconnect to send the typing message
  // (maybe the other contact still has it's window open)
  sendMimeMessageWhenReady(ACK_NONE, message);
}



// Start an application
void MsnSwitchboardConnection::startApp(ApplicationType type)
{
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "Switchboard - start application " << type << "." << endl;
#endif

  MimeApplication *app = 0;

  // Create the application
  if ( type == GNOMEMEETING )
  {
    app = new GnomeMeeting( getLocalIp() );
  }
  else if ( type == MSNREMOTEDESKTOP )
  {
    app = new MsnRemoteDesktop( getLocalIp() );
  }
  else if ( type == FILETRANSFER )
  {
    app = new FileTransfer( CurrentAccount::instance()->getHandle(), getLocalIp() );
  }
  else if( type == VOICECONVERSATION )
  {
    // Stop the current voice conversation
    VoiceConversation *voiceConversation = getVoiceConversation();
    if( voiceConversation != 0 )
    {
      voiceConversation->userAborted();
      return;
    }

    app = new VoiceConversation( getLocalIp() );
  }
#ifdef KMESSTEST
  else
  {
    kdWarning() << "Switchboard - Invalid type passed to startApp()!" << endl;
  }
#endif


  // Connect up the application
  if ( app != 0 )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_FILETRANSFER
    kdDebug() << "Switchboard - Connecting and starting the application." << endl;
#endif

    // This sets the signals, and appends the app to the collection.
    connectApplication(app);
    app->start();

#ifdef KMESSDEBUG_SWITCHBOARD_MIMEAPP
    kdDebug() << "Switchboard - The application's cookie is " << app->getCookie() << endl;
#endif
  }
}



// Start a chat
void MsnSwitchboardConnection::startChat(ChatInformation *chatInfo)
{
#ifdef KMESSTEST
  ASSERT( currentAccount_ != 0 );
  ASSERT( chatInfo != 0 );
  ASSERT( chatInfo->getContactList() != 0 );
  ASSERT( chatInfo->getContactHandle() != 0 );
#endif

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "Switchboard - Starting chat is with " << chatInfo->getContactHandle() << "." << endl;
#endif


  if(isConnected())
  {
    if(chatInfo_ != 0 &&
       chatInfo_->getIp()   == chatInfo->getIp() &&
       chatInfo_->getPort() == chatInfo->getPort())
    {
      // Safety net
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
      kdWarning() << "Switchboard - startChat called twice, already connected with server." << endl;
#endif
      return;
    }

    if( ! contactsInChat_.isEmpty() && ! isExclusiveChatWithContact(chatInfo->getContactHandle()))
    {
      kdWarning() << "Switchboard is already connected to '" << chatInfo_->getIp() << ":" << chatInfo_->getPort() << "', "
                  << "cant't start new chat with '" << chatInfo_->getContactHandle() << "'!" << endl;
      return;
    }

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "Switchboard - Contact resumed the chat but uses a different server, reconnecting." << endl;
#endif
    closeConnection();
  }

  if( p2pApplications_.count() > 0 || mimeApplications_.count() > 0 )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
    kdDebug() << "Switchboard: re-connecting but there are still p2p applications open, terminating applications now." << endl;
#endif
    closeConnection();
  }

#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  kdDebug() << "Switchboard - Connect to " << chatInfo->getIp() << ":" << chatInfo->getPort() << "." << endl;
#endif

  // Store chatinfo object
  chatInfo_ = chatInfo;

  if( ! connectToServer( chatInfo_->getIp(), chatInfo_->getPort() ) )
  {
    kdWarning() << "Switchboard couldn't connect to the server." << endl;
  }
}



// Start a file transfer session based on the client capabilities
void MsnSwitchboardConnection::startFileTransfer(const QString &handle)
{
  if( isContactInChat(handle) )
  {
    startApp(FILETRANSFER);

    /*

    // FIXME: Just get the first contact, we'll support more later. (see ChatWindow::transferFile() for details)
    Contact *contact = chatInfo_->getContactList()->getContactByHandle(handle);
    uint clientCaps = (contact != 0 ? contact->getCapabilities() : 0);

    // Normally we would use an AND mask, but this should be compatible with future MSN versions.
    if( clientCaps >= Contact::MSN_CAP_MSN60 )
    {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
      kdDebug() << "MsnSwitchboardConnection: Contact supports MSN6-style file transfer invitations." << endl;
#endif

      // Ask for a filename
      QString recentTag = ":fileupload";
      KURL    startDir = KFileDialog::getStartURL(QString::null, recentTag);
      QString filename = KFileDialog::getOpenFileName(startDir.url());

      if(! filename.isEmpty())
      {
#ifdef KMESSDEBUG_SWITCHBOARD_P2PAPP
        kdDebug() << "SwitchBoard: Creating new P2PApplication instance to initiate file transfer." << endl;
#endif
        // Use the new P2P style invitation for MSN6 clients.
        P2PApplication *p2pApplication = new FileTransferP2P( getLocalIP(), handle, filename );
        connectApplication( p2pApplication );
        p2pApplication->start();
      }
    }
    else
    {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
      kdDebug() << "MsnSwitchboardConnection: Contact doesn't support MSN6-style file transfer invitations, using old method." << endl;
#endif

      // Use the old fashion MSN4 method.
      startApp(FILETRANSFER);
    }

    */
  }
  else
  {
    kdWarning() << "MsnSwitchboardConnection::startFileTransfer() - Contact is not part of the chat session!" << endl;
  }
}



// Download the contact picture.
void MsnSwitchboardConnection::startPictureDownload(Contact *contact)
{
  // Get contact info
  const QString    handle    = contact->getHandle();
  const MsnObject *msnObject = contact->getMsnObject();

#ifdef KMESSTEST
  ASSERT( msnObject != 0 );
#endif
  if( msnObject == 0 ) return;


#ifdef KMESSDEBUG_SWITCHBOARD_P2PAPP
  kdDebug() << "SwitchBoard: Find out whether we may download the contact picture." << endl;
#endif

  const ChatInformation *msnObjectChat = contact->getMsnObjectChat();

  if(msnObjectChat == 0)
  {
#ifdef KMESSDEBUG_SWITCHBOARD_P2PAPP
    kdDebug() << "SwitchBoard: Contact is free, register this switchboard." << endl;
#endif

    // No one registered, register ourselves
    contact->setMsnObjectChat( this->chatInfo_ );
  }
  else if(msnObjectChat != this->chatInfo_)
  {
#ifdef KMESSDEBUG_SWITCHBOARD_P2PAPP
    kdDebug() << "SwitchBoard: Another switchboard should download the picture." << endl;
#endif

    return;
  }
  else
  {
#ifdef KMESSDEBUG_SWITCHBOARD_P2PAPP
    kdDebug() << "SwitchBoard: This switchboard is assigned to download the picture." << endl;
#endif
  }


  // Get the picture filename, perhaps from a cache
  QString pictureFileName = PictureTransferP2P::getPictureFileName( *msnObject );
  if( QFile::exists(pictureFileName) )
  {
#ifdef KMESSDEBUG_SWITCHBOARD_P2PAPP
    kdDebug() << "SwitchBoard: Contact picture already in cache." << endl;
#endif

    // Already have it.
    contact->getExtension()->setContactPicturePath( pictureFileName );
  }
  else
  {
#ifdef KMESSDEBUG_SWITCHBOARD_P2PAPP
    kdDebug() << "SwitchBoard: Creating new P2PApplication instance to download contact picture." << endl;
#endif

    // Create the app, and start downloading
    P2PApplication *app = new PictureTransferP2P( getLocalIp(), handle, msnObject->objectString() );

    // Connect the application
    connectApplication(app);

    // Specific signals for this object:
    connect( app,  SIGNAL(pictureReceived(const QString&, const QString&)), // Contact picture received
             this,   SLOT(pictureReceived(const QString&, const QString&)));

    app->start();
  }
}



// Store a message for later acknowledgement
void MsnSwitchboardConnection::storeMessageForAcknowledgement(int ack, AckType ackType, const MimeMessage& message)
{
  // don't store if the ack-type indicates so
  if(ackType == ACK_NONE) return;

  acks_.append( QString::number(ack) );
  messages_.append( message.getBody() );
}



// The generic implementation for terminateMimeApplication() / terminateP2PApplication()
void MsnSwitchboardConnection::terminateApplication(Application *app)
{
  // Destroy the instance when all slots are handled
  app->deleteLater();

  // Signal when all applications are aborted
  if(mimeApplications_.count() == 0 &&
     p2pApplications_.count()  == 0)
  {
    // If initiated by closeConnectionLater(), close connection
    if(closingConnection_)
    {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
      kdDebug() << "MsnSwitchboardConnection::terminateApplication: closeConnectionLater() finished aborting applications, calling closeConnection()." << endl;
#endif
      closeConnection();
    }

    // If initiated by abortApplications(), signal that
    if(abortingApplications_)
    {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
      kdDebug() << "MsnSwitchboardConnection::terminateApplication: signaling that abortApplications() finished." << endl;
#endif
      emit applicationsAborted();
    }

    // If initiated by closeConnectionLater(true), also delele this object
    if(autoDeleteLater_)
    {
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
      kdDebug() << "MsnSwitchboardConnection::terminateApplication: deleting instance as requested by closeConnectionLater()" << endl;
#endif
      deleteLater();
    }
  }
}



// Remove and delete the given mimeapplication.
void MsnSwitchboardConnection::terminateMimeApplication(Application *app)
{
  bool success = mimeApplications_.removeRef((MimeApplication*) app);
  if(! success)
  {
    kdWarning() << "MsnSwitchboardConnection::terminateMimeApplication() - Could not remove object!" << endl;
  }

  // Do the generic deletion
  terminateApplication(app);
}



// Remove and delete the given p2papplication.
void MsnSwitchboardConnection::terminateP2PApplication(Application *app)
{
  bool success = p2pApplications_.removeRef((P2PApplication*) app);
  if(! success)
  {
    kdWarning() << "MsnSwitchboardConnection::terminateP2PApplication() - Could not remove object!" << endl;
  }

  // Do the generic deletion
  terminateApplication(app);
}



#include "msnswitchboardconnection.moc"
