/*
    SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@privat.broulik.de>

    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/

#include "notifications.h"

#include <QConcatenateTablesProxyModel>
#include <QDebug>
#include <QFile>
#include <QMetaEnum>
#include <QPointer>

#include <canberra.h>

#include <KConfig>
#include <KConfigGroup>
#include <KConfigWatcher>
#include <KDescendantsProxyModel>
#include <KLocalizedString>
#include <KNotification>

#include "limitedrowcountproxymodel_p.h"
#include "notificationfilterproxymodel_p.h"
#include "notificationgroupcollapsingproxymodel_p.h"
#include "notificationgroupingproxymodel_p.h"
#include "notificationsmodel.h"
#include "notificationsortproxymodel_p.h"
#include "portal_p.h"
#include "portalnotificationsmodel.h"

#include "jobsmodel.h"

#include "settings.h"

#include "notification.h"

#include "utils_p.h"

#include "debug.h"

using namespace Qt::StringLiterals;
using namespace NotificationManager;

#if QT_VERSION < QT_VERSION_CHECK(6, 9, 1)
// This class is a workaround to https://bugreports.qt.io/browse/QTBUG-134210
// and https://bugs.kde.org/show_bug.cgi?id=500749
// if a model is added to an empty QConcatenateTablesProxyModel
// it will report zero columns and non-zero rows, causing a data inconsistence
// which causes an infinite recursion in NotificationFilterProxyModel::filterAcceptsRow
// We fix the number of columns to always be 1
// remove when the upstream bug is fixed
// However, this breaks sorting the model :-(
class SingleColumnConcatenateTables : public QConcatenateTablesProxyModel
{
public:
    using QConcatenateTablesProxyModel::QConcatenateTablesProxyModel;

    int columnCount(const QModelIndex &parent = QModelIndex()) const override
    {
        Q_UNUSED(parent)
        return 1;
    }
};
#endif

class Q_DECL_HIDDEN Notifications::Private
{
public:
    explicit Private(Notifications *q);
    ~Private();

    void initSourceModels();
    void initProxyModels();

    void updateCount();

    bool showNotifications = true;
    bool showJobs = false;

    Notifications::GroupMode groupMode = Notifications::GroupDisabled;
    int groupLimit = 0;
    bool expandUnread = false;

    int activeNotificationsCount = 0;
    int expiredNotificationsCount = 0;

    int unreadNotificationsCount = 0;
    int dismissedResidentNotificationsCount = 0;

    int activeJobsCount = 0;
    int jobsPercentage = 0;

    static bool isGroup(const QModelIndex &idx);
    static uint notificationId(const QModelIndex &idx);
    QModelIndex mapFromModel(const QModelIndex &idx) const;

    // NOTE when you add or re-arrange models make sure to update mapFromModel()!
    NotificationsModel::Ptr notificationsModel;
    PortalNotificationsModel::Ptr portalNotificationsModel;
    JobsModel::Ptr jobsModel;
    std::shared_ptr<Settings> settings() const;

    QConcatenateTablesProxyModel *notificationsAndJobsModel = nullptr;

    NotificationFilterProxyModel *filterModel = nullptr;
    NotificationSortProxyModel *sortModel = nullptr;
    NotificationGroupingProxyModel *groupingModel = nullptr;
    NotificationGroupCollapsingProxyModel *groupCollapsingModel = nullptr;
    KDescendantsProxyModel *flattenModel = nullptr;
    ca_context *canberraContext = nullptr;
    LimitedRowCountProxyModel *limiterModel = nullptr;

    QPointer<KNotification> inhibitionSummaryNotification;

private:
    Notifications *const q;
};

Notifications::Private::Private(Notifications *q)
    : q(q)
{
}

Notifications::Private::~Private() = default;

void Notifications::Private::initSourceModels()
{
    Q_ASSERT(notificationsAndJobsModel); // initProxyModels must be called before initSourceModels

    if (showNotifications) {
        if (!notificationsModel) {
            notificationsModel = NotificationsModel::createNotificationsModel();
            notificationsAndJobsModel->addSourceModel(notificationsModel.get());
            connect(notificationsModel.get(), &NotificationsModel::lastReadChanged, q, [this] {
                updateCount();
                Q_EMIT q->lastReadChanged();
            });
        }
        if (!portalNotificationsModel) {
            portalNotificationsModel = PortalNotificationsModel::create();
            notificationsAndJobsModel->addSourceModel(portalNotificationsModel.get());
        }
    } else {
        if (notificationsModel) {
            notificationsAndJobsModel->removeSourceModel(notificationsModel.get());
            disconnect(notificationsModel.get(), nullptr, q, nullptr); // disconnect all
            notificationsModel = nullptr;
        }
        if (portalNotificationsModel) {
            notificationsAndJobsModel->removeSourceModel(portalNotificationsModel.get());
            disconnect(portalNotificationsModel.get(), nullptr, q, nullptr);
            portalNotificationsModel = nullptr;
        }
    }

    if (showJobs && !jobsModel) {
        jobsModel = JobsModel::createJobsModel();
        notificationsAndJobsModel->addSourceModel(jobsModel.get());
        jobsModel->init();
    } else if (!showJobs && jobsModel) {
        notificationsAndJobsModel->removeSourceModel(jobsModel.get());
        jobsModel = nullptr;
    }
}

void Notifications::Private::initProxyModels()
{
    /* The data flow is as follows:
     * NOTE when you add or re-arrange models make sure to update mapFromModel()!
     *
     * NotificationsModel  PortalNotificationsModel  JobsModel
     *        \\                 //                     /
     *         \\               //                     /
     *     QConcatenateTablesProxyModel
     *               |||
     *               |||
     *     NotificationFilterProxyModel
     *     (filters by urgency, whitelist, etc)
     *                |
     *                |
     *      NotificationSortProxyModel
     *      (sorts by urgency, date, etc)
     *                |
     * --- BEGIN: Only when grouping is enabled ---
     *                |
     *    NotificationGroupingProxyModel
     *    (turns list into tree grouped by app)
     *               //\\
     *               //\\
     *   NotificationGroupCollapsingProxyModel
     *   (limits number of tree leaves for expand/collapse feature)
     *                /\
     *                /\
     *       KDescendantsProxyModel
     *       (flattens tree back into a list for consumption in ListView)
     *                |
     * --- END: Only when grouping is enabled ---
     *                |
     *    LimitedRowCountProxyModel
     *    (limits the total number of items in the model)
     *                |
     *                |
     *               \o/ <- Happy user seeing their notifications
     */

    if (!notificationsAndJobsModel) {
#if QT_VERSION < QT_VERSION_CHECK(6, 9, 1)
        notificationsAndJobsModel = new SingleColumnConcatenateTables(q);
#else
        notificationsAndJobsModel = new QConcatenateTablesProxyModel(q);
#endif
    }

    if (!filterModel) {
        filterModel = new NotificationFilterProxyModel();
        connect(filterModel, &NotificationFilterProxyModel::urgenciesChanged, q, &Notifications::urgenciesChanged);
        connect(filterModel, &NotificationFilterProxyModel::showExpiredChanged, q, &Notifications::showExpiredChanged);
        connect(filterModel, &NotificationFilterProxyModel::showDismissedChanged, q, &Notifications::showDismissedChanged);
        connect(filterModel, &NotificationFilterProxyModel::showAddedDuringInhibitionChanged, q, &Notifications::showAddedDuringInhibitionChanged);
        connect(filterModel, &NotificationFilterProxyModel::ignoreBlacklistDuringInhibitionChanged, q, &Notifications::ignoreBlacklistDuringInhibitionChanged);
        connect(filterModel, &NotificationFilterProxyModel::blacklistedDesktopEntriesChanged, q, &Notifications::blacklistedDesktopEntriesChanged);
        connect(filterModel, &NotificationFilterProxyModel::blacklistedNotifyRcNamesChanged, q, &Notifications::blacklistedNotifyRcNamesChanged);

        filterModel->setSourceModel(notificationsAndJobsModel);

        connect(filterModel, &QAbstractItemModel::rowsInserted, q, [this] {
            updateCount();
        });
        connect(filterModel, &QAbstractItemModel::rowsRemoved, q, [this] {
            updateCount();
        });
        connect(filterModel, &QAbstractItemModel::dataChanged, q, [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles) {
            Q_UNUSED(topLeft);
            Q_UNUSED(bottomRight);
            if (roles.isEmpty() || roles.contains(Notifications::UpdatedRole) || roles.contains(Notifications::ExpiredRole)
                || roles.contains(Notifications::JobStateRole) || roles.contains(Notifications::PercentageRole) || roles.contains(Notifications::ReadRole)
                || roles.contains(Notifications::ResidentRole) || roles.contains(Notifications::DismissedRole)) {
                updateCount();
            }
        });
    }

    if (!sortModel) {
        sortModel = new NotificationSortProxyModel(q);
        connect(sortModel, &NotificationSortProxyModel::sortModeChanged, q, &Notifications::sortModeChanged);
        connect(sortModel, &NotificationSortProxyModel::sortOrderChanged, q, &Notifications::sortOrderChanged);
    }

    if (!limiterModel) {
        limiterModel = new LimitedRowCountProxyModel(q);
        connect(limiterModel, &LimitedRowCountProxyModel::limitChanged, q, &Notifications::limitChanged);
    }

    if (groupMode == GroupApplicationsFlat) {
        if (!groupingModel) {
            groupingModel = new NotificationGroupingProxyModel(q);
            groupingModel->setSourceModel(filterModel);
        }

        if (!groupCollapsingModel) {
            groupCollapsingModel = new NotificationGroupCollapsingProxyModel(q);
            groupCollapsingModel->setLimit(groupLimit);
            groupCollapsingModel->setExpandUnread(expandUnread);
            groupCollapsingModel->setLastRead(q->lastRead());
            groupCollapsingModel->setSourceModel(groupingModel);
        }

        sortModel->setSourceModel(groupCollapsingModel);

        flattenModel = new KDescendantsProxyModel(q);
        flattenModel->setSourceModel(sortModel);

        limiterModel->setSourceModel(flattenModel);
    } else {
        sortModel->setSourceModel(filterModel);
        limiterModel->setSourceModel(sortModel);
        delete std::exchange(flattenModel, nullptr);
        delete std::exchange(groupingModel, nullptr);
    }

    q->setSourceModel(limiterModel);
}

void Notifications::Private::updateCount()
{
    int active = 0;
    int expired = 0;
    int unread = 0;
    int dismissedResident = 0;

    int jobs = 0;
    int totalPercentage = 0;

    // We want to get the numbers after main filtering (urgencies, whitelists, etc)
    // but before any limiting or group limiting, hence asking the filterModel for advice
    // at which point notifications and jobs also have already been merged
    for (int i = 0; i < filterModel->rowCount(); ++i) {
        const QModelIndex idx = filterModel->index(i, 0);

        const bool isExpired = idx.data(Notifications::ExpiredRole).toBool();
        if (isExpired || idx.data(Notifications::WasAddedDuringInhibitionRole).toBool()) {
            ++expired;
        } else {
            ++active;
        }

        const bool read = idx.data(Notifications::ReadRole).toBool();
        if (!active && !read) {
            QDateTime date = idx.data(Notifications::UpdatedRole).toDateTime();
            if (!date.isValid()) {
                date = idx.data(Notifications::CreatedRole).toDateTime();
            }

            if ((Portal::isPortalNotificationId(Private::notificationId(idx)) && date > portalNotificationsModel->lastRead())
                || (notificationsModel && date > notificationsModel->lastRead())) {
                ++unread;
            }
        }

        if (!isExpired && idx.data(Notifications::ResidentRole).toBool() && idx.data(Notifications::DismissedRole).toBool()) {
            ++dismissedResident;
        }

        if (idx.data(Notifications::TypeRole).toInt() == Notifications::JobType) {
            if (idx.data(Notifications::JobStateRole).toInt() != Notifications::JobStateStopped) {
                ++jobs;

                totalPercentage += idx.data(Notifications::PercentageRole).toInt();
            }
        }
    }

    if (activeNotificationsCount != active) {
        activeNotificationsCount = active;
        Q_EMIT q->activeNotificationsCountChanged();
    }
    if (expiredNotificationsCount != expired) {
        expiredNotificationsCount = expired;
        Q_EMIT q->expiredNotificationsCountChanged();
    }
    if (unreadNotificationsCount != unread) {
        unreadNotificationsCount = unread;
        Q_EMIT q->unreadNotificationsCountChanged();
    }
    if (dismissedResidentNotificationsCount != dismissedResident) {
        dismissedResidentNotificationsCount = dismissedResident;
        Q_EMIT q->dismissedResidentNotificationsCountChanged();
    }
    if (activeJobsCount != jobs) {
        activeJobsCount = jobs;
        Q_EMIT q->activeJobsCountChanged();
    }

    const int percentage = (jobs > 0 ? totalPercentage / jobs : 0);
    if (jobsPercentage != percentage) {
        jobsPercentage = percentage;
        Q_EMIT q->jobsPercentageChanged();
    }

    // TODO don't Q_EMIT in dataChanged
    Q_EMIT q->countChanged();
}

bool Notifications::Private::isGroup(const QModelIndex &idx)
{
    return idx.data(Notifications::IsGroupRole).toBool();
}

uint Notifications::Private::notificationId(const QModelIndex &idx)
{
    return idx.data(Notifications::IdRole).toUInt();
}

QModelIndex Notifications::Private::mapFromModel(const QModelIndex &idx) const
{
    QModelIndex resolvedIdx = idx;

    QAbstractItemModel *models[] = {
        notificationsAndJobsModel,
        filterModel,
        sortModel,
        groupingModel,
        groupCollapsingModel,
        flattenModel,
        limiterModel,
    };

    // TODO can we do this with a generic loop like mapFromModel
    while (resolvedIdx.isValid() && resolvedIdx.model() != q) {
        const auto *idxModel = resolvedIdx.model();

        // HACK try to find the model that uses the index' model as source
        bool found = false;
        for (QAbstractItemModel *model : models) {
            if (!model) {
                continue;
            }

            if (auto *proxyModel = qobject_cast<QAbstractProxyModel *>(model)) {
                if (proxyModel->sourceModel() == idxModel) {
                    resolvedIdx = proxyModel->mapFromSource(resolvedIdx);
                    found = true;
                    break;
                }
            } else if (auto *concatenateModel = qobject_cast<QConcatenateTablesProxyModel *>(model)) {
                if (idxModel == notificationsModel.get() || idxModel == portalNotificationsModel.get() || idxModel == jobsModel.get()) {
                    resolvedIdx = concatenateModel->mapFromSource(resolvedIdx);
                    found = true;
                    break;
                }
            }
        }

        if (!found) {
            break;
        }
    }
    return resolvedIdx;
}

std::shared_ptr<Settings> Notifications::Private::settings() const
{
    static std::weak_ptr<Settings> s_instance;
    if (s_instance.expired()) {
        std::shared_ptr<Settings> ptr(new Settings());
        s_instance = ptr;
        return ptr;
    }
    return s_instance.lock();
}

Notifications::Notifications(QObject *parent)
    : QSortFilterProxyModel(parent)
    , d(new Private(this))
{
    // The proxy models are always the same, just with different
    // properties set whereas we want to avoid loading a source model
    // e.g. notifications or jobs when we're not actually using them
    d->initProxyModels();

    // init source models when used from C++
    QMetaObject::invokeMethod(
        this,
        [this] {
            d->initSourceModels();
        },
        Qt::QueuedConnection);
}

Notifications::~Notifications() = default;

void Notifications::classBegin()
{
}

void Notifications::componentComplete()
{
    // init source models when used from QML
    d->initSourceModels();
}

int Notifications::limit() const
{
    return d->limiterModel->limit();
}

void Notifications::setLimit(int limit)
{
    d->limiterModel->setLimit(limit);
}

int Notifications::groupLimit() const
{
    return d->groupLimit;
}

void Notifications::setGroupLimit(int limit)
{
    if (d->groupLimit == limit) {
        return;
    }

    d->groupLimit = limit;
    if (d->groupCollapsingModel) {
        d->groupCollapsingModel->setLimit(limit);
    }
    Q_EMIT groupLimitChanged();
}

bool Notifications::expandUnread() const
{
    return d->expandUnread;
}

void Notifications::setExpandUnread(bool expand)
{
    if (d->expandUnread == expand) {
        return;
    }

    d->expandUnread = expand;
    if (d->groupCollapsingModel) {
        d->groupCollapsingModel->setExpandUnread(expand);
    }
    Q_EMIT expandUnreadChanged();
}

QWindow *Notifications::window() const
{
    if (d->notificationsModel) {
        return d->notificationsModel->window();
    } else if (d->portalNotificationsModel) {
        return d->portalNotificationsModel->window();
    } else {
        return nullptr;
    }
}

void Notifications::setWindow(QWindow *window)
{
    if (!d->notificationsModel || !d->portalNotificationsModel) {
        qCWarning(NOTIFICATIONMANAGER) << "Setting window before initialising the model" << this << window;
    }

    if (d->notificationsModel) {
        d->notificationsModel->setWindow(window);
    }
    if (d->portalNotificationsModel) {
        d->portalNotificationsModel->setWindow(window);
    }
}

bool Notifications::showExpired() const
{
    return d->filterModel->showExpired();
}

void Notifications::setShowExpired(bool show)
{
    d->filterModel->setShowExpired(show);
}

bool Notifications::showDismissed() const
{
    return d->filterModel->showDismissed();
}

void Notifications::setShowDismissed(bool show)
{
    d->filterModel->setShowDismissed(show);
}

bool Notifications::showAddedDuringInhibition() const
{
    return d->filterModel->showAddedDuringInhibition();
}

void Notifications::setShowAddedDuringInhibition(bool show)
{
    d->filterModel->setShowAddedDuringInhibition(show);
}

bool Notifications::ignoreBlacklistDuringInhibition() const
{
    return d->filterModel->ignoreBlacklistDuringInhibition();
}

void Notifications::setIgnoreBlacklistDuringInhibition(bool show)
{
    d->filterModel->setIgnoreBlacklistDuringInhibition(show);
}

QStringList Notifications::blacklistedDesktopEntries() const
{
    return d->filterModel->blacklistedDesktopEntries();
}

void Notifications::setBlacklistedDesktopEntries(const QStringList &blacklist)
{
    d->filterModel->setBlackListedDesktopEntries(blacklist);
}

QStringList Notifications::blacklistedNotifyRcNames() const
{
    return d->filterModel->blacklistedNotifyRcNames();
}

void Notifications::setBlacklistedNotifyRcNames(const QStringList &blacklist)
{
    d->filterModel->setBlacklistedNotifyRcNames(blacklist);
}

QStringList Notifications::whitelistedDesktopEntries() const
{
    return d->filterModel->whitelistedDesktopEntries();
}

void Notifications::setWhitelistedDesktopEntries(const QStringList &whitelist)
{
    d->filterModel->setWhiteListedDesktopEntries(whitelist);
}

QStringList Notifications::whitelistedNotifyRcNames() const
{
    return d->filterModel->whitelistedNotifyRcNames();
}

void Notifications::setWhitelistedNotifyRcNames(const QStringList &whitelist)
{
    d->filterModel->setWhitelistedNotifyRcNames(whitelist);
}

bool Notifications::showNotifications() const
{
    return d->showNotifications;
}

void Notifications::setShowNotifications(bool show)
{
    if (d->showNotifications == show) {
        return;
    }

    d->showNotifications = show;
    d->initSourceModels();
    Q_EMIT showNotificationsChanged();
}

bool Notifications::showJobs() const
{
    return d->showJobs;
}

void Notifications::setShowJobs(bool show)
{
    if (d->showJobs == show) {
        return;
    }

    d->showJobs = show;
    d->initSourceModels();
    Q_EMIT showJobsChanged();
}

Notifications::Urgencies Notifications::urgencies() const
{
    return d->filterModel->urgencies();
}

void Notifications::setUrgencies(Urgencies urgencies)
{
    d->filterModel->setUrgencies(urgencies);
}

Notifications::SortMode Notifications::sortMode() const
{
    return d->sortModel->sortMode();
}

void Notifications::setSortMode(SortMode sortMode)
{
    d->sortModel->setSortMode(sortMode);
}

Qt::SortOrder Notifications::sortOrder() const
{
    return d->sortModel->sortOrder();
}

void Notifications::setSortOrder(Qt::SortOrder sortOrder)
{
    d->sortModel->setSortOrder(sortOrder);
}

Notifications::GroupMode Notifications::groupMode() const
{
    return d->groupMode;
}

void Notifications::setGroupMode(GroupMode groupMode)
{
    if (d->groupMode != groupMode) {
        d->groupMode = groupMode;
        d->initProxyModels();
        Q_EMIT groupModeChanged();
    }
}

int Notifications::count() const
{
    return rowCount(QModelIndex());
}

int Notifications::activeNotificationsCount() const
{
    return d->activeNotificationsCount;
}

int Notifications::expiredNotificationsCount() const
{
    return d->expiredNotificationsCount;
}

QDateTime Notifications::lastRead() const
{
    if (d->notificationsModel) {
        return d->notificationsModel->lastRead();
    } else if (d->portalNotificationsModel) {
        return d->portalNotificationsModel->lastRead();
    }
    return {};
}

void Notifications::setLastRead(const QDateTime &lastRead)
{
    // TODO jobs could also be unread?
    if (d->notificationsModel) {
        d->notificationsModel->setLastRead(lastRead);
    }
    if (d->portalNotificationsModel) {
        d->portalNotificationsModel->setLastRead(lastRead);
    }
    if (d->groupCollapsingModel) {
        d->groupCollapsingModel->setLastRead(lastRead);
    }
}

void Notifications::resetLastRead()
{
    setLastRead(QDateTime::currentDateTimeUtc());
}

int Notifications::unreadNotificationsCount() const
{
    return d->unreadNotificationsCount;
}

int Notifications::dismissedResidentNotificationsCount() const
{
    return d->dismissedResidentNotificationsCount;
}

int Notifications::activeJobsCount() const
{
    return d->activeJobsCount;
}

int Notifications::jobsPercentage() const
{
    return d->jobsPercentage;
}

QPersistentModelIndex Notifications::makePersistentModelIndex(const QModelIndex &idx) const
{
    return QPersistentModelIndex(idx);
}

void Notifications::expire(const QModelIndex &idx)
{
    const QModelIndex sourceIndex = Utils::mapToModel(idx, nullptr);
    auto *sourceModel = const_cast<QAbstractItemModel *>(sourceIndex.model());

    if (auto *notificationModel = qobject_cast<AbstractNotificationsModel *>(sourceModel)) {
        const auto notificationId = Private::notificationId(idx);
        notificationModel->expire(notificationId);
    } else if (auto *jobsModel = qobject_cast<JobsModel *>(sourceModel)) {
        jobsModel->expire(sourceIndex);
    }
}

void Notifications::close(const QModelIndex &idx)
{
    if (idx.data(Notifications::IsGroupRole).toBool()) {
        const QModelIndex groupIdx = Utils::mapToModel(idx, d->groupingModel);
        if (!groupIdx.isValid()) {
            qCWarning(NOTIFICATIONMANAGER) << "Failed to find group model index for this item";
            return;
        }

        Q_ASSERT(groupIdx.model() == d->groupingModel);

        const int childCount = d->groupingModel->rowCount(groupIdx);
        for (int i = childCount - 1; i >= 0; --i) {
            const QModelIndex childIdx = d->groupingModel->index(i, 0, groupIdx);
            close(childIdx);
        }
        return;
    }

    if (!idx.data(Notifications::ClosableRole).toBool()) {
        return;
    }

    const QModelIndex sourceIndex = Utils::mapToModel(idx, nullptr);
    auto *sourceModel = const_cast<QAbstractItemModel *>(sourceIndex.model());

    if (auto *notificationModel = qobject_cast<AbstractNotificationsModel *>(sourceModel)) {
        const auto notificationId = Private::notificationId(idx);
        notificationModel->close(notificationId);
    } else if (auto *jobsModel = qobject_cast<JobsModel *>(sourceModel)) {
        jobsModel->close(sourceIndex);
    }
}

void Notifications::configure(const QModelIndex &idx)
{
    // For groups just configure the application, not the individual event
    if (Private::isGroup(idx)) {
        const QString desktopEntry = idx.data(Notifications::DesktopEntryRole).toString();
        const QString notifyRcName = idx.data(Notifications::NotifyRcNameRole).toString();

        if (d->notificationsModel) {
            d->notificationsModel->configure(desktopEntry, notifyRcName, QString() /*eventId*/);
        } else if (d->portalNotificationsModel) {
            d->portalNotificationsModel->configure(desktopEntry, notifyRcName, QString() /*eventId*/);
        }
        return;
    }

    const QModelIndex sourceIndex = Utils::mapToModel(idx, nullptr);
    auto *sourceModel = const_cast<QAbstractItemModel *>(sourceIndex.model());
    const auto notificationId = Private::notificationId(idx);

    if (auto *notificationsModel = qobject_cast<NotificationsModel *>(sourceModel)) {
        notificationsModel->configure(notificationId);
    } else if (auto *portalNotificationsModel = qobject_cast<PortalNotificationsModel *>(sourceModel)) {
        portalNotificationsModel->configure(notificationId);
    }
}

void Notifications::invokeDefaultAction(const QModelIndex &idx, InvokeBehavior behavior)
{
    if (!checkIndex(idx, CheckIndexOption::IndexIsValid)) {
        return;
    }

    const QModelIndex sourceIndex = Utils::mapToModel(idx, nullptr);
    auto *sourceModel = const_cast<QAbstractItemModel *>(sourceIndex.model());

    if (auto *notificationModel = qobject_cast<AbstractNotificationsModel *>(sourceModel)) {
        const auto notificationId = Private::notificationId(idx);
        notificationModel->invokeDefaultAction(notificationId, behavior);
    }
}

void Notifications::invokeAction(const QModelIndex &idx, const QString &actionId, InvokeBehavior behavior)
{
    if (!checkIndex(idx, CheckIndexOption::IndexIsValid)) {
        return;
    }

    const QModelIndex sourceIndex = Utils::mapToModel(idx, nullptr);
    auto *sourceModel = const_cast<QAbstractItemModel *>(sourceIndex.model());

    if (auto *notificationModel = qobject_cast<AbstractNotificationsModel *>(sourceModel)) {
        const auto notificationId = Private::notificationId(idx);
        notificationModel->invokeAction(notificationId, actionId, behavior);
    }
}

void Notifications::reply(const QModelIndex &idx, const QString &text, InvokeBehavior behavior)
{
    if (!checkIndex(idx, CheckIndexOption::IndexIsValid)) {
        return;
    }

    const QModelIndex sourceIndex = Utils::mapToModel(idx, nullptr);
    auto *sourceModel = const_cast<QAbstractItemModel *>(sourceIndex.model());

    if (auto *notificationModel = qobject_cast<AbstractNotificationsModel *>(sourceModel)) {
        const auto notificationId = Private::notificationId(idx);
        notificationModel->reply(notificationId, text, behavior);
    }
}

void Notifications::startTimeout(const QModelIndex &idx)
{
    startTimeout(Private::notificationId(idx));
}

void Notifications::startTimeout(uint notificationId)
{
    if (Portal::isPortalNotificationId(notificationId) && d->portalNotificationsModel) {
        d->portalNotificationsModel->startTimeout(notificationId);
    } else if (d->notificationsModel) {
        d->notificationsModel->startTimeout(notificationId);
    }
}

void Notifications::stopTimeout(const QModelIndex &idx)
{
    const auto notificationId = Private::notificationId(idx);
    if (Portal::isPortalNotificationId(notificationId) && d->portalNotificationsModel) {
        d->portalNotificationsModel->stopTimeout(notificationId);
    } else if (d->notificationsModel) {
        d->notificationsModel->stopTimeout(notificationId);
    }
}

void Notifications::suspendJob(const QModelIndex &idx)
{
    if (d->jobsModel) {
        d->jobsModel->suspend(Utils::mapToModel(idx, d->jobsModel.get()));
    }
}

void Notifications::resumeJob(const QModelIndex &idx)
{
    if (d->jobsModel) {
        d->jobsModel->resume(Utils::mapToModel(idx, d->jobsModel.get()));
    }
}

void Notifications::killJob(const QModelIndex &idx)
{
    if (d->jobsModel) {
        d->jobsModel->kill(Utils::mapToModel(idx, d->jobsModel.get()));
    }
}

void Notifications::clear(ClearFlags flags)
{
    if (d->notificationsModel) {
        d->notificationsModel->clear(flags);
    }
    if (d->portalNotificationsModel) {
        d->portalNotificationsModel->clear(flags);
    }
    if (d->jobsModel) {
        d->jobsModel->clear(flags);
    }
}

QModelIndex Notifications::groupIndex(const QModelIndex &idx) const
{
    if (idx.data(Notifications::IsGroupRole).toBool()) {
        return idx;
    }

    if (idx.data(Notifications::IsInGroupRole).toBool()) {
        QModelIndex groupingIdx = Utils::mapToModel(idx, d->groupingModel);
        return d->mapFromModel(groupingIdx.parent());
    }

    qCWarning(NOTIFICATIONMANAGER) << "Cannot get group index for item that isn't a group or inside one";
    return {};
}

void Notifications::collapseAllGroups()
{
    if (d->groupCollapsingModel) {
        d->groupCollapsingModel->collapseAll();
    }
}

void Notifications::showInhibitionSummary(Urgency urgency, const QStringList &blacklistedDesktopEntries, const QStringList &blacklistedNotifyRcNames)
{
    int inhibited = 0;
    for (int i = 0, count = d->notificationsAndJobsModel->rowCount(); i < count; ++i) {
        const QModelIndex idx = d->notificationsAndJobsModel->index(i, 0);
        if (!idx.data(Notifications::WasAddedDuringInhibitionRole).toBool()) {
            continue;
        }
        if (idx.data(Notifications::ExpiredRole).toBool()) {
            continue;
        }
        if (idx.data(Notifications::UrgencyRole).toInt() < static_cast<int>(urgency) || idx.data(Notifications::ReadRole).toBool()) {
            continue;
        }
        // Set expired so the individual notifications don't pop up, only the summary does - but they still show up in the history.
        d->notificationsAndJobsModel->setData(idx, true, Notifications::ExpiredRole);
        ++inhibited;
    }

    if (!inhibited) {
        return;
    }

    // Don't just update it so it pops up again.
    hideInhibitionSummary();

    d->inhibitionSummaryNotification = new KNotification(u"inhibitionSummary"_s);
    d->inhibitionSummaryNotification->setTitle(i18ncp("@title", "Unread Notification", "Unread Notifications", inhibited));
    d->inhibitionSummaryNotification->setText(i18ncp("@info",
                                                     "%1 notification was received while Do Not Disturb was active.",
                                                     "%1 notifications were received while Do Not Disturb was active.",
                                                     inhibited));
    d->inhibitionSummaryNotification->setIconName(u"preferences-desktop-notification-bell"_s);
    d->inhibitionSummaryNotification->setFlags(KNotification::CloseOnTimeout);
    d->inhibitionSummaryNotification->setComponentName(u"libnotificationmanager"_s);
    d->inhibitionSummaryNotification->setHint(u"transient"_s, true);

    const QString showNotificationsText = i18nc("@action:button Show the notifications popup", "Show Notifications");

    const KNotificationAction *defaultShowNotificationsAction = d->inhibitionSummaryNotification->addDefaultAction(showNotificationsText);
    connect(defaultShowNotificationsAction, &KNotificationAction::activated, this, &Notifications::showNotificationsRequested);

    const KNotificationAction *showNotificationsAction = d->inhibitionSummaryNotification->addAction(showNotificationsText);
    connect(showNotificationsAction, &KNotificationAction::activated, this, &Notifications::showNotificationsRequested);

    d->inhibitionSummaryNotification->sendEvent();
}

void Notifications::hideInhibitionSummary()
{
    if (d->inhibitionSummaryNotification) {
        d->inhibitionSummaryNotification->close();
        d->inhibitionSummaryNotification = nullptr;
    }
}

QVariant Notifications::data(const QModelIndex &index, int role) const
{
    return QSortFilterProxyModel::data(index, role);
}

bool Notifications::setData(const QModelIndex &index, const QVariant &value, int role)
{
    return QSortFilterProxyModel::setData(index, value, role);
}

bool Notifications::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
    return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
}

bool Notifications::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
{
    return QSortFilterProxyModel::lessThan(source_left, source_right);
}

int Notifications::rowCount(const QModelIndex &parent) const
{
    return QSortFilterProxyModel::rowCount(parent);
}

QHash<int, QByteArray> Notifications::roleNames() const
{
    return Utils::roleNames();
}

void Notifications::playSoundHint(const QModelIndex &idx) const
{
    auto hints = data(idx, Notifications::HintsRole).toMap();
    auto soundFilePath = hints.value(QStringLiteral("sound-file")).toString();
    auto soundName = hints.value(QStringLiteral("sound-name")).toString();
    auto isSuppressSound = hints.value(QStringLiteral("suppress-sound")).toBool();
    auto desktopFile = data(idx, Notifications::DesktopEntryRole).toString();
    auto appName = data(idx, Notifications::ApplicationNameRole).toString();

    if (isSuppressSound || (soundName.isEmpty() && soundFilePath.isEmpty())) {
        return;
    }

    if (!d->canberraContext) {
        const int ret = ca_context_create(&d->canberraContext);
        if (ret != CA_SUCCESS) {
            qCWarning(NOTIFICATIONMANAGER)
                << "Failed to initialize canberra context for audio notification:"
                << ca_strerror(ret);
            d->canberraContext = nullptr;
            return;
        }
    }

    ca_proplist *props = nullptr;
    ca_proplist_create(&props);

    const auto config = KSharedConfig::openConfig(QStringLiteral("kdeglobals"));
    const KConfigGroup soundGroup = config->group(QStringLiteral("Sounds"));
    const auto soundTheme =
        soundGroup.readEntry("Theme", QStringLiteral("ocean"));

    if (!soundName.isEmpty()) {
        ca_proplist_sets(props, CA_PROP_EVENT_ID, soundName.toLatin1().constData());
    }
    if (!soundFilePath.isEmpty()) {
        ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME,
                       QFile::encodeName(soundFilePath).constData());
    }
    ca_proplist_sets(props, CA_PROP_APPLICATION_NAME,
                     appName.toLatin1().constData());
    ca_proplist_sets(props, CA_PROP_APPLICATION_ID,
                     desktopFile.toLatin1().constData());
    ca_proplist_sets(props, CA_PROP_CANBERRA_XDG_THEME_NAME,
                     soundTheme.toLatin1().constData());
    // We'll also want this cached for a time. volatile makes sure the cache is
    // dropped after some time or when the cache is under pressure.
    ca_proplist_sets(props, CA_PROP_CANBERRA_CACHE_CONTROL, "volatile");

    const int ret =
        ca_context_play_full(d->canberraContext, 0, props, nullptr, nullptr);

    ca_proplist_destroy(props);

    if (ret != CA_SUCCESS) {
        qCWarning(NOTIFICATIONMANAGER) << "Failed to play sound" << soundName
                                       << "with canberra:" << ca_strerror(ret);
        return;
    }
}

#include "moc_notifications.cpp"
