/*
    SPDX-FileCopyrightText: 2008 Volker Krause <vkrause@kde.org>

    SPDX-License-Identifier: LGPL-2.0-or-later
*/

#include "standardactionmanager.h"
#include "standardactionmanager_p.h"

#include "actionstatemanager_p.h"
#include "agentconfigurationdialog.h"
#include "agentfilterproxymodel.h"
#include "agentinstancecreatejob.h"
#include "agentmanager.h"
#include "agenttypedialog.h"
#include "collectioncreatejob.h"
#include "collectiondeletejob.h"
#include "collectiondialog.h"
#include "collectionpropertiesdialog.h"
#include "collectionpropertiespage.h"
#include "collectionutils.h"
#include "entitydeletedattribute.h"
#include "entitytreemodel.h"
#include "favoritecollectionsmodel.h"
#include "itemdeletejob.h"
#include "pastehelper_p.h"
#include "recentcollectionaction_p.h"
#include "renamefavoritedialog_p.h"
#include "specialcollectionattribute.h"
#include "subscriptiondialog.h"
#include "trashjob.h"
#include "trashrestorejob.h"

#include <KActionCollection>
#include <KActionMenu>
#include <KConfig>
#include <KConfigGroup>
#include <KLocalizedString>
#include <KMessageBox>
#include <KToggleAction>
#include <QAction>
#include <QIcon>
#include <QMenu>

#include <QApplication>
#include <QClipboard>
#include <QInputDialog>
#include <QItemSelectionModel>
#include <QMimeData>
#include <QPointer>
#include <QRegularExpression>
#include <QTimer>

#include <KLazyLocalizedString>

using namespace Akonadi;

/// @cond PRIVATE

enum ActionType {
    NormalAction,
    ActionWithAlternative, // Normal action, but with an alternative state
    ActionAlternative, // Alternative state of the ActionWithAlternative
    MenuAction,
    ToggleAction
};

struct StandardActionData { // NOLINT(clang-analyzer-optin.performance.Padding) FIXME
    const char *name;
    const KLazyLocalizedString label;
    const KLazyLocalizedString iconLabel;
    const char *icon;
    const char *altIcon;
    QKeySequence shortcut;
    const char *slot;
    ActionType actionType;
};

static const StandardActionData standardActionData[] = {
    {"akonadi_collection_create", kli18n("&New Folder..."), kli18n("New"), "folder-new", nullptr, QKeySequence(), SLOT(slotCreateCollection()), NormalAction},
    {"akonadi_collection_copy",
     KLazyLocalizedString(),
     KLazyLocalizedString(),
     "edit-copy",
     nullptr,
     QKeySequence(),
     SLOT(slotCopyCollections()),
     NormalAction},
    {"akonadi_collection_delete",
     kli18n("&Delete Folder"),
     kli18n("Delete"),
     "edit-delete",
     nullptr,
     QKeySequence(),
     SLOT(slotDeleteCollection()),
     NormalAction},
    {"akonadi_collection_sync",
     kli18n("&Synchronize Folder"),
     kli18n("Synchronize"),
     "view-refresh",
     nullptr,
     Qt::Key_F5,
     SLOT(slotSynchronizeCollection()),
     NormalAction},
    {"akonadi_collection_properties",
     kli18n("Folder &Properties"),
     kli18n("Properties"),
     "configure",
     nullptr,
     QKeySequence(),
     SLOT(slotCollectionProperties()),
     NormalAction},
    {"akonadi_item_copy", KLazyLocalizedString(), KLazyLocalizedString(), "edit-copy", nullptr, 0, SLOT(slotCopyItems()), NormalAction},
    {"akonadi_paste", kli18n("&Paste"), kli18n("Paste"), "edit-paste", nullptr, Qt::CTRL | Qt::Key_V, SLOT(slotPaste()), NormalAction},
    {"akonadi_item_delete", KLazyLocalizedString(), KLazyLocalizedString(), "edit-delete", nullptr, 0, SLOT(slotDeleteItems()), NormalAction},
    {"akonadi_manage_local_subscriptions",
     kli18n("Manage Local &Subscriptions..."),
     kli18n("Manage Local Subscriptions"),
     "folder-bookmarks",
     nullptr,
     QKeySequence(),
     SLOT(slotLocalSubscription()),
     NormalAction},
    {"akonadi_collection_add_to_favorites",
     kli18n("Add to Favorite Folders"),
     kli18n("Add to Favorite"),
     "bookmark-new",
     nullptr,
     QKeySequence(),
     SLOT(slotAddToFavorites()),
     NormalAction},
    {"akonadi_collection_remove_from_favorites",
     kli18n("Remove from Favorite Folders"),
     kli18n("Remove from Favorite"),
     "edit-delete",
     nullptr,
     QKeySequence(),
     SLOT(slotRemoveFromFavorites()),
     NormalAction},
    {"akonadi_collection_rename_favorite",
     kli18n("Rename Favorite..."),
     kli18n("Rename"),
     "edit-rename",
     nullptr,
     QKeySequence(),
     SLOT(slotRenameFavorite()),
     NormalAction},
    {"akonadi_collection_copy_to_menu",
     kli18n("Copy Folder To..."),
     kli18n("Copy To"),
     "edit-copy",
     nullptr,
     QKeySequence(),
     SLOT(slotCopyCollectionTo(QAction *)),
     MenuAction},
    {"akonadi_item_copy_to_menu",
     kli18n("Copy Item To..."),
     kli18n("Copy To"),
     "edit-copy",
     nullptr,
     QKeySequence(),
     SLOT(slotCopyItemTo(QAction *)),
     MenuAction},
    {"akonadi_item_move_to_menu",
     kli18n("Move Item To..."),
     kli18n("Move To"),
     "edit-move",
     "go-jump",
     QKeySequence(),
     SLOT(slotMoveItemTo(QAction *)),
     MenuAction},
    {"akonadi_collection_move_to_menu",
     kli18n("Move Folder To..."),
     kli18n("Move To"),
     "edit-move",
     "go-jump",
     QKeySequence(),
     SLOT(slotMoveCollectionTo(QAction *)),
     MenuAction},
    {"akonadi_item_cut", kli18n("&Cut Item"), kli18n("Cut"), "edit-cut", nullptr, Qt::CTRL | Qt::Key_X, SLOT(slotCutItems()), NormalAction},
    {"akonadi_collection_cut", kli18n("&Cut Folder"), kli18n("Cut"), "edit-cut", nullptr, Qt::CTRL | Qt::Key_X, SLOT(slotCutCollections()), NormalAction},
    {"akonadi_resource_create",
     kli18n("Create Resource"),
     KLazyLocalizedString(),
     "folder-new",
     nullptr,
     QKeySequence(),
     SLOT(slotCreateResource()),
     NormalAction},
    {"akonadi_resource_delete",
     kli18n("Delete Resource"),
     KLazyLocalizedString(),
     "edit-delete",
     nullptr,
     QKeySequence(),
     SLOT(slotDeleteResource()),
     NormalAction},
    {"akonadi_resource_properties",
     kli18n("&Resource Properties"),
     kli18n("Properties"),
     "configure",
     nullptr,
     QKeySequence(),
     SLOT(slotResourceProperties()),
     NormalAction},
    {"akonadi_resource_synchronize",
     kli18n("Synchronize Resource"),
     kli18n("Synchronize"),
     "view-refresh",
     nullptr,
     QKeySequence(),
     SLOT(slotSynchronizeResource()),
     NormalAction},
    {"akonadi_work_offline",
     kli18n("Work Offline"),
     KLazyLocalizedString(),
     "user-offline",
     nullptr,
     QKeySequence(),
     SLOT(slotToggleWorkOffline(bool)),
     ToggleAction},
    {"akonadi_collection_copy_to_dialog",
     kli18n("Copy Folder To..."),
     kli18n("Copy To"),
     "edit-copy",
     nullptr,
     QKeySequence(),
     SLOT(slotCopyCollectionTo()),
     NormalAction},
    {"akonadi_collection_move_to_dialog",
     kli18n("Move Folder To..."),
     kli18n("Move To"),
     "edit-move",
     "go-jump",
     QKeySequence(),
     SLOT(slotMoveCollectionTo()),
     NormalAction},
    {"akonadi_item_copy_to_dialog", kli18n("Copy Item To..."), kli18n("Copy To"), "edit-copy", nullptr, QKeySequence(), SLOT(slotCopyItemTo()), NormalAction},
    {"akonadi_item_move_to_dialog", kli18n("Move Item To..."), kli18n("Move To"), "edit-move", "go-jump", QKeySequence(), SLOT(slotMoveItemTo()), NormalAction},
    {"akonadi_collection_sync_recursive",
     kli18n("&Synchronize Folder Recursively"),
     kli18n("Synchronize Recursively"),
     "view-refresh",
     nullptr,
     Qt::CTRL | Qt::Key_F5,
     SLOT(slotSynchronizeCollectionRecursive()),
     NormalAction},
    {"akonadi_move_collection_to_trash",
     kli18n("&Move Folder To Trash"),
     kli18n("Move Folder To Trash"),
     "edit-delete",
     nullptr,
     QKeySequence(),
     SLOT(slotMoveCollectionToTrash()),
     NormalAction},
    {"akonadi_move_item_to_trash",
     kli18n("&Move Item To Trash"),
     kli18n("Move Item To Trash"),
     "edit-delete",
     nullptr,
     QKeySequence(),
     SLOT(slotMoveItemToTrash()),
     NormalAction},
    {"akonadi_restore_collection_from_trash",
     kli18n("&Restore Folder From Trash"),
     kli18n("Restore Folder From Trash"),
     "view-refresh",
     nullptr,
     QKeySequence(),
     SLOT(slotRestoreCollectionFromTrash()),
     NormalAction},
    {"akonadi_restore_item_from_trash",
     kli18n("&Restore Item From Trash"),
     kli18n("Restore Item From Trash"),
     "view-refresh",
     nullptr,
     QKeySequence(),
     SLOT(slotRestoreItemFromTrash()),
     NormalAction},
    {"akonadi_collection_trash_restore",
     kli18n("&Restore Folder From Trash"),
     kli18n("Restore Folder From Trash"),
     "edit-delete",
     nullptr,
     QKeySequence(),
     SLOT(slotTrashRestoreCollection()),
     ActionWithAlternative},
    {nullptr,
     kli18n("&Restore Collection From Trash"),
     kli18n("Restore Collection From Trash"),
     "view-refresh",
     nullptr,
     QKeySequence(),
     nullptr,
     ActionAlternative},
    {"akonadi_item_trash_restore",
     kli18n("&Restore Item From Trash"),
     kli18n("Restore Item From Trash"),
     "edit-delete",
     nullptr,
     0,
     SLOT(slotTrashRestoreItem()),
     ActionWithAlternative},
    {nullptr, kli18n("&Restore Item From Trash"), kli18n("Restore Item From Trash"), "view-refresh", nullptr, QKeySequence(), nullptr, ActionAlternative},
    {"akonadi_collection_sync_favorite_folders",
     kli18n("&Synchronize Favorite Folders"),
     kli18n("Synchronize Favorite Folders"),
     "view-refresh",
     nullptr,
     Qt::CTRL | Qt::SHIFT | Qt::Key_L,
     SLOT(slotSynchronizeFavoriteCollections()),
     NormalAction},
    {"akonadi_resource_synchronize_collectiontree",
     kli18n("Synchronize Folder Tree"),
     kli18n("Synchronize"),
     "view-refresh",
     nullptr,
     QKeySequence(),
     SLOT(slotSynchronizeCollectionTree()),
     NormalAction},

};
static const int numStandardActionData = sizeof standardActionData / sizeof *standardActionData;

static QIcon standardActionDataIcon(const StandardActionData &data)
{
    if (data.altIcon) {
        return QIcon::fromTheme(QString::fromLatin1(data.icon), QIcon::fromTheme(QString::fromLatin1(data.altIcon)));
    }
    return QIcon::fromTheme(QString::fromLatin1(data.icon));
}

static_assert(numStandardActionData == StandardActionManager::LastType, "StandardActionData table does not match StandardActionManager types");

static bool canCreateCollection(const Akonadi::Collection &collection)
{
    return !!(collection.rights() & Akonadi::Collection::CanCreateCollection);
}

static void setWorkOffline(bool offline)
{
    KConfig config(QStringLiteral("akonadikderc"));
    KConfigGroup group(&config, QStringLiteral("Actions"));

    group.writeEntry("WorkOffline", offline);
}

static bool workOffline()
{
    KConfig config(QStringLiteral("akonadikderc"));
    const KConfigGroup group(&config, QStringLiteral("Actions"));

    return group.readEntry("WorkOffline", false);
}

static QModelIndexList safeSelectedRows(QItemSelectionModel *selectionModel)
{
    QModelIndexList selectedRows = selectionModel->selectedRows();
    if (!selectedRows.isEmpty()) {
        return selectedRows;
    }

    // try harder for selected rows that don't span the full row for some reason (e.g. due to buggy column adding proxy models etc)
    const auto selection = selectionModel->selection();
    for (const auto &range : selection) {
        if (!range.isValid() || range.isEmpty()) {
            continue;
        }
        const QModelIndex parent = range.parent();
        for (int row = range.top(); row <= range.bottom(); ++row) {
            const QModelIndex index = range.model()->index(row, range.left(), parent);
            const Qt::ItemFlags flags = range.model()->flags(index);
            if ((flags & Qt::ItemIsSelectable) && (flags & Qt::ItemIsEnabled)) {
                selectedRows.push_back(index);
            }
        }
    }

    return selectedRows;
}

StandardActionManagerPrivate::StandardActionManagerPrivate(StandardActionManager *parent)
    : q(parent)
{
    actions.fill(nullptr, StandardActionManager::LastType);

    pluralLabels.insert(StandardActionManager::CopyCollections, ki18np("&Copy Folder", "&Copy %1 Folders"));
    pluralLabels.insert(StandardActionManager::CopyItems, ki18np("&Copy Item", "&Copy %1 Items"));
    pluralLabels.insert(StandardActionManager::CutItems, ki18ncp("@action", "&Cut Item", "&Cut %1 Items"));
    pluralLabels.insert(StandardActionManager::CutCollections, ki18ncp("@action", "&Cut Folder", "&Cut %1 Folders"));
    pluralLabels.insert(StandardActionManager::DeleteItems, ki18np("&Delete Item", "&Delete %1 Items"));
    pluralLabels.insert(StandardActionManager::DeleteCollections, ki18ncp("@action", "&Delete Folder", "&Delete %1 Folders"));
    pluralLabels.insert(StandardActionManager::SynchronizeCollections, ki18ncp("@action", "&Synchronize Folder", "&Synchronize %1 Folders"));
    pluralLabels.insert(StandardActionManager::DeleteResources, ki18np("&Delete Resource", "&Delete %1 Resources"));
    pluralLabels.insert(StandardActionManager::SynchronizeResources, ki18np("&Synchronize Resource", "&Synchronize %1 Resources"));

    pluralIconLabels.insert(StandardActionManager::CopyCollections, ki18np("Copy Folder", "Copy %1 Folders"));
    pluralIconLabels.insert(StandardActionManager::CopyItems, ki18np("Copy Item", "Copy %1 Items"));
    pluralIconLabels.insert(StandardActionManager::CutItems, ki18np("Cut Item", "Cut %1 Items"));
    pluralIconLabels.insert(StandardActionManager::CutCollections, ki18np("Cut Folder", "Cut %1 Folders"));
    pluralIconLabels.insert(StandardActionManager::DeleteItems, ki18np("Delete Item", "Delete %1 Items"));
    pluralIconLabels.insert(StandardActionManager::DeleteCollections, ki18np("Delete Folder", "Delete %1 Folders"));
    pluralIconLabels.insert(StandardActionManager::SynchronizeCollections, ki18np("Synchronize Folder", "Synchronize %1 Folders"));
    pluralIconLabels.insert(StandardActionManager::DeleteResources, ki18ncp("@action", "Delete Resource", "Delete %1 Resources"));
    pluralIconLabels.insert(StandardActionManager::SynchronizeResources, ki18ncp("@action", "Synchronize Resource", "Synchronize %1 Resources"));

    setContextText(StandardActionManager::CreateCollection, StandardActionManager::DialogTitle, ki18nc("@title:window", "New Folder"));
    setContextText(StandardActionManager::CreateCollection, StandardActionManager::DialogText, ki18nc("@label:textbox name of Akonadi folder", "Name"));
    setContextText(StandardActionManager::CreateCollection, StandardActionManager::ErrorMessageText, ki18n("Could not create folder: %1"));
    setContextText(StandardActionManager::CreateCollection, StandardActionManager::ErrorMessageTitle, ki18nc("@title:window", "Folder Creation Failed"));

    setContextText(
        StandardActionManager::DeleteCollections,
        StandardActionManager::MessageBoxText,
        ki18np("Do you really want to delete this folder and all its sub-folders?", "Do you really want to delete %1 folders and all their sub-folders?"));
    setContextText(StandardActionManager::DeleteCollections,
                   StandardActionManager::MessageBoxTitle,
                   ki18ncp("@title:window", "Delete Folder?", "Delete Folders?"));
    setContextText(StandardActionManager::DeleteCollections, StandardActionManager::ErrorMessageText, ki18n("Could not delete folder: %1"));
    setContextText(StandardActionManager::DeleteCollections, StandardActionManager::ErrorMessageTitle, ki18nc("@title:window", "Folder Deletion Failed"));

    setContextText(StandardActionManager::CollectionProperties, StandardActionManager::DialogTitle, ki18nc("@title:window", "Properties of Folder %1"));

    setContextText(StandardActionManager::DeleteItems,
                   StandardActionManager::MessageBoxText,
                   ki18np("Do you really want to delete the selected item?", "Do you really want to delete %1 items?"));
    setContextText(StandardActionManager::DeleteItems, StandardActionManager::MessageBoxTitle, ki18ncp("@title:window", "Delete Item?", "Delete Items?"));
    setContextText(StandardActionManager::DeleteItems, StandardActionManager::ErrorMessageText, ki18n("Could not delete item: %1"));
    setContextText(StandardActionManager::DeleteItems, StandardActionManager::ErrorMessageTitle, ki18nc("@title:window", "Item Deletion Failed"));

    setContextText(StandardActionManager::RenameFavoriteCollection, StandardActionManager::DialogTitle, ki18nc("@title:window", "Rename Favorite"));
    setContextText(StandardActionManager::RenameFavoriteCollection, StandardActionManager::DialogText, ki18nc("@label:textbox name of the folder", "Name:"));

    setContextText(StandardActionManager::CreateResource, StandardActionManager::DialogTitle, ki18nc("@title:window", "New Resource"));
    setContextText(StandardActionManager::CreateResource, StandardActionManager::ErrorMessageText, ki18n("Could not create resource: %1"));
    setContextText(StandardActionManager::CreateResource, StandardActionManager::ErrorMessageTitle, ki18nc("@title:window", "Resource Creation Failed"));

    setContextText(StandardActionManager::DeleteResources,
                   StandardActionManager::MessageBoxText,
                   ki18np("Do you really want to delete this resource?", "Do you really want to delete %1 resources?"));
    setContextText(StandardActionManager::DeleteResources,
                   StandardActionManager::MessageBoxTitle,
                   ki18ncp("@title:window", "Delete Resource?", "Delete Resources?"));

    setContextText(StandardActionManager::Paste, StandardActionManager::ErrorMessageText, ki18n("Could not paste data: %1"));
    setContextText(StandardActionManager::Paste, StandardActionManager::ErrorMessageTitle, ki18nc("@title:window", "Paste Failed"));

    mDelayedUpdateTimer.setSingleShot(true);
    QObject::connect(&mDelayedUpdateTimer, &QTimer::timeout, q, [this]() {
        updateActions();
    });

    qRegisterMetaType<Akonadi::Item::List>("Akonadi::Item::List");
}

void StandardActionManagerPrivate::enableAction(StandardActionManager::Type type, bool enable)
{
    Q_ASSERT(type < StandardActionManager::LastType);
    if (actions[type]) {
        actions[type]->setEnabled(enable);
    }

    // Update the action menu
    auto actionMenu = qobject_cast<KActionMenu *>(actions[type]);
    if (actionMenu) {
        // get rid of the submenus, they are re-created in enableAction. clear() is not enough, doesn't remove the submenu object instances.
        QMenu *menu = actionMenu->menu();
        // Not necessary to delete and recreate menu when it was not created
        if (menu->property("actionType").isValid() && menu->isEmpty()) {
            return;
        }
        mRecentCollectionsMenu.remove(type);
        delete menu;
        menu = new QMenu();

        menu->setProperty("actionType", static_cast<int>(type));
        q->connect(menu, &QMenu::aboutToShow, q, [this]() {
            aboutToShowMenu();
        });
        q->connect(menu, SIGNAL(triggered(QAction *)), standardActionData[type].slot); // clazy:exclude=old-style-connect
        actionMenu->setMenu(menu);
    }
}

void StandardActionManagerPrivate::aboutToShowMenu()
{
    auto menu = qobject_cast<QMenu *>(q->sender());
    if (!menu) {
        return;
    }

    if (!menu->isEmpty()) {
        return;
    }
    // collect all selected collections
    const Akonadi::Collection::List selectedCollectionsList = selectedCollections();
    const StandardActionManager::Type type = static_cast<StandardActionManager::Type>(menu->property("actionType").toInt());

    QPointer<RecentCollectionAction> recentCollection = new RecentCollectionAction(type, selectedCollectionsList, collectionSelectionModel->model(), menu);
    mRecentCollectionsMenu.insert(type, recentCollection);
    const QSet<QString> mimeTypes = mimeTypesOfSelection(type);
    fillFoldersMenu(selectedCollectionsList, mimeTypes, type, menu, collectionSelectionModel->model(), QModelIndex());
}

void StandardActionManagerPrivate::createActionFolderMenu(QMenu *menu, StandardActionManager::Type type)
{
    if (type == StandardActionManager::CopyCollectionToMenu || type == StandardActionManager::CopyItemToMenu || type == StandardActionManager::MoveItemToMenu
        || type == StandardActionManager::MoveCollectionToMenu) {
        new RecentCollectionAction(type, Akonadi::Collection::List(), collectionSelectionModel->model(), menu);
        Collection::List selectedCollectionsList = selectedCollections();
        const QSet<QString> mimeTypes = mimeTypesOfSelection(type);
        fillFoldersMenu(selectedCollectionsList, mimeTypes, type, menu, collectionSelectionModel->model(), QModelIndex());
    }
}

void StandardActionManagerPrivate::updateAlternatingAction(StandardActionManager::Type type)
{
    Q_ASSERT(type < StandardActionManager::LastType);
    if (!actions[type]) {
        return;
    }

    /*
     * The same action is stored at the ActionWithAlternative indexes as well as the corresponding ActionAlternative indexes in the actions array.
     * The following simply changes the standardActionData
     */
    if ((standardActionData[type].actionType == ActionWithAlternative) || (standardActionData[type].actionType == ActionAlternative)) {
        actions[type]->setText(standardActionData[type].label.toString());
        actions[type]->setIcon(standardActionDataIcon(standardActionData[type]));

        if (pluralLabels.contains(type) && !pluralLabels.value(type).isEmpty()) {
            actions[type]->setText(pluralLabels.value(type).subs(1).toString());
        } else if (!standardActionData[type].label.isEmpty()) {
            actions[type]->setText(standardActionData[type].label.toString());
        }
        if (pluralIconLabels.contains(type) && !pluralIconLabels.value(type).isEmpty()) {
            actions[type]->setIconText(pluralIconLabels.value(type).subs(1).toString());
        } else if (!standardActionData[type].iconLabel.isEmpty()) {
            actions[type]->setIconText(standardActionData[type].iconLabel.toString());
        }
        if (standardActionData[type].icon) {
            actions[type]->setIcon(standardActionDataIcon(standardActionData[type]));
        }

        // actions[type]->setShortcut( standardActionData[type].shortcut );

        /*if ( standardActionData[type].slot ) {
            switch ( standardActionData[type].actionType ) {
            case NormalAction:
            case ActionWithAlternative:
                connect( action, SIGNAL(triggered()), standardActionData[type].slot );
                break;
            }
        }*/
    }
}

void StandardActionManagerPrivate::updatePluralLabel(StandardActionManager::Type type, int count)
{
    Q_ASSERT(type < StandardActionManager::LastType);
    if (actions[type] && pluralLabels.contains(type) && !pluralLabels.value(type).isEmpty()) {
        actions[type]->setText(pluralLabels.value(type).subs(qMax(count, 1)).toString());
    }
}

bool StandardActionManagerPrivate::isFavoriteCollection(const Akonadi::Collection &collection) const
{
    if (!favoritesModel) {
        return false;
    }

    return favoritesModel->collectionIds().contains(collection.id());
}

void StandardActionManagerPrivate::encodeToClipboard(QItemSelectionModel *selectionModel, bool cut)
{
    Q_UNUSED(cut);
    Q_ASSERT(selectionModel);
    if (safeSelectedRows(selectionModel).isEmpty()) {
        return;
    }

#ifndef QT_NO_CLIPBOARD
    auto model = const_cast<QAbstractItemModel *>(selectionModel->model());
    QMimeData *mimeData = selectionModel->model()->mimeData(safeSelectedRows(selectionModel));
    model->setData(QModelIndex(), false, EntityTreeModel::PendingCutRole);
    markCutAction(mimeData, cut);
    QApplication::clipboard()->setMimeData(mimeData);
    if (cut) {
        const auto rows = safeSelectedRows(selectionModel);
        for (const auto &index : rows) {
            model->setData(index, true, EntityTreeModel::PendingCutRole);
        }
    }
#endif
}

Akonadi::Collection::List StandardActionManagerPrivate::collectionsForIndexes(const QModelIndexList &list)
{
    Akonadi::Collection::List collectionList;
    for (const QModelIndex &index : list) {
        auto collection = index.data(EntityTreeModel::CollectionRole).value<Collection>();
        if (!collection.isValid()) {
            continue;
        }

        const auto parentCollection = index.data(EntityTreeModel::ParentCollectionRole).value<Collection>();
        collection.setParentCollection(parentCollection);

        collectionList << std::move(collection);
    }
    return collectionList;
}

void StandardActionManagerPrivate::delayedUpdateActions()
{
    // Compress changes (e.g. when deleting many rows, do this only once)
    mDelayedUpdateTimer.start(0);
}

void StandardActionManagerPrivate::updateActions()
{
    // favorite collections
    Collection::List selectedFavoriteCollectionsList;
    if (favoriteSelectionModel) {
        const QModelIndexList rows = safeSelectedRows(favoriteSelectionModel);
        selectedFavoriteCollectionsList = collectionsForIndexes(rows);
    }

    // collect all selected collections
    Collection::List selectedCollectionsList;
    if (collectionSelectionModel) {
        const QModelIndexList rows = safeSelectedRows(collectionSelectionModel);
        selectedCollectionsList = collectionsForIndexes(rows);
    }

    // collect all selected items
    Item::List selectedItems;
    if (itemSelectionModel) {
        const QModelIndexList rows = safeSelectedRows(itemSelectionModel);
        for (const QModelIndex &index : rows) {
            Item item = index.data(EntityTreeModel::ItemRole).value<Item>();
            if (!item.isValid()) {
                continue;
            }

            const auto parentCollection = index.data(EntityTreeModel::ParentCollectionRole).value<Collection>();
            item.setParentCollection(parentCollection);

            selectedItems << item;
        }
    }

#ifndef QT_NO_CLIPBOARD
    mActionStateManager.updateState(selectedCollectionsList, selectedFavoriteCollectionsList, selectedItems, clipboardUrls);
#endif
    if (favoritesModel) {
        enableAction(StandardActionManager::SynchronizeFavoriteCollections, (favoritesModel->rowCount() > 0));
    }
    Q_EMIT q->selectionsChanged(selectedCollectionsList, selectedFavoriteCollectionsList, selectedItems);
    Q_EMIT q->actionStateUpdated();
}

#ifndef QT_NO_CLIPBOARD
void StandardActionManagerPrivate::clipboardChanged()
{
    const QMimeData *mimeData = QApplication::clipboard()->mimeData();
    if ((mimeData == nullptr) || (!mimeData->hasUrls())) {
        if (!clipboardUrls.empty()) {
            clipboardUrls.clear();
            updateActions();
        }
        return;
    }

    QList<QUrl> urls = mimeData->urls();
    if (clipboardUrls == urls)
        return;

    clipboardUrls = std::move(urls);
    updateActions();
}
#endif

QItemSelection StandardActionManagerPrivate::mapToEntityTreeModel(const QAbstractItemModel *model, const QItemSelection &selection) const
{
    const auto proxy = qobject_cast<const QAbstractProxyModel *>(model);
    if (proxy) {
        return mapToEntityTreeModel(proxy->sourceModel(), proxy->mapSelectionToSource(selection));
    } else {
        return selection;
    }
}

QItemSelection StandardActionManagerPrivate::mapFromEntityTreeModel(const QAbstractItemModel *model, const QItemSelection &selection) const
{
    const auto proxy = qobject_cast<const QAbstractProxyModel *>(model);
    if (proxy) {
        const QItemSelection select = mapFromEntityTreeModel(proxy->sourceModel(), selection);
        return proxy->mapSelectionFromSource(select);
    } else {
        return selection;
    }
}

void StandardActionManagerPrivate::collectionSelectionChanged()
{
    if (insideSelectionSlot) {
        return;
    }
    InsideSelectionSlotBlocker block(this);
    if (favoriteSelectionModel) {
        QItemSelection selection = collectionSelectionModel->selection();
        selection = mapToEntityTreeModel(collectionSelectionModel->model(), selection);
        selection = mapFromEntityTreeModel(favoriteSelectionModel->model(), selection);

        favoriteSelectionModel->select(selection, QItemSelectionModel::ClearAndSelect);
    }

    updateActions();
}

void StandardActionManagerPrivate::favoriteSelectionChanged()
{
    if (insideSelectionSlot) {
        return;
    }
    QItemSelection selection = favoriteSelectionModel->selection();
    if (selection.isEmpty()) {
        return;
    }

    selection = mapToEntityTreeModel(favoriteSelectionModel->model(), selection);
    selection = mapFromEntityTreeModel(collectionSelectionModel->model(), selection);

    InsideSelectionSlotBlocker block(this);
    collectionSelectionModel->select(selection, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);

    // Also set the current index. This will trigger KMMainWidget::slotFolderChanged in kmail, which we want.
    if (!selection.indexes().isEmpty()) {
        collectionSelectionModel->setCurrentIndex(selection.indexes().first(), QItemSelectionModel::NoUpdate);
    }

    updateActions();
}

void StandardActionManagerPrivate::slotCreateCollection()
{
    Q_ASSERT(collectionSelectionModel);
    if (collectionSelectionModel->selection().indexes().isEmpty()) {
        return;
    }

    const QModelIndex index = collectionSelectionModel->selection().indexes().at(0);
    Q_ASSERT(index.isValid());
    const auto parentCollection = index.data(EntityTreeModel::CollectionRole).value<Collection>();
    Q_ASSERT(parentCollection.isValid());

    if (!canCreateCollection(parentCollection)) {
        return;
    }

    bool ok = false;
    QString name = QInputDialog::getText(parentWidget,
                                         contextText(StandardActionManager::CreateCollection, StandardActionManager::DialogTitle),
                                         contextText(StandardActionManager::CreateCollection, StandardActionManager::DialogText),
                                         {},
                                         {},
                                         &ok);
    name = name.trimmed();
    if (name.isEmpty() || !ok) {
        return;
    }

    if (name.contains(u'/')) {
        KMessageBox::error(parentWidget, i18n("We can not add \"/\" in folder name."), i18nc("@title:window", "Create New Folder Error"));
        return;
    }
    if (name.startsWith(QLatin1Char('.')) || name.endsWith(u'.')) {
        KMessageBox::error(parentWidget, i18n("We can not add \".\" at begin or end of folder name."), i18nc("@title:window", "Create New Folder Error"));
        return;
    }

    Collection collection;
    collection.setName(name);
    collection.setParentCollection(parentCollection);
    if (actions[StandardActionManager::CreateCollection]) {
        const QStringList mts = actions[StandardActionManager::CreateCollection]->property("ContentMimeTypes").toStringList();
        if (!mts.isEmpty()) {
            collection.setContentMimeTypes(mts);
        }
    }
    if (parentCollection.contentMimeTypes().contains(Collection::virtualMimeType())) {
        collection.setVirtual(true);
        collection.setContentMimeTypes(collection.contentMimeTypes() << Collection::virtualMimeType());
    }

    auto job = new CollectionCreateJob(collection);
    q->connect(job, &KJob::result, q, [this](KJob *job) {
        collectionCreationResult(job);
    });
}

void StandardActionManagerPrivate::slotCopyCollections()
{
    encodeToClipboard(collectionSelectionModel);
}

void StandardActionManagerPrivate::slotCutCollections()
{
    encodeToClipboard(collectionSelectionModel, true);
}

Collection::List StandardActionManagerPrivate::selectedCollections()
{
    Collection::List collections;

    Q_ASSERT(collectionSelectionModel);
    const QModelIndexList indexes = safeSelectedRows(collectionSelectionModel);
    collections.reserve(indexes.count());

    for (const QModelIndex &index : indexes) {
        Q_ASSERT(index.isValid());
        const auto collection = index.data(EntityTreeModel::CollectionRole).value<Collection>();
        Q_ASSERT(collection.isValid());

        collections << collection;
    }

    return collections;
}

void StandardActionManagerPrivate::slotDeleteCollection()
{
    const Collection::List collections = selectedCollections();
    if (collections.isEmpty()) {
        return;
    }

    const QString collectionName = collections.first().name();
    const QString text = contextText(StandardActionManager::DeleteCollections, StandardActionManager::MessageBoxText, collections.count(), collectionName);

    if (KMessageBox::questionTwoActions(
            parentWidget,
            text,
            contextText(StandardActionManager::DeleteCollections, StandardActionManager::MessageBoxTitle, collections.count(), collectionName),
            KStandardGuiItem::del(),
            KStandardGuiItem::cancel(),
            QString(),
            KMessageBox::Dangerous)
        != KMessageBox::ButtonCode::PrimaryAction) {
        return;
    }

    for (const Collection &collection : collections) {
        auto job = new CollectionDeleteJob(collection, q);
        q->connect(job, &CollectionDeleteJob::result, q, [this](KJob *job) {
            collectionDeletionResult(job);
        });
    }
}

void StandardActionManagerPrivate::slotMoveCollectionToTrash()
{
    const Collection::List collections = selectedCollections();
    if (collections.isEmpty()) {
        return;
    }

    for (const Collection &collection : collections) {
        auto job = new TrashJob(collection, q);
        q->connect(job, &TrashJob::result, q, [this](KJob *job) {
            moveCollectionToTrashResult(job);
        });
    }
}

void StandardActionManagerPrivate::slotRestoreCollectionFromTrash()
{
    const Collection::List collections = selectedCollections();
    if (collections.isEmpty()) {
        return;
    }

    for (const Collection &collection : collections) {
        auto job = new TrashRestoreJob(collection, q);
        q->connect(job, &TrashRestoreJob::result, q, [this](KJob *job) {
            moveCollectionToTrashResult(job);
        });
    }
}

Item::List StandardActionManagerPrivate::selectedItems() const
{
    Item::List items;

    Q_ASSERT(itemSelectionModel);
    const QModelIndexList indexes = safeSelectedRows(itemSelectionModel);
    items.reserve(indexes.count());

    for (const QModelIndex &index : indexes) {
        Q_ASSERT(index.isValid());
        const Item item = index.data(EntityTreeModel::ItemRole).value<Item>();
        Q_ASSERT(item.isValid());

        items << item;
    }

    return items;
}

void StandardActionManagerPrivate::slotMoveItemToTrash()
{
    const Item::List items = selectedItems();
    if (items.isEmpty()) {
        return;
    }

    auto job = new TrashJob(items, q);
    q->connect(job, &TrashJob::result, q, [this](KJob *job) {
        moveItemToTrashResult(job);
    });
}

void StandardActionManagerPrivate::slotRestoreItemFromTrash()
{
    const Item::List items = selectedItems();
    if (items.isEmpty()) {
        return;
    }

    auto job = new TrashRestoreJob(items, q);
    q->connect(job, &TrashRestoreJob::result, q, [this](KJob *job) {
        moveItemToTrashResult(job);
    });
}

void StandardActionManagerPrivate::slotTrashRestoreCollection()
{
    const Collection::List collections = selectedCollections();
    if (collections.isEmpty()) {
        return;
    }

    bool collectionsAreInTrash = false;
    for (const Collection &collection : collections) {
        if (collection.hasAttribute<EntityDeletedAttribute>()) {
            collectionsAreInTrash = true;
            break;
        }
    }

    if (collectionsAreInTrash) {
        slotRestoreCollectionFromTrash();
    } else {
        slotMoveCollectionToTrash();
    }
}

void StandardActionManagerPrivate::slotTrashRestoreItem()
{
    const Item::List items = selectedItems();
    if (items.isEmpty()) {
        return;
    }

    bool itemsAreInTrash = false;
    for (const Item &item : items) {
        if (item.hasAttribute<EntityDeletedAttribute>()) {
            itemsAreInTrash = true;
            break;
        }
    }

    if (itemsAreInTrash) {
        slotRestoreItemFromTrash();
    } else {
        slotMoveItemToTrash();
    }
}

void StandardActionManagerPrivate::slotSynchronizeCollection()
{
    Q_ASSERT(collectionSelectionModel);
    const QModelIndexList list = safeSelectedRows(collectionSelectionModel);
    if (list.isEmpty()) {
        return;
    }

    const Collection::List collections = selectedCollections();
    if (collections.isEmpty()) {
        return;
    }

    for (const Collection &collection : collections) {
        if (!testAndSetOnlineResources(collection)) {
            break;
        }
        AgentManager::self()->synchronizeCollection(collection, false);
    }
}

bool StandardActionManagerPrivate::testAndSetOnlineResources(const Akonadi::Collection &collection)
{
    // Shortcut for the Search resource, which is a virtual resource and thus
    // is always online (but AgentManager does not know about it, so it returns
    // an invalid AgentInstance, which is "offline").
    //
    // FIXME: AgentManager should return a valid AgentInstance even
    // for virtual resources, which would be always online.
    if (collection.resource() == QLatin1StringView("akonadi_search_resource")) {
        return true;
    }

    Akonadi::AgentInstance instance = Akonadi::AgentManager::self()->instance(collection.resource());
    if (!instance.isOnline()) {
        if (KMessageBox::questionTwoActions(
                parentWidget,
                i18n("Before syncing folder \"%1\" it is necessary to have the resource online. Do you want to make it online?", collection.displayName()),
                i18n("Account \"%1\" is offline", instance.name()),
                KGuiItem(i18nc("@action:button", "Go Online"), QIcon::fromTheme(QStringLiteral("user-online"))),
                KStandardGuiItem::cancel(),
                QStringLiteral("askGoOnline"))
            != KMessageBox::ButtonCode::PrimaryAction) {
            return false;
        }
        instance.setIsOnline(true);
    }
    return true;
}

void StandardActionManagerPrivate::slotSynchronizeCollectionRecursive()
{
    Q_ASSERT(collectionSelectionModel);
    const QModelIndexList list = safeSelectedRows(collectionSelectionModel);
    if (list.isEmpty()) {
        return;
    }

    const Collection::List collections = selectedCollections();
    if (collections.isEmpty()) {
        return;
    }

    for (const Collection &collection : collections) {
        if (!testAndSetOnlineResources(collection)) {
            break;
        }
        AgentManager::self()->synchronizeCollection(collection, true);
    }
}

void StandardActionManagerPrivate::slotCollectionProperties() const
{
    const QModelIndexList list = safeSelectedRows(collectionSelectionModel);
    if (list.isEmpty()) {
        return;
    }

    const QModelIndex index = list.first();
    Q_ASSERT(index.isValid());

    const auto collection = index.data(EntityTreeModel::CollectionRole).value<Collection>();
    Q_ASSERT(collection.isValid());

    auto dlg = new CollectionPropertiesDialog(collection, mCollectionPropertiesPageNames, parentWidget);
    dlg->setWindowTitle(contextText(StandardActionManager::CollectionProperties, StandardActionManager::DialogTitle, collection.displayName()));
    dlg->show();
}

void StandardActionManagerPrivate::slotCopyItems()
{
    encodeToClipboard(itemSelectionModel);
}

void StandardActionManagerPrivate::slotCutItems()
{
    encodeToClipboard(itemSelectionModel, true);
}

void StandardActionManagerPrivate::slotPaste()
{
    Q_ASSERT(collectionSelectionModel);

    const QModelIndexList list = safeSelectedRows(collectionSelectionModel);
    if (list.isEmpty()) {
        return;
    }

    const QModelIndex index = list.first();
    Q_ASSERT(index.isValid());

#ifndef QT_NO_CLIPBOARD
    // TODO: Copy or move? We can't seem to cut yet
    auto model = const_cast<QAbstractItemModel *>(collectionSelectionModel->model());
    const QMimeData *mimeData = QApplication::clipboard()->mimeData();
    model->dropMimeData(mimeData, isCutAction(mimeData) ? Qt::MoveAction : Qt::CopyAction, -1, -1, index);
    model->setData(QModelIndex(), false, EntityTreeModel::PendingCutRole);
    QApplication::clipboard()->clear();
    clipboardUrls.clear();
#endif
}

void StandardActionManagerPrivate::slotDeleteItems()
{
    Q_ASSERT(itemSelectionModel);

    Item::List items;
    const QModelIndexList indexes = safeSelectedRows(itemSelectionModel);
    items.reserve(indexes.count());
    for (const QModelIndex &index : indexes) {
        bool ok;
        const qlonglong id = index.data(EntityTreeModel::ItemIdRole).toLongLong(&ok);
        Q_ASSERT(ok);
        items << Item(id);
    }

    if (items.isEmpty()) {
        return;
    }

    QMetaObject::invokeMethod(
        q,
        [this, items] {
            slotDeleteItemsDeferred(items);
        },
        Qt::QueuedConnection);
}

void StandardActionManagerPrivate::slotDeleteItemsDeferred(const Akonadi::Item::List &items)
{
    Q_ASSERT(itemSelectionModel);

    if (KMessageBox::questionTwoActions(parentWidget,
                                        contextText(StandardActionManager::DeleteItems, StandardActionManager::MessageBoxText, items.count(), QString()),
                                        contextText(StandardActionManager::DeleteItems, StandardActionManager::MessageBoxTitle, items.count(), QString()),
                                        KStandardGuiItem::del(),
                                        KStandardGuiItem::cancel(),
                                        QString(),
                                        KMessageBox::Dangerous)
        != KMessageBox::ButtonCode::PrimaryAction) {
        return;
    }

    auto job = new ItemDeleteJob(items, q);
    q->connect(job, &ItemDeleteJob::result, q, [this](KJob *job) {
        itemDeletionResult(job);
    });
}

void StandardActionManagerPrivate::slotLocalSubscription() const
{
    auto dlg = new SubscriptionDialog(mMimeTypeFilter, parentWidget);
    dlg->showHiddenCollection(true);
    dlg->show();
}

void StandardActionManagerPrivate::slotAddToFavorites()
{
    Q_ASSERT(collectionSelectionModel);
    Q_ASSERT(favoritesModel);
    const QModelIndexList list = safeSelectedRows(collectionSelectionModel);
    if (list.isEmpty()) {
        return;
    }

    for (const QModelIndex &index : list) {
        Q_ASSERT(index.isValid());
        const auto collection = index.data(EntityTreeModel::CollectionRole).value<Collection>();
        Q_ASSERT(collection.isValid());

        favoritesModel->addCollection(collection);
    }

    updateActions();
}

void StandardActionManagerPrivate::slotRemoveFromFavorites()
{
    Q_ASSERT(favoriteSelectionModel);
    Q_ASSERT(favoritesModel);
    const QModelIndexList list = safeSelectedRows(favoriteSelectionModel);
    if (list.isEmpty()) {
        return;
    }

    for (const QModelIndex &index : list) {
        Q_ASSERT(index.isValid());
        const auto collection = index.data(EntityTreeModel::CollectionRole).value<Collection>();
        Q_ASSERT(collection.isValid());

        favoritesModel->removeCollection(collection);
    }

    updateActions();
}

void StandardActionManagerPrivate::slotRenameFavorite()
{
    Q_ASSERT(favoriteSelectionModel);
    Q_ASSERT(favoritesModel);
    const QModelIndexList list = safeSelectedRows(favoriteSelectionModel);
    if (list.isEmpty()) {
        return;
    }
    const QModelIndex index = list.first();
    Q_ASSERT(index.isValid());
    const auto collection = index.data(EntityTreeModel::CollectionRole).value<Collection>();
    Q_ASSERT(collection.isValid());

    QPointer<RenameFavoriteDialog> dlg(
        new RenameFavoriteDialog(favoritesModel->favoriteLabel(collection), favoritesModel->defaultFavoriteLabel(collection), parentWidget));
    if (dlg->exec() == QDialog::Accepted) {
        favoritesModel->setFavoriteLabel(collection, dlg->newName());
    }
    delete dlg;
}

void StandardActionManagerPrivate::slotSynchronizeFavoriteCollections()
{
    Q_ASSERT(favoritesModel);
    for (int row = 0; row < favoritesModel->rowCount(); ++row) {
        const auto idx = favoritesModel->index(row, 0);
        const auto collection = idx.data(EntityTreeModel::CollectionRole).value<Collection>();
        // there might be virtual collections in favorites which cannot be checked
        // so let's be safe here, agentmanager asserts otherwise
        if (!collection.resource().isEmpty()) {
            AgentManager::self()->synchronizeCollection(collection, false);
        }
    }
}

void StandardActionManagerPrivate::slotCopyCollectionTo()
{
    pasteTo(collectionSelectionModel, collectionSelectionModel->model(), StandardActionManager::CopyCollectionToMenu, Qt::CopyAction);
}

void StandardActionManagerPrivate::slotCopyItemTo()
{
    pasteTo(itemSelectionModel, collectionSelectionModel->model(), StandardActionManager::CopyItemToMenu, Qt::CopyAction);
}

void StandardActionManagerPrivate::slotMoveCollectionTo()
{
    pasteTo(collectionSelectionModel, collectionSelectionModel->model(), StandardActionManager::MoveCollectionToMenu, Qt::MoveAction);
}

void StandardActionManagerPrivate::slotMoveItemTo()
{
    pasteTo(itemSelectionModel, collectionSelectionModel->model(), StandardActionManager::MoveItemToMenu, Qt::MoveAction);
}

void StandardActionManagerPrivate::slotCopyCollectionTo(QAction *action)
{
    pasteTo(collectionSelectionModel, action, Qt::CopyAction);
}

void StandardActionManagerPrivate::slotCopyItemTo(QAction *action)
{
    pasteTo(itemSelectionModel, action, Qt::CopyAction);
}

void StandardActionManagerPrivate::slotMoveCollectionTo(QAction *action)
{
    pasteTo(collectionSelectionModel, action, Qt::MoveAction);
}

void StandardActionManagerPrivate::slotMoveItemTo(QAction *action)
{
    pasteTo(itemSelectionModel, action, Qt::MoveAction);
}

AgentInstance::List StandardActionManagerPrivate::selectedAgentInstances() const
{
    AgentInstance::List instances;

    Q_ASSERT(collectionSelectionModel);
    if (collectionSelectionModel->selection().indexes().isEmpty()) {
        return instances;
    }
    const QModelIndexList lstIndexes = collectionSelectionModel->selection().indexes();
    for (const QModelIndex &index : lstIndexes) {
        Q_ASSERT(index.isValid());
        const auto collection = index.data(EntityTreeModel::CollectionRole).value<Collection>();
        Q_ASSERT(collection.isValid());

        if (collection.isValid()) {
            const QString identifier = collection.resource();
            instances << AgentManager::self()->instance(identifier);
        }
    }

    return instances;
}

AgentInstance StandardActionManagerPrivate::selectedAgentInstance() const
{
    const AgentInstance::List instances = selectedAgentInstances();

    if (instances.isEmpty()) {
        return AgentInstance();
    }

    return instances.first();
}

void StandardActionManagerPrivate::slotCreateResource()
{
    QPointer<Akonadi::AgentTypeDialog> dlg(new Akonadi::AgentTypeDialog(parentWidget));
    dlg->setWindowTitle(contextText(StandardActionManager::CreateResource, StandardActionManager::DialogTitle));

    for (const QString &mimeType : std::as_const(mMimeTypeFilter)) {
        dlg->agentFilterProxyModel()->addMimeTypeFilter(mimeType);
    }

    for (const QString &capability : std::as_const(mCapabilityFilter)) {
        dlg->agentFilterProxyModel()->addCapabilityFilter(capability);
    }

    if (dlg->exec() == QDialog::Accepted) {
        const AgentType agentType = dlg->agentType();

        if (agentType.isValid()) {
            auto job = new AgentInstanceCreateJob(agentType, q);
            q->connect(job, &KJob::result, q, [this, job](KJob *) {
                if (job->error()) {
                    resourceCreationResult(job);
                    return;
                }

                auto configureDialog = new Akonadi::AgentConfigurationDialog(job->instance(), parentWidget);
                configureDialog->setAttribute(Qt::WA_DeleteOnClose);
                q->connect(configureDialog, &QDialog::rejected, q, [instance = job->instance()] {
                    Akonadi::AgentManager::self()->removeInstance(instance);
                });
                configureDialog->show();
            });
            job->start();
        }
    }
    delete dlg;
}

void StandardActionManagerPrivate::slotDeleteResource() const
{
    const AgentInstance::List instances = selectedAgentInstances();
    if (instances.isEmpty()) {
        return;
    }

    if (KMessageBox::questionTwoActions(
            parentWidget,
            contextText(StandardActionManager::DeleteResources, StandardActionManager::MessageBoxText, instances.count(), instances.first().name()),
            contextText(StandardActionManager::DeleteResources, StandardActionManager::MessageBoxTitle, instances.count(), instances.first().name()),
            KStandardGuiItem::del(),
            KStandardGuiItem::cancel(),
            QString(),
            KMessageBox::Dangerous)
        != KMessageBox::ButtonCode::PrimaryAction) {
        return;
    }

    for (const AgentInstance &instance : instances) {
        AgentManager::self()->removeInstance(instance);
    }
}

void StandardActionManagerPrivate::slotSynchronizeResource() const
{
    AgentInstance::List instances = selectedAgentInstances();
    for (AgentInstance &instance : instances) {
        instance.synchronize();
    }
}

void StandardActionManagerPrivate::slotSynchronizeCollectionTree() const
{
    AgentInstance::List instances = selectedAgentInstances();
    for (AgentInstance &instance : instances) {
        instance.synchronizeCollectionTree();
    }
}

void StandardActionManagerPrivate::slotResourceProperties() const
{
    AgentInstance instance = selectedAgentInstance();
    if (!instance.isValid()) {
        return;
    }

    auto configureDialog = new Akonadi::AgentConfigurationDialog(instance, parentWidget);
    configureDialog->setAttribute(Qt::WA_DeleteOnClose);
    configureDialog->show();
}

void StandardActionManagerPrivate::slotToggleWorkOffline(bool offline)
{
    setWorkOffline(offline);

    AgentInstance::List instances = AgentManager::self()->instances();
    for (AgentInstance &instance : instances) {
        instance.setIsOnline(!offline);
    }
}

void StandardActionManagerPrivate::pasteTo(QItemSelectionModel *selectionModel,
                                           const QAbstractItemModel *model,
                                           StandardActionManager::Type type,
                                           Qt::DropAction dropAction)
{
    const QSet<QString> mimeTypes = mimeTypesOfSelection(type);

    QPointer<CollectionDialog> dlg(new CollectionDialog(const_cast<QAbstractItemModel *>(model)));
    dlg->setMimeTypeFilter(mimeTypes.values());

    if (type == StandardActionManager::CopyItemToMenu || type == StandardActionManager::MoveItemToMenu) {
        dlg->setAccessRightsFilter(Collection::CanCreateItem);
    } else if (type == StandardActionManager::CopyCollectionToMenu || type == StandardActionManager::MoveCollectionToMenu) {
        dlg->setAccessRightsFilter(Collection::CanCreateCollection);
    }

    if (dlg->exec() == QDialog::Accepted && dlg != nullptr) {
        const QModelIndex index = EntityTreeModel::modelIndexForCollection(collectionSelectionModel->model(), dlg->selectedCollection());
        if (!index.isValid()) {
            delete dlg;
            return;
        }

        const QMimeData *mimeData = selectionModel->model()->mimeData(safeSelectedRows(selectionModel));

        auto model = const_cast<QAbstractItemModel *>(index.model());
        model->dropMimeData(mimeData, dropAction, -1, -1, index);
        delete mimeData;
    }
    delete dlg;
}

void StandardActionManagerPrivate::pasteTo(QItemSelectionModel *selectionModel, QAction *action, Qt::DropAction dropAction)
{
    Q_ASSERT(selectionModel);
    Q_ASSERT(action);

    if (safeSelectedRows(selectionModel).count() <= 0) {
        return;
    }

    const QMimeData *mimeData = selectionModel->model()->mimeData(selectionModel->selectedRows());

    const QModelIndex index = action->data().toModelIndex();
    Q_ASSERT(index.isValid());

    auto model = const_cast<QAbstractItemModel *>(index.model());
    const auto collection = index.data(EntityTreeModel::CollectionRole).value<Collection>();
    addRecentCollection(collection.id());
    model->dropMimeData(mimeData, dropAction, -1, -1, index);
    delete mimeData;
}

void StandardActionManagerPrivate::addRecentCollection(Akonadi::Collection::Id id) const
{
    QMapIterator<StandardActionManager::Type, QPointer<RecentCollectionAction>> item(mRecentCollectionsMenu);
    while (item.hasNext()) {
        item.next();
        if (item.value().data()) {
            item.value().data()->addRecentCollection(item.key(), id);
        }
    }
}

void StandardActionManagerPrivate::collectionCreationResult(KJob *job) const
{
    if (job->error()) {
        KMessageBox::error(parentWidget,
                           contextText(StandardActionManager::CreateCollection, StandardActionManager::ErrorMessageText, job->errorString()),
                           contextText(StandardActionManager::CreateCollection, StandardActionManager::ErrorMessageTitle));
    }
}

void StandardActionManagerPrivate::collectionDeletionResult(KJob *job) const
{
    if (job->error()) {
        KMessageBox::error(parentWidget,
                           contextText(StandardActionManager::DeleteCollections, StandardActionManager::ErrorMessageText, job->errorString()),
                           contextText(StandardActionManager::DeleteCollections, StandardActionManager::ErrorMessageTitle));
    }
}

void StandardActionManagerPrivate::moveCollectionToTrashResult(KJob *job) const
{
    if (job->error()) {
        KMessageBox::error(parentWidget,
                           contextText(StandardActionManager::MoveCollectionsToTrash, StandardActionManager::ErrorMessageText, job->errorString()),
                           contextText(StandardActionManager::MoveCollectionsToTrash, StandardActionManager::ErrorMessageTitle));
    }
}

void StandardActionManagerPrivate::moveItemToTrashResult(KJob *job) const
{
    if (job->error()) {
        KMessageBox::error(parentWidget,
                           contextText(StandardActionManager::MoveItemsToTrash, StandardActionManager::ErrorMessageText, job->errorString()),
                           contextText(StandardActionManager::MoveItemsToTrash, StandardActionManager::ErrorMessageTitle));
    }
}

void StandardActionManagerPrivate::itemDeletionResult(KJob *job) const
{
    if (job->error()) {
        KMessageBox::error(parentWidget,
                           contextText(StandardActionManager::DeleteItems, StandardActionManager::ErrorMessageText, job->errorString()),
                           contextText(StandardActionManager::DeleteItems, StandardActionManager::ErrorMessageTitle));
    }
}

void StandardActionManagerPrivate::resourceCreationResult(KJob *job) const
{
    if (job->error()) {
        KMessageBox::error(parentWidget,
                           contextText(StandardActionManager::CreateResource, StandardActionManager::ErrorMessageText, job->errorString()),
                           contextText(StandardActionManager::CreateResource, StandardActionManager::ErrorMessageTitle));
    }
}

void StandardActionManagerPrivate::pasteResult(KJob *job) const
{
    if (job->error()) {
        KMessageBox::error(parentWidget,
                           contextText(StandardActionManager::Paste, StandardActionManager::ErrorMessageText, job->errorString()),
                           contextText(StandardActionManager::Paste, StandardActionManager::ErrorMessageTitle));
    }
}

/**
 * Returns a set of mime types of the entities that are currently selected.
 */
QSet<QString> StandardActionManagerPrivate::mimeTypesOfSelection(StandardActionManager::Type type) const
{
    QModelIndexList list;
    QSet<QString> mimeTypes;

    const bool isItemAction = (type == StandardActionManager::CopyItemToMenu || type == StandardActionManager::MoveItemToMenu);
    const bool isCollectionAction = (type == StandardActionManager::CopyCollectionToMenu || type == StandardActionManager::MoveCollectionToMenu);

    if (isItemAction) {
        list = safeSelectedRows(itemSelectionModel);
        mimeTypes.reserve(list.count());
        for (const QModelIndex &index : std::as_const(list)) {
            mimeTypes << index.data(EntityTreeModel::MimeTypeRole).toString();
        }
    }

    if (isCollectionAction) {
        list = safeSelectedRows(collectionSelectionModel);
        for (const QModelIndex &index : std::as_const(list)) {
            const auto collection = index.data(EntityTreeModel::CollectionRole).value<Collection>();

            // The mimetypes that the selected collection can possibly contain
            const auto mimeTypesResult = AgentManager::self()->instance(collection.resource()).type().mimeTypes();
            mimeTypes = QSet<QString>(mimeTypesResult.begin(), mimeTypesResult.end());
        }
    }

    return mimeTypes;
}

/**
 * Returns whether items with the given @p mimeTypes can be written to the given @p collection.
 */
bool StandardActionManagerPrivate::isWritableTargetCollectionForMimeTypes(const Collection &collection,
                                                                          const QSet<QString> &mimeTypes,
                                                                          StandardActionManager::Type type) const
{
    if (collection.isVirtual()) {
        return false;
    }

    const bool isItemAction = (type == StandardActionManager::CopyItemToMenu || type == StandardActionManager::MoveItemToMenu);
    const bool isCollectionAction = (type == StandardActionManager::CopyCollectionToMenu || type == StandardActionManager::MoveCollectionToMenu);

    const auto contentMimeTypesList{collection.contentMimeTypes()};
    const QSet<QString> contentMimeTypesSet = QSet<QString>(contentMimeTypesList.cbegin(), contentMimeTypesList.cend());

    const bool canContainRequiredMimeTypes = contentMimeTypesSet.intersects(mimeTypes);
    const bool canCreateNewItems = (collection.rights() & Collection::CanCreateItem);

    const bool canCreateNewCollections = (collection.rights() & Collection::CanCreateCollection);
    const bool canContainCollections =
        collection.contentMimeTypes().contains(Collection::mimeType()) || collection.contentMimeTypes().contains(Collection::virtualMimeType());

    const auto mimeTypesList{AgentManager::self()->instance(collection.resource()).type().mimeTypes()};
    const QSet<QString> mimeTypesListSet = QSet<QString>(mimeTypesList.cbegin(), mimeTypesList.cend());

    const bool resourceAllowsRequiredMimeTypes = mimeTypesListSet.contains(mimeTypes);
    const bool isReadOnlyForItems = (isItemAction && (!canCreateNewItems || !canContainRequiredMimeTypes));
    const bool isReadOnlyForCollections = (isCollectionAction && (!canCreateNewCollections || !canContainCollections || !resourceAllowsRequiredMimeTypes));

    return !(CollectionUtils::isStructural(collection) || isReadOnlyForItems || isReadOnlyForCollections);
}

void StandardActionManagerPrivate::fillFoldersMenu(const Akonadi::Collection::List &selectedCollectionsList,
                                                   const QSet<QString> &mimeTypes,
                                                   StandardActionManager::Type type,
                                                   QMenu *menu,
                                                   const QAbstractItemModel *model,
                                                   const QModelIndex &parentIndex)
{
    const int rowCount = model->rowCount(parentIndex);

    for (int row = 0; row < rowCount; ++row) {
        const QModelIndex index = model->index(row, 0, parentIndex);
        const auto collection = model->data(index, EntityTreeModel::CollectionRole).value<Collection>();

        if (collection.isVirtual()) {
            continue;
        }

        const bool readOnly = !isWritableTargetCollectionForMimeTypes(collection, mimeTypes, type);
        const bool collectionIsSelected = selectedCollectionsList.contains(collection);
        if (type == StandardActionManager::MoveCollectionToMenu && collectionIsSelected) {
            continue;
        }

        QString label = model->data(index).toString();
        label.replace(u'&', QStringLiteral("&&"));

        const auto icon = model->data(index, Qt::DecorationRole).value<QIcon>();

        if (model->rowCount(index) > 0) {
            // new level
            auto popup = new QMenu(menu);
            const bool moveAction = (type == StandardActionManager::MoveCollectionToMenu || type == StandardActionManager::MoveItemToMenu);
            popup->setObjectName(QLatin1StringView("subMenu"));
            popup->setTitle(label);
            popup->setIcon(icon);

            fillFoldersMenu(selectedCollectionsList, mimeTypes, type, popup, model, index);
            if (!(type == StandardActionManager::CopyCollectionToMenu && collectionIsSelected)) {
                if (!readOnly) {
                    popup->addSeparator();

                    QAction *action = popup->addAction(moveAction ? i18n("Move to This Folder") : i18n("Copy to This Folder"));
                    action->setData(QVariant::fromValue<QModelIndex>(index));
                }
            }

            if (!popup->isEmpty()) {
                menu->addMenu(popup);
            }

        } else {
            // insert an item
            QAction *action = menu->addAction(icon, label);
            action->setData(QVariant::fromValue<QModelIndex>(index));
            action->setEnabled(!readOnly && !collectionIsSelected);
        }
    }
}

void StandardActionManagerPrivate::checkModelsConsistency() const
{
    if (favoritesModel == nullptr || favoriteSelectionModel == nullptr) {
        // No need to check when the favorite collections feature is not used
        return;
    }

    // find the base ETM of the favourites view
    const QAbstractItemModel *favModel = favoritesModel;
    while (const auto proxy = qobject_cast<const QAbstractProxyModel *>(favModel)) {
        favModel = proxy->sourceModel();
    }

    // Check that the collection selection model maps to the same
    // EntityTreeModel than favoritesModel
    if (collectionSelectionModel != nullptr) {
        const QAbstractItemModel *model = collectionSelectionModel->model();
        while (const auto proxy = qobject_cast<const QAbstractProxyModel *>(model)) {
            model = proxy->sourceModel();
        }

        Q_ASSERT(model == favModel);
    }

    // Check that the favorite selection model maps to favoritesModel
    const QAbstractItemModel *model = favoriteSelectionModel->model();
    while (const auto proxy = qobject_cast<const QAbstractProxyModel *>(model)) {
        model = proxy->sourceModel();
    }
    Q_ASSERT(model == favModel);
}

void StandardActionManagerPrivate::markCutAction(QMimeData *mimeData, bool cut) const
{
    if (!cut) {
        return;
    }

    const QByteArray cutSelectionData = "1";
    mimeData->setData(QStringLiteral("application/x-kde.akonadi-cutselection"), cutSelectionData);
}

bool StandardActionManagerPrivate::isCutAction(const QMimeData *mimeData) const
{
    const QByteArray data = mimeData->data(QStringLiteral("application/x-kde.akonadi-cutselection"));
    if (data.isEmpty()) {
        return false;
    } else {
        return (data.at(0) == '1'); // true if 1
    }
}

void StandardActionManagerPrivate::setContextText(StandardActionManager::Type type, StandardActionManager::TextContext context, const KLocalizedString &data)
{
    contextTexts[type].insert(context, data);
}

QString StandardActionManagerPrivate::contextText(StandardActionManager::Type type, StandardActionManager::TextContext context) const
{
    return contextTexts.value(type).value(context).toString();
}

QString StandardActionManagerPrivate::contextText(StandardActionManager::Type type, StandardActionManager::TextContext context, const QString &value) const
{
    KLocalizedString text = contextTexts[type].value(context);
    return text.subs(value).toString();
}

QString
StandardActionManagerPrivate::contextText(StandardActionManager::Type type, StandardActionManager::TextContext context, int count, const QString &value) const
{
    KLocalizedString text = contextTexts[type].value(context);
    const QString str = text.subs(count).toString();
    const int argCount = str.count(QRegularExpression(QStringLiteral("%[0-9]")));
    if (argCount > 0) {
        return text.subs(count).subs(value).toString();
    } else {
        return text.subs(count).toString();
    }
}
/// @endcond

#ifndef BUILD_ACTION_STATE_MANAGER_TEST
StandardActionManager::StandardActionManager(KActionCollection *actionCollection, QWidget *parent)
    : QObject(parent)
    , d(new StandardActionManagerPrivate(this))
{
    d->parentWidget = parent;
    d->actionCollection = actionCollection;
    d->mActionStateManager.setReceiver(d.get());
#ifndef QT_NO_CLIPBOARD
    connect(QApplication::clipboard(), &QClipboard::dataChanged, this, [this]() {
        d->clipboardChanged();
    });
#endif
}

StandardActionManager::~StandardActionManager() = default;

void StandardActionManager::setCollectionSelectionModel(QItemSelectionModel *selectionModel)
{
    d->collectionSelectionModel = selectionModel;
    connect(selectionModel, &QItemSelectionModel::selectionChanged, this, [this]() {
        d->collectionSelectionChanged();
    });

    d->checkModelsConsistency();
}

void StandardActionManager::setItemSelectionModel(QItemSelectionModel *selectionModel)
{
    d->itemSelectionModel = selectionModel;
    connect(selectionModel, &QItemSelectionModel::selectionChanged, this, [this]() {
        d->delayedUpdateActions();
    });
}

void StandardActionManager::setFavoriteCollectionsModel(FavoriteCollectionsModel *favoritesModel)
{
    d->favoritesModel = favoritesModel;
    d->checkModelsConsistency();
}

void StandardActionManager::setFavoriteSelectionModel(QItemSelectionModel *selectionModel)
{
    d->favoriteSelectionModel = selectionModel;
    connect(selectionModel, &QItemSelectionModel::selectionChanged, this, [this]() {
        d->favoriteSelectionChanged();
    });
    d->checkModelsConsistency();
}

QAction *StandardActionManager::createAction(Type type)
{
    Q_ASSERT(type < LastType);
    if (d->actions[type]) {
        return d->actions[type];
    }
    QAction *action = nullptr;
    switch (standardActionData[type].actionType) {
    case NormalAction:
    case ActionWithAlternative:
        action = new QAction(d->parentWidget);
        break;
    case ActionAlternative:
        d->actions[type] = d->actions[type - 1];
        Q_ASSERT(d->actions[type]);
        if ((LastType > type + 1) && (standardActionData[type + 1].actionType == ActionAlternative)) {
            createAction(static_cast<Type>(type + 1)); // ensure that alternative actions are initialized when not created by createAllActions
        }
        return d->actions[type];
    case MenuAction:
        action = new KActionMenu(d->parentWidget);
        break;
    case ToggleAction:
        action = new KToggleAction(d->parentWidget);
        break;
    }

    if (d->pluralLabels.contains(type) && !d->pluralLabels.value(type).isEmpty()) {
        action->setText(d->pluralLabels.value(type).subs(1).toString());
    } else if (!standardActionData[type].label.isEmpty()) {
        action->setText(standardActionData[type].label.toString());
    }
    if (d->pluralIconLabels.contains(type) && !d->pluralIconLabels.value(type).isEmpty()) {
        action->setIconText(d->pluralIconLabels.value(type).subs(1).toString());
    } else if (!standardActionData[type].iconLabel.isEmpty()) {
        action->setIconText(standardActionData[type].iconLabel.toString());
    }

    if (standardActionData[type].icon) {
        action->setIcon(standardActionDataIcon(standardActionData[type]));
    }
    if (d->actionCollection) {
        d->actionCollection->setDefaultShortcut(action, QKeySequence(standardActionData[type].shortcut));
    } else {
        action->setShortcut(standardActionData[type].shortcut);
    }

    if (standardActionData[type].slot) {
        switch (standardActionData[type].actionType) {
        case NormalAction:
        case ActionWithAlternative:
            connect(action, SIGNAL(triggered()), standardActionData[type].slot); // clazy:exclude=old-style-connect
            break;
        case MenuAction: {
            auto actionMenu = qobject_cast<KActionMenu *>(action);
            connect(actionMenu->menu(), SIGNAL(triggered(QAction *)), standardActionData[type].slot); // clazy:exclude=old-style-connect
            break;
        }
        case ToggleAction: {
            connect(action, SIGNAL(triggered(bool)), standardActionData[type].slot); // clazy:exclude=old-style-connect
            break;
        }
        case ActionAlternative:
            Q_ASSERT(0);
        }
    }

    if (type == ToggleWorkOffline) {
        // initialize the action state with information from config file
        disconnect(action, SIGNAL(triggered(bool)), this, standardActionData[type].slot); // clazy:exclude=old-style-connect
        action->setChecked(workOffline());
        connect(action, SIGNAL(triggered(bool)), this, standardActionData[type].slot); // clazy:exclude=old-style-connect

        // TODO: find a way to check for updates to the config file
    }

    Q_ASSERT(standardActionData[type].name);
    Q_ASSERT(d->actionCollection);
    d->actionCollection->addAction(QString::fromLatin1(standardActionData[type].name), action);
    d->actions[type] = action;
    if ((standardActionData[type].actionType == ActionWithAlternative) && (standardActionData[type + 1].actionType == ActionAlternative)) {
        createAction(static_cast<Type>(type + 1)); // ensure that alternative actions are initialized when not created by createAllActions
    }
    d->updateActions();
    return action;
}

void StandardActionManager::createAllActions()
{
    for (uint i = 0; i < LastType; ++i) {
        createAction(static_cast<Type>(i));
    }
}

QAction *StandardActionManager::action(Type type) const
{
    Q_ASSERT(type < LastType);
    return d->actions[type];
}

void StandardActionManager::setActionText(Type type, const KLocalizedString &text)
{
    Q_ASSERT(type < LastType);
    d->pluralLabels.insert(type, text);
    d->updateActions();
}

void StandardActionManager::interceptAction(Type type, bool intercept)
{
    Q_ASSERT(type < LastType);

    const QAction *action = d->actions[type];

    if (!action) {
        return;
    }

    if (intercept) {
        disconnect(action, SIGNAL(triggered()), this, standardActionData[type].slot); // clazy:exclude=old-style-connect
    } else {
        connect(action, SIGNAL(triggered()), standardActionData[type].slot); // clazy:exclude=old-style-connect
    }
}

Akonadi::Collection::List StandardActionManager::selectedCollections() const
{
    Collection::List collections;

    if (!d->collectionSelectionModel) {
        return collections;
    }

    const QModelIndexList lst = safeSelectedRows(d->collectionSelectionModel);
    for (const QModelIndex &index : lst) {
        const auto collection = index.data(EntityTreeModel::CollectionRole).value<Collection>();
        if (collection.isValid()) {
            collections << collection;
        }
    }

    return collections;
}

Item::List StandardActionManager::selectedItems() const
{
    Item::List items;

    if (!d->itemSelectionModel) {
        return items;
    }
    const QModelIndexList lst = safeSelectedRows(d->itemSelectionModel);
    for (const QModelIndex &index : lst) {
        const Item item = index.data(EntityTreeModel::ItemRole).value<Item>();
        if (item.isValid()) {
            items << item;
        }
    }

    return items;
}

void StandardActionManager::setContextText(Type type, TextContext context, const KLocalizedString &text)
{
    d->setContextText(type, context, text);
}

void StandardActionManager::setMimeTypeFilter(const QStringList &mimeTypes)
{
    d->mMimeTypeFilter = mimeTypes;
}

void StandardActionManager::setCapabilityFilter(const QStringList &capabilities)
{
    d->mCapabilityFilter = capabilities;
}

void StandardActionManager::setCollectionPropertiesPageNames(const QStringList &names)
{
    d->mCollectionPropertiesPageNames = names;
}

void StandardActionManager::createActionFolderMenu(QMenu *menu, Type type)
{
    d->createActionFolderMenu(menu, type);
}

void StandardActionManager::addRecentCollection(Akonadi::Collection::Id id) const
{
    RecentCollectionAction::addRecentCollection(id);
}

#include "moc_standardactionmanager.cpp"
#endif
