// SPDX-FileCopyrightText: 2026 Marco Allegretti
// SPDX-License-Identifier: EUPL-1.2
//
// SHIFT Dynamic Tiling — KWin declarative script
//
// Architecture:
//   - One persistent layout tree per output and virtual desktop
//   - Tree nodes are split groups or window leaves; leaf rects are absolute
//     in-screen coordinates
//   - Automatic add/remove rebuilds a stable master-stack tree from window
//     order, so normal app churn does not leave arbitrary nested splits behind
//   - If the current page is full, new windows move to another existing
//     virtual desktop with free capacity
//   - The current screen can cycle between master-stack, equal columns, and
//     equal rows without changing window order
//   - Dragging over an existing tile swaps the two window leaves without
//     changing the split tree shape
//   - Drag detection uses interactiveMoveResizeStarted/Stepped/Finished
//   - Dragging over a tile previews the slot that will be swapped on drop
//   - Gap: outer 8px on screen edges, inner 8px between tiles (4px each side)

import QtQuick
import org.kde.kwin as KWinComponents
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings

Item {
    id: root

    // ── Configuration ───────────────────────────────────────────────────────
    readonly property int outerGap: 8
    readonly property int innerGap: 8    // half applied to each edge → 4px per tile
    readonly property int maxWindowsPerPage: 4
    readonly property real stablePrimaryRatio: 0.58
    readonly property var layoutModes: ["master", "columns", "rows"]

    // ── State ───────────────────────────────────────────────────────────────

    // Per-screen/per-desktop layout tree. Key: output.name + desktop key.
    // Value: split/leaf node.
    property var screenLayouts: ({})

    // Per-screen/per-desktop tile list derived from screenLayouts.
    // Value: [{win, rect}], where rect is absolute in-screen coordinates.
    property var screenTiles: ({})

    // Per-screen/per-desktop layout mode. Key: output.name + desktop key.
    // Value: one of layoutModes.
    property var screenLayoutModes: ({})

    // Per-screen/per-desktop maximize sessions. A session keeps one window on
    // the source page and parks the displaced windows on other desktops.
    property var maximizedLayouts: ({})

    // Windows temporarily parked by a maximize session. Key: window UUID.
    // Value: the layout key of the page that may restore it.
    property var displacedWindowOwners: ({})

    // Virtual desktops created by dynamic tiling for overflow parking.
    // User-created desktops are never removed by the script.
    property var autoParkingDesktopKeys: ({})

    // Last focused tiled window per screen/desktop page. Used as the
    // replacement target when a user manually moves another window onto a
    // full page.
    property var lastFocusedWindowKeys: ({})

    // Windows the user has manually floated (by UUID string).
    property var floatedWindows: ({})

    // Windows whose interactive move/resize signals are already connected.
    // Values keep callback references so KWin script reloads can disconnect them.
    property var dragConnectedWindows: ({})

    property int lastWindowRequestSerial: ShellSettings.Settings.dynamicTilingWindowRequestSerial
    property string lastPublishedMaximizedWindowIds: "__unpublished__"

    // Drag state.
    //
    // Behaviour: dragging a tile over another tile previews the target slot
    // that will be swapped on drop. Dropping inside
    // the work area but outside another tile restores the original slot.
    // Dropping outside the work area floats the window, giving users a visible
    // escape hatch from the tiled layout.
    property var draggingWindow: null
    property bool dragOutlineActive: false
    property string dragDropMode: "" // "swap", "restore", or "float"
    property rect dragOutlineRect: Qt.rect(0, 0, 0, 0)

    // Reorder state — kept stable while dragging so the rest of the layout
    // doesn't shuffle under the cursor.
    property string dragSourceScreen: ""
    property int    dragSourceIndex:  -1
    property rect   dragSourceRect:   Qt.rect(0, 0, 0, 0)
    property bool   dragNativeResize: false
    property var    dragSwapTarget: null   // {screen, key, rect} preview slot
    readonly property int floatEscapeMargin: 32

    // Deferred retile queue.
    // The dockSpaceReserver LayerShell exclusive zone needs one Wayland
    // roundtrip after setMaximize() before KWin updates MaximizeArea.
    // We queue output names and flush after 200 ms — same pattern as
    // convergentwindows constrainAfterRestoreTimer.
    property var pendingRetile: []

    Timer {
        id: retileTimer
        interval: 200
        repeat: false
        onTriggered: {
            const queue = root.pendingRetile.slice();
            root.pendingRetile = [];
            const done = {};
            for (let i = 0; i < queue.length; i++) {
                if (!done[queue[i]]) {
                    done[queue[i]] = true;
                    root.retileScreen(queue[i]);
                }
            }
        }
    }

    function scheduleRetile(outputName) {
        const q = root.pendingRetile.slice();
        if (q.indexOf(outputName) < 0) q.push(outputName);
        root.pendingRetile = q;
        retileTimer.restart();
    }

    function desktopKey(desktop) {
        if (!desktop) return "";
        if (desktop.x11DesktopNumber !== undefined) return String(desktop.x11DesktopNumber);

        const desktops = KWinComponents.Workspace.desktops;
        const index = desktops ? desktops.indexOf(desktop) : -1;
        return index >= 0 ? String(index + 1) : String(desktop);
    }

    function desktopForWindow(win) {
        if (win && win.desktops && win.desktops.length > 0) {
            return win.desktops[0];
        }
        return KWinComponents.Workspace.currentDesktop;
    }

    function normalizeWindowDesktopScope(win) {
        if (!win || (win.desktops && win.desktops.length > 0)) return;

        const desktop = KWinComponents.Workspace.currentDesktop;
        if (desktop) {
            win.desktops = [desktop];
        }
    }

    function layoutKeyFor(outputName, desktop) {
        const key = desktopKey(desktop);
        return outputName && key !== "" ? outputName + "::" + key : "";
    }

    function desktopForLayoutKey(outputName) {
        const separator = outputName.lastIndexOf("::");
        if (separator < 0) return null;

        const wantedKey = outputName.slice(separator + 2);
        const desktops = KWinComponents.Workspace.desktops;
        if (!desktops) return null;

        for (let i = 0; i < desktops.length; i++) {
            if (desktopKey(desktops[i]) === wantedKey) {
                return desktops[i];
            }
        }
        return null;
    }

    function desktopForMoveRequest(desktopId, desktopNumber) {
        const wantedId = String(desktopId || "");
        const wantedNumber = String(desktopNumber || "");
        const desktops = KWinComponents.Workspace.desktops;
        if (!desktops) return null;

        for (let i = 0; i < desktops.length; i++) {
            const desktop = desktops[i];
            const indexKey = String(i + 1);
            if (wantedId !== "") {
                if (desktop.id !== undefined && String(desktop.id) === wantedId) return desktop;
                if (desktop.uuid !== undefined && String(desktop.uuid) === wantedId) return desktop;
                if (desktop.name !== undefined && String(desktop.name) === wantedId) return desktop;
                if (wantedId === indexKey) return desktop;
                if (desktop.x11DesktopNumber !== undefined && String(desktop.x11DesktopNumber) === wantedId) return desktop;
            }
            if (wantedNumber !== "") {
                if (wantedNumber === indexKey) return desktop;
                if (desktop.x11DesktopNumber !== undefined && String(desktop.x11DesktopNumber) === wantedNumber) return desktop;
            }
        }

        return null;
    }

    function layoutKeyForWindow(win) {
        if (!win || !win.output) return "";
        return layoutKeyFor(win.output.name, desktopForWindow(win));
    }

    function assignWindowToDesktop(win, desktop) {
        if (!win || !desktop) return;
        if (win.desktops && win.desktops.length === 1 && win.desktops[0] === desktop) return;
        win.desktops = [desktop];
    }

    function windowCountForLayout(outputName) {
        const leaves = [];
        collectLeaves(screenLayouts[outputName], leaves);
        let count = 0;
        for (let i = 0; i < leaves.length; i++) {
            const win = leaves[i].win;
            if (win && !win.deleted && isTileable(win) && layoutKeyForWindow(win) === outputName) {
                count++;
            }
        }
        return count;
    }

    function targetLayoutForNewWindow(win) {
        const currentDesktop = desktopForWindow(win);
        const currentKey = layoutKeyForWindow(win);
        if (!win || !win.output || currentKey === "") {
            return { key: "", desktop: null };
        }
        if (windowCountForLayout(currentKey) < maxWindowsPerPage) {
            return { key: currentKey, desktop: currentDesktop };
        }

        const desktops = KWinComponents.Workspace.desktops;
        if (!desktops || desktops.length < 2) {
            return { key: currentKey, desktop: currentDesktop };
        }

        let startIndex = desktops.indexOf(currentDesktop);
        if (startIndex < 0) {
            startIndex = desktops.indexOf(KWinComponents.Workspace.currentDesktop);
        }
        if (startIndex < 0) {
            startIndex = 0;
        }

        for (let offset = 1; offset < desktops.length; offset++) {
            const desktop = desktops[(startIndex + offset) % desktops.length];
            const key = layoutKeyFor(win.output.name, desktop);
            if (maximizedLayouts[key]) continue;
            if (key !== "" && windowCountForLayout(key) < maxWindowsPerPage) {
                return { key: key, desktop: desktop };
            }
        }

        return { key: currentKey, desktop: currentDesktop };
    }

    function targetLayoutForDisplacedWindow(win, sourceDesktop, reservedCounts) {
        if (!win || !win.output || !sourceDesktop) {
            return { key: "", desktop: null };
        }

        const desktops = KWinComponents.Workspace.desktops;
        if (!desktops) {
            return { key: "", desktop: null };
        }

        let startIndex = desktops.indexOf(sourceDesktop);
        if (startIndex < 0) {
            startIndex = desktops.indexOf(KWinComponents.Workspace.currentDesktop);
        }
        if (startIndex < 0) {
            startIndex = 0;
        }

        for (let offset = 1; offset < desktops.length; offset++) {
            const desktop = desktops[(startIndex + offset) % desktops.length];
            const key = layoutKeyFor(win.output.name, desktop);
            if (key === "") continue;
            if (maximizedLayouts[key]) continue;

            const reserved = reservedCounts && reservedCounts[key] ? reservedCounts[key] : 0;
            if (windowCountForLayout(key) + reserved < maxWindowsPerPage) {
                return { key: key, desktop: desktop };
            }
        }

        const createdDesktop = createParkingDesktop();
        if (!createdDesktop) {
            return { key: "", desktop: null };
        }

        return { key: layoutKeyFor(win.output.name, createdDesktop), desktop: createdDesktop };
    }

    function createParkingDesktop() {
        const desktops = KWinComponents.Workspace.desktops;
        if (!desktops || typeof KWinComponents.Workspace.createDesktop !== "function") {
            return null;
        }

        const previousCount = desktops.length;
        const created = KWinComponents.Workspace.createDesktop(previousCount, "");
        if (created) {
            rememberAutoParkingDesktop(created);
            return created;
        }

        const updatedDesktops = KWinComponents.Workspace.desktops;
        if (updatedDesktops && updatedDesktops.length > previousCount) {
            const desktop = updatedDesktops[updatedDesktops.length - 1];
            rememberAutoParkingDesktop(desktop);
            return desktop;
        }

        return null;
    }

    function createUserDesktop() {
        const desktops = KWinComponents.Workspace.desktops;
        if (!desktops || typeof KWinComponents.Workspace.createDesktop !== "function") {
            return null;
        }

        const previousCount = desktops.length;
        const created = KWinComponents.Workspace.createDesktop(previousCount, "");
        if (created) {
            return created;
        }

        const updatedDesktops = KWinComponents.Workspace.desktops;
        if (updatedDesktops && updatedDesktops.length > previousCount) {
            return updatedDesktops[updatedDesktops.length - 1];
        }

        return null;
    }

    function rememberAutoParkingDesktop(desktop) {
        const key = desktopKey(desktop);
        if (key === "") return;

        const keys = Object.assign({}, autoParkingDesktopKeys);
        keys[key] = true;
        autoParkingDesktopKeys = keys;
    }

    function forgetAutoParkingDesktop(desktop) {
        const key = desktopKey(desktop);
        if (key === "" || !autoParkingDesktopKeys[key]) return;

        const keys = Object.assign({}, autoParkingDesktopKeys);
        delete keys[key];
        autoParkingDesktopKeys = keys;
    }

    // ── Tiling guard ────────────────────────────────────────────────────────

    // Active when convergence is on, gaming is off, AND the user has
    // dynamic tiling enabled in quick settings.  When this returns false
    // the script is fully inert and KWin's native quick-tile behaviour
    // owns window placement.
    function isConvergence() {
        return ShellSettings.Settings.convergenceModeEnabled &&
               !ShellSettings.Settings.gamingModeEnabled &&
               ShellSettings.Settings.dynamicTilingEnabled;
    }

    // Mirror the same guard used by convergentwindows: only normalWindow is
    // reliable in the KWin 6 declarative script API.  Add maximizable to
    // avoid calling setMaximize on layer-shell / panel surfaces.
    function shouldIgnore(win) {
        if (!win || win.deleted) return true;
        if (!win.normalWindow) return true;   // panels, dock, desktop, layer-shell
        if (!win.maximizable) return true;    // fixed/special surfaces
        if (win.fullScreen) return true;
        // Skip xwaylandvideobridge (same as convergentwindows)
        if (win.resourceClass === "xwaylandvideobridge") return true;
        return false;
    }

    function shouldFloat(win) {
        if (!win) return true;
        // Fixed-size windows (won't tile sensibly)
        const minW = win.minSize ? win.minSize.width  : 0;
        const maxW = win.maxSize ? win.maxSize.width  : 0;
        const minH = win.minSize ? win.minSize.height : 0;
        const maxH = win.maxSize ? win.maxSize.height : 0;
        if (minW > 0 && maxW > 0 && minW >= maxW) return true;
        if (minH > 0 && maxH > 0 && minH >= maxH) return true;
        // Manually floated
        if (floatedWindows[windowKey(win)]) return true;
        return false;
    }

    function isTileable(win) {
        if (!isConvergence()) return false;
        if (shouldIgnore(win)) return false;
        if (shouldFloat(win)) return false;
        return true;
    }

    function dynamicTilingOwnsWindow(win) {
        if (!isConvergence()) return false;
        if (!win || win.deleted || !win.normalWindow || !win.maximizable) return false;
        if (floatedWindows[windowKey(win)]) return false;
        return screenNameForWindow(win) !== "" || !shouldFloat(win);
    }

    function normalizeNativeWindowOperationState(win) {
        if (!dynamicTilingOwnsWindow(win)) return false;

        let changed = false;
        if (win.fullScreen === true) {
            try {
                win.fullScreen = false;
                changed = true;
            } catch (error) {}
        }
        if (win.noBorder === true) {
            win.noBorder = false;
            changed = true;
        }
        if (win.keepAbove === true) {
            win.keepAbove = false;
            changed = true;
        }
        if (win.keepBelow === true) {
            win.keepBelow = false;
            changed = true;
        }

        return changed;
    }

    // ── Layout engine ───────────────────────────────────────────────────────

    function workRect(win) {
        const output = win.output;
        const desktop = desktopForWindow(win);
        if (!output || !desktop) return null;
        return KWinComponents.Workspace.clientArea(
            KWinComponents.Workspace.MaximizeArea, output, desktop);
    }

    // Apply outer + inner gaps to a list of rects that together tile a screen.
    // outer: gap between screen edge and tile
    // inner: total gap between two adjacent tiles (split equally, so 4px each side)
    function applyGaps(rects, workArea) {
        if (!rects || rects.length === 0) return rects;
        const half = innerGap / 2;
        const result = [];
        for (let i = 0; i < rects.length; i++) {
            let r = rects[i];
            // Determine which edges touch the work area boundary
            const atLeft   = Math.abs(r.x - workArea.x) < 2;
            const atTop    = Math.abs(r.y - workArea.y) < 2;
            const atRight  = Math.abs((r.x + r.width)  - (workArea.x + workArea.width))  < 2;
            const atBottom = Math.abs((r.y + r.height) - (workArea.y + workArea.height)) < 2;

            const left   = atLeft   ? outerGap       : half;
            const top    = atTop    ? outerGap       : half;
            const right  = atRight  ? outerGap       : half;
            const bottom = atBottom ? outerGap       : half;

            result.push(Qt.rect(
                r.x + left,
                r.y + top,
                r.width  - left - right,
                r.height - top  - bottom
            ));
        }
        return result;
    }

    function setScreenLayout(outputName, layout) {
        const layouts = Object.assign({}, screenLayouts);
        if (layout) {
            layouts[outputName] = layout;
        } else {
            delete layouts[outputName];
        }
        screenLayouts = layouts;
    }

    function setScreenTiles(outputName, tiles) {
        const allTiles = Object.assign({}, screenTiles);
        if (tiles && tiles.length > 0) {
            allTiles[outputName] = tiles;
        } else {
            delete allTiles[outputName];
        }
        screenTiles = allTiles;
    }

    function clearLayoutMetadata(outputName) {
        if (!outputName) return;

        const modes = Object.assign({}, screenLayoutModes);
        delete modes[outputName];
        screenLayoutModes = modes;

        const focused = Object.assign({}, lastFocusedWindowKeys);
        delete focused[outputName];
        lastFocusedWindowKeys = focused;
    }

    function clearDisplacedWindowOwnersForLayout(outputName) {
        if (!outputName) return;

        const owners = Object.assign({}, displacedWindowOwners);
        let changed = false;
        for (const key in owners) {
            if (owners[key] === outputName) {
                delete owners[key];
                changed = true;
            }
        }

        if (changed) {
            displacedWindowOwners = owners;
        }
    }

    function desktopHasNormalWindows(desktop) {
        if (!desktop) return true;

        const wins = KWinComponents.Workspace.windows;
        for (let i = 0; i < wins.length; i++) {
            const win = wins[i];
            if (!win || win.deleted || !win.normalWindow) continue;
            if (!win.desktops || win.desktops.length === 0) continue;
            if (win.desktops.indexOf(desktop) >= 0) {
                return true;
            }
        }
        return false;
    }

    function fallbackDesktopForRemoval(desktop) {
        const desktops = KWinComponents.Workspace.desktops;
        if (!desktops || desktops.length < 2) return null;

        const current = KWinComponents.Workspace.currentDesktop;
        if (current && current !== desktop) {
            return current;
        }

        const index = desktops.indexOf(desktop);
        if (index > 0) {
            return desktops[index - 1];
        }
        if (desktops.length > 1) {
            return desktops[1];
        }
        return null;
    }

    function cleanupAutoParkingDesktopForLayout(outputName) {
        const desktop = desktopForLayoutKey(outputName);
        if (!desktop || !autoParkingDesktopKeys[desktopKey(desktop)]) return;
        if (desktopHasNormalWindows(desktop)) return;
        if (typeof KWinComponents.Workspace.removeDesktop !== "function") return;

        const fallback = fallbackDesktopForRemoval(desktop);
        if (!fallback) return;

        if (KWinComponents.Workspace.currentDesktop === desktop) {
            KWinComponents.Workspace.currentDesktop = fallback;
        }

        KWinComponents.Workspace.removeDesktop(desktop);
        forgetAutoParkingDesktop(desktop);
    }

    function cleanupAllAutoParkingDesktops() {
        const desktops = KWinComponents.Workspace.desktops;
        if (!desktops || typeof KWinComponents.Workspace.removeDesktop !== "function") return;

        const candidates = [];
        for (let i = 0; i < desktops.length; i++) {
            if (autoParkingDesktopKeys[desktopKey(desktops[i])] && !desktopHasNormalWindows(desktops[i])) {
                candidates.push(desktops[i]);
            }
        }

        for (let i = 0; i < candidates.length; i++) {
            const fallback = fallbackDesktopForRemoval(candidates[i]);
            if (!fallback) continue;

            if (KWinComponents.Workspace.currentDesktop === candidates[i]) {
                KWinComponents.Workspace.currentDesktop = fallback;
            }

            KWinComponents.Workspace.removeDesktop(candidates[i]);
            forgetAutoParkingDesktop(candidates[i]);
        }
    }

    function cleanupEmptyLayout(outputName) {
        if (!outputName || windowCountForLayout(outputName) > 0) return;

        setScreenLayout(outputName, null);
        setScreenTiles(outputName, []);
        clearLayoutMetadata(outputName);
        setMaximizedLayout(outputName, null);
        clearDisplacedWindowOwnersForLayout(outputName);
        cleanupAutoParkingDesktopForLayout(outputName);
    }

    function createLayoutTransaction() {
        return {
            retileLayouts: [],
            scheduledLayouts: [],
            cleanupLayouts: []
        };
    }

    function markLayoutRetiled(transaction, outputName) {
        if (!transaction || !outputName || transaction.retileLayouts.indexOf(outputName) >= 0) return;
        transaction.retileLayouts.push(outputName);
    }

    function markLayoutScheduled(transaction, outputName) {
        if (!transaction || !outputName || transaction.scheduledLayouts.indexOf(outputName) >= 0) return;
        transaction.scheduledLayouts.push(outputName);
    }

    function markLayoutCleanup(transaction, outputName) {
        if (!transaction || !outputName || transaction.cleanupLayouts.indexOf(outputName) >= 0) return;
        transaction.cleanupLayouts.push(outputName);
    }

    function markLayoutChanged(transaction, outputName) {
        markLayoutRetiled(transaction, outputName);
        markLayoutScheduled(transaction, outputName);
    }

    function applyLayoutTransaction(transaction) {
        if (!transaction) return;

        const retileLayouts = transaction.retileLayouts || [];
        for (let i = 0; i < retileLayouts.length; i++) {
            retileScreen(retileLayouts[i]);
        }

        const scheduledLayouts = transaction.scheduledLayouts || [];
        for (let i = 0; i < scheduledLayouts.length; i++) {
            scheduleRetile(scheduledLayouts[i]);
        }

        const cleanupLayouts = transaction.cleanupLayouts || [];
        for (let i = 0; i < cleanupLayouts.length; i++) {
            cleanupEmptyLayout(cleanupLayouts[i]);
        }
    }

    function makeLeaf(win) {
        return { kind: "leaf", win: win, rect: Qt.rect(0, 0, 0, 0) };
    }

    function makeSplit(orientation, first, second, ratio) {
        return {
            kind: "split",
            orientation: orientation,
            first: first,
            second: second,
            ratio: ratio || 0.5,
            rect: Qt.rect(0, 0, 0, 0)
        };
    }

    function splitRect(rect, orientation, ratio) {
        const splitRatio = ratio || 0.5;
        if (orientation === "vertical") {
            const firstWidth = Math.round(rect.width * splitRatio);
            return [
                Qt.rect(rect.x, rect.y, firstWidth, rect.height),
                Qt.rect(rect.x + firstWidth, rect.y, rect.width - firstWidth, rect.height)
            ];
        }

        const firstHeight = Math.round(rect.height * splitRatio);
        return [
            Qt.rect(rect.x, rect.y, rect.width, firstHeight),
            Qt.rect(rect.x, rect.y + firstHeight, rect.width, rect.height - firstHeight)
        ];
    }

    function collectLeaves(node, leaves) {
        if (!node) return;
        if (node.kind === "leaf") {
            leaves.push(node);
            return;
        }
        collectLeaves(node.first, leaves);
        collectLeaves(node.second, leaves);
    }

    function leafIndexForWindow(outputName, win) {
        const key = windowKey(win);
        if (!key) return -1;

        const leaves = [];
        collectLeaves(screenLayouts[outputName], leaves);
        for (let i = 0; i < leaves.length; i++) {
            if (windowKey(leaves[i].win) === key) {
                return i;
            }
        }
        return -1;
    }

    function leafForSlot(outputName, preferredIndex) {
        const leaves = [];
        collectLeaves(screenLayouts[outputName], leaves);
        if (leaves.length === 0) return null;
        if (preferredIndex >= 0 && preferredIndex < leaves.length) {
            return leaves[preferredIndex];
        }
        return leaves[leaves.length - 1];
    }

    function rememberFocusedWindow(win) {
        if (!isTileable(win)) return;

        const outputName = layoutKeyForWindow(win);
        const key = windowKey(win);
        if (!outputName || !key || !containsLeaf(screenLayouts[outputName], key)) return;

        const focused = Object.assign({}, lastFocusedWindowKeys);
        focused[outputName] = key;
        lastFocusedWindowKeys = focused;
    }

    function replacementLeafForLayout(outputName, fallbackIndex) {
        const focusedKey = lastFocusedWindowKeys[outputName] || "";
        const focusedLeaf = focusedKey ? findLeaf(screenLayouts[outputName], focusedKey) : null;
        if (focusedLeaf && focusedLeaf.win && layoutKeyForWindow(focusedLeaf.win) === outputName) {
            return focusedLeaf;
        }
        return leafForSlot(outputName, fallbackIndex);
    }

    function orderedWindowsForScreen(outputName) {
        const windows = [];
        const seen = {};
        const leaves = [];
        collectLeaves(screenLayouts[outputName], leaves);

        for (let i = 0; i < leaves.length; i++) {
            const win = leaves[i].win;
            const key = windowKey(win);
            if (!key || seen[key] || !isTileable(win)) continue;
            if (layoutKeyForWindow(win) !== outputName) continue;
            seen[key] = true;
            windows.push(win);
        }

        return windows;
    }

    function appendWindowIfMissing(windows, win) {
        const key = windowKey(win);
        if (!key) return false;

        for (let i = 0; i < windows.length; i++) {
            if (windowKey(windows[i]) === key) {
                return false;
            }
        }

        windows.push(win);
        return true;
    }

    function buildStableStack(windows, startIndex) {
        const remaining = windows.length - startIndex;
        if (remaining <= 0) return null;
        if (remaining === 1) return makeLeaf(windows[startIndex]);

        return makeSplit(
            "horizontal",
            makeLeaf(windows[startIndex]),
            buildStableStack(windows, startIndex + 1),
            1 / remaining
        );
    }

    function buildLinearLayout(windows, startIndex, orientation) {
        const remaining = windows.length - startIndex;
        if (remaining <= 0) return null;
        if (remaining === 1) return makeLeaf(windows[startIndex]);

        return makeSplit(
            orientation,
            makeLeaf(windows[startIndex]),
            buildLinearLayout(windows, startIndex + 1, orientation),
            1 / remaining
        );
    }

    function layoutModeForScreen(outputName) {
        const mode = screenLayoutModes[outputName];
        return layoutModes.indexOf(mode) >= 0 ? mode : "master";
    }

    function setLayoutModeForScreen(outputName, mode) {
        if (!outputName || layoutModes.indexOf(mode) < 0) return;
        const modes = Object.assign({}, screenLayoutModes);
        modes[outputName] = mode;
        screenLayoutModes = modes;
    }

    function buildStableLayout(windows, mode) {
        if (!windows || windows.length === 0) return null;
        if (windows.length === 1) return makeLeaf(windows[0]);
        if (mode === "columns") {
            return buildLinearLayout(windows, 0, "vertical");
        }
        if (mode === "rows") {
            return buildLinearLayout(windows, 0, "horizontal");
        }
        if (windows.length === 2) {
            return makeSplit("vertical", makeLeaf(windows[0]), makeLeaf(windows[1]), 0.5);
        }

        return makeSplit(
            "vertical",
            makeLeaf(windows[0]),
            buildStableStack(windows, 1),
            stablePrimaryRatio
        );
    }

    function setStableLayout(outputName, windows) {
        setScreenLayout(outputName, buildStableLayout(windows, layoutModeForScreen(outputName)));
    }

    function outputNameForActiveWindow() {
        const win = KWinComponents.Workspace.activeWindow;
        const activeName = layoutKeyForWindow(win);
        if (activeName !== "") return activeName;
        for (const name in screenLayouts) {
            return name;
        }
        return "";
    }

    function cycleLayoutMode(outputName) {
        if (!outputName) return;

        const currentMode = layoutModeForScreen(outputName);
        const currentIndex = layoutModes.indexOf(currentMode);
        const nextMode = layoutModes[(currentIndex + 1) % layoutModes.length];
        setLayoutModeForScreen(outputName, nextMode);

        const windows = orderedWindowsForScreen(outputName);
        if (windows.length > 0) {
            setStableLayout(outputName, windows);
            const transaction = createLayoutTransaction();
            markLayoutChanged(transaction, outputName);
            applyLayoutTransaction(transaction);
        }
    }

    function containsLeaf(node, key) {
        if (!node || !key) return false;
        if (node.kind === "leaf") return windowKey(node.win) === key;
        return containsLeaf(node.first, key) || containsLeaf(node.second, key);
    }

    function layoutTree(node, area, leaves) {
        if (!node || !area) return;
        node.rect = Qt.rect(area.x, area.y, area.width, area.height);

        if (node.kind === "leaf") {
            leaves.push(node);
            return;
        }

        const rects = splitRect(area, node.orientation, node.ratio);
        layoutTree(node.first, rects[0], leaves);
        layoutTree(node.second, rects[1], leaves);
    }

    function insertWindowInLayout(outputName, win) {
        const windows = orderedWindowsForScreen(outputName);
        if (!appendWindowIfMissing(windows, win)) return;
        setStableLayout(outputName, windows);
    }

    function removeWindowFromLayout(outputName, win) {
        const key = windowKey(win);
        const rootNode = screenLayouts[outputName];
        if (!key || !rootNode) return false;

        const leaves = [];
        const seen = {};
        const remaining = [];
        let removed = false;
        collectLeaves(rootNode, leaves);

        for (let i = 0; i < leaves.length; i++) {
            const leafWindow = leaves[i].win;
            const leafKey = windowKey(leafWindow);
            if (leafKey === key) {
                removed = true;
                continue;
            }

            if (!leafKey || seen[leafKey] || !isTileable(leafWindow)) continue;
            if (layoutKeyForWindow(leafWindow) !== outputName) continue;
            seen[leafKey] = true;
            remaining.push(leafWindow);
        }

        if (!removed) return false;
        setStableLayout(outputName, remaining);
        return true;
    }

    function collapseMaximizedLayout(outputName) {
        const state = maximizedLayouts[outputName];
        if (!state) return false;

        clearDisplacedWindowOwnersForWindows(state.originalWindows || []);
        setMaximizedLayout(outputName, null);

        const windows = orderedWindowsForScreen(outputName);
        setStableLayout(outputName, windows);
        const transaction = createLayoutTransaction();
        markLayoutChanged(transaction, outputName);
        markLayoutCleanup(transaction, outputName);
        applyLayoutTransaction(transaction);
        return true;
    }

    function normalizeLayoutConflicts(outputName) {
        if (!outputName) return false;

        const state = maximizedLayouts[outputName];
        if (!state) {
            cleanupEmptyLayout(outputName);
            return false;
        }

        const windows = orderedWindowsForScreen(outputName);
        if (windows.length === 1 && windowKey(windows[0]) === state.windowKey) {
            return false;
        }

        collapseMaximizedLayout(outputName);
        return true;
    }

    function releaseWindowForLayoutMove(win, sourceName, targetName) {
        const key = windowKey(win);
        if (!key || !sourceName || sourceName === targetName) return;

        const state = maximizedLayouts[sourceName];
        if (state && state.windowKey === key) {
            clearDisplacedWindowOwnersForWindows(state.originalWindows || []);
            setMaximizedLayout(sourceName, null);
        }
        clearDisplacedWindowOwnerByKey(key);
    }

    function swapWindowBetweenLayouts(win, sourceName, targetName) {
        if (!win || !sourceName || !targetName || sourceName === targetName) return false;

        const sourceDesktop = desktopForLayoutKey(sourceName);
        const targetDesktop = desktopForLayoutKey(targetName);
        const sourceLeaf = findLeaf(screenLayouts[sourceName], windowKey(win));
        if (!sourceDesktop || !targetDesktop || !sourceLeaf) return false;

        const sourceIndex = leafIndexForWindow(sourceName, win);
        const targetLeaf = replacementLeafForLayout(targetName, sourceIndex);
        if (!targetLeaf || !targetLeaf.win || windowKey(targetLeaf.win) === windowKey(win)) return false;

        const displacedWindow = targetLeaf.win;
        targetLeaf.win = win;
        sourceLeaf.win = displacedWindow;
        releaseWindowForLayoutMove(win, sourceName, targetName);
        releaseWindowForLayoutMove(displacedWindow, targetName, sourceName);
        assignWindowToDesktop(win, targetDesktop);
        assignWindowToDesktop(displacedWindow, sourceDesktop);

        normalizeLayoutConflicts(sourceName);
        normalizeLayoutConflicts(targetName);
        const transaction = createLayoutTransaction();
        markLayoutChanged(transaction, sourceName);
        markLayoutChanged(transaction, targetName);
        applyLayoutTransaction(transaction);
        KWinComponents.Workspace.activeWindow = win;
        return true;
    }

    // Recompute and apply layout for a single screen.
    function retileScreen(outputName) {
        const rootNode = screenLayouts[outputName];
        if (!rootNode) {
            setScreenTiles(outputName, []);
            return;
        }

        const leaves = [];
        collectLeaves(rootNode, leaves);
        if (leaves.length === 0) {
            setScreenTiles(outputName, []);
            return;
        }

        // Get work area from the first window's output
        let area = null;
        for (let i = 0; i < leaves.length; i++) {
            const r = workRect(leaves[i].win);
            if (r) { area = r; break; }
        }
        if (!area) return;

        const rawLeaves = [];
        layoutTree(rootNode, area, rawLeaves);
        const rawRects = [];
        for (let i = 0; i < rawLeaves.length; i++) {
            rawRects.push(rawLeaves[i].rect);
        }
        const gappedRects = applyGaps(rawRects, area);

        for (let i = 0; i < rawLeaves.length; i++) {
            const win = rawLeaves[i].win;
            if (!win || win.deleted) continue;
            const r = gappedRects[i];
            rawLeaves[i].rect = r;
            win.frameGeometry = r;
        }
        setScreenLayout(outputName, rootNode);
        setScreenTiles(outputName, rawLeaves.slice());
    }

    // Retile all layout trees.
    function retileAll() {
        for (const name in screenLayouts) {
            retileScreen(name);
        }
    }

    function scheduleRetileAll() {
        for (const name in screenLayouts) {
            scheduleRetile(name);
        }
    }

    function retileCurrentDesktopLayouts() {
        if (!isConvergence()) return;

        const desktop = KWinComponents.Workspace.currentDesktop;
        const screens = KWinComponents.Workspace.screens;
        if (!desktop || !screens) {
            scheduleRetileAll();
            return;
        }

        for (let i = 0; i < screens.length; i++) {
            const outputName = layoutKeyFor(screens[i].name, desktop);
            if (outputName !== "" && screenLayouts[outputName]) {
                const transaction = createLayoutTransaction();
                markLayoutChanged(transaction, outputName);
                applyLayoutTransaction(transaction);
            }
        }
    }

    function screenNameForWindow(win) {
        const key = windowKey(win);
        if (!key) return "";

        for (const name in screenLayouts) {
            if (containsLeaf(screenLayouts[name], key)) {
                return name;
            }
        }
        return "";
    }

    function reconcileWindowOutput(win) {
        if (!isTileable(win)) return;

        normalizeWindowDesktopScope(win);
        normalizeNativeWindowOperationState(win);

        connectDragHandlers(win);

        const output = win.output;
        if (!output) return;

        const currentName = screenNameForWindow(win);
        const targetName = layoutKeyForWindow(win);
        if (targetName === "") return;

        const transaction = createLayoutTransaction();

        if (currentName !== "" && currentName !== targetName) {
            releaseWindowForLayoutMove(win, currentName, targetName);
            if (maximizedLayouts[targetName]) {
                collapseMaximizedLayout(targetName);
            }
            if (windowCountForLayout(targetName) >= maxWindowsPerPage && swapWindowBetweenLayouts(win, currentName, targetName)) {
                return;
            }
            removeWindowFromLayout(currentName, win);
            markLayoutScheduled(transaction, currentName);
            markLayoutCleanup(transaction, currentName);
        }

        if (!containsLeaf(screenLayouts[targetName], windowKey(win))) {
            insertWindowInLayout(targetName, win);
        }

        normalizeLayoutConflicts(targetName);
        if (win.maximizable) win.setMaximize(false, false);
        win.noBorder = false;
        markLayoutChanged(transaction, targetName);
        applyLayoutTransaction(transaction);
    }

    // Add a window to its screen's tile list and retile.
    function addWindow(win) {
        if (!isTileable(win)) return;

        normalizeWindowDesktopScope(win);
        normalizeNativeWindowOperationState(win);

        connectDragHandlers(win);

        const output = win.output;
        if (!output) return;
        const initialName = layoutKeyForWindow(win);
        const target = targetLayoutForNewWindow(win);
        if (target.key === "") return;
        const name = target.key;

        if (target.desktop && initialName !== name) {
            assignWindowToDesktop(win, target.desktop);
            KWinComponents.Workspace.currentDesktop = target.desktop;
        }

        // Avoid duplicates
        const key = windowKey(win);
        if (containsLeaf(screenLayouts[name], key)) return;

        const tiles = screenTiles[name] || [];
        for (let i = 0; i < tiles.length; i++) {
            if (windowKey(tiles[i].win) === key) return;
        }

        insertWindowInLayout(name, win);
        normalizeLayoutConflicts(name);

        // Un-maximize now so the exclusive-zone Wayland roundtrip begins;
        // retileScreen runs 200 ms later when MaximizeArea has settled.
        // (Same pattern as convergentwindows constrainAfterRestoreTimer.)
        if (win.maximizable) win.setMaximize(false, false);
        win.noBorder = false;
        const transaction = createLayoutTransaction();
        markLayoutChanged(transaction, name);
        applyLayoutTransaction(transaction);
    }

    function adoptWindow(win) {
        if (!isConvergence() || !win || win.deleted || !win.normalWindow || !win.maximizable) return;

        normalizeNativeWindowOperationState(win);
        if (isTileable(win)) {
            addWindow(win);
        }
    }

    // Remove a window from its screen's tile list and retile.
    function removeWindow(win) {
        if (!win) return;

        const maximizedName = maximizedLayoutNameForWindow(win);
        if (maximizedName !== "") {
            restoreMaximizedLayout(maximizedName);
        }
        clearDisplacedWindowOwner(win);

        // Search all layout trees (window may have been moved or queued before
        // the deferred tile list has refreshed).
        for (const sName in screenLayouts) {
            if (removeWindowFromLayout(sName, win)) {
                const transaction = createLayoutTransaction();
                markLayoutRetiled(transaction, sName);
                markLayoutCleanup(transaction, sName);
                applyLayoutTransaction(transaction);
                return;
            }
        }
    }

    function windowKey(win) {
        return win && win.internalId !== undefined ? String(win.internalId) : "";
    }

    function findWindowById(windowId) {
        const wanted = String(windowId);
        const wins = KWinComponents.Workspace.windows;
        for (let i = 0; i < wins.length; i++) {
            if (windowKey(wins[i]) === wanted) {
                return wins[i];
            }
        }
        return null;
    }

    function floatWindow(win) {
        const key = windowKey(win);
        if (!key) return;
        const maximizedName = maximizedLayoutNameForWindow(win);
        if (maximizedName !== "") {
            restoreMaximizedLayout(maximizedName);
        }
        clearDisplacedWindowOwner(win);
        floatedWindows[key] = true;
        removeWindow(win);
    }

    function tileWindow(win) {
        const key = windowKey(win);
        if (!key) return;
        delete floatedWindows[key];
        clearDisplacedWindowOwner(win);
        if (isTileable(win)) {
            addWindow(win);
        }
    }

    function setMaximizedLayout(outputName, state) {
        const states = Object.assign({}, maximizedLayouts);
        if (state) {
            states[outputName] = state;
        } else {
            delete states[outputName];
        }
        maximizedLayouts = states;
        publishDynamicTilingWindowState();
    }

    function maximizedWindowIds() {
        const ids = [];
        const seen = {};
        for (const outputName in maximizedLayouts) {
            const state = maximizedLayouts[outputName];
            const key = state ? state.windowKey : "";
            if (key && !seen[key]) {
                ids.push(key);
                seen[key] = true;
            }
        }
        ids.sort();
        return ids;
    }

    function publishDynamicTilingWindowState() {
        const ids = maximizedWindowIds();
        const serialized = ids.join(",");
        if (serialized === lastPublishedMaximizedWindowIds) return;

        lastPublishedMaximizedWindowIds = serialized;
        if (ShellSettings.Settings.reportDynamicTilingWindowState !== undefined) {
            ShellSettings.Settings.reportDynamicTilingWindowState(ids);
        }
    }

    function clearDynamicTilingWindowState() {
        lastPublishedMaximizedWindowIds = "__unpublished__";
        if (ShellSettings.Settings.reportDynamicTilingWindowState !== undefined) {
            ShellSettings.Settings.reportDynamicTilingWindowState([]);
        }
    }

    function maximizedLayoutNameForWindow(win) {
        const key = windowKey(win);
        if (!key) return "";

        for (const outputName in maximizedLayouts) {
            const state = maximizedLayouts[outputName];
            if (state && state.windowKey === key) {
                return outputName;
            }
        }
        return "";
    }

    function setDisplacedWindowOwner(win, outputName) {
        const key = windowKey(win);
        if (!key || !outputName) return;

        const owners = Object.assign({}, displacedWindowOwners);
        owners[key] = outputName;
        displacedWindowOwners = owners;
    }

    function clearDisplacedWindowOwnerByKey(key) {
        if (!key || !displacedWindowOwners[key]) return;

        const owners = Object.assign({}, displacedWindowOwners);
        delete owners[key];
        displacedWindowOwners = owners;
    }

    function clearDisplacedWindowOwner(win) {
        clearDisplacedWindowOwnerByKey(windowKey(win));
    }

    function clearDisplacedWindowOwnersForWindows(windows) {
        if (!windows) return;

        const owners = Object.assign({}, displacedWindowOwners);
        let changed = false;
        for (let i = 0; i < windows.length; i++) {
            const key = windowKey(windows[i]);
            if (key && owners[key]) {
                delete owners[key];
                changed = true;
            }
        }

        if (changed) {
            displacedWindowOwners = owners;
        }
    }

    function restoreMaximizedLayout(outputName) {
        const state = maximizedLayouts[outputName];
        if (!state) return false;

        const sourceDesktop = state.sourceDesktop || desktopForLayoutKey(outputName);
        if (!sourceDesktop) {
            setMaximizedLayout(outputName, null);
            return false;
        }

        const transaction = createLayoutTransaction();
        markLayoutChanged(transaction, outputName);
        markLayoutCleanup(transaction, outputName);
        const restoredWindows = [];
        const seen = {};
        const originalWindows = state.originalWindows || [];

        for (let i = 0; i < originalWindows.length; i++) {
            const win = originalWindows[i];
            const key = windowKey(win);
            if (!key || seen[key] || !win || win.deleted || shouldIgnore(win) || floatedWindows[key]) continue;
            if (key !== state.windowKey && displacedWindowOwners[key] !== outputName) continue;

            const currentName = screenNameForWindow(win);
            if (currentName && currentName !== outputName) {
                removeWindowFromLayout(currentName, win);
                markLayoutChanged(transaction, currentName);
                markLayoutCleanup(transaction, currentName);
            }

            assignWindowToDesktop(win, sourceDesktop);
            restoredWindows.push(win);
            clearDisplacedWindowOwnerByKey(key);
            seen[key] = true;
        }

        setStableLayout(outputName, restoredWindows);
        setMaximizedLayout(outputName, null);
        applyLayoutTransaction(transaction);

        if (state.window && !state.window.deleted) {
            KWinComponents.Workspace.activeWindow = state.window;
        }
        if (restoredWindows.length > 0) {
            KWinComponents.Workspace.currentDesktop = sourceDesktop;
        }
        return true;
    }

    function restoreAllMaximizedLayouts() {
        const names = [];
        for (const outputName in maximizedLayouts) {
            names.push(outputName);
        }

        for (let i = 0; i < names.length; i++) {
            restoreMaximizedLayout(names[i]);
        }
        displacedWindowOwners = {};
        cleanupAllAutoParkingDesktops();
        publishDynamicTilingWindowState();
    }

    function toggleMaximizedLayout(win) {
        if (!isTileable(win)) return;

        const existingName = maximizedLayoutNameForWindow(win);
        if (existingName !== "") {
            restoreMaximizedLayout(existingName);
            return;
        }

        const outputName = screenNameForWindow(win) || layoutKeyForWindow(win);
        if (!outputName) return;

        if (maximizedLayouts[outputName]) {
            collapseMaximizedLayout(outputName);
        }

        const sourceDesktop = desktopForLayoutKey(outputName) || desktopForWindow(win);
        const windows = orderedWindowsForScreen(outputName);
        const key = windowKey(win);
        if (!sourceDesktop || !key || windows.length < 2) {
            clearDisplacedWindowOwnersForWindows(windows);
            const transaction = createLayoutTransaction();
            markLayoutChanged(transaction, outputName);
            applyLayoutTransaction(transaction);
            return;
        }

        const transaction = createLayoutTransaction();
        markLayoutChanged(transaction, outputName);
        const plannedMoves = [];
        const reservedCounts = {};

        for (let i = 0; i < windows.length; i++) {
            const other = windows[i];
            if (windowKey(other) === key) continue;

            const target = targetLayoutForDisplacedWindow(other, sourceDesktop, reservedCounts);
            if (target.key === "" || !target.desktop) {
                applyLayoutTransaction(transaction);
                return;
            }

            plannedMoves.push({ window: other, target: target });
            reservedCounts[target.key] = (reservedCounts[target.key] || 0) + 1;
        }

        const movedWindows = [];
        clearDisplacedWindowOwnersForWindows(windows);

        for (let i = 0; i < plannedMoves.length; i++) {
            const other = plannedMoves[i].window;
            const target = plannedMoves[i].target;

            removeWindowFromLayout(outputName, other);
            assignWindowToDesktop(other, target.desktop);
            insertWindowInLayout(target.key, other);
            setDisplacedWindowOwner(other, outputName);
            markLayoutChanged(transaction, target.key);
            movedWindows.push(other);
        }

        if (movedWindows.length === 0) {
            setStableLayout(outputName, windows);
            applyLayoutTransaction(transaction);
            return;
        }

        setStableLayout(outputName, [win]);
        setMaximizedLayout(outputName, {
            window: win,
            windowKey: key,
            sourceDesktop: sourceDesktop,
            originalWindows: windows,
            movedWindows: movedWindows
        });

        applyLayoutTransaction(transaction);

        KWinComponents.Workspace.currentDesktop = sourceDesktop;
        KWinComponents.Workspace.activeWindow = win;
    }

    function handleNativeMaximizeChange(win) {
        if (!win || !isWindowMaximized(win) || !dynamicTilingOwnsWindow(win)) return;

        win.setMaximize(false, false);
        normalizeNativeWindowOperationState(win);
        toggleMaximizedLayout(win);
    }

    function handleNativeWindowOperationStateChange(win) {
        if (!normalizeNativeWindowOperationState(win)) return;

        reconcileWindowOutput(win);
    }

    function isWindowMaximized(win) {
        if (!win) return false;
        if (win.maximizeMode !== undefined) return win.maximizeMode !== 0;
        if (win.maximized !== undefined) return win.maximized;
        return false;
    }

    function connectDragHandlers(win) {
        const key = windowKey(win);
        if (!key || dragConnectedWindows[key]) return;

        const handlers = {
            window: win,
            moveStarted: function() { if (root) root.onDragStart(win); },
            moveStepped: function(geo) { if (root) root.onDragStep(win, geo); },
            moveFinished: function() { if (root) root.onDragEnd(win); },
            desktopsChanged: null,
            maximizedChanged: null,
            maximizeModeChanged: null,
            fullScreenChanged: null,
            noBorderChanged: null,
            keepAboveChanged: null,
            keepBelowChanged: null
        };

        dragConnectedWindows[key] = handlers;
        win.interactiveMoveResizeStarted.connect(handlers.moveStarted);
        win.interactiveMoveResizeStepped.connect(handlers.moveStepped);
        win.interactiveMoveResizeFinished.connect(handlers.moveFinished);
        if (win.desktopsChanged !== undefined) {
            handlers.desktopsChanged = function() { if (root) root.reconcileWindowOutput(win); };
            win.desktopsChanged.connect(handlers.desktopsChanged);
        }
        if (win.maximizedChanged !== undefined) {
            handlers.maximizedChanged = function() { if (root) root.handleNativeMaximizeChange(win); };
            win.maximizedChanged.connect(handlers.maximizedChanged);
        }
        if (win.maximizeModeChanged !== undefined) {
            handlers.maximizeModeChanged = function() { if (root) root.handleNativeMaximizeChange(win); };
            win.maximizeModeChanged.connect(handlers.maximizeModeChanged);
        }
        if (win.fullScreenChanged !== undefined) {
            handlers.fullScreenChanged = function() { if (root) root.handleNativeWindowOperationStateChange(win); };
            win.fullScreenChanged.connect(handlers.fullScreenChanged);
        }
        if (win.noBorderChanged !== undefined) {
            handlers.noBorderChanged = function() { if (root) root.handleNativeWindowOperationStateChange(win); };
            win.noBorderChanged.connect(handlers.noBorderChanged);
        }
        if (win.keepAboveChanged !== undefined) {
            handlers.keepAboveChanged = function() { if (root) root.handleNativeWindowOperationStateChange(win); };
            win.keepAboveChanged.connect(handlers.keepAboveChanged);
        }
        if (win.keepBelowChanged !== undefined) {
            handlers.keepBelowChanged = function() { if (root) root.handleNativeWindowOperationStateChange(win); };
            win.keepBelowChanged.connect(handlers.keepBelowChanged);
        }
    }

    function disconnectDragHandlers() {
        for (const key in dragConnectedWindows) {
            const handlers = dragConnectedWindows[key];
            if (!handlers || !handlers.window) continue;

            const win = handlers.window;
            try { win.interactiveMoveResizeStarted.disconnect(handlers.moveStarted); } catch (error) {}
            try { win.interactiveMoveResizeStepped.disconnect(handlers.moveStepped); } catch (error) {}
            try { win.interactiveMoveResizeFinished.disconnect(handlers.moveFinished); } catch (error) {}
            if (handlers.desktopsChanged) {
                try { win.desktopsChanged.disconnect(handlers.desktopsChanged); } catch (error) {}
            }
            if (handlers.maximizedChanged) {
                try { win.maximizedChanged.disconnect(handlers.maximizedChanged); } catch (error) {}
            }
            if (handlers.maximizeModeChanged) {
                try { win.maximizeModeChanged.disconnect(handlers.maximizeModeChanged); } catch (error) {}
            }
            if (handlers.fullScreenChanged) {
                try { win.fullScreenChanged.disconnect(handlers.fullScreenChanged); } catch (error) {}
            }
            if (handlers.noBorderChanged) {
                try { win.noBorderChanged.disconnect(handlers.noBorderChanged); } catch (error) {}
            }
            if (handlers.keepAboveChanged) {
                try { win.keepAboveChanged.disconnect(handlers.keepAboveChanged); } catch (error) {}
            }
            if (handlers.keepBelowChanged) {
                try { win.keepBelowChanged.disconnect(handlers.keepBelowChanged); } catch (error) {}
            }
        }
        dragConnectedWindows = ({});
    }

    function moveWindowToDesktop(win, desktop) {
        if (!win || !desktop || !win.output || shouldIgnore(win)) return false;

        const targetName = layoutKeyFor(win.output.name, desktop);
        if (targetName === "") return false;

        const sourceName = screenNameForWindow(win);
        if (sourceName === targetName) {
            assignWindowToDesktop(win, desktop);
            KWinComponents.Workspace.currentDesktop = desktop;
            KWinComponents.Workspace.activeWindow = win;
            return true;
        }

        if (shouldFloat(win)) {
            clearDisplacedWindowOwner(win);
            assignWindowToDesktop(win, desktop);
            KWinComponents.Workspace.currentDesktop = desktop;
            KWinComponents.Workspace.activeWindow = win;
            return true;
        }

        connectDragHandlers(win);
        if (maximizedLayouts[targetName]) {
            collapseMaximizedLayout(targetName);
        }
        if (sourceName !== "" && windowCountForLayout(targetName) >= maxWindowsPerPage) {
            if (swapWindowBetweenLayouts(win, sourceName, targetName)) {
                KWinComponents.Workspace.currentDesktop = desktop;
                KWinComponents.Workspace.activeWindow = win;
                return true;
            }
            return false;
        }

        const transaction = createLayoutTransaction();
        if (sourceName !== "") {
            releaseWindowForLayoutMove(win, sourceName, targetName);
            if (removeWindowFromLayout(sourceName, win)) {
                markLayoutScheduled(transaction, sourceName);
                markLayoutCleanup(transaction, sourceName);
            }
        }

        assignWindowToDesktop(win, desktop);
        if (!containsLeaf(screenLayouts[targetName], windowKey(win))) {
            insertWindowInLayout(targetName, win);
        }

        normalizeLayoutConflicts(targetName);
        if (win.maximizable) win.setMaximize(false, false);
        win.noBorder = false;
        markLayoutChanged(transaction, targetName);
        applyLayoutTransaction(transaction);
        KWinComponents.Workspace.currentDesktop = desktop;
        KWinComponents.Workspace.activeWindow = win;
        return true;
    }

    function handleWindowTilingRequest() {
        const serial = ShellSettings.Settings.dynamicTilingWindowRequestSerial;
        if (serial === lastWindowRequestSerial) return;
        lastWindowRequestSerial = serial;

        if (!isConvergence()) return;
        const win = findWindowById(ShellSettings.Settings.dynamicTilingWindowRequestId);
        if (!win || shouldIgnore(win)) return;

        const action = ShellSettings.Settings.dynamicTilingWindowRequestAction;
        if (action === "float") {
            floatWindow(win);
        } else if (action === "tile") {
            tileWindow(win);
        } else if (action === "maximize" || action === "maximize-toggle") {
            toggleMaximizedLayout(win);
        } else if (action === "restore") {
            const outputName = maximizedLayoutNameForWindow(win);
            if (outputName !== "") {
                restoreMaximizedLayout(outputName);
            }
        } else if (action.indexOf("move-to-desktop:") === 0) {
            const payload = action.slice("move-to-desktop:".length);
            const separator = payload.lastIndexOf("|");
            const desktopId = separator >= 0 ? payload.slice(0, separator) : payload;
            const desktopNumber = separator >= 0 ? payload.slice(separator + 1) : "";
            const desktop = desktopForMoveRequest(desktopId, desktopNumber);
            if (desktop) {
                moveWindowToDesktop(win, desktop);
            }
        } else if (action === "move-to-new-desktop") {
            const desktop = createUserDesktop();
            if (desktop) {
                moveWindowToDesktop(win, desktop);
            }
        }
    }

    function promoteWindow(win) {
        if (!isTileable(win)) return;

        const outputName = screenNameForWindow(win) || layoutKeyForWindow(win);
        if (!outputName) return;

        const key = windowKey(win);
        const windows = orderedWindowsForScreen(outputName);
        if (!key || windows.length < 2) return;

        const reordered = [];
        let promoted = null;
        for (let i = 0; i < windows.length; i++) {
            if (windowKey(windows[i]) === key) {
                promoted = windows[i];
            } else {
                reordered.push(windows[i]);
            }
        }

        if (!promoted) return;
        reordered.unshift(promoted);
        setStableLayout(outputName, reordered);
        const transaction = createLayoutTransaction();
        markLayoutChanged(transaction, outputName);
        applyLayoutTransaction(transaction);
        KWinComponents.Workspace.activeWindow = win;
    }

    // ── Keyboard navigation helpers ──────────────────────────────────────────

    function centreOf(rect) {
        return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
    }

    // Find the tile on-screen whose centre is most in `direction` from `fromRect`.
    // direction: "left"|"right"|"up"|"down"
    function findNeighbour(fromWin, direction) {
        const outputName = layoutKeyForWindow(fromWin);
        if (!outputName) return null;
        const tiles = screenTiles[outputName];
        if (!tiles) return null;

        const from = fromWin.frameGeometry;
        const fc = centreOf(from);
        let best = null;
        let bestScore = Infinity;

        for (let i = 0; i < tiles.length; i++) {
            const t = tiles[i];
            if (windowKey(t.win) === windowKey(fromWin)) continue;
            const tc = centreOf(t.rect);
            const dx = tc.x - fc.x;
            const dy = tc.y - fc.y;

            let inDirection = false;
            let primary = 0;
            let secondary = 0;
            switch (direction) {
            case "left":  inDirection = dx < -5; primary = -dx; secondary = Math.abs(dy); break;
            case "right": inDirection = dx >  5; primary =  dx; secondary = Math.abs(dy); break;
            case "up":    inDirection = dy < -5; primary = -dy; secondary = Math.abs(dx); break;
            case "down":  inDirection = dy >  5; primary =  dy; secondary = Math.abs(dx); break;
            }
            if (!inDirection) continue;
            // Score: penalise perpendicular distance lightly
            const score = primary + secondary * 0.3;
            if (score < bestScore) { bestScore = score; best = t.win; }
        }
        return best;
    }

    // ── Workspace connections ─────────────────────────────────────────────

    Connections {
        target: KWinComponents.Workspace

        function onWindowAdded(win) {
            root.adoptWindow(win);
        }

        function onWindowRemoved(win) {
            const key = root.windowKey(win);
            if (key) delete root.dragConnectedWindows[key];
            root.removeWindow(win);
        }

        function onActiveWindowChanged() {
            root.rememberFocusedWindow(KWinComponents.Workspace.activeWindow);
        }

        function onCurrentDesktopChanged() {
            root.retileCurrentDesktopLayouts();
        }

        function onScreensChanged() {
            if (!root.isConvergence()) {
                return;
            }

            const wins = KWinComponents.Workspace.windows;
            for (let i = 0; i < wins.length; i++) {
                root.reconcileWindowOutput(wins[i]);
            }

            root.scheduleRetileAll();
        }
    }

    Connections {
        target: ShellSettings.Settings

        function onConvergenceModeEnabledChanged() {
            if (isConvergence()) {
                // Tile all existing normal windows
                const wins = KWinComponents.Workspace.windows;
                for (let i = 0; i < wins.length; i++) {
                    adoptWindow(wins[i]);
                }
            } else {
                // Clear all tiles — the convergentwindows script will re-maximize
                restoreAllMaximizedLayouts();
                screenLayouts = {};
                screenTiles = {};
                screenLayoutModes = {};
                lastFocusedWindowKeys = {};
                maximizedLayouts = {};
            }
        }

        function onGamingModeEnabledChanged() {
            if (ShellSettings.Settings.gamingModeEnabled) {
                restoreAllMaximizedLayouts();
                screenLayouts = {};
                screenTiles = {};
                screenLayoutModes = {};
                lastFocusedWindowKeys = {};
                maximizedLayouts = {};
            } else if (isConvergence()) {
                const wins = KWinComponents.Workspace.windows;
                for (let i = 0; i < wins.length; i++) {
                    adoptWindow(wins[i]);
                }
            }
        }

        function onDynamicTilingEnabledChanged() {
            if (isConvergence()) {
                const wins = KWinComponents.Workspace.windows;
                for (let i = 0; i < wins.length; i++) {
                    adoptWindow(wins[i]);
                }
            } else {
                // Tiling turned off — leave windows where they are.
                restoreAllMaximizedLayouts();
                screenLayouts = {};
                screenTiles = {};
                screenLayoutModes = {};
                lastFocusedWindowKeys = {};
                maximizedLayouts = {};
            }
        }

        function onDynamicTilingWindowRequestChanged() {
            root.handleWindowTilingRequest();
        }
    }

    // ── Drag handlers ─────────────────────────────────────────────────────

    // Find the (screen, index) of an existing tile holding this window.
    function findTileSlot(win) {
        for (const sName in screenTiles) {
            const tiles = screenTiles[sName];
            for (let i = 0; i < tiles.length; i++) {
                if (tiles[i].win && windowKey(tiles[i].win) === windowKey(win)) {
                    return { screen: sName, index: i };
                }
            }
        }
        return null;
    }

    // Find the tile under a cursor position, ignoring the dragged window.
    function findTileAtCursor(cursor, ignoreWin) {
        const activeName = dragSourceScreen || layoutKeyForWindow(ignoreWin);
        for (const sName in screenTiles) {
            if (activeName && sName !== activeName) continue;
            const tiles = screenTiles[sName];
            for (let i = 0; i < tiles.length; i++) {
                const t = tiles[i];
                if (ignoreWin && t.win && windowKey(t.win) === windowKey(ignoreWin)) continue;
                const r = t.rect;
                if (!r || r.width <= 0 || r.height <= 0) continue;
                if (cursor.x >= r.x && cursor.x <= r.x + r.width &&
                    cursor.y >= r.y && cursor.y <= r.y + r.height) {
                    return { screen: sName, index: i, rect: r, key: windowKey(t.win), win: t.win };
                }
            }
        }
        return null;
    }

    function findLeaf(node, key) {
        if (!node || !key) return null;
        if (node.kind === "leaf") {
            return windowKey(node.win) === key ? node : null;
        }
        return findLeaf(node.first, key) || findLeaf(node.second, key);
    }

    function swapDraggedWindow(target) {
        if (!target || !draggingWindow || !dragSourceScreen) return;

        const sourceKey = windowKey(draggingWindow);
        if (!sourceKey || sourceKey === target.key || target.screen !== dragSourceScreen) return;

        const rootNode = screenLayouts[dragSourceScreen];
        const sourceLeaf = findLeaf(rootNode, sourceKey);
        const targetLeaf = findLeaf(rootNode, target.key);
        if (!sourceLeaf || !targetLeaf) return;

        const sourceWindow = sourceLeaf.win;
        sourceLeaf.win = targetLeaf.win;
        targetLeaf.win = sourceWindow;
        const transaction = createLayoutTransaction();
        markLayoutChanged(transaction, dragSourceScreen);
        applyLayoutTransaction(transaction);
        KWinComponents.Workspace.activeWindow = sourceWindow;
    }

    function validRect(rect) {
        return rect && rect.width > 0 && rect.height > 0;
    }

    function rectContainsPoint(rect, point) {
        return validRect(rect) &&
               point.x >= rect.x && point.x <= rect.x + rect.width &&
               point.y >= rect.y && point.y <= rect.y + rect.height;
    }

    function rectsClose(a, b) {
        return Math.abs(a.x - b.x) < 2 &&
               Math.abs(a.y - b.y) < 2 &&
               Math.abs(a.width - b.width) < 2 &&
               Math.abs(a.height - b.height) < 2;
    }

    function outsideWorkArea(win, cursor) {
        const area = workRect(win);
        if (!area) return false;
        const expanded = Qt.rect(
            area.x - floatEscapeMargin,
            area.y - floatEscapeMargin,
            area.width + floatEscapeMargin * 2,
            area.height + floatEscapeMargin * 2
        );
        return !rectContainsPoint(expanded, cursor);
    }

    function showDragOutline(mode, rect) {
        if (!validRect(rect)) {
            clearDragOutline();
            return;
        }
        if (dragOutlineActive && dragDropMode === mode && rectsClose(dragOutlineRect, rect)) {
            return;
        }
        dragDropMode = mode;
        dragOutlineRect = Qt.rect(rect.x, rect.y, rect.width, rect.height);
        dragOutlineActive = true;
    }

    function clearDragOutline() {
        dragOutlineActive = false;
        dragDropMode = "";
        dragOutlineRect = Qt.rect(0, 0, 0, 0);
    }

    function resetDragState() {
        clearDragOutline();
        dragSwapTarget = null;
        dragSourceScreen = "";
        dragSourceIndex = -1;
        dragSourceRect = Qt.rect(0, 0, 0, 0);
        dragNativeResize = false;
        draggingWindow = null;
    }

    function updateDragIntent(win, geo) {
        if (validRect(dragSourceRect) && validRect(geo)
                && (Math.abs(geo.width - dragSourceRect.width) > 2
                    || Math.abs(geo.height - dragSourceRect.height) > 2)) {
            dragNativeResize = true;
            dragSwapTarget = null;
            showDragOutline("restore", dragSourceRect);
            return;
        }

        const cursor = KWinComponents.Workspace.cursorPos;
        const target = findTileAtCursor(cursor, win);

        if (target && target.screen === dragSourceScreen && validRect(target.rect)) {
            dragSwapTarget = {
                screen: target.screen,
                key: target.key,
                rect: target.rect
            };
            showDragOutline("swap", target.rect);
            return;
        }

        dragSwapTarget = null;
        if (outsideWorkArea(win, cursor)) {
            showDragOutline("float", validRect(geo) ? geo : win.frameGeometry);
        } else {
            showDragOutline("restore", dragSourceRect);
        }
    }

    function onDragStart(win) {
        if (!isConvergence()) return;
        draggingWindow = win;
        clearDragOutline();
        dragSwapTarget = null;
        dragNativeResize = false;
        dragSourceRect = Qt.rect(0, 0, 0, 0);

        // Remember the source slot so we can swap on drop.
        // The tile stays in screenTiles[] during the drag so the rest of
        // the layout doesn't shuffle.
        const slot = findTileSlot(win);
        if (slot) {
            dragSourceScreen = slot.screen;
            dragSourceIndex  = slot.index;
            const tiles = screenTiles[slot.screen];
            if (tiles && tiles[slot.index]) {
                const rect = tiles[slot.index].rect;
                dragSourceRect = Qt.rect(rect.x, rect.y, rect.width, rect.height);
            }
        } else {
            dragSourceScreen = "";
            dragSourceIndex  = -1;
        }

        showDragOutline("restore", dragSourceRect);
    }

    function onDragStep(win, geo) {
        if (!isConvergence()) {
            resetDragState();
            return;
        }
        if (draggingWindow !== win) return;
        updateDragIntent(win, geo);
    }

    function onDragEnd(win) {
        if (!isConvergence()) {
            resetDragState();
            return;
        }

        const finalDropMode = dragDropMode;
        clearDragOutline();

        if (dragNativeResize && dragSourceScreen && dragSourceIndex >= 0) {
            retileScreen(dragSourceScreen);
        }

        else if (finalDropMode === "float") {
            floatWindow(win);
        }

        else if (finalDropMode === "swap" && dragSwapTarget) {
            swapDraggedWindow(dragSwapTarget);
        }

        // Dropped elsewhere → restore the source tile to its original slot.
        // (KWin's native quick-tile may have moved the window; retileScreen
        // sets frameGeometry back to the BSP rect so the layout stays intact.)
        else if (dragSourceScreen && dragSourceIndex >= 0) {
            retileScreen(dragSourceScreen);
        }

        resetDragState();
    }

    // ── Keyboard shortcuts ─────────────────────────────────────────────────

    // Focus navigation
    KWinComponents.ShortcutHandler {
        name: "SHIFT Tiling Focus Left"
        text: "SHIFT Tiling: Focus window to the left"
        sequence: "Meta+H"
        onActivated: {
            const win = KWinComponents.Workspace.activeWindow;
            if (!win) return;
            const target = root.findNeighbour(win, "left");
            if (target) KWinComponents.Workspace.activeWindow = target;
        }
    }
    KWinComponents.ShortcutHandler {
        name: "SHIFT Tiling Focus Right"
        text: "SHIFT Tiling: Focus window to the right"
        sequence: "Meta+L"
        onActivated: {
            const win = KWinComponents.Workspace.activeWindow;
            if (!win) return;
            const target = root.findNeighbour(win, "right");
            if (target) KWinComponents.Workspace.activeWindow = target;
        }
    }
    KWinComponents.ShortcutHandler {
        name: "SHIFT Tiling Focus Up"
        text: "SHIFT Tiling: Focus window above"
        sequence: "Meta+K"
        onActivated: {
            const win = KWinComponents.Workspace.activeWindow;
            if (!win) return;
            const target = root.findNeighbour(win, "up");
            if (target) KWinComponents.Workspace.activeWindow = target;
        }
    }
    KWinComponents.ShortcutHandler {
        name: "SHIFT Tiling Focus Down"
        text: "SHIFT Tiling: Focus window below"
        sequence: "Meta+J"
        onActivated: {
            const win = KWinComponents.Workspace.activeWindow;
            if (!win) return;
            const target = root.findNeighbour(win, "down");
            if (target) KWinComponents.Workspace.activeWindow = target;
        }
    }

    // Float toggle
    KWinComponents.ShortcutHandler {
        name: "SHIFT Tiling Float Toggle"
        text: "SHIFT Tiling: Toggle float for active window"
        sequence: "Meta+F"
        onActivated: {
            const win = KWinComponents.Workspace.activeWindow;
            if (!win) return;
            const id = root.windowKey(win);
            if (root.floatedWindows[id]) {
                root.tileWindow(win);
            } else {
                root.floatWindow(win);
            }
        }
    }

    KWinComponents.ShortcutHandler {
        name: "SHIFT Tiling Promote Primary"
        text: "SHIFT Tiling: Promote active window to primary tile"
        sequence: "Meta+Shift+Return"
        onActivated: {
            const win = KWinComponents.Workspace.activeWindow;
            if (win) root.promoteWindow(win);
        }
    }

    KWinComponents.ShortcutHandler {
        name: "SHIFT Tiling Cycle Layout"
        text: "SHIFT Tiling: Cycle layout mode"
        sequence: "Meta+Shift+T"
        onActivated: root.cycleLayoutMode(root.outputNameForActiveWindow())
    }

    // Tiling on/off
    KWinComponents.ShortcutHandler {
        name: "SHIFT Tiling Toggle"
        text: "SHIFT Tiling: Toggle tiling on/off"
        sequence: "Meta+T"
        onActivated: {
            ShellSettings.Settings.dynamicTilingEnabled = !ShellSettings.Settings.dynamicTilingEnabled;
        }
    }

    // ── Component setup ───────────────────────────────────────────────────

    Component.onCompleted: {
        // Connect to existing windows
        const wins = KWinComponents.Workspace.windows;
        for (let i = 0; i < wins.length; i++) {
            adoptWindow(wins[i]);
        }
        rememberFocusedWindow(KWinComponents.Workspace.activeWindow);
        publishDynamicTilingWindowState();
    }

    Component.onDestruction: {
        disconnectDragHandlers();
        clearDynamicTilingWindowState();
    }
}
