/*
    SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@privat.broulik.de>
    SPDX-FileCopyrightText: 2020 MBition GmbH
        Author: Kai Uwe Broulik <kai_uwe.broulik@mbition.io>

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

#include "microphoneindicator.h"

#include <QAction>
#include <QIcon>
#include <QMenu>
#include <QTimer>

#include <KLocalizedString>
#include <KStatusNotifierItem>

#include <PulseAudioQt/Client>
#include <PulseAudioQt/Context>
#include <PulseAudioQt/Models>
#include <PulseAudioQt/Server>
#include <PulseAudioQt/Source>

using namespace PulseAudioQt;
using namespace Qt::Literals::StringLiterals;

constexpr QLatin1String OSD_DBUS_SERVICE = "org.kde.plasmashell"_L1;
constexpr QLatin1String OSD_DBUS_PATH = "/org/kde/osdService"_L1;

MicrophoneIndicator::MicrophoneIndicator(QObject *parent)
    : QObject(parent)
    , m_sourceModel(new SourceModel(this))
    , m_sourceOutputModel(new SourceOutputModel(this))
    , m_osdDBusInterface(new OsdServiceInterface(OSD_DBUS_SERVICE, OSD_DBUS_PATH, QDBusConnection::sessionBus(), this))
    , m_updateTimer(new QTimer(this))
{
    connect(m_sourceModel, &QAbstractItemModel::rowsInserted, this, &MicrophoneIndicator::scheduleUpdate);
    connect(m_sourceModel, &QAbstractItemModel::rowsRemoved, this, &MicrophoneIndicator::scheduleUpdate);
    connect(m_sourceModel, &QAbstractItemModel::dataChanged, this, &MicrophoneIndicator::scheduleUpdate);

    connect(m_sourceOutputModel, &QAbstractItemModel::rowsInserted, this, &MicrophoneIndicator::scheduleUpdate);
    connect(m_sourceOutputModel, &QAbstractItemModel::rowsRemoved, this, &MicrophoneIndicator::scheduleUpdate);
    connect(m_sourceOutputModel, &QAbstractItemModel::dataChanged, this, &MicrophoneIndicator::scheduleUpdate);

    m_updateTimer->setInterval(0);
    m_updateTimer->setSingleShot(true);
    connect(m_updateTimer, &QTimer::timeout, this, &MicrophoneIndicator::update);

    scheduleUpdate();
}

MicrophoneIndicator::~MicrophoneIndicator() = default;

void MicrophoneIndicator::init()
{
    // does nothing, just prompts QML engine to create an instance of the singleton
}

void MicrophoneIndicator::scheduleUpdate()
{
    if (!m_updateTimer->isActive()) {
        m_updateTimer->start();
    }
}

void MicrophoneIndicator::update()
{
    const auto apps = recordingApplications();
    if (apps.isEmpty()) {
        m_showOsdOnUpdate = false;
        delete m_sni;
        m_sni = nullptr;
        return;
    }

    if (!m_sni) {
        m_sni = new KStatusNotifierItem(QStringLiteral("microphone"));
        m_sni->setCategory(KStatusNotifierItem::Hardware);
        // always Active since it is completely removed when microphone isn't in use
        m_sni->setStatus(KStatusNotifierItem::Active);

        // but also middle click to be consistent with volume icon
        connect(m_sni, &KStatusNotifierItem::secondaryActivateRequested, this, &MicrophoneIndicator::toggleMuted);
        connect(m_sni, &KStatusNotifierItem::activateRequested, this, &MicrophoneIndicator::toggleMuted);

        connect(m_sni, &KStatusNotifierItem::scrollRequested, this, [this](int delta, Qt::Orientation orientation) {
            if (orientation != Qt::Vertical) {
                return;
            }

            m_wheelDelta += delta;

            while (m_wheelDelta >= 120) {
                m_wheelDelta -= 120;
                adjustVolume(+1);
            }
            while (m_wheelDelta <= -120) {
                m_wheelDelta += 120;
                adjustVolume(-1);
            }
        });

        QMenu *menu = m_sni->contextMenu();

        m_muteAction = menu->addAction(QIcon::fromTheme(QStringLiteral("microphone-sensitivity-muted")), i18n("Mute"));
        m_muteAction->setCheckable(true);
        connect(m_muteAction.data(), &QAction::triggered, this, &MicrophoneIndicator::setMuted);

        // don't let it quit plasmashell
        m_sni->setStandardActionsEnabled(false);
    }

    const bool allMuted = muted();

    QString iconName;
    if (allMuted) {
        iconName = QStringLiteral("microphone-sensitivity-muted");
    } else {
        if (Source *defaultSource = m_preferredDevice.source()) {
            const int percent = volumePercent(defaultSource);
            iconName = QStringLiteral("microphone-sensitivity");
            // it deliberately never shows the "muted" icon unless *all* microphones are muted
            if (percent <= 25) {
                iconName.append(QStringLiteral("-low"));
            } else if (percent <= 75) {
                iconName.append(QStringLiteral("-medium"));
            } else {
                iconName.append(QStringLiteral("-high"));
            }
        } else {
            iconName = QStringLiteral("microphone-sensitivity-high");
        }
    }

    m_sni->setTitle(i18n("Microphone"));
    m_sni->setIconByName(iconName);
    m_sni->setToolTip(QIcon::fromTheme(iconName), allMuted ? i18n("Microphone Muted") : i18n("Microphone"), toolTipForApps(apps));

    if (m_muteAction) {
        m_muteAction->setChecked(allMuted);
    }

    if (m_showOsdOnUpdate) {
        showOsd();
        m_showOsdOnUpdate = false;
    }
}

bool MicrophoneIndicator::muted() const
{
    static const int s_mutedRole = m_sourceModel->role(QByteArrayLiteral("Muted"));
    Q_ASSERT(s_mutedRole > -1);

    for (int row = 0; row < m_sourceModel->rowCount(); ++row) {
        const QModelIndex idx = m_sourceModel->index(row);
        if (!idx.data(s_mutedRole).toBool()) {
            // this is deliberately checking if *all* microphones are muted rather than the preferred one
            return false;
        }
    }

    return true;
}

void MicrophoneIndicator::setMuted(bool muted)
{
    static const int s_mutedRole = m_sourceModel->role(QByteArrayLiteral("Muted"));
    Q_ASSERT(s_mutedRole > -1);

    m_showOsdOnUpdate = true;

    if (muted) {
        for (int row = 0; row < m_sourceModel->rowCount(); ++row) {
            const QModelIndex idx = m_sourceModel->index(row);
            if (!idx.data(s_mutedRole).toBool()) {
                m_sourceModel->setData(idx, true, s_mutedRole);
                m_mutedIndices.append(QPersistentModelIndex(idx));
                continue;
            }
        }
        return;
    }

    // If we didn't mute it, unmute all
    if (m_mutedIndices.isEmpty()) {
        for (int i = 0; i < m_sourceModel->rowCount(); ++i) {
            m_sourceModel->setData(m_sourceModel->index(i), false, s_mutedRole);
        }
        return;
    }

    // Otherwise unmute the devices we muted
    for (auto &idx : std::as_const(m_mutedIndices)) {
        if (!idx.isValid()) {
            continue;
        }
        m_sourceModel->setData(idx, false, s_mutedRole);
    }
    m_mutedIndices.clear();

    // no update() needed as the model signals a change
}

void MicrophoneIndicator::toggleMuted()
{
    setMuted(!muted());
}

void MicrophoneIndicator::adjustVolume(int direction)
{
    Source *source = m_preferredDevice.source();
    if (!source) {
        return;
    }

    const int step = qRound(5 * normalVolume() / 100.0);

    const auto newVolume = qBound(minimumVolume(), //
                                  source->volume() + direction * step, //
                                  normalVolume());

    source->setVolume(newVolume);
    source->setMuted(newVolume == minimumVolume());

    m_showOsdOnUpdate = true;
}

int MicrophoneIndicator::volumePercent(Source *source)
{
    return source->isMuted() ? 0 : qRound(source->volume() / static_cast<qreal>(normalVolume()) * 100);
}

void MicrophoneIndicator::showOsd()
{
    auto *preferredSource = m_preferredDevice.source();
    if (!preferredSource) {
        return;
    }

    m_osdDBusInterface->microphoneVolumeChanged(volumePercent(preferredSource));
}

QList<QModelIndex> MicrophoneIndicator::recordingApplications() const
{
    QList<QModelIndex> indices;

    // If there are no microphones present, there's nothing to record
    if (m_sourceModel->rowCount() == 0) {
        return indices;
    }

    static const int s_virtualStreamRole = m_sourceOutputModel->role(QByteArrayLiteral("VirtualStream"));
    Q_ASSERT(s_virtualStreamRole > -1);

    indices.reserve(m_sourceOutputModel->rowCount());

    for (int i = 0; i < m_sourceOutputModel->rowCount(); ++i) {
        const QModelIndex idx = m_sourceOutputModel->index(i);

        if (idx.data(s_virtualStreamRole).toBool()) {
            continue;
        }

        indices.append(idx);
    }

    return indices;
}

QString MicrophoneIndicator::toolTipForApps(const QList<QModelIndex> &apps) const
{
    Q_ASSERT(!apps.isEmpty());

    if (apps.count() > 1) {
        QStringList names;
        names.reserve(apps.count());
        for (const QModelIndex &idx : apps) {
            names.append(sourceOutputDisplayName(idx));
        }
        names.removeDuplicates();
        // Still more than one app?
        if (names.count() > 1) {
            return i18nc("List of apps is using mic", "%1 are using the microphone", names.join(i18nc("list separator", ", ")));
        }
    }

    const QModelIndex appIdx = apps.constFirst();

    // If there is more than one microphone, show which one is being used.
    // An app could record multiple microphones simultaneously, or the user having the same app running
    // multiple times recording the same microphone, but this isn't covered here for simplicity.
    if (apps.count() == 1 && m_sourceModel->rowCount() > 1) {
        static const int s_sourceModelDescriptionRole = m_sourceModel->role(QByteArrayLiteral("Description"));
        Q_ASSERT(s_sourceModelDescriptionRole > -1);
        static const int s_sourceModelIndexRole = m_sourceModel->role("Index");
        Q_ASSERT(s_sourceModelIndexRole > -1);

        static const int s_sourceOutputModelDeviceIndexRole = m_sourceOutputModel->role("DeviceIndex");
        Q_ASSERT(s_sourceOutputModelDeviceIndexRole > -1);

        const int sourceOutputDeviceIndex = appIdx.data(s_sourceOutputModelDeviceIndexRole).toInt();

        for (int i = 0; i < m_sourceModel->rowCount(); ++i) {
            const QModelIndex sourceDeviceIdx = m_sourceModel->index(i, 0);
            const int sourceDeviceIndex = sourceDeviceIdx.data(s_sourceModelIndexRole).toInt();

            if (sourceDeviceIndex == sourceOutputDeviceIndex) {
                return i18nc("App %1 is using mic with name %2",
                             "%1 is using the microphone (%2)",
                             sourceOutputDisplayName(appIdx),
                             sourceDeviceIdx.data(s_sourceModelDescriptionRole).toString());
            }
        }
    }

    return i18nc("App is using mic", "%1 is using the microphone", sourceOutputDisplayName(appIdx));
}

QString MicrophoneIndicator::sourceOutputDisplayName(const QModelIndex &idx) const
{
    Q_ASSERT(idx.model() == m_sourceOutputModel);

    static const int s_nameRole = m_sourceOutputModel->role(QByteArrayLiteral("Name"));
    Q_ASSERT(s_nameRole > -1);
    static const int s_clientRole = m_sourceOutputModel->role(QByteArrayLiteral("Client"));
    Q_ASSERT(s_clientRole > -1);

    auto *client = qobject_cast<Client *>(idx.data(s_clientRole).value<QObject *>());

    return client ? client->name() : idx.data(s_nameRole).toString();
}

#include "moc_microphoneindicator.cpp"
