#include "dbus.hpp"
#include "log.hpp"
#include "backend.hpp"
#include <random>
#include <fmt/core.h>
#include <fmt/format.h>
#ifdef DBUS_ENABLED
MprisAPI::MprisAPI(sdbus::IConnection &connection, std::string objectPath, DBusAPI *dbus_api)
 : AdaptorInterfaces(connection, std::move(objectPath))
 , dbus_api(dbus_api)
 , connection(connection)
{
    registerAdaptor();
    connection.enterEventLoopAsync();
}
void MprisAPI::Quit() {
    dbus_api->Quit();
}
void MprisAPI::Pause() {
    dbus_api->Paused(true);
}
void MprisAPI::PlayPause() {
    dbus_api->TogglePause();
}
void MprisAPI::Stop() {
    dbus_api->Stop();
}
void MprisAPI::Play() {
    dbus_api->Paused(false);
}
void MprisAPI::Seek(const int64_t &offset) {
    double value = offset;
    value /= 1000000;
    dbus_api->Position(value);
}
void MprisAPI::SetPosition(const sdbus::ObjectPath &TrackId, const int64_t &offset) {
    if (TrackId == playing_track_id) {
        Seek(offset);
    }
}
void MprisAPI::OpenUri(const std::string &Uri) {
    dbus_api->Start(Uri, true);
}
std::string MprisAPI::PlaybackStatus() {
    if (dbus_api->IsStopped()) {
        return "Stopped";
    } else {
        return dbus_api->Paused() ? "Paused" : "Playing";
    }
}
double MprisAPI::Rate()  {
    return dbus_api->Speed() * dbus_api->Tempo();
}
void MprisAPI::Rate(const double &value) {
    dbus_api->Speed(value / dbus_api->Tempo());
}
std::map<std::string, sdbus::Variant> MprisAPI::Metadata() {
    std::map<std::string, sdbus::Variant> output;
    if (!dbus_api->IsStopped()) {
        output["mpris:length"] = (int64_t)(dbus_api->Length() * 1000000);
        output["mpris:trackid"] = playing_track_id;
        output["xesam:title"] = dbus_api->FileTitle();
        output["xesam:url"] = dbus_api->FilePath();
    } else {
        output["mpris:trackid"] = empty_track_id;
    }
    return output;
}
double MprisAPI::Volume() {
    return dbus_api->Volume();
}
void MprisAPI::Volume(const double &value) {
    dbus_api->Volume(value);
}
int64_t MprisAPI::Position() {
    return dbus_api->Position() * 1000000;
}
bool MprisAPI::CanPlay() {
    return !dbus_api->IsStopped();
}
bool MprisAPI::CanPause() {
    return !dbus_api->IsStopped();
}
bool MprisAPI::CanSeek() {
    return !dbus_api->IsStopped() && dbus_api->Length() > 0;
}

std::vector<meta_t> MprisAPI::GetTracksMetadata(const std::vector<track_id_t> &TrackIds) {
    std::vector<meta_t> output;
    if (!dbus_api->IsStopped()) {
        output.push_back(Metadata());
        auto streams = dbus_api->playback->get_streams();
        int i = 0;
        for (auto stream : streams) {
            std::map<std::string, sdbus::Variant> meta;
            meta["mpris:trackid"] = fmt::format("{}{}", streamPrefix, i);
            meta["xesam:title"] = stream.name;
            i++;
        }
    }
    return output;
}
void MprisAPI::AddTrack(const std::string &Uri, const track_id_t &AfterTrack, const bool &SetAsCurrent) {
    return;
}
void MprisAPI::RemoveTrack(const track_id_t &TrackId) {
    return;
}
void MprisAPI::GoTo(const track_id_t &TrackId) {
    if (TrackId.starts_with(streamPrefix)) {
        std::string stream = TrackId.substr(streamPrefix.length());
        int streamIdx = std::stoi(stream);
        dbus_api->PlayStream((uint32_t)streamIdx);
    }
    return;
}
std::vector<track_id_t> MprisAPI::Tracks() {
    std::vector<track_id_t> output;
    if (dbus_api->IsStopped()) {
        return output;
    }
    output.push_back(playing_track_id);
    auto streams = dbus_api->playback->get_streams();
    int i = 0;
    for (auto stream : streams) {
        output.push_back(fmt::format("{}{}", streamPrefix, i));
        i++;
    }
    return output;
}
bool MprisAPI::CanEditTracks() {
    return false;
}
MprisAPI::~MprisAPI() {
	connection.leaveEventLoop();
    unregisterAdaptor();
}
#endif
#ifndef DBUS_ENABLED
DBusAPI::DBusAPI(Playback *playback, bool daemon)
 : playback(playback) {

}
#else
DBusAPI::DBusAPI(Playback *playback, sdbus::IConnection &connection, std::string objectPath, bool daemon)
 : AdaptorInterfaces(connection, std::move(objectPath))
 , daemon(daemon)
 , playback(playback)
 , connection(connection)
{
    registerAdaptor();
    playback->register_handle(this);
    auto mprisConnection = sdbus::createSessionBusConnection("org.mpris.MediaPlayer2.Looper");
    auto &mprisConRef = *mprisConnection.release();
    mpris = new MprisAPI(mprisConRef, "/org/mpris/MediaPlayer2", this);
    threadFunc = std::thread([this]() {
        while (!threadExitFlag.load()) {
            Update();
            std::this_thread::yield();
        }
    });
    connection.enterEventLoopAsync();
}
#endif
const char *DBusAPI::objectPath = "/com/complecwaft/looper";
const char *DBusAPI::busName = "com.complecwaft.looper";
DBusAPI *DBusAPI::Create(Playback *playback, bool daemon) {
#ifdef DBUS_ENABLED
    auto connection = sdbus::createSessionBusConnection(busName);
    auto &con_ref = *connection.release();
    return new DBusAPI(playback, con_ref, objectPath, daemon);
#else
    return new DBusAPI(playback, daemon);
#endif
}
#ifdef DBUS_ENABLED
double DBusAPI::Position() {
    return playback->GetPosition();
}
void DBusAPI::Position(const double &value) {
    playback->Seek(value);
}
double DBusAPI::Length() {
    return playback->GetLength();
}
std::string DBusAPI::FilePath() {
    return playback->get_current_file().value_or("");
}
std::string DBusAPI::FileTitle() {
    return playback->get_current_title().value_or("");
}
void DBusAPI::Update() {
    uint16_t signals = playback->handle_signals(PlaybackSignalErrorOccurred|PlaybackSignalFileChanged|PlaybackSignalPaused|PlaybackSignalPitchChanged|PlaybackSignalResumed|PlaybackSignalSeeked|PlaybackSignalSpeedChanged|PlaybackSignalStarted|PlaybackSignalStopped|PlaybackSignalTempoChanged, this);
    if (signals & PlaybackSignalErrorOccurred) {
        auto error = playback->GetError(this).value_or("Unknown error.");
        emitErrorOccurred(error, error);
        mpris->sendPropertiesChanged(mpris->playerInterface, {"PlaybackStatus", "CanSeek", "CanPlay", "CanPause", "Metadata"});
    }
    if (signals & PlaybackSignalFileChanged) {
        emitFileChanged(playback->get_current_file().value_or(""), playback->get_current_title().value_or(""));
        mpris->emitTrackMetadataChanged(mpris->playing_track_id, mpris->Metadata());
        mpris->sendPropertiesChanged(mpris->playerInterface, {"Position", "Metadata"});
    }
    if (signals & (PlaybackSignalPaused|PlaybackSignalResumed)) {
        emitPauseChanged(playback->IsPaused());
        mpris->sendPropertiesChanged(mpris->playerInterface, {"PlaybackStatus"});
    }
    if (signals & PlaybackSignalStopped) {
        emitStopped();
        emitPauseChanged(playback->IsPaused());
        mpris->emitTrackRemoved(mpris->playing_track_id);
        mpris->sendPropertiesChanged(mpris->playerInterface, {"PlaybackStatus", "CanSeek", "CanPlay", "CanPause", "Metadata"});
    }
    if (signals & PlaybackSignalPitchChanged) {
        emitPitchChanged(playback->GetPitch());
    }
    if (signals & PlaybackSignalTempoChanged) {
        emitTempoChanged(playback->GetTempo());
        mpris->sendPropertiesChanged(mpris->playerInterface, {"Rate"});
    }
    if (signals & PlaybackSignalSpeedChanged) {
        emitSpeedChanged(playback->GetSpeed());
        mpris->sendPropertiesChanged(mpris->playerInterface, {"Rate"});
    }
    if (signals & PlaybackSignalSeeked) {
        emitSeeked(playback->GetPosition());
        mpris->emitSeeked(playback->GetPosition() * 1000000);
        mpris->sendPropertiesChanged(mpris->playerInterface, {"Position"});
    }
    if (signals & PlaybackSignalStarted) {
        emitPlaybackEngineStarted();
        mpris->emitTrackAdded(mpris->Metadata(), mpris->empty_track_id);
        mpris->sendPropertiesChanged(mpris->playerInterface, {"CanSeek", "CanPlay", "CanPause", "Position", "Metadata", "PlaybackStatus"});
    }
}
void DBusAPI::Activate(const std::map<std::string, sdbus::Variant> &platform_data) {
    // TODO: Raise window?
}
void DBusAPI::Open(const std::vector<std::string>& uris, const std::map<std::string, sdbus::Variant> &platform_data) {
    if (uris.size() > 0) {
        Start(uris[0], true);
    }
}
void DBusAPI::ActivateAction(const std::string &action_name, const std::vector<sdbus::Variant>& parameter, const std::map<std::string, sdbus::Variant> &platform_data) {
    // Nothing yet.
}
std::string uriToPath(const std::string &uri) {

    const static std::string file_prefix = "file:";
    std::string tmp = uri;
    if (tmp.starts_with(file_prefix)) {
        tmp = tmp.substr(file_prefix.length());
        return tmp;
    }
    return uri;
}
void DBusAPI::Start(const std::string &path, const bool &isUri) {
    StartWithStreamIndex(path, isUri, 0);
}
void DBusAPI::StartWithStreamIndex(const std::string &path, const bool &isUri, const uint32_t &streamIndex) {
    if (isUri) {
        StartWithStreamIndex(uriToPath(path), false, streamIndex);
    } else {
        playback->Start(path, (int)streamIndex);
    }
}
void DBusAPI::Load(const std::string &path, const bool &isUri) {
    if (isUri) {
        Load(uriToPath(path), false);
    } else {
        playback->Load(path);
    }
}
void DBusAPI::Stop() {
    playback->Stop();
}
void DBusAPI::TogglePause() {
    playback->Pause();
}
double DBusAPI::Speed() {
    return playback->GetSpeed();
}
void DBusAPI::Speed(const double &value) {
    playback->SetSpeed(value);
}
double DBusAPI::Tempo() {
    return playback->GetTempo();
}
void DBusAPI::Tempo(const double &value) {
    playback->SetTempo(value);
}
double DBusAPI::Pitch() {
    return playback->GetPitch();
}
void DBusAPI::Pitch(const double &value) {
    playback->SetPitch(value);
}
double DBusAPI::Volume() {
    return playback->GetVolume();
}
void DBusAPI::Volume(const double &value) {
    playback->SetVolume(value);
}
bool DBusAPI::Paused() {
    return playback->IsPaused();
}
void DBusAPI::Paused(const bool &value) {
    if (value != playback->IsPaused()) {
        playback->Pause();
    }
}
bool DBusAPI::IsStopped() {
    return playback->IsStopped();
}
bool DBusAPI::IsDaemon() {
    return daemon;
}
std::string DBusAPI::CreateHandle() {
    size_t idx = handle_idx++;
    size_t hash = std::hash<size_t>()(idx);
    size_t rand = (size_t)rand_engine();
    size_t rand_hash = std::hash<size_t>()(rand);
    std::string value = fmt::format("{}:{}:{}", idx, hash, rand_hash);
    handles[value] = (void*)&value;
    playback->register_handle(handles[value]);
    return value;
}
void DBusAPI::ClearHandle(const std::string &handle) {
    if (handles.contains(handle)) {
        playback->unregister_handle(handles[handle]);
        handles.erase(handle);
    }
}
std::deque<std::string> *DBusAPI::get_errors_by_handle(const std::string &handle) {
    if (handles.contains(handle)) {
        auto *v_handle = handles[handle];
        if (playback->errors_occurred.contains(v_handle)) {
            return &playback->errors_occurred[v_handle];
        }
    }
    return nullptr;
}
std::string DBusAPI::PopFront(const std::string &handle) {
    auto *errors_list = get_errors_by_handle(handle);
    if (errors_list != nullptr) {
        if (errors_list->empty()) {
            return "";
        }
        std::string output = errors_list->front();
        errors_list->pop_front();
        return output;
    }
    return "";
}
std::string DBusAPI::PopBack(const std::string &handle) {
    auto *errors_list = get_errors_by_handle(handle);
    if (errors_list != nullptr) {
        if (errors_list->empty()) {
            return "";
        }
        std::string output = errors_list->back();
        errors_list->pop_back();
        return output;
    }
    return "";
}
std::string DBusAPI::PeekFront(const std::string &handle) {
    auto *errors_list = get_errors_by_handle(handle);
    if (errors_list != nullptr) {
        if (errors_list->empty()) {
            return "";
        }
        std::string output = errors_list->front();
        return output;
    }
    return "";
}
void DBusAPI::Quit() {
    UIBackend::running_ui_backend->QuitHandler();
}
std::string DBusAPI::PeekBack(const std::string &handle) {
    auto *errors_list = get_errors_by_handle(handle);
    if (errors_list != nullptr) {
        if (errors_list->empty()) {
            return "";
        }
        std::string output = errors_list->back();
        return output;
    }
    return "";
}
uint32_t DBusAPI::GetCount(const std::string &handle) {
    auto *errors_list = get_errors_by_handle(handle);
    if (errors_list != nullptr) {
        return errors_list->size();
    }
    return 0;
}
bool DBusAPI::IsEmpty(const std::string& handle) {
    return GetCount(handle) == 0;
}
void DBusAPI::Clear(const std::string &handle) {
    auto *errors_list = get_errors_by_handle(handle);
    if (errors_list != nullptr) {
        errors_list->clear();
    }
}
std::vector<std::string> DBusAPI::PeekAll(const std::string &handle) {
    auto *errors_list = get_errors_by_handle(handle);
    if (errors_list != nullptr) {
        std::vector<std::string> output;
        for (auto error : (*errors_list)) {
            output.push_back(error);
        }
        return output;
    }
    return std::vector<std::string>();
}
std::vector<std::string> DBusAPI::GetAllAndClear(const std::string &handle) {
    auto output = PeekAll(handle);
    Clear(handle);
    return output;
}
uint32_t DBusAPI::StreamIdx() {
    return playback->get_current_stream();
}
std::vector<sdbus::Struct<double, std::string, int32_t>> DBusAPI::GetStreams() {
    std::vector<sdbus::Struct<double, std::string, int32_t>> output;
    auto streams = playback->get_streams();
    for (auto stream : streams) {
        sdbus::Struct<double, std::string, int32_t> tmp(stream.length, stream.name, stream.id);
        output.push_back(tmp);
    }
    return output;
}
void DBusAPI::PlayStream(const uint32_t &idx) {
    playback->play_stream((int)idx);
}
#endif
DBusAPI::~DBusAPI() {
#ifdef DBUS_ENABLED
    threadExitFlag.store(true);
    threadFunc.detach();
    connection.leaveEventLoop();
    unregisterAdaptor();
#endif
}
bool DBusAPISender::isOnlyInstance() {
#ifdef DBUS_ENABLED
    bool output;
    try {
        auto *tmp = Create();
        output = tmp == nullptr;
        delete tmp;
        return output;
    } catch (sdbus::Error) {
        return true;
    }
#else
    return true;
#endif
}
#ifdef DBUS_ENABLED

std::optional<std::string> DBusAPISender::get_current_file() {
    if (IsStopped()) {
        return {};
    } else {
        return FilePath();
    }
}
std::optional<std::string> DBusAPISender::get_current_title() {
    if (IsStopped()) {
        return {};
    } else {
        return FileTitle();
    }
}
void DBusAPISender::onPlaybackEngineStarted() {
    set_signal(PlaybackSignalStarted);
}
void DBusAPISender::onFileChanged(const std::string &FilePath, const std::string &Title) {
    set_signal(PlaybackSignalFileChanged);
}
void DBusAPISender::onSpeedChanged(const double &new_speed) {
    set_signal(PlaybackSignalSpeedChanged);
}
void DBusAPISender::onTempoChanged(const double &new_tempo) {
    set_signal(PlaybackSignalTempoChanged);
}
void DBusAPISender::onPitchChanged(const double &new_pitch) {
    set_signal(PlaybackSignalPitchChanged);
}
void DBusAPISender::onPauseChanged(const bool &now_paused) {
    set_signal(now_paused ? PlaybackSignalPaused : PlaybackSignalResumed);
}
void DBusAPISender::onStopped() {
    set_signal(PlaybackSignalStopped);
}
void DBusAPISender::onErrorOccurred(const std::string &error_desc, const std::string &error_type) {
    set_signal(PlaybackSignalErrorOccurred);
}
void DBusAPISender::onSeeked(const double &to_position) {
    set_signal(PlaybackSignalSeeked);
}
double DBusAPISender::GetPosition() {
    return ProxyInterfaces::Position();
}
double DBusAPISender::GetLength() {
    return ProxyInterfaces::Length();
}
void DBusAPISender::Seek(double position) {
    ProxyInterfaces::Position(position);
}
void DBusAPISender::Start(std::string filePath, int streamIdx) {
    ProxyInterfaces::Start(filePath, streamIdx);
}
void DBusAPISender::Load(std::string filePath) {
    ProxyInterfaces::Load(filePath, false);
}
int DBusAPISender::get_current_stream() {
    return (int)ProxyInterfaces::StreamIdx();
}
std::vector<PlaybackStream> DBusAPISender::get_streams() {
    std::vector<PlaybackStream> output;
    auto input = ProxyInterfaces::GetStreams();
    for (auto stream : input) {
        PlaybackStream tmp;
        tmp.length = stream.get<0>();
        tmp.name = stream.get<1>();
        tmp.id = stream.get<2>();
        output.push_back(tmp);
    }
    return output;
}
bool DBusAPISender::IsPaused() {
    return ProxyInterfaces::Paused();
}
void DBusAPISender::Pause() {
    ProxyInterfaces::TogglePause();
}
void DBusAPISender::Stop() {
    ProxyInterfaces::Stop();
}
bool DBusAPISender::IsStopped() {
    return ProxyInterfaces::IsStopped();
}
void DBusAPISender::SetTempo(float tempo) {
    ProxyInterfaces::Tempo(tempo);
}
void DBusAPISender::SetPitch(float pitch) {
    ProxyInterfaces::Pitch(pitch);
}
void DBusAPISender::SetSpeed(float speed) {
    ProxyInterfaces::Speed(speed);
}
void DBusAPISender::SetVolume(float volume) {
    ProxyInterfaces::Volume(volume);
}
float DBusAPISender::GetTempo() {
    return (float)ProxyInterfaces::Tempo();
}
float DBusAPISender::GetPitch() {
    return (float)ProxyInterfaces::Pitch();
}
float DBusAPISender::GetSpeed() {
    return (float)ProxyInterfaces::Speed();
}
float DBusAPISender::GetVolume() {
    return (float)ProxyInterfaces::Volume();
}
void DBusAPISender::Update() {

}
#endif
DBusAPISender *DBusAPISender::Create() {
#ifdef DBUS_ENABLED
    try {
        auto connection = sdbus::createSessionBusConnection();
        auto &con_ref = *connection.release();
        auto *output = new DBusAPISender(con_ref, DBusAPI::busName, DBusAPI::objectPath);
        return output;
    } catch (sdbus::Error error) {
        DEBUG.writefln("sdbus::Error: %s: %s", error.getName().c_str(), error.getMessage().c_str());
        return nullptr;
    }
#else
    return nullptr;
#endif
}
#ifdef DBUS_ENABLED
DBusAPISender::DBusAPISender(sdbus::IConnection &connection, std::string busName, std::string objectPath)
 : ProxyInterfaces(connection, std::move(busName), std::move(objectPath)) {
    registerProxy();
    DEBUG.writeln("Pinging DBus API to check for its existance.");
    Ping();
    DEBUG.writeln("Requesting handle...");
    handle = CreateHandle();
    DEBUG.writeln("Preparing updates for UI frontend...");
    if (IsStopped()) {
        set_signal(PlaybackSignalStopped);
    } else {
        filePath = ProxyInterfaces::FilePath();
        title = ProxyInterfaces::FileTitle();
        length = ProxyInterfaces::Length();
        set_signal(PlaybackSignalStarted|PlaybackSignalSeeked|PlaybackSignalFileChanged);
    }
    if (IsPaused()) {
        set_signal(PlaybackSignalPaused);
    } else {
        set_signal(PlaybackSignalResumed);
    }
    set_signal(PlaybackSignalSpeedChanged|PlaybackSignalPitchChanged|PlaybackSignalTempoChanged);
    connection.enterEventLoopAsync();
}
DBusAPISender::~DBusAPISender() {
    DEBUG.writeln("Requesting handle deletion...");
    ClearHandle(handle);
    unregisterProxy();
}
#endif