/***************************************************************************
                          directconnectionpool.cpp -  description
                             -------------------
    begin                : Thu 1 5 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 "directconnectionpool.h"
#include "directconnectionbase.h"

#include "../../kmessdebug.h"


#ifdef KMESSDEBUG_DIRECTCONNECTION
  #define KMESSDEBUG_DIRECTCONNECTION_GENERAL
#endif



// The constructor
DirectConnectionPool::DirectConnectionPool( bool waitForAuthorized )
  : QObject()
  , activeConnection_(0)
  , waitForAuthorized_( waitForAuthorized )
{
}



// The destructor
DirectConnectionPool::~DirectConnectionPool()
{
#ifdef KMESSDEBUG_DIRECTCONNECTION_GENERAL
  kmDebug() << "Deleting pending sockets and active connection.";
#endif

  clearPending();

  // Remove the active connection if it was still active.
  if( activeConnection_ != 0 )
  {
    activeConnection_->blockSignals( true );
    activeConnection_->deleteLater();
    activeConnection_ = 0;
  }

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



/**
 * Add a connection to the list, tells the object to connect to the given ipaddress/port.
 * Returns true when the connection could be added to the pending list (e.g. it's openConnection() method didn't fail).
 */
bool DirectConnectionPool::addConnection(DirectConnectionBase *connection, const QString &ipAddress, const quint16 port)
{
  // Refuse if there is already an active connection
  if( activeConnection_ != 0 )
  {
    kmWarning() << "Refusing connection attempt, a connection has already been made.";
    return 0;
  }

#ifdef KMESSDEBUG_DIRECTCONNECTION_GENERAL
  kmDebug() << "Adding new client connection to peer "
            << ipAddress << ":" << port << endl;
#endif

  pendingConnections_.append( connection );  // append to pending list before signals are fired.

  // Connect it
  connect( connection, SIGNAL(     connectionEstablished() ) ,  // The connection was established
           this,         SLOT( slotConnectionEstablished() ) );
  connect( connection, SIGNAL(          connectionFailed() ) ,  // The connection could not be made
           this,         SLOT(      slotConnectionFailed() ) );
  connect( connection, SIGNAL(          connectionClosed() ) ,  // The connection was closed
           this,         SLOT(      slotConnectionClosed() ) );
  connect( connection, SIGNAL(      connectionAuthorized() ) ,  // The connection was authorized
           this,         SLOT(  slotConnectionAuthorized() ) );

  // Connect the the host
  bool opened = connection->openConnection( ipAddress, port );
  if( ! opened )
  {
    pendingConnections_.removeAll( connection );
    connection->deleteLater();

#ifdef KMESSDEBUG_DIRECTCONNECTION_GENERAL
    kmDebug() << "Could not connect to " << ipAddress << ":" << port;
#endif
    return false;
  }

  return true;
}



/**
 * Add a connection to the list, starts listening for incoming connections.
 */
int DirectConnectionPool::addServerConnection(DirectConnectionBase *connection)
{
  // Refuse if there is already an active connection
  if( activeConnection_ != 0 )
  {
    kmWarning() << "Refusing server connection attempt, a connection has already been made.";
    return 0;
  }

#ifdef KMESSDEBUG_DIRECTCONNECTION_GENERAL
  kmDebug() << "Adding new server connection to listen at port" << connection->getLocalServerPort();
#endif

  pendingConnections_.append( connection );  // append to pending list before signals are fired.

  // Connect it
  connect( connection, SIGNAL(     connectionEstablished() ) ,  // The connection was established
           this,         SLOT( slotConnectionEstablished() ) );
  connect( connection, SIGNAL(          connectionFailed() ) ,  // The connection could not be made
           this,         SLOT(      slotConnectionFailed() ) );
  connect( connection, SIGNAL(          connectionClosed() ) ,  // The connection was closed
           this,         SLOT(      slotConnectionClosed() ) );
  connect( connection, SIGNAL(      connectionAuthorized() ) ,  // The connection was authorized
           this,         SLOT(  slotConnectionAuthorized() ) );

  bool listening = false;

  // Attempt to listen at one of the 10 ports we use.
  // FIXME: loop from start range to end.
  for(int i = 0; i < 10; i++)
  {
    // The openServerPort() automatically picks another port
    // if the previous call failed.
    listening = connection->openServerPort();

    if(listening)
    {
      break;
    }
  }

  if( ! listening )
  {
    pendingConnections_.removeAll( connection );
    connection->deleteLater();

    kmWarning() << "No available free port could be found";
    return 0;
  }

  // Return port we're listening at.
  return connection->getLocalServerPort();
}



/**
 * Remove all connections from the list.
 */
void DirectConnectionPool::clearPending()
{
  // Avoid unwanted debug messages..
  if( pendingConnections_.isEmpty() && unauthorizedConnections_.isEmpty() )
  {
    return;
  }

#ifdef KMESSDEBUG_DIRECTCONNECTION_GENERAL
  kmDebug() << "Removing pending sockets.";
#endif

  // Tell all other pending connections to abort
  foreach( DirectConnectionBase *pendingConnection, pendingConnections_ )
  {
#ifdef KMESSDEBUG_DIRECTCONNECTION_GENERAL
    kmDebug() << "Deleting pending connection to peer "
             << pendingConnection->getRemoteIp() << ":" << pendingConnection->getRemotePort() << endl;
#endif

    pendingConnection->deleteLater();  // delete should not be called from a slot.
  }

  // Tell all other unauthorized connections to abort
  foreach( DirectConnectionBase *unauthorizedConnection, unauthorizedConnections_ )
  {
#ifdef KMESSDEBUG_DIRECTCONNECTION_GENERAL
    kmDebug() << "Deleting unauthorized connection to peer "
             << unauthorizedConnection->getRemoteIp() << ":" << unauthorizedConnection->getRemotePort() << endl;
#endif

    unauthorizedConnection->deleteLater();  // delete should not be called from a slot.
  }

  // Clear the list
  pendingConnections_.clear();
  unauthorizedConnections_.clear();
}



/**
 * Return the active connection.
 */
DirectConnectionBase * DirectConnectionPool::getActiveConnection() const
{
  return activeConnection_;
}



/**
 * Indicate whether the is a active connection or not.
 */
bool DirectConnectionPool::hasActiveConnection() const
{
  return (activeConnection_ != 0);
}



/**
 * Indicate whether there are connections pending or not.
 */
bool DirectConnectionPool::hasPendingConnections() const
{
  return ( ! pendingConnections_.isEmpty()
        || ! unauthorizedConnections_.isEmpty() );
}



/**
 * A direct connection was authorized
 */
void DirectConnectionPool::slotConnectionAuthorized()
{
#ifdef KMESSDEBUG_DIRECTCONNECTION_GENERAL
  kmDebug() << "A direct connection was authorized.";
#endif

  // Find out which object sent the signal
  const QObject *eventSender = sender();
  if(KMESS_NULL(eventSender)) return;
#ifdef KMESSTEST
  KMESS_ASSERT( eventSender->inherits("DirectConnectionBase") );
#endif

  // Signal to ApplicationList
  DirectConnectionBase *connection = static_cast<DirectConnectionBase*>(const_cast<QObject*>(eventSender));

  // If the pool had to wait for an authorized connection, it's done now!
  if( waitForAuthorized_ )
  {
#ifdef KMESSTEST
    KMESS_ASSERT( activeConnection_ == 0 );
#endif

    // Make active now.
    activeConnection_ = connection;
    unauthorizedConnections_.removeAll( connection );
    clearPending();

    // Signal the connection is established, allows to sent any handshake, etc..
    emit connectionEstablished();
  }

#ifdef KMESSTEST
  KMESS_ASSERT( connection == activeConnection_ );
#else
  Q_UNUSED( connection ); // Avoid compiler warning
#endif
  emit activeConnectionAuthorized();
}



/**
 * A direct connection was closed
 */
void DirectConnectionPool::slotConnectionClosed()
{
  // Find out which object sent the signal
  const QObject *eventSender = sender();
  if(KMESS_NULL(eventSender)) return;
#ifdef KMESSTEST
  KMESS_ASSERT( eventSender->inherits("DirectConnectionBase") );
#endif

  // Remove the connection
  DirectConnectionBase *connection = static_cast<DirectConnectionBase*>(const_cast<QObject*>(eventSender));
  pendingConnections_.removeAll( connection );
  unauthorizedConnections_.removeAll( connection );

  // Signal when the active connection closed
  if(connection == activeConnection_)
  {
#ifdef KMESSDEBUG_DIRECTCONNECTION_GENERAL
    kmDebug() << "Closed connection was the active connection!";
#endif

    emit activeConnectionClosed();
    activeConnection_ = 0;  // reset, will deleter later
  }
  else
  {
#ifdef KMESSDEBUG_DIRECTCONNECTION_GENERAL
    kmDebug() << "Closed connection was another pending socket from the pool.";
#endif

    // Check whethere there is still a chance to get a direct connection
    if( activeConnection_ == 0 && ! hasPendingConnections() )
    {
#ifdef KMESSDEBUG_DIRECTCONNECTION_GENERAL
      kmDebug() << "No remaining direct connections left.";
#endif

      // Emit signal for fallback (e.g. sending files over the switchboard)
      emit allConnectionsFailed();
    }
  }

  // delete the connection
  connection->blockSignals( true );
  connection->deleteLater();
}



/**
 * A direct connection is established.
 */
void DirectConnectionPool::slotConnectionEstablished()
{
#ifdef KMESSDEBUG_DIRECTCONNECTION_GENERAL
  kmDebug() << "Direct connection established.";
#endif

  // Test whether two connection attempts were established quickly after each other.
  if( activeConnection_ != 0 )
  {
#ifdef KMESSDEBUG_DIRECTCONNECTION_GENERAL
    kmDebug() << "Already got an active connection, removing other connections.";
#endif
    clearPending();
    return;
  }

  // No connection yet, find the first successful connection.
  foreach( DirectConnectionBase *pendingConnection, pendingConnections_ )
  {
    if( pendingConnection->isConnected() )
    {
#ifdef KMESSDEBUG_DIRECTCONNECTION_GENERAL
      kmDebug() << "Connected to peer "
               << pendingConnection->getRemoteIp() << ":" << pendingConnection->getRemotePort() << endl;
#endif

      // Established connection found, remove from list.
      pendingConnections_.removeAll( pendingConnection );

      // Try to initialize the connection.
      bool success = pendingConnection->initialize();
      if( ! success )
      {
        // Not initialized (e.g. preamble cound not be sent in case of a MsnDirectConnection),
        // try the next item in the list instead.
#ifdef KMESSDEBUG_DIRECTCONNECTION_GENERAL
        kmDebug() << "could not initialize connection, "
                    "trying next available connection." << endl;
#endif
        pendingConnection->deleteLater();
        pendingConnection = 0;
        continue;
      }
      else
      {
        // For webcam connections at port 80: wait until connection is authorized too.
        if( waitForAuthorized_ )
        {
#ifdef KMESSDEBUG_DIRECTCONNECTION_GENERAL
          kmDebug() << "by request, waiting for the connection to authorize first...";
#endif

          unauthorizedConnections_.append( pendingConnection );
          return;
        }

        // Initiated, this is the active connection!
        activeConnection_ = pendingConnection;
        break;
      }
    }
  }

  // If there is an initialized connection, remove all other pending connections.
  if( activeConnection_ != 0 )
  {
    clearPending();

    // Signal the connection is established, allows to sent any handshake, etc..
    emit connectionEstablished();
  }
}



/**
 * A direct connection could not be made.
 */
void DirectConnectionPool::slotConnectionFailed()
{
#ifdef KMESSDEBUG_DIRECTCONNECTION_GENERAL
  kmDebug() << "A direct connection failed.";
#endif

  // Find out which object sent the signal
  const QObject *eventSender = sender();
  if(KMESS_NULL(eventSender)) return;
#ifdef KMESSTEST
  KMESS_ASSERT( eventSender->inherits("DirectConnectionBase") );
#endif

  // Remove the connection
  DirectConnectionBase *connection = static_cast<DirectConnectionBase*>(const_cast<QObject*>(eventSender));
  pendingConnections_.removeAll( connection );

  // If there is still no connection made
  if( activeConnection_ == 0 )
  {
    // If there are no remaining pending connections
    if( ! hasPendingConnections() )
    {
#ifdef KMESSDEBUG_DIRECTCONNECTION_GENERAL
      kmDebug() << "No remaining direct connections left.";
#endif

      // Emit signal for fallback (e.g. sending files over the switchboard)
      emit allConnectionsFailed();
    }
    else
    {
#ifdef KMESSDEBUG_DIRECTCONNECTION_GENERAL
      kmDebug() << "There are " << ( pendingConnections_.count() + unauthorizedConnections_.count() ) << " remaining direct connections, waiting for one to succeed.";
#endif
    }
  }

  // If the current connection failed
  if( connection == activeConnection_ )
  {
    // This really shouldn't happen
    kmWarning() << "The active connection failed!";

    // Reset current object
    activeConnection_ = 0;

    // Emit signal so some fallback can occur
    emit allConnectionsFailed();
  }

#ifdef KMESSDEBUG_DIRECTCONNECTION_GENERAL
  kmDebug() << "Deleting failed connection.";
#endif

  // Delete the failed connection object.
  connection->deleteLater();
}



/**
 * Verify whether the currently active connection is still connected.
 * If the connection appears to be closed or timed out, it will be deleted.
 * @return Returns false when the connection was invalid and deleted, true otherwise.
 */
bool DirectConnectionPool::verifyActiveConnection()
{
  if( activeConnection_ != 0
  && ( ! activeConnection_->isConnected() || activeConnection_->hasTimedOut() ) )
  {
#ifdef KMESSDEBUG_DIRECTCONNECTION_GENERAL
    kmDebug() << "Active connection is no longer connected, deleting connection.";
#endif

    // Delete connection
    activeConnection_->blockSignals( true );
    activeConnection_->deleteLater();
    activeConnection_ = 0;

    return false;
  }
  else
  {
    return true;
  }
}



#include "directconnectionpool.moc"
