/***************************************************************************
                          chatmaster.cpp  -  description
                             -------------------
    begin                : Sat Jan 18 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 "chatmaster.h"

#include "../contact/contact.h"
#include "../contact/contactextension.h"
#include "../contact/msnobject.h"
#include "../model/contactlist.h"
#include "../network/applications/applicationlist.h"
#include "../network/applications/mimeapplication.h"
#include "../network/applications/p2papplication.h"
#include "../network/applications/filetransfer.h"
#include "../network/applications/filetransferp2p.h"
#include "../network/applications/inktransferp2p.h"
#include "../network/applications/msnobjecttransferp2p.h"
#include "../network/chatmessage.h"
#include "../network/msnswitchboardconnection.h"
#include "../currentaccount.h"
#include "../kmessapplication.h"
#include "../kmessdebug.h"
#include "../utils/kmessconfig.h"
#include "chat.h"
#include "chatwindow.h"

#include <QFile>
#include <QImageReader>
#include <QTextDocument>
#include <QTest>

#include <KWindowSystem>

#ifdef Q_WS_X11
# include <QX11Info>
#endif

// Settings for debugging
#define CHATMASTER_SEND_FILES_MSNFTP 0



// The constructor
ChatMaster::ChatMaster(QObject *parent)
 : QObject(parent),
   initialized_(false)
{
  setObjectName("ChatMaster");
}



// The destructor
ChatMaster::~ChatMaster()
{
  disconnected();

  // Delete all open windows
  kmDebug() << chatWindows_;
  qDeleteAll( chatWindows_ );
  chatWindows_.clear();

  // Cleanup pending messages
  qDeleteAll( pendingMimeMessages_ );
  pendingMimeMessages_.clear();

#ifdef KMESSDEBUG_CHATMASTER
  kmDebug() << "DESTROYED.";
#endif
}



// The connection has been established
void ChatMaster::connected()
{
  // There are no chats to reactivate
  if( chats_.isEmpty() )
  {
    return;
  }

#ifdef KMESSDEBUG_CHATMASTER
  kmDebug() << "Trying reactivation of" << chats_.count() << "open chats.";
#endif

  QList<ChatWindow*> windowsList;

  // Go through all the chats and reactivate them
  foreach( Chat *chat, chats_ )
  {
    // Reactivate right away chats which have a connection already. Should never happen though.
    if( chat->getSwitchboardConnection() != 0 )
    {
      chat->setEnabled( true );

      // Mark this chat's window for reactivation
      if( ! windowsList.contains( chat->getChatWindow() ) )
      {
        windowsList.append( chat->getChatWindow() );
      }

      continue;
    }

    const QStringList participants( chat->getParticipants() );

#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "Reactivating chat:" << participants;
#endif

    // Check if the participants list is empty: if so, there's something fishy going on.
    if( participants.isEmpty() )
    {
      kmWarning() << "Tried to reactivate chat with no contacts - has a window?" << ( chat->getChatWindow() != 0 );
      chat->getChatWindow()->removeChatTab( chat );
      continue;
    }

    // Do not re-enable group chats, as they need a new invitation to work again
    if( participants.count() > 1 )
    {
#ifdef KMESSDEBUG_CHATMASTER
      kmDebug() << "This is a group chat, skipping it.";
#endif
      continue;
    }

    const QString &handle = participants.first();

    // Only reactivate the chat if this contact is in the connected account's list
    if( currentAccount_->getContactByHandle( handle ) == 0 )
    {
#ifdef KMESSDEBUG_CHATMASTER
      kmDebug() << "This chat is with a non-listed contact, closing it.";
#endif
      chat->getChatWindow()->removeChatTab( chat );
      continue;
    }

#ifdef KMESSDEBUG_CHATMASTER
      kmDebug() << "Reactivating chat with listed contact:" << handle;
#endif

    // Create a new switchboard, setting it as empty but with an hint about
    // who to call if the user wants to chat again.
    MsnSwitchboardConnection *switchboard = createSwitchboardConnection( 0, handle );
    if( switchboard )
    {
      chat->setSwitchboardConnection( switchboard );
      chat->setEnabled( true );

      // Mark this chat's window for reactivation
      if( ! windowsList.contains( chat->getChatWindow() ) )
      {
        windowsList.append( chat->getChatWindow() );
      }
    }
  }

  // Go through all the windows and reactivate them
  foreach( ChatWindow *window, windowsList )
  {
    window->setUIState( ChatWindow::Connected );
  }
}


// The user has disconnected, so close all open switchboard connections and disable the chats
void ChatMaster::disconnected()
{
  // Don't waste time disabling the chats when exiting.
  KMessApplication *kmessApp = static_cast<KMessApplication*>( kapp );
  if( kmessApp->quitSelected() )
  {
    return;
  }

  if( ! chatWindows_.isEmpty() )
  {
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "Disabling" << chatWindows_.count() << "open chat windows.";
#endif

    // Go through all the chats
    foreach( ChatWindow *window, chatWindows_ )
    {
      // Disable the chats so they can't receive user input anymore
      window->setUIState( ChatWindow::Disconnected );
    }
  }

  // close down all switchboards first.
  if( ! switchboardConnections_.isEmpty() )
  {
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "Closing" << switchboardConnections_.count() << "open switchboards.";
#endif

    // give each switchboard a chance to close properly.
    foreach( MsnSwitchboardConnection *conn, switchboardConnections_ )
    {
      conn->closeConnection();
    }
  }

  if( ! chats_.isEmpty() )
  {
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "Closing" << chats_.count() << "open chats.";
#endif

    // Go through all the chats and disable them
    foreach( Chat *chat, chats_ )
    {
      chat->setEnabled( false );
    }
  }

  // Cleanup switchboard connections.
  qDeleteAll( switchboardConnections_ );
  switchboardConnections_.clear();
}



// KMess is establishing a connection.
void ChatMaster::connecting()
{
  // There are no chats modify.
  if( chats_.isEmpty() )
  {
    return;
  }

#ifdef KMESSDEBUG_CHATMASTER
  kmDebug() << "Disabling reconnect button of" << chats_.count() << "open chats.";
#endif

  QList<ChatWindow*> windowsList;

  // Go through all the chats and add their window.
  foreach( Chat *chat, chats_ )
  {
    if( ! windowsList.contains( chat->getChatWindow() ) )
    {
      windowsList.append( chat->getChatWindow() );
    }
  }

  // Go through all the windows and disable their reconnect button.
  foreach( ChatWindow *window, windowsList )
  {
    window->setUIState( ChatWindow::Connecting );
  }
}



// Forward a new switchboard server request signal coming from an existing switchboard connection
void ChatMaster::forwardRequestNewSwitchboard( QString handle )
{
#ifdef KMESSDEBUG_CHATMASTER
  kmDebug() << "Forwarding the new switchboard request signal for "
            << handle << "." << endl;
#endif

  emit requestSwitchboard( handle, ChatInformation::CONNECTION_REFRESH );
}



// Forward a request to add or remove a contact from the contactlist, which comes from the ContactFrame.
void ChatMaster::forwardContactAdded( QString handle, bool isAdded )
{
  if( isAdded )
  {
    emit addContact( handle );
  }
  else
  {
    emit removeContact( handle, false );
  }
}



// Forward a request to block or unblock a contact, which comes from the ContactFrame.
void ChatMaster::forwardContactBlocked( QString handle, bool isBlocked )
{
  if( isBlocked )
  {
    emit blockContact( handle );
  }
  else
  {
    emit unblockContact( handle );
  }
}



// Return the application list for a given contact
ApplicationList * ChatMaster::getApplicationList(const QString &handle)
{
  ApplicationList *appList = 0;

  // Get the contact.
  ContactBase *contact = currentAccount_->getContactByHandle(handle);
  if(KMESS_NULL(contact)) return 0;

  if( handle == currentAccount_->getHandle() )
  {
    kmWarning() << "Cannot start applications with yourself.";
    return 0;
  }

  // Get the application list
  if( contact->hasApplicationList() )
  {
    appList = contact->getApplicationList();
  }
  else
  {
    // Create object if it wasn't created yet for this contact.
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "Creating application list for contact " << handle;
#endif
    appList = contact->createApplicationList();

    // Connect signals from the applist
    connect( appList, SIGNAL(         newApplication(Application*)        ),   // A new application was created.
             this,      SLOT( slotConnectApplication(Application*)        ));
    connect( appList, SIGNAL(                 putMsg(const MimeMessage&, const QString&, bool)   ),   // Request to send message
             this,      SLOT( slotDeliverMimeMessage(const MimeMessage&, const QString&, bool)   ));
  }

  return appList;
}



// Return the chat tab which uses the given switchboard connection
Chat *ChatMaster::getChatBySwitchboard( const MsnSwitchboardConnection *connection )
{
  // Look through the chat windows for an exclusive chat with the given contact
  foreach( Chat *chat, chats_ )
  {
    if( chat->getSwitchboardConnection() == connection )
    {
      return chat;
    }
  }

  return 0;
}



// Return the chat window which uses the given switchboard connection
ChatWindow * ChatMaster::getChatWindowBySwitchboard(const MsnSwitchboardConnection *connection)
{
  Chat *chat = getChatBySwitchboard( connection );
  if( chat != 0 )
  {
    return chat->getChatWindow();
  }

  return 0;
}



// Return the chat where we're having an conversation with the given contact.
Chat *ChatMaster::getContactsChat( const QString &handle, bool privateChat )
{
  Chat *result = 0;

  // Look through the chats for an exclusive one with the contact given
  foreach( Chat *chat, chats_ )
  {
    if( chat->isContactInChat( handle, true ) )
    {
      return chat; // best candidate found
    }
    else if( ! privateChat && chat->isContactInChat( handle ) )
    {
      result = chat;  // keep it as a second choice
    }
  }

  return result;
}



// Return the chat where we're having a conversation with the given contacts.
// The privateChat parameter is ignored.
Chat *ChatMaster::getContactsChat( const QStringList &handles, bool privateChat )
{
  // Do nothing if there are no open chats or if the list is empty
  if( handles.isEmpty() || chats_.isEmpty() )
  {
    return 0;
  }

  // If the list only has one contact, just call the single handle version of this method.
  if( handles.count() == 1 )
  {
    return getContactsChat( handles.first(), privateChat );
  }

  Chat *result = 0;

  // Look through the chats for a group chat with all the contacts of the list
  foreach( Chat *chat, chats_ )
  {
    bool found = true;

    // Check every contact in the list to see if they're all present in this chat
    for( QStringList::ConstIterator it = handles.begin(); it != handles.end(); ++it )
    {
      if( ! chat->isContactInChat( *it ) )
      {
        found = false;
        break;
      }
    }

    if( found )
    {
      result = chat;
      break;
    }
  }

  return result;
}



// Return the chat connection where we're having an conversation with the given contact.
MsnSwitchboardConnection *ChatMaster::getContactSwitchboardConnection( const QString &handle, bool doRequirePrivateChat )
{
  foreach( MsnSwitchboardConnection *connection, switchboardConnections_ )
  {
    if( ! connection->isContactInChat( handle ) )
    {
      continue;
    }

    // If the wanted contact is in this chat, it's 99% sure this is the one we want, except when we want a private chat, but this is not
    if( ! connection->isExclusiveChatWithContact( handle ) && doRequirePrivateChat )
    {
      continue;
    }

    return connection;
  }

  return 0;
}



// Initialize the class
bool ChatMaster::initialize()
{
  if ( initialized_ )
  {
    kmDebug() << "Already initialized.";
    return false;
  }

  // Save the current account reference
  currentAccount_ = CurrentAccount::instance();

  // Get the tabbed chat settings
  chatTabbedMode_ = currentAccount_->getTabbedChatMode();

  // Connect the contact list
  const ContactList *contactList = currentAccount_->getContactList();
  connect( contactList, SIGNAL(     contactChangedMsnObject(Contact*) ),
           this,        SLOT  ( slotContactChangedMsnObject(Contact*) ) );

  // Update chat grouping when settings are changed
  connect( currentAccount_, SIGNAL(    changedChatStyleSettings() ),
           this,            SLOT  (          updateChatGrouping() ) );
  // If the user has added him/herself as a contact, we need to update its picture
  // when a new display picture is set
  connect( currentAccount_, SIGNAL(            changedMsnObject() ),
           this,            SLOT  ( slotContactChangedMsnObject() ) );

  initialized_ = true;
  return true;
}



// Check whether the contact is in any of the existing chat windows.
bool ChatMaster::isContactInChat( const QString &handle )
{
  // Find the chat where we have an exclusive chat with the given contact
  return ( getContactsChat( handle, false ) != 0 );
}



// Verify if a new chat can be opened and requests a switchboard
void ChatMaster::requestChat( QString handle )
{
  ChatInformation::ConnectionType connectionType;

  // Forbid chatting with yourself
  if( handle == currentAccount_->getHandle() )
  {
    kmWarning() << "Cannot chat with yourself.";
    return;
  }

  // Look through the chats for an exclusive chat with the given contact
  Chat *chat = getContactsChat( handle, true );

  // No need to open another chat, notify about the presence of the existing one.
  if( chat != 0 )
  {
    raiseChat( chat, true );
    return;
  }

  // Get the contact, see if it's online
  CurrentAccount *currentAccount = currentAccount_;
  ContactBase *contact = currentAccount->getContactByHandle( handle );

  // TODO we shouldn't decide if the chat will be in offline mode,
  // we should see if the CAL return value from SB is 217
  if( contact && contact->getStatus() == STATUS_OFFLINE )
  {
    // Either the contact is offline or invisible.
    // We'll start an offline chat.
    connectionType = ChatInformation::CONNECTION_OFFLINE;

#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "Starting an offline chat with " << handle;
#endif
  }
  else
  {
    // Start a normal chat with an online contact
    connectionType = ChatInformation::CONNECTION_CHAT;

#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "Requesting a switchboard to chat with " << handle;
#endif
  }

  MsnSwitchboardConnection *switchboard = getContactSwitchboardConnection( handle, true );
  if( switchboard != 0 )
  {
    // The user request chat with user that have already opened one SB
    // So, re-use the SB and force the raise of chat!
    createChat( switchboard, true );
    return;
  }

  // Append the requested chat with handle to the list
  // So when the chat will be created it will be raised
  requestedChats_.append( handle );
  emit requestSwitchboard( handle, connectionType );
}



// A chat is closing
void ChatMaster::slotChatClosing( Chat *chat )
{
#ifdef KMESSDEBUG_CHATMASTER
  kmDebug() << "Marking chat as closing.";
#endif

  // Make sure this chat window don't receive new messages.
  closingChats_.append( chat );

  // The contains check avoids deleting references to old switchboards which may be outdated and don't exist anymore.
  MsnSwitchboardConnection *switchboard = chat->getSwitchboardConnection();
  if( switchboard && switchboardConnections_.contains( switchboard ) )
  {
    switchboardConnections_.removeAll( switchboard );
    delete switchboard;
  }
}


// A chat was destroyed
void ChatMaster::slotChatDestroyed( QObject *chatObject )
{
#ifdef KMESSDEBUG_CHATMASTER
  kmDebug() << "Removing chat from list.";
#endif

  // Remove from the list
  Chat *chat = static_cast<Chat*>( chatObject );

  chats_       .removeAll( chat );
  closingChats_.removeAll( chat );
}


// A chat window was destroyed
void ChatMaster::slotChatWindowDestroyed(QObject *chatWindow)
{
#ifdef KMESSDEBUG_CHATMASTER
  kmDebug() << "Removing chat window from list.";
#endif

  // Remove from the list
  ChatWindow *window = static_cast<ChatWindow*>( chatWindow );

  chatWindows_.removeAll( window );
}



// A new application was created for a contact.
void ChatMaster::slotConnectApplication(Application *application)
{
#ifdef KMESSDEBUG_CHATMASTER
  kmDebug() << "Connecting application signals.";
#endif

  connect( application, SIGNAL(       applicationMessage(const ChatMessage&)            ),
           this,          SLOT(       showSpecialMessage(const ChatMessage&)            ) );
  connect( application, SIGNAL( updateApplicationMessage(const QString&,const QString&) ),
           this,        SIGNAL( updateApplicationMessage(const QString&,const QString&) ) );

  // Special connections for applications which were started dynamically to handle an incoming data stream.
  if( application->getMode() == Application::APP_MODE_DATACAST )
  {
    if( qobject_cast<InkTransferP2P*>( application ) != 0 )
    {
#ifdef KMESSDEBUG_CHATMASTER
      kmDebug() << "Connecting additional signals for InkTransferP2P.";
#endif

      connect( application, SIGNAL(       inkReceived(const QString&,const QString&,InkFormat) ),
               this,          SLOT( slotGotInkMessage(const QString&,const QString&,InkFormat) ));
    }
    else
    {
      kmWarning() << "Unknown datacast application: " << application->metaObject()->className();
    }
  }
}



// Append the message to the queue, waiting to be delivered
void ChatMaster::queueMessage( const MimeMessage &message, const QString &handle, bool privateChatRequired )
{
#ifdef KMESSDEBUG_CHATMASTER
  kmDebug() << "Adding message for '" << handle << "' to the queue "
            << "(size=" << ( pendingMimeMessages_.count() + 1 ) << ")." << endl;
#endif
  // NOTE: Store these pending in ApplicationList class instead?

  PendingMimeMessage *pending = new PendingMimeMessage;
  pending->handle  = handle;
  pending->message = message;
  pending->privateChatRequired = privateChatRequired;

  // No active connection found.
  // Request a new chat with the contact, keep the message pending.
  pendingMimeMessages_.append( pending );
}



// Raise an existing chat window. Forcing a raise is reasonable only when the user requests to, since it makes the window
// to grab keyboard focus and be raised on top of other windows.
void ChatMaster::raiseChat( Chat *chat, bool force )
{
  // Check if the pointer is valid
  if( chat == 0 || ! chats_.contains( chat ) )
  {
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "Invalid raising request received; chat is probably already closed.";
#endif
    return;
  }

  ChatWindow *chatWindow = chat->getChatWindow();
  if( chatWindow == 0 || ! chatWindows_.contains( chatWindow ) )
  {
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "Invalid raising request received; chat is probably already closed.";
#endif
    return;
  }

  if( chatWindow->isVisible() && ! force )
  {
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "Window has focus already.";
#endif
    return;
  }

  // Move the window on the current desktop if it wasn't shown before or if we're forcing its raise
  // TODO: make this optional, maybe the user doesn't want the windows to span over desktops
  if( ! chatWindow->isVisible() || force )
  {
    KWindowSystem::setOnDesktop( chatWindow->winId(), KWindowSystem::currentDesktop() );
  }

  // Making windows raise and get focus interrupts the user... it's better to make them appear minimized,
  // and let them just flash in the taskbar.
  if( ! force )
  {
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "Showing minimized window.";
#endif
    // Required for new chats
    chatWindow->showMinimized();
    return;
  }

  // Activate the selected tab
  chatWindow->setCurrentChat( chat );

  // Notify about the new window and force it to show and receive focus. Only use upon user request.

#ifdef KMESSDEBUG_CHATMASTER
  kmDebug() << "Forcing window raise.";
#endif

  // Show the chat's window
  if( chatWindow->isMinimized() )
  {
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "Window was minimized, showing it.";
#endif
    chatWindow->showNormal();
  }
  else
  {
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "Window was not minimized, showing it again.";
#endif
    chatWindow->show();
  }

#ifdef Q_WS_X11
  // Bypass focus stealing prevention (code from Quassel)
  QX11Info::setAppUserTime( QX11Info::appTime() );
#endif

  // Workaround a bug in KWin: force the window to get focus
  if( chatWindow->windowState() & Qt::WindowMinimized) {
    chatWindow->setWindowState( (chatWindow->windowState() & ~Qt::WindowMinimized) | Qt::WindowActive );
    chatWindow->show();
  }

  // Set the keyboard focus to the chat window
  chatWindow->raise();
  KWindowSystem::activateWindow( chatWindow->winId() );
  KApplication::setActiveWindow( chatWindow );
}



/**
 * @brief Send all pending mime messages for the contact.
 * @param handle      The contact to send messages for.
 * @param connection  The switchboard connection that can be used to send the messages.
 */
void ChatMaster::sendPendingMimeMessages( const QString &handle, MsnSwitchboardConnection *connection )
{
#ifdef KMESSDEBUG_CHATMASTER
  kmDebug() << "Sending pending messages for contact " << handle << ", if any.";
#endif

  QList<PendingMimeMessage*> sentMessages;

  bool isPrivateChat = (connection->getContactsInChat().size() < 2);

  foreach( PendingMimeMessage *pendingMimeMessage, pendingMimeMessages_ )
  {
    // Ignore messages for other contacts
    if( pendingMimeMessage->handle != handle )
    {
      // Get next message
      continue;
    }

    // Ignore messages that can only be sent on a private chat.
    if( pendingMimeMessage->privateChatRequired && ! isPrivateChat )
    {
#ifdef KMESSDEBUG_CHATMASTER
      kmDebug() << "Switchboard is not a private chat, keeping message in queue.";
#endif

      // Get next message
      continue;
    }

    // Check when the connection is busy again
    if( connection->isBusy() )
    {
#ifdef KMESSDEBUG_CHATMASTER
      kmDebug() << "Switchboard is busy again, keeping remaining messages in the queue.";
#endif
      goto cleanup_return;
    }

    // Check whether the contact is still in the chat.
    if( ! connection->isConnected() || ! connection->getContactsInChat().contains( handle ) )
    {
      // getContactsInChat() may be empty, don't re-invite the last contact to deviver the message
#ifdef KMESSDEBUG_CHATMASTER
      kmDebug() << "Switchboard is not connected or contact left chat, keeping remaining messages in the queue.";
#endif
      goto cleanup_return;
    }

    // Send the message
    connection->sendApplicationMessage( pendingMimeMessage->message );
    sentMessages.append( pendingMimeMessage );
  }


cleanup_return:
  // Do deletion out of the loop, just to be sure.
  foreach( PendingMimeMessage *sentMessage, sentMessages )
  {
    pendingMimeMessages_.removeAll( sentMessage );
    delete sentMessage;
  }
}



// Deliver a chat message to the chat window (can be an Application message or an Offline-IM from MsnNotificationConnection).
void ChatMaster::showSpecialMessage( const ChatMessage &message, const MsnSwitchboardConnection *switchboardChat )
{
#ifdef KMESSDEBUG_CHATMASTER
  kmDebug() << "Delivering application/ink/OIM chat message to chat window.";
#endif

  const QString &handle = message.getContactHandle();

#ifdef KMESSDEBUG_CHATMASTER
  kmDebug() << "Message info, Contact: " << handle << " - Message body: '" << message.getBody() << "' - "
              "Message type: " << message.getType() << " - Message class: " << message.getContentsClass() << "." << endl;
#endif

  Chat *chat;
  if( switchboardChat != 0 )
  {
    chat = getChatBySwitchboard( switchboardChat );
  }
  else
  {
    chat = getContactsChat( handle, true );
  }

  // Deliver the message to the contact's chat window
  if( chat != 0 )
  {
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "Delivering message.";
#endif

    // The chat window is currently closing
    if( closingChats_.contains( chat ) )
    {
      if( message.isNormalMessage() )
      {
        // TODO: Sometimes, you close a window when the other contact sends a new message, and you lose it.
        // Maybe it's possible to avoid the loss, for example with a notification balloon to notify of the message.
        kmWarning() << "Chat window is closing, suppressing message '" << message.getBody() << "' from '" << handle << "'.";
      }
#ifdef KMESSDEBUG_CHATMASTER
      else
      {
        kmDebug() << "Chat window is closing, suppressing message.";
      }
#endif

      return;
    }

    chat->receivedMessage( message );
    return;
  }

  // No window is available - we should check if it's needed to create a new window, or if the message
  // can be safely ignored.
   switch( message.getContentsClass() )
  {
    case ChatMessage::CONTENT_MESSAGE:
    case ChatMessage::CONTENT_MESSAGE_INK:
    case ChatMessage::CONTENT_NOTIFICATION_NUDGE:
    case ChatMessage::CONTENT_NOTIFICATION_WINK:
    case ChatMessage::CONTENT_APP_INVITE:
#ifdef KMESSDEBUG_CHATMASTER
      kmDebug() << "Message is important, delivering it.";
#endif
      break;

    default:
#ifdef KMESSDEBUG_CHATMASTER
      kmDebug() << "Message is not important, will be ignored.";
#endif
      return;
  }


  // Check if a switchboard is present
  MsnSwitchboardConnection *switchboard = getContactSwitchboardConnection( handle, true );
  if( switchboard == 0 )
  {
    // Create a new offline switchboard connection to deliver the message. It can be converted to an
    // online switchboard one if needed.
    switchboard = startSwitchboard( ChatInformation( 0, handle, "", 0, "", "", ChatInformation::CONNECTION_OFFLINE ) );
    if( switchboard == 0 )
    {
#ifdef KMESSDEBUG_CHATMASTER
      kmDebug() << "Could not create an offline switchboard.";
#endif
      return;
    }
  }

  // Create a new chat window for the message
  chat = createChat( switchboard );
  if( chat == 0 )
  {
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "Could not create a chat window.";
#endif
    return;
  }

  // Deliver the message to the new chat
  chat->receivedMessage( message );
}



// Display an MSN Object that was received.
void ChatMaster::showMsnObject( const QString &handle, const MsnObject &msnObject, Chat *chat )
{
  QString fileName;
  QString emoticonCode;

  // Get the contact
  // Note I don't use msnObject.getCreator, because I don't want to trust the other client!
  ContactBase *contact = 0;
  Contact     *msnContact = 0;

  // Get msn object filename
  fileName = KMessConfig::instance()->getMsnObjectFileName( msnObject );

  // Handle the picture
  switch( msnObject.getType() )
  {
    case MsnObject::DISPLAYPIC:
      // Get the real MSN contact from the MSN contact list.
      // Picture is stored in the ContactExtension because it's not an official property of the contact (not stored server-side).
      msnContact = currentAccount_->getContactList()->getContactByHandle(handle);
      if(KMESS_NULL(msnContact)) return;
      msnContact->getExtension()->setContactPicturePath( fileName );
      pendingDisplayPictures_.removeAll( handle );
      break;

    case MsnObject::WINK:
      // Show the Wink in the chat window where the original datacast message came from.
      if(KMESS_NULL(chat)) return;
      chat->showWink( handle, fileName, msnObject.getFriendly() );
      break;

    case MsnObject::EMOTICON:
      // Inform the contact that a custom emoticon can be parsed.
      contact = currentAccount_->getContactByHandle(handle);
      if(KMESS_NULL(contact)) return;
      emoticonCode = contact->getEmoticonCode(msnObject.getDataHash());  // always put before addEmoticonFile()!
      contact->addEmoticonFile( msnObject.getDataHash(), fileName );

      // Update all chats where the contact is present
      foreach( Chat *chatUpdate, chats_ )
      {
        if( chatUpdate->isContactInChat( handle ) )
        {
          chatUpdate->updateCustomEmoticon( handle, emoticonCode );
        }
      }
      break;

    case MsnObject::BACKGROUND:
      // handle background transfers

    default:
      kmWarning() << "Unable to handle MsnObject pictures of type '" << msnObject.getType() << "'.";
  }
}



// Determine what to do when a contact changed it's picture.
void ChatMaster::slotContactChangedMsnObject( Contact *contact )
{
  // Avoid trying to download our own display picture, if the user has added him/herself;
  // instead simply use the current one.
  // This method can be called to update the user's own picture in list from two places:
  // 1)on login when the contact is first received, 2)when the user changes the display pic.
  if( contact == 0 )
  {
    contact = currentAccount_->getContactList()->getContactByHandle( currentAccount_->getHandle() );

    // When the connection starts, this method gets called
    // but the contact list is not ready yet. Cancel.
    if( contact == 0 )
    {
      return;
    }
  }
  if( contact->getHandle() == currentAccount_->getHandle() )
  {
    contact->getExtension()->setContactPicturePath( currentAccount_->getPicturePath() );
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "Updated user's own display picture";
#endif
    return;
  }

  QString handle( contact->getHandle() );

  // If the contact no longer likes to display an msn object, remove it
  if( contact->getMsnObject() == 0 )
  {
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "Contact '" << handle << "' removed its MSN object";
#endif

    contact->getExtension()->setContactPicturePath( QString::null );
    return;
  }

  MsnObject msnObject( *contact->getMsnObject() );

  // See if the file already exists.
  QString objectFileName( KMessConfig::instance()->getMsnObjectFileName( msnObject ) );
  if( QFile::exists( objectFileName ) )
  {
    bool imageInvalid = QImageReader::imageFormat( objectFileName ).isEmpty();
    if( ! imageInvalid && msnObject.verifyFile( objectFileName ) )
    {
#ifdef KMESSDEBUG_CHATMASTER
      kmDebug() << "Picture for '" << handle << "' is already downloaded.";
#endif

      contact->getExtension()->setContactPicturePath( objectFileName );
      return;
    }
    else
    {
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "Picture for '" << handle
              << "' is already downloaded, but corrupt "
              << ( imageInvalid ? "(detected by QImageReader)" : "(detected by MsnObject)" )
              << ", deleting it: '" << objectFileName << "'" << endl;
#endif
      QFile::remove( objectFileName );

      // If the file was set as picture. reset right away.
      if( contact->getExtension()->getContactPicturePath() == objectFileName )
      {
#ifdef KMESSDEBUG_CHATMASTER
        kmWarning() << "Corrupt picture was already set, "
                    << "resetting contact picture." << endl;
#endif
        contact->getExtension()->setContactPicturePath( QString::null );
      }
    }
  }

  // If the contact is active in a chat,
  // download the new picture straight away.
  MsnSwitchboardConnection *switchboard = getContactSwitchboardConnection( handle, true );
  if( switchboard != 0 )
  {
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "Contact '" << handle << "' changed its MSN object.";
#endif

    // Automatically decides which switchboard is the best to use,
    // this may change during the transfer.
    startMsnObjectDownload( handle, &msnObject, 0 );
    return;
  }


  // Queue a request to download it later in the background.
  // That method will check for the prerequisites to download it,
  // since that may change between this point and the timedUpdate() call.

#ifdef KMESSDEBUG_CHATMASTER
  kmDebug() << "Contact '" << handle << "' changed its MSN object, queueing a request for it.";
#endif

  // Request the contact's msnobject
  if( ! pendingDisplayPictures_.contains( handle ) )
  {
    pendingDisplayPictures_.append( handle );
  }
}



// A contact joined to one of our switchboard sessions.
void ChatMaster::slotContactJoinedChat( ContactBase *contact )
{
  // Get switchboard connection from slot sender
  MsnSwitchboardConnection *connection = static_cast<MsnSwitchboardConnection*>( const_cast<QObject*>( sender() ) );
  if(KMESS_NULL(connection)) return;

  QString handle( contact->getHandle() );

  // Send any pending messages, if any
  if( pendingMimeMessages_.count() > 0 )
  {
    // Find the chat window where the contact is the only participant.
    if( connection->getContactsInChat().size() > 1 )
    {
      // Contact joined chat conversation with multiple partipipants, ignore this.
#ifdef KMESSDEBUG_CHATMASTER
      kmDebug() << "Contact '" << handle << "' joined multi-chat, not sending pending messages.";
#endif
    }
    else
    {
#ifdef KMESSDEBUG_CHATMASTER
      kmDebug() << "Contact '" << handle << "' joined chat, checking for pending messages.";
#endif

      // Send the messages
      if( pendingMimeMessages_.count() > 0 )
      {
        sendPendingMimeMessages( handle, connection );
      }
    }
  }


  // Check whether the contact picture is outdated.
  // Use getContactByHandle() from the MSN contact list to get the msn object.
  const Contact *fullContact = currentAccount_->getContactList()->getContactByHandle( handle );
  if( fullContact != 0 && fullContact->hasP2PSupport() )
  {
    if( fullContact->getMsnObject() != 0 )
    {
      startMsnObjectDownload( fullContact->getHandle(), fullContact->getMsnObject(), 0 );
    }
  }
}



/**
 * @brief Deliver an application command to the correct application.
 */
void ChatMaster::slotDeliverAppCommand(QString cookie, QString handle, QString command)
{
#ifdef KMESSDEBUG_CHATMASTER
  kmDebug() << "Delivering application command.";
#endif

  Application *app = 0;

  // Get the contact.
  ContactBase *contact = currentAccount_->getContactByHandle(handle);
  if(KMESS_NULL(contact)) return;

  // Get the application list
  if( contact->hasApplicationList() )
  {
    app = contact->getApplicationList()->getApplicationByCookie(cookie);
  }

  // App not found.
  if( app == 0 )
  {
    kmWarning() << "Application not found to deliver user command (contact=" << contact << ", command=" << command << ").";
    return;
  }

  // Deliver the 'accept' or 'cancel' command.
  app->gotCommand( command );
}



/**
 * @brief Deliver a message from an Application to the switchboard connection.
 * @param message The Mime message to deliver. The message can contain a normal MIME or binary P2P payload.
 * @param handle  The contact the message should be delivered to.
 * @param privateChatRequired  Whether a private chat is required to deliver the message. If not, a multi-chat will be used when available.
 *
 * This method automatically determines which switchboard is should use to deliver the message.
 * When the contact is not present in any of the existing switchboard connections, a new connection request will be made.
 * The message will be queued for delivery to the switchboard so it can be delivered once it's available.
 * Conversations with multiple contacts are avoided because of the networking overhead;
 * since the switchboard is a broadcast channel, each participant receives the message.
 *
 * Unlike the direct connection link, the switchboard is only capable of transferring mime messages.
 * To transfer the P2P data, the P2PApplication has to wrap the message as MimeMessage payload.
 */
void ChatMaster::slotDeliverMimeMessage(const MimeMessage &message, const QString &handle, bool privateChatRequired)
{
#ifdef KMESSDEBUG_CHATMASTER
  kmDebug() << "Delivering application message for '" << handle << "' to switchboard.";
#endif
#ifdef KMESSTEST
  QString contentType( message.getValue("Content-Type") );
  KMESS_ASSERT( contentType == "application/x-msnmsgrp2p"
       || contentType == "text/x-msmsgsinvite"
       || contentType.section(';', 0, 0) == "text/x-msmsgsinvite" );  // for ; charset= suffix
  if( contentType == "application/x-msnmsgrp2p" )
  {
    KMESS_ASSERT( message.getValue("P2P-Dest") == handle );
  }
#endif

  MsnSwitchboardConnection *connection = getContactSwitchboardConnection(handle, privateChatRequired);

  if( connection != 0 )
  {
    if( connection->isBusy() )
    {
      // Connection has unacked messages, keep waiting until the switchboard is ready to send
#ifdef KMESSDEBUG_CHATMASTER
      kmDebug() << "Switchboard is currently busy, queueing message until the switchboard is ready to send.";
#endif
      queueMessage( message, handle, privateChatRequired );

      // Pause application so it won't continue to send much more messages (see P2PApplication::slotSendData()).
      ApplicationList *appList = getApplicationList(handle);
      if( appList != 0 )
      {
        appList->pauseApplications();
      }
    }
    else
    {
      // Deliver the message
      connection->sendApplicationMessage(message);
    }
  }
  else
  {
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "No switchboard available to deliver message,"
              << " requesting chat and queueing message " << (pendingMimeMessages_.count() + 1) << "." << endl;
#endif

    emit requestSwitchboard( handle, ChatInformation::CONNECTION_BACKGROUND );
    queueMessage( message, handle, privateChatRequired );
  }
}



// Parse an gif ink message
void ChatMaster::slotGotInkMessage( const QString &ink, const QString &contactHandle, InkFormat format )
{
#ifdef KMESSDEBUG_SWITCHBOARD_GENERAL
  switch( format )
  {
    case FORMAT_ISF: kmDebug() << "Parsing ISF ink message"; break;
    case FORMAT_GIF: kmDebug() << "Parsing GIF ink message"; break;
  }
#endif

  // Get the contact data for the shown messages
  QString friendlyName;
  QString contactPicture;
  const ContactBase *contact = currentAccount_->getContactByHandle( contactHandle );
  if( contact != 0 )
  {
    friendlyName   = contact->getFriendlyName( STRING_ORIGINAL );
    contactPicture = contact->getContactPicturePath();
  }

#if KMESS_ENABLE_INK == 0
  // Receiving Ink is supported only for GIF, as GIF reading is always built-in into Qt
  // and KHTML can show GIF images without any extra libs
  if( format != FORMAT_GIF )
  {
    showSpecialMessage( ChatMessage( ChatMessage::TYPE_SYSTEM,
                                     ChatMessage::CONTENT_SYSTEM_ERROR,
                                     true,
                                     i18nc( "Error message shown in chat, %1 is the name of the contact",
                                            "You received an handwritten message from %1, "
                                            "but it could not be displayed. This version of "
                                            "KMess was built without ISF support.", friendlyName ),
                                     contactHandle,
                                     friendlyName ) );
    return;
  }
#endif


  // Validate the incoming data
  QString inkData = ink.trimmed();
  QRegExp inkChars( "([^A-Za-z0-9:+/=])" );
  if( ! inkData.startsWith( "base64:" ) || inkChars.indexIn( inkData ) != -1 )
  {
    kmWarning() << "Received invalid ink message from" << contactHandle << ", found char 0x" << QString::number( inkChars.cap(1)[0].unicode(), 16 ) << "at position" << inkChars.pos(1);
    kmWarning() << "Ink data dump:" << inkData;

    showSpecialMessage( ChatMessage( ChatMessage::TYPE_SYSTEM,
                                     ChatMessage::CONTENT_SYSTEM_ERROR,
                                     true,
                                     i18nc( "Error message shown in chat, %1 is the name of the contact",
                                            "You received an handwritten message from %1, "
                                            "but it could not be displayed. The data could "
                                            "not be read.", friendlyName ),
                                     contactHandle,
                                     friendlyName ) );
    return;
  }

  QString imageMimeType;
  QByteArray inkEncodedData( inkData.toLatin1() );
  inkEncodedData.replace( "base64:", "" );

  switch( format )
  {
    case FORMAT_ISF:
    {
#if KMESS_ENABLE_INK == 1
      // ISF messages need to be base64-decoded, converted to a picture readable by KHTML,
      // then base64-encoded again.
      // Since I'd rather avoid using giflib to encode ISF data into pictures, I'll use PNG.
      imageMimeType = "image/png";

      Isf::Drawing drawing( Isf::Stream::reader( inkEncodedData, true /* fromBase64 */ ) );
      inkEncodedData = Isf::Stream::writerPng( drawing, true );
#endif
      break;
    }

    case FORMAT_GIF:
      // Yes, this is it, just set the GIF mimetype. The data is already
      // there in the inkEncodedData variable, and KHTML can show inline
      // gif images right away
      imageMimeType = "image/gif";
      break;
  }

  // Use the ink data as inline image

  // Cast the sender object to determine if the ink is received from p2p or switchboard.
  // WLM sends the ink (gif) by p2p protocol for private chat and by mime for multi-chat
  // Other clients send all by mime message, so it's important to determine what switchboard
  // is used, to find the correct chat window where to show the message.
  MsnSwitchboardConnection *switchboard = qobject_cast<MsnSwitchboardConnection *>( sender() );

  // Send the message as HTML to the chat window.
  // Using data url scheme for embedded images (src="data:image/gif;base64,fdsfsdfsd")
  showSpecialMessage( ChatMessage( ChatMessage::TYPE_INCOMING,
                                   ChatMessage::CONTENT_MESSAGE_INK,
                                   true,
                                   "<img src=\"data:" + imageMimeType + ";"
                                   "base64," + Qt::escape( inkEncodedData ) + "\">",
                                   contactHandle,
                                   friendlyName,
                                   contactPicture ), switchboard );
}



// The switchboard received a Mime message
void ChatMaster::slotGotMessage(const MimeMessage &message, const QString &handle)
{
  ApplicationList *appList = getApplicationList(handle);
  if( appList != 0 )
  {
    appList->gotMessage(message);
  }
}



// The switchboard received a P2P message
void ChatMaster::slotGotMessage(const P2PMessage &message, const QString &handle)
{
  ApplicationList *appList = getApplicationList(handle);
  if( appList != 0 )
  {
    appList->gotMessage(message);
  }
}



// The switchboard received an msn object
void ChatMaster::slotGotMsnObject(const QString &msnObjectData, const QString &handle)
{
  // Get the contact.
  ContactBase *contact = currentAccount_->getContactByHandle(handle);
  if(KMESS_NULL(contact)) return;

  // Get the switchboard connection where the emoticon/wink/voiceclip was posted
  const MsnSwitchboardConnection *connection = static_cast<const MsnSwitchboardConnection*>( sender() );
  Chat *chat = 0;
  if( connection != 0 )
  {
    chat = getChatBySwitchboard( connection );
  }

  // Parse the msn object
  MsnObject msnObject(msnObjectData);

  // Get the contact name
  QString friendlyName( currentAccount_->getContactFriendlyNameByHandle( handle, STRING_CLEANED_ESCAPED ) );

  // Determine the statusmessage to display.
  QString statusMessage;
  MsnObject::MsnObjectType objectType = msnObject.getType();
  if( objectType == MsnObject::WINK )
  {
    statusMessage = i18n( "%1 is sending a wink: &quot;%2&quot;", friendlyName, msnObject.getFriendly() );
  }

  // Show the status message
  if( ! statusMessage.isEmpty() )
  {
    if( ! KMESS_NULL(connection) && ! KMESS_NULL(chat) )
    {
      chat->getChatWindow()->showStatusMessage( statusMessage );
    }
  }

  startMsnObjectDownload( handle, &msnObject, chat );
}



// A msn object (picture, wink, emoticon) was received for the contact.
void ChatMaster::slotMsnObjectReceived(const QString &handle, const MsnObject &msnObject)
{
#ifdef KMESSDEBUG_CHATMASTER
  kmDebug() << "Received msn object from: " << handle << ".";
#endif

  Chat *chat = 0;

  // Get sender of this signal, slotGotMsnObject() assigned this to the p2papp.
  const P2PApplication *application = static_cast<const P2PApplication*>( sender() );
  if( application != 0 )
  {
    chat = application->getChat();
    if( chat != 0 && ! chats_.contains( chat ) )
    {
#ifdef KMESSDEBUG_CHATMASTER
      kmDebug() << "Original chat not found for received MSNObject "
                << "(objecttype=" << msnObject.getType()
                << " contact="    << handle << ")." << endl;
#endif
      chat = getContactsChat( handle, true );
    }
  }

  showMsnObject( handle, msnObject, chat );
}



// The switchboard is ready to send more messages.
void ChatMaster::slotSwitchboardReady()
{
  // No need to send pending messages or resume an application (a message is always queued before the app is paused).
  if( pendingMimeMessages_.isEmpty() )
  {
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "A switchboard is ready to send more messages, no messages pending.";
#endif
    return;
  }

  // Get the connection
  MsnSwitchboardConnection *connection = static_cast<MsnSwitchboardConnection*>( const_cast<QObject*>( sender() ) );
  if(KMESS_NULL(connection)) return;

  // Get the contacts
  const QStringList &contacts = connection->getContactsInChat();
  bool isPrivateChat          = (contacts.size() < 2);

#ifdef KMESSDEBUG_CHATMASTER
  kmDebug() << "A switchboard is ready to send more messages, " << pendingMimeMessages_.count() << " messages pending.";
#endif

  // Send all pending messages
  sendPendingMimeMessages( contacts[0], connection );

  // See if the switchboard is still ready.
  // Don't use connection->isBusy() as it will break with
  // the test above when exactly all messages are sent before the connection became busy.
  if( ! pendingMimeMessages_.isEmpty() )
  {
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "Switchboard is busy again, not notifying applications.";
#endif
    return;
  }

  // A switchboard is still available.
  // If the contact applications were paused, resume them now.
  ContactBase *contact = currentAccount_->getContactByHandle( contacts[0] );
  if( contact != 0 && contact->hasApplicationList() )
  {
    contact->getApplicationList()->resumeApplications(isPrivateChat);
  }
}



// Delete an existing switchboard
void ChatMaster::slotSwitchboardDelete( MsnSwitchboardConnection *closing, bool deleteObject )
{
  // Remove from collection
  switchboardConnections_.removeAll( closing );

  // Do not delete the object: this method can be called as slot from the switchboard's destructor
  if( ! deleteObject )
  {
    return;
  }

  // Disconnect and destroy
  disconnect( closing, 0, this, 0 );
  closing->deleteLater();
}



// Configure and start the Mime application object.
void ChatMaster::startApplication( MimeApplication *application )
{
  // Get the application list.
  ApplicationList *appList = getApplicationList( application->getContactHandle() );
  if(KMESS_NULL(appList)) return;

  // Add to list
  appList->addApplication( application );   // requires MimeApplication/P2PApplication type.

  // Start application
  application->start();
}



// Configure and start the P2P application object.
void ChatMaster::startApplication( P2PApplication *application )
{
  // Get the application list.
  ApplicationList *appList = getApplicationList( application->getContactHandle() );
  if(KMESS_NULL(appList)) return;

  // Add to list.
  appList->addApplication( application );   // requires MimeApplication/P2PApplication type.

  // Start application
  application->start();
}



// Start a connection with the information gathered from the Notification connection
MsnSwitchboardConnection *ChatMaster::startSwitchboard( const ChatInformation &chatInfo )
{
  MsnSwitchboardConnection *switchboard = 0;
  const QString &handle = chatInfo.getContactHandle();

#ifdef KMESSDEBUG_CHATMASTER
  kmDebug() << "Starting new switchboard session with" << handle;
#endif

  /**
   * Refresh requests are made by expired/disconnected switchboard sessions, and they are refreshed
   * with the new connection data.
   * All other types of chat request create a new switchboard session from the chat info.
   * We could attach the newly created SB to a chat window, but we don't know what kind of chat it
   * may be at this point (the list of initial participants will be received later, when the SB
   * connection will be established). Therefore, we just don't attach it to any chat, and so we
   * posticipate the decision to later.
   * (Also remember that you may have more than one chat with a contact, a 1-on-1, a group one,
   * and a data transfer one)
   */
  if( chatInfo.getType() == ChatInformation::CONNECTION_REFRESH )
  {
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "Finding the session to refresh.";
#endif
    foreach( MsnSwitchboardConnection *currentSwitchboard, switchboardConnections_ )
    {
      if( currentSwitchboard->getContactsInChat().contains( handle )
      &&  currentSwitchboard->isWaiting() )
      {
#ifdef KMESSDEBUG_CHATMASTER
        kmDebug() << "Session found, it will be refreshed.";
#endif
        switchboard = currentSwitchboard;
        break;
      }
    }

    if( switchboard == 0 )
    {
      kmWarning() << "Unable to find which session requested a refresh.";
      return 0;
    }
  }
  else
  {
    switchboard = createSwitchboardConnection();
    if( KMESS_NULL(switchboard) ) return 0;
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "Creating new session.";
#endif
  }

  // If this switchboard is a background one, made to request a contact's picture,
  // we can remove the pending request from the list now.
  if( chatInfo.getType() == ChatInformation::CONNECTION_BACKGROUND
  &&  pendingDisplayPictures_.contains( handle ) )
  {
    pendingDisplayPictures_.removeAll( handle );
  }

  // Connect to the new server
  switchboard->start( chatInfo );

  Chat *chat = getChatBySwitchboard( switchboard );
  if( chat )
  {
    chat->setEnabled( true );
  }

  return switchboard;
}



// Start a chat with the information gathered from a switchboard connection
Chat *ChatMaster::createChat( MsnSwitchboardConnection *switchboard, bool requestedByUser )
{
#ifdef KMESSTEST
  KMESS_ASSERT( switchboard != 0 );
#endif

  Chat       *newChat;
  ChatWindow *newChatWindow;

  // If the new chat is a group chat, find existing group chat windows; else find private chats
  const QStringList partecipants( switchboard->getContactsInChat() );
  newChat = getContactsChat( partecipants, true );

  // If the chat was requested by a contact, check if we have a chat request
  // pending for that contact and remove it
  if( ! requestedByUser && partecipants.count() < 2 )
  {
    const QString& handle = partecipants.first();
    // Also change the request source to the user if we asked a chat with this contact first
    requestedByUser = requestedChats_.contains( handle );
    if( requestedByUser )
    {
      // Remove the request
      requestedChats_.removeAll( handle );
    }
  }

  if( newChat != 0 )
  {
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "A chat with contacts"
              << switchboard->getContactsInChat().join(",") << "already exists, raising it." << endl;
#endif

    // The chat may have been disabled, reenable it just in case.
    // Seen happening with group chats where you get reinvited to after a reconnection.
    newChat->setEnabled( true );

    // Delete the previous switchboard ( if it's different from the current one ) and attach the new
    MsnSwitchboardConnection *oldConnection = newChat->getSwitchboardConnection();
    if( oldConnection != switchboard )
    {
      newChat->setSwitchboardConnection( switchboard );
      slotSwitchboardDelete( oldConnection, true );
    }

    // If we've requested a chat window, raise it forcing it open over other KMess' windows;
    // if some contact wants to chat with us, the chat window will open minimized.
    raiseChat( newChat, requestedByUser );
    return newChat;
  }

  // Create the chat widget
  newChat = new Chat();

  if( newChat == 0 )
  {
    kmDebug() << "Couldn't create a new chat tab.";
    return 0;
  }

  // Initialize it
  if( ! newChat->initialize( switchboard ) )
  {
    kmDebug() << "Couldn't initialize the chat tab.";
    return 0;
  }

  // Select the chat window where to open the new tab, or create a new one
  newChatWindow = createChatWindow( newChat );

  // Create the first tab in the chat
  newChat = newChatWindow->addChatTab( newChat, switchboard->getUserStartedChat() );
  if( newChat == 0 )
  {
    kmWarning() << "Couldn't add the initial chat.";
    return 0;
  }

  // Connect the chat's request signals
  connect( newChat, SIGNAL(           gotChatMessage(const ChatMessage&,Chat*)      ),
           this,    SIGNAL(           newChatMessage(const ChatMessage&,Chat*)      ) );
  connect( newChat, SIGNAL(        contactAllowed(QString)                       ),
           this,    SIGNAL(             allowContact(QString)                       ) );
  connect( newChat, SIGNAL(               addContact(QString)                       ),
           this,    SIGNAL(               addContact(QString)                       ) );
  connect( newChat, SIGNAL(             contactAdded(QString,bool)                  ),
           this,    SLOT  (      forwardContactAdded(QString,bool)                  ) );
  connect( newChat, SIGNAL(           contactBlocked(QString,bool)                  ),
           this,    SLOT  (    forwardContactBlocked(QString,bool)                  ) );
  connect( newChat, SIGNAL(         startPrivateChat(const QString&)                ),
           this,    SLOT  (                   requestChat(QString)                  ) );
  connect( newChat, SIGNAL(               appCommand(QString,QString,QString)       ),
           this,    SLOT  (    slotDeliverAppCommand(QString,QString,QString)       ) );
  connect( newChat, SIGNAL(      requestFileTransfer(const QString&,const QString&) ),
           this,    SLOT  (        startFileTransfer(const QString&,const QString&) ) );
  connect( newChat, SIGNAL(                  closing(Chat*)                         ),
           this,    SLOT  (          slotChatClosing(Chat*)                         ) );
  connect( newChat, SIGNAL(                destroyed(QObject*)                      ),
           this,    SLOT  (        slotChatDestroyed(QObject*)                      ) );
  connect( this,    SIGNAL( updateApplicationMessage(const QString&,const QString&) ),
           newChat, SIGNAL( updateApplicationMessage(const QString&,const QString&) ) );

  // Add the new chat to our list
  chats_.append( newChat );

  // Finally, start the new chat
  newChat->startChat();

  // If we've requested a chat window, raise it forcing it open over other KMess' windows;
  // if some contact wants to chat with us, the chat window will open minimized.
  raiseChat( newChat, requestedByUser );

  return newChat;
}



// Create a new chat window or retrieve an existing one if tabbed chatting is enabled
ChatWindow *ChatMaster::createChatWindow( Chat *chat )
{
  // Search a window for the chat according to the users grouping settings
  ChatWindow *window = findWindowForChat( chat );

  // Found a window to use, return that
  if( window != 0 )
  {
    return window;
  }

  // Create the new chat window
#ifdef KMESSDEBUG_CHATMASTER
  kmDebug() << "No existing chat window, creating new.";
#endif
  // Create the window: it should have no parent, so it will be displayed in the taskbar for Windows.
  window = new ChatWindow();

  // Bail out if the window could not be initialized
  if( ! window->initialize() )
  {
    kmWarning() << "Couldn't initialize the new chat window!";
    return 0;
  }

  // Connect its signals
  connect( window, SIGNAL(               destroyed(QObject*) ),
           this,   SLOT  ( slotChatWindowDestroyed(QObject*) ) );
  connect( window, SIGNAL(               reconnect()         ),
           this,   SIGNAL(               reconnect()         ) );

  // Add the new chat window to our list
  chatWindows_.append( window );

  return window;
}



// Create and register a new switchboard
MsnSwitchboardConnection *ChatMaster::createSwitchboardConnection( MsnSwitchboardConnection *replace, QString handle )
{
  MsnSwitchboardConnection *switchboard;

#ifdef KMESSDEBUG_CHATMASTER
  kmDebug() << "Creating a new switchboard connection.";
#endif

  // Initialize the switchboard connection
  if( replace != 0 )
  {
    switchboard = new MsnSwitchboardConnection( *replace );
  }
  else
  {
    switchboard = new MsnSwitchboardConnection();
  }
  if( ! switchboard || ! switchboard->initialize( handle ) )
  {
    kmWarning() << "Couldn't initialize switchboard connection.";
    delete switchboard;
    return 0;
  }

  switchboardConnections_.append( switchboard );

  // Connect the switchboard's signals to the Chat Master.
  connect(switchboard, SIGNAL(            contactJoinedChat(ContactBase*)                       ),
          this,        SLOT  (        slotContactJoinedChat(ContactBase*)                       ));
  connect(switchboard, SIGNAL(                   gotMessage(const MimeMessage&, const QString&) ),
          this,        SLOT  (               slotGotMessage(const MimeMessage&, const QString&) ));
  connect(switchboard, SIGNAL(                   gotMessage(const P2PMessage&,  const QString&) ),
          this,        SLOT  (               slotGotMessage(const P2PMessage&,  const QString&) ));
  connect(switchboard, SIGNAL(                gotInkMessage(const QString&,     const QString&,InkFormat) ),
          this,        SLOT  (            slotGotInkMessage(const QString&,     const QString&,InkFormat) ));
  connect(switchboard, SIGNAL(                 gotMsnObject(const QString&,     const QString&) ),
          this,        SLOT  (             slotGotMsnObject(const QString&,     const QString&) ));
  connect(switchboard, SIGNAL(                    readySend()                                   ),
          this,        SLOT  (         slotSwitchboardReady()                                   ));
  connect(switchboard, SIGNAL(        requestNewSwitchboard(QString)                            ),
          this,        SLOT  ( forwardRequestNewSwitchboard(QString)                            ));
  connect(switchboard, SIGNAL(            requestChatWindow(MsnSwitchboardConnection*)          ),
          this,        SLOT  (                   createChat(MsnSwitchboardConnection*)          ));
  connect(switchboard, SIGNAL(                     deleteMe(MsnSwitchboardConnection*)          ),
          this,        SLOT  (        slotSwitchboardDelete(MsnSwitchboardConnection*)          ));

  return switchboard;
}



// Start a file transfer with the information from the Chat
void ChatMaster::startFileTransfer( const QString &handle, const QString &filename )
{
  ApplicationList *appList = getApplicationList(handle);
  if(KMESS_NULL(appList)) return;

  // Get the contact properties, see how we can transfer the file.
  const ContactBase *contact = currentAccount_->getContactByHandle( handle );

  // If the contact is offline (that is, not online nor invisible), refuse to start the file transfer
  MsnSwitchboardConnection *connection = getContactSwitchboardConnection( handle, true );
  if( ! connection || ( contact && contact->isOffline() && connection->isInactive() ) )
  {
//     kmWarning() << "Attempted to start a file transfer with an offline contact!";
    return;
  }

  if( contact == 0 || ! contact->hasP2PSupport() )
  {
    // This should be so rare that it justifies a warning message.
    if( contact == 0 )
    {
      kmWarning() << "Contact" << handle << "object not found, using old MIME invitations because P2P might not be supported.";
    }
    else
    {
      kmWarning() << "Contact" << handle << "does not support P2P transfers, using old MIME invitations instead.";
    }

    // The contact only supports file transfer the old way
    MimeApplication *app = new FileTransfer(handle, filename);
    startApplication(app);
    return;
  }

#if CHATMASTER_SEND_FILES_MSNFTP
  kmWarning() << "Sending files is forced over MSNFTP for debugging, using old MIME invitations.";
  MimeApplication *app = new FileTransfer(handle, filename);
  startApplication(app);
#else
  // The contact supports file transfer over MSNP2P
  P2PApplication *app = new FileTransferP2P(appList, filename);
  startApplication(app);
#endif
}



// Start a chat and send a file to the contact
void ChatMaster::startChatAndFileTransfer( const QString &handle, const QString &filename )
{
  requestChat( handle );
  int i = 0;

  // FIXME: Workaround needed, because the filetransfer is "lost" when the Switchboard is not open yet.
  // suggested workarround: passing an "initial action" to requestChat().
  while( !getContactSwitchboardConnection( handle, true ) && i++ < 75 )
  {
    QTest::qWait(200);
  }

  startFileTransfer( handle, filename );
}



// Start a msnobject transfer to get the contact's msn object.
void ChatMaster::startMsnObjectDownload( const QString &handle, const MsnObject *msnObject, Chat *chat )
{
  // Test input
  if(KMESS_NULL(msnObject)) return;

  // Get the application list
  ApplicationList *appList = getApplicationList(handle);
  if(KMESS_NULL(appList))   return;

  // First check if the actual object is currently being downloaded already.
  // call before cache check, since file can be partially downloaded.
  if( appList->hasMsnObjectTransfer(*msnObject) )
  {
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "object is already being downloaded, not sending a second invite.";
#endif
    return;
  }

  // Get the picture filename, perhaps from a cache
  QString objectFileName( KMessConfig::instance()->getMsnObjectFileName( *msnObject ) );
  bool fileExists = QFile::exists( objectFileName );

  // Check if the image can be read. ( TODO this step is skipped for WINK for the moment )
  if( fileExists && msnObject->getType() != MsnObject::WINK )
  {
    bool imageCorrupt = QImageReader::imageFormat( objectFileName ).isEmpty();
    if( imageCorrupt || ! msnObject->verifyFile( objectFileName ) )
    {
#ifdef KMESSDEBUG_CHATMASTER
      kmDebug() << "Cached MsnObject is corrupt "
                << ( imageCorrupt ? "(detected by QImage)" : "(detected by MsnObject)" )
                << ", deleting it: '" << objectFileName << "'" << endl;
#endif
      QFile::remove( objectFileName );
      fileExists = false;
    }
  }

  // Avoid downloading again if it exists
  if( fileExists )
  {
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "Contact MsnObject is already in cache.";
#endif

    // Already have it, handle processing in a generic way.
    // Don't use slotMsnObjectReceived() because it uses sender() to get the Chat object.
    showMsnObject( handle, msnObject->objectString(), chat );
  }
  else
  {
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "Starting MsnObject download (type=" << msnObject->getType() << ").";
#endif

    // Create and initialize the application.
    P2PApplication *app = new MsnObjectTransferP2P(appList, *msnObject);
    app->setChat( chat );   // for winks, to display in originating chat window.
    connect(app,  SIGNAL(     msnObjectReceived(const QString&, const MsnObject&)  ),
            this, SLOT  ( slotMsnObjectReceived(const QString&, const MsnObject&)  ));
    startApplication(app);
  }
}



/**
 * Periodically called method for update commands.
 *
 * This method is synchronized with the ping timer of MsnNotificationConnection
 * to avoid multiple timers which reduces power consumption (see www.linuxpowertop.org).
 *
 * Currently this is used to download display pictures in the background,
 * but it can also be used for other events.
 */
void ChatMaster::timedUpdate()
{
  // Handle the queue of display pictures to download, but only if we're not hidden
  if( pendingDisplayPictures_.count() == 0 || currentAccount_->getStatus() ==  STATUS_INVISIBLE )
  {
    return;
  }

#ifdef KMESSDEBUG_CHATMASTER
  kmDebug() << "handling timed events.";
#endif

  // See if there are pictures to download.
  // Get 4 pictures at once.
  int requestCount = pendingDisplayPictures_.count();
  requestCount = requestCount > 4 ? 4 : requestCount;
  while( requestCount > 0 && ! pendingDisplayPictures_.isEmpty() )
  {
    // Unshift the first handle of the list.
    QString handle( pendingDisplayPictures_.first() );
    pendingDisplayPictures_.removeAll( handle );

#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "see if the display picture of '" << handle << "' should  be downloaded.";
#endif

    // See if the picture was already downloaded somehow (e.g. user opened a chat)

    // First get the contact.
    // Using getContactList()->.. because it skips the InvitedContact objectss returned by CurrentAccount::getContactByHandle()
    Contact *contact = currentAccount_->getContactList()->getContactByHandle( handle );
    if( contact == 0 )
    {
#ifdef KMESSDEBUG_CHATMASTER
      kmDebug() << "contact not found, was it removed?";
#endif
      continue;
    }

    // See if the contact is still online.
    if( contact->isOffline() )
    {
#ifdef KMESSDEBUG_CHATMASTER
      kmDebug() << "contact is no longer online.";
#endif
      continue;
    }

    // See if the contact is still blocked.
    // Avoid starting a chat which "invites" the contact to chat back.
    if( contact->isBlocked() )
    {
#ifdef KMESSDEBUG_CHATMASTER
      kmDebug() << "contact is blocked.";
#endif
      continue;
    }

    // Get the msn object, can become null now.
    const MsnObject *msnObject = contact->getMsnObject();
    if( msnObject == 0 )
    {
#ifdef KMESSDEBUG_CHATMASTER
      kmDebug() << "MsnObject has been reset.";
#endif
      continue;
    }

    // See if the object already exists in the cache
    QString objectFileName( KMessConfig::instance()->getMsnObjectFileName( *msnObject ) );
    if( QFile::exists( objectFileName )
    &&  ! QImageReader::imageFormat( objectFileName ).isEmpty()
    &&  msnObject->verifyFile( objectFileName ) )
    {
#ifdef KMESSDEBUG_CHATMASTER
      kmDebug() << "Picture is already downloaded.";
#endif
      continue;
    }


    // See if the actual object is currently being downloaded (e.g. user just opened a chat).
    // Put as last check because it creates the ApplicationList on demand.


#ifdef KMESSDEBUG_CHATMASTER
    // Made the debug output easier to understand.
    if( ! contact->hasApplicationList() )
    {
      kmDebug() << "Picture is not available, creating application list.";
    }
#endif

    // Get the application list
    ApplicationList *appList = getApplicationList( handle );
    if( KMESS_NULL(appList) ) continue;

    // See if a picture is currently being downloaded.
    if( appList->hasMsnObjectTransfer( *msnObject ) )
    {
#ifdef KMESSDEBUG_CHATMASTER
      kmDebug() << "transfer for the picture is already active.";
#endif
      continue;
    }


    // All tests passed.
    // Download the picture.

#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "Picture is not available, starting switchboard connection to download it.";
#endif

    // Start a chat to download the picture.
    // The download will be initiated up by ChatMaster::slotContactJoinedChat().
    emit requestSwitchboard( handle, ChatInformation::CONNECTION_BACKGROUND );
    requestCount--;
  }
}



// Update the grouping of chats/tabs.
void ChatMaster::updateChatGrouping()
{
  if( currentAccount_->getTabbedChatMode() == chatTabbedMode_ )
  {
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "Chat grouping mode not changed, nothing to update";
#endif
    return;
  }

#ifdef KMESSDEBUG_CHATMASTER
  kmDebug() << "Updating Chat grouping";
#endif
  chatTabbedMode_ = currentAccount_->getTabbedChatMode();

  if( chatWindows_.empty() )
  {
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "No ChatWindows, nothing to update";
#endif
    return;
  }

  ChatWindow *currentWindow;
  ChatWindow *window;

  foreach( Chat *chat, chats_ )
  {
    currentWindow = chat->getChatWindow();
    window        = findWindowForChat( chat);

    if( window == 0)
    {
#ifdef KMESSDEBUG_CHATMASTER
      kmDebug() << "No suited window found for chat" << chat;
#endif

      ChatWindow* newChatWindow = createChatWindow( chat);

      newChatWindow->addChatTab( chat, false );
      currentWindow->removeChatTab( chat);
      currentWindow->checkAndCloseWindow();

      raiseChat( chat, false );
    }
    else if( window != currentWindow )
    {
#ifdef KMESSDEBUG_CHATMASTER
      kmDebug() << "Moving chat" << chat << "to other window" << window;
#endif

      window->addChatTab( chat, false );
      currentWindow->removeChatTab( chat);
      currentWindow->checkAndCloseWindow();

      raiseChat( chat, false );
    }
  }
}



// Search the appropriate window for a chat.
ChatWindow *ChatMaster::findWindowForChat( Chat *chat )
{
  ChatWindow *window = 0;
  ChatWindow *otherGroupWindow = 0;
  CurrentAccount *currentAccount = currentAccount_;

  if( ! chatWindows_.empty() )
  {
#ifdef KMESSDEBUG_CHATMASTER
    kmDebug() << "At least one chat window is already open: checking the tabbed chatting settings.";
#endif

    if( currentAccount->getTabbedChatMode() == 0 ) // Always group in tabs
    {
      // Pick the last open chat window
      window = chatWindows_.last();
#ifdef KMESSDEBUG_CHATMASTER
      kmDebug() << "Always group in tabs. Picking the last created window.";
#endif
    }
    else if( currentAccount->getTabbedChatMode() == 1 ) // Group tabs by initial contact's group
    {
#ifdef KMESSDEBUG_CHATMASTER
      kmDebug() << "Group in tabs by contact group. Choosing a window.";
#endif
      QString handle( chat->getSwitchboardConnection()->getFirstContact() );
      Contact *contact = currentAccount->getContactList()->getContactByHandle( handle );

      if( contact && ! contact->getGroupIds().isEmpty() )
      {
        QString group( contact->getGroupIds().first() );
#ifdef KMESSDEBUG_CHATMASTER
        kmDebug() << "Searching chats for one whose first contact is in the same group as the new one's.";
#endif
        foreach( Chat *chatItem, chats_ )
        {
          QString handle( chatItem->getSwitchboardConnection()->getFirstContact() );
          Contact *contact = currentAccount->getContactList()->getContactByHandle( handle );

          if( contact && ! contact->getGroupIds().isEmpty()
          && contact->getGroupIds().first() == group )
          {
#ifdef KMESSDEBUG_CHATMASTER
            kmDebug() << "Match found!";
#endif
            window = chatItem->getChatWindow();

            // We found a possibly suited window.
            // Check if it already displays tabs of another group.
            // If so, we can't use it.
            if( otherGroupWindow == window )
            {
              window = 0;
            }

            break;
          }
          else
          {
            // Keep track of the window used by another group.
            otherGroupWindow = chatItem->getChatWindow();
          }
        }
      }
    }
  }
  return window;
}



#include "chatmaster.moc"
