import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import St from 'gi://St';
import Gio from 'gi://Gio';
import GdkPixbuf from 'gi://GdkPixbuf';
import Meta from 'gi://Meta'; // Import Meta to check window types

import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';

// --- Configuration Constants ---
const UPDATE_INTERVAL_MS = 50; // Update loop runs every 50ms (~20 FPS)
const GRAVITY = 2;             // Vertical acceleration per frame
const WALK_SPEED = 3;          // Horizontal pixels per frame
const JUMP_VELOCITY = -20;     // Initial jump force (negative is up)
const JUMP_REACH_X = (WALK_SPEED * 2) * Math.abs(JUMP_VELOCITY / GRAVITY); // Max horizontal travel during jump ascent
const MAX_JUMP_HEIGHT = (JUMP_VELOCITY * JUMP_VELOCITY) / (2 * GRAVITY); // Max vertical travel during jump ascent

// State Machine Definitions for the Gnomelet
const State = {
    FALLING: 'FALLING',
    WALKING: 'WALKING',
    IDLE: 'IDLE',
    JUMPING: 'JUMPING'
};

/**
 * Utility to check if a window is maximized
 */
function isWindowMaximized(window) {
    return window && window.maximized_horizontally && window.maximized_vertically;
}

/**
 * Gnomelet Class
 * Represents a single animated character on the screen.
 * Uses pre-sliced images loaded by the Manager.
 */
const Gnomelet = GObject.registerClass(
    class Gnomelet extends GObject.Object {
        _init(typeName, frameImages, frameWidth, frameHeight, settings, resourceProvider) {
            super._init();

            this._typeName = typeName;
            this._settings = settings;
            this._resourceProvider = resourceProvider;

            // --- State Initialization ---
            this._state = State.FALLING;
            this._vx = 0; // Velocity X
            this._vy = 0; // Velocity Y

            // --- Animation State ---
            this._frame = 0; // Current sprite frame index
            this._animationTimer = 0; // Counter for animation timing
            this._savedFacing = Math.random() > 0.5; // Initial Direction
            this._idleTimer = 0; // Countdown for how long to stay idle

            // --- Configurable Dimensions ---
            this._frameWidth = frameWidth;
            this._frameHeight = frameHeight;

            const TARGET_HEIGHT = this._settings.get_int('gnomelet-scale');
            const scaleFactor = TARGET_HEIGHT / this._frameHeight;

            // displayW/H are used for physics and collision logic
            this._displayW = Math.floor(this._frameWidth * scaleFactor);
            this._displayH = Math.floor(TARGET_HEIGHT);

            this._randomizeStartPos();

            // --- Image Resources ---
            this._frameImages = frameImages;

            // --- Actor Setup (St.Icon) ---
            // NOTE: St.Icon handles scaling better when using only icon_size.
            // We avoid explicit width/height to prevent conflicts with internal icon management.
            this.actor = new St.Icon({
                visible: true,
                reactive: false,
                icon_size: this._displayH,
                style: 'padding: 0px;',
            });

            // Set initial frame content
            if (this._frameImages.length > 0 && this._frameImages[0]) {
                this.actor.set_gicon(this._frameImages[0]);
            }

            // Set initial layer based on context (Fix for maximized window visibility)
            this._resetLayer();
            this.actor.set_position(this._x, this._y);

            this._updateAnimation();
        }

        // Property to define facing based on Velocity X
        get facingRight() {
            let sign = Math.sign(this._vx);
            if (sign !== 0 && !isNaN(sign)) {
                this._savedFacing = (sign > 0);
            }
            return this._savedFacing;
        }

        /**
         * Updates the gnomelet scale when settings change.
         */
        updateScale() {
            let oldH = this._displayH;

            const TARGET_HEIGHT = this._settings.get_int('gnomelet-scale');
            const scaleFactor = TARGET_HEIGHT / this._frameHeight;

            this._displayW = Math.floor(this._frameWidth * scaleFactor);
            this._displayH = Math.floor(TARGET_HEIGHT);

            // Update the visual size of the icon
            this.actor.set_icon_size(this._displayH);

            // Adjust Y position so feet stay at the same ground level
            this._y = this._y + oldH - this._displayH;
            this.actor.set_position(this._x, this._y);
        }

        /**
         * Serializes the state for persistence
         */
        serialize() {
            return {
                type: this._typeName,
                x: this._x,
                y: this._y,
                vx: this._vx,
                vy: this._vy,
                state: this._state,
                facing: this._savedFacing,
                idleTimer: this._idleTimer
            };
        }

        /**
         * Restores saved state
         */
        deserialize(data) {
            if (!data) return;


            // Handle type change if the saved state has a different gnomelet type
            if (data.type && data.type !== this._typeName && this._resourceProvider) {
                let res = this._resourceProvider(data.type);
                if (res) {
                    this._typeName = res.type;
                    this._frameImages = res.frames;
                    this._frameWidth = res.w;
                    this._frameHeight = res.h;
                    this.updateScale();
                }
            }

            if (data.x !== undefined) this._x = data.x;
            if (data.y !== undefined) this._y = data.y;
            if (data.vx !== undefined) this._vx = data.vx;
            if (data.vy !== undefined) this._vy = data.vy;
            if (data.state !== undefined) this._state = data.state;
            if (data.facing !== undefined) this._savedFacing = data.facing;
            if (data.idleTimer !== undefined) this._idleTimer = data.idleTimer;

            this.actor.set_position(this._x, this._y);
            this.setFrame(this._frame); // Refresh frame in case type changed
        }

        /**
         * Main update loop for the gnomelet.
         */
        update(windows) {
            if (!this.actor) return; // Guard against updates after destruction

            // Physics (Gravity)
            if (this._state === State.FALLING || this._state === State.JUMPING) {
                this._vy += GRAVITY;
            }

            // Stop movement if idle (prevents sliding)
            if (this._state === State.IDLE) {
                this._vx = 0;
            }

            // Update Position
            let prevY = this._y;
            this._x += this._vx;
            this._y += this._vy;

            // Determine current monitor based on "feet" position
            let feetX = this._x + this._displayW / 2;
            let feetY = this._y + this._displayH;

            // Find monitors containing the X coordinate
            let monitors = Main.layoutManager.monitors.filter(m => feetX >= m.x && feetX < m.x + m.width);

            let currentMonitor = null;
            if (monitors.length === 0) {
                currentMonitor = Main.layoutManager.primaryMonitor;
            } else if (monitors.length === 1) {
                currentMonitor = monitors[0];
            } else {
                // Handle vertically stacked monitors
                monitors.sort((a, b) => a.y - b.y);
                currentMonitor = monitors.find(m => feetY < m.y + m.height);
                if (!currentMonitor) currentMonitor = monitors[monitors.length - 1];
            }

            let floorY = currentMonitor.y + currentMonitor.height;

            // --- Logic: Reposition if walking off floor ---
            let onFloorLevel = (this._y + this._displayH) >= floorY - 10;

            if (onFloorLevel) {
                // If walking outside horizontal bounds of the stage, respawn at the top
                if (this._x < -this._displayW || this._x > global.stage.width) {
                    this._respawn();
                    return;
                }
            } else {
                // Routine Wall Bounce when in air or on windows
                let maxX = global.stage.width - this._displayW;
                if (this._x < 0) {
                    this._x = 0;
                    this._vx *= -1;
                } else if (this._x > maxX) {
                    this._x = maxX;
                    this._vx *= -1;
                }
            }

            // --- Collision Detection ---
            let onGround = false;
            let landedOnWindow = null;

            if (this._vy >= 0) { // Only collide if falling downwards
                for (let win of windows) {
                    let rect = win.rect;

                    // Hitbox: Feet within window width AND close to top edge
                    let prevFeetY = prevY + this._displayH;
                    let inHorizontalRange = (feetX >= rect.x) && (feetX <= rect.x + rect.width);
                    let inVerticalRange = (feetY >= rect.y) && (prevFeetY <= rect.y + 25);

                    if (inHorizontalRange && inVerticalRange) {
                        this._y = rect.y - this._displayH + 1;
                        this._vy = 0;
                        onGround = true;
                        landedOnWindow = win;
                        break;
                    }
                }

                // Allow landing on screen bottom (Floor)
                if (!landedOnWindow) {
                    if (feetY >= floorY) {
                        this._y = floorY - this._displayH;
                        this._vy = 0;
                        onGround = true;
                    }
                }
            }

            // --- Z-Ordering / Layering Logic ---
            if (landedOnWindow) {
                // If on a window, it must be in the same group to be ordered relative to it
                let parent = this.actor.get_parent();
                if (parent !== global.window_group) {
                    if (parent === Main.layoutManager.uiGroup) {
                        Main.layoutManager.removeChrome(this.actor);
                    } else if (parent) {
                        parent.remove_child(this.actor);
                    }
                    global.window_group.add_child(this.actor);
                }
                // Place it right above the window it landed on
                global.window_group.set_child_above_sibling(this.actor, landedOnWindow.actor);

            } else if (onGround && !landedOnWindow) {
                // If on the floor, follow user settings (Allow/Partial/Disallow)
                let floorMode = this._settings.get_string('floor-z-order');
                let parent = this.actor.get_parent();

                let shouldBeInFront = (floorMode === 'allow');

                if (shouldBeInFront) {
                    if (parent !== Main.layoutManager.uiGroup) {
                        if (parent) parent.remove_child(this.actor);
                        Main.layoutManager.addChrome(this.actor);
                    }
                } else {
                    // For 'partial' or 'disallow', if we are on the floor, we are likely behind windows.
                    // We prefer Background Group.
                    let bgGroup = Main.layoutManager._backgroundGroup;
                    if (bgGroup && parent !== bgGroup) {
                        if (parent === Main.layoutManager.uiGroup) {
                            Main.layoutManager.removeChrome(this.actor);
                        } else if (parent) {
                            parent.remove_child(this.actor);
                        }
                        bgGroup.add_child(this.actor);
                    }
                }
            }

            // --- State Machine Transitions ---
            if (onGround) {
                // On ground
                if (this._state === State.FALLING || this._state === State.JUMPING) {
                    this._vy = 0;
                    this._pickNewAction();
                }
            } else {
                // Not on ground
                if (this._state !== State.JUMPING) {
                    // Logic: If we were walking and now we are not on ground, we are walking off an edge.
                    let jumped = false;
                    if (this._state === State.WALKING) {
                        // "Jump for falling": Chance to jump when reaching the edge
                        if (Math.random() < 0.5) {
                            this._performJump();
                            jumped = true;
                        }
                    }

                    if (!jumped) {
                        this._state = State.FALLING;
                    }
                }
            }

            // --- AI Behavior ---
            if (this._state === State.WALKING) {
                this._idleTimer = 0;
                // Chance to stop walking
                if (Math.random() < 0.02) {
                    this._state = State.IDLE;
                    this._vx = 0;
                    this._idleTimer = Math.random() * 60 + 20;
                }

                // Logic: "Jump only when useful" - Check for window overhead
                let canJump = false;

                // Let's recalculate precise feet position after collision adjustments
                let currFeetY = this._y + this._displayH;
                let currFeetX = this._x + this._displayW / 2;

                for (let win of windows) {
                    // Skip the window we are currently standing on
                    if (landedOnWindow && win === landedOnWindow) continue;

                    let rect = win.rect;

                    let effectiveMinX = rect.x;
                    let effectiveMaxX = rect.x + rect.width;

                    // Check if window is horizontally within range of our feet
                    // We extend the "virtual" window size if we are approaching it from the side
                    if (currFeetX < rect.x && this.facingRight) {
                        effectiveMinX -= JUMP_REACH_X;
                    } else if (currFeetX > rect.x + rect.width && !this.facingRight) {
                        effectiveMaxX += JUMP_REACH_X;
                    }

                    if (currFeetX >= effectiveMinX && currFeetX <= effectiveMaxX) {
                        // Check if window is vertically above us and reachable
                        // Window top (rect.y) must be less than feet (currFeetY)
                        let dist = currFeetY - rect.y;
                        if (dist > 0 && dist <= MAX_JUMP_HEIGHT) {
                            canJump = true;
                            break;
                        }
                    }
                }

                // If useful, chance to jump
                if (canJump) {
                    if (Math.random() < 0.25) {
                        this._performJump();
                    }
                }

            } else if (this._state === State.IDLE) {
                this._vx = 0;
                this._idleTimer--;
                if (this._idleTimer <= 0) {
                    this._pickNewAction();
                }
            }

            this._updateAnimation();
            this.actor.set_position(Math.floor(this._x), Math.floor(this._y));
        }

        /**
         * Sets the initial layer for the actor based on the floor z-order setting.
         */
        _resetLayer() {
            let floorMode = this._settings.get_string('floor-z-order');
            let spawnInBackground = false;

            if (floorMode === 'allow') {
                spawnInBackground = false;
            } else if (floorMode === 'partial') {
                // Partial: Background only if focused window is maximized
                let focusWindow = global.display.focus_window;
                spawnInBackground = focusWindow && isWindowMaximized(focusWindow);
            } else if (floorMode === 'disallow') {
                // Disallow: Background if ANY visible window is maximized
                let actors = global.window_group.get_children();
                for (let actor of actors) {
                    // Important: Check for NORMAL window type to avoid detecting desktop icons/docks as maximized
                    if (actor.visible && actor.meta_window && !actor.meta_window.minimized &&
                        actor.meta_window.get_window_type() === Meta.WindowType.NORMAL &&
                        isWindowMaximized(actor.meta_window)) {
                        spawnInBackground = true;
                        break;
                    }
                }
            }

            let parent = this.actor.get_parent();

            if (spawnInBackground) {
                let bgGroup = Main.layoutManager._backgroundGroup;
                if (bgGroup && parent !== bgGroup) {
                    if (parent === Main.layoutManager.uiGroup) {
                        Main.layoutManager.removeChrome(this.actor);
                    } else if (parent) {
                        parent.remove_child(this.actor);
                    }
                    bgGroup.add_child(this.actor);
                }
            } else {
                if (parent !== Main.layoutManager.uiGroup) {
                    if (parent) parent.remove_child(this.actor);
                    Main.layoutManager.addChrome(this.actor);
                }
            }
        }

        /**
         * Generates a random start position above one of the monitors.
         */
        _randomizeStartPos() {
            let monitors = Main.layoutManager.monitors;
            if (!monitors || monitors.length === 0) {
                this._x = Math.floor(Math.random() * (global.stage.width - this._displayW));
                this._y = 0;
            } else {
                let m = monitors[Math.floor(Math.random() * monitors.length)];
                this._x = Math.floor(m.x + Math.random() * (m.width - this._displayW));
                this._y = m.y;
            }
        }

        /**
         * Resets the character to the top.
         */
        _respawn() {
            // Pick new type/identity on respawn
            if (this._resourceProvider) {
                let res = this._resourceProvider(); // Pick random
                if (res) {
                    this._typeName = res.type;
                    this._frameImages = res.frames;
                    this._frameWidth = res.w;
                    this._frameHeight = res.h;
                    this.updateScale();
                }
            }

            this._randomizeStartPos();
            this._vx = 0;
            this._vy = 0;
            this._state = State.FALLING;

            this._resetLayer();
        }

        /**
         * Decides the next action when on the ground.
         */
        _pickNewAction() {
            let r = Math.random();
            if (r < 0.6) {
                this._state = State.WALKING;
                let dir = (Math.random() > 0.5) ? 1 : -1;
                this._vx = dir * WALK_SPEED;
            } else {
                this._state = State.IDLE;
                this._idleTimer = Math.random() * 60 + 20;
            }
        }

        /**
         * Performs a jump.
         */
        _performJump() {
            this._state = State.JUMPING;
            this._vy = JUMP_VELOCITY;
            let dir = this.facingRight ? 1 : -1;
            this._vx = dir * WALK_SPEED * 2;
        }

        /**
         * Calculates the correct frame based on state and time.
         */
        _updateAnimation() {
            this._animationTimer++;
            let frameIndex = 0;
            switch (this._state) {
                case State.WALKING:
                    let walkFrames = [0, 1, 2, 3];
                    let speed = 4; // Frame changes every 4 ticks
                    let idx = Math.floor(this._animationTimer / speed) % walkFrames.length;
                    frameIndex = walkFrames[idx];
                    break;
                case State.IDLE:
                    frameIndex = 4;
                    break;
                case State.JUMPING:
                case State.FALLING:
                    frameIndex = 5;
                    break;
            }
            this.setFrame(frameIndex);
        }

        /**
         * Applies the frame and handles horizontal flipping (mirroring).
         */
        setFrame(frameIndex) {
            if (this._frame === frameIndex && this._lastFacing === this.facingRight) return;

            this._frame = frameIndex;
            this._lastFacing = this.facingRight;

            if (this._frameImages && this._frameImages[frameIndex]) {
                this.actor.set_gicon(this._frameImages[frameIndex]);
            }

            // Horizontal mirroring
            this.actor.set_pivot_point(0.5, 0.5);
            if (this.facingRight) {
                this.actor.scale_x = 1;
            } else {
                this.actor.scale_x = -1;
            }
        }

        /**
         * Removes the actor and cleans up references.
         */
        destroy() {
            if (this.actor) {
                let parent = this.actor.get_parent();
                if (parent === Main.layoutManager.uiGroup) {
                    Main.layoutManager.removeChrome(this.actor);
                } else if (parent) {
                    parent.remove_child(this.actor);
                }
                this.actor.destroy();
                this.actor = null;
            }
            this._frameImages = [];
        }
    });

/**
 * GnomeletManager Class
 * Orchestrates the lifecycle of all characters and global resources.
 */
class GnomeletManager {
    constructor(settings) {
        this._gnomelets = [];
        this._settings = settings;
        this._windows = [];
        this._timerId = 0;
        this._cancellable = null;

        // Resource cache: { [type: string]: { frames: GIcon[], w: int, h: int } }
        this._resources = {};
        this._cacheFile = GLib.get_user_cache_dir() + '/gnomelets-state.json';
        this._pendingState = null;
        this._isPaused = !this._settings.get_boolean('is-enabled');
    }

    /**
     * Helper to resolve the correct folder for a given type name asynchronously.
     */
    _resolveImageFolderAsync(typeName, callback) {
        if (!typeName) {
            callback(null);
            return;
        }

        let file = Gio.File.new_for_uri(import.meta.url);
        let imagesDir = file.get_parent().get_child('images');
        let typeDir = imagesDir.get_child(typeName);

        typeDir.query_info_async(
            Gio.FILE_ATTRIBUTE_STANDARD_TYPE,
            Gio.FileQueryInfoFlags.NONE,
            GLib.PRIORITY_DEFAULT,
            null,
            (obj, res) => {
                try {
                    let info = obj.query_info_finish(res);
                    if (info.get_file_type() === Gio.FileType.DIRECTORY) {
                        callback(typeDir);
                    } else {
                        callback(null);
                    }
                } catch (e) {
                    callback(null);
                }
            }
        );
    }

    /**
     * Loads resources and spawns gnomelets.
     */
    async _loadResourcesAndSpawn(hardReset = false) {
        let types = this._settings.get_strv('gnomelet-type');
        if (!types || types.length === 0) types = ['Santa'];

        let loadedSomething = false;

        const loadType = async (typeName) => {
            if (this._resources[typeName]) return; // Already loaded

            return new Promise((resolve) => {
                this._resolveImageFolderAsync(typeName, async (typeDir) => {
                    if (!typeDir) { resolve(); return; }

                    let frames = [];
                    let frameW = 0;
                    let frameH = 0;
                    let anySuccess = false;

                    const loadFrame = (index) => {
                        return new Promise((r) => {
                            let imgFile = typeDir.get_child(`${index}.png`);
                            imgFile.read_async(GLib.PRIORITY_DEFAULT, this._cancellable, (file, res) => {
                                try {
                                    let stream = file.read_finish(res);
                                    GdkPixbuf.Pixbuf.new_from_stream_async(stream, this._cancellable, (source, res2) => {
                                        try {
                                            let pixbuf = GdkPixbuf.Pixbuf.new_from_stream_finish(res2);
                                            stream.close(null);
                                            r({
                                                valid: true,
                                                w: pixbuf.get_width(),
                                                h: pixbuf.get_height(),
                                                icon: new Gio.FileIcon({ file: imgFile })
                                            });
                                        } catch (e) {
                                            try { stream.close(null); } catch (err) { }
                                            r({ valid: false });
                                        }
                                    });
                                } catch (e) {
                                    r({ valid: false });
                                }
                            });
                        });
                    };

                    let promises = [];
                    for (let i = 0; i < 6; i++) promises.push(loadFrame(i));

                    let results = await Promise.all(promises);
                    for (let res of results) {
                        if (res && res.valid) {
                            frames.push(res.icon);
                            if (frameW === 0) {
                                frameW = res.w;
                                frameH = res.h;
                            }
                            anySuccess = true;
                        } else {
                            frames.push(null);
                        }
                    }

                    if (anySuccess && frameW > 0 && frameH > 0) {
                        this._resources[typeName] = { frames, w: frameW, h: frameH };
                        loadedSomething = true;
                    }
                    resolve();
                });
            });
        };

        // Load all selected types
        await Promise.all(types.map(t => loadType(t)));

        if (hardReset) this._hardReset();
        else this._spawnGnomelets(null);
    }

    get isVisualizationEnabled() {
        return !this._isPaused;
    }

    /**
     * Toggles the visualization state.
     */
    toggleVisualization() {
        let current = this._settings.get_boolean('is-enabled');
        this._settings.set_boolean('is-enabled', !current);
    }

    /**
     * Starts the update timer if not already running.
     * Prevents duplicate timers and ensures single source of truth.
     */
    _startTimer() {
        if (this._timerId) return;

        this._timerId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, UPDATE_INTERVAL_MS, () => {
            this._tick();
            return GLib.SOURCE_CONTINUE;
        });
    }

    /**
     * Stops the update timer if running.
     */
    _stopTimer() {
        if (this._timerId) {
            GLib.source_remove(this._timerId);
            this._timerId = 0;
        }
    }

    /**
     * Enables manager, listeners, and the main timer.
     */
    enable() {
        this._cancellable = new Gio.Cancellable();
        this._settingsSignal = this._settings.connect('changed', (settings, key) => {
            if (key === 'gnomelet-count') this._updateCount();
            else if (key === 'gnomelet-scale') this._updateScale();
            else if (key === 'gnomelet-type') {
                // Trigger async load and reset
                this._loadResourcesAndSpawn(true);
            } else if (key === 'reset-trigger') {
                this._hardReset();
            } else if (key === 'is-enabled') {
                this._updateEnabledState();
            }
        });

        // Async load: Start loading state asynchronously.
        // The gnomelets will spawn in the callback.
        this._loadStateAsync();

        if (!this._isPaused) {
            this._startTimer();
        }
    }

    /**
     * Disables everything and saves current state.
     */
    disable() {
        // Cancel any pending async load
        if (this._cancellable) {
            this._cancellable.cancel();
            this._cancellable = null;
        }

        this._saveState();
        this._stopTimer();

        if (this._settingsSignal) {
            this._settings.disconnect(this._settingsSignal);
            this._settingsSignal = 0;
        }
        this._destroyGnomelets();
        this._resources = {};
    }

    /**
     * Loads saved state from JSON cache file asynchronously.
     * When done, it triggers resource loading, then spawning.
     */
    _loadStateAsync() {
        let file = Gio.File.new_for_path(this._cacheFile);

        file.load_contents_async(this._cancellable, (obj, res) => {
            let savedState = null;
            try {
                let [success, contents, etag] = obj.load_contents_finish(res);
                if (success) {
                    let decoder = new TextDecoder('utf-8');
                    savedState = JSON.parse(decoder.decode(contents));
                }
            } catch (e) {
                // Ignore cancellation errors or missing files
                if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) return;
            }

            this._pendingState = savedState;
            this._loadResourcesAndSpawn(false);
        });
    }

    /**
     * Saves the state of all characters to file.
     */
    _saveState() {
        try {
            let data = this._gnomelets.map(p => p.serialize());
            // NOTE: Using synchronous write on disable is generally accepted 
            // because we need to ensure state is saved before the extension object dies.
            GLib.file_set_contents(this._cacheFile, JSON.stringify(data));
        } catch (e) { }
    }

    _updateScale() {
        for (let p of this._gnomelets) {
            p.updateScale();
        }
    }

    _hardReset() {
        this._destroyGnomelets();
        try {
            let f = Gio.File.new_for_path(this._cacheFile);
            // This delete is synchronous but very fast and rare (user triggered).
            if (f.query_exists(null)) f.delete(null);
        } catch (e) { }
        this._spawnGnomelets(null);
    }

    /**
     * Adds or removes characters based on settings count.
     */
    _pickResource(typeName = null) {
        if (typeName && this._resources[typeName]) {
            let res = this._resources[typeName];
            return { type: typeName, frames: res.frames, w: res.w, h: res.h };
        }

        // Random from selected
        let selectedTypes = this._settings.get_strv('gnomelet-type');
        if (!selectedTypes || selectedTypes.length === 0) selectedTypes = ['Santa'];
        // Filter those that are actually loaded
        let valid = selectedTypes.filter(t => this._resources[t]);

        // If none of selected are loaded just return null.
        if (valid.length === 0) return null;

        let t = valid[Math.floor(Math.random() * valid.length)];
        let res = this._resources[t];
        return { type: t, frames: res.frames, w: res.w, h: res.h };
    }

    /**
     * Adds or removes characters based on settings count.
     */
    _updateCount() {
        if (this._isPaused) return;
        let count = this._settings.get_int('gnomelet-count');
        let current = this._gnomelets.length;

        if (count > current) {
            for (let i = 0; i < (count - current); i++) {
                let res = this._pickResource();
                if (!res) break;
                // Bind resource provider
                let provider = (t) => this._pickResource(t);
                this._gnomelets.push(new Gnomelet(res.type, res.frames, res.w, res.h, this._settings, provider));
            }
        } else if (count < current) {
            for (let i = 0; i < (current - count); i++) {
                let p = this._gnomelets.pop();
                p.destroy();
            }
        }
    }

    _spawnGnomelets(savedState) {
        if (this._isPaused) return;
        if (!this._cancellable || this._cancellable.is_cancelled()) return;

        let count = this._settings.get_int('gnomelet-count');

        // Ensure we have at least one resource loaded
        let res = this._pickResource();
        if (!res) {
            this._loadResourcesAndSpawn(false);
            return;
        }

        if (this._gnomelets.length > 0) this._destroyGnomelets();

        let stateToUse = savedState || this._pendingState;
        this._pendingState = null;

        // Shared provider reference
        let provider = (t) => this._pickResource(t);

        for (let i = 0; i < count; i++) {
            // Priority: Saved Type -> Random
            let specificType = null;
            if (stateToUse && stateToUse[i] && stateToUse[i].type) {
                specificType = stateToUse[i].type;
            }

            let instanceRes = this._pickResource(specificType);

            if (!instanceRes) instanceRes = this._pickResource(); // Fallback if somehow both failed (e.g. valid types empty)
            if (!instanceRes) break;

            let p = new Gnomelet(instanceRes.type, instanceRes.frames, instanceRes.w, instanceRes.h, this._settings, provider);
            if (stateToUse && stateToUse[i]) p.deserialize(stateToUse[i]);
            this._gnomelets.push(p);
        }
    }

    _destroyGnomelets() {
        for (let p of this._gnomelets) p.destroy();
        this._gnomelets = [];
    }

    /**
     * Tick function executed every loop to gather windows and update character states.
     */
    _tick() {
        try {
            this._windows = [];
            let actors = global.window_group.get_children();
            let focusWindow = global.display.focus_window;
            let floorMode = this._settings.get_string('floor-z-order');

            // 1. Find indices of maximized windows.
            // highestMaximizedIndex = topmost (last in list) maximized window.
            // lowestMaximizedIndex = bottommost (first in list) maximized window.
            let highestMaximizedIndex = -1;
            let lowestMaximizedIndex = -1;

            for (let i = 0; i < actors.length; i++) {
                let actor = actors[i];
                if (actor.visible && actor.meta_window && !actor.meta_window.minimized &&
                    actor.meta_window.get_window_type() === Meta.WindowType.NORMAL &&
                    isWindowMaximized(actor.meta_window)) {

                    // Logic for Disallow: Track the absolute lowest (first found) maximized window
                    if (lowestMaximizedIndex === -1) {
                        lowestMaximizedIndex = i;
                    }

                    // Logic for Partial: Track highest, but respect focus exclusion
                    if (floorMode === 'partial' && actor.meta_window === focusWindow) {
                        // Skip updating highest if it's the focused window in partial mode
                    } else {
                        highestMaximizedIndex = i;
                    }
                }
            }

            // 2. Collect valid windows for landing.
            for (let i = 0; i < actors.length; i++) {
                let actor = actors[i];
                if (!actor.visible || !actor.meta_window) continue;

                // Skip if the window itself is minimized or maximized.
                if (actor.meta_window.minimized || isWindowMaximized(actor.meta_window)) continue;

                if (floorMode === 'disallow') {
                    // Disallow: Only land on windows BEHIND the lowest maximized window.
                    // If a maximized window exists, ignore everything at or above its level.
                    // Important: We only apply this filter if a valid maximized window was found (lowest > -1)
                    if (lowestMaximizedIndex > -1 && i >= lowestMaximizedIndex) continue;
                } else {
                    // Allow/Partial: Only land on windows IN FRONT OF the highest relevant maximized window.
                    // If index is lower than highest max window, we skip it (it's obscured).
                    if (highestMaximizedIndex > -1 && i <= highestMaximizedIndex) continue;
                }

                let rect = actor.meta_window.get_frame_rect();
                this._windows.push({ rect, actor });
            }

            for (let p of this._gnomelets) p.update(this._windows);
        } catch (e) { }
    }

    /**
     * Updates the enabled state of the extension.
     */
    _updateEnabledState() {
        let enabled = this._settings.get_boolean('is-enabled');
        this._isPaused = !enabled;

        if (this._isPaused) {
            this._stopTimer();
            this._destroyGnomelets();
        } else {
            this._startTimer();
            // If we have pending state (e.g. from initial load), it will be used.
            // Otherwise, normal spawn.
            this._spawnGnomelets(null);
        }
    }
}

/**
 * Indicator for the extension.
 */
const GnomeletIndicator = GObject.registerClass(
    class GnomeletIndicator extends PanelMenu.Button {
        _init(extension) {
            super._init(0.0, 'Gnomelets Indicator');

            this._extension = extension;
            this._manager = extension._manager;

            let iconPath = extension.dir.get_child('images').get_child('icon.png');
            let gicon = new Gio.FileIcon({ file: iconPath });
            let icon = new St.Icon({
                gicon: gicon,
                style_class: 'system-status-icon',
            });
            this.add_child(icon);

            this._settingsSignal = extension._settings.connect('changed', (settings, key) => {
                if (key === 'is-enabled') {
                    this._updateToggleLabel();
                }
            });

            // Item: Re-spawn
            this.respawnItem = new PopupMenu.PopupMenuItem('Re-spawn Gnomelets');
            this.respawnItem.connect('activate', () => {
                this._manager._hardReset();
            });
            this.menu.addMenuItem(this.respawnItem);

            // Item: Toggle
            this.toggleItem = new PopupMenu.PopupMenuItem('');
            this.toggleItem.connect('activate', () => {
                this._manager.toggleVisualization();
            });
            this.menu.addMenuItem(this.toggleItem);

            this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());

            // Item: Settings
            this.settingsItem = new PopupMenu.PopupMenuItem('Settings');
            this.settingsItem.connect('activate', () => {
                this._extension.openPreferences();
            });
            this.menu.addMenuItem(this.settingsItem);

            this._updateToggleLabel();
        }

        _updateToggleLabel() {
            if (this._manager.isVisualizationEnabled) {
                this.toggleItem.label.text = 'Disable Gnomelets';
            } else {
                this.toggleItem.label.text = 'Enable Gnomelets';
            }
        }

        destroy() {
            if (this._settingsSignal) {
                this._extension._settings.disconnect(this._settingsSignal);
                this._settingsSignal = null;
            }
            super.destroy();
        }
    });

/**
 * Extension Entry Point
 */
export default class DesktopGnomeletsExtension extends Extension {
    enable() {
        this._settings = this.getSettings();
        this._manager = new GnomeletManager(this._settings);
        this._manager.enable();
        this._indicator = new GnomeletIndicator(this);
        Main.panel.addToStatusArea('gnomelets-indicator', this._indicator);
    }

    disable() {
        if (this._indicator) {
            this._indicator.destroy();
            this._indicator = null;
        }

        if (this._manager) {
            this._manager.disable();
            this._manager = null;
        }

        this._settings = null;
    }
}