import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import Meta from 'gi://Meta';
import St from 'gi://St';

import { DispatcherMode } from './utils.js';

import * as Main from 'resource:///org/gnome/shell/ui/main.js';

import {
    Utils, Tiling, Keybindings, Topbar,
    Scratch, Minimap, Settings
} from './imports.js';

/**
  Navigation and previewing functionality.

  This is a somewhat messy tangle of functionality relying on
  `SwitcherPopup.SwitcherPopup` when we really should just take full control.
 */

const { signals: Signals } = imports;
const display = global.display;

export let navigating; // exported
let grab, dispatcher, signals;
export function enable() {
    navigating = false;

    /**
     * Stop navigation before before/after overview. Avoids a corner-case issue
     * in multimonitors where workspaces can get snapped to another monitor.
     */
    signals = new Utils.Signals();
    signals.connect(Main.overview, 'showing', () => {
        finishNavigation();
    });
    signals.connect(Main.overview, 'hidden', () => {
        finishNavigation();
    });
}

export function disable() {
    navigating = false;
    grab = null;
    dispatcher = null;
    signals.destroy();
    signals = null;
    index = null;
}

export function primaryModifier(mask) {
    if (mask === 0)
        return 0;

    let primary = 1;
    while (mask > 1) {
        mask >>= 1;
        primary <<= 1;
    }
    return primary;
}

/**
   Handle catching keyevents and dispatching actions

   Adapted from SwitcherPopup, without any visual handling.
 */
class ActionDispatcher {
    /** @type {number} DispatcherMode bitmask */
    mode;

    constructor() {
        console.debug("#dispatch", "created");
        this.signals = new Utils.Signals();
        this.actor = Tiling.spaces.spaceContainer;
        this.actor.reactive = true;
        this.navigator = getNavigator();

        if (grab) {
            console.debug("#dispatch", "already in grab");
            return;
        }

        grab = Main.pushModal(this.actor);
        if (!grab) {
            console.error("Failed to grab modal");
            throw new Error('Could not grab modal');
        }

        this.signals.connect(this.actor, 'key-press-event', this._keyPressEvent.bind(this));
        this.signals.connect(this.actor, 'key-release-event', this._keyReleaseEvent.bind(this));

        this.keyPressCallbacks = [];
        this.keyReleaseCallbacks = [];

        this._noModsTimeoutId = null;
        this._doActionTimeout = null;
    }

    /**
     * Adds a signal to this dispatcher.  Will be destroyed when this
     * dispatcher is destroyed.
     */
    addKeypressCallback(handler) {
        this.keyPressCallbacks.push(handler);
        return this;
    }

    /**
     * Adds a signal to this dispatcher.  Will be destroyed when this
     * dispatcher is destroyed.
     */
    addKeyReleaseCallback(handler) {
        this.keyReleaseCallbacks.push(handler);
        return this;
    }

    show(_backward, binding, mask) {
        this._modifierMask = primaryModifier(mask);
        this.navigator = getNavigator();
        Topbar.fixTopBar();
        let actionId = Keybindings.idOf(binding);
        if (actionId === Meta.KeyBindingAction.NONE) {
            try {
                // Check for built-in actions
                actionId = Meta.prefs_get_keybinding_action(binding);
            } catch (e) {
                console.debug("Couldn't resolve action name");
                return false;
            }
        }

        this._doAction(actionId);

        // There's a race condition; if the user released Alt before
        // we got the grab, then we won't be notified. (See
        // https://bugzilla.gnome.org/show_bug.cgi?id=596695 for
        // details.) So we check now. (straight from SwitcherPopup)
        if (this._modifierMask) {
            let [, , mods] = global.get_pointer();
            if (!(mods & this._modifierMask)) {
                this._finish(global.get_current_time());
                return false;
            }
        } else {
            this._resetNoModsTimeout();
        }

        return true;
    }

    _resetNoModsTimeout() {
        Utils.timeout_remove(this._noModsTimeoutId);
        this._noModsTimeoutId = GLib.timeout_add(
            GLib.PRIORITY_DEFAULT,
            0, () => {
                this._finish(global.get_current_time());
                this._noModsTimeoutId = null;
                return false; // stops timeout recurrence
            });
    }

    _keyPressEvent(_actor, event) {
        if (!this._modifierMask) {
            this._modifierMask = primaryModifier(event.get_state());
        }
        let keysym = event.get_key_symbol();
        let action = global.display.get_keybinding_action(
            event.get_key_code(),
            event.get_state());

        // run callbacks and if any return true, stop bubbling
        if (this.keyPressCallbacks.some(callback => {
            return callback(this._modifierMask, keysym, event);
        })) {
            return Clutter.EVENT_STOP;
        }

        // Popping the modal on keypress doesn't work properly, as the release
        // event will leak to the active window. To work around this we initate
        // visual destruction on key-press and signal to the release handler
        // that we should destroy the dispactcher too
        // https://github.com/paperwm/PaperWM/issues/70
        if (keysym === Clutter.KEY_Escape) {
            this._destroy = true;
            getNavigator().accept();
            getNavigator().destroy();
            return Clutter.EVENT_STOP;
        }

        this._doAction(action);

        return Clutter.EVENT_STOP;
    }

    _keyReleaseEvent(_actor, event) {
        if (this._destroy) {
            dismissDispatcher(DispatcherMode.KEYBOARD);
        }

        if (this._modifierMask) {
            let [, , mods] = global.get_pointer();
            let state = mods & this._modifierMask;

            if (state === 0)
                this._finish(event.get_time());
        } else {
            this._resetNoModsTimeout();
        }

        this.keyReleaseCallbacks.forEach(callback => callback());
        return Clutter.EVENT_STOP;
    }

    _doAction(mutterActionId) {
        let action = Keybindings.byId(mutterActionId);
        let space = Tiling.spaces.selectedSpace;
        let metaWindow = space.selectedWindow;
        const nav = getNavigator();

        if (mutterActionId === Meta.KeyBindingAction.MINIMIZE) {
            metaWindow.minimize();
        } else if (action && action.options.activeInNavigator) {
            // action is performed while navigator is open (e.g. switch-left)
            if (!metaWindow && (action.options.mutterFlags & Meta.KeyBindingFlags.PER_WINDOW)) {
                return;
            }

            if (!Tiling.inGrab && action.options.opensMinimap) {
                nav.showMinimap(space);
            }
            action.handler(metaWindow, space, { navigator: this.navigator });
            if (space !== Tiling.spaces.selectedSpace) {
                this.navigator.minimaps.forEach(m => typeof m === 'number'
                    ? Utils.timeout_remove(m) : m.hide());
            }
            if (Tiling.inGrab && !Tiling.inGrab.dnd && Tiling.inGrab.window) {
                Tiling.inGrab.beginDnD();
            }
        } else if (action) {
            // closes navigator and action is performed afterwards
            // (e.g. switch-monitor-left)
            this._resetNoModsTimeout();
            this._doActionTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 0, () => {
                action.handler(metaWindow, space);
                this._doActionTimeout = null;
                return false; // on return false destroys timeout
            });
        }
    }

    _finish(_timestamp) {
        let nav = getNavigator();
        nav.accept();
        !this._destroy && nav.destroy();
        dismissDispatcher(DispatcherMode.KEYBOARD);
        let space = Tiling.spaces.selectedSpace;
        let metaWindow = space.selectedWindow;
        if (metaWindow) {
            if (!metaWindow.appears_focused) {
                space.setSelectionInactive();
            }
        }
    }

    destroy() {
        Utils.timeout_remove(this._noModsTimeoutId);
        this._noModsTimeoutId = null;
        Utils.timeout_remove(this._doActionTimeout);
        this._doActionTimeout = null;

        try {
            if (grab) {
                Main.popModal(grab);
                grab = null;
            }
        } catch (e) {
            console.debug("Failed to release grab: ", e);
        }

        this.actor.reactive = false;
        this.signals.destroy();
        this.signals = null;
        // We have already destroyed the navigator
        getNavigator().destroy();
        dispatcher = null;
    }
}

let index = 0;
export let navigator;
class NavigatorClass {
    constructor() {
        console.debug("#navigator", "nav created");

        /**
         * Hint for using take window mode (used in `takeWindow`).
         */
        this.takeHint = new St.Label({ style_class: 'take-window-hint' });
        this.takeHint.clutter_text.set_markup(
            `<i>• press <span foreground="#6be67b">spacebar</span> to return the last taken window</i>
<i>• press <span foreground="#6be67b">tab</span> to cycle forward through taken windows</i>
<i>• press <span foreground="#6be67b">shift+tab</span> to cycle backward through taken windows</i>
<i>• press <span foreground="#6be67b">q</span> to close all taken windows</i>`
        );

        navigating = true;

        this.was_accepted = false;
        this.index = index++;

        this._block = Main.wm._blockAnimations;
        Main.wm._blockAnimations = true;
        // Meta.disable_unredirect_for_screen(screen);
        this.space = Tiling.spaces.activeSpace;

        this._startWindow = this.space.selectedWindow;
        this.from = this.space;
        this.monitor = this.space.monitor;
        this.monitor.clickOverlay.hide();
        this.minimaps = new Map();

        Topbar.fixTopBar();

        Scratch.animateWindows();
        this.space.startAnimate();
    }

    showMinimap(space) {
        let minimap = this.minimaps.get(space);
        if (!minimap) {
            let minimapId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 200, () => {
                minimap = new Minimap.Minimap(space, this.monitor);
                space.startAnimate();
                minimap.show(false);
                this.minimaps.set(space, minimap);
                return false; // on return false destroys timeout
            });
            this.minimaps.set(space, minimapId);
        } else {
            typeof minimap !== 'number' && minimap.show();
        }
    }

    /**
     * Shows the "take window" hint.
     * @param {Boolean} show
     */
    showTakeHint(show = true) {
        if (show) {
            // set position on stage, take into account monitor
            const monitor = this.space.monitor;
            const x = monitor.x + monitor.width - 402;
            const y = monitor.height - 100;

            this.takeHint.opacity = 0;
            // global.stage.add_child(this.takeHint);
            Utils.actor_add_child(global.stage, this.takeHint);
            this.takeHint.set_position(x, y);

            Utils.Easer.addEase(this.takeHint, {
                time: Settings.prefs.animation_time,
                opacity: 255,
            });
        } else {
            this.takeHint.opacity = 255;
            // global.stage.add_child(this.takeHint);
            Utils.actor_add_child(global.stage, this.takeHint);
            Utils.Easer.addEase(this.takeHint, {
                time: Settings.prefs.animation_time,
                opacity: 0,
                onComplete: () => {
                    // global.stage.remove_child(this.takeHint);
                    Utils.actor_remove_child(global.stage, this.takeHint);
                },
            });
        }
    }

    accept() {
        this.was_accepted = true;
    }

    finish(force = false) {
        if (!force && grab) {
            return;
        }

        this.accept();
        this.destroy();
    }

    destroy() {
        this.minimaps.forEach(m => {
            if (typeof  m === 'number') {
                Utils.timeout_remove(m);
            }
            else {
                m.destroy();
            }
        });

        if (Tiling.inGrab && !Tiling.inGrab.dnd) {
            Tiling.inGrab?.beginDnD();
        }

        if (Main.panel.statusArea.appMenu)
            Main.panel.statusArea.appMenu.container.show();

        let force = Tiling.inPreview;
        navigating = false;

        if (force) {
            this?.space?.monitor?.clickOverlay.hide();
        }

        let space = Tiling.spaces.selectedSpace;
        this.space = space;

        let from = this.from;
        let selected = this.space.selectedWindow;
        if (!this.was_accepted) {
            // Abort the navigation
            this.space = from;
            if (this.startWindow && this._startWindow.get_compositor_private())
                selected = this._startWindow;
            else
                selected = display.focus_window;
        }

        let visible = [];
        for (let monitor of Main.layoutManager.monitors) {
            visible.push(Tiling.spaces.monitors.get(monitor));
        }

        if (!visible.includes(space) && this.monitor !== this.space.monitor) {
            this.space.setMonitor(this.monitor, true);
        }

        const workspaceId = this.space.workspace.index();
        if (this.space === from) {
            // Animate the selected space into full view - normally this
            // happens on workspace switch, but activating the same workspace
            // again doesn't trigger a switch signal
            if (force) {
                Tiling.spaces.switchWorkspace(null, workspaceId, workspaceId, force);
            }
        } else if (Tiling.inGrab && Tiling.inGrab.window) {
            this.space.activateWithFocus(Tiling.inGrab.window, false, true);
        } else {
            this.space.activate(false, true);
        }

        selected = this.space.indexOf(selected) !== -1 ? selected
            : this.space.selectedWindow;

        let curFocus = display.focus_window;
        if (force && curFocus && curFocus.is_on_all_workspaces()) {
            selected = curFocus;
        }

        if (selected && !Tiling.inGrab) {
            let hasFocus = selected && selected.has_focus();
            selected.foreach_transient(mw => {
                hasFocus = mw.has_focus() || hasFocus;
            });
            if (hasFocus) {
                Tiling.focus_handler(selected);
            } else {
                Main.activateWindow(selected);
            }
        }
        if (selected && Tiling.inGrab && !this.was_accepted) {
            Tiling.focus_handler(selected);
        }

        if (!Tiling.inGrab)
            Scratch.showWindows();

        Topbar.fixTopBar();

        Main.wm._blockAnimations = this._block;
        this.space.moveDone();

        this.emit('destroy', this.was_accepted);
        navigator = false;
    }
}
export let Navigator = NavigatorClass;
Signals.addSignalMethods(Navigator.prototype);

export function getNavigator() {
    if (navigator)
        return navigator;

    navigator = new Navigator();
    return navigator;
}

/**
 * Finishes navigation if navigator exists.
 * Useful to call before disabling other modules.
 */
export function finishNavigation(force = true) {
    if (navigator) {
        navigator.finish(force);
    }
}

/**
 * @param {number} mode - DispatcherMode bitmask
 * @returns {ActionDispatcher}
 */
export function getActionDispatcher(mode) {
    if (dispatcher) {
        dispatcher.mode |= mode;
        return dispatcher;
    }
    dispatcher = new ActionDispatcher();
    return getActionDispatcher(mode);
}

/**
 * Fishes current dispatcher (if any).
 */
export function finishDispatching() {
    dispatcher?._finish(global.get_current_time());
}

/**
 * @param {number} mode - DispatcherMode bitmask
 */
export function dismissDispatcher(mode) {
    if (!dispatcher) {
        return;
    }

    dispatcher.mode ^= mode;
    if (dispatcher.mode === DispatcherMode.NONE) {
        dispatcher.destroy();
    }
}

export function preview_navigate(meta_window, space, { _display, _screen, binding }) {
    let tabPopup = getActionDispatcher(DispatcherMode.KEYBOARD);
    tabPopup.show(binding.is_reversed(), binding.get_name(), binding.get_mask());
}
