/*
 *    Copyright (c) 2011 Christian Mollekopf <chrigi_1@fastmail.fm>
 *
 *    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 "trashrestorejob.h"

#include "collection.h"
#include "entitydeletedattribute.h"
#include "item.h"
#include "job_p.h"
#include "trashsettings.h"

#include <KLocale>

#include <akonadi/itemdeletejob.h>
#include <akonadi/collectiondeletejob.h>
#include <akonadi/itemmovejob.h>
#include <akonadi/collectionmovejob.h>
#include <akonadi/itemmodifyjob.h>
#include <akonadi/collectionmodifyjob.h>
#include <akonadi/collectionfetchjob.h>
#include <akonadi/itemfetchjob.h>
#include <akonadi/collectionfetchscope.h>
#include <akonadi/itemfetchscope.h>

#include <QHash>


using namespace Akonadi;

class TrashRestoreJob::TrashRestoreJobPrivate : public JobPrivate
{
  public:
    TrashRestoreJobPrivate( TrashRestoreJob *parent )
        : JobPrivate( parent ) {
    }

    void selectResult( KJob * );

    //Called when the target collection was fetched,
    //will issue the move and the removal of the attributes if collection is valid
    void targetCollectionFetched( KJob * );

    void removeAttribute( const Akonadi::Item::List & );
    void removeAttribute( const Akonadi::Collection::List & );

    //Called after initial fetch of items, issues fetch of target collection or removes attributes for in place restore
    void itemsReceived( const Akonadi::Item::List & );
    void collectionsReceived( const Akonadi::Collection::List & );

    Q_DECLARE_PUBLIC( TrashRestoreJob )

    Item::List mItems;
    Collection mCollection;
    Collection mTargetCollection;
    QHash<Collection, Item::List> restoreCollections; //groups items to target restore collections

};

void TrashRestoreJob::TrashRestoreJobPrivate::selectResult( KJob *job )
{
  Q_Q( TrashRestoreJob );
  if ( job->error() ) {
    kWarning() << job->errorString();
    return; // KCompositeJob takes care of errors
  }

  if ( !q->hasSubjobs() || ( q->subjobs().contains( static_cast<KJob*>( q->sender() ) ) && q->subjobs().size() == 1 ) ) {
    //kWarning() << "trash restore finished";
    q->emitResult();
  }
}

void TrashRestoreJob::TrashRestoreJobPrivate::targetCollectionFetched( KJob *job )
{
  Q_Q( TrashRestoreJob );

  CollectionFetchJob *fetchJob = qobject_cast<CollectionFetchJob*> ( job );
  Q_ASSERT( fetchJob );
  const Collection::List &list = fetchJob->collections();

  if ( list.isEmpty() || !list.first().isValid() || list.first().hasAttribute<Akonadi::EntityDeletedAttribute>() ) { //target collection is invalid/not existing

    const QString res = fetchJob->property( "Resource" ).toString();
    if ( res.isEmpty() ) { //There is no fallback
      q->setError( Job::Unknown );
      q->setErrorText( i18n( "Could not find restore collection and restore resource is not available" ) );
      q->emitResult();
      //FAIL
      kWarning() << "restore collection not available";
      return;
    }

    //Try again with the root collection of the resource as fallback
    CollectionFetchJob *resRootFetch = new CollectionFetchJob( Collection::root(), CollectionFetchJob::FirstLevel, q );
    resRootFetch->fetchScope().setResource( res );
    const QVariant &var = fetchJob->property( "Items" );
    if ( var.isValid() ) {
      resRootFetch->setProperty( "Items", var.toInt() );
    }
    q->connect( resRootFetch, SIGNAL(result(KJob*)), SLOT(targetCollectionFetched(KJob*)) );
    q->connect( resRootFetch, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)) );
    return;
  }
  Q_ASSERT( list.size() == 1 );
  //SUCCESS
  //We know where to move the entity, so remove the attributes and move them to the right location
  if ( !mItems.isEmpty() ) {
    const QVariant &var = fetchJob->property( "Items" );
    Q_ASSERT( var.isValid() );
    const Item::List &items = restoreCollections[Collection( var.toInt() )];

    //store removed attribute if destination collection is valid or the item doesn't have a restore collection
    //TODO only remove the attribute if the move job was succesful (although it is unlikely that it fails since we already fetched the collection)
    removeAttribute( items );
    if ( items.first().parentCollection() != list.first() ) {
      ItemMoveJob *job = new ItemMoveJob( items, list.first(), q );
      q->connect( job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)) );
    }
  } else {
    Q_ASSERT( mCollection.isValid() );
    //TODO only remove the attribute if the move job was succesful
    removeAttribute( Collection::List() << mCollection );
    CollectionFetchJob *collectionFetchJob = new CollectionFetchJob( mCollection, CollectionFetchJob::Recursive, q );
    q->connect( collectionFetchJob, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)) );
    q->connect( collectionFetchJob, SIGNAL(collectionsReceived(Akonadi::Collection::List)), SLOT(removeAttribute(Akonadi::Collection::List)) );

    if ( mCollection.parentCollection() != list.first() ) {
      CollectionMoveJob *job = new CollectionMoveJob( mCollection, list.first(), q );
      q->connect( job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)) );
    }
  }

}

void TrashRestoreJob::TrashRestoreJobPrivate::itemsReceived( const Akonadi::Item::List &items )
{
  Q_Q( TrashRestoreJob );
  if ( items.isEmpty() ) {
    q->setError( Job::Unknown );
    q->setErrorText( i18n( "Invalid items passed" ) );
    q->emitResult();
    return;
  }
  mItems = items;

  //Sort by restore collection
  foreach( Item item, mItems ) {
    if ( !item.hasAttribute<Akonadi::EntityDeletedAttribute>() ) {
      continue;
    }
    //If the restore collection is invalid we restore the item in place, so we don't need to know its restore resource => we can put those cases in the same list
    restoreCollections[item.attribute<Akonadi::EntityDeletedAttribute>()->restoreCollection()].append( item );
  }

  foreach( const Collection &col, restoreCollections.keys() ) {
    const Item &first = restoreCollections.value( col ).first();
    //Move the items to the correct collection if available
    Collection targetCollection = col;
    const QString restoreResource = first.attribute<Akonadi::EntityDeletedAttribute>()->restoreResource();

    //Restore in place if no restore collection is set
    if ( !targetCollection.isValid() ) {
      removeAttribute( restoreCollections.value( col ) );
      return;
    }

    //Explicit target overrides the resource
    if ( mTargetCollection.isValid() ) {
      targetCollection = mTargetCollection;
    }

    //Try to fetch the target resource to see if it is available
    CollectionFetchJob *fetchJob = new CollectionFetchJob( targetCollection, Akonadi::CollectionFetchJob::Base, q );
    if ( !mTargetCollection.isValid() ) { //explicit targets don't have a fallback
      fetchJob->setProperty( "Resource", restoreResource );
    }
    fetchJob->setProperty( "Items", col.id() );  //to find the items in restore collections again
    q->connect( fetchJob, SIGNAL(result(KJob*)), SLOT(targetCollectionFetched(KJob*)) );
  }
}


void TrashRestoreJob::TrashRestoreJobPrivate::collectionsReceived( const Akonadi::Collection::List &collections )
{
  Q_Q( TrashRestoreJob );
  if ( collections.isEmpty() ) {
    q->setError( Job::Unknown );
    q->setErrorText( i18n( "Invalid collection passed" ) );
    q->emitResult();
    return;
  }
  Q_ASSERT( collections.size() == 1 );
  mCollection = collections.first();

  if ( !mCollection.hasAttribute<Akonadi::EntityDeletedAttribute>() ) {
    return;
  }

  const QString restoreResource = mCollection.attribute<Akonadi::EntityDeletedAttribute>()->restoreResource();
  Collection targetCollection = mCollection.attribute<EntityDeletedAttribute>()->restoreCollection();

  //Restore in place if no restore collection/resource is set
  if ( !targetCollection.isValid() ) {
    removeAttribute( Collection::List() << mCollection );
    CollectionFetchJob *collectionFetchJob = new CollectionFetchJob( mCollection, CollectionFetchJob::Recursive, q );
    q->connect( collectionFetchJob, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)) );
    q->connect( collectionFetchJob, SIGNAL(collectionsReceived(Akonadi::Collection::List)), SLOT(removeAttribute(Akonadi::Collection::List)) );
    return;
  }

  //Explicit target overrides the resource/configured restore collection
  if ( mTargetCollection.isValid() ) {
    targetCollection = mTargetCollection;
  }

  //Fetch the target collection to check if it's valid
  CollectionFetchJob *fetchJob = new CollectionFetchJob( targetCollection, CollectionFetchJob::Base, q );
  if ( !mTargetCollection.isValid() ) { //explicit targets don't have a fallback
    fetchJob->setProperty( "Resource", restoreResource );
  }
  q->connect( fetchJob, SIGNAL(result(KJob*)), SLOT(targetCollectionFetched(KJob*)) );
}


void TrashRestoreJob::TrashRestoreJobPrivate::removeAttribute( const Akonadi::Collection::List &list )
{
  Q_Q( TrashRestoreJob );
  QListIterator<Collection> i( list );
  while ( i.hasNext() ) {
    Collection col = i.next();
    col.removeAttribute<EntityDeletedAttribute>();

    CollectionModifyJob *job = new CollectionModifyJob( col, q );
    q->connect( job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)) );

    ItemFetchJob *itemFetchJob = new ItemFetchJob( col, q );
    itemFetchJob->fetchScope().fetchAttribute<EntityDeletedAttribute> ( true );
    q->connect( itemFetchJob, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)) );
    q->connect( itemFetchJob, SIGNAL(itemsReceived(Akonadi::Item::List)), SLOT(removeAttribute(Akonadi::Item::List)) );
  }
}

void TrashRestoreJob::TrashRestoreJobPrivate::removeAttribute( const Akonadi::Item::List &list )
{
  Q_Q( TrashRestoreJob );
  Item::List items = list;
  QMutableListIterator<Item> i( items );
  while ( i.hasNext() ) {
    Item &item = i.next();
    item.removeAttribute<EntityDeletedAttribute>();
    ItemModifyJob *job = new ItemModifyJob( item, q );
    job->setIgnorePayload( true );
    q->connect( job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)) );
  }
  //For some reason it is not possible to apply this change to multiple items at once
  //ItemModifyJob *job = new ItemModifyJob(items, q);
  //q->connect( job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)) );
}


TrashRestoreJob::TrashRestoreJob( const Item & item, QObject * parent )
    : Job( new TrashRestoreJobPrivate( this ), parent )
{
  Q_D( TrashRestoreJob );
  d->mItems << item;
}

TrashRestoreJob::TrashRestoreJob( const Item::List& items, QObject* parent )
    : Job( new TrashRestoreJobPrivate( this ), parent )
{
  Q_D( TrashRestoreJob );
  d->mItems = items;
}

TrashRestoreJob::TrashRestoreJob( const Collection& collection, QObject* parent )
    : Job( new TrashRestoreJobPrivate( this ), parent )
{
  Q_D( TrashRestoreJob );
  d->mCollection = collection;
}

TrashRestoreJob::~TrashRestoreJob()
{
}

void TrashRestoreJob::setTargetCollection( const Akonadi::Collection collection )
{
  Q_D( TrashRestoreJob );
  d->mTargetCollection = collection;
}


Item::List TrashRestoreJob::items() const
{
  Q_D( const TrashRestoreJob );
  return d->mItems;
}

void TrashRestoreJob::doStart()
{
  Q_D( TrashRestoreJob );

  //We always have to fetch the entities to ensure that the EntityDeletedAttribute is available
  if ( !d->mItems.isEmpty() ) {
    ItemFetchJob *job = new ItemFetchJob( d->mItems, this );
    job->fetchScope().setCacheOnly( true );
    job->fetchScope().fetchAttribute<EntityDeletedAttribute> ( true );
    connect( job, SIGNAL(itemsReceived(Akonadi::Item::List)), this, SLOT(itemsReceived(Akonadi::Item::List)) );
  } else if ( d->mCollection.isValid() ) {
      CollectionFetchJob *job = new CollectionFetchJob( d->mCollection, CollectionFetchJob::Base, this );
      connect( job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), this, SLOT(collectionsReceived(Akonadi::Collection::List)) );
  }  else {
    kWarning() << "No valid collection or empty itemlist";
    setError( Job::Unknown );
    setErrorText( i18n( "No valid collection or empty itemlist" ) );
    emitResult();
  }

}

#include "trashrestorejob.moc"
