/***************************************************************************
                          contactbase.cpp  -  description
                             -------------------
    begin                : Thu Jan 16 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 "contactbase.h"

#include "../contact/msnobject.h"   // only used for detectClientName()!
#include "../network/applications/applicationlist.h"
#include "../utils/kmessshared.h"
#include "../kmessdebug.h"

#include <QFile>
#include <QImage>

#include <KLocale>
#include <KStandardDirs>



// The constructor
ContactBase::ContactBase( const QString& handle, const QString& friendlyName, uint capabilities)
 : capabilities_(capabilities)
 , handle_(handle)
 , applicationList_(0)
{
  // Use the handle in the rare cases where a friendly name is not available
  if( friendlyName.isEmpty() )
  {
    friendlyName_ = handle;
  }
  else
  {
    friendlyName_ = friendlyName;
  }
}



// The destructor
ContactBase::~ContactBase()
{
  delete applicationList_;
  applicationList_ = 0;
}



// Add a custom emoticon definition
void ContactBase::addEmoticonDefinition(const QString &emoticonCode, const QString &msnObjectDataHash )
{
#ifdef KMESSDEBUG_CONTACTBASE
  kDebug() << "adding '" << emoticonCode << "' to pending replacements";
#endif

  // Escape the code so the HTML parser won't be fooled.
  QString shortcut( emoticonCode );
  KMessShared::htmlEscape( shortcut );

  // Add emoticon to pending list, until Msn object is received.
  pendingEmoticons_.insert( msnObjectDataHash, shortcut );

  // Regenerate the pending emoticon regexp
  regeneratePendingEmoticonPattern();
}



// Add a custom emoticon file
void ContactBase::addEmoticonFile(const QString &msnObjectDataHash, const QString &filename)
{
  // Check whether the mapping is available.
  if( ! pendingEmoticons_.contains(msnObjectDataHash) )
  {
    kWarning() << "Received an custom emoticon, but no emoticon code found!";
    return;
  }

  // Get the emoticon code from pending list, remove it
  QString emoticonCode( pendingEmoticons_[msnObjectDataHash] );    // no QString& because record gets removed.
  pendingEmoticons_.remove(msnObjectDataHash);

#ifdef KMESSDEBUG_CONTACTBASE
  kDebug() << "linking '" << emoticonCode << "' to received file.";
#endif

  // Check if filename exists, attempt to read it.
  if( ! QFile::exists(filename) )
  {
    kWarning() << "Could not read emoticon file: file not found!";
    return;
  }
  QImage emoticonData(filename);
  if( emoticonData.isNull() )
  {
    kWarning() << "Could not read emoticon file!";
    return;
  }

#ifdef KMESSDEBUG_CONTACTBASE
  kDebug() << "Setting image for '" << emoticonCode << " to " << filename;
#endif

  // Escape the code to insert it safely into a regular expression
  QString escapedCode( QRegExp::escape( emoticonCode ) );

  // Update regexp pattern
  if( emoticonRegExp_.isEmpty() )
  {
    emoticonRegExp_ = QRegExp( escapedCode );
  }
  else
  {
    emoticonRegExp_ = QRegExp( emoticonRegExp_.pattern() + "|" + escapedCode );
  }

  // Regenerate the pending emoticon pattern
  regeneratePendingEmoticonPattern();

#ifdef KMESSTEST
  KMESS_ASSERT( emoticonRegExp_.isValid() );
#endif


  // Get the image dimensions from the file
  int originalWidth = emoticonData.width();
  int originalHeight = emoticonData.height();

  // Resize the displayed image to match KMess' maximum emoticon size
  int width  = originalWidth; // qMin( EMOTICONS_MAX_SIZE, originalWidth ); // Require only a max height
  int height = qMin( EMOTICONS_MAX_SIZE, originalHeight );

  if( originalWidth > originalHeight )
  {
      height = ( width * originalHeight ) / originalWidth;
  }
  else
  {
      width = ( height * originalWidth ) / originalHeight;
  }

  // Add replacement.
  QString altCode ( emoticonCode );
  altCode = KMessShared::htmlUnescape( altCode ).replace("'", "&#39;");
  QString replacement( "<img src='" + filename
                     + "' alt='" + altCode
                     + "' width='" + QString::number( width )
                     + "' height='" + QString::number( height )
                     + "' valign='middle'"
                     +  " class='customEmoticon' />" );

  customEmoticons_.insert( emoticonCode, replacement );
  customHashes_.insert( emoticonCode, msnObjectDataHash );
}



// Register the contact as participant in the chat session
void ContactBase::addSwitchboardConnection( const MsnSwitchboardConnection *connection )
{
#ifdef KMESSDEBUG_CONTACTBASE
  kDebug() << "adding connection to list.";
#endif

  if( chatSessions_.contains(connection) )
  {
    kWarning() << "chat session is already added.";
    return;
  }

  chatSessions_.append( connection );
}



// Initialize an application list for the contact.
ApplicationList * ContactBase::createApplicationList()
{
  if( applicationList_ != 0 )
  {
    kWarning() << "application list already created for contact '" << handle_ << "'.";
  }
  else
  {
    applicationList_ = new ApplicationList( handle_ );

    connect( applicationList_, SIGNAL(        applicationsAborted(const QString &) ),
             this,             SLOT  ( slotApplicationListAborted()                ) );
  }

  return applicationList_;
}



// Set the application name based on the capabilities
void ContactBase::detectClientName( uint capabilities, const MsnObject *msnObject )
{
  // Note there are two ways to detect the client name.
  // - in the notification connection, we can read the 'capabilities' and filename from the 'msnobject' (which some thirdparty clients tag with their name).
  // - in the switchboard connection, a x-clientcaps message is sent by most thirdparty clients with exact version information.

  // Use capabilities to give an indication what client we're dealing with.
  uint clientVersion = capabilities & 0xF0000000;

  // Find msn version
  float msnVersion = 0;
  QString suffix;
  switch( clientVersion )
  {
    case 0: break;
    case MSN_CAP_MSN60:     msnVersion = 6.0f; break;
    case MSN_CAP_MSN61:     msnVersion = 6.1f; break;
    case MSN_CAP_MSN62:     msnVersion = 6.2f; break;
    case MSN_CAP_MSN70:     msnVersion = 7.0f; break;      // default caps 0x4000C024: ink-gif, multi-packet, direct-im, winks
    case MSN_CAP_MSN75:     msnVersion = 7.5f; break;
    case MSN_CAP_MSN80:     msnVersion = 8.0f; break;
    case MSN_CAP_MSN81:     msnVersion = 8.1f; break;      // default caps 0x765DC02C: ink-gif, (ink-isf), multi-packet, direct-im, winks, shared-search, voice-clips, secure-channel, sip, folder-sharing, turn, uun
    case MSN_CAP_MSN85:     msnVersion = 8.5f; break;      // default caps 0x865DC02C: ink-gif, ink-isf, multi-packet, direct-im, winks, shared-search, voice-clips, secure-channel, sip, folder-sharing
    case MSN_CAP_MSN90beta: msnVersion = 9.0f; suffix = " beta"; break;  // default caps 0x963CC03C:
    case MSN_CAP_MSN2009:   msnVersion = 2009.0f; break;
    default:
      kWarning() << "Unable to parse the WLM version of for the contact" << handle_ << ", msncversion=" << ( clientVersion >> 28 ) << " capabilities=" << capabilities;
      msnVersion = 9999999;
  }


  // Third party clients also include their name in the MsnObject,
  // since that's already available at the notification connection.
  QString newClientName;
  QString pictureClientName;
  if( msnObject != 0 )
  {
    const QString &pictureLocation = msnObject->getLocation();

    if( ! pictureLocation.isEmpty()
    &&  pictureLocation != "0"   // WLM 8.1
    &&  ! pictureLocation.startsWith("TFR") )  // MSN / WLM
    {
      // Known variations:
      if( pictureLocation == "KMess.tmp" )
      {
        pictureClientName = "KMess";
      }
      else if( pictureLocation == "Mercury.tmp" )
      {
        pictureClientName = "Mercury";
      }
      else if( pictureLocation == "kopete.tmp" or pictureLocation == "1.tmp" )
      {
        pictureClientName = "Kopete";
      }
      else if( pictureLocation == "amsn.tmp" )
      {
        pictureClientName = "aMSN";
      }
      else if( pictureLocation == "EncartaIcon.png" )
      {
        // Encarta Instant Answers.
        // ignore, no special string for this one.
      }
      else
      {
#ifdef KMESSDEBUG_CONTACTBASE
        kDebug() << "NOTICE: Unknown client name in MsnObject location:" << pictureLocation;
#endif
      }
    }
  }

  if( ! pictureClientName.isNull() )
  {
    newClientName = pictureClientName;
  }
  // Check capabilities for special software
  else if( capabilities & MSN_CAP_WIN_MOBILE )
  {
    newClientName = i18n( "Windows Mobile" );
  }
  else if( capabilities & MSN_CAP_WEB_CLIENT )
  {
    newClientName = i18n( "Web Messenger" );
  }
  else if( capabilities & MSN_CAP_MSO_CLIENT )
  {
    newClientName = i18n( "Office Communicator" );
  }
  else if( capabilities & MSN_CAP_BOT )
  {
    newClientName = i18n( "Messenger Bot" );
  }
  else if( capabilities & MSN_CAP_MCE_CLIENT )
  {
    newClientName = i18n( "Windows Media Center" );
  }
  else
  {
    if( msnVersion == 0 )
    {
      // pre p2p clients. msn 4.x is locked out after the switch to SSL logins.
      newClientName = i18n( "MSN Messenger %1 compatible", QString( "5.0" ) );
    }
    else if( msnVersion < 8.0 )
    {
      // pre wlm client
      newClientName = i18n( "MSN Messenger %1 compatible", QString::number( msnVersion ) );
    }
    else
    {
      // WLM client.
      // See if it has all capabilities of WLM 8.1 / 2009, or it's a third party client which is just faking...
      // ink-isf is optional, requires Windows Journal to be installed.
      const uint wlm80Mask   = MSN_CAP_INK_GIF | MSN_CAP_MULTI_PACKET | MSN_CAP_DIRECT_IM | MSN_CAP_WINKS | MSN_CAP_SHARED_SEARCH | MSN_CAP_VOICE_CLIPS | MSN_CAP_SCHANNEL | MSN_CAP_SIP_INVITE | MSN_CAP_FOLDER_SHARING;  // FIXME: turn / uun seen in 81, also in 8.0?
      const uint wlm2009Mask = MSN_CAP_INK_GIF | MSN_CAP_MULTI_PACKET | MSN_CAP_DIRECT_IM | MSN_CAP_WINKS | MSN_CAP_VOICE_CLIPS | MSN_CAP_SCHANNEL | MSN_CAP_SIP_INVITE | MSN_CAP_UNKN_2009 | MSN_CAP_P2P_TURN | MSN_CAP_P2P_UUN;
      bool isRealWlm = ( ( msnVersion < 9.0 && ( capabilities & wlm80Mask ) == wlm80Mask )
                      || ( capabilities & wlm2009Mask ) == wlm2009Mask );

      if( isRealWlm )
      {
        if( msnVersion < 9999999 )
        {
          // Supports all default caps of 8.1 or 2009, remove the "compatible" string. This truly is WLM.
          newClientName = i18n( "Windows Live Messenger %1", QString::number( msnVersion ) + suffix );
        }
        else
        {
          // Beyond our current finger print database, can't show version number yet.
          newClientName = i18n( "Windows Live Messenger" );
        }
      }
      else
      {
        if( msnVersion < 9999999 )
        {
          newClientName = i18n( "Windows Live Messenger %1 compatible", QString::number( msnVersion ) );
        }
        else
        {
          // Kopete seems to use 0xc0000000 as capabilities.. 2 versions beyond WLM2009
          // (this is fixed in a later version, turns out they added all supported MSN versions instead of just sending the newest one)
          // Its caps are 0xc0148028; ink-isf, multi-packet, winks, voice-clips, sip
          newClientName = i18n( "Windows Live Messenger compatible" );
        }
      }
    }
  }

  // If the base client name has changed, the full name has probably changed too
  if( newClientName != clientName_ )
  {
    clientName_ = newClientName;
    clientFullName_.clear();
  }

#ifdef KMESSDEBUG_CONTACTBASE
  if( isOnline() )
  {
    QStringList caps;
    if( capabilities & MSN_CAP_WIN_MOBILE     ) caps << "mobile-client";   // 0x1
    if( capabilities & MSN_CAP_MSN8_USER      ) caps << "msn8-user?";      // 0x2
    if( capabilities & MSN_CAP_MSO_CLIENT     ) caps << "office-client";   // 0x800
    if( capabilities & MSN_CAP_WEB_CLIENT     ) caps << "web-client";      // 0x200
    if( capabilities & MSN_CAP_BOT            ) caps << "bot-client";      // 0x20000
    if( capabilities & MSN_CAP_MCE_CLIENT     ) caps << "mce-client";      // 0x2000

    if( capabilities & MSN_CAP_INK_GIF        ) caps << "ink-gif";         // 0x4
    if( capabilities & MSN_CAP_INK_ISF        ) caps << "ink-isf";         // 0x8
    if( capabilities & MSN_CAP_MULTI_PACKET   ) caps << "multi-packet";    // 0x20
    if( capabilities & MSN_CAP_DIRECT_IM      ) caps << "direct-im";       // 0x4000
    if( capabilities & MSN_CAP_WINKS          ) caps << "winks";           // 0x8000
    if( capabilities & MSN_CAP_SHARED_SEARCH  ) caps << "shared-search";   // 0x10000
    if( capabilities & MSN_CAP_VOICE_CLIPS    ) caps << "voice-clips";     // 0x40000
    if( capabilities & MSN_CAP_SCHANNEL       ) caps << "secure-channel";  // 0x80000
    if( capabilities & MSN_CAP_SIP_INVITE     ) caps << "sip";             // 0x100000
    if( capabilities & MSN_CAP_UNKN_2009      ) caps << "unknown-2009";    // 0x200000
    if( capabilities & MSN_CAP_FOLDER_SHARING ) caps << "folder-sharing";  // 0x400000
    if( capabilities & MSN_CAP_P2P_TURN       ) caps << "p2p-turn";        // 0x2000000
    if( capabilities & MSN_CAP_P2P_UUN        ) caps << "p2p-uun";         // 0x4000000

    if( capabilities & MSN_CAP_VIDEO_CHAT     ) caps << "has-webcam";      // 0x10
    if( capabilities & MSN_CAP_MSN_MOBILE     ) caps << "has-mobile";      // 0x40
    if( capabilities & MSN_CAP_MSN_DIRECT     ) caps << "has-direct";      // 0x80
    if( capabilities & MSN_CAP_LIVE_SPACE     ) caps << "has-space";       // 0x1000
    if( capabilities & MSN_CAP_ONECARE        ) caps << "has-onecare";     // 0x1000000

    kDebug() << "contact" << getHandle() << "uses client:" << clientName_ << "msn version:" << msnVersion << "capabilities:" << capabilities << caps;
  }
#endif
}



// Return the application list of the contact
ApplicationList * ContactBase::getApplicationList() const
{
  return applicationList_;
}



// The capabilities of the client
uint ContactBase::getCapabilities() const
{
  return capabilities_;
}



// Return a user readable client name string.
const QString & ContactBase::getClientName( bool getFullNameIfAvailable ) const
{
  if( getFullNameIfAvailable && ! getClientFullName().isEmpty() )
  {
    return clientFullName_;
  }

  return clientName_;
}



// Return the third party client name field
const QString & ContactBase::getClientFullName() const
{
  return clientFullName_;
}



// Return the default contact picture path
QString ContactBase::getContactDefaultPicturePath() const
{
  KStandardDirs *dirs   = KGlobal::dirs();
  return dirs->findResource( "data", "kmess/pics/unknown.png" );
}



// Return the list of custom emoticons to ignore.
const QStringList &ContactBase::getEmoticonBlackList() const
{
  return emoticonBlackList_;
}



// Return the custom emoticon code for an msn object
QString ContactBase::getEmoticonCode(const QString &msnObjectDataHash) const
{
  return pendingEmoticons_[ msnObjectDataHash ];
}



// Return the custom emoticon search pattern.
const QRegExp & ContactBase::getEmoticonPattern() const
{
  return emoticonRegExp_;
}



// Return the custom emoticon replacements.
const QHash<QString,QString>& ContactBase::getEmoticonReplacements() const
{
  return customEmoticons_;
}



// Return the custom emoticon hashes.
const QHash<QString,QString>& ContactBase::getEmoticonHashes() const
{
  return customHashes_;
}



// Return the contact's friendly name
const QString& ContactBase::getFriendlyName( FormattingMode mode ) const
{
  return friendlyName_.getString( mode );
}



// Return the contact's handle
const QString& ContactBase::getHandle() const
{
  return handle_;
}



// Return the search pattern for pending custom emoticons.
const QRegExp & ContactBase::getPendingEmoticonPattern() const
{
  return pendingRegExp_;
}



// Regenerate the pending emoticon regexp, because it was changed
void ContactBase::regeneratePendingEmoticonPattern()
{
  QString pattern;
  foreach( const QString& shortcut, pendingEmoticons_ )
  {
    // Escape the code to insert it safely into a regular expression
    QString escapedCode( QRegExp::escape( shortcut ) );
    if( pattern.isEmpty() )
    {
      pattern = escapedCode;
    }
    else
    {
      pattern.append( "|" + escapedCode );
    }
  }

  pendingRegExp_ = QRegExp( pattern );

#ifdef KMESSTEST
  KMESS_ASSERT( pendingRegExp_.isValid() );
#endif
}



// Return the list of switchboard connections
const QList<const MsnSwitchboardConnection*> ContactBase::getSwitchboardConnections() const
{
  return chatSessions_;
}



// Returns whether the contact has an application list initialized.
bool ContactBase::hasApplicationList() const
{
  return (applicationList_ != 0);
}



// Returns whether the contact has capability
bool  ContactBase::hasCapability( MsnClientCapabilities flag ) const
{
  return ( capabilities_ & flag ) == flag;
}



// Returns whether the contact's client supports the given MSNP2P version
bool ContactBase::hasP2PSupport( MsnClientCapabilities minimalVersion ) const
{
#ifdef KMESSTEST
  KMESS_ASSERT( minimalVersion != 0 && (minimalVersion & 0x0fffffff) == 0 );  // don't pass some other capability.
#endif

  return capabilities_ >= (uint) minimalVersion;
}



// Return true if the contact is offline
bool ContactBase::isOffline() const
{
  return getStatus() == STATUS_OFFLINE;
}



// Return true if the contact is online
bool ContactBase::isOnline() const
{
  return getStatus() != STATUS_OFFLINE;
}



// Add or remove a custom emoticon from the black list
bool ContactBase::manageEmoticonBlackList( bool add, const QString &emoticonCode )
{
  // Add the emoticon
  if( add )
  {
    // Don't duplicate emoticons in the list
    if( emoticonBlackList_.contains( emoticonCode ) )
    {
      return false;
    }

    emoticonBlackList_.append( emoticonCode );
    return true;
  }
  // Remove the emoticon
  else
  {
    return ( emoticonBlackList_.removeAll( emoticonCode ) > 0 );
  }
}



// Unregister the contact as participant, contact has left the chat.
void ContactBase::removeSwitchboardConnection( const MsnSwitchboardConnection *connection, bool userInitiated )
{
#ifdef KMESSDEBUG_CONTACTBASE
  kDebug() << "removing connection, userInitiated=" << userInitiated;
#endif

  bool found = chatSessions_.removeAll( connection ) > 0;
  if( ! found )
  {
    kWarning() << "chat session not found.";
  }

  if( chatSessions_.isEmpty() )
  {
#ifdef KMESSDEBUG_CONTACTBASE
    kDebug() << handle_ << " is no longer in any other chat conversation, cleaning up.";
#endif

    // Inform the ApplicationList if we have one.
    // It's possible some sessions need to be terminated now.
    if( applicationList_ != 0 )
    {
      bool hasAbortingApps = applicationList_->contactLeftChat( userInitiated );

      // If there aren't any applications aborting, the object can be deleted now.
      // Also avoid deleting the object if there is still a direct connection.
      // It's possible a switchboard is replaced with another one (with WLM -> background chat -> normal chat).
      // Note the object could be destroyed from slotApplicationListAborted() too.
      if( ! hasAbortingApps && applicationList_ != 0 && ! applicationList_->hasAuthorizedDirectConnection() )
      {
#ifdef KMESSDEBUG_CONTACTBASE
        kDebug() << "no applications to abort, deleting ApplicationList object.";
#endif

        delete applicationList_;
        applicationList_ = 0;
      }
    }

    // This will be used by CurrentAccount to clean up temporary InvitedContact objects.
    emit leftAllChats( this );
  }
}



// Set the client capabilities
void ContactBase::setCapabilities( uint capabilities )
{
  // See if the client software could have changed.
  // Ignore flags which can be toggled by the client.
  const int toggleFlags = ( MSN_CAP_VIDEO_CHAT | MSN_CAP_MSN_MOBILE | MSN_CAP_MSN_DIRECT | MSN_CAP_LIVE_SPACE | MSN_CAP_ONECARE );
  bool clientChanged = ( capabilities_ | toggleFlags ) != ( capabilities | toggleFlags );

  // Reset the client name information if the capabilities changed.
  if( clientChanged && capabilities > 0 )
  {
#ifdef KMESSDEBUG_CONTACTBASE
    kDebug() << "capabilities of" << handle_ << "changed from" << capabilities_ << "to" << capabilities << ", resetting detected app name.";
#endif

    clientName_.clear();
  }

  // Set caps
  capabilities_ = capabilities;
}



// Save the simple name of the client used by the contact
void ContactBase::setClientName( const QString &clientName )
{
  clientName_ = clientName;
}



// Set the full name of the client used by the contact
void ContactBase::setClientFullName( const QString& clientFullName )
{
  clientFullName_ = clientFullName;

  // Make the name more readable
  clientFullName_ = clientFullName_.replace( '/', ' ' );

  // 'Purple 2.3.1' means the user actually has Pidgin or Adium (because they use libpurple).
  if( clientFullName_.startsWith( "Purple " ) )
  {
    clientFullName_ = "Pidgin/Adium (" + clientFullName_ + ")";
  }
}



// The application list completed aborting it's applications.
void ContactBase::slotApplicationListAborted()
{
  // Avoid killing an object if it was filled with new entries again
  if( ! applicationList_->isEmpty() || applicationList_->hasAuthorizedDirectConnection() )
  {
#ifdef KMESSDEBUG_CONTACTBASE
    kDebug() << "requested abortion completed, but ApplicationList object has object references: not deleting object.";
#endif
    return;
  }

#ifdef KMESSDEBUG_CONTACTBASE
  kDebug() << "requested abortion completed, deleting ApplicationList object.";
#endif


  // Use deleteLater() because this is called from the signal loop.
  applicationList_->deleteLater();
  applicationList_ = 0;
}



#include "contactbase.moc"
