/*
    Copyright (c) 2009 Kevin Ottens <ervin@kde.org>

    Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
    Author: Kevin Ottens <kevin@kdab.com>

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

    This library is distributed in the hope that it will be useful, but WITHOUT
    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
    License for more details.

    You should have received a copy of the GNU Library General Public License
    along with this library; see the file COPYING.LIB.  If not, write to the
    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
    02110-1301, USA.
*/

#include "session.h"
#include "session_p.h"
#include "sessionuiproxy.h"

#include <QtCore/QDebug>
#include <QtCore/QTimer>

#include <KDebug>
#include <KDE/KLocalizedString>

#include "job.h"
#include "loginjob.h"
#include "message_p.h"
#include "sessionlogger_p.h"
#include "sessionthread_p.h"
#include "rfccodecs.h"

Q_DECLARE_METATYPE( KTcpSocket::SslVersion )
Q_DECLARE_METATYPE( QSslSocket::SslMode )
static const int _kimap_sslVersionId = qRegisterMetaType<KTcpSocket::SslVersion>();

using namespace KIMAP;

Session::Session( const QString &hostName, quint16 port, QObject *parent)
  : QObject( parent ), d( new SessionPrivate( this ) )
{
  if ( !qgetenv( "KIMAP_LOGFILE" ).isEmpty() ) {
    d->logger = new SessionLogger;
  }

  d->isSocketConnected = false;
  d->state = Disconnected;
  d->jobRunning = false;

  d->thread = new SessionThread( hostName, port );
  connect( d->thread, SIGNAL(encryptionNegotiationResult(bool,KTcpSocket::SslVersion)),
           d, SLOT(onEncryptionNegotiationResult(bool,KTcpSocket::SslVersion)) );
  connect( d->thread, SIGNAL(sslError(KSslErrorUiData)),
           d, SLOT(handleSslError(KSslErrorUiData)) );
  connect( d->thread, SIGNAL(socketDisconnected()),
           d, SLOT(socketDisconnected()) );
  connect( d->thread, SIGNAL(responseReceived(KIMAP::Message)),
           d, SLOT(responseReceived(KIMAP::Message)) );
  connect( d->thread, SIGNAL(socketConnected()),
           d, SLOT(socketConnected()) );
  connect( d->thread, SIGNAL(socketActivity()),
           d, SLOT(socketActivity()) );
  connect( d->thread, SIGNAL(socketError(KTcpSocket::Error)),
           d, SLOT(socketError(KTcpSocket::Error)) );

  d->socketTimer.setSingleShot( true );
  connect( &d->socketTimer, SIGNAL(timeout()),
           d, SLOT(onSocketTimeout()) );

  d->startSocketTimer();
}

Session::~Session()
{
  delete d->thread;
  d->thread = 0;
}

void Session::setUiProxy(SessionUiProxy::Ptr proxy)
{
  d->uiProxy = proxy;
}

void Session::setUiProxy(SessionUiProxy *proxy)
{
  setUiProxy( SessionUiProxy::Ptr( proxy ) );
}

QString Session::hostName() const
{
  return d->thread->hostName();
}

quint16 Session::port() const
{
  return d->thread->port();
}

Session::State Session::state() const
{
  return d->state;
}

QString Session::userName() const
{
    return d->userName;
}

QByteArray Session::serverGreeting() const
{
  return d->greeting;
}

int Session::jobQueueSize() const
{
  return d->queue.size() + ( d->jobRunning ? 1 : 0 );
}

void KIMAP::Session::close()
{
  d->thread->closeSocket();
}

void SessionPrivate::handleSslError(const KSslErrorUiData& errorData)
{
  const bool ignoreSslError = uiProxy && uiProxy->ignoreSslError( errorData );
  //ignoreSslError is async, so the thread might already be gone when it returns
  if ( thread ) {
    thread->sslErrorHandlerResponse(ignoreSslError);
  }
}

SessionPrivate::SessionPrivate( Session *session )
  : QObject( session ),
    q( session ),
    state( Session::Disconnected ),
    logger( 0 ),
    currentJob( 0 ),
    tagCount( 0 ),
    sslVersion( KTcpSocket::UnknownSslVersion ),
    socketTimerInterval( 30000 ) // By default timeouts on 30s
{
}

SessionPrivate::~SessionPrivate()
{
  delete logger;
}

void SessionPrivate::addJob(Job *job)
{
  queue.append( job );
  emit q->jobQueueSizeChanged( q->jobQueueSize() );

  QObject::connect( job, SIGNAL(result(KJob*)), this, SLOT(jobDone(KJob*)) );
  QObject::connect( job, SIGNAL(destroyed(QObject*)), this, SLOT(jobDestroyed(QObject*)) );

  if ( state != Session::Disconnected ) {
    startNext();
  }
}

void SessionPrivate::startNext()
{
  QMetaObject::invokeMethod( this, "doStartNext" );
}

void SessionPrivate::doStartNext()
{
  if ( queue.isEmpty() || jobRunning || !isSocketConnected ) {
    return;
  }

  restartSocketTimer();
  jobRunning = true;

  currentJob = queue.dequeue();
  currentJob->doStart();
}

void SessionPrivate::jobDone( KJob *job )
{
  Q_UNUSED( job );
  Q_ASSERT( job == currentJob );

  stopSocketTimer();

  jobRunning = false;
  currentJob = 0;
  emit q->jobQueueSizeChanged( q->jobQueueSize() );
  startNext();
}

void SessionPrivate::jobDestroyed( QObject *job )
{
  queue.removeAll( static_cast<KIMAP::Job*>( job ) );
  if ( currentJob == job ) {
    currentJob = 0;
  }
}

void SessionPrivate::responseReceived( const Message &response )
{
  if ( logger && ( state == Session::Authenticated || state == Session::Selected ) ) {
    logger->dataReceived( response.toString() );
  }

  QByteArray tag;
  QByteArray code;

  if ( response.content.size()>=1 ) {
    tag = response.content[0].toString();
  }

  if ( response.content.size()>=2 ) {
    code = response.content[1].toString();
  }

  // BYE may arrive as part of a LOGOUT sequence or before the server closes the connection after an error.
  // In any case we should wait until the server closes the connection, so we don't have to do anything.
  if ( code == "BYE" ) {
    Message simplified = response;
    if ( simplified.content.size() >= 2 ) {
      simplified.content.removeFirst(); // Strip the tag
      simplified.content.removeFirst(); // Strip the code
    }
    kDebug() << "Received BYE: " << simplified.toString();
    return;
  }

  switch ( state ) {
  case Session::Disconnected:
    if ( socketTimer.isActive() ) {
      stopSocketTimer();
    }
    if ( code == "OK" ) {
      setState( Session::NotAuthenticated );

      Message simplified = response;
      simplified.content.removeFirst(); // Strip the tag
      simplified.content.removeFirst(); // Strip the code
      greeting = simplified.toString().trimmed(); // Save the server greeting

      startNext();
    } else if ( code == "PREAUTH" ) {
      setState( Session::Authenticated );

      Message simplified = response;
      simplified.content.removeFirst(); // Strip the tag
      simplified.content.removeFirst(); // Strip the code
      greeting = simplified.toString().trimmed(); // Save the server greeting

      startNext();
    } else {
      thread->closeSocket();
    }
    return;
  case Session::NotAuthenticated:
    if ( code == "OK" && tag == authTag ) {
      setState( Session::Authenticated );
    }
    break;
  case Session::Authenticated:
    if ( code == "OK" && tag == selectTag ) {
      setState( Session::Selected );
      currentMailBox = upcomingMailBox;
    }
    break;
  case Session::Selected:
    if ( ( code == "OK" && tag == closeTag ) ||
         ( code != "OK" && tag == selectTag ) ) {
      setState( Session::Authenticated );
      currentMailBox = QByteArray();
    } else if ( code == "OK" && tag == selectTag ) {
      currentMailBox = upcomingMailBox;
    }
    break;
  }

  if ( tag == authTag ) {
    authTag.clear();
  }
  if ( tag == selectTag ) {
    selectTag.clear();
  }
  if ( tag == closeTag ) {
    closeTag.clear();
  }

  // If a job is running forward it the response
  if ( currentJob != 0 ) {
    restartSocketTimer();
    currentJob->handleResponse( response );
  } else {
    qWarning() << "A message was received from the server with no job to handle it:"
               << response.toString()
               << '(' + response.toString().toHex() + ')';
  }
}

void SessionPrivate::setState(Session::State s)
{
  if ( s != state ) {
    Session::State oldState = state;
    state = s;
    emit q->stateChanged( state, oldState );
  }
}

QByteArray SessionPrivate::sendCommand( const QByteArray &command, const QByteArray &args )
{
  QByteArray tag = 'A' + QByteArray::number( ++tagCount ).rightJustified( 6, '0' );

  QByteArray payload = tag + ' ' + command;
  if ( !args.isEmpty() ) {
    payload += ' ' + args;
  }

  sendData( payload );

  if ( command == "LOGIN" || command == "AUTHENTICATE" ) {
    authTag = tag;
  } else if ( command == "SELECT" || command == "EXAMINE" ) {
    selectTag = tag;
    upcomingMailBox = args;
    upcomingMailBox.remove( 0, 1 );
    upcomingMailBox = upcomingMailBox.left( upcomingMailBox.indexOf( '\"') );
    upcomingMailBox = KIMAP::decodeImapFolderName( upcomingMailBox );
  } else if ( command == "CLOSE" ) {
    closeTag = tag;
  }
  return tag;
}

void SessionPrivate::sendData( const QByteArray &data )
{
  restartSocketTimer();

  if ( logger && ( state == Session::Authenticated || state == Session::Selected ) ) {
    logger->dataSent( data );
  }

  thread->sendData( data + "\r\n" );
}

void SessionPrivate::socketConnected()
{
  stopSocketTimer();
  isSocketConnected = true;

  bool willUseSsl = false;
  if ( !queue.isEmpty() ) {
    KIMAP::LoginJob *login = qobject_cast<KIMAP::LoginJob*>( queue.first() );
    if ( login ) {
      willUseSsl = ( login->encryptionMode() == KIMAP::LoginJob::SslV2 ) ||
                   ( login->encryptionMode() == KIMAP::LoginJob::SslV3 ) ||
                   ( login->encryptionMode() == KIMAP::LoginJob::SslV3_1 ) ||
                   ( login->encryptionMode() == KIMAP::LoginJob::AnySslVersion );

      userName = login->userName();
    }
  }

  if ( state == Session::Disconnected && willUseSsl ) {
    startNext();
  } else {
    startSocketTimer();
  }
}

void SessionPrivate::socketDisconnected()
{
  if ( socketTimer.isActive() ) {
    stopSocketTimer();
  }

  if ( logger && ( state == Session::Authenticated || state == Session::Selected ) ) {
    logger->disconnectionOccured();
  }

  if ( state != Session::Disconnected ) {
    setState( Session::Disconnected );
    emit q->connectionLost();
  } else {
    emit q->connectionFailed();
  }

  isSocketConnected = false;

  clearJobQueue();
}

void SessionPrivate::socketActivity()
{
  restartSocketTimer();
}

void SessionPrivate::socketError(KTcpSocket::Error error)
{
  if ( socketTimer.isActive() ) {
    stopSocketTimer();
  }

  if ( currentJob ) {
    currentJob->setSocketError(error);
  } else if ( !queue.isEmpty() ) {
    currentJob = queue.takeFirst();
    currentJob->setSocketError(error);
  }

  if ( isSocketConnected ) {
    thread->closeSocket();
  } else {
    emit q->connectionFailed();
    emit q->connectionLost();    // KDE5: Remove this. We shouldn't emit connectionLost() if we weren't connected in the first place
    clearJobQueue();
  }
}

void SessionPrivate::clearJobQueue()
{
  if ( currentJob ) {
    currentJob->connectionLost();
  } else if ( !queue.isEmpty() ) {
    currentJob = queue.takeFirst();
    currentJob->connectionLost();
  }

  QQueue<Job*> queueCopy = queue; // copy because jobDestroyed calls removeAll
  qDeleteAll(queueCopy);
  queue.clear();
  emit q->jobQueueSizeChanged( 0 );
}

void SessionPrivate::startSsl(const KTcpSocket::SslVersion &version)
{
  thread->startSsl( version );
}

QString Session::selectedMailBox() const
{
  return QString::fromUtf8( d->currentMailBox );
}

void SessionPrivate::onEncryptionNegotiationResult(bool isEncrypted, KTcpSocket::SslVersion version)
{
  if ( isEncrypted ) {
    sslVersion = version;
  } else {
    sslVersion = KTcpSocket::UnknownSslVersion;
  }
  emit encryptionNegotiationResult( isEncrypted );
}

KTcpSocket::SslVersion SessionPrivate::negotiatedEncryption() const
{
  return sslVersion;
}

void SessionPrivate::setSocketTimeout( int ms )
{
  bool timerActive = socketTimer.isActive();

  if ( timerActive ) {
    stopSocketTimer();
  }

  socketTimerInterval = ms;

  if ( timerActive ) {
    startSocketTimer();
  }
}

int SessionPrivate::socketTimeout() const
{
  return socketTimerInterval;
}

void SessionPrivate::startSocketTimer()
{
  if ( socketTimerInterval < 0 ) {
    return;
  }
  Q_ASSERT( !socketTimer.isActive() );

  socketTimer.start( socketTimerInterval );
}

void SessionPrivate::stopSocketTimer()
{
  if ( socketTimerInterval < 0 ) {
    return;
  }

  socketTimer.stop();
}

void SessionPrivate::restartSocketTimer()
{
  if ( socketTimer.isActive() ) {
    stopSocketTimer();
  }
  startSocketTimer();
}

void SessionPrivate::onSocketTimeout()
{
  kDebug() << "Socket timeout!";
  thread->closeSocket();
}

void Session::setTimeout( int timeout )
{
  d->setSocketTimeout( timeout * 1000 );
}

int Session::timeout() const
{
  return d->socketTimeout() / 1000;
}

#include "moc_session.cpp"
#include "moc_session_p.cpp"
