/*

  Copyright (c) 2009 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.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) version 3 or any later version
   accepted by the membership of KDE e.V. (or its successor approved
   by the membership of KDE e.V.), which shall act as a proxy
   defined in Section 14 of version 3 of the license.

   This program 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 General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "backupjob.h"

#include <libkdepim/misc/broadcaststatus.h>

#include <Akonadi/CollectionDeleteJob>
#include <Akonadi/CollectionFetchJob>
#include <Akonadi/CollectionFetchScope>
#include <Akonadi/ItemFetchJob>
#include <Akonadi/ItemFetchScope>

#include <KMime/Message>

#include <KLocale>
#include <KMessageBox>
#include <KTar>
#include <KZip>
#include <kio/global.h> //krazy:exclude=camelcase as there is no such

#include <QFileInfo>
#include <QTimer>

using namespace MailCommon;
static const mode_t archivePerms = S_IFREG | 0644;

BackupJob::BackupJob( QWidget *parent )
    : QObject( parent ),
      mArchiveTime(QDateTime::currentDateTime().toTime_t()),
      mArchiveType( Zip ),
      mRootFolder( 0 ),
      mArchive( 0 ),
      mParentWidget( parent ),
      mArchivedMessages( 0 ),
      mArchivedSize( 0 ),
      mProgressItem( 0 ),
      mAborted( false ),
      mDeleteFoldersAfterCompletion( false ),
      mRecursive( true ),
      mCurrentFolder( Akonadi::Collection() ),
      mCurrentJob( 0 ),
      mDisplayMessageBox(true)
{
}

BackupJob::~BackupJob()
{
    mPendingFolders.clear();
    delete mArchive;
    mArchive = 0;
}

void BackupJob::setRootFolder( const Akonadi::Collection &rootFolder )
{
    mRootFolder = rootFolder;
}

void BackupJob::setRealPath(const QString &path)
{
    mRealPath = path;
}

void BackupJob::setSaveLocation( const KUrl &savePath )
{
    mMailArchivePath = savePath;
}

void BackupJob::setArchiveType( ArchiveType type )
{
    mArchiveType = type;
}

void BackupJob::setDeleteFoldersAfterCompletion( bool deleteThem )
{
    mDeleteFoldersAfterCompletion = deleteThem;
}

void BackupJob::setRecursive( bool recursive )
{
    mRecursive = recursive;
}

bool BackupJob::queueFolders( const Akonadi::Collection &root )
{
    mPendingFolders.append( root );
    if ( mRecursive ) {
        // FIXME: Get rid of the exec()
        // We could do a recursive CollectionFetchJob, but we only fetch the first level
        // and then recurse manually. This is needed because a recursive fetch doesn't
        // sort the collections the way we want. We need all first level children to be
        // in the mPendingFolders list before all second level children, so that the
        // directories for the first level are written before the directories in the
        // second level, in the archive file.
        Akonadi::CollectionFetchJob *job =
                new Akonadi::CollectionFetchJob( root, Akonadi::CollectionFetchJob::FirstLevel );
        job->fetchScope().setAncestorRetrieval( Akonadi::CollectionFetchScope::All );
        job->exec();
        if ( job->error() ) {
            kWarning() << job->errorString();
            abort( i18n( "Unable to retrieve folder list." ) );
            return false;
        }

        foreach ( const Akonadi::Collection &collection, job->collections() ) {
            if ( !queueFolders( collection ) ) {
                return false;
            }
        }
    }
    mAllFolders = mPendingFolders;
    return true;
}

bool BackupJob::hasChildren( const Akonadi::Collection &collection ) const
{
    foreach ( const Akonadi::Collection &curCol, mAllFolders ) {
        if ( collection == curCol.parentCollection() ) {
            return true;
        }
    }
    return false;
}

void BackupJob::cancelJob()
{
    abort( i18n( "The operation was canceled by the user." ) );
}

void BackupJob::abort( const QString &errorMessage )
{
    // We could be called this twice, since killing the current job below will
    // cause the job to fail, and that will call abort()
    if ( mAborted ) {
        return;
    }

    mAborted = true;
    if ( mCurrentFolder.isValid() ) {
        mCurrentFolder = Akonadi::Collection();
    }

    if ( mArchive && mArchive->isOpen() ) {
        mArchive->close();
    }

    if ( mCurrentJob ) {
        mCurrentJob->kill();
        mCurrentJob = 0;
    }

    if ( mProgressItem ) {
        mProgressItem->setComplete();
        mProgressItem = 0;
        // The progressmanager will delete it
    }
    QString text = i18n( "Failed to archive the folder '%1'.", mRootFolder.name() );
    text += QLatin1Char('\n') + errorMessage;
    Q_EMIT error(text);
    if (mDisplayMessageBox)
        KMessageBox::sorry( mParentWidget, text, i18n( "Archiving failed" ) );
    deleteLater();
    // Clean up archive file here?
}

void BackupJob::finish()
{
    if ( mArchive->isOpen() ) {
        if ( !mArchive->close() ) {
            abort( i18n( "Unable to finalize the archive file." ) );
            return;
        }
    }

    const QString archivingStr( i18n( "Archiving finished" ) );
    KPIM::BroadcastStatus::instance()->setStatusMsg( archivingStr );

    if ( mProgressItem ) {
        mProgressItem->setStatus( archivingStr );
        mProgressItem->setComplete();
        mProgressItem = 0;
    }

    QFileInfo archiveFileInfo( mMailArchivePath.path() );
    QString text = i18n( "Archiving folder '%1' successfully completed. "
                         "The archive was written to the file '%2'.",
                         mRealPath.isEmpty() ? mRootFolder.name() : mRealPath, mMailArchivePath.path() );
    text += QLatin1Char('\n') + i18np( "1 message of size %2 was archived.",
                                       "%1 messages with the total size of %2 were archived.",
                                       mArchivedMessages, KIO::convertSize( mArchivedSize ) );
    text += QLatin1Char('\n') + i18n( "The archive file has a size of %1.",
                                      KIO::convertSize( archiveFileInfo.size() ) );
    if (mDisplayMessageBox) {
        KMessageBox::information( mParentWidget, text, i18n( "Archiving finished" ) );
    }

    if ( mDeleteFoldersAfterCompletion ) {
        // Some safety checks first...
        if ( archiveFileInfo.exists() && ( mArchivedSize > 0 || mArchivedMessages == 0 ) ) {
            // Sorry for any data loss!
            new Akonadi::CollectionDeleteJob( mRootFolder );
        }
    }
    Q_EMIT backupDone(text);
    deleteLater();
}

void BackupJob::archiveNextMessage()
{
    if ( mAborted ) {
        return;
    }

    if ( mPendingMessages.isEmpty() ) {
        kDebug() << "===> All messages done in folder " << mCurrentFolder.name();
        archiveNextFolder();
        return;
    }

    Akonadi::Item item = mPendingMessages.front();
    mPendingMessages.pop_front();
    kDebug() << "Fetching item with ID" << item.id() << "for folder" << mCurrentFolder.name();

    mCurrentJob = new Akonadi::ItemFetchJob( item );
    mCurrentJob->fetchScope().fetchFullPayload( true );
    connect( mCurrentJob, SIGNAL(result(KJob*)),
             this, SLOT(itemFetchJobResult(KJob*)) );
}

void BackupJob::processMessage( const Akonadi::Item &item )
{
    if ( mAborted ) {
        return;
    }

    const KMime::Message::Ptr message = item.payload<KMime::Message::Ptr>();
    kDebug() << "Processing message with subject " << message->subject( false );
    const QByteArray messageData = message->encodedContent();
    const qint64 messageSize = messageData.size();
    const QString messageName = QString::number( item.id() );
    const QString fileName = pathForCollection( mCurrentFolder ) + QLatin1String("/cur/") + messageName;

    // PORT ME: user and group!
    kDebug() << "AKONDI PORT: disabled code here!";
    if ( !mArchive->writeFile( fileName, QLatin1String("user"), QLatin1String("group"), messageData, messageSize, archivePerms, mArchiveTime, mArchiveTime, mArchiveTime) ) {
        abort( i18n( "Failed to write a message into the archive folder '%1'.",
                     mCurrentFolder.name() ) );
        return;
    }

    ++mArchivedMessages;
    mArchivedSize += messageSize;

    // Use a singleshot timer, otherwise the job started in archiveNextMessage()
    // will hang
    QTimer::singleShot( 0, this, SLOT(archiveNextMessage()) );
}

void BackupJob::itemFetchJobResult( KJob *job )
{
    if ( mAborted ) {
        return;
    }

    Q_ASSERT( job == mCurrentJob );
    mCurrentJob = 0;

    if ( job->error() ) {
        Q_ASSERT( mCurrentFolder.isValid() );
        kWarning() << job->errorString();
        abort( i18n( "Downloading a message in folder '%1' failed.", mCurrentFolder.name() ) );
    } else {
        Akonadi::ItemFetchJob *fetchJob = dynamic_cast<Akonadi::ItemFetchJob*>( job );
        Q_ASSERT( fetchJob );
        Q_ASSERT( fetchJob->items().size() == 1 );
        processMessage( fetchJob->items().first() );
    }
}

bool BackupJob::writeDirHelper( const QString &directoryPath )
{
    // PORT ME: Correct user/group
    kDebug() << "AKONDI PORT: Disabled code here!";
    return mArchive->writeDir( directoryPath, QLatin1String("user"), QLatin1String("group"), 040755, mArchiveTime, mArchiveTime, mArchiveTime );
}

QString BackupJob::collectionName( const Akonadi::Collection &collection ) const
{
    foreach ( const Akonadi::Collection &curCol, mAllFolders ) {
        if ( curCol == collection ) {
            return curCol.name();
        }
    }
    Q_ASSERT( false );
    return QString();
}

QString BackupJob::pathForCollection( const Akonadi::Collection &collection ) const
{
    QString fullPath = collectionName( collection );
    Akonadi::Collection curCol = collection.parentCollection();
    if ( collection != mRootFolder ) {
        Q_ASSERT( curCol.isValid() );
        while ( curCol != mRootFolder ) {
            fullPath.prepend( QLatin1Char('.') + collectionName( curCol ) + QString::fromLatin1(".directory/") );
            curCol = curCol.parentCollection();
        }
        Q_ASSERT( curCol == mRootFolder );
        fullPath.prepend( QLatin1Char('.') + collectionName( curCol ) + QString::fromLatin1(".directory/") );
    }
    return fullPath;
}

QString BackupJob::subdirPathForCollection( const Akonadi::Collection &collection ) const
{
    QString path = pathForCollection( collection );
    const int parentDirEndIndex = path.lastIndexOf( collection.name() );
    Q_ASSERT( parentDirEndIndex != -1 );
    path = path.left( parentDirEndIndex );
    path.append( QLatin1Char('.') + collection.name() + QLatin1String(".directory") );
    return path;
}

void BackupJob::archiveNextFolder()
{
    if ( mAborted ) {
        return;
    }

    if ( mPendingFolders.isEmpty() ) {
        finish();
        return;
    }

    mCurrentFolder = mPendingFolders.takeAt( 0 );
    kDebug() << "===> Archiving next folder: " << mCurrentFolder.name();
    const QString archivingStr( i18n( "Archiving folder %1", mCurrentFolder.name() ) );
    if ( mProgressItem ) {
        mProgressItem->setStatus( archivingStr );
    }
    KPIM::BroadcastStatus::instance()->setStatusMsg( archivingStr );

    const QString folderName = mCurrentFolder.name();
    bool success = true;
    if ( hasChildren( mCurrentFolder ) ) {
        if ( !writeDirHelper( subdirPathForCollection( mCurrentFolder ) ) ) {
            success = false;
        }
    }
    if (success) {
        if ( !writeDirHelper( pathForCollection( mCurrentFolder ) ) ) {
            success = false;
        } else if ( !writeDirHelper( pathForCollection( mCurrentFolder ) + QLatin1String("/cur") ) ) {
            success = false;
        } else if ( !writeDirHelper( pathForCollection( mCurrentFolder ) + QLatin1String("/new") ) ) {
            success = false;
        } else if ( !writeDirHelper( pathForCollection( mCurrentFolder ) + QLatin1String("/tmp") ) ) {
            success = false;
        }
    }
    if ( !success ) {
        abort( i18n( "Unable to create folder structure for folder '%1' within archive file.",
                     mCurrentFolder.name() ) );
        return;
    }
    Akonadi::ItemFetchJob *job = new Akonadi::ItemFetchJob( mCurrentFolder );
    job->setProperty( "folderName", folderName );
    connect( job, SIGNAL(result(KJob*)), SLOT(onArchiveNextFolderDone(KJob*)) );
}

void BackupJob::onArchiveNextFolderDone( KJob *job )
{
    if ( job->error() ) {
        kWarning() << job->errorString();
        abort( i18n( "Unable to get message list for folder %1.",
                     job->property( "folderName" ).toString() ) );
        return;
    }

    Akonadi::ItemFetchJob *fetchJob = qobject_cast<Akonadi::ItemFetchJob*>( job );
    mPendingMessages += fetchJob->items();
    archiveNextMessage();
}

void BackupJob::start()
{
    Q_ASSERT( !mMailArchivePath.isEmpty() );
    Q_ASSERT( mRootFolder.isValid() );

    if ( !queueFolders( mRootFolder ) ) {
        return;
    }

    switch ( mArchiveType ) {
    case Zip:
    {
        KZip *zip = new KZip( mMailArchivePath.path() );
        zip->setCompression( KZip::DeflateCompression );
        mArchive = zip;
        break;
    }
    case Tar:
    {
        mArchive = new KTar( mMailArchivePath.path(), QLatin1String("application/x-tar") );
        break;
    }
    case TarGz:
    {
        mArchive = new KTar( mMailArchivePath.path(), QLatin1String("application/x-gzip") );
        break;
    }
    case TarBz2:
    {
        mArchive = new KTar( mMailArchivePath.path(), QLatin1String("application/x-bzip2") );
        break;
    }
    }

    kDebug() << "Starting backup.";
    if ( !mArchive->open( QIODevice::WriteOnly ) ) {
        abort( i18n( "Unable to open archive for writing." ) );
        return;
    }

    mProgressItem = KPIM::ProgressManager::createProgressItem(
                QLatin1String("BackupJob"),
                i18n( "Archiving" ),
                QString(),
                true );
    mProgressItem->setUsesBusyIndicator( true );
    connect( mProgressItem, SIGNAL(progressItemCanceled(KPIM::ProgressItem*)),
             this, SLOT(cancelJob()) );

    archiveNextFolder();
}

void BackupJob::setDisplayMessageBox(bool display)
{
    mDisplayMessageBox = display;
}


