/*  -*- c++ -*-
    sievejob.h

    KMail, the KDE mail client.
    Copyright (c) 2002 Marc Mutz <mutz@kde.org>

    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public License,
    version 2.0, as published by the Free Software Foundation.
    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software Foundation,
    Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, US
*/

#include "sievejob.h"
#include "sievejob_p.h"
#include "session.h"

#include <kdebug.h>
#include <KUrl>
#include <KMessageBox>
#include <KLocalizedString>
#include <QtCore/QPointer>

using namespace KManageSieve;

QHash<KUrl, QPointer<Session> > SieveJob::Private::m_sessionPool;

Session* SieveJob::Private::sessionForUrl(const KUrl &url)
{
    KUrl hostUrl( url );
    hostUrl.setPath( QString() ); // remove parts not required to identify the server
    QPointer<Session> sessionPtr = m_sessionPool.value( hostUrl );
    if ( !sessionPtr ) {
        sessionPtr = QPointer<Session>( new Session() );
        m_sessionPool.insert( hostUrl, sessionPtr );
        sessionPtr->connectToHost( hostUrl );
    }
    return sessionPtr.data();
}

static void append_lf2crlf( QByteArray &out, const QByteArray &in ) {
    if ( in.isEmpty() )
        return;
    const unsigned int oldOutSize = out.size();
    out.resize( oldOutSize + 2 * in.size() );
    const char * s = in.begin();
    const char * const end = in.end();
    char * d = out.begin() + oldOutSize;
    char last = '\0';
    while ( s < end ) {
        if ( *s == '\n' && last != '\r' )
            *d++ = '\r';
        *d++ = last = *s++;
    }
    out.resize( d - out.begin() );
}

void SieveJob::Private::run( Session *session )
{
    switch ( mCommands.top() ) {
    case Get:
    {
        const QString filename = mUrl.fileName( KUrl::ObeyTrailingSlash );
        session->sendData( "GETSCRIPT \"" + filename.toUtf8() + "\"" );
        break;
    }
    case Put:
    {
        const QString filename = mUrl.fileName( KUrl::ObeyTrailingSlash );
        QByteArray encodedData;
        append_lf2crlf( encodedData, mScript.toUtf8() );
        session->sendData( "PUTSCRIPT \"" + filename.toUtf8() + "\" {" + QByteArray::number( encodedData.size() ) + "+}" );
        session->sendData( encodedData );
        break;
    }
    case Activate:
    {
        const QString filename = mUrl.fileName( KUrl::ObeyTrailingSlash );
        session->sendData( "SETACTIVE \"" + filename.toUtf8() + "\"" );
        break;
    }
    case Deactivate:
        session->sendData( "SETACTIVE \"\"" );
        break;
    case List:
    case SearchActive:
        session->sendData( "LISTSCRIPTS" );
        break;
    case Delete:
    {
        const QString filename = mUrl.fileName( KUrl::ObeyTrailingSlash );
        session->sendData( "DELETESCRIPT \"" + filename.toUtf8() + "\"" );
        break;
    }
    default:
        Q_ASSERT( false );
    }
}

bool SieveJob::Private::handleResponse( const Response &response, const QByteArray &data )
{
    const Command lastCmd = mCommands.top();
    Session* session = sessionForUrl( mUrl );

    QString errMsg;
    // handle non-action responses
    if ( response.type() != Response::Action ) {
        switch ( lastCmd ) {
        case Get:
            mScript = QString::fromUtf8( data );
            break;
        case List:
        case SearchActive:
        {
            const QString filename = QString::fromUtf8( response.key() );
            mAvailableScripts.append( filename );
            const bool isActive = response.extra() == "ACTIVE";

            if ( isActive )
                mActiveScriptName = filename;

            if ( mFileExists == DontKnow && filename == mUrl.fileName() )
                mFileExists = Yes;

            emit q->item( q, filename, isActive );
            break;
        }
        case Put:
            if ( response.type() == Response::KeyValuePair ) {
                errMsg = QString::fromUtf8( response.key() );
                session->setErrorMessage( i18n("The script did not upload successfully.\n"
                                               "This is probably due to errors in the script.\n"
                                               "The server responded:\n%1", errMsg ) );
            } else if ( response.type() == Response::Quantity ) {
                errMsg = QString::fromUtf8( data );
                session->setErrorMessage( i18n("The script did not upload successfully.\n"
                                               "This is probably due to errors in the script.\n"
                                               "The server responded:\n%1", errMsg ) );
            } else {
                session->setErrorMessage( i18n("The script did not upload successfully.\nThe script may contain errors.") );
            }
            break;
        default:
            kDebug() << "Unhandled response: " << response.key() << response.value() << response.extra() << data;
        }
        if ( lastCmd != Put )
            return false; // we expect more
    }

    // First, let's see if we come back from a SearchActive. If so, set
    // mFileExists to No if we didn't see the mUrl.fileName() during
    // listDir...
    if ( lastCmd == SearchActive && mFileExists == DontKnow && response.operationSuccessful() )
        mFileExists = No;

    // PUTSCRIPT reports the error message in an literal after the final response sometimes
    if ( lastCmd == Put && response.operationResult() == Response::No ) {
        if ( response.action().size() > 3 ) {
            const QByteArray extra = response.action().right( response.action().size() - 3 );
            sessionForUrl( mUrl )->feedBack( extra );
            return false;
        }
    }

    // prepare for next round:
    mCommands.pop();
    // check for errors:
    if ( !response.operationSuccessful() ) {
        if ( mInteractive ) {
            if ( session->errorMessage().isEmpty() )
                KMessageBox::error( 0, i18n( "Sieve operation failed.\n"
                                             "The server responded:\n%1",QString::fromUtf8( response.key() ) ), i18n( "Sieve Error" ) );
            else
                KMessageBox::error( 0, session->errorMessage(), i18n( "Sieve Error" ) );
        }
        emit q->errorMessage( q, false, errMsg );
        emit q->result( q, false, mScript, (mUrl.fileName() == mActiveScriptName));

        if ( lastCmd == List )
            emit q->gotList( q, false, mAvailableScripts, mActiveScriptName );
        else
            emit q->gotScript( q, false, mScript, (mUrl.fileName() == mActiveScriptName) );

        q->deleteLater();
        return true;
    }

    // check for new tasks:
    if ( !mCommands.empty() ) {
        // Don't fail getting a non-existent script:
        if ( (mCommands.top() == Get) && (mFileExists == No) ) {
            mScript.clear();
            mCommands.pop();
        }
    }

    if ( mCommands.empty() ) {
        emit q->errorMessage( q, true, QString());
        // was last command; report success and delete this object:
        emit q->result( q, true, mScript, (mUrl.fileName() == mActiveScriptName) );
        if ( lastCmd == List )
            emit q->gotList( q, true, mAvailableScripts, mActiveScriptName );
        else
            emit q->gotScript( q, true, mScript, (mUrl.fileName() == mActiveScriptName) );

        q->deleteLater();
        return true;
    } else {
        // schedule the next command:
        run( sessionForUrl( mUrl ) );
        return false;
    }

    return true;
}

void SieveJob::Private::killed()
{
    emit q->result( q, false, mScript, (mUrl.fileName() == mActiveScriptName) );
    if ( mCommands.top() == List )
        emit q->gotList( q, false, mAvailableScripts, mActiveScriptName );
    else
        emit q->gotScript( q, false, mScript, (mUrl.fileName() == mActiveScriptName) );
}

SieveJob::SieveJob( QObject *parent )
    : QObject( parent ), d( new Private( this ) )
{
}

SieveJob::~SieveJob()
{
    kill();

    delete d;
}

void SieveJob::kill( KJob::KillVerbosity verbosity )
{
    Q_UNUSED( verbosity );
    if ( d->mCommands.isEmpty() )
        return; // done already
    Private::sessionForUrl( d->mUrl )->killJob( this );
}

void SieveJob::setInteractive( bool interactive )
{
    d->mInteractive = interactive;
}

QStringList SieveJob::sieveCapabilities() const
{
    Session* session = d->sessionForUrl( d->mUrl );
    if ( session )
        return session->sieveExtensions();
    return QStringList();
}

bool SieveJob::fileExists() const
{
    return d->mFileExists;
}

SieveJob* SieveJob::put( const KUrl &destination, const QString &script,
                         bool makeActive, bool wasActive )
{
    QStack<Private::Command> commands;
    if ( makeActive )
        commands.push( Private::Activate );

    if ( wasActive )
        commands.push( Private::Deactivate );

    commands.push( Private::Put );

    SieveJob *job = new SieveJob;
    job->d->mUrl = destination;
    job->d->mScript = script;
    job->d->mCommands = commands;

    Private::sessionForUrl( destination )->scheduleJob( job );
    return job;
}

SieveJob* SieveJob::get( const KUrl &source )
{
    QStack<Private::Command> commands;
    commands.push( Private::Get );
    commands.push( Private::SearchActive );

    SieveJob *job = new SieveJob;
    job->d->mUrl = source;
    job->d->mCommands = commands;

    Private::sessionForUrl( source )->scheduleJob( job );
    return job;
}

SieveJob* SieveJob::list( const KUrl &source )
{
    QStack<Private::Command> commands;
    commands.push( Private::List );

    SieveJob *job = new SieveJob;
    job->d->mUrl = source;
    job->d->mCommands = commands;

    Private::sessionForUrl( source )->scheduleJob( job );
    return job;
}

SieveJob* SieveJob::del( const KUrl &url )
{
    QStack<Private::Command> commands;
    commands.push( Private::Delete );

    SieveJob *job = new SieveJob;
    job->d->mUrl = url;
    job->d->mCommands = commands;

    Private::sessionForUrl( url )->scheduleJob( job );
    return job;
}

SieveJob* SieveJob::deactivate( const KUrl &url )
{
    QStack<Private::Command> commands;
    commands.push( Private::Deactivate );

    SieveJob *job = new SieveJob;
    job->d->mUrl = url;
    job->d->mCommands = commands;

    Private::sessionForUrl( url )->scheduleJob( job );
    return job;
}

SieveJob* SieveJob::activate( const KUrl &url )
{
    QStack<Private::Command> commands;
    commands.push( Private::Activate );

    SieveJob *job = new SieveJob;
    job->d->mUrl = url;
    job->d->mCommands = commands;

    Private::sessionForUrl( url )->scheduleJob( job );
    return job;
}

