/**
 * SPDX-FileCopyrightText: 2018 Jun Bo Bi <jambonmcyeah@gmail.com>
 *
 * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
 */

#include "mpriscontrolplugin-win.h"

#include "plugin_mpriscontrol_debug.h"
#include <core/device.h>

#include <KLocalizedString>
#include <KPluginFactory>

#include <chrono>
#include <random>

#include <ppltasks.h>
#include <windows.h>

K_PLUGIN_CLASS_WITH_JSON(MprisControlPlugin, "kdeconnect_mpriscontrol.json")

namespace
{
const QString DEFAULT_PLAYER =
    i18nc("@title Users select this to control the current media player when we can't detect a specific player name like VLC", "Current Player");
}

MprisControlPlugin::MprisControlPlugin(QObject *parent, const QVariantList &args)
    : KdeConnectPlugin(parent, args)
    , sessionManager(NULL)
{
    try {
        sessionManager = GlobalSystemMediaTransportControlsSessionManager::RequestAsync().get();
        if (sessionManager) {
            sessionManager.SessionsChanged([this](GlobalSystemMediaTransportControlsSessionManager, SessionsChangedEventArgs) {
                this->updatePlayerList();
            });
        } else {
            qCWarning(KDECONNECT_PLUGIN_MPRISCONTROL) << "Failed to obtain Session Manager.";
        }
    } catch (winrt::hresult_error e) {
        qCWarning(KDECONNECT_PLUGIN_MPRISCONTROL) << "Failed to register mediaSessionsChanged listener";
    }
    this->updatePlayerList();
}

std::optional<QString> MprisControlPlugin::getPlayerName(const GlobalSystemMediaTransportControlsSession &player)
{
    auto entry = std::find(this->playerList.constBegin(), this->playerList.constEnd(), player);

    if (entry == this->playerList.constEnd()) {
        qCWarning(KDECONNECT_PLUGIN_MPRISCONTROL) << "PlaybackInfoChanged received for no longer tracked session" << player.SourceAppUserModelId().c_str();
        return std::nullopt;
    }

    return entry.key();
}

TimeSpan MprisControlPlugin::getPlayerPosition(const GlobalSystemMediaTransportControlsSession &player)
{
    auto playbackInfo = player.GetPlaybackInfo();
    bool isPlaying = playbackInfo.PlaybackStatus() == GlobalSystemMediaTransportControlsSessionPlaybackStatus::Playing;
    auto position = player.GetTimelineProperties().Position();
    if (isPlaying)
        return position + winrt::clock::now() - player.GetTimelineProperties().LastUpdatedTime();
    else
        return position;
}

QString MprisControlPlugin::randomUrl()
{
    const QString VALID_CHARS = QStringLiteral("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ");
    std::random_device rd;
    std::default_random_engine generator(rd());
    std::uniform_int_distribution<int> distribution(0, VALID_CHARS.size() - 1);

    const int size = 10;
    QString fileUrl(size, QChar());
    for (int i = 0; i < size; i++) {
        fileUrl[i] = VALID_CHARS[distribution(generator)];
    }

    return QStringLiteral("file://") + fileUrl;
}

void MprisControlPlugin::sendMediaProperties(const std::variant<NetworkPacket, QString> &packetOrName, const GlobalSystemMediaTransportControlsSession &player)
{
    NetworkPacket np = packetOrName.index() == 0 ? std::get<0>(packetOrName) : NetworkPacket(PACKET_TYPE_MPRIS);
    if (packetOrName.index() == 1)
        np.set(QStringLiteral("player"), std::get<1>(packetOrName));

    QString title = i18n("N/A");
    QString artist = i18n("N/A");
    QString album = i18n("N/A");
    // Calling GlobalSystemMediaTransportControlsSession.TryGetMediaPropertiesAsync().get()
    // can trigger a RuntimeError: Element not found in some scenarios.
    // See: https://bugs.kde.org/show_bug.cgi?id=502269
    try {
        auto mediaProperties = player.TryGetMediaPropertiesAsync().get();
        title = QString::fromWCharArray(mediaProperties.Title().c_str());
        artist = QString::fromWCharArray(mediaProperties.Artist().c_str());
        album = QString::fromWCharArray(mediaProperties.AlbumTitle().c_str());
    } catch (winrt::hresult_error e) {
        qCDebug(KDECONNECT_PLUGIN_MPRISCONTROL) << "Failed to get media properties";
    }

    np.set(QStringLiteral("title"), title);
    np.set(QStringLiteral("artist"), artist);
    np.set(QStringLiteral("album"), album);
    np.set(QStringLiteral("albumArtUrl"), randomUrl());

    np.set(QStringLiteral("url"), QString());
    sendTimelineProperties(np, player, true); // "length"

    if (packetOrName.index() == 1)
        sendPacket(np);
}

void MprisControlPlugin::sendPlaybackInfo(const std::variant<NetworkPacket, QString> &packetOrName, const GlobalSystemMediaTransportControlsSession &player)
{
    NetworkPacket np = packetOrName.index() == 0 ? std::get<0>(packetOrName) : NetworkPacket(PACKET_TYPE_MPRIS);
    if (packetOrName.index() == 1)
        np.set(QStringLiteral("player"), std::get<1>(packetOrName));

    sendMediaProperties(np, player);

    auto playbackInfo = player.GetPlaybackInfo();
    auto playbackControls = playbackInfo.Controls();

    np.set(QStringLiteral("isPlaying"), playbackInfo.PlaybackStatus() == GlobalSystemMediaTransportControlsSessionPlaybackStatus::Playing);
    np.set(QStringLiteral("canPause"), playbackControls.IsPauseEnabled());
    np.set(QStringLiteral("canPlay"), playbackControls.IsPlayEnabled());
    np.set(QStringLiteral("canGoNext"), playbackControls.IsNextEnabled());
    np.set(QStringLiteral("canGoPrevious"), playbackControls.IsPreviousEnabled());
    np.set(QStringLiteral("canSeek"), playbackControls.IsPlaybackPositionEnabled());

    if (playbackInfo.IsShuffleActive()) {
        const bool shuffleStatus = playbackInfo.IsShuffleActive().Value();
        np.set(QStringLiteral("shuffle"), shuffleStatus);
    }

    if (playbackInfo.AutoRepeatMode()) {
        QString loopStatus;
        switch (playbackInfo.AutoRepeatMode().Value()) {
        case Windows::Media::MediaPlaybackAutoRepeatMode::List: {
            loopStatus = QStringLiteral("Playlist");
            break;
        }
        case Windows::Media::MediaPlaybackAutoRepeatMode::Track: {
            loopStatus = QStringLiteral("Track");
            break;
        }
        default: {
            loopStatus = QStringLiteral("None");
            break;
        }
        }
        np.set(QStringLiteral("loopStatus"), loopStatus);
    }

    sendTimelineProperties(np, player);

    if (packetOrName.index() == 1)
        sendPacket(np);
}

void MprisControlPlugin::sendTimelineProperties(const std::variant<NetworkPacket, QString> &packetOrName,
                                                const GlobalSystemMediaTransportControlsSession &player,
                                                bool lengthOnly)
{
    NetworkPacket np = packetOrName.index() == 0 ? std::get<0>(packetOrName) : NetworkPacket(PACKET_TYPE_MPRIS);
    if (packetOrName.index() == 1)
        np.set(QStringLiteral("player"), std::get<1>(packetOrName));

    auto timelineProperties = player.GetTimelineProperties();

    if (!lengthOnly) {
        const auto playbackInfo = player.GetPlaybackInfo();
        const auto playbackControls = playbackInfo.Controls();
        np.set(QStringLiteral("canSeek"), playbackControls.IsPlaybackPositionEnabled());
        np.set(QStringLiteral("pos"),
               std::chrono::duration_cast<std::chrono::milliseconds>(timelineProperties.Position() - timelineProperties.StartTime()).count());
    }
    np.set(QStringLiteral("length"),
           std::chrono::duration_cast<std::chrono::milliseconds>(timelineProperties.EndTime() - timelineProperties.StartTime()).count());

    if (packetOrName.index() == 1)
        sendPacket(np);
}

void MprisControlPlugin::updatePlayerList()
{
    playerList.clear();
    playbackInfoChangedHandlers.clear();
    mediaPropertiesChangedHandlers.clear();
    timelinePropertiesChangedHandlers.clear();

    try {
        auto sessions = GlobalSystemMediaTransportControlsSessionManager::RequestAsync().get().GetSessions();
        playbackInfoChangedHandlers.resize(sessions.Size());
        mediaPropertiesChangedHandlers.resize(sessions.Size());
        timelinePropertiesChangedHandlers.resize(sessions.Size());

        for (uint32_t i = 0; i < sessions.Size(); i++) {
            const auto player = sessions.GetAt(i);
            auto playerName = player.SourceAppUserModelId();

#if WIN_SDK_VERSION >= 19041
            // try to resolve the AUMID to a user-friendly name
            try {
                playerName = AppInfo::GetFromAppUserModelId(playerName).DisplayInfo().DisplayName();
            } catch (winrt::hresult_error e) {
                qCDebug(KDECONNECT_PLUGIN_MPRISCONTROL)
                    << QString::fromWCharArray(playerName.c_str()) << "doesn\'t have a valid AppUserModelID! Sending as-is..";
            }
#endif
            QString uniqueName = QString::fromWCharArray(playerName.c_str());
            for (int i = 2; playerList.contains(uniqueName); ++i) {
                uniqueName += QStringLiteral(" [") + QString::number(i) + QStringLiteral("]");
            }

            playerList.insert(uniqueName, player);

            player
                .PlaybackInfoChanged(auto_revoke,
                                     [this](GlobalSystemMediaTransportControlsSession player, PlaybackInfoChangedEventArgs args) {
                                         if (auto name = getPlayerName(player))
                                             this->sendPlaybackInfo(name.value(), player);
                                     })
                .swap(playbackInfoChangedHandlers[i]);
            concurrency::create_task([this, player] {
                std::chrono::milliseconds timespan(50);
                std::this_thread::sleep_for(timespan);

                if (auto name = getPlayerName(player))
                    this->sendPlaybackInfo(name.value(), player);
            });

            if (auto name = getPlayerName(player))
                sendPlaybackInfo(name.value(), player);

            player
                .MediaPropertiesChanged(auto_revoke,
                                        [this](GlobalSystemMediaTransportControlsSession player, MediaPropertiesChangedEventArgs args) {
                                            if (auto name = getPlayerName(player))
                                                this->sendMediaProperties(name.value(), player);
                                        })
                .swap(mediaPropertiesChangedHandlers[i]);
            concurrency::create_task([this, player] {
                std::chrono::milliseconds timespan(50);
                std::this_thread::sleep_for(timespan);

                if (auto name = getPlayerName(player))
                    this->sendMediaProperties(name.value(), player);
            });

            player
                .TimelinePropertiesChanged(auto_revoke,
                                           [this](GlobalSystemMediaTransportControlsSession player, TimelinePropertiesChangedEventArgs args) {
                                               if (auto name = getPlayerName(player))
                                                   this->sendTimelineProperties(name.value(), player);
                                           })
                .swap(timelinePropertiesChangedHandlers[i]);
            concurrency::create_task([this, player] {
                std::chrono::milliseconds timespan(50);
                std::this_thread::sleep_for(timespan);

                if (auto name = getPlayerName(player))
                    this->sendTimelineProperties(name.value(), player);
            });
        }
    } catch (winrt::hresult_error e) {
        qCDebug(KDECONNECT_PLUGIN_MPRISCONTROL) << "Failed to update player list";
    }

    sendPlayerList();
}

void MprisControlPlugin::sendPlayerList()
{
    NetworkPacket np(PACKET_TYPE_MPRIS);

    np.set(QStringLiteral("playerList"), playerList.keys() + QStringList(DEFAULT_PLAYER));
    np.set(QStringLiteral("supportAlbumArtPayload"), true);

    sendPacket(np);
}

void MprisControlPlugin::getThumbnail(const std::variant<NetworkPacket, QString> &packetOrName,
                                      const GlobalSystemMediaTransportControlsSession &player,
                                      const QString artUrl)
{
    QSharedPointer<QBuffer> qdata = QSharedPointer<QBuffer>(new QBuffer());

    try {
        auto mediaProperties = player.TryGetMediaPropertiesAsync().get();
        auto thumbnail = mediaProperties.Thumbnail();
        if (thumbnail) {
            auto stream = thumbnail.OpenReadAsync().get();
            if (stream && stream.CanRead()) {
                IBuffer data = Buffer(stream.Size());
                data = stream.ReadAsync(data, stream.Size(), InputStreamOptions::None).get();
                qdata->setData((char *)data.data(), data.Capacity());
            }
        }
    } catch (const winrt::hresult_error &ex) {
        qWarning(KDECONNECT_PLUGIN_MPRISCONTROL) << "Unable to get thumbnails: " << QString::fromWCharArray(ex.message().c_str());
        return;
    }

    // We now obtain the thumbnail, but we are in a background thread.
    // Calling QMetaObject::invokeMethod with Qt::QueuedConnection ensures
    // the subsequent operations happen in the original object's thread,
    // preventing crashes due to cross-thread operations in Qt.
    QMetaObject::invokeMethod(
        this,
        [=]() {
            sendAlbumArt(packetOrName, qdata, artUrl);
        },
        Qt::QueuedConnection);
}

bool MprisControlPlugin::sendAlbumArt(const std::variant<NetworkPacket, QString> &packetOrName, const QSharedPointer<QBuffer> qdata, const QString artUrl)
{
    qWarning(KDECONNECT_PLUGIN_MPRISCONTROL) << "Sending Album Art";
    NetworkPacket np = packetOrName.index() == 0 ? std::get<0>(packetOrName) : NetworkPacket(PACKET_TYPE_MPRIS);
    if (packetOrName.index() == 1)
        np.set(QStringLiteral("player"), std::get<1>(packetOrName));

    np.set(QStringLiteral("transferringAlbumArt"), true);
    np.set(QStringLiteral("albumArtUrl"), artUrl);
    np.setPayload(qdata, qdata->size());

    sendPacket(np);
    return true;
}

void MprisControlPlugin::handleDefaultPlayer(const NetworkPacket &np)
{
    if (np.has(QStringLiteral("action"))) {
        INPUT input = {0};
        input.type = INPUT_KEYBOARD;

        input.ki.time = 0;
        input.ki.dwExtraInfo = 0;
        input.ki.wScan = 0;
        input.ki.dwFlags = 0;

        const QString &action = np.get<QString>(QStringLiteral("action"));

        if (action == QStringLiteral("PlayPause") || (action == QStringLiteral("Play")) || (action == QStringLiteral("Pause"))) {
            input.ki.wVk = VK_MEDIA_PLAY_PAUSE;
            ::SendInput(1, &input, sizeof(INPUT));
        } else if (action == QStringLiteral("Stop")) {
            input.ki.wVk = VK_MEDIA_STOP;
            ::SendInput(1, &input, sizeof(INPUT));
        } else if (action == QStringLiteral("Next")) {
            input.ki.wVk = VK_MEDIA_NEXT_TRACK;
            ::SendInput(1, &input, sizeof(INPUT));
        } else if (action == QStringLiteral("Previous")) {
            input.ki.wVk = VK_MEDIA_PREV_TRACK;
            ::SendInput(1, &input, sizeof(INPUT));
        } else if (action == QStringLiteral("Stop")) {
            input.ki.wVk = VK_MEDIA_STOP;
            ::SendInput(1, &input, sizeof(INPUT));
        }
    }

    // Send something read from the mpris interface
    NetworkPacket answer(PACKET_TYPE_MPRIS);
    answer.set(QStringLiteral("player"), DEFAULT_PLAYER);
    bool somethingToSend = false;
    if (np.get<bool>(QStringLiteral("requestNowPlaying"))) {
        answer.set(QStringLiteral("pos"), 0);
        answer.set(QStringLiteral("isPlaying"), false);
        answer.set(QStringLiteral("canPause"), false);
        answer.set(QStringLiteral("canPlay"), true);
        answer.set(QStringLiteral("canGoNext"), true);
        answer.set(QStringLiteral("canGoPrevious"), true);
        answer.set(QStringLiteral("canSeek"), false);
        somethingToSend = true;
    }

    if (np.get<bool>(QStringLiteral("requestVolume"))) {
        // we don't support setting per-app volume levels yet
        answer.set(QStringLiteral("volume"), -1);
        somethingToSend = true;
    }

    if (somethingToSend) {
        sendPacket(answer);
    }
}

void MprisControlPlugin::receivePacket(const NetworkPacket &np)
{
    if (np.has(QStringLiteral("playerList"))) {
        return; // Whoever sent this is an mpris client and not an mpris control!
    }

    // Send the player list
    const QString name = np.get<QString>(QStringLiteral("player"));

    if (name == DEFAULT_PLAYER) {
        handleDefaultPlayer(np);
        return;
    }

    auto it = playerList.find(name);
    bool valid_player = (it != playerList.end());
    if (!valid_player || np.get<bool>(QStringLiteral("requestPlayerList"))) {
        updatePlayerList();
        sendPlayerList();
        if (!valid_player) {
            return;
        }
    }

    auto player = it.value();

    if (np.has(QStringLiteral("albumArtUrl"))) {
        // Obtain the thumbnail in a separate working thread because the task
        // calling IRandomAccessStreamReference.OpenReadAsync().get() seems to
        // block the Qt main event loop. This prevents freezes during thumbnail
        // retrieval.
        concurrency::create_task([=] {
            getThumbnail(name, player, np.get<QString>(QStringLiteral("albumArtUrl")));
        });
        return;
    }

    if (np.has(QStringLiteral("action"))) {
        const QString &action = np.get<QString>(QStringLiteral("action"));
        if (action == QStringLiteral("Next")) {
            player.TrySkipNextAsync().get();
        } else if (action == QStringLiteral("Previous")) {
            player.TrySkipPreviousAsync().get();
        } else if (action == QStringLiteral("Pause")) {
            player.TryPauseAsync().get();
        } else if (action == QStringLiteral("PlayPause")) {
            player.TryTogglePlayPauseAsync().get();
        } else if (action == QStringLiteral("Stop")) {
            player.TryStopAsync().get();
        } else if (action == QStringLiteral("Play")) {
            player.TryPlayAsync().get();
        }
    }
    if (np.has(QStringLiteral("setVolume"))) {
        qWarning(KDECONNECT_PLUGIN_MPRISCONTROL) << "Setting volume is not supported";
    }
    if (np.has(QStringLiteral("Seek"))) {
        TimeSpan offset = std::chrono::microseconds(np.get<int>(QStringLiteral("Seek")));
        qWarning(KDECONNECT_PLUGIN_MPRISCONTROL) << "Seeking" << offset.count() << "ns to" << name;
        player.TryChangePlaybackPositionAsync((getPlayerPosition(player) + offset).count()).get();
    }

    if (np.has(QStringLiteral("SetPosition"))) {
        TimeSpan position = std::chrono::milliseconds(np.get<qlonglong>(QStringLiteral("SetPosition"), 0));
        player.TryChangePlaybackPositionAsync((player.GetTimelineProperties().StartTime() + position).count()).get();
    }

    if (np.has(QStringLiteral("setShuffle"))) {
        player.TryChangeShuffleActiveAsync(np.get<bool>(QStringLiteral("setShuffle")));
    }

    if (np.has(QStringLiteral("setLoopStatus"))) {
        QString loopStatus = np.get<QString>(QStringLiteral("setLoopStatus"));
        enum class winrt::Windows::Media::MediaPlaybackAutoRepeatMode loopStatusEnumVal;
        if (loopStatus == QStringLiteral("Track")) {
            loopStatusEnumVal = Windows::Media::MediaPlaybackAutoRepeatMode::Track;
        } else if (loopStatus == QStringLiteral("Playlist")) {
            loopStatusEnumVal = Windows::Media::MediaPlaybackAutoRepeatMode::List;
        } else {
            loopStatusEnumVal = Windows::Media::MediaPlaybackAutoRepeatMode::None;
        }
        player.TryChangeAutoRepeatModeAsync(loopStatusEnumVal);
    }

    // Send something read from the mpris interface
    NetworkPacket answer(PACKET_TYPE_MPRIS);
    answer.set(QStringLiteral("player"), name);
    bool somethingToSend = false;
    if (np.get<bool>(QStringLiteral("requestNowPlaying"))) {
        sendPlaybackInfo(answer, player);
        somethingToSend = true;
    }
    if (np.get<bool>(QStringLiteral("requestVolume"))) {
        // we don't support setting per-app volume levels yet
        answer.set(QStringLiteral("volume"), -1);
        somethingToSend = true;
    }

    if (somethingToSend) {
        sendPacket(answer);
    }
}

#include "moc_mpriscontrolplugin-win.cpp"
#include "mpriscontrolplugin-win.moc"
