/*
 *  $Id: shortcut-editor.c 30074 2026-06-16 14:55:44Z yeti-dn $
 *  Copyright (C) 2026 David Necas (Yeti).
 *  E-mail: yeti@gwyddion.net.
 *
 *  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, write to the
 *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "config.h"
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <glib/gi18n.h>
#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>

#include "libgwyddion/macros.h"
#include "libgwyui/icons.h"
#include "libgwyui/utils.h"
#include "libgwyui/combo-boxes.h"
#include "libgwyapp/gwyapp.h"
#include "libgwyapp/module-utils.h"
#include "libgwyapp/sanity.h"

#include "gwyddion/gwyddion.h"
#include "gwyddion/toolbox.h"

typedef struct {
    GtkWidget *dialogue;
    GtkWidget *type_combo;
    GtkTreeView *function_list;
    GwyAppActionType type;
    const gchar *function;
    GtkWidget *accel_key_label;
    GtkWidget *accel_key;
    guint accel_symbol;
    GdkModifierType accel_modif;
    GtkWidget *set_shortcut;
    GtkWidget *clear_shortcut;
    GtkWidget *detail;
} GwyShortcutEditor;

typedef struct {
    GwyShortcutEditor *editor;
    GtkDialog *dialog;
    GtkWidget *ok;
    GtkWidget *cancel;
    GtkWidget *big_label;
    GtkWidget *warning;
    const gchar *overwriting_accel_path;
    GtkAccelKey key;
    gulong key_press_id;
    gboolean can_set_shortcut;
} KeyboardShortcutDialog;

static void function_type_changed        (GtkComboBox *combo,
                                          GwyShortcutEditor *editor);
static void function_selection_changed   (GwyShortcutEditor *editor);
static void set_keyboard_shortcut        (GwyShortcutEditor *editor);
static void clear_keyboard_shortcut      (GwyShortcutEditor *editor);
static void update_keyboard_shortcut_info(GwyShortcutEditor *editor);
static void scan_shortcuts               (gpointer user_data,
                                          const gchar *accel_path,
                                          guint accel_key,
                                          GdkModifierType accel_mods,
                                          gboolean changed);

void
gwy_shortcut_editor(void)
{
    GwyShortcutEditor editor;
    gwy_clear1(editor);
    editor.type = GWY_APP_ACTION_TYPE_IMAGE;

    GwyDict *settings = gwy_app_settings_get();
    const gchar *typestr = "image";
    if (gwy_dict_gis_string_by_name(settings, "/app/shortcut-editor/type", &typestr)) {
        GwyAppActionType maybetype = gwy_toolbox_find_action_type(typestr);
        if (maybetype >= 0
            && maybetype != GWY_APP_ACTION_TYPE_PLACEHOLDER
            && maybetype != GWY_APP_ACTION_TYPE_BUILTIN)
            editor.type = maybetype;
        else
            typestr = "image";
    }

    GtkWindow *toolbox = GTK_WINDOW(gwy_app_main_window());

    editor.dialogue = gtk_dialog_new_with_buttons(_("Keyboard Shortcuts"), toolbox, GTK_DIALOG_MODAL, NULL, NULL);
    gwy_stock_add_button_to_dialog(GTK_DIALOG(editor.dialogue), GWY_STOCK_CLOSE);
    gtk_window_set_default_size(GTK_WINDOW(editor.dialogue), 560, 480);

    GtkBox *vbox = GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(editor.dialogue)));
    GtkBox *hbox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6));
    gtk_container_set_border_width(GTK_CONTAINER(hbox), 4);
    gtk_box_pack_start(vbox, GTK_WIDGET(hbox), TRUE, TRUE, 0);

    editor.function_list = GTK_TREE_VIEW(gwy_function_list_new(typestr));
    GtkWidget *scwin = gtk_scrolled_window_new(NULL, NULL);
    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scwin), GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS);
    gtk_container_add(GTK_CONTAINER(scwin), GTK_WIDGET(editor.function_list));
    gtk_box_pack_start(hbox, GTK_WIDGET(scwin), TRUE, TRUE, 0);

    GtkGrid *grid = GTK_GRID(gtk_grid_new());
    gtk_grid_set_row_spacing(grid, 8);
    gtk_grid_set_column_spacing(grid, 6);
    gtk_box_pack_start(hbox, GTK_WIDGET(grid), FALSE, FALSE, 0);

    GtkWidget *label = gtk_label_new_with_mnemonic(_("Function _type:"));
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_grid_attach(grid, label, 0, 0, 1, 1);

    editor.type_combo = gwy_enum_combo_box_newl(G_CALLBACK(function_type_changed), &editor, editor.type,
                                                _("Image"), GWY_APP_ACTION_TYPE_IMAGE,
                                                _("Graph"), GWY_APP_ACTION_TYPE_GRAPH,
                                                _("Synthetic"), GWY_APP_ACTION_TYPE_SYNTH,
                                                _("Volume Data"), GWY_APP_ACTION_TYPE_VOLUME,
                                                _("XYZ Data"), GWY_APP_ACTION_TYPE_XYZ,
                                                _("Curve Maps"), GWY_APP_ACTION_TYPE_CMAP,
                                                _("Spectra"), GWY_APP_ACTION_TYPE_SPS,
                                                _("Tool"), GWY_APP_ACTION_TYPE_TOOL,
                                                NULL);
    gtk_widget_set_halign(editor.type_combo, GTK_ALIGN_FILL);
    gtk_grid_attach_next_to(grid, editor.type_combo, label, GTK_POS_RIGHT, 1, 1);

    editor.detail = gtk_label_new(NULL);
    gtk_label_set_xalign(GTK_LABEL(editor.detail), 0.0);
    gtk_label_set_ellipsize(GTK_LABEL(editor.detail), PANGO_ELLIPSIZE_END);

    GtkWidget *shrink = gwy_shrink_wrap_new(GTK_SIZE_GROUP_HORIZONTAL);
    gtk_container_add(GTK_CONTAINER(shrink), editor.detail);
    gtk_grid_attach(grid, shrink, 0, 1, 2, 1);

    label = editor.accel_key_label = gtk_label_new_with_mnemonic(_("_Keyboard shortcut:"));
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_grid_attach(grid, label, 0, 2, 1, 1);

    editor.accel_key = gtk_label_new(NULL);
    gtk_label_set_xalign(GTK_LABEL(editor.accel_key), 0.0);
    gtk_grid_attach_next_to(grid, editor.accel_key, label, GTK_POS_RIGHT, 1, 1);

    GtkWidget *buttonbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_box_set_homogeneous(GTK_BOX(buttonbox), TRUE);
    gtk_grid_attach(grid, buttonbox, 0, 3, 2, 1);

    editor.set_shortcut = gtk_button_new_with_mnemonic(_("_Set Shortcut"));
    gtk_box_pack_start(GTK_BOX(buttonbox), editor.set_shortcut, TRUE, TRUE, 0);
    g_signal_connect_swapped(editor.set_shortcut, "clicked", G_CALLBACK(set_keyboard_shortcut), &editor);
    editor.clear_shortcut = gtk_button_new_with_mnemonic(_("Cle_ar Shortcut"));
    gtk_box_pack_start(GTK_BOX(buttonbox), editor.clear_shortcut, TRUE, TRUE, 0);
    g_signal_connect_swapped(editor.clear_shortcut, "clicked", G_CALLBACK(clear_keyboard_shortcut), &editor);

    g_signal_connect(editor.type_combo, "changed", G_CALLBACK(function_type_changed), &editor);
    GtkTreeSelection *selection = gtk_tree_view_get_selection(editor.function_list);
    g_signal_connect_swapped(selection, "changed", G_CALLBACK(function_selection_changed), &editor);

    gtk_widget_show_all(editor.dialogue);

    gint response = gtk_dialog_run(GTK_DIALOG(editor.dialogue));
    if (response != GTK_RESPONSE_NONE)
        gtk_widget_destroy(editor.dialogue);

    typestr = gwy_toolbox_action_type_name(editor.type);
    gwy_dict_set_string_by_name(settings, "/app/shortcut-editor/type", typestr);
}

static void
function_type_changed(GtkComboBox *combo, GwyShortcutEditor *editor)
{
    GwyAppActionType type = gwy_enum_combo_box_get_active(combo);
    editor->type = type;
    gwy_function_list_set_kind(GWY_FUNCTION_LIST(editor->function_list), gwy_toolbox_action_type_name(type));
}

static void
function_selection_changed(GwyShortcutEditor *editor)
{
    editor->function = gwy_function_list_get_selected(GWY_FUNCTION_LIST(editor->function_list));

    /* Update function information. */
    const gchar *detail = gwy_toolbox_action_detail(editor->type, editor->function);
    gtk_label_set_text(GTK_LABEL(editor->detail), detail ? detail : "");
    update_keyboard_shortcut_info(editor);
}

static void
scan_shortcuts(gpointer user_data,
               const gchar *accel_path,
               guint accel_key,
               GdkModifierType accel_mods,
               G_GNUC_UNUSED gboolean changed)
{
    KeyboardShortcutDialog *ksd = (KeyboardShortcutDialog*)user_data;
    if (!accel_key || accel_key == GDK_KEY_VoidSymbol)
        return;

    if (ksd->key.accel_key == accel_key && ksd->key.accel_mods == accel_mods)
        ksd->overwriting_accel_path = accel_path;
}

static gboolean
shortcut_key_pressed(KeyboardShortcutDialog *ksd, GdkEventKey *event)
{
    if (event->is_modifier || event->keyval == GDK_KEY_Escape)
        return FALSE;

    GdkModifierType mod_mask = gtk_accelerator_get_default_mod_mask();
    GdkDisplay *display = gdk_window_get_display(gdk_event_get_window((GdkEvent*)event));
    GdkKeymap *keymap = gdk_keymap_get_for_display(display);
    GdkModifierType consumed = 0, modifiers = event->state;
    guint keyval = 0;
    gdk_keymap_translate_keyboard_state(keymap, event->hardware_keycode,
                                        modifiers, event->group,
                                        &keyval, NULL, NULL, &consumed);

    // Translation tends to eat Shift, but we want to distinguish e.g. Ctrl and Ctrl+Shift. Put it back.
    if ((modifiers & GDK_SHIFT_MASK) && (consumed & GDK_SHIFT_MASK))
        consumed &= ~GDK_SHIFT_MASK;

    modifiers &= ~consumed & mod_mask;
    ksd->key.accel_key = keyval;
    ksd->key.accel_mods = modifiers;

    gchar *label = gtk_accelerator_get_label(keyval, modifiers);
    gchar *escaped = g_markup_escape_text(label, -1);
    g_free(label);
    gchar *markup = g_strconcat("<span size='large' weight='bold'>", escaped, "</span>", NULL);
    g_free(escaped);
    gtk_label_set_markup(GTK_LABEL(ksd->big_label), markup);
    g_free(markup);

    ksd->can_set_shortcut = TRUE;
    gboolean judged = FALSE;

    if (!judged) {
        static const GtkAccelKey hardcoded_keys[] = {
            // Help
            { GDK_KEY_F1,     0,                                 1, },
            // Show tool
            { GDK_KEY_F3,     0,                                 0, },
            // Selection
            { GDK_KEY_Up,     0,                                 1, },
            { GDK_KEY_Down,   0,                                 1, },
            { GDK_KEY_Left,   0,                                 1, },
            { GDK_KEY_Right,  0,                                 1, },
            // Zoom
            { GDK_KEY_plus,   0,                                 0, },
            { GDK_KEY_minus,  0,                                 0, },
            { GDK_KEY_equal,  0,                                 0, },
            { GDK_KEY_z,      0,                                 0, },
            { GDK_KEY_x,      0,                                 0, },
            // Run-last
            { GDK_KEY_f,      GDK_CONTROL_MASK,                  0, },
            { GDK_KEY_f,      GDK_CONTROL_MASK | GDK_SHIFT_MASK, 0, },
            // General weirdness
            { GDK_KEY_Escape, 0,                                 0, },
            { GDK_KEY_Return, 0,                                 0, },
        };
        for (guint i = 0; i < G_N_ELEMENTS(hardcoded_keys); i++) {
            if (keyval == hardcoded_keys[i].accel_key && (hardcoded_keys[i].accel_flags
                                                          || modifiers == hardcoded_keys[i].accel_mods)) {
                gwy_set_message_label(GTK_LABEL(ksd->warning), _("Key is assigned to a builtin function."),
                                      GTK_MESSAGE_ERROR, FALSE);
                ksd->can_set_shortcut = FALSE;
                judged = TRUE;
                break;
            }
        }
    }
    if (!judged) {
        gtk_accel_map_foreach(ksd, scan_shortcuts);
        if (ksd->overwriting_accel_path) {
            GtkAccelKey key;
            gtk_accel_map_lookup_entry(ksd->overwriting_accel_path, &key);
            if (key.accel_flags & GTK_ACCEL_LOCKED) {
                gwy_set_message_label(GTK_LABEL(ksd->warning), _("Key is assigned to a builtin function."),
                                      GTK_MESSAGE_ERROR, FALSE);
                ksd->can_set_shortcut = FALSE;
            }
            else {
                gwy_set_message_label(GTK_LABEL(ksd->warning), _("Key is assigned to a function."),
                                      GTK_MESSAGE_WARNING, FALSE);
            }
            judged = TRUE;
        }
    }
    if (!judged) {
        if (g_utf8_strlen(event->string, -1) == 1 && g_unichar_isprint(g_utf8_get_char(event->string))) {
            gwy_set_message_label(GTK_LABEL(ksd->warning), _("Key looks like a regular character."),
                                  GTK_MESSAGE_WARNING, FALSE);
            judged = TRUE;
        }
    }

    g_signal_handler_disconnect(ksd->dialog, ksd->key_press_id);
    if (ksd->can_set_shortcut) {
        gtk_dialog_set_response_sensitive(GTK_DIALOG(ksd->dialog), GTK_RESPONSE_OK, TRUE);
        gtk_dialog_set_default_response(GTK_DIALOG(ksd->dialog), GTK_RESPONSE_OK);
        gtk_widget_grab_focus(ksd->ok);
    }

    return TRUE;
}

static void
set_keyboard_shortcut(GwyShortcutEditor *editor)
{
    g_return_if_fail(editor->function);

    KeyboardShortcutDialog ksd;
    gwy_clear1(ksd);
    ksd.editor = editor;

    GtkWidget *dialog = gtk_dialog_new();
    ksd.dialog = GTK_DIALOG(dialog);
    GtkBox *vbox = GTK_BOX(gtk_dialog_get_content_area(ksd.dialog));
    gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(editor->dialogue));
    gtk_window_set_title(GTK_WINDOW(dialog), _("Set Keyboard Shortcut"));

    // Do not give the buttons focus at the beinning.
    ksd.cancel = gwy_stock_add_button_to_dialog(ksd.dialog, GWY_STOCK_CANCEL);
    gtk_widget_set_can_focus(ksd.cancel, FALSE);
    ksd.ok = gwy_stock_add_button_to_dialog(ksd.dialog, GWY_STOCK_OK);
    gtk_widget_set_can_focus(ksd.ok, FALSE);
    gtk_widget_set_sensitive(ksd.ok, FALSE);

    const gchar *nicename = gwy_toolbox_action_nice_name(editor->type, editor->function);
    gchar *text = g_strdup_printf(_("Change keyboard shortcut for ‘%s’"), nicename);
    GtkWidget *what = gtk_label_new(text);
    g_free(text);
    gtk_label_set_xalign(GTK_LABEL(what), 0.0);
    gtk_box_pack_start(vbox, what, FALSE, FALSE, 4);

    ksd.big_label = gtk_label_new(NULL);
    gtk_label_set_markup(GTK_LABEL(ksd.big_label), _("<span size='large' weight='bold'>Press a key</span>"));
    gtk_box_pack_start(vbox, ksd.big_label, TRUE, TRUE, 20);

    ksd.warning = gtk_label_new(NULL);
    gtk_label_set_xalign(GTK_LABEL(ksd.warning), 0.0);
    gtk_box_pack_start(vbox, ksd.warning, FALSE, FALSE, 4);

    ksd.key_press_id = g_signal_connect_swapped(dialog, "key-press-event", G_CALLBACK(shortcut_key_pressed), &ksd);

    gtk_widget_show_all(dialog);
    gint response = gwy_run_subdialog(ksd.dialog);
    if (response == GTK_RESPONSE_OK && ksd.can_set_shortcut) {
        // This is kind of weird because we do not wait for Apply. It is not actually toolbox editing.
        gchar *accel_path = gwy_toolbox_action_accel_path(editor->type, editor->function);
        gboolean ok = gtk_accel_map_change_entry(accel_path, ksd.key.accel_key, ksd.key.accel_mods, TRUE);
        if (ok)
            update_keyboard_shortcut_info(editor);
        else
            g_warning("Failed to change the map entry.");
    }
}

static void
clear_keyboard_shortcut(GwyShortcutEditor *editor)
{
    g_return_if_fail(editor->accel_symbol);
    g_return_if_fail(editor->function);
    const gchar *name = editor->function;
    gchar *accel_path = gwy_toolbox_action_accel_path(editor->type, name);
    gtk_accel_map_change_entry(accel_path, 0, 0, TRUE);
    g_free(accel_path);
    update_keyboard_shortcut_info(editor);
}

static void
update_keyboard_shortcut_info(GwyShortcutEditor *editor)
{
    const gchar *name = editor->function;

    editor->accel_symbol = 0;
    editor->accel_modif = 0;
    if (name) {
        gchar *accel_path = gwy_toolbox_action_accel_path(editor->type, name);
        GtkAccelKey key;
        // GTK+ seems to report GDK_KEY_VoidSymbol (which is 0xffffff), but also uses zero to mean no-key, for
        // instance for removal in gtk_accel_map_change_entry(). Consolidate to zero.
        if (gtk_accel_map_lookup_entry(accel_path, &key) && key.accel_key && key.accel_key != GDK_KEY_VoidSymbol) {
            editor->accel_symbol = key.accel_key;
            editor->accel_modif = key.accel_mods;
        }
        g_free(accel_path);
    }

    if (editor->accel_symbol) {
        gchar *label = gtk_accelerator_get_label(editor->accel_symbol, editor->accel_modif);
        gtk_label_set_text(GTK_LABEL(editor->accel_key), label);
        g_free(label);
    }
    else {
        gtk_label_set_markup(GTK_LABEL(editor->accel_key), _("<i>none</i>"));
    }

    gtk_widget_set_sensitive(editor->accel_key_label, !!name);
    gtk_widget_set_sensitive(editor->accel_key, !!name);
    gtk_widget_set_sensitive(editor->set_shortcut, !!name);
    gtk_widget_set_sensitive(editor->clear_shortcut, !!editor->accel_symbol);
}

/* vim: set cin columns=120 tw=118 et ts=4 sw=4 cino=>1s,e0,n0,f0,{0,}0,^0,\:1s,=0,g1s,h0,t0,+1s,c3,(0,u0 : */
