/*
 * SPDX-FileCopyrightText: 2015 Kevin Ottens <ervin@kde.org>
 * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
 */


#include "akonadilivequeryhelpers.h"

#include "akonadi/akonadicollectionfetchjobinterface.h"
#include "akonadi/akonadiitemfetchjobinterface.h"

#include "utils/jobhandler.h"

using namespace Akonadi;

LiveQueryHelpers::LiveQueryHelpers(const SerializerInterface::Ptr &serializer,
                                   const StorageInterface::Ptr &storage)
    : m_serializer(serializer),
      m_storage(storage)
{
}

LiveQueryHelpers::CollectionFetchFunction LiveQueryHelpers::fetchAllCollections(QObject *parent) const
{
    auto storage = m_storage;
    return [storage, parent, job = static_cast<CollectionFetchJobInterface *>(nullptr)] (const Domain::LiveQueryInput<Collection>::AddFunction &add) mutable {
        if (job) {
            // Cancel on-going job, so that only the latest list of items is returned
            // This prevents a race where we'd return items twice
            job->kjob()->kill();
        }
        job = storage->fetchCollections(Collection::root(), StorageInterface::Recursive, parent);
        Utils::JobHandler::install(job->kjob(), [&job, add] {
            if (job->kjob()->error() == KJob::NoError) {
                foreach (const auto &collection, job->collections())
                    add(collection);

                job = nullptr;
            }
        });
    };
}

LiveQueryHelpers::CollectionFetchFunction LiveQueryHelpers::fetchCollections(const Collection &root, QObject *parent) const
{
    auto storage = m_storage;
    return [storage, root, parent, job = static_cast<CollectionFetchJobInterface *>(nullptr)] (const Domain::LiveQueryInput<Collection>::AddFunction &add) mutable {
        if (job) {
            // Cancel on-going job, so that only the latest list of items is returned
            // This prevents a race where we'd return items twice
            job->kjob()->kill();
        }
        job = storage->fetchCollections(root, StorageInterface::Recursive, parent);
        Utils::JobHandler::install(job->kjob(), [root, &job, add] {
            if (job->kjob()->error() == KJob::NoError) {
                auto directChildren = QHash<Collection::Id, Collection>();
                foreach (const auto &collection, job->collections()) {
                    auto directChild = collection;
                    while (directChild.parentCollection() != root)
                        directChild = directChild.parentCollection();
                    if (!directChildren.contains(directChild.id()))
                        directChildren[directChild.id()] = directChild;
                }

                foreach (const auto &directChild, directChildren)
                    add(directChild);
            }
            job = nullptr;
        });
    };
}

LiveQueryHelpers::ItemFetchFunction LiveQueryHelpers::fetchItems(QObject *parent) const
{
    auto serializer = m_serializer;
    auto storage = m_storage;
    return [serializer, storage, parent, job = static_cast<CollectionFetchJobInterface *>(nullptr)] (const Domain::LiveQueryInput<Item>::AddFunction &add) mutable {
        if (job) {
            // Cancel on-going job, so that only the latest list of items is returned
            // This prevents a race where we'd return items twice
            job->kjob()->kill();
        }
        job = storage->fetchCollections(Akonadi::Collection::root(),
                                             StorageInterface::Recursive,
                                             parent);
        Utils::JobHandler::install(job->kjob(), [serializer, storage, &job, add, parent] {
            if (job->kjob()->error() == KJob::NoError) {
                foreach (const auto &collection, job->collections()) {
                    if (!serializer->isSelectedCollection(collection))
                        continue;

                    auto itemsJob = storage->fetchItems(collection, parent);
                    Utils::JobHandler::install(itemsJob->kjob(), [itemsJob, add] {
                        if (itemsJob->kjob()->error() != KJob::NoError)
                            return;

                        foreach (const auto &item, itemsJob->items())
                            add(item);
                    });
                }
            }
            job = nullptr;
        });
    };
}

LiveQueryHelpers::ItemFetchFunction LiveQueryHelpers::fetchItems(const Collection &collection, QObject *parent) const
{
    auto storage = m_storage;
    return [storage, collection, parent, job = static_cast<ItemFetchJobInterface *>(nullptr)](const Domain::LiveQueryInput<Item>::AddFunction &add) mutable {
        // Called by LiveQuery::doFetch() / LiveRelationshipQuery::doFetch()
        if (job) {
            // Cancel on-going job, so that only the latest list of items is returned
            // This prevents a race where we'd return items twice
            job->kjob()->kill();
        }
        job = storage->fetchItems(collection, parent);
        Utils::JobHandler::install(job->kjob(), [&job, add] {
            if (job->kjob()->error() == KJob::NoError) {
                const auto items = job->items();
                for(const auto &item : items)
                    add(item);
            }
            job = nullptr;
        });
    };
}

LiveQueryHelpers::ItemFetchFunction LiveQueryHelpers::fetchItemsForContext(const Domain::Context::Ptr &context, QObject *parent) const
{
    auto fetchFunction = fetchItems(parent);
    auto serializer = m_serializer;

    return [context, fetchFunction, serializer] (const Domain::LiveQueryInput<Item>::AddFunction &add) {
        auto filterAdd = [context, add, serializer] (const Item &item) {
            if (serializer->isContextChild(context, item))
                add(item);
        };
        fetchFunction(filterAdd);
    };
}

// TODO: missing a unittest in AkonadiLiveQueryHelpersTest
LiveQueryHelpers::ItemFetchFunction LiveQueryHelpers::fetchTaskAndAncestors(Domain::Task::Ptr task, QObject *parent) const
{
    Akonadi::Item childItem = m_serializer->createItemFromTask(task);
    Q_ASSERT(childItem.parentCollection().isValid()); // do I really need a fetchItem first, like fetchSiblings does?
    // Note: if the task moves to another collection, this live query will then be invalid...

    const Akonadi::Item::Id childId = childItem.id();
    auto storage = m_storage;
    auto serializer = m_serializer;
    return [storage, serializer, childItem, childId, parent, job = static_cast<ItemFetchJobInterface *>(nullptr)](const Domain::LiveQueryInput<Item>::AddFunction &add) mutable {
        if (job) {
            job->kjob()->kill();
        }
        job = storage->fetchItems(childItem.parentCollection(), parent);
        Utils::JobHandler::install(job->kjob(), [&job, add, serializer, childId] {
            if (job->kjob()->error() == KJob::NoError) {
                const auto items = job->items();
                // The item itself is part of the result, we need that in findProject, to react on changes of the item itself
                // To return a correct child item in case it got updated, we can't use childItem, we need to find it in the list.
                const auto myself = std::find_if(items.cbegin(), items.cend(), [childId](const Akonadi::Item &item) { return childId == item.id(); });
                if (myself == items.cend()) {
                    qWarning() << "Did not find item in the listing for its parent collection. Item ID:" << childId;
                    return;
                }
                add(*myself);
                auto parentUid = serializer->relatedUidFromItem(*myself);
                while (!parentUid.isEmpty()) {
                    const auto parent = std::find_if(items.cbegin(), items.cend(), [serializer, parentUid](const Akonadi::Item &item) { return serializer->itemUid(item) == parentUid; });
                    if (parent == items.cend()) {
                        break;
                    }
                    add(*parent);
                    parentUid = serializer->relatedUidFromItem(*parent);
                }
            }
            job = nullptr;
        });
    };
}

LiveQueryHelpers::CollectionFetchFunction LiveQueryHelpers::fetchItemCollection(const Item& item, QObject *parent) const
{
    auto storage = m_storage;
    return [storage, item, parent] (const Domain::LiveQueryInput<Collection>::AddFunction &add) {
        auto job = storage->fetchCollections(item.parentCollection(), StorageInterface::Base, parent);
        Utils::JobHandler::install(job->kjob(), [storage, job, add] {
            if (job->kjob()->error() != KJob::NoError)
                return;
            auto collection = job->collections().at(0);
            add(collection);
        });
    };
}


LiveQueryHelpers::ItemFetchFunction LiveQueryHelpers::fetchSiblings(const Item &item, QObject *parent) const
{
    auto storage = m_storage;
    return [storage, item, parent, job = static_cast<ItemFetchJobInterface *>(nullptr)](const Domain::LiveQueryInput<Item>::AddFunction &add) mutable {
        // Called by LiveQuery::doFetch() / LiveRelationshipQuery::doFetch()
        if (job) {
            // Cancel on-going job, so that only the latest list of items is returned
            // This prevents a race where we'd return items twice
            job->kjob()->kill();
        }
        job = storage->fetchItem(item, parent);
        Utils::JobHandler::install(job->kjob(), [storage, &job, add, parent] {
            if (job->kjob()->error() == KJob::NoError) {
                Q_ASSERT(job->items().size() == 1);
                auto item = job->items().at(0);
                Q_ASSERT(item.parentCollection().isValid());
                auto itemsJob = storage->fetchItems(item.parentCollection(), parent);
                Utils::JobHandler::install(itemsJob->kjob(), [itemsJob, add] {
                    if (itemsJob->kjob()->error() != KJob::NoError)
                        return;

                    foreach (const auto &item, itemsJob->items())
                        add(item);
                });
            }
            job = nullptr;
        });
    };
}
