/* Copyright 2009 Klarälvdalens Datakonsult AB

   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 "kmmsgdict.h"
#include "kmfolder.h"
#include "kmfoldercachedimap.h"
#include "kmfolderdir.h"
#include "folderutil.h"

#include "progressmanager.h"

#include "kzip.h"
#include "ktar.h"
#include "tdemessagebox.h"

#include "tqfile.h"
#include "tqfileinfo.h"
#include "tqstringlist.h"

using namespace KMail;

BackupJob::BackupJob( TQWidget *parent )
  : TQObject( parent ),
    mArchiveType( Zip ),
    mRootFolder( 0 ),
    mArchive( 0 ),
    mParentWidget( parent ),
    mCurrentFolderOpen( false ),
    mArchivedMessages( 0 ),
    mArchivedSize( 0 ),
    mProgressItem( 0 ),
    mAborted( false ),
    mDeleteFoldersAfterCompletion( false ),
    mCurrentFolder( 0 ),
    mCurrentMessage( 0 ),
    mCurrentJob( 0 )
{
}

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

void BackupJob::setRootFolder( KMFolder *rootFolder )
{
  mRootFolder = rootFolder;
}

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

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

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

TQString BackupJob::stripRootPath( const TQString &path ) const
{
  TQString ret = path;
  ret = ret.remove( mRootFolder->path() );
  if ( ret.startsWith( "/" ) )
    ret = ret.right( ret.length() - 1 );
  return ret;
}

void BackupJob::queueFolders( KMFolder *root )
{
  mPendingFolders.append( root );
  KMFolderDir *dir = root->child();
  if ( dir ) {
    for ( KMFolderNode * node = dir->first() ; node ; node = dir->next() ) {
      if ( node->isDir() )
        continue;
      KMFolder *folder = static_cast<KMFolder*>( node );
      queueFolders( folder );
    }
  }
}

bool BackupJob::hasChildren( KMFolder *folder ) const
{
  KMFolderDir *dir = folder->child();
  if ( dir ) {
    for ( KMFolderNode * node = dir->first() ; node ; node = dir->next() ) {
      if ( !node->isDir() )
        return true;
    }
  }
  return false;
}

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

void BackupJob::abort( const TQString &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 ( mCurrentFolderOpen && mCurrentFolder ) {
    mCurrentFolder->close( "BackupJob" );
    mCurrentFolder = 0;
  }
  if ( mArchive && mArchive->isOpened() ) {
    mArchive->close();
  }
  if ( mCurrentJob ) {
    mCurrentJob->kill();
    mCurrentJob = 0;
  }
  if ( mProgressItem ) {
    mProgressItem->setComplete();
    mProgressItem = 0;
    // The progressmanager will delete it
  }

  TQString text = i18n( "Failed to archive the folder '%1'." ).arg( mRootFolder->name() );
  text += "\n" + errorMessage;
  KMessageBox::sorry( mParentWidget, text, i18n( "Archiving failed." ) );
  deleteLater();
  // Clean up archive file here?
}

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

  mProgressItem->setStatus( i18n( "Archiving finished" ) );
  mProgressItem->setComplete();
  mProgressItem = 0;

  TQFileInfo archiveFileInfo( mMailArchivePath.path() );
  TQString text = i18n( "Archiving folder '%1' successfully completed. "
                       "The archive was written to the file '%2'." )
                   .arg( mRootFolder->name() ).arg( mMailArchivePath.path() );
  text += "\n" + i18n( "1 message of size %1 was archived.",
                       "%n messages with the total size of %1 were archived.", mArchivedMessages )
                   .arg( TDEIO::convertSize( mArchivedSize ) );
  text += "\n" + i18n( "The archive file has a size of %1." )
                   .arg( TDEIO::convertSize( archiveFileInfo.size() ) );
  KMessageBox::information( mParentWidget, text, i18n( "Archiving finished." ) );

  if ( mDeleteFoldersAfterCompletion ) {
    // Some saftey checks first...
    if ( archiveFileInfo.size() > 0 && ( mArchivedSize > 0 || mArchivedMessages == 0 ) ) {
      // Sorry for any data loss!
      FolderUtil::deleteFolder( mRootFolder, mParentWidget );
    }
  }

  deleteLater();
}

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

  mCurrentMessage = 0;
  if ( mPendingMessages.isEmpty() ) {
    kdDebug(5006) << "===> All messages done in folder " << mCurrentFolder->name() << endl;
    mCurrentFolder->close( "BackupJob" );
    mCurrentFolderOpen = false;
    archiveNextFolder();
    return;
  }

  unsigned long serNum = mPendingMessages.front();
  mPendingMessages.pop_front();

  KMFolder *folder;
  mMessageIndex = -1;
  KMMsgDict::instance()->getLocation( serNum, &folder, &mMessageIndex );
  if ( mMessageIndex == -1 ) {
    kdWarning(5006) << "Failed to get message location for sernum " << serNum << endl;
    abort( i18n( "Unable to retrieve a message for folder '%1'." ).arg( mCurrentFolder->name() ) );
    return;
  }

  Q_ASSERT( folder == mCurrentFolder );
  const KMMsgBase *base = mCurrentFolder->getMsgBase( mMessageIndex );
  mUnget = base && !base->isMessage();
  KMMessage *message = mCurrentFolder->getMsg( mMessageIndex );
  if ( !message ) {
    kdWarning(5006) << "Failed to retrieve message with index " << mMessageIndex << endl;
    abort( i18n( "Unable to retrieve a message for folder '%1'." ).arg( mCurrentFolder->name() ) );
    return;
  }

  kdDebug(5006) << "Going to get next message with subject " << message->subject() << ", "
                << mPendingMessages.size() << " messages left in the folder." << endl;

  if ( message->isComplete() ) {
    // Use a singleshot timer, or otherwise we risk ending up in a very big recursion
    // for folders that have many messages
    mCurrentMessage = message;
    TQTimer::singleShot( 0, this, TQT_SLOT( processCurrentMessage() ) );
  }
  else if ( message->parent() ) {
    mCurrentJob = message->parent()->createJob( message );
    mCurrentJob->setCancellable( false );
    connect( mCurrentJob, TQT_SIGNAL( messageRetrieved( KMMessage* ) ),
             this, TQT_SLOT( messageRetrieved( KMMessage* ) ) );
    connect( mCurrentJob, TQT_SIGNAL( result( KMail::FolderJob* ) ),
             this, TQT_SLOT( folderJobFinished( KMail::FolderJob* ) ) );
    mCurrentJob->start();
  }
  else {
    kdWarning(5006) << "Message with subject " << mCurrentMessage->subject()
                    << " is neither complete nor has a parent!" << endl;
    abort( i18n( "Internal error while trying to retrieve a message from folder '%1'." )
              .arg( mCurrentFolder->name() ) );
  }

  mProgressItem->setProgress( ( mProgressItem->progress() + 5 ) );
}

static int fileInfoToUnixPermissions( const TQFileInfo &fileInfo )
{
  int perm = 0;
  if ( fileInfo.permission( TQFileInfo::ExeOther ) ) perm += S_IXOTH;
  if ( fileInfo.permission( TQFileInfo::WriteOther ) ) perm += S_IWOTH;
  if ( fileInfo.permission( TQFileInfo::ReadOther ) ) perm += S_IROTH;
  if ( fileInfo.permission( TQFileInfo::ExeGroup ) ) perm += S_IXGRP;
  if ( fileInfo.permission( TQFileInfo::WriteGroup ) ) perm += S_IWGRP;
  if ( fileInfo.permission( TQFileInfo::ReadGroup ) ) perm += S_IRGRP;
  if ( fileInfo.permission( TQFileInfo::ExeOwner ) ) perm += S_IXUSR;
  if ( fileInfo.permission( TQFileInfo::WriteOwner ) ) perm += S_IWUSR;
  if ( fileInfo.permission( TQFileInfo::ReadOwner ) ) perm += S_IRUSR;
  return perm;
}

void BackupJob::processCurrentMessage()
{
  if ( mAborted )
    return;

  if ( mCurrentMessage ) {
    kdDebug(5006) << "Processing message with subject " << mCurrentMessage->subject() << endl;
    const DwString &messageDWString = mCurrentMessage->asDwString();
    const uint messageSize = messageDWString.size();
    const char *messageString = mCurrentMessage->asDwString().c_str();
    TQString messageName;
    TQFileInfo fileInfo;
    if ( messageName.isEmpty() ) {
      messageName = TQString::number( mCurrentMessage->getMsgSerNum() ); // IMAP doesn't have filenames
      if ( mCurrentMessage->storage() ) {
        fileInfo.setFile( mCurrentMessage->storage()->location() );
        // TODO: what permissions etc to take when there is no storage file?
      }
    }
    else {
      // TODO: What if the message is not in the "cur" directory?
      fileInfo.setFile( mCurrentFolder->location() + "/cur/" + mCurrentMessage->fileName() );
      messageName = mCurrentMessage->fileName();
    }

    const TQString fileName = stripRootPath( mCurrentFolder->location() ) +
                             "/cur/" + messageName;

    TQString user;
    TQString group;
    mode_t permissions = 0700;
    time_t creationTime = time( 0 );
    time_t modificationTime = time( 0 );
    time_t accessTime = time( 0 );
    if ( !fileInfo.fileName().isEmpty() ) {
      user = fileInfo.owner();
      group = fileInfo.group();
      permissions = fileInfoToUnixPermissions( fileInfo );
      creationTime = fileInfo.created().toTime_t();
      modificationTime = fileInfo.lastModified().toTime_t();
      accessTime = fileInfo.lastRead().toTime_t();
    }
    else {
      kdWarning(5006) << "Unable to find file for message " << fileName << endl;
    }

    if ( !mArchive->writeFile( fileName, user, group, messageSize, permissions, accessTime,
                               modificationTime, creationTime, messageString ) ) {
      abort( i18n( "Failed to write a message into the archive folder '%1'." ).arg( mCurrentFolder->name() ) );
      return;
    }

    if ( mUnget ) {
      Q_ASSERT( mMessageIndex >= 0 );
      mCurrentFolder->unGetMsg( mMessageIndex );
    }

    mArchivedMessages++;
    mArchivedSize += messageSize;
  }
  else {
    // No message? According to ImapJob::slotGetMessageResult(), that means the message is no
    // longer on the server. So ignore this one.
    kdWarning(5006) << "Unable to download a message for folder " << mCurrentFolder->name() << endl;
  }
  archiveNextMessage();
}

void BackupJob::messageRetrieved( KMMessage *message )
{
  mCurrentMessage = message;
  processCurrentMessage();
}

void BackupJob::folderJobFinished( KMail::FolderJob *job )
{
  if ( mAborted )
    return;

  // The job might finish after it has emitted messageRetrieved(), in which case we have already
  // started a new job. Don't set the current job to 0 in that case.
  if ( job == mCurrentJob ) {
    mCurrentJob = 0;
  }

  if ( job->error() ) {
    if ( mCurrentFolder )
      abort( i18n( "Downloading a message in folder '%1' failed." ).arg( mCurrentFolder->name() ) );
    else
      abort( i18n( "Downloading a message in the current folder failed." ) );
  }
}

bool BackupJob::writeDirHelper( const TQString &directoryPath, const TQString &permissionPath )
{
  TQFileInfo fileInfo( permissionPath );
  TQString user = fileInfo.owner();
  TQString group = fileInfo.group();
  mode_t permissions = fileInfoToUnixPermissions( fileInfo );
  time_t creationTime = fileInfo.created().toTime_t();
  time_t modificationTime = fileInfo.lastModified().toTime_t();
  time_t accessTime = fileInfo.lastRead().toTime_t();
  return mArchive->writeDir( stripRootPath( directoryPath ), user, group, permissions, accessTime,
                             modificationTime, creationTime );
}

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

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

  mCurrentFolder = mPendingFolders.take( 0 );
  kdDebug(5006) << "===> Archiving next folder: " << mCurrentFolder->name() << endl;
  mProgressItem->setStatus( i18n( "Archiving folder %1" ).arg( mCurrentFolder->name() ) );
  if ( mCurrentFolder->open( "BackupJob" ) != 0 ) {
    abort( i18n( "Unable to open folder '%1'.").arg( mCurrentFolder->name() ) );
    return;
  }
  mCurrentFolderOpen = true;

  const TQString folderName = mCurrentFolder->name();
  bool success = true;
  if ( hasChildren( mCurrentFolder ) ) {
    if ( !writeDirHelper( mCurrentFolder->subdirLocation(), mCurrentFolder->subdirLocation() ) )
      success = false;
  }
  if ( !writeDirHelper( mCurrentFolder->location(), mCurrentFolder->location() ) )
    success = false;
  if ( !writeDirHelper( mCurrentFolder->location() + "/cur", mCurrentFolder->location() ) )
    success = false;
  if ( !writeDirHelper( mCurrentFolder->location() + "/new", mCurrentFolder->location() ) )
    success = false;
  if ( !writeDirHelper( mCurrentFolder->location() + "/tmp", mCurrentFolder->location() ) )
    success = false;
  if ( !success ) {
    abort( i18n( "Unable to create folder structure for folder '%1' within archive file." )
              .arg( mCurrentFolder->name() ) );
    return;
  }

  for ( int i = 0; i < mCurrentFolder->count( false /* no cache */ ); i++ ) {
    unsigned long serNum = KMMsgDict::instance()->getMsgSerNum( mCurrentFolder, i );
    if ( serNum == 0 ) {
      // Uh oh
      kdWarning(5006) << "Got serial number zero in " << mCurrentFolder->name()
                      << " at index " << i << "!" << endl;
      // TODO: handle error in a nicer way. this is _very_ bad
      abort( i18n( "Unable to backup messages in folder '%1', the index file is corrupted." )
               .arg( mCurrentFolder->name() ) );
      return;
    }
    else
      mPendingMessages.append( serNum );
  }
  archiveNextMessage();
}

// TODO
// - error handling
// - import
// - connect to progressmanager, especially abort
// - messagebox when finished (?)
// - ui dialog
// - use correct permissions
// - save index and serial number?
// - guarded pointers for folders
// - online IMAP: check mails first, so sernums are up-to-date?
// - "ignore errors"-mode, with summary how many messages couldn't be archived?
// - do something when the user quits KMail while the backup job is running
// - run in a thread?
// - delete source folder after completion. dangerous!!!
//
// BUGS
// - Online IMAP: Test Mails -> Test%20Mails
// - corrupted sernums indices stop backup job
void BackupJob::start()
{
  Q_ASSERT( !mMailArchivePath.isEmpty() );
  Q_ASSERT( mRootFolder );

  queueFolders( mRootFolder );

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

  kdDebug(5006) << "Starting backup." << endl;
  if ( !mArchive->open( IO_WriteOnly ) ) {
    abort( i18n( "Unable to open archive for writing." ) );
    return;
  }

  mProgressItem = KPIM::ProgressManager::createProgressItem(
      "BackupJob",
      i18n( "Archiving" ),
      TQString(),
      true );
  mProgressItem->setUsesBusyIndicator( true );
  connect( mProgressItem, TQT_SIGNAL(progressItemCanceled(KPIM::ProgressItem*)),
           this, TQT_SLOT(cancelJob()) );

  archiveNextFolder();
}

#include "backupjob.moc"

