import * as Config from './config.js';
import * as Forest from './forest.js';
import * as Ecs from './ecs.js';
import * as Events from './events.js';
import * as Focus from './focus.js';
import * as Geom from './geom.js';
import * as GrabOp from './grab_op.js';
import * as Keybindings from './keybindings.js';
import * as Lib from './lib.js';
import * as log from './log.js';
import * as PanelSettings from './panel_settings.js';
import * as Rect from './rectangle.js';
import * as Settings from './settings.js';
import * as Tiling from './tiling.js';
import * as Window from './window.js';
import * as launcher from './launcher.js';
import * as auto_tiler from './auto_tiler.js';
import * as node from './node.js';
import * as utils from './utils.js';
import * as Executor from './executor.js';
import * as movement from './movement.js';
import * as stack from './stack.js';
import * as add_exception from './dialog_add_exception.js';
import * as exec from './executor.js';
import * as dbus_service from './dbus_service.js';
import * as scheduler from './scheduler.js';
import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';
import type { Entity } from './ecs.js';
import type { ExtEvent } from './events.js';
import { Rectangle } from './rectangle.js';
import type { Indicator } from './panel_settings.js';
import type { Launcher } from './launcher.js';

import { Fork } from './fork.js';

const display = global.display;
const wim = global.window_manager;
const wom = global.workspace_manager;

const Movement = movement.Movement;

import GLib from 'gi://GLib';
import Gio from 'gi://Gio';
import St from 'gi://St';
import Shell from 'gi://Shell';
import Meta from 'gi://Meta';
const { GlobalEvent, WindowEvent } = Events;
const { cursor_rect, is_keyboard_op, is_resize_op, is_move_op } = Lib;
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
const {
    layoutManager,
    loadTheme,
    overview,
    panel,
    setThemeStylesheet,
    screenShield,
    sessionMode,
    windowAttentionHandler,
} = Main;
import { ScreenShield } from 'resource:///org/gnome/shell/ui/screenShield.js';
import {
    // AppSwitcher,
    // AppIcon,
    WindowSwitcherPopup,
} from 'resource:///org/gnome/shell/ui/altTab.js';
// import { SwitcherList } from 'resource:///org/gnome/shell/ui/switcherPopup.js';
import { Workspace } from 'resource:///org/gnome/shell/ui/workspace.js';
import { WorkspaceThumbnail } from 'resource:///org/gnome/shell/ui/workspaceThumbnail.js';
import { WindowPreview } from 'resource:///org/gnome/shell/ui/windowPreview.js';
import { PACKAGE_VERSION } from 'resource:///org/gnome/shell/misc/config.js';
import * as Tags from './tags.js';
import { get_current_path } from './paths.js';

const STYLESHEET_PATHS = ['light', 'dark', 'highcontrast'].map(stylesheet_path);
const STYLESHEETS = STYLESHEET_PATHS.map((path) => Gio.File.new_for_path(path));
const GNOME_VERSION = PACKAGE_VERSION;

enum Style {
    Light,
    Dark,
    HighContrast,
}

interface Display {
    area: Rectangle;
    ws: Rectangle;
}

interface Monitor extends Rectangular {
    index: number;
}

interface Injection {
    object: any;
    method: string;
    func: any;
}

export class Ext extends Ecs.System<ExtEvent> {
    /** Mechanism for managing keybindings */
    keybindings: Keybindings.Keybindings = new Keybindings.Keybindings(this);

    /** Manage interactions with GSettings */
    settings: Settings.ExtensionSettings = new Settings.ExtensionSettings();

    // Widgets

    /** An overlay which shows a preview of where a window will be moved */
    overlay: St.Widget = new St.BoxLayout({ style_class: 'pop-shell-overlay', visible: false });

    /** The application launcher, focus search, and calculator dialog */
    window_search: Launcher = new launcher.Launcher(this);

    /** DBus */
    dbus: dbus_service.Service = new dbus_service.Service();

    // State

    /** Animate window movements */
    animate_windows: boolean = true;

    button: any = null;
    button_gio_icon_auto_on: any = null;
    button_gio_icon_auto_off: any = null;

    conf: Config.Config = new Config.Config();

    conf_watch: null | [any, SignalID] = null;

    /** Column sizes in snap-to-grid */
    column_size: number = 32;

    /** The currently-loaded theme variant */
    current_style: Style = Style.Dark;

    /** Set when the display configuration has been triggered for execution */
    displays_updating: SignalID | null = null;

    /** Row size in snap-to-grid */
    row_size: number = 32;

    /** The known display configuration, for tracking monitor removals and changes */
    displays: [number, Map<number, Display>] = [global.display.get_primary_monitor(), new Map()];

    /** The current scaling factor in GNOME Shell */
    dpi: number = St.ThemeContext.get_for_stage(global.stage).scale_factor;

    drag_signal: null | SignalID = null;

    /** If set, the user is currently selecting a window to add to floating exceptions */
    exception_selecting: boolean = false;

    /** The number of pixels between windows */
    gap_inner: number = 0;

    /** Exactly half of the value of the inner gap */
    gap_inner_half: number = 0;

    /** Previously-set value of the inner gap */
    gap_inner_prev: number = 0;

    /** The number of pixels around a display's work area */
    gap_outer: number = 0;

    /** Previously-set value of the outer gap */
    gap_outer_prev: number = 0;

    /** Information about a current possible grab operation */
    grab_op: GrabOp.GrabOp | null = null;

    /** A display config update is triggered on a workspace addition */
    ignore_display_update: boolean = false;

    /** Functions replaced in GNOME */
    injections: Array<Injection> = new Array();

    /** The window that was focused before the last window */
    private prev_focused: [null | Entity, null | Entity] = [null, null];

    /** Initially set to true when the extension is initializing */
    init: boolean = true;

    was_locked: boolean = false;

    /** Set when a window is being moved by the mouse */
    moved_by_mouse: boolean = false;

    private workareas_update: null | SignalID = null;

    /** Record of misc. global objects and their attached signals */
    private signals: Map<GObject.Object, Array<SignalID>> = new Map();

    private size_requests: Map<GObject.Object, SignalID> = new Map();

    /** Stores windows that were focused on a workspace */
    private workspace_active: Map<number, null | Entity> = new Map();

    // Entity-component associations

    /** Store for stable sequences of each registered window */
    ids: Ecs.Storage<number> = this.register_storage();

    /** Store for keeping track of which monitor + workspace a window is on */
    monitors: Ecs.Storage<[number, number]> = this.register_storage();

    /** Stores movements that have been queued */
    movements: Ecs.Storage<Rectangular> = this.register_storage();

    /** Store for names associated with windows */
    names: Ecs.Storage<string> = this.register_storage();

    /** Signal ID which handles size-changed signals */
    size_changed_signal: SignalID = 0;

    /** Store for size-changed signals attached to each window */
    size_signals: Ecs.Storage<SignalID[]> = this.register_storage();

    /** Set to true if a window is snapped to the grid */
    snapped: Ecs.Storage<boolean> = this.register_storage();

    /** Primary storage for the window entities, containing the actual window */
    windows: Ecs.Storage<Window.ShellWindow> = this.register_storage();

    /** Signals which have been registered for each window */
    window_signals: Ecs.Storage<Array<SignalID>> = this.register_storage();

    // Systems

    /** Manages automatic tiling behaviors in the shell */
    auto_tiler: auto_tiler.AutoTiler | null = null;

    /** Performs focus selections */
    focus_selector: Focus.FocusSelector = new Focus.FocusSelector();

    /** Calculates window placements when tiling and focus-switching */
    tiler: Tiling.Tiler = new Tiling.Tiler(this);

    constructor() {
        super(new Executor.GLibExecutor());

        this.load_settings();
        this.reload_theme();

        this.register_fn(() => load_theme(this.current_style));

        this.conf.reload();

        if (this.settings.int) {
            this.settings.int.connect('changed::gtk-theme', () => {
                this.register(Events.global(GlobalEvent.GtkThemeChanged));
            });
        }

        if (this.settings.shell) {
            this.settings.shell.connect('changed::name', () => {
                this.register(Events.global(GlobalEvent.GtkShellChanged));
            });
        }

        this.dbus.FocusUp = () => this.focus_up();
        this.dbus.FocusDown = () => this.focus_down();
        this.dbus.FocusLeft = () => this.focus_left();
        this.dbus.FocusRight = () => this.focus_right();
        this.dbus.Launcher = () => this.window_search.open(this);

        this.dbus.WindowFocus = (window: [number, number]) => {
            const target_window = this.windows.get(window);
            if (target_window) {
                target_window.activate();
                this.on_focused(target_window);
            }
            this.window_search.close();
        };

        this.dbus.WindowList = (): Array<[[number, number], string, string, string]> => {
            const wins = new Array();

            for (const window of this.tab_list(Meta.TabList.NORMAL, null)) {
                const string = window.window_app.get_id();
                wins.push([window.entity, window.title(), window.name(this), string ? string : '']);
            }

            return wins;
        };

        this.dbus.WindowQuit = (win: [number, number]) => {
            this.windows.get(win)?.meta.delete(global.get_current_time());
            this.window_search.close();
        };
    }

    // System interface

    /** Registers a generic callback to be executed in the event loop. */
    register_fn(callback: () => void, name?: string) {
        this.register({ tag: 1, callback, name });
    }

    /** Executes an event on the system */
    run(event: ExtEvent) {
        switch (event.tag) {
            /** Callback Event */
            case 1:
                event.callback();
                break;

            /** Window Event */
            case 2:
                let win = event.window;

                /** Validate that the window's actor still exists. */
                if (!win.actor_exists()) return;

                if (event.kind.tag === 1) {
                    const { window } = event;

                    let movement = this.movements.remove(window.entity);
                    if (!movement) return;

                    let actor = window.meta.get_compositor_private();
                    if (!actor) {
                        this.auto_tiler?.detach_window(this, window.entity);
                        return;
                    }

                    actor.remove_all_transitions();
                    const { x, y, width, height } = movement;

                    window.meta.move_resize_frame(true, x, y, width, height);
                    window.meta.move_frame(true, x, y);

                    this.monitors.insert(window.entity, [win.meta.get_monitor(), win.workspace_id()]);

                    if (win.activate_after_move) {
                        win.activate_after_move = false;
                        win.activate();
                    }

                    return;
                }

                switch (event.kind.event) {
                    case WindowEvent.Maximize:
                        this.unset_grab_op();
                        this.on_maximize(win);
                        break;

                    case WindowEvent.Minimize:
                        this.unset_grab_op();
                        this.on_minimize(win);
                        break;

                    case WindowEvent.Size:
                        if (this.auto_tiler && !win.is_maximized() && !win.meta.is_fullscreen()) {
                            this.auto_tiler.reflow(this, win.entity);
                        }
                        break;

                    case WindowEvent.Workspace:
                        this.on_workspace_changed(win);
                        break;

                    case WindowEvent.Fullscreen:
                        if (this.auto_tiler) {
                            let attachment = this.auto_tiler.attached.get(win.entity);
                            if (attachment) {
                                if (!win.meta.is_fullscreen()) {
                                    let fork = this.auto_tiler.forest.forks.get(win.entity);
                                    if (fork) {
                                        this.auto_tiler.reflow(this, win.entity);
                                    }
                                    if (win.stack !== null) {
                                        this.auto_tiler.forest.stacks.get(win.stack)?.set_visible(true);
                                    }
                                } else {
                                    // not full screened
                                    if (win.stack !== null) {
                                        this.auto_tiler.forest.stacks.get(win.stack)?.set_visible(false);
                                    }
                                }
                            }
                        }

                        break;
                }

                break;

            /** Window Create Event */
            case 3:
                let actor = event.window.get_compositor_private();
                if (!actor) return;

                this.on_window_create(event.window, actor);
                break;

            /** Stateless global events */
            case 4:
                switch (event.event) {
                    case GlobalEvent.GtkShellChanged:
                        this.on_gtk_shell_changed();
                        break;

                    case GlobalEvent.GtkThemeChanged:
                        this.on_gtk_theme_change();
                        break;

                    case GlobalEvent.MonitorsChanged:
                        this.update_display_configuration(false);
                        break;

                    case GlobalEvent.OverviewShown:
                        this.on_overview_shown();
                        break;
                }

                break;
        }
    }

    // Extension methods

    activate_window(window: Window.ShellWindow | null) {
        if (window) {
            window.activate();
        }
    }

    active_monitor(): number {
        return display.get_current_monitor();
    }

    active_window_list(): Array<Window.ShellWindow> {
        let workspace = wom.get_active_workspace();
        return this.tab_list(Meta.TabList.NORMAL_ALL, workspace);
    }

    active_workspace(): number {
        return wom.get_active_workspace_index();
    }

    actor_of(entity: Entity): null | Clutter.Actor {
        const window = this.windows.get(entity);
        return window ? window.meta.get_compositor_private() : null;
    }

    /// Connects a callback signal to a GObject, and records the signal.
    connect(object: GObject.Object, property: string, callback: (...args: any) => boolean | void): SignalID {
        const signal = object.connect(property, callback);
        const entry = this.signals.get(object);
        if (entry) {
            entry.push(signal);
        } else {
            this.signals.set(object, [signal]);
        }

        return signal;
    }

    connect_meta(win: Window.ShellWindow, signal: string, callback: (...args: any[]) => void): number {
        const id = win.meta.connect(signal, () => {
            if (win.actor_exists()) callback();
        });

        this.window_signals.get_or(win.entity, () => new Array()).push(id);

        return id;
    }

    connect_size_signal(win: Window.ShellWindow, signal: string, func: () => void): number {
        return this.connect_meta(win, signal, () => {
            if (!this.contains_tag(win.entity, Tags.Blocked)) func();
        });
    }

    connect_window(win: Window.ShellWindow) {
        const size_event = () => {
            const old = this.size_requests.get(win.meta);

            if (old) {
                try {
                    GLib.source_remove(old);
                } catch (_) {}
            }

            const new_s = GLib.timeout_add(GLib.PRIORITY_LOW, 500, () => {
                this.register(Events.window_event(win, WindowEvent.Size));
                this.size_requests.delete(win.meta);
                return false;
            });

            this.size_requests.set(win.meta, new_s);
        };

        this.connect_meta(win, 'workspace-changed', () => {
            this.register(Events.window_event(win, WindowEvent.Workspace));
        });

        this.size_signals.insert(win.entity, [
            this.connect_size_signal(win, 'size-changed', size_event),

            this.connect_size_signal(win, 'position-changed', size_event),

            this.connect_size_signal(win, 'notify::minimized', () => {
                this.register(Events.window_event(win, WindowEvent.Minimize));
            }),
        ]);
    }

    exception_add(win: Window.ShellWindow) {
        this.exception_selecting = false;
        let d = new add_exception.AddExceptionDialog(
            // Cancel
            () => this.exception_dialog(),
            // this_app
            () => {
                let wmclass = win.meta.get_wm_class();
                if (wmclass !== null && wmclass.length === 0) {
                    wmclass = win.name(this);
                }

                if (wmclass) this.conf.add_app_exception(wmclass);
                this.exception_dialog();
            },
            // current-window
            () => {
                let wmclass = win.meta.get_wm_class();
                if (wmclass) this.conf.add_window_exception(wmclass, win.title());
                this.exception_dialog();
            },
            // Reload the tiling config on dialog close
            () => {
                this.conf.reload();
                this.tiling_config_reapply();
            },
        );
        d.open();
    }

    exception_dialog() {
        let path = get_current_path() + '/floating_exceptions/main.js';

        const event_handler = (event: string): boolean => {
            switch (event) {
                case 'MODIFIED':
                    this.register_fn(() => {
                        this.conf.reload();
                        this.tiling_config_reapply();
                    });
                    break;
                case 'SELECT':
                    this.register_fn(() => this.exception_select());
                    return false;
            }

            return true;
        };

        const ipc = utils.async_process_ipc(['gjs', '--module', path]);

        if (ipc) {
            const generator = (stdout: any, res: any) => {
                try {
                    const [bytes] = stdout.read_line_finish(res);
                    if (bytes) {
                        if (event_handler((imports.byteArray.toString(bytes) as string).trim())) {
                            ipc.stdout.read_line_async(0, ipc.cancellable, generator);
                        }
                    }
                } catch (why) {
                    log.error(`failed to read response from floating exceptions dialog: ${why}`);
                }
            };

            ipc.stdout.read_line_async(0, ipc.cancellable, generator);
        }
    }

    exception_select() {
        GLib.timeout_add(GLib.PRIORITY_LOW, 500, () => {
            this.exception_selecting = true;
            overview.show();
            return false;
        });
    }

    exit_modes() {
        this.tiler.exit(this);
        this.window_search.reset();
        this.window_search.close();
        this.overlay.visible = false;
    }

    find_monitor_to_retach(width: number, height: number): [number, Display] {
        if (!this.settings.workspaces_only_on_primary()) {
            for (const [index, display] of this.displays[1]) {
                if (display.area.width == width && display.area.height == height) {
                    return [index, display];
                }
            }
        }

        const primary = display.get_primary_monitor();
        return [primary, this.displays[1].get(primary) as Display];
    }

    find_unused_workspace(monitor: number): [number, any] {
        if (!this.auto_tiler) return [0, wom.get_workspace_by_index(0)];

        let id = 0;

        const tiled_windows = new Array<Window.ShellWindow>();

        for (const [window] of this.auto_tiler.attached.iter()) {
            if (!this.auto_tiler.attached.contains(window)) continue;

            const win = this.windows.get(window);

            if (win && !win.reassignment && win.meta.get_monitor() === monitor) tiled_windows.push(win);
        }

        cancel: while (true) {
            for (const window of tiled_windows) {
                if (window.workspace_id() === id) {
                    id += 1;
                    continue cancel;
                }
            }

            break;
        }

        let new_work;

        if (id + 1 === wom.get_n_workspaces()) {
            id += 1;
            new_work = wom.append_new_workspace(true, global.get_current_time());
        } else {
            new_work = wom.get_workspace_by_index(id);
        }

        return [id, new_work];
    }

    focus_left() {
        this.stack_select(
            (id, stack) => (id === 0 ? null : stack.tabs[id - 1].entity),
            () => this.activate_window(this.focus_selector.left(this, null)),
        );
    }

    focus_right() {
        this.stack_select(
            (id, stack) => (stack.tabs.length > id + 1 ? stack.tabs[id + 1].entity : null),
            () => this.activate_window(this.focus_selector.right(this, null)),
        );
    }

    focus_down() {
        this.activate_window(this.focus_selector.down(this, null));
    }

    focus_up() {
        this.activate_window(this.focus_selector.up(this, null));
    }

    focus_window(): Window.ShellWindow | null {
        return this.get_window(display.get_focus_window());
    }

    stack_select(select: (id: number, stack: stack.Stack) => Entity | null, focus_shift: () => void) {
        const switched = this.stack_switch((stack: any) => {
            if (!stack) return false;

            const stack_con = this.auto_tiler?.forest.stacks.get(stack.idx);
            if (stack_con) {
                const id = stack_con.active_id;
                if (id !== -1) {
                    const next = select(id, stack_con);
                    if (next) {
                        stack_con.activate(next);
                        const window = this.windows.get(next);
                        if (window) {
                            window.activate();
                            return true;
                        }
                    }
                }
            }

            return false;
        });

        if (!switched) {
            focus_shift();
        }
    }

    stack_switch(apply: (stack: node.NodeStack) => boolean) {
        const window = this.focus_window();
        if (window) {
            if (this.auto_tiler) {
                const node = this.auto_tiler.find_stack(window.entity);
                return node ? apply(node[1].inner as node.NodeStack) : false;
            }
        }
    }

    /// Fetches the window component from the entity associated with the metacity window metadata.
    get_window(meta: Meta.Window | null): Window.ShellWindow | null {
        let entity = this.window_entity(meta);
        return entity ? this.windows.get(entity) : null;
    }

    inject(object: any, method: string, func: any) {
        const prev = object[method];
        this.injections.push({ object, method, func: prev });
        object[method] = func;
    }

    injections_add() {
        const screen_unlock_fn = ScreenShield.prototype['deactivate'];
        this.inject(ScreenShield.prototype, 'deactivate', (args: any) => {
            screen_unlock_fn.apply(screenShield, [args]);
            this.update_display_configuration(true);
        });
    }

    injections_remove() {
        for (const { object, method, func } of this.injections.splice(0)) {
            object[method] = func;
        }
    }

    load_settings() {
        this.set_gap_inner(this.settings.gap_inner());
        this.set_gap_outer(this.settings.gap_outer());
        this.gap_inner_prev = this.gap_inner;
        this.gap_outer_prev = this.gap_outer;

        this.column_size = this.settings.column_size() * this.dpi;
        this.row_size = this.settings.row_size() * this.dpi;
    }

    monitor_work_area(monitor: number): Rectangle {
        const meta = wom.get_active_workspace().get_work_area_for_monitor(monitor);

        return Rect.Rectangle.from_meta(meta as Rectangular);
    }

    monitor_area(monitor: number): Rectangle | null {
        const rect = global.display.get_monitor_geometry(monitor);
        return rect ? Rect.Rectangle.from_meta(rect as Rectangular) : null;
    }

    on_active_workspace_changed() {
        this.register_fn(() => {
            this.exit_modes();
            this.restack();

            const activate_window = (window: Window.ShellWindow) => {
                this.on_focused(window);
                window.activate(true);
                this.prev_focused = [null, window.entity];
            };

            const focused = this.focus_window();
            if (focused && focused.same_workspace()) {
                activate_window(focused);
                return;
            }

            // Activate the last-active window on workspace.
            const workspace_id = this.active_workspace();
            const active = this.workspace_active.get(workspace_id);
            if (active) {
                const window = this.windows.get(active);
                if (window && window.meta.get_workspace().index() == workspace_id && !window.meta.minimized) {
                    activate_window(window);
                    return;
                }
            }

            // If window was not found, activate the first window on workspace.
            const workspace = wom.get_workspace_by_index(workspace_id);
            if (workspace) {
                for (const win of workspace.list_windows()) {
                    const window = this.get_window(win);
                    if (window && !window.meta.minimized) {
                        activate_window(window);
                        return;
                    }
                }
            }
        });
    }

    on_destroy(win: Entity) {
        // Exit tiling adjustment mode on window destroy.
        if (this.tiler.window !== null && win == this.tiler.window) this.tiler.exit(this);

        const [prev_a, prev_b] = this.prev_focused;

        if (prev_a && Ecs.entity_eq(win, prev_a)) {
            this.prev_focused[0] = null;
        } else if (prev_b && Ecs.entity_eq(win, prev_b)) {
            this.prev_focused[1] = this.prev_focused[0];
            this.prev_focused[0] = null;
        }

        const window = this.windows.get(win);
        if (!window) return;

        const stack = window.stack;

        window.destroying = true;

        // Disconnect all signals on this window
        this.window_signals.take_with(win, (signals) => {
            for (const signal of signals) {
                window.meta.disconnect(signal);
            }
        });

        if (this.auto_tiler) {
            const entity = this.auto_tiler.attached.get(win);
            if (entity) {
                const fork = this.auto_tiler.forest.forks.get(entity);
                if (fork?.right?.is_window(win)) {
                    const entity = fork.right.inner.kind === 3 ? fork.right.inner.entities[0] : fork.right.inner.entity;

                    this.windows.with(entity, (sibling) => sibling.activate());
                }
            }
        }

        if (this.auto_tiler) this.auto_tiler.detach_window(this, win);

        // If destroyed window belonged to a stack, ensure that the next window
        // to be focused is also a window in the same stack
        if (this.auto_tiler && stack !== null) {
            const stack_object = this.auto_tiler.forest.stacks.get(stack);
            const prev = this.prev_focused[1];
            if (stack_object && prev) {
                const prev_window = this.windows.get(prev);
                if (prev_window) {
                    if (prev_window.stack !== stack) {
                        stack_object.auto_activate();
                        this.prev_focused = [null, stack_object.active];
                        this.windows.get(stack_object.active)?.activate();
                    }
                }
            }
        }

        this.movements.remove(win);
        this.windows.remove(win);
        this.delete_entity(win);
    }

    on_display_move(_from_id: number, _to_id: number) {
        if (!this.auto_tiler) return;
    }

    /** Triggered when a window has been focused */
    on_focused(win: Window.ShellWindow) {
        this.workspace_active.set(this.active_workspace(), win.entity);
        scheduler.setForeground(win.meta);

        this.size_signals_unblock(win);

        if (this.exception_selecting) {
            this.exception_add(win);
        }

        // Track history of focused windows, but do not permit duplicates.
        if (this.prev_focused[1] !== win.entity) {
            this.prev_focused[0] = this.prev_focused[1];
            this.prev_focused[1] = win.entity;
        }

        // Update the active tab in the stack.
        if (null !== this.auto_tiler && null !== win.stack) {
            ext?.auto_tiler?.forest.stacks.get(win.stack)?.activate(win.entity);
        }

        this.unmaximize_workspace(win);

        this.show_border_on_focused();

        if (this.auto_tiler && win.is_tilable(this) && this.prev_focused[0] !== null) {
            let prev = this.windows.get(this.prev_focused[0]);
            let is_attached = this.auto_tiler.attached.contains(this.prev_focused[0]);

            if (
                prev &&
                prev !== win &&
                is_attached &&
                prev.actor_exists() &&
                prev.name(this) !== win.name(this) &&
                prev.workspace_id() === win.workspace_id()
            ) {
                if (prev.rect().contains(win.rect())) {
                    if (prev.is_maximized()) {
                        prev.meta.unmaximize(Meta.MaximizeFlags.BOTH);
                    }
                } else if (prev.stack) {
                    prev.meta.unmaximize(Meta.MaximizeFlags.BOTH);
                    this.auto_tiler.forest.stacks.get(prev.stack)?.restack();
                }
            }
        }

        if (this.conf.log_on_focus) {
            let msg =
                `focused Window(${win.entity}) {\n` +
                `  class: "${win.meta.get_wm_class()}",\n` +
                `  cmdline: ${win.cmdline()},\n` +
                `  monitor: ${win.meta.get_monitor()},\n` +
                `  name: ${win.name(this)},\n` +
                `  rect: ${win.rect().fmt()},\n` +
                `  workspace: ${win.workspace_id()},\n` +
                `  xid: ${win.xid()},\n` +
                `  stack: ${win.stack},\n`;

            if (this.auto_tiler) {
                msg += `  fork: (${this.auto_tiler.attached.get(win.entity)}),\n`;
            }

            log.debug(msg + '}');
        }
    }

    on_tile_attach(entity: Entity, window: Entity) {
        if (this.auto_tiler) {
            if (!this.auto_tiler.attached.contains(window)) {
                this.windows.with(window, (w) => {
                    if (w.prev_rect === null) {
                        w.prev_rect = w.meta.get_frame_rect();
                    }
                });
            }

            this.auto_tiler.attached.insert(window, entity);
        }
    }

    on_tile_detach(win: Entity) {
        this.windows.with(win, (window) => {
            if (window.prev_rect && !window.ignore_detach) {
                this.register(Events.window_move(this, window, window.prev_rect));
                window.prev_rect = null;
            }
        });
    }

    show_border_on_focused() {
        this.hide_all_borders();
        const focus = this.focus_window();
        if (focus) focus.show_border();
    }

    hide_all_borders() {
        for (const win of this.windows.values()) {
            win.hide_border();
        }
    }

    maximized_on_active_display(): boolean {
        const aws = this.workspace_id();
        for (const window of this.windows.values()) {
            if (!window.actor_exists()) continue;

            const wws = this.workspace_id(window);
            if (aws[0] === wws[0] && aws[1] === wws[1]) {
                if (window.is_maximized()) return true;
            }
        }

        return false;
    }

    on_gap_inner() {
        let current = this.settings.gap_inner();
        this.set_gap_inner(current);
        let prev_gap = this.gap_inner_prev / 4 / this.dpi;

        if (current != prev_gap) {
            this.update_inner_gap();
            Gio.Settings.sync();
        }
    }

    update_inner_gap() {
        if (this.auto_tiler) {
            for (const [entity] of this.auto_tiler.forest.toplevel.values()) {
                const fork = this.auto_tiler.forest.forks.get(entity);
                if (fork) {
                    this.auto_tiler.tile(this, fork, fork.area);
                }
            }
        } else {
            this.update_snapped();
        }
    }

    /** Unmaximize any maximized windows on the same workspace. */
    unmaximize_workspace(win: Window.ShellWindow) {
        if (this.auto_tiler) {
            let mon;
            let work;

            if (!win.is_tilable(this)) {
                return;
            }

            mon = win.meta.get_monitor();
            work = win.meta.get_workspace().index();

            for (const [, compare] of this.windows.iter()) {
                const is_same_space =
                    compare.meta.get_monitor() === mon && compare.meta.get_workspace().index() === work;

                if (
                    is_same_space &&
                    !this.contains_tag(compare.entity, Tags.Floating) &&
                    compare.is_maximized() &&
                    win.entity[0] !== compare.entity[0]
                ) {
                    compare.meta.unmaximize(Meta.MaximizeFlags.BOTH);
                }
            }
        }
    }

    on_gap_outer() {
        let current = this.settings.gap_outer();
        this.set_gap_outer(current);

        let prev_gap = this.gap_outer_prev / 4 / this.dpi;
        let diff = current - prev_gap;

        if (diff != 0) {
            this.set_gap_outer(current);
            this.update_outer_gap(diff);

            Gio.Settings.sync();
        }
    }

    update_outer_gap(diff: number) {
        if (this.auto_tiler) {
            for (const [entity] of this.auto_tiler.forest.toplevel.values()) {
                const fork = this.auto_tiler.forest.forks.get(entity);

                if (fork) {
                    fork.area.array[0] += diff * 4;
                    fork.area.array[1] += diff * 4;
                    fork.area.array[2] -= diff * 8;
                    fork.area.array[3] -= diff * 8;

                    this.auto_tiler.tile(this, fork, fork.area);
                }
            }
        } else {
            this.update_snapped();
        }
    }

    /** Triggered when a grab operation has been ended */
    on_grab_end(meta: Meta.Window, op?: any) {
        let win = this.get_window(meta);

        if (win !== null) {
            win.grab = false;
        }

        if (null === win || !win.is_tilable(this)) {
            this.unset_grab_op();
            return;
        }

        this.on_grab_end_(win, op);
        this.unset_grab_op();
    }

    on_grab_end_(win: Window.ShellWindow, op?: any) {
        this.moved_by_mouse = true;
        this.size_signals_unblock(win);

        if (win.meta && win.meta.minimized) {
            this.on_minimize(win);
            return;
        }

        if (win.is_maximized()) {
            return;
        }

        const grab_op = this.grab_op;

        if (!win) {
            log.error('an entity was dropped, but there is no window');
            return;
        }

        if (this.auto_tiler && op === undefined) {
            let mon = this.monitors.get(win.entity);
            if (mon) {
                let rect = win.meta.get_work_area_for_monitor(mon[0]);
                if (rect && Rect.Rectangle.from_meta(rect).contains(cursor_rect())) {
                    this.auto_tiler.reflow(this, win.entity);
                } else {
                    this.auto_tiler.on_drop(this, win, true);
                }
            }

            return;
        }

        if (!(grab_op && Ecs.entity_eq(grab_op.entity, win.entity))) {
            log.error(`grabbed entity is not the same as the one that was dropped`);
            return;
        }

        if (this.auto_tiler) {
            let crect = win.rect();
            const rect = grab_op.rect;
            if (is_move_op(op)) {
                const cmon = win.meta.get_monitor();
                const prev_mon = this.monitors.get(win.entity);
                const mon_drop = prev_mon ? prev_mon[0] !== cmon : false;

                this.monitors.insert(win.entity, [win.meta.get_monitor(), win.workspace_id()]);

                if (rect.x != crect.x || rect.y != crect.y) {
                    if (rect.contains(cursor_rect())) {
                        if (this.auto_tiler.attached.contains(win.entity)) {
                            this.auto_tiler.on_drop(this, win, mon_drop);
                        } else {
                            this.auto_tiler.reflow(this, win.entity);
                        }
                    } else {
                        this.auto_tiler.on_drop(this, win, mon_drop);
                    }
                }
            } else {
                const fork_entity = this.auto_tiler.attached.get(win.entity);
                if (fork_entity) {
                    const forest = this.auto_tiler.forest;
                    const fork = forest.forks.get(fork_entity);
                    if (fork) {
                        if (win.stack) {
                            const tab_dimension = this.dpi * stack.TAB_HEIGHT;
                            crect.height += tab_dimension;
                            crect.y -= tab_dimension;
                        }

                        let top_level = forest.find_toplevel(this.workspace_id());
                        if (top_level) {
                            crect.clamp((forest.forks.get(top_level) as Fork).area);
                        }

                        const movement = grab_op.operation(crect);

                        if (this.movement_is_valid(win, movement)) {
                            forest.resize(this, fork_entity, fork, win.entity, movement, crect);
                            forest.arrange(this, fork.workspace);
                        } else {
                            forest.tile(this, fork, fork.area);
                        }
                    } else {
                        log.error(`no fork component found`);
                    }
                } else {
                    log.error(`no fork entity found`);
                }
            }
        } else if (this.settings.snap_to_grid()) {
            this.tiler.snap(this, win);
        }
    }

    previously_focused(active: Window.ShellWindow): null | Ecs.Entity {
        for (const id of [1, 0]) {
            const prev = this.prev_focused[id];
            if (prev && !Ecs.entity_eq(active.entity, prev)) {
                return prev;
            }
        }

        return null;
    }

    movement_is_valid(win: Window.ShellWindow, movement: movement.Movement) {
        if ((movement & Movement.SHRINK) !== 0) {
            if ((movement & Movement.DOWN) !== 0) {
                const w = this.focus_selector.up(this, win);
                if (!w) return false;
                const r = w.rect();
                if (r.y + r.height > win.rect().y) return false;
            } else if ((movement & Movement.UP) !== 0) {
                const w = this.focus_selector.down(this, win);
                if (!w) return false;
                const r = w.rect();
                if (r.y + r.height < win.rect().y) return false;
            } else if ((movement & Movement.LEFT) !== 0) {
                const w = this.focus_selector.right(this, win);
                if (!w) return false;
                const r = w.rect();
                if (r.x + r.width < win.rect().x) return false;
            } else if ((movement & Movement.RIGHT) !== 0) {
                const w = this.focus_selector.left(this, win);
                if (!w) return false;
                const r = w.rect();
                if (r.x + r.width > win.rect().x) return false;
            }
        }

        return true;
    }

    workspace_window_move(win: Window.ShellWindow, prev_monitor: number, next_monitor: number) {
        const prev_area = win.meta.get_work_area_for_monitor(prev_monitor);
        const next_area = win.meta.get_work_area_for_monitor(next_monitor);

        if (prev_area && next_area) {
            // get the current window rect
            let rect = win.rect();

            let h_ratio: number = 1;
            let w_ratio: number = 1;

            h_ratio = next_area.height / prev_area.height;
            rect.height = rect.height * h_ratio;

            w_ratio = next_area.width / prev_area.width;
            rect.width = rect.width * w_ratio;

            if (next_area.x < prev_area.x) {
                rect.x = ((next_area.x + rect.x - prev_area.x) / prev_area.width) * next_area.width;
            } else if (next_area.x > prev_area.x) {
                rect.x = (rect.x / prev_area.width) * next_area.width + next_area.x;
            }

            if (next_area.y < prev_area.y) {
                rect.y = ((next_area.y + rect.y - prev_area.y) / prev_area.height) * next_area.height;
            } else if (next_area.y > prev_area.y) {
                rect.y = (rect.y / prev_area.height) * next_area.height + next_area.y;
            }

            if (this.auto_tiler) {
                if (this.is_floating(win)) {
                    win.meta.unmaximize(Meta.MaximizeFlags.HORIZONTAL);
                    win.meta.unmaximize(Meta.MaximizeFlags.VERTICAL);
                    win.meta.unmaximize(Meta.MaximizeFlags.BOTH);
                }

                this.register(Events.window_move(this, win, rect));
            } else {
                win.move(this, rect, () => {});
                // if the resulting dimensions of rect == next
                if (rect.width == next_area.width && rect.height == next_area.height) {
                    win.meta.maximize(Meta.MaximizeFlags.BOTH);
                }
            }
        }
    }

    move_monitor(direction: Meta.DisplayDirection) {
        const win = this.focus_window();
        if (!win) return;

        const prev_monitor = win.meta.get_monitor();
        const next_monitor = Tiling.locate_monitor(win, direction);

        if (next_monitor !== null) {
            if (this.auto_tiler && !this.is_floating(win)) {
                win.ignore_detach = true;
                this.auto_tiler.detach_window(this, win.entity);
                this.auto_tiler.attach_to_workspace(this, win, [next_monitor[0], win.workspace_id()]);
            } else {
                this.workspace_window_move(win, prev_monitor, next_monitor[0]);
            }
        }

        win.activate_after_move = true;
    }

    /** Moves the focused window across workspaces and displays */
    move_workspace(direction: Meta.DisplayDirection) {
        const win = this.focus_window();
        if (!win) return;

        /** Move a window between workspaces */
        const workspace_move = (direction: Meta.MotionDirection) => {
            const ws = win.meta.get_workspace();
            let neighbor = ws.get_neighbor(direction);

            const last_window = (): boolean => {
                const last = wom.get_n_workspaces() - 2 === ws.index() && ws.n_windows === 1;
                return last;
            };

            /** Places window onto the nearest window of a given workspace */
            const place_on_nearest_window = (auto_tiler: auto_tiler.AutoTiler, ws: Meta.Workspace, monitor: number) => {
                const src = win.meta.get_frame_rect();

                auto_tiler.detach_window(this, win.entity);

                const index = ws.index();
                const coord: [number, number] = [src.x, src.y];

                let nearest_window = null;
                let nearest_distance = null;

                for (const [entity, window] of this.windows.iter()) {
                    const other_monitor = window.meta.get_monitor();
                    const other_index = window.meta.get_workspace().index();
                    if (
                        !this.contains_tag(entity, Tags.Floating) &&
                        other_monitor == monitor &&
                        other_index === index &&
                        !Ecs.entity_eq(win.entity, window.entity)
                    ) {
                        const other_rect = window.rect();
                        const other_coord: [number, number] = [other_rect.x, other_rect.y];
                        const distance = Geom.distance(coord, other_coord);
                        if (nearest_distance === null || nearest_distance > distance) {
                            nearest_window = window;
                            nearest_distance = distance;
                        }
                    }
                }

                if (nearest_window === null) {
                    auto_tiler.attach_to_workspace(this, win, [monitor, index]);
                } else {
                    auto_tiler.attach_to_window(this, nearest_window, win, { src }, false);
                }
            };

            const move_to_neighbor = (neighbor: Meta.Workspace) => {
                const monitor = win.meta.get_monitor();
                if (this.auto_tiler && win.is_tilable(this)) {
                    win.ignore_detach = true;

                    place_on_nearest_window(this.auto_tiler, neighbor, monitor);

                    if (win.meta.minimized) {
                        this.size_signals_block(win);
                        win.meta.change_workspace_by_index(neighbor.index(), false);
                        this.size_signals_unblock(win);
                    }
                } else {
                    this.workspace_window_move(win, monitor, monitor);
                }

                this.workspace_active.set(neighbor.index(), win.entity);

                win.activate_after_move = true;
            };

            if (neighbor && neighbor.index() !== ws.index()) {
                move_to_neighbor(neighbor);
            } else if (direction === Meta.MotionDirection.DOWN && !last_window()) {
                if (this.settings.dynamic_workspaces()) {
                    neighbor = wom.append_new_workspace(false, global.get_current_time());
                } else {
                    return;
                }
            } else if (direction === Meta.MotionDirection.UP && ws.index() === 0) {
                if (this.settings.dynamic_workspaces()) {
                    // Add a new workspace, to push everyone to free up the first one
                    wom.append_new_workspace(false, global.get_current_time());

                    // Move everything one workspace down
                    this.on_workspace_modify(
                        () => true,
                        (current) => current + 1,
                        true,
                    );

                    neighbor = wom.get_workspace_by_index(0);

                    if (!neighbor) return;

                    move_to_neighbor(neighbor);
                } else {
                    return;
                }
            } else {
                return;
            }

            this.size_signals_block(win);
            win.meta.change_workspace_by_index(neighbor.index(), true);
            neighbor.activate_with_focus(win.meta, global.get_current_time());
            this.size_signals_unblock(win);
        };

        switch (direction) {
            case Meta.DisplayDirection.DOWN:
                workspace_move(Meta.MotionDirection.DOWN);
                break;

            case Meta.DisplayDirection.UP:
                workspace_move(Meta.MotionDirection.UP);
                break;
        }

        if (this.auto_tiler) this.restack();
    }

    /** Triggered when a grab operation has been started */
    on_grab_start(meta: null | Meta.Window, op: any) {
        if (!meta) return;
        let win = this.get_window(meta);
        if (win) {
            win.grab = true;

            if (win.is_tilable(this)) {
                let entity = win.entity;
                let rect = win.rect();

                this.unset_grab_op();

                this.grab_op = new GrabOp.GrabOp(entity, rect);

                this.size_signals_block(win);

                /** Display an overlay indicating where the window will be placed if dropped */

                if (overview.visible || !win || is_keyboard_op(op) || is_resize_op(op)) return;

                const workspace = this.active_workspace();

                this.drag_signal = GLib.timeout_add(GLib.PRIORITY_LOW, 200, () => {
                    this.overlay.visible = false;

                    if (!win || !this.auto_tiler || !this.grab_op || this.grab_op.entity !== entity) {
                        this.drag_signal = null;
                        return false;
                    }

                    const [cursor, monitor] = this.cursor_status();

                    let attach_to = null;
                    for (const found of this.windows_at_pointer(cursor, monitor, workspace)) {
                        if (found != win && this.auto_tiler.attached.contains(found.entity)) {
                            attach_to = found;
                            break;
                        }
                    }

                    const fork = this.auto_tiler.get_parent_fork(entity);
                    if (!fork) return true;

                    let windowless = this.auto_tiler.largest_on_workspace(this, monitor, workspace) === null;

                    if (attach_to === null) {
                        if (fork.left.inner.kind === 2 && fork.right?.inner.kind === 2) {
                            let attaching = fork.left.is_window(entity)
                                ? fork.right.inner.entity
                                : fork.left.inner.entity;

                            attach_to = this.windows.get(attaching);
                        }
                    }

                    let area, monitor_attachment;

                    if (windowless) {
                        [area, monitor_attachment] = [this.monitor_work_area(monitor), true];
                        area.x += this.gap_outer;
                        area.y += this.gap_outer;
                        area.width -= this.gap_outer * 2;
                        area.height -= this.gap_outer * 2;
                    } else if (attach_to) {
                        const is_sibling = this.auto_tiler.windows_are_siblings(entity, attach_to.entity);

                        [area, monitor_attachment] =
                            (win.stack === null && attach_to.stack === null && is_sibling) ||
                            (win.stack === null && is_sibling)
                                ? [fork.area, false]
                                : [attach_to.meta.get_frame_rect(), false];
                    } else {
                        return true;
                    }

                    const result = monitor_attachment ? null : auto_tiler.cursor_placement(this, area, cursor);

                    if (!result) {
                        this.overlay.x = area.x;
                        this.overlay.y = area.y;
                        this.overlay.width = area.width;
                        this.overlay.height = area.height;

                        this.overlay.visible = true;

                        return true;
                    }

                    const { orientation, swap } = result;

                    const half_width = area.width / 2;
                    const half_height = area.height / 2;

                    let new_area: [number, number, number, number] =
                        orientation === Lib.Orientation.HORIZONTAL
                            ? swap
                                ? [area.x, area.y, half_width, area.height]
                                : [area.x + half_width, area.y, half_width, area.height]
                            : swap
                            ? [area.x, area.y, area.width, half_height]
                            : [area.x, area.y + half_height, area.width, half_height];

                    this.overlay.x = new_area[0];
                    this.overlay.y = new_area[1];
                    this.overlay.width = new_area[2];
                    this.overlay.height = new_area[3];

                    this.overlay.visible = true;

                    return true;
                });
            }
        }
    }

    on_gtk_shell_changed() {
        this.reload_theme();
        load_theme(this.current_style);
    }

    on_gtk_theme_change() {
        this.reload_theme();
        load_theme(this.current_style);
    }

    reload_theme() {
        this.current_style = this.settings.is_dark()
            ? Style.Dark
            : this.settings.is_high_contrast()
            ? Style.HighContrast
            : Style.Light;
    }

    /** Handle window maximization notifications */
    on_maximize(win: Window.ShellWindow) {
        if (win.is_maximized()) {
            // Raise maximized to top so stacks won't appear over them.
            const actor = win.meta.get_compositor_private();
            if (actor) global.window_group.set_child_above_sibling(actor, null);

            this.on_monitor_changed(win, (_cfrom, cto, workspace) => {
                if (win) {
                    win.ignore_detach = true;
                    this.monitors.insert(win.entity, [cto, workspace]);
                    this.auto_tiler?.detach_window(this, win.entity);
                }
            });
        } else {
            // Retile on unmaximize after waiting for other events to complete, such as animations
            this.register_fn(() => {
                if (this.auto_tiler) {
                    let fork_ent = this.auto_tiler.attached.get(win.entity);
                    if (fork_ent) {
                        let fork = this.auto_tiler.forest.forks.get(fork_ent);
                        if (fork) this.auto_tiler.tile(this, fork, fork.area);
                    }
                }
            });
        }
    }

    /** Handle window minimization notifications */
    on_minimize(win: Window.ShellWindow) {
        if (this.focus_window() == win && this.settings.active_hint()) {
            if (win.meta.minimized) {
                win.hide_border();
            } else {
                this.show_border_on_focused();
            }
        }

        if (this.auto_tiler) {
            if (win.meta.minimized) {
                const attached = this.auto_tiler.attached.get(win.entity);
                if (!attached) return;

                const fork = this.auto_tiler.forest.forks.get(attached);
                if (!fork) return;

                let attachment: boolean | number;
                if (win.stack !== null) {
                    attachment = win.stack;
                } else {
                    attachment = fork.left.is_window(win.entity);
                }

                win.was_attached_to = [attached, attachment];
                this.auto_tiler.detach_window(this, win.entity);
            } else if (!this.contains_tag(win.entity, Tags.Floating)) {
                if (win.was_attached_to) {
                    const [entity, attachment] = win.was_attached_to;
                    delete win.was_attached_to;

                    const tiler = this.auto_tiler;

                    const fork = tiler.forest.forks.get(entity);
                    if (fork) {
                        if (typeof attachment === 'boolean') {
                            tiler.forest.attach_fork(this, fork, win.entity, attachment);
                            tiler.tile(this, fork, fork.area);
                            return;
                        } else {
                            const stack = tiler.forest.stacks.get(attachment);
                            if (stack) {
                                const stack_info = tiler.find_stack(stack.active);
                                if (stack_info) {
                                    const node = stack_info[1].inner as node.NodeStack;

                                    win.stack = attachment;
                                    node.entities.push(win.entity);
                                    tiler.update_stack(this, node);
                                    tiler.forest.on_attach(fork.entity, win.entity);
                                    stack.activate(win.entity);
                                    tiler.tile(this, fork, fork.area);
                                    return;
                                }
                            }
                        }
                    }
                }

                this.auto_tiler.auto_tile(this, win, false);
            }
        }
    }

    /** Handles the event of a window moving from one monitor to another. */
    on_monitor_changed(
        win: Window.ShellWindow,
        func: (exp_mon: null | number, act_mon: number, act_work: number) => void,
    ) {
        const actual_monitor = win.meta.get_monitor();
        const actual_workspace = win.workspace_id();
        const monitor = this.monitors.get(win.entity);

        if (monitor) {
            const [expected_monitor, expected_workspace] = monitor;
            if (expected_monitor != actual_monitor || actual_workspace != expected_workspace) {
                func(expected_monitor, actual_monitor, actual_workspace);
            }
        } else {
            func(null, actual_monitor, actual_workspace);
        }
    }

    on_overview_shown() {
        this.exit_modes();
        this.unset_grab_op();
    }

    on_show_window_titles() {
        const show_title = this.settings.show_title();

        if (indicator) {
            indicator.toggle_titles.setToggleState(show_title);
        }

        for (const window of this.windows.values()) {
            if (window.is_client_decorated()) continue;

            if (show_title) {
                window.decoration_show(this);
            } else {
                window.decoration_hide(this);
            }
        }
    }

    on_smart_gap() {
        if (this.auto_tiler) {
            const smart_gaps = this.settings.smart_gaps();
            for (const [entity, [mon]] of this.auto_tiler.forest.toplevel.values()) {
                const node = this.auto_tiler.forest.forks.get(entity);
                if (node?.right === null) {
                    this.auto_tiler.update_toplevel(this, node, mon, smart_gaps);
                }
            }
        }
    }

    on_window_create(window: Meta.Window, actor: Clutter.Actor) {
        let win = this.get_window(window);
        if (win) {
            const entity = win.entity;
            actor.connect('destroy', () => {
                if (win && win.border) {
                    win.border.destroy();
                    win.border = null;
                }

                this.on_destroy(entity);

                return false;
            });

            if (win.is_tilable(this)) {
                this.connect_window(win);
            }
        }
    }

    on_workspace_added(_number: number) {
        this.ignore_display_update = true;
    }

    /** Handle workspace change events */
    on_workspace_changed(win: Window.ShellWindow) {
        if (this.auto_tiler && !this.contains_tag(win.entity, Tags.Floating)) {
            const id = this.workspace_id(win);
            const prev_id = this.monitors.get(win.entity);
            if (!prev_id || id[0] != prev_id[0] || id[1] != prev_id[1]) {
                win.ignore_detach = true;
                this.monitors.insert(win.entity, id);
                if (win.is_tilable(this)) {
                    this.auto_tiler.detach_window(this, win.entity);
                    this.auto_tiler.attach_to_workspace(this, win, id);
                }
            }

            if (win.meta.minimized) {
                this.size_signals_block(win);
                win.meta.unminimize();
                this.size_signals_unblock(win);
            }
        }
    }

    on_workspace_index_changed(prev: number, next: number) {
        this.on_workspace_modify(
            (current) => current == prev,
            (_) => next,
        );
    }

    on_workspace_modify(
        condition: (current: number) => boolean,
        modify: (current: number) => number,
        change_workspace: boolean = false,
    ) {
        function window_move(ext: Ext, entity: Entity, ws: WorkspaceID) {
            if (change_workspace) {
                const window = ext.windows.get(entity);
                if (!window || !window.actor_exists() || window.meta.is_on_all_workspaces()) return;

                ext.size_signals_block(window);
                window.meta.change_workspace_by_index(ws, false);
                ext.size_signals_unblock(window);
            }
        }

        if (this.auto_tiler) {
            for (const [entity, monitor] of this.auto_tiler.forest.toplevel.values()) {
                if (condition(monitor[1])) {
                    const value = modify(monitor[1]);
                    monitor[1] = value;
                    let fork = this.auto_tiler.forest.forks.get(entity);
                    if (fork) {
                        fork.workspace = value;
                        for (const child of this.auto_tiler.forest.iter(entity)) {
                            if (child.inner.kind === 1) {
                                fork = this.auto_tiler.forest.forks.get(child.inner.entity);
                                if (fork) fork.workspace = value;
                            } else if (child.inner.kind === 2) {
                                window_move(this, child.inner.entity, value);
                            } else if (child.inner.kind === 3) {
                                let stack = this.auto_tiler.forest.stacks.get(child.inner.idx);
                                if (stack) {
                                    stack.workspace = value;

                                    for (const entity of child.inner.entities) {
                                        window_move(this, entity, value);
                                    }

                                    stack.restack();
                                }
                            }
                        }
                    }
                }
            }

            // Fix phantom apps in dash
            for (const window of this.windows.values()) {
                if (!window.actor_exists()) this.auto_tiler.detach_window(this, window.entity);
            }
        } else {
            let to_delete = new Array();

            for (const [entity, window] of this.windows.iter()) {
                if (!window.actor_exists()) {
                    to_delete.push(entity);
                    continue;
                }

                const ws = window.workspace_id();
                if (condition(ws)) {
                    window_move(this, entity, modify(ws));
                }
            }

            for (const e of to_delete) this.delete_entity(e);
        }
    }

    on_workspace_removed(number: number) {
        this.on_workspace_modify(
            (current) => current > number,
            (prev) => prev - 1,
        );
    }

    restack() {
        // NOTE: Workaround for GNOME Shell showing our hidden windows on a workspace switch
        let attempts = 0;
        GLib.timeout_add(GLib.PRIORITY_LOW, 50, () => {
            if (this.auto_tiler) {
                for (const container of this.auto_tiler.forest.stacks.values()) {
                    container.restack();
                }
            }

            let x = attempts;
            attempts += 1;
            return x < 10;
        });
    }

    set_gap_inner(gap: number) {
        this.gap_inner_prev = this.gap_inner;
        this.gap_inner = gap * 4 * this.dpi;
        this.gap_inner_half = this.gap_inner / 2;
    }

    set_gap_outer(gap: number) {
        this.gap_outer_prev = this.gap_outer;
        this.gap_outer = gap * 4 * this.dpi;
    }

    set_overlay(rect: Rectangle) {
        this.overlay.x = rect.x;
        this.overlay.y = rect.y;
        this.overlay.width = rect.width;
        this.overlay.height = rect.height;
    }

    /** Begin listening for signals from windows, and add any pre-existing windows. */
    signals_attach() {
        // this.conf_watch = this.attach_config();

        this.tiler.queue.start(100, (movement) => {
            movement();
            return true;
        });

        const workspace_manager = wom;

        for (const [, ws] of iter_workspaces(workspace_manager)) {
            let index = ws.index();

            this.connect(ws, 'notify::workspace-index', () => {
                if (ws !== null) {
                    let new_index = ws.index();
                    this.on_workspace_index_changed(index, new_index);
                    index = new_index;
                }
            });
        }

        this.connect(display, 'workareas-changed', () => {
            this.update_display_configuration(true);
        });

        this.size_changed_signal = this.connect(wim, 'size-change', (_, actor, event, _before, _after) => {
            if (this.auto_tiler) {
                let win = this.get_window(actor.get_meta_window());
                if (!win) return;

                if (event === Meta.SizeChange.MAXIMIZE || event === Meta.SizeChange.UNMAXIMIZE) {
                    this.register(Events.window_event(win, WindowEvent.Maximize));
                } else {
                    this.register(Events.window_event(win, WindowEvent.Fullscreen));
                }
            }
        });

        this.connect(this.settings.ext, 'changed', (_s, key: string) => {
            switch (key) {
                case 'active-hint':
                    if (indicator) indicator.toggle_active.setToggleState(this.settings.active_hint());

                    this.show_border_on_focused();
                case 'gap-inner':
                    this.on_gap_inner();
                    break;
                case 'gap-outer':
                    this.on_gap_outer();
                    break;
                case 'show-title':
                    this.on_show_window_titles();
                    break;
                case 'smart-gaps':
                    this.on_smart_gap();
                    this.show_border_on_focused();
                    break;
                case 'show-skip-taskbar':
                    if (this.settings.show_skiptaskbar()) {
                        _show_skip_taskbar_windows(this);
                    } else {
                        _hide_skip_taskbar_windows();
                    }
            }
        });

        if (this.settings.mutter) {
            this.connect(this.settings.mutter, 'changed::workspaces-only-on-primary', () => {
                this.register(Events.global(GlobalEvent.MonitorsChanged));
            });
        }

        this.connect(layoutManager, 'monitors-changed', () => {
            this.register(Events.global(GlobalEvent.MonitorsChanged));
        });

        this.connect(sessionMode, 'updated', () => {
            if (indicator) {
                indicator.button.visible = !sessionMode.isLocked;
            }

            if (sessionMode.isLocked) {
                this.exit_modes();
            }
        });

        this.connect(overview, 'showing', () => {
            this.register(Events.global(GlobalEvent.OverviewShown));
        });

        this.connect(overview, 'hiding', () => {
            const window = this.focus_window();
            if (window) {
                this.on_focused(window);
            }
            this.register(Events.global(GlobalEvent.OverviewHidden));
        });

        // We have to connect this signal in an idle_add; otherwise work areas stop being calculated
        this.register_fn(() => {
            if (screenShield?.locked) this.update_display_configuration(false);

            this.connect(display, 'notify::focus-window', () => {
                // Disallow refocus if a modal window is active
                if (Main.modalCount !== 0) {
                    const { actor } = Main.modalActorFocusStack[0];
                    if (actor.style_class !== 'switcher-popup') {
                        return;
                    }
                }

                const refocus_tiled_window = () => {
                    // Re-focus a window that was unfocused.
                    let window: Window.ShellWindow | null = null;
                    const [x, y] = this.prev_focused;

                    if (y) {
                        window = this.windows.get(y);
                    }

                    if (window === null && x) {
                        window = this.windows.get(x);
                    }

                    if (window && window.same_monitor() && window.same_workspace() && !window.meta.minimized) {
                        window.activate(false);
                    } else {
                        this.hide_all_borders();
                    }
                };

                // Delay in case the focused window was not focused yet.
                // Note: Fixes Intellij IDE windows.
                this.register_fn(() => {
                    let meta_window = global.display.get_focus_window();

                    if (meta_window) {
                        const shell_window = this.get_window(meta_window);

                        if (shell_window) {
                            // Avoid re-focusing a window that's already focused.
                            if (shell_window.entity !== this.prev_focused[1] && !shell_window.meta.minimized) {
                                this.on_focused(shell_window);
                            }
                        } else if (!meta_window.is_override_redirect()) {
                            // Prevent focusing desktop extension in auto-tiler mode
                            if (this.auto_tiler && meta_window.window_type === Meta.WindowType.DESKTOP) {
                                refocus_tiled_window();
                            } else {
                                // This section fixes Steam's sub-menus.
                                meta_window.activate(global.get_current_time());
                            }
                        }
                    } else if (this.auto_tiler) {
                        refocus_tiled_window();
                    }
                });

                return false;
            });

            const window = this.focus_window();
            if (window) {
                this.on_focused(window);
            }

            return false;
        });

        this.connect(display, 'window_created', (_, window: Meta.Window) => {
            this.register({ tag: 3, window });
        });

        if (GNOME_VERSION?.startsWith('3.')) {
            this.connect(display, 'grab-op-begin', (_, _display, win, op) => {
                this.on_grab_start(win, op);
            });

            this.connect(display, 'grab-op-end', (_, _display, win, op) => {
                this.register_fn(() => this.on_grab_end(win, op));
            });
        } else {
            // GNOME 40 removed the first argument of the callback
            this.connect(display, 'grab-op-begin', (_display, win, op) => {
                this.on_grab_start(win, op);
            });

            this.connect(display, 'grab-op-end', (_display, win, op) => {
                this.register_fn(() => this.on_grab_end(win, op));
            });
        }

        this.connect(overview, 'window-drag-begin', (_, win) => {
            this.on_grab_start(win, 1);
        });

        this.connect(overview, 'window-drag-end', (_, win) => {
            this.register_fn(() => this.on_grab_end(win));
        });

        this.connect(overview, 'window-drag-cancelled', () => {
            this.unset_grab_op();
        });

        this.connect(wim, 'switch-workspace', () => {
            this.hide_all_borders();
        });

        this.connect(workspace_manager, 'active-workspace-changed', () => {
            this.on_active_workspace_changed();
        });

        this.connect(workspace_manager, 'workspace-removed', (_, number) => {
            this.on_workspace_removed(number);
        });

        this.connect(workspace_manager, 'workspace-added', (_, number) => {
            this.on_workspace_added(number);
        });

        // Bind show desktop and remove the active hint
        this.connect(workspace_manager, 'showing-desktop-changed', () => {
            this.hide_all_borders();
            this.prev_focused = [null, null];
        });

        St.ThemeContext.get_for_stage(global.stage).connect('notify::scale-factor', () => this.update_scale());

        // Modes

        if (this.settings.tile_by_default() && !this.auto_tiler) {
            this.auto_tiler = new auto_tiler.AutoTiler(
                new Forest.Forest()
                    .connect_on_attach(this.on_tile_attach.bind(this))
                    .connect_on_detach(this.on_tile_detach.bind(this)),
                this.register_storage<Entity>(),
            );
        }

        // Post-init

        if (this.init) {
            for (const window of this.tab_list(Meta.TabList.NORMAL, null)) {
                this.register({ tag: 3, window: window.meta });
            }

            this.register_fn(() => (this.init = false));
        }
    }

    signals_remove() {
        for (const [object, signals] of this.signals) {
            for (const signal of signals) {
                object.disconnect(signal);
            }
        }

        if (this.conf_watch) {
            this.conf_watch[0].disconnect(this.conf_watch[1]);
            this.conf_watch = null;
        }

        this.tiler.queue.stop();

        this.signals.clear();
    }

    size_changed_block() {
        utils.block_signal(wim, this.size_changed_signal);
    }

    size_changed_unblock() {
        utils.unblock_signal(wim, this.size_changed_signal);
    }

    size_signals_block(win: Window.ShellWindow) {
        this.add_tag(win.entity, Tags.Blocked);
    }

    size_signals_unblock(win: Window.ShellWindow) {
        this.delete_tag(win.entity, Tags.Blocked);
    }

    // Snaps all windows to the window grid
    snap_windows() {
        for (const window of this.windows.values()) {
            if (window.is_tilable(this)) this.tiler.snap(this, window);
        }
    }

    /** Switch to a workspace by its index */
    switch_to_workspace(id: number) {
        this.workspace_by_id(id)?.activate(global.get_current_time());
    }

    stop_launcher_services() {
        this.window_search.stop_services(this);
    }

    tab_list(tablist: number, workspace: Meta.Workspace | null): Array<Window.ShellWindow> {
        const windows = display.get_tab_list(tablist, workspace);

        const matched = new Array();

        for (const window of windows) {
            const win = this.get_window(window);
            if (win) matched.push(win);
        }

        return matched;
    }

    *tiled_windows(): IterableIterator<Entity> {
        for (const entity of this.entities()) {
            if (this.contains_tag(entity, Tags.Tiled)) {
                yield entity;
            }
        }
    }

    /// If the auto-tilable status of a window has changed, detach or attach the window.
    tiling_config_reapply() {
        if (this.auto_tiler) {
            const at = this.auto_tiler;
            for (const [entity, window] of this.windows.iter()) {
                const attachment = at.attached.get(entity);
                if (window.is_tilable(this)) {
                    if (!attachment) {
                        at.auto_tile(this, window, this.init);
                    }
                } else if (attachment) {
                    at.detach_window(this, entity);
                }
            }
        }
    }

    toggle_tiling() {
        if (this.settings.tile_by_default()) {
            this.auto_tile_off();
        } else {
            this.auto_tile_on();
        }
    }

    auto_tile_off() {
        this.settings.set_edge_tiling(true);
        this.hide_all_borders();

        if (this.auto_tiler) {
            this.unregister_storage(this.auto_tiler.attached);
            this.auto_tiler.destroy(this);
            this.auto_tiler = null;
            this.settings.set_tile_by_default(false);

            if (indicator) indicator.toggle_tiled.setToggleState(false);

            this.button.icon.gicon = this.button_gio_icon_auto_off; // type: Gio.Icon

            if (this.settings.active_hint()) {
                this.show_border_on_focused();
            }
        }
    }

    auto_tile_on() {
        this.settings.set_edge_tiling(false);
        this.hide_all_borders();

        if (indicator) indicator.toggle_tiled.setToggleState(true);

        const original = this.active_workspace();

        let tiler = new auto_tiler.AutoTiler(
            new Forest.Forest()
                .connect_on_attach(this.on_tile_attach.bind(this))
                .connect_on_detach(this.on_tile_detach.bind(this)),
            this.register_storage(),
        );

        this.auto_tiler = tiler;

        this.settings.set_tile_by_default(true);
        this.button.icon.gicon = this.button_gio_icon_auto_on; // type: Gio.Icon

        for (const window of this.windows.values()) {
            if (window.is_tilable(this)) {
                let actor = window.meta.get_compositor_private();
                if (actor) {
                    if (!window.meta.minimized) {
                        tiler.auto_tile(this, window, true);
                    }
                }
            }
        }

        this.register_fn(() => this.switch_to_workspace(original));
    }

    /** Calls a function once windows are no longer queued for movement. */
    schedule_idle(func: () => boolean): boolean {
        if (!this.movements.is_empty()) {
            GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => {
                if (!this.movements.is_empty()) return true;
                return func();
            });
        } else {
            func();
        }
        return false;
    }

    should_ignore_workspace(monitor: number): boolean {
        return this.settings.workspaces_only_on_primary() && monitor !== global.display.get_primary_monitor();
    }

    unset_grab_op() {
        if (this.drag_signal !== null) {
            this.overlay.visible = false;
            GLib.source_remove(this.drag_signal);
            this.drag_signal = null;
        }

        if (this.grab_op !== null) {
            let window = this.windows.get(this.grab_op.entity);
            if (window) this.size_signals_unblock(window);
            this.grab_op = null;
        }

        this.moved_by_mouse = false;
    }

    update_display_configuration_before() {}

    update_display_configuration(workareas_only: boolean) {
        if (!this.auto_tiler || sessionMode.isLocked) return;

        if (this.ignore_display_update) {
            this.ignore_display_update = false;
            return;
        }

        // Ignore the update if there are no monitors to assign to
        if (layoutManager.monitors.length === 0) return;

        const primary_display = global.display.get_primary_monitor();

        const primary_display_ready = (ext: Ext): boolean => {
            const area = global.display.get_monitor_geometry(primary_display);
            const work_area = ext.monitor_work_area(primary_display);

            if (!area || !work_area) return false;

            return !(area.width === work_area.width && area.height === work_area.height);
        };

        function displays_ready(): boolean {
            const monitors = global.display.get_n_monitors();

            if (monitors === 0) return false;

            for (let i = 0; i < monitors; i += 1) {
                const display = global.display.get_monitor_geometry(i);

                if (!display) return false;

                if (display.width < 1 || display.height < 1) return false;
            }

            return true;
        }

        if (!displays_ready() || !primary_display_ready(this)) {
            if (this.displays_updating !== null) return;
            if (this.workareas_update !== null) GLib.source_remove(this.workareas_update);

            this.workareas_update = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => {
                this.register_fn(() => {
                    this.update_display_configuration(workareas_only);
                });

                this.workareas_update = null;

                return false;
            });

            return;
        }

        // Update every tree on each display with the new dimensions
        const update_tiling = () => {
            if (!this.auto_tiler) return;

            for (const f of this.auto_tiler.forest.forks.values()) {
                if (!f.is_toplevel) continue;

                const display = this.monitor_work_area(f.monitor);

                if (display) {
                    const area = new Rect.Rectangle([display.x, display.y, display.width, display.height]);

                    f.smart_gapped = false;
                    f.set_area(area.clone());
                    this.auto_tiler.update_toplevel(this, f, f.monitor, this.settings.smart_gaps());
                }
            }
        };

        type Migration = [Fork, number, Rectangle, boolean];

        let migrations: Array<Migration> = new Array();

        const apply_migrations = (assigned_monitors: Set<number>) => {
            if (!migrations) return;

            new exec.OnceExecutor<Migration, Migration[]>(migrations).start(
                500,
                ([fork, new_monitor, workspace, find_workspace]) => {
                    let new_workspace;

                    if (find_workspace) {
                        if (assigned_monitors.has(new_monitor)) {
                            [new_workspace] = this.find_unused_workspace(new_monitor);
                        } else {
                            assigned_monitors.add(new_monitor);
                            new_workspace = 0;
                        }
                    } else {
                        new_workspace = fork.workspace;
                    }

                    fork.migrate(this, forest, workspace, new_monitor, new_workspace);
                    fork.set_ratio(fork.length() / 2);

                    return true;
                },
                () => update_tiling(),
            );
        };

        function mark_for_reassignment(ext: Ext, fork: Ecs.Entity) {
            for (const win of forest.iter(fork, node.NodeKind.WINDOW)) {
                if (win.inner.kind === 2) {
                    const entity = win.inner.entity;
                    const window = ext.windows.get(entity);
                    if (window) window.reassignment = true;
                }
            }
        }

        const [old_primary, old_displays] = this.displays;

        const changes = new Map<number, number>();

        // Records which display's windows were moved to what new display's ID
        for (const [entity, w] of this.windows.iter()) {
            if (!w.actor_exists()) continue;

            this.monitors.with(entity, ([mon]) => {
                const assignment = mon === old_primary ? primary_display : w.meta.get_monitor();
                changes.set(mon, assignment);
            });
        }

        // Fetch a new list of monitors
        const updated = new Map();

        for (const monitor of layoutManager.monitors) {
            const mon = monitor as Monitor;

            const area = new Rect.Rectangle([mon.x, mon.y, mon.width, mon.height]);
            const ws = this.monitor_work_area(mon.index);

            updated.set(mon.index, { area, ws });
        }

        const forest = this.auto_tiler.forest;

        if (old_displays.size === updated.size) {
            update_tiling();

            this.displays = [primary_display, updated];

            return;
        }

        this.displays = [primary_display, updated];

        if (utils.map_eq(old_displays, updated)) {
            return;
        }

        if (this.displays_updating !== null) GLib.source_remove(this.displays_updating);

        if (this.workareas_update !== null) {
            GLib.source_remove(this.workareas_update);
            this.workareas_update = null;
        }

        // Delay actions in case of temporary connection loss
        this.displays_updating = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 2000, () => {
            (() => {
                if (!this.auto_tiler) return;

                const toplevels = new Array();
                const assigned_monitors = new Set<number>();

                for (const [old_mon, new_mon] of changes) {
                    if (old_mon === new_mon) assigned_monitors.add(new_mon);
                }

                for (const f of forest.forks.values()) {
                    if (f.is_toplevel) {
                        toplevels.push(f);

                        let migration: null | [Fork, number, Rectangle, boolean] = null;

                        const displays = this.displays[1];

                        for (const [old_monitor, new_monitor] of changes) {
                            const display = displays.get(new_monitor);

                            if (!display) continue;

                            if (f.monitor === old_monitor) {
                                f.monitor = new_monitor;
                                f.workspace = 0;
                                migration = [f, new_monitor, display.ws, true];
                            }
                        }

                        if (!migration) {
                            const display = displays.get(f.monitor);
                            if (display) {
                                migration = [f, f.monitor, display.ws, false];
                            }
                        }

                        if (migration) {
                            mark_for_reassignment(this, migration[0].entity);
                            migrations.push(migration);
                        }
                    }
                }

                apply_migrations(assigned_monitors);

                return;
            })();

            this.displays_updating = null;
            return false;
        });
    }

    update_scale() {
        const new_dpi = St.ThemeContext.get_for_stage(global.stage).scale_factor;
        const diff = new_dpi / this.dpi;
        this.dpi = new_dpi;

        this.column_size *= diff;
        this.row_size *= diff;

        this.gap_inner_prev *= diff;
        this.gap_inner *= diff;
        this.gap_inner_half *= diff;
        this.gap_outer_prev *= diff;
        this.gap_outer *= diff;

        this.update_inner_gap();
        this.update_outer_gap(diff);
    }

    update_snapped() {
        for (const entity of this.snapped.find((val) => val)) {
            const window = this.windows.get(entity);
            if (window) this.tiler.snap(this, window);
        }
    }

    /// Fetches the window entity which is associated with the metacity window metadata.
    window_entity(meta: Meta.Window | null): Entity | null {
        if (!meta) return null;

        let id: number;

        try {
            id = meta.get_stable_sequence();
        } catch (e) {
            return null;
        }

        // Locate the window entity with the matching ID
        let entity = this.ids.find((comp) => comp == id).next().value;

        // If not found, create a new entity with a ShellWindow component.
        if (!entity) {
            const actor = meta.get_compositor_private();
            if (!actor) return null;

            let window_app: any, name: string;

            try {
                window_app = Window.window_tracker.get_window_app(meta);
                name = window_app.get_name().replace(/&/g, '&amp;');
            } catch (e) {
                return null;
            }

            // Only permit normal, dialog, and modal dialogs
            const window_type = (meta as any).get_window_type();
            if (window_type !== 0 && window_type !== 3 && window_type !== 4) {
                return null;
            }

            entity = this.create_entity();

            this.ids.insert(entity, id);
            this.names.insert(entity, name);

            let win = new Window.ShellWindow(entity, meta, window_app, this);

            this.windows.insert(entity, win);
            this.monitors.insert(entity, [win.meta.get_monitor(), win.workspace_id()]);

            const grab_focus = () => {
                this.schedule_idle(() => {
                    this.windows.with(entity, (window) => {
                        window.meta.raise();
                        window.meta.unminimize();
                        window.activate(false);
                    });

                    return false;
                });
            };

            if (this.auto_tiler && !win.meta.minimized && win.is_tilable(this)) {
                let id = actor.connect('first-frame', () => {
                    this.auto_tiler?.auto_tile(this, win, this.init);
                    grab_focus();
                    actor.disconnect(id);
                });
            } else {
                grab_focus();
            }
        }
        return entity;
    }

    /// Returns the tilable window(s) that the mouse pointer is currently hovering above.
    *windows_at_pointer(cursor: Rectangle, monitor: number, workspace: number): IterableIterator<Window.ShellWindow> {
        for (const entity of this.monitors.find((m) => m[0] == monitor && m[1] == workspace)) {
            let window = this.windows.with(entity, (window) => {
                return window.is_tilable(this) && window.rect().contains(cursor) ? window : null;
            });

            if (window) yield window;
        }
    }

    cursor_status(): [Rectangle, number] {
        const cursor = cursor_rect();
        const rect = new Meta.Rectangle({ x: cursor.x, y: cursor.y, width: 1, height: 1 });
        const monitor = display.get_monitor_index_for_rect(rect);
        return [cursor, monitor];
    }

    /** Fetch a workspace by its index */
    workspace_by_id(id: number): Meta.Workspace | null {
        return wom.get_workspace_by_index(id);
    }

    workspace_id(window: Window.ShellWindow | null = null): [number, number] {
        let id: [number, number] = window
            ? [window.meta.get_monitor(), window.workspace_id()]
            : [this.active_monitor(), this.active_workspace()];

        id[0] = Math.max(0, id[0]);
        id[1] = Math.max(0, id[1]);

        return id;
    }

    is_floating(window: Window.ShellWindow): boolean {
        let shall_float: boolean = false;
        let wm_class = window.meta.get_wm_class();
        let wm_title = window.meta.get_title();

        if (wm_class && wm_title) {
            shall_float = this.conf.window_shall_float(wm_class, wm_title);
        }

        let floating_tagged = this.contains_tag(window.entity, Tags.Floating);
        let force_tiled_tagged = this.contains_tag(window.entity, Tags.ForceTile);
        // Tags.Tiled does not seem to matter, so not checking here

        return (floating_tagged && !force_tiled_tagged) || (shall_float && !force_tiled_tagged);
    }
}

let ext: Ext | null = null;
let indicator: Indicator | null = null;

declare global {
  var popShellExtension: any;
}

export default class PopShellExtension extends Extension {
    enable() {
        globalThis.popShellExtension  = this;
        log.info('enable');

        if (!ext) {
            ext = new Ext();

            ext.register_fn(() => {
                if (ext?.auto_tiler) ext.snap_windows();
            });
        }

        if (ext.settings.show_skiptaskbar()) {
            _show_skip_taskbar_windows(ext);
        } else {
            _hide_skip_taskbar_windows();
        }

        if (ext.was_locked) {
            ext.was_locked = false;
            return;
        }

        ext.injections_add();
        ext.signals_attach();

        disable_window_attention_handler();

        layoutManager.addChrome(ext.overlay);

        if (!indicator) {
            indicator = new PanelSettings.Indicator(ext);
            panel.addToStatusArea('pop-shell', indicator.button);
        }

        ext.keybindings.enable(ext.keybindings.global).enable(ext.keybindings.window_focus);

        if (ext.settings.tile_by_default()) {
            ext.auto_tile_on();
        }
    }
    disable() {
        log.info('disable');

        if (ext) {
            if (sessionMode.isLocked) {
                ext.was_locked = true;
                return;
            }

            delete globalThis.popShellExtension;
            ext.injections_remove();
            ext.signals_remove();
            ext.exit_modes();
            ext.stop_launcher_services();
            ext.hide_all_borders();
            ext.window_search.remove_injections();

            layoutManager.removeChrome(ext.overlay);

            ext.keybindings.disable(ext.keybindings.global).disable(ext.keybindings.window_focus);

            if (ext.auto_tiler) {
                ext.auto_tiler.destroy(ext);
                ext.auto_tiler = null;
            }

            _hide_skip_taskbar_windows();
        }

        if (indicator) {
            indicator.destroy();
            indicator = null;
        }

        enable_window_attention_handler();
    }
}

const handler = windowAttentionHandler;

function enable_window_attention_handler() {
    if (handler && !handler._windowDemandsAttentionId) {
        handler._windowDemandsAttentionId = global.display.connect('window-demands-attention', (display, window) => {
            handler._onWindowDemandsAttention(display, window);
        });
    }
}

function disable_window_attention_handler() {
    if (handler && handler._windowDemandsAttentionId) {
        global.display.disconnect(handler._windowDemandsAttentionId);
        handler._windowDemandsAttentionId = null;
    }
}

function stylesheet_path(name: string) {
    return get_current_path() + '/' + name + '.css';
}

// Supplements the loaded theme with the extension's theme.
function load_theme(style: Style): string | any {
    let pop_stylesheet = Number(style);
    try {
        const theme_context = St.ThemeContext.get_for_stage(global.stage);

        const existing_theme: null | any = theme_context.get_theme();

        const pop_stylesheet_path = STYLESHEET_PATHS[pop_stylesheet];

        if (existing_theme) {
            /* Must unload stylesheets, or else the previously loaded
             * stylesheets will persist when loadTheme() is called
             * (found in source code of imports.ui.main).
             */
            for (const s of STYLESHEETS) {
                existing_theme.unload_stylesheet(s);
            }

            // Merge theme update with pop shell styling
            existing_theme.load_stylesheet(STYLESHEETS[pop_stylesheet]);

            // Perform theme update
            theme_context.set_theme(existing_theme);
        } else {
            // User does not have a theme loaded, so use pop styling + default
            setThemeStylesheet(pop_stylesheet_path);
            loadTheme();
        }

        return pop_stylesheet_path;
    } catch (e) {
        log.error('failed to load stylesheet: ' + e);
        return null;
    }
}

function* iter_workspaces(manager: any): IterableIterator<[number, any]> {
    let idx = 0;
    let ws = manager.get_workspace_by_index(idx);
    while (ws !== null) {
        yield [idx, ws];
        idx += 1;
        ws = manager.get_workspace_by_index(idx);
    }
}

let default_isoverviewwindow_ws: any;
let default_isoverviewwindow_ws_thumbnail: any;
let default_init_appswitcher: any;
let default_getwindowlist_windowswitcher: any;
let default_getcaption_windowpreview: any;
let default_getcaption_workspace: any;

/**
 * Decorates the default gnome-shell workspace/overview handling
 * of skip_task_bar. And have those window types included in pop-shell.
 * Should only be called on extension#enable()
 *
 * NOTE to future maintainer:
 * Skip taskbar has been left out by upstream for a reason. And the
 * Shell.WindowTracker seems to skip handling skip taskbar windows, so they are
 * null or undefined. GNOME 40+ and lower version checking should be done to
 * constantly support having them within pop-shell.
 *
 * Known skip taskbars ddterm, conky, guake, minimized to tray apps, etc.
 *
 * While minimize to tray are the target for this feature,
 * skip taskbars that float/and avail workspace all
 * need to added to config.ts as default floating
 *
 */
function _show_skip_taskbar_windows(ext: Ext) {
    // Handle the overview
    if (!default_isoverviewwindow_ws) {
        default_isoverviewwindow_ws = Workspace.prototype._isOverviewWindow;
        Workspace.prototype._isOverviewWindow = function (win: any) {
            let meta_win = win;
            if (GNOME_VERSION?.startsWith('3.36')) meta_win = win.get_meta_window();
            return is_valid_minimize_to_tray(meta_win, ext) || default_isoverviewwindow_ws(win);
        };
    }

    // Handle _getCaption errors
    if (GNOME_VERSION?.startsWith('3.36')) {
        // imports.ui.windowPreview is not in 3.36,
        // _getCaption() is still in workspace.js
        if (!default_getcaption_workspace) {
            default_getcaption_workspace = Workspace.prototype._getCaption;
            // 3.36 _getCaption
            Workspace.prototype._getCaption = function () {
                let metaWindow = this._windowClone.metaWindow;
                if (metaWindow.title) return metaWindow.title;

                let tracker = Shell.WindowTracker.get_default();
                let app = tracker.get_window_app(metaWindow);
                return app ? app.get_name() : '';
            };
        }
    } else {
        if (!default_getcaption_windowpreview) {
            default_getcaption_windowpreview = WindowPreview.prototype._getCaption;
            log.debug(`override workspace._getCaption`);
            // 3.38 _getCaption
            WindowPreview.prototype._getCaption = function () {
                if (this.metaWindow.title) return this.metaWindow.title;

                let tracker = Shell.WindowTracker.get_default();
                let app = tracker.get_window_app(this.metaWindow);
                return app ? app.get_name() : '';
            };
        }
    }

    // Handle the workspace thumbnail
    if (!default_isoverviewwindow_ws_thumbnail) {
        default_isoverviewwindow_ws_thumbnail = WorkspaceThumbnail.prototype._isOverviewWindow;
        WorkspaceThumbnail.prototype._isOverviewWindow = function (win: any) {
            let meta_win = win.get_meta_window();
            return is_valid_minimize_to_tray(meta_win, ext) || default_isoverviewwindow_ws_thumbnail(win);
        };
    }

    // let cfg = ext.conf;

    // Handle switch-applications
    // if (!default_init_appswitcher) {
    //   default_init_appswitcher = AppSwitcher.prototype._init;
    //   // Do not use the Shell.AppSystem apps
    //   AppSwitcher.prototype._init = function (_apps: any, altTabPopup: any) {
    //     // Simulate super._init(true);
    //     SwitcherList.prototype._init.call(this, true);
    //     this.icons = [];
    //     this._arrows = [];

    //     let windowTracker = Shell.WindowTracker.get_default();
    //     let settings = new Gio.Settings({ schema_id: 'org.gnome.shell.app-switcher' });

    //     let workspace = null;
    //     if (settings.get_boolean('current-workspace-only')) {
    //       let workspaceManager = global.workspace_manager;
    //       workspace = workspaceManager.get_active_workspace();
    //     }

    //     let allWindows = global.display.get_tab_list(Meta.TabList.NORMAL_ALL, workspace);
    //     // Remove duplicate app names after including skip task bar windows too
    //     // E.g. Extensions instance plus when opening an extensions prefs window
    //     // Or Android windows when alt-tabbing (depends on where switch apps is bound)
    //     let allWindowsWithSkipTaskBar = allWindows.filter((w, i, a) => {
    //       let app: any = windowTracker.get_window_app(w);
    //       return (
    //         i ===
    //         a.findIndex(wi => {
    //           let w_app: any = windowTracker.get_window_app(wi);
    //           return app && w_app ? app.get_name() === w_app.get_name() : false;
    //         })
    //       );
    //     });

    //     // This block collects the windows associated to an app icon
    //     for (let i = 0; i < allWindowsWithSkipTaskBar.length; i++) {
    //       let meta_win = allWindowsWithSkipTaskBar[i];
    //       let show_skiptb = !cfg.skiptaskbar_shall_hide(meta_win);
    //       if (meta_win.is_skip_taskbar() && !show_skiptb) continue;
    //       let appIcon = new AppIcon(windowTracker.get_window_app(meta_win));
    //       appIcon.cachedWindows = allWindows.filter(
    //         w => windowTracker.get_window_app(w) === appIcon.app
    //       );
    //       if (appIcon.cachedWindows.length > 0) this._addIcon(appIcon);
    //     }

    //     this._curApp = -1;
    //     this._altTabPopup = altTabPopup;
    //     this._mouseTimeOutId = 0;

    //     this.connect('destroy', this._onDestroy.bind(this));
    //   };
    // }

    // Handle switch-windows
    if (!default_getwindowlist_windowswitcher) {
        default_getwindowlist_windowswitcher = WindowSwitcherPopup.prototype._getWindowList;
        WindowSwitcherPopup.prototype._getWindowList = function () {
            let workspace = null;

            if (this._settings.get_boolean('current-workspace-only')) {
                let workspaceManager = global.workspace_manager;
                workspace = workspaceManager.get_active_workspace();
            }

            let windows = global.display.get_tab_list(Meta.TabList.NORMAL_ALL, workspace);
            return windows
                .map((w) => {
                    let meta_win = w.is_attached_dialog() ? w.get_transient_for() : w;
                    if (meta_win) {
                        if (!meta_win.skip_taskbar || is_valid_minimize_to_tray(meta_win, ext)) {
                            return meta_win;
                        }
                    }
                    return null;
                })
                .filter((w, i, a) => w != null && a.indexOf(w) == i);
        };
    }
}

/**
 * This is the cleanup/restore of the decorator for skip_taskbar when pop-shell
 * is disabled.
 * Should only be called on extension#disable()
 *
 * Default functions should be checked if they exist,
 * especially when skip taskbar setting was left on during an update
 *
 */
function _hide_skip_taskbar_windows() {
    if (default_isoverviewwindow_ws) {
        Workspace.prototype._isOverviewWindow = default_isoverviewwindow_ws;
        default_isoverviewwindow_ws = null;
    }

    if (GNOME_VERSION?.startsWith('3.36')) {
        if (default_getcaption_workspace) {
            Workspace.prototype._getCaption = default_getcaption_workspace;
            default_getcaption_workspace = null;
        }
    } else {
        if (default_getcaption_windowpreview) {
            WindowPreview.prototype._getCaption = default_getcaption_windowpreview;
            default_getcaption_windowpreview = null;
        }
    }

    if (default_isoverviewwindow_ws_thumbnail) {
        WorkspaceThumbnail.prototype._isOverviewWindow = default_isoverviewwindow_ws_thumbnail;
        default_isoverviewwindow_ws_thumbnail = null;
    }

    if (default_init_appswitcher) {
        // AppSwitcher.prototype._init = default_init_appswitcher;
        default_init_appswitcher = null;
    }

    if (default_getwindowlist_windowswitcher) {
        WindowSwitcherPopup.prototype._getWindowList = default_getwindowlist_windowswitcher;
        default_getwindowlist_windowswitcher = null;
    }
}

/**
 * Moved skip task bar checking on this function/method
 * Synchronized with ShellTracker type checks and watch out for attached dialogs
 *
 * Thanks to Bananaman and upstream gnome-shell devs for the information
 *
 * https://github.com/pop-os/shell/issues/1251
 */
function is_valid_minimize_to_tray(meta_win: Meta.Window, ext: Ext) {
    let cfg = ext.conf;
    let valid_min_to_tray = false;
    switch (meta_win.window_type) {
        case Meta.WindowType.NORMAL:
        case Meta.WindowType.UTILITY: // Gimp (Non-Single Window Mode)
            // Don't track OR (override redirect)-windows since those are never
            // allowed to be window managed:
            valid_min_to_tray = !meta_win.is_override_redirect();
            break;
    }

    let gnome_shell_wm_class = meta_win.get_wm_class() === 'Gjs' || meta_win.get_wm_class() === 'Gnome-shell';
    let show_skiptb = !cfg.skiptaskbar_shall_hide(meta_win);

    valid_min_to_tray =
        valid_min_to_tray &&
        !meta_win.is_attached_dialog() &&
        show_skiptb &&
        meta_win.skip_taskbar &&
        meta_win.get_wm_class() !== null &&
        !gnome_shell_wm_class;

    return valid_min_to_tray;
}
