// SPDX-FileCopyrightText: 2024 KylinSoft Co., Ltd.
//
// SPDX-License-Identifier: Expat

#include <gio/gio.h>

#include "config_p.h"
#include "effect/effect.h"
#include "input/input.h"
#include "server.h"
#include "theme.h"
#include "util/color.h"
#include "util/dbus.h"
#include "util/macros.h"

struct ukui_settings {
    struct {
        GSettings *settings;
        char *theme;
        int size;
    } cursor;

    struct {
        GSettings *settings;
        char *font_name;
        char *font_size;
        enum theme_type type;
        char *widget_theme;
        bool window_radius_is_string;
    } style;

    struct {
        GSettings *settings;
        char *picture_file;
        int picture_options;
        int primary_color;
    } background;

    struct wl_listener destroy;
};

#define UKUI_THEME_LIGHT "ukui-light"
#define UKUI_THEME_DARK "ukui-dark"

static const char *cursor_path = "/org/ukui/desktop/peripherals/mouse/";
#define CURSOR_PATH_LEN (36)
static const char *style_path = "/org/ukui/style/";
#define STYLE_PATH_LEN (16)
static const char *background_path = "/org/mate/desktop/background/";
#define BACKGROUND_PATH_LEN (29)

static const char *cursor_schema = "org.ukui.peripherals-mouse";
static const char *cursor_theme_key = "cursor-theme";
static const char *cursor_size_key = "cursor-size";
static const char *locate_pointer_key = "locate-pointer";
static const char *shake_cursor_key = "shake-cursor";

static const char *style_schema = "org.ukui.style";
static const char *style_name_key = "style-name";
static const char *icon_theme_key = "icon-theme-name";
static const char *widget_theme_key = "widget-theme-name";
static const char *font_name_key = "system-font";
static const char *font_size_key = "system-font-size";
static const char *accent_color_key = "theme-color";
static const char *window_radius_key = "window-radius";
static const char *menu_transparency_key = "menu-transparency";

static const char *background_schema = "org.mate.background";
static const char *background_picture_key = "picture-filename";
static const char *background_options_key = "picture-options";
static const char *background_color_key = "primary-color";

static const struct ukui_accent_color {
    char *name;
    int32_t color;
} ukui_accent_colors[] = {
    { "daybreakBlue", 0x3790FA }, { "jamPurple", 0x722ED1 },    { "magenta", 0xEB3096 },
    { "sunRed", 0xF3222D },       { "sunsetOrange", 0xF68C27 }, { "dustGold", 0xF9C53D },
    { "polarGreen", 0x52C429 },
};

static const struct {
    char *name;
    enum background_option option;
} background_options[] = {
    { "scaled", BACKGROUND_OPTION_SCALED },     { "wallpaper", BACKGROUND_OPTION_WALLPAPER },
    { "centered", BACKGROUND_OPTION_CENTERED }, { "stretched", BACKGROUND_OPTION_STRETCHED },
    { "zoom", BACKGROUND_OPTION_ZOOM },         { "spanned", BACKGROUND_OPTION_SPANNED },
};

static struct ukui_settings *settings = NULL;

static GSettingsSchema *is_schema_installed(const char *schema_id)
{
    GSettingsSchemaSource *source = g_settings_schema_source_get_default();
    return g_settings_schema_source_lookup(source, schema_id, TRUE);
}

static void handle_cursor_settings_changed(GSettingsSchema *schema, GSettings *mouse,
                                           const char *key)
{
    if (strcmp(key, locate_pointer_key) == 0) {
        bool enabled = g_settings_get_boolean(mouse, key);
        struct effect *effect = effect_by_name("locate_pointer");
        if (effect) {
            effect_set_enabled(effect, enabled);
        }
        return;
    } else if (strcmp(key, shake_cursor_key) == 0) {
        bool enabled = g_settings_get_boolean(mouse, key);
        struct effect *effect = effect_by_name("shake_cursor");
        if (effect) {
            effect_set_enabled(effect, enabled);
        }
        return;
    }

    if (strcmp(key, cursor_theme_key) == 0) {
        free(settings->cursor.theme);
        settings->cursor.theme = g_settings_get_string(mouse, key);
    } else if (strcmp(key, cursor_size_key) == 0) {
        settings->cursor.size = g_settings_get_int(mouse, key);
    } else {
        return;
    }

    if (settings->cursor.theme && settings->cursor.size > 0) {
        input_set_all_cursor(settings->cursor.theme, settings->cursor.size);
    }
}

static void style_name_changed(GSettings *style, const char *key)
{
    if (strcmp(widget_theme_key, key) == 0) {
        free(settings->style.widget_theme);
        settings->style.widget_theme = g_settings_get_string(style, key);
    } else {
        const char *style_name = g_settings_get_string(style, key);
        if (!strcmp(style_name, UKUI_THEME_LIGHT)) {
            settings->style.type = THEME_TYPE_LIGHT;
        } else if (!strcmp(style_name, UKUI_THEME_DARK)) {
            settings->style.type = THEME_TYPE_DARK;
        }
        free((void *)style_name);
    }

    if (settings->style.type != THEME_TYPE_UNDEFINED) {
        theme_manager_set_widget_theme(settings->style.widget_theme, settings->style.type);
    }
}

static void icon_theme_changed(GSettings *style, const char *key)
{
    const char *icon_theme = g_settings_get_string(style, key);
    theme_manager_set_icon_theme(icon_theme);
    free((void *)icon_theme);
}

static void font_style_changed(GSettings *style, const char *key)
{
    if (strcmp(key, font_name_key) == 0) {
        free(settings->style.font_name);
        settings->style.font_name = g_settings_get_string(style, key);
    } else {
        free(settings->style.font_size);
        settings->style.font_size = g_settings_get_string(style, key);
    }
    if (settings->style.font_name && settings->style.font_size) {
        theme_manager_set_font(settings->style.font_name, round(atof(settings->style.font_size)));
    }
}

static void accent_color_changed(GSettings *style, const char *key)
{
    const char *accent_color = g_settings_get_string(style, key);
    /* try '6,192,199,1' format first */
    struct color color;
    int num = sscanf(accent_color, "%hhu,%hhu,%hhu,%f", &color.r, &color.g, &color.b, &color.a);
    if (num == 4) {
        theme_manager_set_accent_color(color_to_uint24(&color));
        free((void *)accent_color);
        return;
    }
    /* fallback to old format */
    for (size_t i = 0; i < ARRAY_SIZE(ukui_accent_colors); i++) {
        if (strcmp(accent_color, ukui_accent_colors[i].name) == 0) {
            theme_manager_set_accent_color(ukui_accent_colors[i].color);
            break;
        }
    }
    free((void *)accent_color);
}

static void window_radius_changed(GSettings *style, const char *key)
{
    int window_radius = -1, menu_radius = -1;

    if (settings->style.window_radius_is_string) {
        const char *radius = g_settings_get_string(style, key);
        int num = sscanf(radius, "%d,%d", &window_radius, &menu_radius);
        free((void *)radius);
        if (num < 1 || window_radius < 0) {
            return;
        }
    } else {
        window_radius = g_settings_get_int(style, key);
    }

    theme_manager_set_corner_radius(window_radius, menu_radius);
}

static void menu_transparency_changed(GSettings *style, const char *key)
{
    int menu_transparency = g_settings_get_int(style, key);
    theme_manager_set_opacity(menu_transparency);
}

static void handle_style_settings_changed(GSettingsSchema *schema, GSettings *style,
                                          const char *key)
{
    if (strcmp(key, style_name_key) == 0 || (strcmp(key, widget_theme_key) == 0)) {
        style_name_changed(style, key);
    } else if (strcmp(key, icon_theme_key) == 0) {
        icon_theme_changed(style, key);
    } else if (strcmp(key, font_name_key) == 0 || strcmp(key, font_size_key) == 0) {
        font_style_changed(style, key);
    } else if (strcmp(key, accent_color_key) == 0) {
        accent_color_changed(style, key);
    } else if (strcmp(key, window_radius_key) == 0) {
        if (schema) {
            GSettingsSchemaKey *schema_key = g_settings_schema_get_key(schema, key);
            const GVariantType *value_type = g_settings_schema_key_get_value_type(schema_key);
            if (g_variant_type_equal(value_type, G_VARIANT_TYPE_STRING)) {
                settings->style.window_radius_is_string = true;
            }
            g_settings_schema_key_unref(schema_key);
        }
        window_radius_changed(style, key);
    } else if (strcmp(key, menu_transparency_key) == 0) {
        menu_transparency_changed(style, key);
    }
}

static int options_strings_to_option(const char *options)
{
    size_t num = sizeof(background_options) / sizeof(background_options[0]);
    for (size_t i = 0; i < num; i++) {
        if (strcmp(options, background_options[i].name) == 0) {
            return background_options[i].option;
        }
    }
    return BACKGROUND_OPTION_STRETCHED;
}

static void handle_background_settings_changed(GSettingsSchema *schema, GSettings *background,
                                               const char *key)
{
    if (strcmp(key, background_picture_key) == 0) {
        free(settings->background.picture_file);
        settings->background.picture_file = g_settings_get_string(background, key);
    } else if (strcmp(key, background_options_key) == 0) {
        const char *options = g_settings_get_string(background, key);
        settings->background.picture_options = options_strings_to_option(options);
        free((void *)options);
    } else if (strcmp(key, background_color_key) == 0) {
        const char *color = g_settings_get_string(background, key);
        sscanf(color + 1, "%x", &settings->background.primary_color);
        settings->background.primary_color &= 0xffffff;
        free((void *)color);
    } else {
        return;
    }

    bool has_picture = settings->background.picture_file && *settings->background.picture_file &&
                       settings->background.picture_options != 0;
    bool has_solid = settings->background.picture_file && !*settings->background.picture_file &&
                     settings->background.primary_color != 0;
    if (has_picture || has_solid) {
        theme_manager_set_background(has_picture ? settings->background.picture_file : NULL,
                                     settings->background.picture_options,
                                     settings->background.primary_color);
    }
}

static int dconf_notify(sd_bus_message *msg, void *userdata, sd_bus_error *ret_error)
{
    const char *prefix;
    CK(sd_bus_message_read_basic(msg, 's', &prefix));
    CK(sd_bus_message_enter_container(msg, 'a', "s"));

    const char *change;
    char key[128];
    int ret = 0;

    /* get current changed key */
    while (true) {
        ret = sd_bus_message_read(msg, "s", &change);
        if (ret < 0) {
            return ret;
        } else if (ret == 0) {
            break;
        }
        snprintf(key, 128, "%s%s", prefix, change);

        if (strncmp(key, style_path, STYLE_PATH_LEN) == 0) {
            handle_style_settings_changed(NULL, settings->style.settings, key + STYLE_PATH_LEN);
        } else if (strncmp(key, cursor_path, CURSOR_PATH_LEN) == 0) {
            handle_cursor_settings_changed(NULL, settings->cursor.settings, key + CURSOR_PATH_LEN);
        } else if (strncmp(key, background_path, BACKGROUND_PATH_LEN) == 0) {
            handle_background_settings_changed(NULL, settings->background.settings,
                                               key + BACKGROUND_PATH_LEN);
        }
    }

    return 0;
}

static GSettings *init_schema_settings(const char *schema_id,
                                       void (*handler)(GSettingsSchema *schema, GSettings *settings,
                                                       const char *key))
{
    GSettingsSchema *schema = is_schema_installed(schema_id);
    if (!schema) {
        return NULL;
    }

    GSettings *settings = g_settings_new(schema_id);
    if (!settings) {
        g_settings_schema_unref(schema);
        return NULL;
    }

    gchar **keys = g_settings_schema_list_keys(schema);
    for (int i = 0; keys && keys[i] != NULL; i++) {
        handler(schema, settings, keys[i]);
    }

    g_strfreev(keys);
    g_settings_schema_unref(schema);

    return settings;
}

static void handle_display_destroy(struct wl_listener *listener, void *data)
{
    wl_list_remove(&settings->destroy.link);

    g_object_unref(settings->cursor.settings);
    g_object_unref(settings->style.settings);
    g_object_unref(settings->background.settings);

    free(settings->cursor.theme);
    free(settings->style.font_name);
    free(settings->style.font_size);
    free(settings->style.widget_theme);
    free(settings->background.picture_file);

    free(settings);
}

bool ukui_gsettings_create(struct config_manager *config_manager)
{
    settings = calloc(1, sizeof(*settings));
    if (!settings) {
        return false;
    }

    settings->style.type = THEME_TYPE_UNDEFINED;
    settings->cursor.settings = init_schema_settings(cursor_schema, handle_cursor_settings_changed);
    settings->style.settings = init_schema_settings(style_schema, handle_style_settings_changed);

    if (!settings->cursor.settings && !settings->style.settings) {
        free(settings);
        settings = NULL;
        return false;
    }

    settings->background.settings =
        init_schema_settings(background_schema, handle_background_settings_changed);

    /* monitor dconf dbus notify */
    dbus_match_signal(NULL, "/ca/desrt/dconf/Writer/user", "ca.desrt.dconf.Writer", "Notify",
                      dconf_notify, NULL);

    settings->destroy.notify = handle_display_destroy;
    wl_display_add_destroy_listener(config_manager->server->display, &settings->destroy);

    return true;
}
