/*
 * mpristunecontroller.cpp
 * Copyright (C) 2010  Vitaly Tonkacheyev
 *
 * 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.
 *
 * 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 "mpristunecontroller.h"

#include <QDBusConnectionInterface>
#include <QDBusMetaType>
#include <QDBusReply>

/**
 * \class MPRISTuneController
 * \brief A common controller class for MPRIS compilant players.
 */

const char *         MPRISTuneController::MPRIS_PREFIX = "org.mpris";
static const QString busName                           = "SessionBus";

QDBusArgument &operator<<(QDBusArgument &arg, const PlayerStatus &ps)
{
    arg.beginStructure();
    arg << ps.playStatus;
    arg << ps.playOrder;
    arg << ps.playRepeat;
    arg << ps.stopOnce;
    arg.endStructure();
    return arg;
}

const QDBusArgument &operator>>(const QDBusArgument &arg, PlayerStatus &ps)
{
    arg.beginStructure();
    arg >> ps.playStatus;
    arg >> ps.playOrder;
    arg >> ps.playRepeat;
    arg >> ps.stopOnce;
    arg.endStructure();
    return arg;
}

MPRISTuneController::MPRISTuneController() : tuneSent_(false)
{
    qDBusRegisterMetaType<PlayerStatus>();
    QDBusConnection bus = QDBusConnection::connectToBus(QDBusConnection::SessionBus, busName);
    players_            = bus.interface()->registeredServiceNames().value().filter(MPRIS_PREFIX);
    for (const QString &player : qAsConst(players_)) {
        connectToBus(player);
    }
    bus.connect(QLatin1String("org.freedesktop.DBus"), QLatin1String("/org/freedesktop/DBus"),
                QLatin1String("org.freedesktop.DBus"), QLatin1String("NameOwnerChanged"), this,
                SLOT(checkMprisService(QString, QString, QString)));
}

MPRISTuneController::~MPRISTuneController()
{
    for (const QString &player : qAsConst(players_)) {
        disconnectFromBus(player);
    }
    QDBusConnection(busName).disconnect(QLatin1String("org.freedesktop.DBus"), QLatin1String("/org/freedesktop/DBus"),
                                        QLatin1String("org.freedesktop.DBus"), QLatin1String("NameOwnerChanged"), this,
                                        SLOT(checkMprisService(QString, QString, QString)));
    QDBusConnection::disconnectFromBus(busName);
}

void MPRISTuneController::checkMprisService(const QString &name, const QString &oldOwner, const QString &newOwner)
{
    Q_UNUSED(oldOwner);
    if (name.startsWith(MPRIS_PREFIX)) {
        int playerIndex = players_.indexOf(name);
        if (playerIndex == -1) {
            if (!newOwner.isEmpty()) {
                players_.append(name);
                connectToBus(name);
            }
        } else if (newOwner.isEmpty()) {
            disconnectFromBus(name);
            players_.removeAt(playerIndex);
        }
    }
}

int MPRISTuneController::version(const QString &service_) const
{
    return service_.contains("MediaPlayer2") ? MPRIS_2 : MPRIS_1;
}

void MPRISTuneController::connectToBus(const QString &service_)
{
    QDBusConnection bus = QDBusConnection(busName);
    if (version(service_) != MPRIS_2) {
        bus.connect(service_, QLatin1String("/Player"), QLatin1String("org.freedesktop.MediaPlayer"),
                    QLatin1String("StatusChange"), QLatin1String("(iiii)"), this,
                    SLOT(onPlayerStatusChange(PlayerStatus)));
        bus.connect(service_, QLatin1String("/Player"), QLatin1String("org.freedesktop.MediaPlayer"),
                    QLatin1String("TrackChange"), QLatin1String("a{sv}"), this, SLOT(onTrackChange(QVariantMap)));
    } else {
        bus.connect(service_, QLatin1String("/org/mpris/MediaPlayer2"),
                    QLatin1String("org.freedesktop.DBus.Properties"), QLatin1String("PropertiesChanged"), this,
                    SLOT(onPropertyChange(QDBusMessage)));
    }
}

void MPRISTuneController::disconnectFromBus(const QString &service_)
{
    QDBusConnection bus = QDBusConnection(busName);
    if (version(service_) != MPRIS_2) {
        bus.disconnect(service_, QLatin1String("/Player"), QLatin1String("org.freedesktop.MediaPlayer"),
                       QLatin1String("StatusChange"), QLatin1String("(iiii)"), this,
                       SLOT(onPlayerStatusChange(PlayerStatus)));
        bus.disconnect(service_, QLatin1String("/Player"), QLatin1String("org.freedesktop.MediaPlayer"),
                       QLatin1String("TrackChange"), QLatin1String("a{sv}"), this, SLOT(onTrackChange(QVariantMap)));
    } else {
        bus.disconnect(service_, QLatin1String("/org/mpris/MediaPlayer2"),
                       QLatin1String("org.freedesktop.DBus.Properties"), QLatin1String("PropertiesChanged"), this,
                       SLOT(onPropertyChange(QDBusMessage)));
    }
    if (!currentTune_.isNull()) {
        emit stopped();
        tuneSent_    = false;
        currentTune_ = Tune();
    }
}

void MPRISTuneController::onPlayerStatusChange(const PlayerStatus &ps)
{
    if (!currentTune_.isNull()) {
        if (ps.playStatus != StatusPlaying) {
            emit stopped();
            tuneSent_ = false;
            if (ps.playStatus == StatusStopped)
                currentTune_ = Tune();
        } else if (!tuneSent_) {
            emit playing(currentTune_);
            tuneSent_ = true;
        }
    }
}

void MPRISTuneController::onTrackChange(const QVariantMap &map)
{
    Tune tune = getTune(map);
    if (tune != currentTune_ && !tune.isNull()) {
        currentTune_ = tune;
        emit playing(currentTune_);
        tuneSent_ = true;
    }
}

void MPRISTuneController::onPropertyChange(const QDBusMessage &msg)
{
    QDBusArgument arg = msg.arguments().at(1).value<QDBusArgument>();
    QVariantMap   map = qdbus_cast<QVariantMap>(arg);
    QVariant      v   = map.value(QLatin1String("Metadata"));
    if (v.isValid()) {
        arg       = v.value<QDBusArgument>();
        Tune tune = getMpris2Tune(qdbus_cast<QVariantMap>(arg));
        if (tune != currentTune_ && !tune.isNull()) {
            currentTune_ = tune;
            emit playing(currentTune_);
            tuneSent_ = true;
        }
    }
    v = map.value(QLatin1String("PlaybackStatus"));
    if (v.isValid()) {
        PlayerStatus status;
        status.playStatus = getMpris2Status(v.toString());
        onPlayerStatusChange(status);
    }
}

int MPRISTuneController::getMpris2Status(const QString &status) const
{
    if (status == QLatin1String("Playing")) {
        return StatusPlaying;
    } else if (status == QLatin1String("Paused")) {
        return StatusPaused;
    }
    return StatusStopped;
}

Tune MPRISTuneController::currentTune() const { return currentTune_; }

Tune MPRISTuneController::getTune(const QVariantMap &map) const
{
    Tune tune;
    tune.setName(map.value("title").toString());
    tune.setArtist(map.value("artist").toString());
    tune.setAlbum(map.value("album").toString());
    tune.setTrack(map.value("track").toString());
    tune.setURL(map.value("location").toString());
    tune.setTime(map.value("time").toUInt());
    return tune;
}

Tune MPRISTuneController::getMpris2Tune(const QVariantMap &map) const
{
    Tune tune;
    tune.setName(map.value("xesam:title").toString());
    tune.setArtist(map.value("xesam:artist").toString());
    tune.setAlbum(map.value("xesam:album").toString());
    tune.setTrack(QVariant(map.value("xesam:trackNumber").toUInt()).toString());
    tune.setURL(map.value("xesam:url").toString());
    tune.setTime(QVariant(map.value("mpris:length").toLongLong() / 1000000).toUInt());
    return tune;
}
