// Copyright (c) 2011-2016 The Bitcoin Core developers
// Copyright (c) 2017-2021 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <qt/notificator.h>

#include <QApplication>
#include <QByteArray>
#include <QIcon>
#include <QImageWriter>
#include <QMessageBox>
#include <QMetaType>
#include <QStyle>
#include <QSystemTrayIcon>
#include <QTemporaryFile>
#include <QVariant>
#ifdef USE_DBUS
#include <QDBusMetaType>
#include <QtDBus>
#include <cstdint>
#include <cstring>
#endif
// Include ApplicationServices.h after QtDbus to avoid redefinition of check().
// This affects at least OSX 10.6. See /usr/include/AssertMacros.h for details.
// Note: This could also be worked around using:
// #define __ASSERT_MACROS_DEFINE_VERSIONS_WITHOUT_UNDERSCORES 0
#ifdef Q_OS_MAC
#include <ApplicationServices/ApplicationServices.h>
#include <qt/macnotificationhandler.h>
#endif

#ifdef USE_DBUS
// https://wiki.ubuntu.com/NotificationDevelopmentGuidelines recommends at least
// 128
const int FREEDESKTOP_NOTIFICATION_ICON_SIZE = 128;
#endif

Notificator::Notificator(const QString &_programName,
                         QSystemTrayIcon *_trayIcon, QWidget *_parent)
    : QObject(_parent), parent(_parent), programName(_programName), mode(None),
      trayIcon(_trayIcon)
#ifdef USE_DBUS
      ,
      interface(0)
#endif
{
    if (_trayIcon && _trayIcon->supportsMessages()) {
        mode = QSystemTray;
    }
#ifdef USE_DBUS
    interface = new QDBusInterface("org.freedesktop.Notifications",
                                   "/org/freedesktop/Notifications",
                                   "org.freedesktop.Notifications");
    if (interface->isValid()) {
        mode = Freedesktop;
    }
#endif
#ifdef Q_OS_MAC
    // check if users OS has support for NSUserNotification
    if (MacNotificationHandler::instance()
            ->hasUserNotificationCenterSupport()) {
        mode = UserNotificationCenter;
    }
#endif
}

Notificator::~Notificator() {
#ifdef USE_DBUS
    delete interface;
#endif
}

#ifdef USE_DBUS

// Loosely based on http://www.qtcentre.org/archive/index.php/t-25879.html
class FreedesktopImage {
public:
    FreedesktopImage() {}
    explicit FreedesktopImage(const QImage &img);

    // Image to variant that can be marshalled over DBus
    static QVariant toVariant(const QImage &img);

private:
    int width, height, stride;
    bool hasAlpha;
    int channels;
    int bitsPerSample;
    QByteArray image;

    friend QDBusArgument &operator<<(QDBusArgument &a,
                                     const FreedesktopImage &i);
    friend const QDBusArgument &operator>>(const QDBusArgument &a,
                                           FreedesktopImage &i);
};

Q_DECLARE_METATYPE(FreedesktopImage);

// Image configuration settings
const int CHANNELS = 4;
const int BYTES_PER_PIXEL = 4;
const int BITS_PER_SAMPLE = 8;

FreedesktopImage::FreedesktopImage(const QImage &img)
    : width(img.width()), height(img.height()),
      stride(img.width() * BYTES_PER_PIXEL), hasAlpha(true), channels(CHANNELS),
      bitsPerSample(BITS_PER_SAMPLE) {
    // Convert 00xAARRGGBB to RGBA bytewise (endian-independent) format
    QImage tmp = img.convertToFormat(QImage::Format_ARGB32);

    unsigned int num_pixels = width * height;
    image.resize(num_pixels * BYTES_PER_PIXEL);

    for (unsigned int ptr = 0; ptr < num_pixels; ++ptr) {
        uint32_t pixel;
        std::memcpy(&pixel, tmp.bits() + ptr * sizeof(uint32_t), sizeof(uint32_t));
        image[ptr * BYTES_PER_PIXEL + 0] = pixel >> 16; // R
        image[ptr * BYTES_PER_PIXEL + 1] = pixel >> 8;  // G
        image[ptr * BYTES_PER_PIXEL + 2] = pixel;       // B
        image[ptr * BYTES_PER_PIXEL + 3] = pixel >> 24; // A
    }
}

QDBusArgument &operator<<(QDBusArgument &a, const FreedesktopImage &i) {
    a.beginStructure();
    a << i.width << i.height << i.stride << i.hasAlpha << i.bitsPerSample
      << i.channels << i.image;
    a.endStructure();
    return a;
}

const QDBusArgument &operator>>(const QDBusArgument &a, FreedesktopImage &i) {
    a.beginStructure();
    a >> i.width >> i.height >> i.stride >> i.hasAlpha >> i.bitsPerSample >>
        i.channels >> i.image;
    a.endStructure();
    return a;
}

QVariant FreedesktopImage::toVariant(const QImage &img) {
    FreedesktopImage fimg(img);
    return QVariant(qDBusRegisterMetaType<FreedesktopImage>(), &fimg);
}

void Notificator::notifyDBus(Class cls, const QString &title,
                             const QString &text, const QIcon &icon,
                             int millisTimeout) {
    Q_UNUSED(cls);
    // Arguments for DBus call:
    QList<QVariant> args;

    // Program Name:
    args.append(programName);

    // Unique ID of this notification type:
    args.append(0U);

    // Application Icon, empty string
    args.append(QString());

    // Summary
    args.append(title);

    // Body
    args.append(text);

    // Actions (none, actions are deprecated)
    QStringList actions;
    args.append(actions);

    // Hints
    QVariantMap hints;

    // If no icon specified, set icon based on class
    QIcon tmpicon;
    if (icon.isNull()) {
        QStyle::StandardPixmap sicon = QStyle::SP_MessageBoxQuestion;
        switch (cls) {
            case Information:
                sicon = QStyle::SP_MessageBoxInformation;
                break;
            case Warning:
                sicon = QStyle::SP_MessageBoxWarning;
                break;
            case Critical:
                sicon = QStyle::SP_MessageBoxCritical;
                break;
            default:
                break;
        }
        tmpicon = QApplication::style()->standardIcon(sicon);
    } else {
        tmpicon = icon;
    }
    hints["icon_data"] = FreedesktopImage::toVariant(
        tmpicon.pixmap(FREEDESKTOP_NOTIFICATION_ICON_SIZE).toImage());
    args.append(hints);

    // Timeout (in msec)
    args.append(millisTimeout);

    // "Fire and forget"
    interface->callWithArgumentList(QDBus::NoBlock, "Notify", args);
}
#endif

void Notificator::notifySystray(Class cls, const QString &title,
                                const QString &text, const QIcon &icon,
                                int millisTimeout) {
    Q_UNUSED(icon);
    QSystemTrayIcon::MessageIcon sicon = QSystemTrayIcon::NoIcon;
    switch (cls) // Set icon based on class
    {
        case Information:
            sicon = QSystemTrayIcon::Information;
            break;
        case Warning:
            sicon = QSystemTrayIcon::Warning;
            break;
        case Critical:
            sicon = QSystemTrayIcon::Critical;
            break;
    }
    trayIcon->showMessage(title, text, sicon, millisTimeout);
}

// Based on Qt's tray icon implementation
#ifdef Q_OS_MAC
void Notificator::notifyMacUserNotificationCenter(Class cls,
                                                  const QString &title,
                                                  const QString &text,
                                                  const QIcon &icon) {
    // icon is not supported by the user notification center yet. OSX will use
    // the app icon.
    MacNotificationHandler::instance()->showNotification(title, text);
}

#endif

void Notificator::notify(Class cls, const QString &title, const QString &text,
                         const QIcon &icon, int millisTimeout) {
    switch (mode) {
#ifdef USE_DBUS
        case Freedesktop:
            notifyDBus(cls, title, text, icon, millisTimeout);
            break;
#endif
        case QSystemTray:
            notifySystray(cls, title, text, icon, millisTimeout);
            break;
#ifdef Q_OS_MAC
        case UserNotificationCenter:
            notifyMacUserNotificationCenter(cls, title, text, icon);
            break;
#endif
        default:
            if (cls == Critical) {
                // Fall back to old fashioned pop-up dialog if critical and no
                // other notification available
                QMessageBox::critical(parent, title, text, QMessageBox::Ok,
                                      QMessageBox::Ok);
            }
            break;
    }
}
