// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
//
// SPDX-License-Identifier: GPL-2.0-or-later

import Gdk from 'gi://Gdk';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Gtk from 'gi://Gtk';


/*
 * A list of modifier keysyms we ignore
 */
const _MODIFIERS = [
    Gdk.KEY_Alt_L,
    Gdk.KEY_Alt_R,
    Gdk.KEY_Caps_Lock,
    Gdk.KEY_Control_L,
    Gdk.KEY_Control_R,
    Gdk.KEY_Meta_L,
    Gdk.KEY_Meta_R,
    Gdk.KEY_Num_Lock,
    Gdk.KEY_Shift_L,
    Gdk.KEY_Shift_R,
    Gdk.KEY_Super_L,
    Gdk.KEY_Super_R,
];

/**
 * Response enum for ShortcutChooserDialog
 */
export const ResponseType = {
    CANCEL: Gtk.ResponseType.CANCEL,
    SET: Gtk.ResponseType.APPLY,
    UNSET: 2,
};


/**
 * A simplified version of the shortcut editor from GNOME Control Center
 */
export const ShortcutChooserDialog = GObject.registerClass({
    GTypeName: 'GSConnectPreferencesShortcutEditor',
    Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/preferences-shortcut-editor.ui',
    Children: [
        'cancel-button', 'set-button',
        'stack', 'summary-label',
        'shortcut-label', 'conflict-label',
    ],
}, class ShortcutChooserDialog extends Gtk.Dialog {

    _init(params) {
        super._init({
            transient_for: Gio.Application.get_default().get_active_window(),
            use_header_bar: true,
        });

        this._seat = Gdk.Display.get_default().get_default_seat();

        // Current accelerator or %null
        this.accelerator = params.accelerator;

        // TRANSLATORS: Summary of a keyboard shortcut function
        // Example: Enter a new shortcut to change Messaging
        this.summary = _('Enter a new shortcut to change <b>%s</b>').format(
            params.summary
        );
    }

    get accelerator() {
        return this.shortcut_label.accelerator;
    }

    set accelerator(value) {
        this.shortcut_label.accelerator = value;
    }

    get summary() {
        return this.summary_label.label;
    }

    set summary(value) {
        this.summary_label.label = value;
    }

    vfunc_key_press_event(event) {
        let keyvalLower = Gdk.keyval_to_lower(event.keyval);
        let realMask = event.state & Gtk.accelerator_get_default_mod_mask();

        // TODO: Critical: 'WIDGET_REALIZED_FOR_EVENT (widget, event)' failed
        if (_MODIFIERS.includes(keyvalLower))
            return true;

        // Normalize Tab
        if (keyvalLower === Gdk.KEY_ISO_Left_Tab)
            keyvalLower = Gdk.KEY_Tab;

        // Put shift back if it changed the case of the key, not otherwise.
        if (keyvalLower !== event.keyval)
            realMask |= Gdk.ModifierType.SHIFT_MASK;

        // HACK: we don't want to use SysRq as a keybinding (but we do want
        // Alt+Print), so we avoid translation from Alt+Print to SysRq
        if (keyvalLower === Gdk.KEY_Sys_Req && (realMask & Gdk.ModifierType.MOD1_MASK) !== 0)
            keyvalLower = Gdk.KEY_Print;

        // A single Escape press cancels the editing
        if (realMask === 0 && keyvalLower === Gdk.KEY_Escape) {
            this.response(ResponseType.CANCEL);
            return false;
        }

        // Backspace disables the current shortcut
        if (realMask === 0 && keyvalLower === Gdk.KEY_BackSpace) {
            this.response(ResponseType.UNSET);
            return false;
        }

        // CapsLock isn't supported as a keybinding modifier, so keep it from
        // confusing us
        realMask &= ~Gdk.ModifierType.LOCK_MASK;

        if (keyvalLower !== 0 && realMask !== 0) {
            this._ungrab();

            // Set the accelerator property/label
            this.accelerator = Gtk.accelerator_name(keyvalLower, realMask);

            // TRANSLATORS: When a keyboard shortcut is unavailable
            // Example: [Ctrl]+[S] is already being used
            this.conflict_label.label = _('%s is already being used').format(
                Gtk.accelerator_get_label(keyvalLower, realMask)
            );

            // Show Cancel button and switch to confirm/conflict page
            this.cancel_button.visible = true;
            this.stack.visible_child_name = 'confirm';

            this._check();
        }

        return true;
    }

    async _check() {
        try {
            const available = await checkAccelerator(this.accelerator);
            this.set_button.visible = available;
            this.conflict_label.visible = !available;
        } catch (e) {
            logError(e);
            this.response(ResponseType.CANCEL);
        }
    }

    _grab() {
        const success = this._seat.grab(
            this.get_window(),
            Gdk.SeatCapabilities.KEYBOARD,
            true, // owner_events
            null, // cursor
            null, // event
            null
        );

        if (success !== Gdk.GrabStatus.SUCCESS)
            return this.response(ResponseType.CANCEL);

        if (!this._seat.get_keyboard() && !this._seat.get_pointer())
            return this.response(ResponseType.CANCEL);

        this.grab_add();
    }

    _ungrab() {
        this._seat.ungrab();
        this.grab_remove();
    }

    // Override to use our own ungrab process
    response(response_id) {
        this.hide();
        this._ungrab();

        return super.response(response_id);
    }

    // Override with a non-blocking version of Gtk.Dialog.run()
    run() {
        this.show();

        // Wait a bit before attempting grab
        GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => {
            this._grab();
            return GLib.SOURCE_REMOVE;
        });
    }
});


/**
 * Check the availability of an accelerator using GNOME Shell's DBus interface.
 *
 * @param {string} accelerator - An accelerator
 * @param {number} [modeFlags] - Mode Flags
 * @param {number} [grabFlags] - Grab Flags
 * @returns {boolean} %true if available, %false on error or unavailable
 */
export async function checkAccelerator(accelerator, modeFlags = 0, grabFlags = 0) {
    try {
        let result = false;

        // Try to grab the accelerator
        const action = await new Promise((resolve, reject) => {
            Gio.DBus.session.call(
                'org.gnome.Shell',
                '/org/gnome/Shell',
                'org.gnome.Shell',
                'GrabAccelerator',
                new GLib.Variant('(suu)', [accelerator, modeFlags, grabFlags]),
                null,
                Gio.DBusCallFlags.NONE,
                -1,
                null,
                (connection, res) => {
                    try {
                        res = connection.call_finish(res);
                        resolve(res.deepUnpack()[0]);
                    } catch (e) {
                        reject(e);
                    }
                }
            );
        });

        // If successful, use the result of ungrabbing as our return
        if (action !== 0) {
            result = await new Promise((resolve, reject) => {
                Gio.DBus.session.call(
                    'org.gnome.Shell',
                    '/org/gnome/Shell',
                    'org.gnome.Shell',
                    'UngrabAccelerator',
                    new GLib.Variant('(u)', [action]),
                    null,
                    Gio.DBusCallFlags.NONE,
                    -1,
                    null,
                    (connection, res) => {
                        try {
                            res = connection.call_finish(res);
                            resolve(res.deepUnpack()[0]);
                        } catch (e) {
                            reject(e);
                        }
                    }
                );
            });
        }

        return result;
    } catch (e) {
        logError(e);
        return false;
    }
}


/**
 * Show a dialog to get a keyboard shortcut from a user.
 *
 * @param {string} summary - A description of the keybinding's function
 * @param {string} accelerator - An accelerator as taken by Gtk.ShortcutLabel
 * @returns {string} An accelerator or %null if it should be unset.
 */
export async function getAccelerator(summary, accelerator = null) {
    try {
        const dialog = new ShortcutChooserDialog({
            summary: summary,
            accelerator: accelerator,
        });

        accelerator = await new Promise((resolve, reject) => {
            dialog.connect('response', (dialog, response) => {
                switch (response) {
                    case ResponseType.SET:
                        accelerator = dialog.accelerator;
                        break;

                    case ResponseType.UNSET:
                        accelerator = null;
                        break;

                    case ResponseType.CANCEL:
                        // leave the accelerator as passed in
                        break;
                }

                dialog.destroy();

                resolve(accelerator);
            });

            dialog.run();
        });

        return accelerator;
    } catch (e) {
        logError(e);
        return accelerator;
    }
}
