/*
 * SPDX-FileCopyrightText: 2018-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 "portal_p.h"

#include "debug.h"

#include "portalnotificationsadaptor.h"

#include "notification_p.h"

#include "server.h"
#include "utils_p.h"

#include <QDBusConnection>

#include <KStartupInfo>
#include <KWaylandExtras>
#include <KWindowSystem>

using namespace Qt::StringLiterals;

namespace NotificationManager
{

Portal::Portal(QObject *parent)
    : QObject(parent)
{
}

Portal::~Portal() = default;

Portal &Portal::self()
{
    static auto *s_self = new Portal(qApp);
    return *s_self;
}

uint Portal::portalIdMask()
{
    return 1u << 31;
}

bool Portal::isPortalNotificationId(uint notificationId)
{
    return notificationId & portalIdMask();
}

bool Portal::init()
{
    if (m_valid) {
        return true;
    }

    new PortalNotificationsAdaptor(this);

    QDBusConnection sessionBus = QDBusConnection::sessionBus();

    if (!m_dbusObjectValid) { // if already registered, don't fail here
        m_dbusObjectValid = sessionBus.registerObject(u"/org/freedesktop/portal/desktop"_s, this);
    }

    if (!m_dbusObjectValid) {
        qCWarning(NOTIFICATIONMANAGER) << "Failed to register Portal Notification DBus object";
        return false;
    }

    if (!sessionBus.interface()->registerService(u"org.freedesktop.impl.portal.desktop.plasmanotify"_s)) {
        qCWarning(NOTIFICATIONMANAGER) << "Failed to register Portal Notification service on DBus";
        return false;
    }

    m_valid = true;
    return true;
}

void Portal::removeNotification(uint notificationId)
{
    m_idMap.remove(notificationId);
}

void Portal::invokeAction(uint notificationId, const QString &actionId, const QVariant &target, const QString &response, QWindow *window)
{
    const auto idPair = m_idMap.value(notificationId);

    const auto [appId, portalNotificationId] = idPair;
    if (appId.isEmpty() || portalNotificationId.isEmpty()) {
        qCWarning(NOTIFICATIONMANAGER) << "Trying to invoke action" << actionId << "on notification that doesn't exist";
        return;
    }

    auto activateWithPlatformData = [this, appId, portalNotificationId, actionId, target, response](const QVariantMap &platformData) {
        if (constexpr QLatin1String appPrefix{"app."}; actionId.startsWith(appPrefix)) {
            const QString dbusActionId = actionId.mid(appPrefix.size());
            qCDebug(NOTIFICATIONMANAGER) << "Invoking exported action" << dbusActionId << "on" << appId;

            QString appPath = appId;
            appPath.replace('.'_L1, '/'_L1);
            appPath.replace('-'_L1, '_'_L1);
            appPath.prepend('/'_L1);

            QVariantList params;
            if (target.isValid()) {
                params.append(target);
            }
            if (!response.isEmpty()) {
                params.append(response);
            }

            auto message = QDBusMessage::createMethodCall(appId, appPath, u"org.freedesktop.Application"_s, u"ActivateAction"_s);
            message.setArguments({dbusActionId, params, platformData});
            QDBusConnection::sessionBus().call(message, QDBus::NoBlock);
        } else {
            qCDebug(NOTIFICATIONMANAGER) << "Emitting action invoked for" << actionId << "on" << appId;

            QVariantList params;
            if (target.isValid()) {
                params.append(target);
            }
            params.append(platformData);
            if (!response.isEmpty()) {
                params.append(response);
            }

            Q_EMIT ActionInvoked(appId, portalNotificationId, actionId, params);
        }
    };

    if (KWindowSystem::isPlatformWayland()) {
        KWaylandExtras::xdgActivationToken(window, appId).then(this, [activateWithPlatformData](const QString &token) {
            activateWithPlatformData({
                {u"activation-token"_s, token},
            });
        });
    } else {
        KStartupInfoId startupId;
        startupId.initId();
        activateWithPlatformData({
            {u"desktop-startup-id"_s, QString::fromUtf8(startupId.id())},
        });
    }
}

void Portal::AddNotification(const QString &appId, const QString &portalNotificationId, const QVariantMap &props)
{
    // Does the portal enforce this maybe?
    if (portalNotificationId.isEmpty()) {
        return;
    }

    const auto idPair = PortalNotificationId{appId, portalNotificationId};
    auto notificationId = m_idMap.key(idPair);
    const bool wasReplaced = notificationId != 0;
    if (!wasReplaced) {
        // Avoid wrapping around to 0 in case of overflow
        if (!m_highestNotificationId) {
            m_highestNotificationId = portalIdMask() + 1;
        }
        notificationId = m_highestNotificationId;
        ++m_highestNotificationId;
    }

    Notification notification(notificationId);

    notification.setSummary(props.value(QStringLiteral("title")).toString());

    // TODO is this correct? does the portal do any stripping?
    // version 2 has an explicit body-markup but version 1 does not.
    QString body = props.value(QStringLiteral("body")).toString().toHtmlEscaped();
    body.replace('\n'_L1, "<br>"_L1);
    notification.setBody(body);

    notification.setDesktopEntry(appId);
    notification.setWasAddedDuringInhibition(Server::self().inhibited());

    // Not setting dbusService since Portal notifications probably don't need
    // to be watched, they do DBus activation and what not.

    notification.d->processPortalProperties(props);

    const bool showAsNew = props.value(u"display-hints"_s).toStringList().contains(u"show-as-new"_s);

    if (wasReplaced) {
        notification.resetUpdated();
        if (showAsNew) {
            // TODO or should we do a replaced "show as new" so it just doesn't carry over read etc?
            Q_EMIT notificationRemoved(notificationId);
            Q_EMIT notificationAdded(notification);
        } else {
            Q_EMIT notificationReplaced(notificationId, notification);
        }
    } else {
        m_idMap.insert(notificationId, idPair);
        Q_EMIT notificationAdded(notification);
    }

    // TODO NotificationWatcher
}

void Portal::RemoveNotification(const QString &appId, const QString &portalNotificationId)
{
    const auto idPair = PortalNotificationId{appId, portalNotificationId};
    const auto notificationId = m_idMap.key(idPair);

    if (notificationId) {
        m_idMap.remove(notificationId);
        Q_EMIT notificationRemoved(notificationId);
    }
}

uint Portal::version() const
{
    return 1;
}

} // namespace NotificationManager

#include "moc_portal_p.cpp"
