/**
 * @file   utils/dbus_portal.cpp
 * @author Matej Hrica       <492778@mail.muni.cz>
 * @author Martin Piatka     <piatka@cesnet.cz>
 */
/*
 * Copyright (c) 2023 CESNET z.s.p.o.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, is permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * 3. Neither the name of CESNET nor the names of its contributors may be
 *    used to endorse or promote products derived from this software without
 *    specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHORS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING,
 * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 * EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


#include <algorithm>
#include <string>
#include <memory>
#include <cassert>
#include <future>
#include <fstream>
#include <gio/gio.h>
#include <gio/gunixfdlist.h>
#include <glib-object.h>      // for GObject, g_object_unref
#include <glib.h>             // for g_variant_builder_add, GVariant, g_vari...
#include <glib/gtypes.h>      // for gint32
#include <initializer_list>   // for initializer_list

#include "debug.h"
#include "misc.h"
#include "dbus_portal.hpp"


#define MOD_NAME "[dbus] "

namespace {
struct request_path_t {
        std::string token;
        std::string path;

        static request_path_t create(const std::string &name) {
                ++token_counter;

                auto token = std::string("uv") + std::to_string(token_counter);
                request_path_t result = {
                        .token = token,
                        .path = std::string("/org/freedesktop/portal/desktop/request/") + name + "/" + token
                };

                LOG(LOG_LEVEL_DEBUG) << "new request: '" << result.path << "'\n";
                return result;
        }

private:
        static unsigned int token_counter;
};

unsigned int request_path_t::token_counter = 0;

struct session_path_t {
        std::string token;
        std::string path;

        static session_path_t create(const std::string &name) {
                ++token_counter;

                auto token = std::string("uv") + std::to_string(token_counter);
                return {
                        .token = token,
                        .path = std::string("/org/freedesktop/portal/desktop/session/") + name + "/" + token
                };
        }

private:
        static unsigned int token_counter;
};

unsigned int session_path_t::token_counter = 0;

struct GVariant_deleter { void operator()(GVariant *a) { g_variant_unref(a); } };
using GVariant_uniq = std::unique_ptr<GVariant, GVariant_deleter>;


} //anon namespace

class ScreenCastPortal_impl {
private:
        template<auto callback>
        void call_with_request(const char* method_name,
                        std::initializer_list<GVariant*> arguments,
                        GVariantBuilder &params_builder);

        static void pipewire_opened(GObject *source,
                        GAsyncResult *res,
                        void *user_data);

        static void started(uint32_t response, GVariant *results, void *user_data);
        static void sources_selected(uint32_t response, GVariant *results, ScreenCastPortal_impl *s);
        static void session_created(uint32_t response, GVariant *results, ScreenCastPortal_impl *s);

        GMainLoop *dbus_loop;
        GDBusConnection *connection;
        GDBusProxy *screencast_proxy;
        std::string unique_name;
        session_path_t session;

        std::string restore_file;
        bool show_cursor = false;

        int pw_fd = -1;
        uint32_t pw_node = UINT32_MAX;

        std::promise<std::string> promise;

public:
        // see https://flatpak.github.io/xdg-desktop-portal/#gdbus-signal-org-freedesktop-portal-Request.Response
        static constexpr uint32_t REQUEST_RESPONSE_OK = 0;
        static constexpr uint32_t REQUEST_RESPONSE_CANCELLED_BY_USER = 1;
        static constexpr uint32_t REQUEST_RESPONSE_OTHER_ERROR = 2;

        ScreenCastPortal_impl(); 
        ~ScreenCastPortal_impl();

        void run(std::string restore_file, bool show_cursor);

        std::future<std::string> get_future() {
                return promise.get_future();
        }

        int get_pw_fd(){ return pw_fd; }
        uint32_t get_pw_node(){ return pw_node; }
        

        void run_loop() {
                g_main_loop_run(dbus_loop);
                LOG(LOG_LEVEL_VERBOSE) << MOD_NAME "finished dbus loop \n";
        }

        void quit_loop() {
                g_main_loop_quit(dbus_loop);
        }

        GDBusProxy *proxy() {
                return screencast_proxy;
        }

        GDBusConnection *dbus_connection() const {
                return connection;
        }

        const std::string& sender_name() const
        {
                return unique_name;
        }

        const std::string& session_path() const
        {
                return session.path;
        }

        const std::string& session_token() const
        {
                return session.token;
        }
};

ScreenCastPortal_impl::ScreenCastPortal_impl(){
        GError *error = nullptr;

        dbus_loop = g_main_loop_new(nullptr, false);
        connection = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error);
        g_assert_no_error(error);
        assert(connection != nullptr);

        unique_name = g_dbus_connection_get_unique_name(connection) + 1;
        std::replace(unique_name.begin(), unique_name.end(), '.', '_');
        screencast_proxy = g_dbus_proxy_new_sync(
                        connection, G_DBUS_PROXY_FLAGS_NONE, nullptr,
                        "org.freedesktop.portal.Desktop",
                        "/org/freedesktop/portal/desktop",
                        "org.freedesktop.portal.ScreenCast", nullptr, &error);
        g_assert_no_error(error); 
        assert(screencast_proxy != nullptr);

        session = session_path_t::create(unique_name);
        LOG(LOG_LEVEL_VERBOSE) << MOD_NAME "session path: '" << session.path << "'" << " token: '" << session.token << "'\n";
}

ScreenCastPortal_impl::~ScreenCastPortal_impl() {
        g_dbus_connection_call(dbus_connection(),
                        "org.freedesktop.portal.Desktop",
                        session.path.c_str(),
                        "org.freedesktop.portal.Session",
                        "Close", nullptr, nullptr,
                        G_DBUS_CALL_FLAGS_NONE, -1, nullptr, nullptr,
                        nullptr);
        g_main_loop_quit(dbus_loop);
        g_object_unref(screencast_proxy);
        g_object_unref(connection);
        g_main_loop_unref(dbus_loop);
}

template<auto func>
void CallbackWrapper(GDBusConnection *connection,
                const gchar *sender_name,
                const gchar *object_path,
                const gchar *interface_name,
                const gchar *signal_name,
                GVariant *parameters,
                gpointer user_data)
{
                (void) sender_name;
                (void) interface_name;
                (void) signal_name;

                uint32_t response;
                GVariant_uniq results;
                g_variant_get(parameters, "(u@a{sv})", &response, out_ptr(results).operator GVariant**());

                func(response, results.get(), static_cast<ScreenCastPortal_impl *>(user_data));

                g_dbus_connection_call(connection, "org.freedesktop.portal.Desktop",
                                object_path, "org.freedesktop.portal.Request", "Close",
                                nullptr, nullptr, G_DBUS_CALL_FLAGS_NONE, -1, nullptr, nullptr, nullptr);

}

template<auto callback>
void ScreenCastPortal_impl::call_with_request(const char* method_name,
                std::initializer_list<GVariant*> arguments,
                GVariantBuilder &params_builder)
{
        assert(method_name != nullptr);
        request_path_t request_path = request_path_t::create(sender_name());
        LOG(LOG_LEVEL_VERBOSE) << MOD_NAME "call_with_request: '" << method_name << "' request: '" << request_path.path << "'\n";

        g_dbus_connection_signal_subscribe(connection, "org.freedesktop.portal.Desktop",
                        "org.freedesktop.portal.Request",
                        "Response",
                        request_path.path.c_str(),
                        nullptr,
                        G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
                        CallbackWrapper<callback>,
                        this,
                        nullptr);

        auto call_finished = [](GObject *source_object, GAsyncResult *result, gpointer user_data) {
                auto s = static_cast<ScreenCastPortal_impl *>(user_data);
                GError *error = nullptr;
                GVariant_uniq result_finished(g_dbus_proxy_call_finish(G_DBUS_PROXY(source_object), result, &error));

                if(error != nullptr){
                        s->promise.set_value(error->message);
                        return;
                }

                const char *path = nullptr;
                g_variant_get(result_finished.get(), "(o)", &path);
                LOG(LOG_LEVEL_VERBOSE) << MOD_NAME "call_with_request finished: '" << path << "'\n";
        };


        g_variant_builder_add(&params_builder, "{sv}", "handle_token", g_variant_new_string(request_path.token.c_str()));

        GVariantBuilder args_builder;
        g_variant_builder_init(&args_builder, G_VARIANT_TYPE_TUPLE);
        for(GVariant* arg : arguments){
                g_variant_builder_add_value(&args_builder, arg);
        }
        g_variant_builder_add_value(&args_builder, g_variant_builder_end(&params_builder));

        g_dbus_proxy_call(screencast_proxy, method_name, g_variant_builder_end(&args_builder), G_DBUS_CALL_FLAGS_NONE, -1, nullptr, call_finished, this);     
}

static void on_portal_session_closed([[maybe_unused]] GDBusConnection *connection,
                [[maybe_unused]] const gchar *sender_name,
                [[maybe_unused]] const gchar *object_path,
                [[maybe_unused]] const gchar *interface_name,
                [[maybe_unused]] const gchar *signal_name,
                [[maybe_unused]] GVariant *parameters,
                [[maybe_unused]] gpointer user_data)
{
        //Doesn't seem to be called by GNOME
        LOG(LOG_LEVEL_INFO) << MOD_NAME "session closed by compositor\n";
}

void ScreenCastPortal_impl::pipewire_opened(GObject *source,
                GAsyncResult *res,
                void *user_data)
{
        auto s = static_cast<ScreenCastPortal_impl *>(user_data);
        GError *error = nullptr;
        GUnixFDList *fd_list = nullptr;

        GVariant_uniq result(g_dbus_proxy_call_with_unix_fd_list_finish(G_DBUS_PROXY(source), &fd_list, res, &error));
        g_assert_no_error(error);

        gint32 handle;
        g_variant_get(result.get(), "(h)", &handle);
        assert(handle == 0); //it should always be the first index

        s->pw_fd = g_unix_fd_list_get(fd_list, handle, &error);
        g_assert_no_error(error);

        s->promise.set_value("");
}

void ScreenCastPortal_impl::started(uint32_t response, GVariant *results, void *user_data) {
        auto s = static_cast<ScreenCastPortal_impl *>(user_data);
        LOG(LOG_LEVEL_DEBUG) << MOD_NAME "started: " << g_variant_print(results, true) << "\n";

        if(response == ScreenCastPortal_impl::REQUEST_RESPONSE_CANCELLED_BY_USER) {
                s->promise.set_value("failed to start (dialog cancelled by user)");
                return;
        } else if(response != ScreenCastPortal_impl::REQUEST_RESPONSE_OK) {
                s->promise.set_value("failed to start (unknown reason)");
                return;
        }

        const char *restore_token = nullptr;
        if (g_variant_lookup(results, "restore_token", "s", &restore_token)){
                std::ofstream file(s->restore_file);
                file<<restore_token;
        }

        GVariant *streams = g_variant_lookup_value(results, "streams", G_VARIANT_TYPE_ARRAY);
        GVariant *stream_properties;
        GVariantIter iter;
        g_variant_iter_init(&iter, streams);
        assert(g_variant_iter_n_children(&iter) == 1);
        bool got_item = g_variant_iter_loop(&iter, "(u@a{sv})", &s->pw_node, &stream_properties);
        assert(got_item);

        GVariantBuilder builder;
        g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
        g_dbus_proxy_call_with_unix_fd_list(s->proxy(), "OpenPipeWireRemote",
                        g_variant_new("(oa{sv})", s->session_path().c_str(), &builder),
                        G_DBUS_CALL_FLAGS_NONE, -1,
                        nullptr, nullptr, ScreenCastPortal_impl::pipewire_opened, s);
        g_variant_builder_clear(&builder);
}

void ScreenCastPortal_impl::sources_selected(uint32_t response, GVariant *results, ScreenCastPortal_impl *s) {
        gchar *pretty = g_variant_print(results, true);
        LOG(LOG_LEVEL_INFO) << MOD_NAME "selected sources: " << pretty << "\n";
        g_free((gpointer) pretty);

        if(response != ScreenCastPortal_impl::REQUEST_RESPONSE_OK) {
                s->promise.set_value("Failed to select sources");
                return;
        }

        {
                GVariantBuilder options;
                g_variant_builder_init(&options, G_VARIANT_TYPE_VARDICT);
                s->call_with_request<started>("Start", {g_variant_new_object_path(s->session_path().c_str()),  /*parent window: */ g_variant_new_string("")}, options);
        }
};

void ScreenCastPortal_impl::session_created(uint32_t response, GVariant *results, ScreenCastPortal_impl *s) {
        if(response != ScreenCastPortal_impl::REQUEST_RESPONSE_OK) {
                s->promise.set_value("Failed to create session");
                return;
        }

        const char *session_handle = nullptr;
        g_variant_lookup(results, "session_handle", "s", &session_handle);

        LOG(LOG_LEVEL_DEBUG) << MOD_NAME "session created with handle: " << session_handle << "\n";
        assert(s->session_path() == session_handle);

        {
                GVariantBuilder params;
                g_variant_builder_init(&params, G_VARIANT_TYPE_VARDICT);
                g_variant_builder_add(&params, "{sv}", "types", g_variant_new_uint32(3)); // 1 full screen, 2 - a window, 3 - both
                g_variant_builder_add(&params, "{sv}", "multiple", g_variant_new_boolean(false));
                if(s->show_cursor)
                        g_variant_builder_add(&params, "{sv}", "cursor_mode", g_variant_new_uint32(2));

                if(!s->restore_file.empty()){
                        std::string token;
                        std::ifstream file(s->restore_file);

                        if(file.is_open()) {
                                std::ostringstream ss;
                                ss << file.rdbuf();
                                token = ss.str();
                        }

                        //  0: Do not persist (default), 1: Permissions persist as long as the application is running, 2: Permissions persist until explicitly revoked
                        g_variant_builder_add(&params, "{sv}", "persist_mode", g_variant_new_uint32(2)); 
                        if(!token.empty())
                                g_variant_builder_add(&params, "{sv}", "restore_token", g_variant_new_string(token.c_str())); 
                }

                s->call_with_request<sources_selected>("SelectSources", {g_variant_new_object_path(s->session_path().c_str())}, params);
        }
};

void ScreenCastPortal_impl::run(std::string restore_file, bool show_cursor){
        this->restore_file = restore_file;
        this->show_cursor = show_cursor;

        g_dbus_connection_signal_subscribe(dbus_connection(), 
                        nullptr, // sender
                        "org.freedesktop.portal.Session", // interface_name
                        "closed", //signal name
                        session_path().c_str(), // object path
                        nullptr, // arg0
                        G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
                        on_portal_session_closed,
                        nullptr,
                        nullptr);

        GVariantBuilder params;
        g_variant_builder_init(&params, G_VARIANT_TYPE_VARDICT);
        g_variant_builder_add(&params, "{sv}", "session_handle_token", g_variant_new_string(session_token().c_str()));

        call_with_request<session_created>("CreateSession", {}, params);
        
        run_loop();
}

ScreenCastPortal::ScreenCastPortal():
        impl(std::make_unique<ScreenCastPortal_impl>())
{

}

ScreenCastPortal::~ScreenCastPortal(){
        if(thread.joinable()){
                impl->quit_loop();
                thread.join();
        }
}

ScreenCastPortalResult ScreenCastPortal::run(std::string restore_file, bool show_cursor){
        auto future = impl->get_future();

        thread = std::thread(&ScreenCastPortal_impl::run, impl.get(),
                        restore_file, show_cursor);

        auto error = future.get();
        if(!error.empty()){
                log_msg(LOG_LEVEL_ERROR, MOD_NAME "%s\n", error.c_str());
                return {-1, UINT32_MAX};
        }

        return {impl->get_pw_fd(), impl->get_pw_node()};
}
