/*
 * Copyright © 2016 Canonical Ltd.
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 3,
 * as published by the Free Software Foundation.
 *
 * 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 <http://www.gnu.org/licenses/>.
 *
 * Authored by: Alexandros Frantzis <alexandros.frantzis@canonical.com>
 */

#include "src/adapters/scoped_g_error.h"

#include <gio/gio.h>

#include <csignal>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <vector>

std::string get_progname(int argc, char** argv)
{
    if (argc == 0)
        return "";
    else
        return argv[0];
}

std::vector<std::string> get_args(int argc, char** argv)
{
    std::vector<std::string> args;

    for (int i = 1; i < argc; ++i)
        args.push_back(argv[i]);

    return args;
}

void show_usage(std::string const& progname)
{
    std::cerr << "Usage: " << progname << " <command>" << std::endl;
    std::cerr << "Available commands: " << std::endl;
    std::cerr << "  display [on]: keep display on until program is terminated" << std::endl;
    std::cerr << "  active: inhibit device suspend until program is terminated" << std::endl;
    std::cerr << "  listsysrequests: list external suspend inhibitors" << std::endl;
    std::cerr << "  get double-tap-to-wake-enabled" << std::endl;
    std::cerr << "  get double-tap-to-wake-supported" << std::endl;
    std::cerr << "  set double-tap-to-wake-enabled <true/false>" << std::endl;
    std::cerr << "  settings inactivity <power_action> <power_supply> <timeout>" << std::endl;
    std::cerr << "  settings lid <power_action> <power-supply>" << std::endl;
    std::cerr << "  settings critical-power <power_action>" << std::endl;
    std::cerr << std::endl;
    std::cerr << "<power_action>: none, display-off, suspend, power-off" << std::endl;
    std::cerr << "<power_supply>: battery, line-power" << std::endl;
}

std::unique_ptr<GDBusProxy,void(*)(void*)> create_unity_screen_proxy()
{
    repowerd::ScopedGError error;

    auto uscreen_proxy = g_dbus_proxy_new_for_bus_sync(
        G_BUS_TYPE_SYSTEM,
        G_DBUS_PROXY_FLAGS_NONE,
        NULL,
        "com.canonical.Unity.Screen",
        "/com/canonical/Unity/Screen",
        "com.canonical.Unity.Screen",
        NULL,
        error);

    if (uscreen_proxy == nullptr)
    {
        throw std::runtime_error(
            "Failed to connect to com.canonical.Unity.Screen: " + error.message_str());
    }

    return {uscreen_proxy, g_object_unref};
}

std::unique_ptr<GDBusProxy,void(*)(void*)> create_repowerd_proxy()
{
    repowerd::ScopedGError error;

    auto repowerd_proxy = g_dbus_proxy_new_for_bus_sync(
        G_BUS_TYPE_SYSTEM,
        G_DBUS_PROXY_FLAGS_NONE,
        NULL,
        "com.lomiri.Repowerd",
        "/com/lomiri/Repowerd",
        "com.lomiri.Repowerd",
        NULL,
        error);

    if (repowerd_proxy == nullptr)
    {
        throw std::runtime_error(
            "Failed to connect to com.lomiri.Repowerd: " + error.message_str());
    }

    return {repowerd_proxy, g_object_unref};
}

std::unique_ptr<GDBusProxy,void(*)(void*)> create_repowerd_settings_proxy()
{
    repowerd::ScopedGError error;

    auto repowerd_settings_proxy = g_dbus_proxy_new_for_bus_sync(
        G_BUS_TYPE_SYSTEM,
        G_DBUS_PROXY_FLAGS_NONE,
        NULL,
        "com.lomiri.Repowerd.Settings",
        "/com/lomiri/Repowerd/Settings",
        "com.lomiri.Repowerd.Settings",
        NULL,
        error);

    if (repowerd_settings_proxy == nullptr)
    {
        throw std::runtime_error(
            "Failed to connect to com.lomiri.Repowerd.Settings: " + error.message_str());
    }

    return {repowerd_settings_proxy, g_object_unref};
}

int32_t keep_display_on(GDBusProxy* uscreen_proxy)
{
    repowerd::ScopedGError error;

    auto const ret = g_dbus_proxy_call_sync(
        uscreen_proxy,
        "keepDisplayOn",
        NULL,
        G_DBUS_CALL_FLAGS_NONE,
        -1,
        NULL,
        error);

    if (ret == nullptr)
    {
        throw std::runtime_error(
            "com.canonical.Unity.Screen.keepDisplayOn() failed: " + error.message_str());
    }

    int32_t cookie{0};
    g_variant_get(ret, "(i)", &cookie);
    g_variant_unref(ret);

    return cookie;
}

void remove_display_on_request(GDBusProxy* uscreen_proxy, int32_t cookie)
{
    repowerd::ScopedGError error;

    auto const ret = g_dbus_proxy_call_sync(
        uscreen_proxy,
        "removeDisplayOnRequest",
        g_variant_new("(i)", cookie),
        G_DBUS_CALL_FLAGS_NONE,
        -1,
        NULL,
        error);

    if (ret == nullptr)
    {
        throw std::runtime_error(
            "com.canonical.Unity.Screen.removeDisplayOnRequest() failed: " + error.message_str());
    }

    g_variant_unref(ret);
}

std::string request_sys_state_active(GDBusProxy* repowerd_proxy)
{
    static int constexpr active_state = 1;
    repowerd::ScopedGError error;

    auto const ret = g_dbus_proxy_call_sync(
        repowerd_proxy,
        "requestSysState",
        g_variant_new("(si)", "repowerd-cli", active_state),
        G_DBUS_CALL_FLAGS_NONE,
        -1,
        NULL,
        error);

    if (ret == nullptr)
    {
        throw std::runtime_error(
            "com.lomiri.Repowerd.requestSysState(active) failed: " + error.message_str());
    }

    char const* cookie_raw{""};
    g_variant_get(ret, "(&s)", &cookie_raw);
    std::string cookie{cookie_raw};
    g_variant_unref(ret);

    return cookie;
}

void clear_sys_state(GDBusProxy* repowerd_proxy, std::string const& cookie)
{
    repowerd::ScopedGError error;

    auto const ret = g_dbus_proxy_call_sync(
        repowerd_proxy,
        "clearSysState",
        g_variant_new("(s)", cookie.c_str()),
        G_DBUS_CALL_FLAGS_NONE,
        -1,
        NULL,
        error);

    if (ret == nullptr)
    {
        throw std::runtime_error(
            "com.lomiri.Repowerd.clearSysState() failed: " + error.message_str());
    }

    g_variant_unref(ret);
}

void set_inactivity_behavior(
    GDBusProxy* repowerd_settings_proxy,
    std::string const& action,
    std::string const& supply,
    int32_t timeout)
{
    repowerd::ScopedGError error;

    auto const ret = g_dbus_proxy_call_sync(
        repowerd_settings_proxy,
        "SetInactivityBehavior",
        g_variant_new("(ssi)", action.c_str(), supply.c_str(), timeout),
        G_DBUS_CALL_FLAGS_NONE,
        -1,
        NULL,
        error);

    if (ret == nullptr)
    {
        throw std::runtime_error(
            "com.lomiri.Repowerd.Settings.SetInactivityBehavior() failed: " + error.message_str());
    }

    g_variant_unref(ret);
}

void set_lid_behavior(
    GDBusProxy* repowerd_settings_proxy,
    std::string const& action,
    std::string const& supply)
{
    repowerd::ScopedGError error;

    auto const ret = g_dbus_proxy_call_sync(
        repowerd_settings_proxy,
        "SetLidBehavior",
        g_variant_new("(ss)", action.c_str(), supply.c_str()),
        G_DBUS_CALL_FLAGS_NONE,
        -1,
        NULL,
        error);

    if (ret == nullptr)
    {
        throw std::runtime_error(
            "com.lomiri.Repowerd.Settings.SetLidBehavior() failed: " + error.message_str());
    }

    g_variant_unref(ret);
}

void set_critical_power_behavior(
    GDBusProxy* repowerd_settings_proxy,
    std::string const& action)
{
    repowerd::ScopedGError error;

    auto const ret = g_dbus_proxy_call_sync(
        repowerd_settings_proxy,
        "SetCriticalPowerBehavior",
        g_variant_new("(s)", action.c_str()),
        G_DBUS_CALL_FLAGS_NONE,
        -1,
        NULL,
        error);

    if (ret == nullptr)
    {
        throw std::runtime_error(
            "com.lomiri.Repowerd.Settings.SetCriticalPowerBehavior() failed: " + error.message_str());
    }

    g_variant_unref(ret);
}

bool get_double_tap_to_wake_enabled(GDBusProxy* uscreen_proxy)
{
    repowerd::ScopedGError error;

    gboolean enabled = FALSE;
    auto ret = g_dbus_proxy_call_sync(
        uscreen_proxy,
        "getDoubleTapToWakeEnabled",
        NULL,
        G_DBUS_CALL_FLAGS_NONE,
        -1,
        NULL,
        error);

    if (ret == nullptr)
    {
        throw std::runtime_error(
            "com.canonical.Unity.Screen.getDoubleTapToWakeEnabled() failed: " + error.message_str());
    }

    ret = g_variant_get_child_value(ret, 0);
    enabled = g_variant_get_boolean(ret);

    g_variant_unref(ret);
    return enabled == TRUE;
}

bool get_double_tap_to_wake_supported(GDBusProxy* uscreen_proxy)
{
    repowerd::ScopedGError error;

    gboolean enabled = FALSE;
    auto ret = g_dbus_proxy_call_sync(
        uscreen_proxy,
        "getDoubleTapToWakeSupported",
        NULL,
        G_DBUS_CALL_FLAGS_NONE,
        -1,
        NULL,
        error);

    if (ret == nullptr)
    {
        throw std::runtime_error(
            "com.canonical.Unity.Screen.getDoubleTapToWakeSupported() failed: " + error.message_str());
    }

    ret = g_variant_get_child_value(ret, 0);
    enabled = g_variant_get_boolean(ret);

    g_variant_unref(ret);
    return enabled == TRUE;
}

void set_double_tap_to_wake_enabled(
    GDBusProxy* uscreen_proxy,
    bool const enable)
{
    repowerd::ScopedGError error;

    auto const ret = g_dbus_proxy_call_sync(
        uscreen_proxy,
        "setDoubleTapToWakeEnabled",
        g_variant_new("(b)", enable),
        G_DBUS_CALL_FLAGS_NONE,
        -1,
        NULL,
        error);

    if (ret == nullptr)
    {
        throw std::runtime_error(
            "com.canonical.Unity.Screen.setDoubleTapToWakeEnabled() failed: " + error.message_str());
    }

    g_variant_unref(ret);
}

void handle_display_command(GDBusProxy* uscreen_proxy)
{
    auto const cookie = keep_display_on(uscreen_proxy);

    std::cout << "keepDisplayOn requested, cookie is " << cookie << std::endl;
    std::cout << "Press ctrl-c to exit" << std::endl;

    pause();

    remove_display_on_request(uscreen_proxy, cookie);
}

void handle_active_command(GDBusProxy* uscreen_proxy)
{
    auto const cookie = request_sys_state_active(uscreen_proxy);

    std::cout << "requestSysState(active) requested, cookie is " << cookie << std::endl;
    std::cout << "Press ctrl-c to exit" << std::endl;

    pause();

    clear_sys_state(uscreen_proxy, cookie);
}

void handle_listsysrequests_command(GDBusProxy* repowerd_proxy)
{
    repowerd::ScopedGError error;
    auto const ret = g_dbus_proxy_call_sync(
        repowerd_proxy,
        "listSysRequests",
        NULL,
        G_DBUS_CALL_FLAGS_NONE,
        -1,
        NULL,
        error);

    if (ret == nullptr)
    {
        throw std::runtime_error(
            "com.lomiri.Repowerd.listSysRequests() failed: " + error.message_str());
    }

    auto dict = g_variant_get_child_value(ret, 0);
    auto dict_size = g_variant_n_children(dict);
    for(gsize i = 0; i < dict_size; i++){
        auto dict_entry = g_variant_get_child_value(dict, i);
        auto dict_key = g_variant_get_child_value(dict_entry, 0);
        auto dict_value_boxed = g_variant_get_child_value(dict_entry, 1);
        auto dict_value = g_variant_get_child_value(dict_value_boxed, 0);
        auto dict_key_str = std::string(g_variant_get_string(dict_key, NULL));

        if(dict_key_str == "request_sys_state"){
            std::cout << "Listing currently active sys requests (suspend block request):" << std::endl;
            auto list_size = g_variant_n_children(dict_value);
            for(gsize j = 0;j < list_size; j++){
                auto child = g_variant_get_child_value(dict_value, j);
                char const* sender;
                int32_t id;
                uint64_t pid;
                char const* name;
                g_variant_get(child, "(&sit&s)", &sender, &id, &pid, &name);
                std::cout << "sender: " << sender;
                std::cout << " | sender pid: " << pid;
                std::cout << " | id/cookie: " << id;
                std::cout << " | name: " << name << std::endl;
                g_variant_unref(child);
            }
            std::cout << std::endl;
        }else if (dict_key_str == "keep_display_on"){
            std::cout << "Listing currently active keep display on requests:" << std::endl;
            auto list_size = g_variant_n_children(dict_value);
            for(gsize j = 0;j < list_size; j++){
                auto child = g_variant_get_child_value(dict_value, j);
                char const* sender;
                int32_t id;
                uint64_t pid;
                g_variant_get(child, "(&sit)", &sender, &id, &pid);
                std::cout << "sender: " << sender;
                std::cout << " | sender pid: " << pid;
                std::cout << " | id/cookie: " << id << std::endl;
                g_variant_unref(child);
            }
            std::cout << std::endl;
        }else if (dict_key_str == "active_notifications"){
            std::cout << "Listing currently active notification requests:" << std::endl;
            auto list_size = g_variant_n_children(dict_value);
            for(gsize j = 0;j < list_size; j++){
                auto child = g_variant_get_child_value(dict_value, j);
                char const* sender;
                uint64_t pid;
                int32_t reason;
                g_variant_get(child, "(&sti)", &sender, &pid, &reason);
                std::cout << "sender: " << sender;
                std::cout << " | sender pid: " << pid;
                std::cout << " | reason: " << reason << std::endl;
                g_variant_unref(child);
            }
            std::cout << std::endl;
        }else{
            std::cout << "Unknown dictionary key " << dict_key_str << std::endl << std::endl;
        }

        g_variant_unref(dict_key);
        g_variant_unref(dict_value);
        g_variant_unref(dict_value_boxed);
        g_variant_unref(dict_entry);
    }
    g_variant_unref(dict);
    g_variant_unref(ret);
}

void handle_set_command(
    GDBusProxy* uscreen_proxy,
    std::vector<std::string> const& args)
{
    if (args.size() < 2)
        throw std::invalid_argument{""};

    if (args[1] == "double-tap-to-wake-enabled")
    {
        if (args.size() != 3)
            throw std::invalid_argument{""};

        auto const enable = (args[2] == "true" || args[2] == "1");
        set_double_tap_to_wake_enabled(uscreen_proxy, enable);
    }
    else
    {
        throw std::invalid_argument{""};
    }
}

void handle_get_command(
    GDBusProxy* uscreen_proxy,
    std::vector<std::string> const& args)
{
    if (args.size() < 2)
        throw std::invalid_argument{""};

    if (args[1] == "double-tap-to-wake-enabled")
    {
        auto enabled = get_double_tap_to_wake_enabled(uscreen_proxy);
        std::cerr << "double-tap-to-wake-enabled: "<< (enabled ? "true" : "false") << std::endl;
    }
    else if (args[1] == "double-tap-to-wake-supported")
    {
        auto supported = get_double_tap_to_wake_supported(uscreen_proxy);
        std::cerr << "double-tap-to-wake-supported: "<< (supported ? "true" : "false") << std::endl;
    }
    else
    {
        throw std::invalid_argument{""};
    }
}

void handle_settings_command(
    GDBusProxy* repowerd_settings_proxy,
    std::vector<std::string> const& args)
{
    if (args.size() < 2)
        throw std::invalid_argument{""};

    if (args[1] == "inactivity")
    {
        if (args.size() != 5)
            throw std::invalid_argument{""};

        auto const& action = args[2];
        auto const& supply = args[3];
        auto const timeout = std::stoi(args[4]);

        set_inactivity_behavior(repowerd_settings_proxy, action, supply, timeout);
    }
    else if (args[1] == "lid")
    {
        if (args.size() != 4)
            throw std::invalid_argument{""};

        auto const& action = args[2];
        auto const& supply = args[3];

        set_lid_behavior(repowerd_settings_proxy, action, supply);
    }
    else if (args[1] == "critical-power")
    {
        if (args.size() != 3)
            throw std::invalid_argument{""};

        auto const& action = args[2];

        set_critical_power_behavior(repowerd_settings_proxy, action);
    }
    else
    {
        throw std::invalid_argument{""};
    }
}

void null_signal_handler(int) {}

int main(int argc, char** argv)
try
{
    signal(SIGINT, null_signal_handler);
    signal(SIGTERM, null_signal_handler);

    auto const args = get_args(argc, argv);

    if (args.size() == 0)
        throw std::invalid_argument{""};

    auto const uscreen_proxy = create_unity_screen_proxy();
    auto const repowerd_proxy = create_repowerd_proxy();
    auto const repowerd_settings_proxy = create_repowerd_settings_proxy();

    if (args[0] == "display")
    {
        handle_display_command(uscreen_proxy.get());
    }
    else if (args[0] == "active")
    {
        handle_active_command(repowerd_proxy.get());
    }
    else if (args[0] == "listsysrequests")
    {
        handle_listsysrequests_command(repowerd_proxy.get());
    }
    else if (args[0] == "settings")
    {
        handle_settings_command(repowerd_settings_proxy.get(), args);
    }
    else if (args[0] == "get")
    {
        handle_get_command(uscreen_proxy.get(), args);
    }
    else if (args[0] == "set")
    {
        handle_set_command(uscreen_proxy.get(), args);
    }
    else
    {
        throw std::invalid_argument{""};
    }
}
catch (std::invalid_argument const& e)
{
    show_usage(get_progname(argc, argv));
    return -1;
}
catch (std::exception const& e)
{
    std::cerr << e.what() << std::endl;
    return -1;
}
