/***************************************************************************
                          networkwindow.cpp  -  description
                             -------------------
    begin                : Wed Jan 28 2005
    copyright            : (C) 2005 by Diederik van der Boor
    email                : "vdboor" --at-- "codingdomain.com"
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

#include "../kmessdebug.h"

#include "networkwindow.h"

#include "../network/mimemessage.h"
#include "../network/msnnotificationconnection.h"
#include "../network/p2pmessage.h"
#include "../utils/kmessconfig.h"

#include <QFile>
#include <QRegExp>
#include <QSize>
#include <QDateTime>
#include <QTextCodec>
#include <QTextStream>

#include <kdeversion.h>
#include <KFileDialog>
#include <KLocale>
#include <KMessageBox>
#include <KTabWidget>
#include <KTextBrowser>
#include <kdeversion.h>


// Declare the instance
NetworkWindow * NetworkWindow::instance_(0);



// Constructor
NetworkWindow::NetworkWindow( QWidget *parent )
  : KDialog( parent, Qt::Dialog ) // Explicitly set the window type to disable the destructive close behavior
  , Ui::NetworkWindow()
{
  // Set up the dialog and its buttons
  setObjectName( "Network" );
  setButtons( Close | User1 | User2 | User3 );
  setDefaultButton( Close );
  setCaption( i18n("Network Window") );
  restoreDialogSize( KMessConfig::instance()->getGlobalConfig( "NetworkWindow" ) );

  // Load the ui file in a main qwidget
  QWidget *mainWidget = new QWidget(this);
  setupUi( mainWidget );
  setMainWidget( mainWidget );

  // Set up the behavior of the Tab Widget
#if KDE_IS_VERSION(4,1,0)
  connectionTabs_->setCloseButtonEnabled( true );
#else
  connectionTabs_->setHoverCloseButton( true );
#endif
  connect( connectionTabs_,    SIGNAL(   closeRequest(QWidget*) ), this, SLOT(          closeTab(QWidget*) ) );
  connect( connectionTabs_,    SIGNAL( currentChanged(int)      ), this, SLOT( slotUpdateWidgets(int)      ) );

  // Set up the behavior of the Send button
  connect( sendCommandButton_, SIGNAL(       released()         ), this, SLOT(   slotSendCommand()         ) );

  // Name our custom buttons and setup their behavior
  setButtonText( User1, i18n("S&ave Tab") );
  setButtonText( User2, i18n("C&lear Tab") );
  setButtonText( User3, i18n("C&lose All Tabs") );
  setButtonIcon( User1, KIcon("document-save") );
  setButtonIcon( User2, KIcon("edit-clear-list") );
  setButtonIcon( User3, KIcon("edit-clear") );
  connect( this, SIGNAL( user1Clicked() ), this, SLOT(  saveCurrentTab() ) );
  connect( this, SIGNAL( user2Clicked() ), this, SLOT( clearCurrentTab() ) );
  connect( this, SIGNAL( user3Clicked() ), this, SLOT(    closeAllTabs() ) );

  // Set the default state of the ui widgets
  sendStandardCmdRadio_->setChecked( true );
}



// Destructor
NetworkWindow::~NetworkWindow()
{
  KConfigGroup group = KMessConfig::instance()->getGlobalConfig( "NetworkWindow" );
  saveDialogSize( group );

  // Disconnect all signals, prevent i.e. slotUpdateWidgets from being called
  disconnect( connectionTabs_,    0, this, 0 );
  disconnect( sendCommandButton_, 0, this, 0 );
  disconnect( this,               0, this, 0 );

  foreach( ConnectionEntry *entry, connectionViews_ )
  {
    delete entry->logView;
    delete entry;
  }
  connectionViews_.clear();
}


  // The internal function to add a message
  void NetworkWindow::addMessage(ConnectionEntry *entry, QString html)
  {
    html = html.replace( "  ", " &nbsp;")
               .replace( QRegExp("[\r\n] "), "\n&nbsp;");

    // Add spacer if previous message was some time ago.
    QDateTime now = QDateTime::currentDateTime();
    if( entry->lastMessage.secsTo( now ) > 20 )
    {
      entry->logView->append( "<p style='margin: 0; font-size: 8pt'>&nbsp;<!-- time spacer --></p>" );
    }

    entry->logView->append( html.toUtf8() );
    entry->lastMessage = now;

    // Update the widgets for the currently visible tab
    slotUpdateWidgets();
  }



  // Add a log message to the last log entry
  void NetworkWindow::addLogMessage(QObject *connection, const QString &message)
  {
    ConnectionEntry *entry = getConnectionEntry(connection);
    QString html( "<p style='margin: 0'><font color='#333333'>&nbsp; " + message + "</font></p>\n" );
    addMessage( entry, html );
  }



  // Add an incoming message to the log.
  void NetworkWindow::addIncomingMessage(QObject *connection, const QByteArray &message)
  {
    ConnectionEntry *entry = getConnectionEntry(connection);
    QString html( "<p style='margin: 0; color: #0033FF'><hr style='padding: 0; margin: 0;'>" + formatMessage(entry, true, message) + "</p>\n" );
    addMessage(entry, html);
  }



  // Add an outgoing message to the log.
  void NetworkWindow::addOutgoingMessage(QObject *connection, const QByteArray &message)
  {
    ConnectionEntry *entry = getConnectionEntry(connection);
    QString html( "<p style='margin: 0; color: #FF3300'><hr style='padding: 0; margin: 0;'>" + formatMessage(entry, false, message) + "</p>\n" );
    addMessage(entry, html);
  }



  // Inform the connection was closed
  void NetworkWindow::connectionClosed(QObject *connection)
  {
    // Add the message
    ConnectionEntry *entry = getConnectionEntry(connection);
    entry->logView->append("<p style='margin:0; color: #C2C2C2'>the connection was closed<br>" + getTime() + "</p>\n");
    entry->lastMessage = QDateTime::currentDateTime();
    entry->isClosed = true;
//    // Unregister the connection, so new attempts from the same class generate a new object.
//    connectionViews_.remove(browser);
  }



  // Return a log description for a standard command.
  QString NetworkWindow::describeCommand(const QString &command)
  {
    QString firstPart( command.left(3) );  // command[0] in normal code.
    int errorCode = firstPart.toInt();

    if( errorCode != 0 )
    {
      switch( errorCode )
      {
        // Command is a 3 digit error code.
        case 200: return formatDescription("Error: Invalid syntax");
        case 201: return formatDescription("Error: Invalid parameter");
        case 205: return formatDescription("Error: Invalid principal; not an MSN account");
        case 206: return formatDescription("Error: Domain name missing");
        case 207: return formatDescription("Error: Already logged in");
        case 208: return formatDescription("Error: Invalid principal; not an MSN account");
        case 209: return formatDescription("Error: Nickname change illegal");
        case 210: return formatDescription("Error: Principal list full");
        case 213: return formatDescription("Error: Invalid rename request");
        case 215: return formatDescription("Error: Principal already on list");
        case 216: return formatDescription("Error: Principal not on list");
        case 217: return formatDescription("Error: Principal not online");
        case 218: return formatDescription("Error: Already in this mode");
        case 219: return formatDescription("Error: Principal is in the opposite list");
        case 223: return formatDescription("Error: Too many groups");
        case 224: return formatDescription("Error: Invalid group");
        case 225: return formatDescription("Error: Principal not in group");
        case 227: return formatDescription("Error: Group not empty");
        case 228: return formatDescription("Error: Group with same name already exists");
        case 229: return formatDescription("Error: Group name too long");
        case 230: return formatDescription("Error: Cannot remove group zero");
        case 231: return formatDescription("Error: Invalid group");
        case 240: return formatDescription("Error: Empty domain");
        case 280: return formatDescription("Error: Switchboard server failed");
        case 281: return formatDescription("Error: Transfer to switchboard failed");
        case 282: return formatDescription("Error: P2P header error");
        case 300: return formatDescription("Error: Required field missing");
        case 302: return formatDescription("Error: Not logged in");
        case 402: // same as 403
        case 403: return formatDescription("Error: Error accessing contact list");
        case 420: return formatDescription("Error: Invalid account permissions");  // CVR with unofficial client, non beta service tester
        case 500: return formatDescription("Error: Service Temporarily Unavailable");
        case 501: return formatDescription("Error: Database server error");
        case 502: return formatDescription("Error: Protocol command is disabled");
        case 510: return formatDescription("Error: File operation failed");
        case 511: return formatDescription("Error: Account is banned");
        case 520: return formatDescription("Error: Memory allocation failed");
        case 540: return formatDescription("Error: Challenge response failed");
        case 600: return formatDescription("Error: Server is busy");
        case 601: return formatDescription("Error: Server is unavailable");
        case 602: return formatDescription("Error: Peer nameserver is down");
        case 603: return formatDescription("Error: Database connection failed");
        case 604: return formatDescription("Error: Server is going down");
        case 605: return formatDescription("Error: Server unavailable");
        case 700: return formatDescription("Error: Connection failed");
        case 710: return formatDescription("Error: Bad command parameter");
        case 711: return formatDescription("Error: Write is blocking");
        case 712: return formatDescription("Error: Session is overloaded");
        case 713: return formatDescription("Error: Opening chat sessions too fast");
        case 714: return formatDescription("Error: Too many chat sessions open");
        case 715: return formatDescription("Error: Unexpected command value");
        case 717: return formatDescription("Error: Bad friend file");
        case 731: return formatDescription("Error: Not expected");
        case 800: return formatDescription("Error: Changing names too fast");
        case 911: return formatDescription("Error: Authentication ticket was incorrect");
        case 913: return formatDescription("Error: not allowed when hidden");
        case 923: return formatDescription("Error: Kids' Passport without parental consent");
        case 924: return formatDescription("Error: passport account not verified yet");
        case 928: return formatDescription("Error: bad ticket");
        case 931: return formatDescription("Error: account not on this server");  // if using a cached NS instead of the main dispatch point.

        case 900:
        case 912:
        case 918:
        case 919:
        case 921:
        case 922: return formatDescription("Error: Server too busy");

        case 914:
        case 915:
        case 916: return formatDescription("Error: Server unavailable");
        case 917: return formatDescription("Error: authentication failed");
        default:  return formatDescription("Error, type is unknown!");
      }
    }
    else
    {
      if( firstPart == "VER" )
      {
        return formatDescription("Protocol version negotiation");
      }
      else if ( firstPart == "CVR" )
      {
        return formatDescription("Application version exchange");
      }
      else if ( firstPart == "XFR" )
      {
        return formatDescription("Server transfer request");
      }
      else if ( firstPart == "USR" )
      {
        return formatDescription("User login information");
      }
      else if ( firstPart == "SYN" )
      {
        return formatDescription("Contactlist sync request");
      }
      else if ( firstPart == "CHG" )
      {
        return formatDescription("Client status message (change of online state, client capabilities or msn object)");
      }
      else if ( firstPart == "CHL" )
      {
        return formatDescription("Challenge query");
      }
      else if ( firstPart == "IRO" )
      {
        return formatDescription("Contact already in chat");
      }
      else if ( firstPart == "CAL" )
      {
        return formatDescription("Calling a contact to join chat");
      }
      else if ( firstPart == "RNG" )
      {
        return formatDescription("Invitation for a chat");
      }
      else if ( firstPart == "ANS" )
      {
        return formatDescription("Chat invitation answer with user login information");
      }
      else if ( firstPart == "JOI" )
      {
        return formatDescription("Contact joins chat");
      }
      else if ( firstPart == "FIL" )
      {
        return formatDescription("Indicates the size of the entire file");
      }
      else if ( firstPart == "TFR" )
      {
        return formatDescription("Start of file transfer");
      }
    }

    // Default
//     return QString::null;
    return formatDescription( QString::null );  // always add time.
  }



  // Return a log description for the direct connection preamble
  QString NetworkWindow::describeDirectConnectionPreamble(const QByteArray &message)
  {
    if( message.size() == 4 && memcmp(message.data(), "\x04" "\0\0\0", 4) == 0 )
    {
      return formatDescription("Indicates the size of the next block");
    }
    else if( message.size() == 4 && memcmp(message.data(), "foo\0", 4) == 0 )
    {
      return formatDescription("The preamble of the direct connection: foo\\0");
    }
    else if( message.size() == 8 && memcmp(message.data(), "\x04" "\0\0\0foo\0", 8) == 0 )
    {
      return formatDescription("The block size followed by the preamble of the direct connection: foo\\0");
    }
    else
    {
//       return QString::null;
      return formatDescription( QString::null );  // always add time.
    }
  }



  // Return a log description for the mime message
  QString NetworkWindow::describeMimeMessage(const QString &mimeMessage)
  {
    QRegExp rx("Content-Type: ([^\r\n; ]+)");
    if( rx.indexIn(mimeMessage) == -1 )
    {
      return formatDescription("Message could not be parsed!");
    }

    // Return a description based on the content type
    QString contentType( rx.cap(1) );


    // Notification server messages
    if( contentType == "text/x-msmsgsprofile" )
    {
      // Message with all meta-data of the profile
      return formatDescription("Profile data message");
    }
    else if( contentType == "text/x-msmsgsinitialemailnotification" )
    {
      // The initial status of the mailbox
      return formatDescription("Initial email status message");
    }
    else if( contentType == "text/x-msmsgsemailnotification" )
    {
      // A new email was received
      return formatDescription("New email notification message");
    }
    else if( contentType == "text/x-msmsgsactivemailnotification" )
    {
      // The number of unread emails has changed
      return formatDescription("Email activity message");
    }
    else if( contentType == "application/x-msmsgssystemmessage" )
    {
      // The server is going down for maintenance
      return formatDescription("System maintenance notification");
    }
    else if( contentType == "text/x-msmsgsinitialmdatanotification" )
    {
      // Another contact sent an Offline-IM, which is received upon logging-in
      return formatDescription("Offline-IM notification or Hotmail email status");
    }
    else if( contentType == "text/x-msmsgsoimnotification" )
    {
      // Another contact sent an Offline-IM while we're in invisible mode.
      return formatDescription("Offline-IM notification");
    }

    // Switchboard server messages
    if( contentType == "text/plain" )
    {
      // A standard chat message
      return formatDescription("Chat text message");
    }
    else if( contentType == "text/x-msmsgscontrol" )
    {
      // The contact is typing a message
      return formatDescription("Typing notification message");
    }
    else if( contentType == "text/x-mms-emoticon" )
    {
      // List of custom emoticons
      return formatDescription("Custom emoticon list");
    }
    else if( contentType == "text/x-mms-animemoticon" )
    {
      // List of animated custom emoticons
      return formatDescription("Custom animated emoticon list");
    }
    else if( contentType == "image/gif" || contentType == "application/x-ms-ink" )
    {
      // Ink (drawing) message
      return formatDescription("Ink drawing message");
    }
    else if( contentType == "text/x-clientcaps" )
    {
      // Third party client-capabilities message
      return formatDescription("Third-party client capabilities message");
    }
    else if( contentType == "text/x-keepalive" )
    {
      // Third party keep-alive message
      return formatDescription("Third-party keep-alive message");
    }
    else if( contentType == "text/x-msmsgsinvite" )
    {
      // Old invitation method
      return formatDescription("Mime-style invitatation message");
    }
    else if( contentType == "application/x-msnmsgrp2p" )
    {
      // New invitation method
      return formatDescription("P2P-style invitation message");
    }
    else if( contentType == "text/x-msnmsgr-datacast" )
    {
      // Datacast (nudge, voice-clip)
      return formatDescription("Datacase message, nudge, wink or voice-clip");
    }


    // Unknown message type
    return formatDescription("Unknown content-type: " + contentType + "!");
  }



  // Return a log description for the message
  QString NetworkWindow::describeP2PMessage(const QByteArray &message, const int headerStart)
  {
    // Prepare to extract certain P2P header fields
    QDataStream binaryStream( message );
    binaryStream.setByteOrder( QDataStream::LittleEndian );

    // 0    4    8        16        24   28   32   36   40        48
    // |....|....|....|....|....|....|....|....|....|....|....|....|
    // |sid |mid |offset   |totalsize|size|flag|asid|auid|a-datasz |

    quint32 sessionId, messageId, dataSize, flags, ackSessionId, ackUniqueId;
    quint64 dataOffset, totalSize, ackDataSize;

    // Get header fields
    bool posFound = binaryStream.device()->seek( headerStart );
    if( ! posFound )
    {
      return formatDescription("Message could not be parsed!");
    }

    binaryStream >> sessionId;
    binaryStream >> messageId;
    binaryStream >> dataOffset;
    binaryStream >> totalSize;
    binaryStream >> dataSize;
    binaryStream >> flags;
    binaryStream >> ackSessionId;
    binaryStream >> ackUniqueId;
    binaryStream >> ackDataSize;

    switch( flags )
    {
      case 0:
      case 0x1000000:   // since WLM 2009
        // Check for abuse of the P2P system (datacast emulation)
        if( sessionId == 64 )
        {
          if( dataOffset > 0 )
          {
            QString partName( ( dataOffset + dataSize ) >= totalSize ? "last part" : "next part" );
            return formatDescription("Ink message over P2P, " + partName);
          }
          else
          {
            return formatDescription("Ink message over P2P");
          }
        }

        // Another awkward data message (webcam setup)
        if( sessionId  != 0
        &&  flags      == 0
        &&  dataSize   >= 18
        &&  message[ headerStart + 48 ] == '\x80' )
        {
          return formatDescription("Webcam setup message");
        }

        // No flags means the contents is a string with SLP-like mime fields, used to negotiate the session.
        // datasize of 0 is not handed here, is an invalid packet.
        // KMess does parse it as ACK message for compatibility with broken clients.
        if( dataSize == 4 && dataOffset == 0 )
        {
          return formatDescription("P2P Data preparation message");
        }
        else if( dataOffset > 0 )
        {
          QString partName( ( dataOffset + dataSize ) >= totalSize ? "last part" : "next part" );
          return formatDescription("P2P Session negotiation message, " + partName);
        }
        else if( sessionId == 0 && dataSize > 4 )
        {
          int preambleStart  = headerStart + 48;
          QString slpMessage ( QString::fromUtf8( message.data() + preambleStart, message.size() - preambleStart - 5 ) ); // last 5 is for the footer code.
          bool isTransfer    = slpMessage.contains("Content-Type: application/x-msnmsgr-trans");
          if( slpMessage.startsWith("INVITE ") )
          {
            return isTransfer ? formatDescription("P2P Transfer invitation message")
                              : formatDescription("P2P Session invititation message");
          }
          else if( slpMessage.startsWith("MSNSLP/") )
          {
            int statusCode = slpMessage.section(' ', 1, 1).toInt();
            if( statusCode == 200 )
            {
              return isTransfer ? formatDescription("P2P Transfer accept message")
                                : formatDescription("P2P Session accept message");
            }
            else if( statusCode == 404 )
            {
              return formatDescription("P2P Session error message");
            }
            else if( statusCode == 481 )
            {
              return formatDescription("P2P Session error message");
            }
            else if( statusCode == 500 )
            {
              return formatDescription("P2P Session error message");
            }
            else if( statusCode == 603 )
            {
              return isTransfer ? formatDescription("P2P Transfer decline message")
                                : formatDescription("P2P Session decline message");
            }
            else
            {
              formatDescription("P2P Session status message, type is unknown!");
            }
          }
          else if( slpMessage.startsWith("ACK ") )
          {
            return formatDescription("P2P Session acknowledgement message (meaning unknown!)");
          }
          else if( slpMessage.startsWith("BYE ") )
          {
            return formatDescription("P2P Session bye message");
          }
          else
          {
            return formatDescription("P2P Session negotiation message, type is unknown!");
          }
        }
        break;

      case 0x01:
        return formatDescription("P2P Negative ack, message with ID " + QString::number(ackSessionId) +
                                 " failed at byte " + QString::number(ackDataSize));

      case 0x02:
        if( dataSize == 4 )
        {
          return formatDescription("P2P Data preparation message (invalid flag!)");
        }
        else if ( ackDataSize == 4 )
        {
          return formatDescription("P2P Data preparation ack");
        }
        else
        {
          return formatDescription("P2P Ack message for message with"
                                   " ID "         + QString::number(ackSessionId) +
                                   " and ackSid " + QString::number(ackUniqueId));
        }

      case 0x04:
        return formatDescription("P2P Timeout message, session should terminate");

      case 0x06:
        return formatDescription("P2P Timeout message, still waiting for ACK message");

      case 0x08:
        // The logs of the official client call this a "RST" message.
        return formatDescription("P2P Reset message for message with "
                                 "ackSid " + QString::number(ackSessionId) + ", session should terminate");

      case 0x20:
      case 0x1000020:   // since WLM 2009
        return formatDescription("P2P MsnObject data,"
                                 " bytes " + QString::number(dataOffset) + "-" + QString::number(dataOffset + dataSize) +
                                 " of " + QString::number(totalSize));

      case 0x40:
        return formatDescription("P2P Close ack, sender of message with ID " + QString::number(ackSessionId) + " aborted it's own data stream");

      case 0x80:
        return formatDescription("P2P Close ack, receiver of message with ID " + QString::number(ackSessionId) + " aborted the data stream");

      case 0x100:
        return formatDescription("P2P Direct-connection handshake");

      case 0x1000030:
        return formatDescription("P2P File data,"
                                 " bytes " + QString::number(dataOffset) + "-" + QString::number(dataOffset + dataSize) +
                                 " of " + QString::number(totalSize));
    }

    return formatDescription("P2P Message, type is unknown!");
  }



  // Destroy the singleton.
  void NetworkWindow::destroy()
  {
    if( ! instance_ )
    {
      // The singleton has already been destroyed, or was never used
      return;
    }

    delete instance_;
    instance_ = 0;
  }



  // Format the description to be displayed
  QString NetworkWindow::formatDescription(const QString &description)
  {
    return "<br>" + getTime() + "<font color='#333333'>&nbsp; " + description + "</font>\n";
  }



  // Format the message to be displayed
  QString NetworkWindow::formatMessage(ConnectionEntry *entry, bool incoming, const QByteArray &message)
  {
    // If the connection type is unknown, auto-detect it.
    if( entry->type == TYPE_UNKNOWN )
    {
      // Try to detemine which type of connection this is.
      // TODO: As of Qt4 use QByteArray functions instead of memcmp().
      if( ( message.size() == 4 && memcmp(message.data(), "\x04" "\0\0\0",      4) == 0 )
       || ( message.size() == 8 && memcmp(message.data(), "\x04" "\0\0\0foo\0", 8) == 0 ) )
      {
        // Start of direct connection: size field of 4 + foo\0
        entry->type = TYPE_DC;
      }
      else if( message.size() >= 10 )
      {
        if( memcmp(message.data(), "VER MSNFTP", 10) == 0 )
        {
          entry->type = TYPE_FTP;
        }
        else if( memcmp(message.data(), "VER 0 MSNP", 10) == 0
              || memcmp(message.data(), "VER 1 MSNP", 10) == 0 )
        {
          entry->type = TYPE_NS;
        }
        else if( memcmp(message.data(), "USR ", 4) == 0
              || memcmp(message.data(), "ANS ", 4) == 0)
        {
          entry->type = TYPE_SB;
        }
        else if( memcmp(message.data(), "GET ",  4) == 0
              || memcmp(message.data(), "POST ", 5) == 0
              || memcmp(message.data(), "<?xml", 5) == 0)
        {
          entry->type = TYPE_HTTP;
        }
      }

      // If no type is found, assume it's a standard MSN connection.
      if( entry->type == TYPE_UNKNOWN )
      {
        kWarning() << "Could not determine connection type for message: " << message.data();
        entry->type = TYPE_NS;
      }
    }


    // Format the message.
    QString logMessage;
    switch( entry->type )
    {
      // Messages from a DC are always sent as binary
      case TYPE_DC:
        if( message.size() < 48 )
        {
          // Not a p2p message, but the length field sent in advance.
          logMessage += formatRawData(incoming, message, 0, message.size());
          logMessage += describeDirectConnectionPreamble(message);
        }
        else
        {
          // A p2p message from the direct connection
          logMessage += formatP2PMessage(incoming, message, 0);
          logMessage += describeP2PMessage(message, 0);
        }
        break;

      // After the TFR is received, the FTP connection sends raw data.
      case TYPE_FTP_RAW:
        if( message.size() == 3 )
        {
          // This is a control message
          logMessage += formatRawData(incoming, message, 0, message.size());
          if( message[0] == 0 )
          {
            logMessage += formatDescription("Indicates the size of the next block");
          }
          else if( memcmp(message.data(), "\x01" "\0\0", 3) == 0 )
          {
            logMessage += formatDescription("Cancel message");
          }
          break;
        }
        else if( message.size() == 14 && memcmp(message.data(), "BYE 16777989\r\n", 14) == 0 )
        {
          // This is the last BYE message
          entry->type = TYPE_FTP;
          // fallthrough
        }
        else
        {
          logMessage += "<font color=gray>(" + QString::number(message.size()) + " bytes of binary data)</font>";
          break;
        }

      // Assume it's a standard connection.
      default:
        // Convert the message to UTF-8
        QString utf8Message( QString::fromUtf8( message.data(), message.size() ) );

        // Treat Mime messages with P2P content different.
        if( utf8Message.contains("Content-Type: application/x-msnmsgrp2p") )
        {
          // Find end of Mime, start of binary P2P
          int mimeEnd = utf8Message.indexOf("\r\n\r\n");
          if( mimeEnd != -1 )
          {
            // Extract the first utf-8 mime part, locate start of p2p header
            QString p2pMimePart( utf8Message.left(mimeEnd + 4) );
            int     headerStart = p2pMimePart.toUtf8().size();  // convert mimeEnd index, utf-8 may have double characters

            // Add the mime part and binary p2p part
            logMessage += formatString( p2pMimePart );
            logMessage += formatP2PMessage(incoming, message, headerStart);
            logMessage += describeP2PMessage(message, headerStart);
          }
        }
        else
        {
          // Whole message is in utf-8

          // First hide things we don't want to log.
          if( entry->type == TYPE_HTTP )
          {
            // Hide passwords from the passport login, if they are included in the message
            int passStart = utf8Message.indexOf("<wsse:Password>");
            if( passStart != -1 )
            {
              passStart += 15;  // size of xml tag
              int passEnd = utf8Message.indexOf("</wsse:Password>", passStart);
              int passLen = ( passEnd - passStart );
              utf8Message = utf8Message.replace( passStart, passLen, QString( passLen, '*' ) );
            }
          }

          // Convert to HTML.
          // Remove last <br> because KTextBrowser::append() already adds it.
          logMessage = formatString( utf8Message );
          logMessage = logMessage.replace(QRegExp("<br>$"), QString::null);

          if( entry->type == TYPE_FTP
           || entry->type == TYPE_NS
           || entry->type == TYPE_SB )
          {
            if( utf8Message.startsWith("MIME-Version:") )
            {
              logMessage += describeMimeMessage( utf8Message );
            }
            else
            {
              logMessage += describeCommand( utf8Message );
            }
          }
        }
    }

    // At a certain point, a MSNFTP connection starts sending raw data.
    // Detect this so the next formatMessage() invocation handles this.
    if( entry->type == TYPE_FTP )
    {
      if( message.size() >= 3 && memcmp(message.data(), "TFR", 3) == 0 )
      {
        entry->type = TYPE_FTP_RAW;
      }
    }

    return logMessage;
  }



  // Format a msnp2p message to be displayed.
  QString NetworkWindow::formatP2PMessage(bool incoming, const QByteArray &message, const int headerStart)
  {
    QString logMessage;

    // Prepare to extract certain P2P header fields
    QDataStream binaryStream( message );
    binaryStream.setByteOrder( QDataStream::LittleEndian );
    int p2pDataStart = headerStart + 48;
    bool posFound;

    // 0    4    8        16        24   28   32   36   40        48
    // |....|....|....|....|....|....|....|....|....|....|....|....|
    // |sid |mid |offset   |totalsize|size|flag|asid|auid|a-datasz |

    quint32 sessionId, messageId, dataSize, flags, ackSessionId, ackUniqueId;
    quint64 dataOffset, totalSize, ackDataSize;

    // Get header fields
    posFound = binaryStream.device()->seek( headerStart ); // return value ignored.
    binaryStream >> sessionId;
    binaryStream >> messageId;
    binaryStream >> dataOffset;
    binaryStream >> totalSize;
    binaryStream >> dataSize;
    binaryStream >> flags;
    binaryStream >> ackSessionId;
    binaryStream >> ackUniqueId;
    binaryStream >> ackDataSize;

    #ifdef KMESSTEST
      KMESS_ASSERT( posFound );
    #endif

    // If the data size is completely wrong, we're dealing with partially sent data here,
    // caused by DirectConnectionBase::slotSocketReadyWrite().
    if( dataSize > 3000 )
    {
      return "<font color=gray>(" + QString::number( message.size() ) + " bytes of binary data, flushed afterwards, can't parse)</font>";
    }

    // Show header information
    logMessage += formatRawData(incoming, message, headerStart, 48, 4, 16);

    // Add header debug details
    logMessage += "<font color='#333333'>";
    logMessage += "\n<br>session id: " + QString::number(sessionId);
    logMessage += "\n<br>message id: " + QString::number(messageId);
    logMessage += "\n<br>offset: "     + QString::number(dataOffset);
    logMessage += "\n<br>total: "      + QString::number(totalSize);
    logMessage += "\n<br>size: "       + QString::number(dataSize);
    logMessage += "\n<br>flags: 0x"    + QString::number(flags, 16);
    if( flags == 0x100 )
    {
      // For the handshake message, the last three fields are replaced with the nonce.
      logMessage += "\n<br>nonce: " + P2PMessage::extractNonce( message.data(), headerStart + 32 );
    }
    else
    {
      logMessage += "\n<br>ackSid: "  + QString::number(ackSessionId);
      logMessage += "\n<br>ackUid: "  + QString::number(ackUniqueId);
      logMessage += "\n<br>ackSize: " + QString::number(ackDataSize);
    }

    logMessage += "</font>";

    // The message a utf-8 (MSNSLP) payload if the session id is zero.
    const char *dataPtr = ( message.data() + p2pDataStart );
    bool hasSlpPayload  = ( sessionId == 0 && dataSize > 0 );
    int  safeEnd        = qMin( message.size(), (int) ( p2pDataStart + dataSize ) );
    int  safeSize       = qMin( (int) dataSize, ( message.size() - p2pDataStart ) );
    if( hasSlpPayload )
    {
      // See if the SLP body is terminated with a final null character (should be).
      int  slpLength     = safeSize;
      bool hasSlpNullEnd = ( message[ p2pDataStart + slpLength - 1 ] == 0x00 );
      if( hasSlpNullEnd )
      {
        slpLength--;
      }

      // Display the SLP body
      QString slpMessage( QString::fromUtf8( dataPtr, slpLength ) );
      logMessage += "\n<br>" + formatString( slpMessage );

      // Add message footer
      if( hasSlpNullEnd )
      {
        // Display the \0 that was dropped in the QString::fromUtf8() call.
        logMessage += formatRawData( incoming, message, slpLength, 1 );
      }
    }
    else if( dataSize >= 18
         &&  message.size() > p2pDataStart
         &&  message[ p2pDataStart ] == '\x80' )
    {
      // Webcam invitation
      QString camMessage( QString::fromUtf16( reinterpret_cast<const ushort *>( dataPtr + 10 ), ( safeSize - 10 - 1 ) / 2 ) );

      logMessage += "\n<br>" + formatRawData( incoming, message, p2pDataStart, 10, 1 ) // 1 per col.
                  + "\n<br>" + formatString( camMessage )
                  + "\n<br>" + formatRawData( incoming, message, safeEnd - 2, 2 );  // final null (in utf16).
    }
    else
    {
      // Don't display the data.
      logMessage += "\n<br><font color=gray>(" + QString::number(dataSize) + " bytes of binary data)</font>";
    }

    // Display the remaining bytes, if any.
    // This is the footer when the p2p message is sent over the SB.
    if( safeEnd < message.size() )
    {
      logMessage += "<br>" + formatRawData( incoming, message, safeEnd, message.size() - safeEnd );
    }

    return logMessage;
  }



  // Format a utf-8 string to be displayed
  QString NetworkWindow::formatRawData(bool incoming, const QByteArray &message, const int start, const int length, const int bytesPerCol, const int bytesPerRow)
  {
    QString logMessage;
    logMessage += "<tt><font color=";
    logMessage += (incoming? "#4747C2" : "#CC334D");
    logMessage += ">";

    // If length field is corrupt (e.g. 432712149, avoid looping though 4GB of memory..
    int safeLength = qMin( length, message.size() - start );

    for(int i = 0; i < safeLength; i++)
    {
      // Add spacers between ints, make it span multiple rows.
      if( i > 0 )
      {
        if( i % bytesPerRow == 0 )
        {
          logMessage += "<br>";
        }
        else if( i % bytesPerCol == 0 )
        {
          logMessage += " ";
        }
      }

      // Convert to hex string
      const char *hexMap = "0123456789abcdef";
      char byte = message[start + i];
      int upper = (byte & 0xf0) >> 4;
      int lower = (byte & 0x0f);
      logMessage += hexMap[upper];
      logMessage += hexMap[lower];
    }
    logMessage += "</font></tt>";

    return logMessage;
  }



  // Format a utf-8 string to be displayed
  QString NetworkWindow::formatString(const QString &message)
  {
    // Parse any HTML characters.
    QString logMessage( Qt::escape( message ) );

    // Format newline characters
    logMessage = logMessage.replace("\r\n", "<tt><font color=gray>\\r\\n</font></tt><br>");
    logMessage = logMessage.replace("\n",   "<tt><font color=gray>\\n</font></tt><br>");
    logMessage = logMessage.replace("\r",   "<tt><font color=gray>\\r</font></tt><br>");

    return logMessage;
  }



  // Find the tab for the connection, or create a new one.
  NetworkWindow::ConnectionEntry * NetworkWindow::getConnectionEntry(QObject *connection)
  {
    #ifdef KMESSTEST
      KMESS_ASSERT( connection != 0 );  // no KMESS_NULL(), this is debug code already.
    #endif

    // Find the entry
    ConnectionEntry *entry = connectionViews_.value( connection, 0 );

    // If the view didn't exist before, create a new one.
    if( entry == 0 )
    {
      // Create new entry
      entry = new ConnectionEntry;
      entry->logView = new KTextBrowser( connectionTabs_ );
      entry->logView->setObjectName( connection->objectName() + "_log" );
      entry->type        = TYPE_UNKNOWN;
      entry->lastMessage = QDateTime::currentDateTime();
      connectionViews_.insert( connection, entry );

      // Add new tab
      connectionTabs_->addTab( entry->logView, KIcon( "edit-select-all" ), connection->objectName() );

      // Enable the Close All Tabs button
      enableButton( User3, ( connectionViews_.count() > 1 ) );
    }

    // Initialize new connections and reinitialize old ones as open
    entry->isClosed = false;

    return entry;
  }



  // Return the current time.
  QString NetworkWindow::getTime() const
  {
    return "<font color=black size=1>" + QTime::currentTime().toString( "hh:mm:ss:zzz" ) + "</font>";
  }



  // Set the title of the connection in the display
  void NetworkWindow::setConnectionTitle(QObject *connection, const QString &title)
  {
    ConnectionEntry *entry = getConnectionEntry(connection);

    // If empty, set the initial label. Otherwise add a IRC-like rename message
    if( entry->type == TYPE_UNKNOWN )
    {
      connectionTabs_->setTabText( connectionTabs_->indexOf( entry->logView ), title );
      entry->logView->append("<p><font color='#C2C2C2'>Log started by " + QString(connection->objectName()) + "</font></p>");
    }
    else
    {
      entry->logView->append("<p><font color='#C2C2C2'>re-initialized as: " + title + "</font></p>");
    }
  }



  // Show the window if there are connection logs to show
  void NetworkWindow::show()
  {
    if( connectionTabs_->count() < 1 )
    {
      KMessageBox::error( this, i18n("No connections are present.\nCannot open the Network Window.") );
      return;
    }

    KDialog::show();
  }



  // The 'save tab' button was pressed.
  void NetworkWindow::saveCurrentTab()
  {
    if( connectionTabs_->currentWidget() == 0 )
      return;

    QString path;
    QString name( connectionTabs_->tabText( connectionTabs_->currentIndex() ) );

    path = KFileDialog::getSaveFileName( KUrl(), "*.html" );
    if( path.isEmpty() )
      return;

    kDebug() << "NetworkWindow - saving network log for window '" << name << "' to '" << path << "'.";

    bool failed = false;

    // Create and open the file.
    QFile file( path );
    if( ! file.open( QIODevice::WriteOnly ) )
    {
      failed = true;
      kWarning() << "File save failed - couldn't open file.";
    }

    // Output the HTML with the right text encoding
    QString text( static_cast<KTextBrowser *>( connectionTabs_->currentWidget() )->toHtml() );
    QTextStream textStream( &file );
    textStream.setCodec( QTextCodec::codecForLocale() );
    textStream << text;
    file.close();
    file.flush();

    if( failed || ! file.exists() )
    {
      KMessageBox::sorry( this, i18n("Could not save the Network Window log. Make sure you have permission to write in the folder where it is being saved.") );
    }
  }



  // The 'clear tab' button was pressed.
  void NetworkWindow::clearCurrentTab()
  {
    if( connectionTabs_->currentWidget() == 0 )
      return;

    static_cast<KTextBrowser *>( connectionTabs_->currentWidget() )->clear();
  }



  // The 'close all tabs' button was pressed.
  void NetworkWindow::closeAllTabs()
  {
    // Activate the tab of the main connection
    connectionTabs_->setCurrentIndex( 0 );

    // Get the main connection widget
    QWidget *mainConnectionWidget = connectionTabs_->widget( 0 );

    // Loop through all tabs
    foreach( ConnectionEntry *entry, connectionViews_ )
    {
      // Don't close the main connection tab
      if( entry->logView == mainConnectionWidget )
      {
        continue;
      }

      connectionTabs_->removeTab( connectionTabs_->indexOf( entry->logView ) );
      closeTab( entry->logView );
    }
  }



  // A 'close tab' button was pressed.
  void NetworkWindow::closeTab( QWidget *removedWidget )
  {
    if( connectionTabs_->indexOf( removedWidget ) == 0 )
    {
      KMessageBox::information( this, i18n("Cannot close the main connection tab.") );
      return;
    }

    // Remove the widget from the list of views
    QHashIterator<void*,ConnectionEntry*>it( connectionViews_ );
    while( it.hasNext() )
    {
      it.next();
      if( it.value()->logView == removedWidget )
      {
        connectionViews_.remove( it.key() );
        delete it.value();
        break;
      }
    }

    // Delete the page of the removed item
    delete removedWidget;

    // Disable the Close All Tabs button when there are no tabs
    enableButton( User3, ( connectionViews_.count() > 1 ) );
  }



  // Return the primary instance of the network window.
  NetworkWindow* NetworkWindow::instance()
  {
    // Create on demand
    if( instance_ == 0 )
    {
      instance_ = new NetworkWindow();
    }

    // Return active instance
    return instance_;
  }


  // A tab was selected.
  void NetworkWindow::slotUpdateWidgets( int selectedTab )
  {
    bool enable = false;

    // Automatically obtain the tab index instead of getting it as a parameter
    if( selectedTab < -1 )
    {
      selectedTab = connectionTabs_->currentIndex();
    }

    // If there are open tabs, find out which one is selected.
    // If there are none, just disable the command sending group
    if( selectedTab != -1 )
    {
      QWidget *currentWidget = connectionTabs_->currentWidget();
      QHashIterator<void*,ConnectionEntry*> it( connectionViews_ );

      // Iterate through all connections to find whether the current one is connected or not
      while( it.hasNext() )
      {
        it.next();
        if( it.value()->logView == currentWidget )
        {
          // The active tab's connection is active, enable the widgets
          if( ! it.value()->isClosed )
          {
            enable = true;
          }
          break;
        }
      }
    }

    commandSendingGroup_->setEnabled( enable );
  }



  // Send a command
  void NetworkWindow::slotSendCommand()
  {
    // Get the text fields' text
    QString command( commandEdit_->text().toUpper() );
    QString payload( payloadEdit_->toPlainText() );

    // Empty commands can't be sent
    if( command.isEmpty() || connectionTabs_->currentIndex() == -1 )
    {
      commandEdit_->setFocus();
      return;
    }


    // Warn the developer about what he or she is doing.
    int result = KMessageBox::warningContinueCancel( this,
                                                     i18n( "Sending commands to the server is a risky operation.<br />"
                                                           "If you do not know how to exactly do it, "
                                                           "you could be lucky and just get disconnected, or <i>you may "
                                                           "incur in more serious consequences</i>.<br />"
                                                           "You have been warned!<br />"
                                                           "<b>Do you want to continue sending this message?</b>" ),
                                                     i18n( "Network Window" ),
                                                     KStandardGuiItem::cont(),
                                                     KStandardGuiItem::cancel(),
                                                     "sendServerCommandWarning" );
    // Bail out, just in time
    if( result != KMessageBox::Continue )
    {
      return;
    }

    // The developer knows what he or she is doing, send the command

    // Replace all newlines with the kind used by the server
    payload.replace( QRegExp( "\r?\n" ), "\r\n" );

    // Empty payloads can, and will be replaced with a single newline
    if( payload.isEmpty() )
    {
      payload = "\r\n";
    }
    else if( ! payload.endsWith( "\r\n" ) )
    {
      // Warn the developer that the payloads must end with a newline
      int result = KMessageBox::warningYesNo( this,
                                              i18n( "The payload you are trying to send does not end with the "
                                                    "required newline ('\\r\\n')!<br />"
                                                    "Do you want KMess to add it for you?" ),
                                                    i18n( "Network Window" ),
                                                    KStandardGuiItem::yes(),
                                                    KStandardGuiItem::no(),
                                                    "noPayloadWarning" );
      // Append the missing newline to the payload
      if( result == KMessageBox::Yes )
      {
        payload.append( "\r\n" );
      }
    }

    // Find the currently active tab
    QWidget *currentWidget = connectionTabs_->currentWidget();
    QHashIterator<void*,ConnectionEntry*> it( connectionViews_ );
    void *connection;
    const ConnectionEntry *entry;

    // Iterate through all connections to find out which one's tab is selected in the UI
    while( it.hasNext() )
    {
      it.next();

      // Save the entry, we'll need it later
      entry = it.value();

      // If the current tab is for this connection..
      if( entry->logView == currentWidget )
      {
        // .. disallow sending commands to disconnected connections
        if( entry->isClosed )
        {
          commandEdit_->setFocus();
          return;
        }

        // ..save the tab's connection to send commands there
        connection = it.key();
        break;
      }
    }

    // Send the command to the right type of connection
    MsnNotificationConnection *nsConnection;
    switch( entry->type )
    {
      case TYPE_NS:
      case TYPE_SB:
        nsConnection = static_cast<MsnNotificationConnection*>( connection );

        // Disallow sending commands to disconnected connections
        if( ! nsConnection->isConnected() )
        {
          return;
        }

        // Send the type of command the developer wants
        if( sendStandardCmdRadio_->isChecked() )
        {
          nsConnection->sendCommand( command, payload );
        }
        else
        {
          nsConnection->sendMimeMessage( MsnNotificationConnection::ACK_ALWAYS, MimeMessage( payload ) );
        }
        return;

      // TODO: Enable sending commands to these types of connections.
      case TYPE_UNKNOWN:
      case TYPE_FTP:
      case TYPE_FTP_RAW:
      case TYPE_DC:
      case TYPE_HTTP:
      default:
        KMessageBox::error( 0, i18n( "Cannot send commands to this kind of connection!" ) );
        return;
    }
  }


#include "networkwindow.moc"
