/*
    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 "specialcollectionsrequestjob.h"

#include "specialcollectionattribute_p.h"
#include "specialcollections.h"
#include "specialcollections_p.h"
#include "specialcollectionshelperjobs_p.h"

#include "akonadi/agentmanager.h"
#include "akonadi/collectioncreatejob.h"
#include "akonadi/entitydisplayattribute.h"

#include <KDebug>

#include <QtCore/QVariant>

using namespace Akonadi;

/**
  @internal
*/
class Akonadi::SpecialCollectionsRequestJobPrivate
{
public:
    SpecialCollectionsRequestJobPrivate(SpecialCollections *collections, SpecialCollectionsRequestJob *qq);

    bool isEverythingReady();
    void lockResult(KJob *job);   // slot
    void releaseLock(); // slot
    void nextResource();
    void resourceScanResult(KJob *job);   // slot
    void createRequestedFolders(ResourceScanJob *job, QHash<QByteArray, bool> &requestedFolders);
    void collectionCreateResult(KJob *job);   // slot

    SpecialCollectionsRequestJob *q;
    SpecialCollections *mSpecialCollections;
    int mPendingCreateJobs;

    QByteArray mRequestedType;
    AgentInstance mRequestedResource;

    // Input:
    QHash<QByteArray, bool> mDefaultFolders;
    bool mRequestingDefaultFolders;
    QHash< QString, QHash<QByteArray, bool> > mFoldersForResource;
    QString mDefaultResourceType;
    QVariantMap mDefaultResourceOptions;
    QList<QByteArray> mKnownTypes;
    QMap<QByteArray, QString> mNameForTypeMap;
    QMap<QByteArray, QString> mIconForTypeMap;

    // Output:
    QStringList mToForget;
    QVector< QPair<Collection, QByteArray> > mToRegister;
};

SpecialCollectionsRequestJobPrivate::SpecialCollectionsRequestJobPrivate(SpecialCollections *collections,
                                                                         SpecialCollectionsRequestJob *qq)
    : q(qq)
    , mSpecialCollections(collections)
    , mPendingCreateJobs(0)
    , mRequestingDefaultFolders(false)
{
}

bool SpecialCollectionsRequestJobPrivate::isEverythingReady()
{
    // check if all requested folders are known already
    if (mRequestingDefaultFolders) {
        QHashIterator<QByteArray, bool> it(mDefaultFolders);
        while (it.hasNext()) {
            it.next();
            if (it.value() && !mSpecialCollections->hasDefaultCollection(it.key())) {
                return false;
            }
        }
    }

    const QStringList resourceIds = mFoldersForResource.keys();
    QHashIterator<QString, QHash<QByteArray, bool> > resourceIt(mFoldersForResource);
    while (resourceIt.hasNext()) {
        resourceIt.next();

        const QHash<QByteArray, bool> &requested = resourceIt.value();
        QHashIterator<QByteArray, bool> it(requested);
        while (it.hasNext()) {
            it.next();
            if (it.value() && !mSpecialCollections->hasCollection(it.key(), AgentManager::self()->instance(resourceIt.key()))) {
                return false;
            }
        }
    }

    return true;
}

void SpecialCollectionsRequestJobPrivate::lockResult(KJob *job)
{
    if (job->error()) {
        kWarning() << "Failed to get lock:" << job->errorString();
        q->setError(job->error());
        q->setErrorText(job->errorString());
        q->emitResult();
        return;
    }

    if (mRequestingDefaultFolders) {
        // If default folders are requested, deal with that first.
        DefaultResourceJob *resjob = new DefaultResourceJob(mSpecialCollections->d->mSettings, q);
        resjob->setDefaultResourceType(mDefaultResourceType);
        resjob->setDefaultResourceOptions(mDefaultResourceOptions);
        resjob->setTypes(mKnownTypes);
        resjob->setNameForTypeMap(mNameForTypeMap);
        resjob->setIconForTypeMap(mIconForTypeMap);
        QObject::connect(resjob, SIGNAL(result(KJob*)), q, SLOT(resourceScanResult(KJob*)));
    } else {
        // If no default folders are requested, go straight to the next step.
        nextResource();
    }
}

void SpecialCollectionsRequestJobPrivate::releaseLock()
{
    const bool ok = Akonadi::releaseLock();
    if (!ok) {
        kWarning() << "WTF, can't release lock.";
    }
}

void SpecialCollectionsRequestJobPrivate::nextResource()
{
    if (mFoldersForResource.isEmpty()) {
        kDebug() << "All done! Comitting.";

        mSpecialCollections->d->beginBatchRegister();

        // Forget everything we knew before about these resources.
        foreach (const QString &resourceId, mToForget) {
            mSpecialCollections->d->forgetFoldersForResource(resourceId);
        }

        // Register all the collections that we fetched / created.
        typedef QPair<Collection, QByteArray> RegisterPair;
        foreach (const RegisterPair &pair, mToRegister) {
            const bool ok = mSpecialCollections->registerCollection(pair.second, pair.first);
            Q_ASSERT(ok);
            Q_UNUSED(ok);
        }

        mSpecialCollections->d->endBatchRegister();

        // Release the lock once the transaction has been committed.
        QObject::connect(q, SIGNAL(result(KJob*)), q, SLOT(releaseLock()));

        // We are done!
        q->commit();

    } else {
        const QString resourceId = mFoldersForResource.keys().first();
        kDebug() << "A resource is done," << mFoldersForResource.count()
                 << "more to do. Now doing resource" << resourceId;
        ResourceScanJob *resjob = new ResourceScanJob(resourceId, mSpecialCollections->d->mSettings, q);
        QObject::connect(resjob, SIGNAL(result(KJob*)), q, SLOT(resourceScanResult(KJob*)));
    }
}

void SpecialCollectionsRequestJobPrivate::resourceScanResult(KJob *job)
{
    ResourceScanJob *resjob = qobject_cast<ResourceScanJob *>(job);
    Q_ASSERT(resjob);

    const QString resourceId = resjob->resourceId();
    kDebug() << "resourceId" << resourceId;

    if (job->error()) {
        kWarning() << "Failed to request resource" << resourceId << ":" << job->errorString();
        return;
    }

    if (qobject_cast<DefaultResourceJob *>(job)) {
        // This is the default resource.
        if (resourceId != mSpecialCollections->d->defaultResourceId()) {
            kError() << "Resource id's don't match: " << resourceId
                     << mSpecialCollections->d->defaultResourceId();
            Q_ASSERT(false);
        }
        //mToForget.append( mSpecialCollections->defaultResourceId() );
        createRequestedFolders(resjob, mDefaultFolders);
    } else {
        // This is not the default resource.
        QHash<QByteArray, bool> requestedFolders = mFoldersForResource[resourceId];
        mFoldersForResource.remove(resourceId);
        createRequestedFolders(resjob, requestedFolders);
    }
}

void SpecialCollectionsRequestJobPrivate::createRequestedFolders(ResourceScanJob *scanJob,
                                                                 QHash<QByteArray, bool> &requestedFolders)
{
    // Remove from the request list the folders which already exist.
    foreach (const Collection &collection, scanJob->specialCollections()) {
        Q_ASSERT(collection.hasAttribute<SpecialCollectionAttribute>());
        const SpecialCollectionAttribute *attr = collection.attribute<SpecialCollectionAttribute>();
        const QByteArray type = attr->collectionType();

        if (!type.isEmpty()) {
            mToRegister.append(qMakePair(collection, type));
            requestedFolders.insert(type, false);
        }
    }
    mToForget.append(scanJob->resourceId());

    // Folders left in the request list must be created.
    Q_ASSERT(mPendingCreateJobs == 0);
    Q_ASSERT(scanJob->rootResourceCollection().isValid());

    QHashIterator<QByteArray, bool> it(requestedFolders);
    while (it.hasNext()) {
        it.next();

        if (it.value()) {
            Collection collection;
            collection.setParentCollection(scanJob->rootResourceCollection());
            collection.setName(mNameForTypeMap.value(it.key()));

            setCollectionAttributes(collection, it.key(), mNameForTypeMap, mIconForTypeMap);

            CollectionCreateJob *createJob = new CollectionCreateJob(collection, q);
            createJob->setProperty("type", it.key());
            QObject::connect(createJob, SIGNAL(result(KJob*)), q, SLOT(collectionCreateResult(KJob*)));

            mPendingCreateJobs++;
        }
    }

    if (mPendingCreateJobs == 0) {
        nextResource();
    }
}

void SpecialCollectionsRequestJobPrivate::collectionCreateResult(KJob *job)
{
    if (job->error()) {
        kWarning() << "Failed CollectionCreateJob." << job->errorString();
        return;
    }

    CollectionCreateJob *createJob = qobject_cast<CollectionCreateJob *>(job);
    Q_ASSERT(createJob);

    const Collection collection = createJob->collection();
    mToRegister.append(qMakePair(collection, createJob->property("type").toByteArray()));

    Q_ASSERT(mPendingCreateJobs > 0);
    mPendingCreateJobs--;
    kDebug() << "mPendingCreateJobs now" << mPendingCreateJobs;

    if (mPendingCreateJobs == 0) {
        nextResource();
    }
}

// TODO KDE5: do not inherit from TransactionSequence
SpecialCollectionsRequestJob::SpecialCollectionsRequestJob(SpecialCollections *collections, QObject *parent)
    : TransactionSequence(parent)
    , d(new SpecialCollectionsRequestJobPrivate(collections, this))
{
    setProperty("transactionsDisabled", true);
}

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

void SpecialCollectionsRequestJob::requestDefaultCollection(const QByteArray &type)
{
    d->mDefaultFolders[type] = true;
    d->mRequestingDefaultFolders = true;
    d->mRequestedType = type;
}

void SpecialCollectionsRequestJob::requestCollection(const QByteArray &type, const AgentInstance &instance)
{
    d->mFoldersForResource[instance.identifier()][type] = true;
    d->mRequestedType = type;
    d->mRequestedResource = instance;
}

Akonadi::Collection SpecialCollectionsRequestJob::collection() const
{
    if (d->mRequestedResource.isValid()) {
        return d->mSpecialCollections->collection(d->mRequestedType, d->mRequestedResource);
    } else {
        return d->mSpecialCollections->defaultCollection(d->mRequestedType);
    }
}

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

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

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

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

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

void SpecialCollectionsRequestJob::doStart()
{
    if (d->isEverythingReady()) {
        emitResult();
    } else {
        GetLockJob *lockJob = new GetLockJob(this);
        connect(lockJob, SIGNAL(result(KJob*)), this, SLOT(lockResult(KJob*)));
        lockJob->start();
    }
}

void SpecialCollectionsRequestJob::slotResult(KJob *job)
{
    if (job->error()) {
        // If we failed, let others try.
        kWarning() << "Failed SpecialCollectionsRequestJob::slotResult" << job->errorString();

        d->releaseLock();
    }
    TransactionSequence::slotResult(job);
}

#include "moc_specialcollectionsrequestjob.cpp"
