/*
 * psidbusnotifier.cpp: Psi's interface to org.freedesktop.Notify
 * Copyright (C) 2012  Evgeny Khryukin
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * You can also redistribute and/or modify this program under the
 * terms of the Psi License, specified in the accompanied COPYING
 * file, as published by the Psi Project; either dated January 1st,
 * 2005, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

#include "psidbusnotifier.h"

#include "avatars.h"
#include "common.h"
#include "iconset.h"
#include "moc_psidbusnotifier.cpp"
#include "psiaccount.h"
#include "psicon.h"
#include "psievent.h"
#include "psioptions.h"
#include "textutil.h"
#include "userlist.h"

#include <QDBusArgument>
#include <QDBusConnection>
#include <QDBusInterface>
#include <QDBusMessage>
#include <QDBusMetaType>
#include <QDBusReply>
#include <QImage>
#include <QTimer>
#include <QtPlugin>

static const int     minLifeTime = 5000;
static const QString markupCaps  = "body-markup";

class iiibiiay {
public:
    iiibiiay(QImage *img)
    {
        if (img->format() != QImage::Format_ARGB32)
            *img = img->convertToFormat(QImage::Format_ARGB32);
        width         = img->width();
        height        = img->height();
        rowstride     = img->bytesPerLine();
        hasAlpha      = img->hasAlphaChannel();
        channels      = img->isGrayscale() ? 1 : hasAlpha ? 4 : 3;
        bitsPerSample = img->depth() / channels;
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
        image.append((char *)img->rgbSwapped().constBits(), int(img->sizeInBytes()));
#else
        image.append((char *)img->rgbSwapped().constBits(), img->byteCount());
#endif
    }
    iiibiiay() { }
    static const decltype(qDBusRegisterMetaType<iiibiiay>()) id;

    int        width;
    int        height;
    int        rowstride;
    bool       hasAlpha;
    int        bitsPerSample;
    int        channels;
    QByteArray image;
};
Q_DECLARE_METATYPE(iiibiiay)

const decltype(qDBusRegisterMetaType<iiibiiay>()) iiibiiay::id { qDBusRegisterMetaType<iiibiiay>() };

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

static QDBusMessage createMessage(const QString &method)
{
    return QDBusMessage::createMethodCall("org.freedesktop.Notifications", "/org/freedesktop/Notifications",
                                          "org.freedesktop.Notifications", method);
}

PsiDBusNotifier::PsiDBusNotifier(QObject *parent) :
    QObject(parent), id_(0), account_(nullptr), lifeTimer_(new QTimer(this))
{
    QDBusConnection::sessionBus().connect("org.freedesktop.Notifications", "/org/freedesktop/Notifications",
                                          "org.freedesktop.Notifications", "NotificationClosed", this,
                                          SLOT(popupClosed(uint, uint)));
    QDBusConnection::sessionBus().connect("org.freedesktop.Notifications", "/org/freedesktop/Notifications",
                                          "org.freedesktop.Notifications", "ActionInvoked", this,
                                          SLOT(actionInvoked(uint, QString)));
    lifeTimer_->setSingleShot(true);
    connect(lifeTimer_, SIGNAL(timeout()), SLOT(readyToDie()));
}

PsiDBusNotifier::~PsiDBusNotifier() { }

bool PsiDBusNotifier::isAvailable()
{
    static bool ret = checkServer();
    return ret;
}

bool PsiDBusNotifier::checkServer()
{
    QDBusInterface i("org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications",
                     QDBusConnection::sessionBus());
    if (!i.isValid())
        return false;

    // We'll need caps in the future
    QDBusMessage m   = createMessage("GetCapabilities");
    QDBusMessage ret = QDBusConnection::sessionBus().call(m);
    if (ret.type() != QDBusMessage::InvalidMessage && !ret.arguments().isEmpty()) {
        QVariant v = ret.arguments().constFirst();
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
        if (v.type() == QVariant::StringList)
#else
        if (v.typeId() == QMetaType::QStringList)
#endif
            caps_ = v.toStringList();
    }

    return true;
}

QStringList PsiDBusNotifier::capabilities() { return caps_; }

void PsiDBusNotifier::popup(PsiAccount *account, PopupManager::PopupType type, const Jid &jid, const Resource &r,
                            const UserListItem *uli, const PsiEvent::Ptr &event)
{
    account_ = account;
    jid_     = jid;
    if (event) {
        event_ = event;
    }

    QString title, desc, contact, text, statusMsg;
    QString statusTxt = status2txt(makeSTATUS(r.status()));
    int     len
        = PsiOptions::instance()->getOption("options.ui.notifications.passive-popups.maximum-status-length").toInt();
    if (len != 0)
        statusMsg = r.status().status();
    if (len > 0)
        if (((int)statusMsg.length()) > len)
            statusMsg = statusMsg.left(len) + "...";
    bool     doAlert = false;
    PsiIcon *ico     = nullptr;

    if (uli && !uli->name().isEmpty()) {
        contact = uli->name();
    } else if (event && event->type() == PsiEvent::Auth) {
        contact = event.staticCast<AuthEvent>()->nick();
    } else if (event && event->type() == PsiEvent::Message) {
        contact = event.staticCast<MessageEvent>()->nick();
    }

    QString j = jid.full();
    int     jidLen
        = PsiOptions::instance()->getOption("options.ui.notifications.passive-popups.maximum-jid-length").toInt();
    if (jidLen > 0 && ((int)j.length()) > jidLen)
        j = j.left(jidLen) + "...";

    if (contact.isEmpty()) {
        contact = j;
    } else {
        contact = QString("%1(%2)").arg(contact, j);
    }

    title = this->title(type, &doAlert, &ico);

    QVariantMap hints;
    if (PsiOptions::instance()->getOption("options.ui.notifications.passive-popups.dbus.transient-hint").toBool()
        || type == PopupManager::AlertComposing || type == PopupManager::AlertOffline
        || type == PopupManager::AlertOnline || type == PopupManager::AlertStatusChange
        || type == PopupManager::AlertNone) {
        hints.insert("transient", QVariant(true));
    }
    //    if(doAlert) {
    //        hints.insert("urgency", QVariant(2));
    //    }
    QImage im;
    if (account) {
        if (uli && uli->isPrivate())
            im = account->avatarFactory()->getMucAvatar(jid).toImage();
        else
            im = account->avatarFactory()->getAvatar(jid).toImage();

        if (!im.isNull()) {
            int size = PsiOptions::instance()->getOption("options.ui.notifications.passive-popups.avatar-size").toInt();
            im       = im.scaledToWidth(size, Qt::SmoothTransformation);
        }
    }

    if (im.isNull() && ico) {
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
        const auto fontSize = qApp->fontMetrics().height();
#else
        const auto fontSize = QFontMetrics(qApp->font()).height();
#endif
        im = ico->pixmap(QSize(fontSize, fontSize)).toImage();
    }

    if (!im.isNull()) {
        iiibiiay i(&im);
        hints.insert("icon_data", QVariant(iiibiiay::id, &i));
    }

    bool showMessage
        = PsiOptions::instance()->getOption("options.ui.notifications.passive-popups.showMessage").toBool();

    switch (type) {
    case PopupManager::AlertOnline:
        text = QString("%1 (%2)").arg(contact, statusTxt);
        desc = statusMsg;
        break;
    case PopupManager::AlertOffline:
        text = QString("%1 (%2)").arg(contact, statusTxt);
        desc = statusMsg;
        break;
    case PopupManager::AlertStatusChange:
        text = QString("%1 (%2)").arg(contact, statusTxt);
        desc = statusMsg;
        break;
    case PopupManager::AlertComposing:
        text = QString("%1%2").arg(contact, QObject::tr(" is typing..."));
        desc = "";
        break;
    case PopupManager::AlertMessage:
    case PopupManager::AlertChat:
    case PopupManager::AlertGcHighlight: {
        text = QObject::tr("%1 says:").arg(contact);
        if (showMessage) {
            const Message *jmessage = &event.staticCast<MessageEvent>()->message();
            desc                    = jmessage->body();
        }
        break;
    }
    case PopupManager::AlertHeadline: {
        text = QObject::tr("Headline from %1").arg(contact);
        if (showMessage) {
            const Message *jmessage = &event.staticCast<MessageEvent>()->message();
            if (!jmessage->subject().isEmpty())
                title = jmessage->subject();
            desc = jmessage->body();
        }
        break;
    }
    case PopupManager::AlertFile:
        text = QObject::tr("Incoming file from %1").arg(contact);
        break;
    case PopupManager::AlertAvCall:
        text = QObject::tr("Incoming call from %1").arg(contact);
        break;
    default:
        break;
    }

    if (!desc.isEmpty()) {
        desc = clipText(desc);
        text += "\n" + desc;
    }

    int  lifeTime   = duration();
    bool bodyMarkup = capabilities().contains(markupCaps);
    text            = TextUtil::rich2plain(text);
    text            = bodyMarkup ? TextUtil::escape(text) : text;
    QDBusMessage m  = createMessage("Notify");
    QVariantList args;
    args << QString(ApplicationInfo::name());
    args << quint32(0);
    args << QVariant("");
    args << QString(title);
    args << QString(text);
    args << (QStringList() << "open" << tr("Open"));
    args << hints;
    args << lifeTime;
    m.setArguments(args);
    QDBusPendingCall         call    = QDBusConnection::sessionBus().asyncCall(m);
    QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this);

    connect(watcher, &QDBusPendingCallWatcher::finished, this, &PsiDBusNotifier::asyncCallFinished);

    lifeTime = (lifeTime < 0) ? lifeTime : qMax(minLifeTime, lifeTime);
    if (lifeTime >= 0)
        lifeTimer_->start(lifeTime);
}

void PsiDBusNotifier::popup(PsiAccount *account, PopupManager::PopupType /*type*/, const Jid &j,
                            const PsiIcon *titleIcon, const QString &titleText, const QPixmap &avatar,
                            const PsiIcon * /*icon*/, const QString &text)
{
    account_ = account;
    jid_     = j;

    QVariantMap hints;
    if (PsiOptions::instance()->getOption("options.ui.notifications.passive-popups.dbus.transient-hint").toBool()) {
        hints.insert("transient", QVariant(true));
    }
    if (!avatar.isNull() || titleIcon) {
        int    size = PsiOptions::instance()->getOption("options.ui.notifications.passive-popups.avatar-size").toInt();
        QImage im   = avatar.isNull() ? titleIcon->pixmap(QSize(size, size)).toImage()
                                      : avatar.toImage().scaledToWidth(size, Qt::SmoothTransformation);
        iiibiiay i(&im);
        hints.insert("icon_data", QVariant(iiibiiay::id, &i));
    }

    int     lifeTime   = duration();
    bool    bodyMarkup = capabilities().contains(markupCaps);
    QString plainText  = TextUtil::rich2plain(text);
    plainText          = bodyMarkup ? TextUtil::escape(plainText) : plainText;
    QDBusMessage m     = createMessage("Notify");
    QVariantList args;
    args << QString(ApplicationInfo::name());
    args << quint32(0);
    args << QVariant("");
    args << QString(titleText);
    args << QString(plainText);
    args << QStringList();
    args << hints;
    args << lifeTime;
    m.setArguments(args);
    QDBusPendingCall         call    = QDBusConnection::sessionBus().asyncCall(m);
    QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this);

    connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher *)), this,
            SLOT(asyncCallFinished(QDBusPendingCallWatcher *)));

    lifeTime = (lifeTime < 0) ? lifeTime : qMax(minLifeTime, lifeTime);
    if (lifeTime >= 0)
        lifeTimer_->start(lifeTime);
}

void PsiDBusNotifier::asyncCallFinished(QDBusPendingCallWatcher *watcher)
{
    QDBusMessage m = watcher->reply();
    if (m.type() == QDBusMessage::InvalidMessage || m.arguments().isEmpty()) {
        readyToDie();
        return;
    }

    QVariant repl = m.arguments().constFirst();
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
    if (repl.type() != QVariant::UInt || repl.toUInt() == 0) {
#else
    if (repl.typeId() != QMetaType::UInt || repl.toUInt() == 0) {
#endif
        readyToDie();
    } else {
        id_ = repl.toUInt();
    }
}

void PsiDBusNotifier::popupClosed(uint id, uint reason)
{
    Q_UNUSED(reason)
    if (id_ != 0 && id_ == id) {
        readyToDie();
    }
}

void PsiDBusNotifier::actionInvoked(uint id, const QString &actionKey)
{
    if (id_ != 0 && id_ == id && account_) {
        Q_UNUSED(actionKey);
        if (event_) {
            account_->psi()->processEvent(event_, UserAction);
        } else if (jid_.isValid()) {
            account_->actionDefault(Jid(jid_.bare()));
        }
    }
}

void PsiDBusNotifier::readyToDie()
{
    if (lifeTimer_->isActive()) {
        lifeTimer_->stop();
    }
    deleteLater();
}

QStringList PsiDBusNotifier::caps_ = QStringList();
