/*
    Copyright (c) 2009 Constantin Berzan <exit3219@gmail.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 "specialcollectionshelperjobs_p.h"

#include "dbusconnectionpool.h"
#include "specialcollectionattribute_p.h"
#include "specialcollections.h"
#include "servermanager.h"

#include <akonadi/agentinstance.h>
#include <akonadi/agentinstancecreatejob.h>
#include <akonadi/agentmanager.h>
#include <akonadi/collectionfetchjob.h>
#include <akonadi/collectionfetchscope.h>
#include <akonadi/collectionmodifyjob.h>
#include <akonadi/entitydisplayattribute.h>
#include <akonadi/resourcesynchronizationjob.h>

#include <KDebug>
#include <KLocalizedString>
#include <kcoreconfigskeleton.h>

#include <QtDBus/QDBusConnectionInterface>
#include <QtDBus/QDBusInterface>
#include <QtDBus/QDBusServiceWatcher>
#include <QtCore/QMetaMethod>
#include <QtCore/QTime>
#include <QtCore/QTimer>

#define LOCK_WAIT_TIMEOUT_SECONDS 10

using namespace Akonadi;

// convenient methods to get/set the default resource id
static void setDefaultResourceId( KCoreConfigSkeleton *settings, const QString &value )
{
  KConfigSkeletonItem *item = settings->findItem( QLatin1String( "DefaultResourceId" ) );
  Q_ASSERT( item );
  item->setProperty( value );
}

static QString defaultResourceId( KCoreConfigSkeleton *settings )
{
  const KConfigSkeletonItem *item = settings->findItem( QLatin1String( "DefaultResourceId" ) );
  Q_ASSERT( item );
  return item->property().toString();
}

static QString dbusServiceName()
{
  QString service = QString::fromLatin1("org.kde.pim.SpecialCollections");
  if (ServerManager::hasInstanceIdentifier())
    return service + ServerManager::instanceIdentifier();
  return service;
}

static QVariant::Type argumentType( const QMetaObject *mo, const QString &method )
{
  QMetaMethod m;
  for ( int i = 0; i < mo->methodCount(); ++i ) {
    const QString signature = QString::fromLatin1( mo->method( i ).signature() );
    if ( signature.startsWith( method ) )
      m = mo->method( i );
  }

  if ( !m.signature() )
    return QVariant::Invalid;

  const QList<QByteArray> argTypes = m.parameterTypes();
  if ( argTypes.count() != 1 )
    return QVariant::Invalid;

  return QVariant::nameToType( argTypes.first() );
}

// ===================== ResourceScanJob ============================

/**
  @internal
*/
class Akonadi::ResourceScanJob::Private
{
  public:
    Private( KCoreConfigSkeleton *settings, ResourceScanJob *qq );

    void fetchResult( KJob *job ); // slot

    ResourceScanJob *const q;

    // Input:
    QString mResourceId;
    KCoreConfigSkeleton *mSettings;

    // Output:
    Collection mRootCollection;
    Collection::List mSpecialCollections;
};

ResourceScanJob::Private::Private( KCoreConfigSkeleton *settings, ResourceScanJob *qq )
  : q( qq ), mSettings( settings )
{
}

void ResourceScanJob::Private::fetchResult( KJob *job )
{
  if ( job->error() ) {
    kWarning() << job->errorText();
    return;
  }

  CollectionFetchJob *fetchJob = qobject_cast<CollectionFetchJob *>( job );
  Q_ASSERT( fetchJob );

  Q_ASSERT( !mRootCollection.isValid() );
  Q_ASSERT( mSpecialCollections.isEmpty() );
  foreach ( const Collection &collection, fetchJob->collections() ) {
    if ( collection.parentCollection() == Collection::root() ) {
      if ( mRootCollection.isValid() )
        kWarning() << "Resource has more than one root collection. I don't know what to do.";
      else
        mRootCollection = collection;
    }

    if ( collection.hasAttribute<SpecialCollectionAttribute>() )
      mSpecialCollections.append( collection );
  }

  kDebug() << "Fetched root collection" << mRootCollection.id()
           << "and" << mSpecialCollections.count() << "local folders"
           << "(total" << fetchJob->collections().count() << "collections).";

  if ( !mRootCollection.isValid() ) {
    q->setError( Unknown );
    q->setErrorText( i18n( "Could not fetch root collection of resource %1.", mResourceId ) );
    q->emitResult();
    return;
  }

  // We are done!
  q->emitResult();
}

ResourceScanJob::ResourceScanJob( const QString &resourceId, KCoreConfigSkeleton *settings, QObject *parent )
  : Job( parent ),
    d( new Private( settings, this ) )
{
  setResourceId( resourceId );
}

ResourceScanJob::~ResourceScanJob()
{
  delete d;
}

QString ResourceScanJob::resourceId() const
{
  return d->mResourceId;
}

void ResourceScanJob::setResourceId( const QString &resourceId )
{
  d->mResourceId = resourceId;
}

Akonadi::Collection ResourceScanJob::rootResourceCollection() const
{
  return d->mRootCollection;
}

Akonadi::Collection::List ResourceScanJob::specialCollections() const
{
  return d->mSpecialCollections;
}

void ResourceScanJob::doStart()
{
  if ( d->mResourceId.isEmpty() ) {
    kError() << "No resource ID given.";
    setError( Job::Unknown );
    setErrorText( i18n( "No resource ID given." ) );
    emitResult();
    return;
  }

  CollectionFetchJob *fetchJob = new CollectionFetchJob( Collection::root(),
                                                         CollectionFetchJob::Recursive, this );
  fetchJob->fetchScope().setResource( d->mResourceId );
  fetchJob->fetchScope().setIncludeStatistics( true );
  connect( fetchJob, SIGNAL(result(KJob*)), this, SLOT(fetchResult(KJob*)) );
}

// ===================== DefaultResourceJob ============================

/**
  @internal
*/
class Akonadi::DefaultResourceJobPrivate
{
  public:
    DefaultResourceJobPrivate( KCoreConfigSkeleton *settings, DefaultResourceJob *qq );

    void tryFetchResource();
    void resourceCreateResult( KJob *job ); // slot
    void resourceSyncResult( KJob *job ); // slot
    void collectionFetchResult( KJob *job ); // slot
    void collectionModifyResult( KJob *job ); // slot

    DefaultResourceJob *const q;
    KCoreConfigSkeleton *mSettings;
    bool mResourceWasPreexisting;
    int mPendingModifyJobs;
    QString mDefaultResourceType;
    QVariantMap mDefaultResourceOptions;
    QList<QByteArray> mKnownTypes;
    QMap<QByteArray, QString> mNameForTypeMap;
    QMap<QByteArray, QString> mIconForTypeMap;
};

DefaultResourceJobPrivate::DefaultResourceJobPrivate( KCoreConfigSkeleton *settings, DefaultResourceJob *qq )
  : q( qq ),
    mSettings( settings ),
    mResourceWasPreexisting( true /* for safety, so as not to accidentally delete data */ ),
    mPendingModifyJobs( 0 )
{
}

void DefaultResourceJobPrivate::tryFetchResource()
{
  // Get the resourceId from config. Another instance might have changed it in the meantime.
  mSettings->readConfig();

  const QString resourceId = defaultResourceId( mSettings );

  kDebug() << "Read defaultResourceId" << resourceId << "from config.";

  const AgentInstance resource = AgentManager::self()->instance( resourceId );
  if ( resource.isValid() ) {
    // The resource exists; scan it.
    mResourceWasPreexisting = true;
    kDebug() << "Found resource" << resourceId;
    q->setResourceId( resourceId );

    CollectionFetchJob *fetchJob = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive, q );
    fetchJob->fetchScope().setResource( resourceId );
    fetchJob->fetchScope().setIncludeStatistics( true );
    q->connect( fetchJob, SIGNAL(result(KJob*)), q, SLOT(collectionFetchResult(KJob*)) );
  } else {
    // Try harder: maybe the default resource has been removed and another one added
    //             without updating the config file, in this case search for a resource
    //             of the same type and the default name
    const AgentInstance::List resources = AgentManager::self()->instances();
    foreach ( const AgentInstance &resource, resources ) {
      if ( resource.type().identifier() == mDefaultResourceType ) {
        if ( resource.name() == mDefaultResourceOptions.value( QLatin1String( "Name" ) ).toString() ) {
          // found a matching one...
          setDefaultResourceId( mSettings, resource.identifier() );
          mSettings->writeConfig();
          mResourceWasPreexisting = true;
          kDebug() << "Found resource" << resource.identifier();
          q->setResourceId( resource.identifier() );
          q->ResourceScanJob::doStart();
          return;
        }
      }
    }

    // Create the resource.
    mResourceWasPreexisting = false;
    kDebug() << "Creating maildir resource.";
    const AgentType type = AgentManager::self()->type( mDefaultResourceType );
    AgentInstanceCreateJob *job = new AgentInstanceCreateJob( type, q );
    QObject::connect( job, SIGNAL(result(KJob*)), q, SLOT(resourceCreateResult(KJob*)) );
    job->start(); // non-Akonadi::Job
  }
}

void DefaultResourceJobPrivate::resourceCreateResult( KJob *job )
{
  if ( job->error() ) {
    kWarning() << job->errorText();
    //fail( i18n( "Failed to create the default resource (%1).", job->errorString() ) );
    q->setError( job->error() );
    q->setErrorText( job->errorText() );
    q->emitResult();
    return;
  }

  AgentInstance agent;

  // Get the resource instance.
  {
    AgentInstanceCreateJob *createJob = qobject_cast<AgentInstanceCreateJob*>( job );
    Q_ASSERT( createJob );
    agent = createJob->instance();
    setDefaultResourceId( mSettings, agent.identifier() );
    kDebug() << "Created maildir resource with id" << defaultResourceId( mSettings );
  }

  const QString defaultId = defaultResourceId( mSettings );

  // Configure the resource.
  {
    agent.setName( mDefaultResourceOptions.value( QLatin1String( "Name" ) ).toString() );

    QDBusInterface conf( QString::fromLatin1( "org.freedesktop.Akonadi.Resource." ) + defaultId,
                         QString::fromLatin1( "/Settings" ), QString() );

    if ( !conf.isValid() ) {
      q->setError( -1 );
      q->setErrorText( i18n( "Invalid resource identifier '%1'", defaultId ) );
      q->emitResult();
      return;
    }

    QMapIterator<QString, QVariant> it( mDefaultResourceOptions );
    while ( it.hasNext() ) {
      it.next();

      if ( it.key() == QLatin1String( "Name" ) )
        continue;

      const QString methodName = QString::fromLatin1( "set%1" ).arg( it.key() );
      const QVariant::Type argType = argumentType( conf.metaObject(), methodName );
      if ( argType == QVariant::Invalid ) {
        q->setError( Job::Unknown );
        q->setErrorText( i18n( "Failed to configure default resource via D-Bus." ) );
        q->emitResult();
        return;
      }

      QDBusReply<void> reply = conf.call( methodName, it.value() );
      if ( !reply.isValid() ) {
        q->setError( Job::Unknown );
        q->setErrorText( i18n( "Failed to configure default resource via D-Bus." ) );
        q->emitResult();
        return;
      }
    }

    conf.call( QLatin1String( "writeConfig" ) );

    agent.reconfigure();
  }

  // Sync the resource.
  {
    ResourceSynchronizationJob *syncJob = new ResourceSynchronizationJob( agent, q );
    QObject::connect( syncJob, SIGNAL(result(KJob*)), q, SLOT(resourceSyncResult(KJob*)) );
    syncJob->start(); // non-Akonadi
  }
}

void DefaultResourceJobPrivate::resourceSyncResult( KJob *job )
{
  if ( job->error() ) {
    kWarning() << job->errorText();
    //fail( i18n( "ResourceSynchronizationJob failed (%1).", job->errorString() ) );
    return;
  }

  // Fetch the collections of the resource.
  kDebug() << "Fetching maildir collections.";
  CollectionFetchJob *fetchJob = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive, q );
  fetchJob->fetchScope().setResource( defaultResourceId( mSettings ) );
  QObject::connect( fetchJob, SIGNAL(result(KJob*)), q, SLOT(collectionFetchResult(KJob*)) );
}

void DefaultResourceJobPrivate::collectionFetchResult( KJob *job )
{
  if ( job->error() ) {
    kWarning() << job->errorText();
    //fail( i18n( "Failed to fetch the root maildir collection (%1).", job->errorString() ) );
    return;
  }

  CollectionFetchJob *fetchJob = qobject_cast<CollectionFetchJob *>( job );
  Q_ASSERT( fetchJob );

  const Collection::List collections = fetchJob->collections();
  kDebug() << "Fetched" << collections.count() << "collections.";

  // Find the root maildir collection.
  Collection::List toRecover;
  Collection resourceCollection;
  foreach ( const Collection &collection, collections ) {
    if ( collection.parentCollection() == Collection::root() ) {
      resourceCollection = collection;
      toRecover.append( collection );
      break;
    }
  }

  if ( !resourceCollection.isValid() ) {
    q->setError( Job::Unknown );
    q->setErrorText( i18n( "Failed to fetch the resource collection." ) );
    q->emitResult();
    return;
  }

  // Find all children of the resource collection.
  foreach ( const Collection &collection, collections ) {
    if ( collection.parentCollection() == resourceCollection ) {
      toRecover.append( collection );
    }
  }

  QHash<QString, QByteArray> typeForName;
  foreach ( const QByteArray &type, mKnownTypes ) {
    const QString displayName = mNameForTypeMap.value( type );
    typeForName[ displayName ] = type;
  }

  // These collections have been created by the maildir resource, when it
  // found the folders on disk. So give them the necessary attributes now.
  Q_ASSERT( mPendingModifyJobs == 0 );
  foreach ( Collection collection, toRecover ) {          // krazy:exclude=foreach

    if ( collection.hasAttribute<SpecialCollectionAttribute>() )
      continue;

    // Find the type for the collection.
    const QString name = collection.displayName();
    const QByteArray type = typeForName.value( name );

    if ( !type.isEmpty() ) {
      kDebug() << "Recovering collection" << name;
      setCollectionAttributes( collection, type, mNameForTypeMap, mIconForTypeMap );

      CollectionModifyJob *modifyJob = new CollectionModifyJob( collection, q );
      QObject::connect( modifyJob, SIGNAL(result(KJob*)), q, SLOT(collectionModifyResult(KJob*)) );
      mPendingModifyJobs++;
    } else {
      kDebug() << "Searching for names: " << typeForName.keys();
      kDebug() << "Unknown collection name" << name << "-- not recovering.";
    }
  }

  if ( mPendingModifyJobs == 0 ) {
    // Scan the resource.
    q->setResourceId( defaultResourceId( mSettings ) );
    q->ResourceScanJob::doStart();
  }
}

void DefaultResourceJobPrivate::collectionModifyResult( KJob *job )
{
  if ( job->error() ) {
    kWarning() << job->errorText();
    //fail( i18n( "Failed to modify the root maildir collection (%1).", job->errorString() ) );
    return;
  }

  Q_ASSERT( mPendingModifyJobs > 0 );
  mPendingModifyJobs--;
  kDebug() << "pendingModifyJobs now" << mPendingModifyJobs;
  if ( mPendingModifyJobs == 0 ) {
    // Write the updated config.
    kDebug() << "Writing defaultResourceId" << defaultResourceId( mSettings ) << "to config.";
    mSettings->writeConfig();

    // Scan the resource.
    q->setResourceId( defaultResourceId( mSettings ) );
    q->ResourceScanJob::doStart();
  }
}

DefaultResourceJob::DefaultResourceJob( KCoreConfigSkeleton *settings, QObject *parent )
  : ResourceScanJob( QString(), settings, parent ),
    d( new DefaultResourceJobPrivate( settings, this ) )
{
}

DefaultResourceJob::~DefaultResourceJob()
{
  delete d;
}

void DefaultResourceJob::setDefaultResourceType( const QString &type )
{
  d->mDefaultResourceType = type;
}

void DefaultResourceJob::setDefaultResourceOptions( const QVariantMap &options )
{
  d->mDefaultResourceOptions = options;
}

void DefaultResourceJob::setTypes( const QList<QByteArray> &types )
{
  d->mKnownTypes = types;
}

void DefaultResourceJob::setNameForTypeMap( const QMap<QByteArray, QString> &map )
{
  d->mNameForTypeMap = map;
}

void DefaultResourceJob::setIconForTypeMap( const QMap<QByteArray, QString> &map )
{
  d->mIconForTypeMap = map;
}

void DefaultResourceJob::doStart()
{
  d->tryFetchResource();
}

void DefaultResourceJob::slotResult( KJob *job )
{
  if ( job->error() ) {
    kWarning() << job->errorText();
    // Do some cleanup.
    if ( !d->mResourceWasPreexisting ) {
      // We only removed the resource instance if we have created it.
      // Otherwise we might lose the user's data.
      const AgentInstance resource = AgentManager::self()->instance( defaultResourceId( d->mSettings ) );
      kDebug() << "Removing resource" << resource.identifier();
      AgentManager::self()->removeInstance( resource );
    }
  }

  Job::slotResult( job );
}

// ===================== GetLockJob ============================

class Akonadi::GetLockJob::Private
{
  public:
    Private( GetLockJob *qq );

    void doStart(); // slot
    void serviceOwnerChanged( const QString &name, const QString &oldOwner,
                              const QString &newOwner ); // slot
    void timeout(); // slot

    GetLockJob *const q;
    QTimer *mSafetyTimer;
};

GetLockJob::Private::Private( GetLockJob *qq )
  : q( qq ),
    mSafetyTimer( 0 )
{
}

void GetLockJob::Private::doStart()
{
  // Just doing registerService() and checking its return value is not sufficient,
  // since we may *already* own the name, and then registerService() returns true.

  QDBusConnection bus = DBusConnectionPool::threadConnection();
  const bool alreadyLocked = bus.interface()->isServiceRegistered( dbusServiceName() );
  const bool gotIt = bus.registerService( dbusServiceName() );

  if ( gotIt && !alreadyLocked ) {
    //kDebug() << "Got lock immediately.";
    q->emitResult();
  } else {
    QDBusServiceWatcher *watcher = new QDBusServiceWatcher( dbusServiceName(), DBusConnectionPool::threadConnection(),
                                                            QDBusServiceWatcher::WatchForOwnerChange, q );
    //kDebug() << "Waiting for lock.";
    connect( watcher, SIGNAL(serviceOwnerChanged(QString,QString,QString)),
             q, SLOT(serviceOwnerChanged(QString,QString,QString)) );

    mSafetyTimer = new QTimer( q );
    mSafetyTimer->setSingleShot( true );
    mSafetyTimer->setInterval( LOCK_WAIT_TIMEOUT_SECONDS * 1000 );
    mSafetyTimer->start();
    connect( mSafetyTimer, SIGNAL(timeout()), q, SLOT(timeout()) );
  }
}

void GetLockJob::Private::serviceOwnerChanged( const QString&, const QString&, const QString &newOwner )
{
  if ( newOwner.isEmpty() ) {
    const bool gotIt = DBusConnectionPool::threadConnection().registerService( dbusServiceName() );
    if ( gotIt ) {
      mSafetyTimer->stop();
      q->emitResult();
    }
  }
}

void GetLockJob::Private::timeout()
{
  kWarning() << "Timeout trying to get lock. Check who has acquired the name" << dbusServiceName() << "on DBus, using qdbus or qdbusviewer.";
  q->setError( Job::Unknown );
  q->setErrorText( i18n( "Timeout trying to get lock." ) );
  q->emitResult();
}

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

GetLockJob::~GetLockJob()
{
  delete d;
}

void GetLockJob::start()
{
  QTimer::singleShot( 0, this, SLOT(doStart()) );
}

void Akonadi::setCollectionAttributes( Akonadi::Collection &collection, const QByteArray &type,
                                       const QMap<QByteArray, QString> &nameForType,
                                       const QMap<QByteArray, QString> &iconForType )
{
  {
    EntityDisplayAttribute *attr = new EntityDisplayAttribute;
    attr->setIconName( iconForType.value( type ) );
    attr->setDisplayName( nameForType.value( type ) );
    collection.addAttribute( attr );
  }

  {
    SpecialCollectionAttribute *attr = new SpecialCollectionAttribute;
    attr->setCollectionType( type );
    collection.addAttribute( attr );
  }
}

bool Akonadi::releaseLock()
{
  return DBusConnectionPool::threadConnection().unregisterService( dbusServiceName() );
}

#include "moc_specialcollectionshelperjobs_p.cpp"
