pragma Singleton

// Display configuration: resolution, refresh rate, scale, and rotation.
//
// This is the only page in Panama Settings where a wrong value can leave you
// unable to SEE the screen well enough to undo it. A mode the display cannot
// show, or a scale that makes everything unreadable, is not recoverable through
// the same UI that caused it.
//
// So a change is never applied irreversibly. It is applied, then reverted
// automatically after a countdown unless confirmed -- the same contract every
// desktop uses for this one setting, and for the same reason. Confirming is
// what writes it to the settings store; letting the countdown run leaves
// nothing behind.
//
// Applied with `hyprctl eval` and hl.monitor{}. As everywhere else in Panama,
// success means the value was read back from the compositor and matched, never
// that a command exited zero.

import Quickshell
import Quickshell.Io
import QtQuick
import qs.config

Singleton {
    id: root

    // [{ name, description, width, height, refreshRate, scale, transform,
    //    modes: [{ label, mode, width, height, refresh }] }]
    property var monitors: []
    property string lastError: ""

    // Set while a change is applied but not yet confirmed.
    property string pendingOutput: ""
    property var pendingPrevious: null
    property var pendingRequested: null
    property bool pendingVerified: false
    property bool revertQueued: false
    property var revertExpected: null
    property string revertReason: ""
    property bool revertVerificationActive: false
    property int operationGeneration: 0
    property int revertGeneration: -1
    property bool externalChangeBlocked: false
    property int secondsLeft: 0

    readonly property bool awaitingConfirmation: root.pendingOutput !== ""
    readonly property bool canConfirm: root.awaitingConfirmation
        && root.pendingVerified
        && !root.busy
    readonly property bool busy: query.running || applyRun.running || revertRun.running
        || root.revertExpected !== null

    readonly property int confirmSeconds: 15

    readonly property var transforms: [
        { value: 0, label: "Landscape" },
        { value: 1, label: "Portrait" },
        { value: 2, label: "Landscape (flipped)" },
        { value: 3, label: "Portrait (flipped)" }
    ]

    // Scales that divide this desktop's common resolutions into whole pixels.
    // Hyprland rejects a fractional scale that does not, and the message it
    // gives is not something to put in front of a user.
    readonly property var scales: [1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0]

    Process {
        id: query
        property int generation: 0
        command: ["hyprctl", "-j", "monitors"]
        stdout: StdioCollector {
            onStreamFinished: root.parse(this.text, query.generation)
        }
        onExited: (exitCode, exitStatus) => {
            if (exitCode !== 0)
                root.lastError = "Could not read the connected displays.";
            if (root.revertQueued && !applyRun.running && root.awaitingConfirmation)
                root.performRevert();
        }
    }

    Process {
        id: applyRun
        onExited: (exitCode, exitStatus) => {
            if (!root.awaitingConfirmation)
                return;
            if (root.revertQueued) {
                if (!query.running)
                    root.performRevert();
                return;
            }
            if (exitCode !== 0) {
                root.revertWithMessage("The display rejected that change and Panama restored the previous setting.");
                return;
            }
            verifyTimer.attempts = 0;
            verifyTimer.ticks = 0;
            verifyTimer.restart();
        }
    }

    Process {
        id: revertRun
        onExited: (exitCode, exitStatus) => {
            // Exit status is advisory only. Hyprland's Lua bridge can report
            // success without applying a value, so exact readback decides.
            root.revertVerificationActive = true;
            revertVerifyTimer.attempts = 0;
            revertVerifyTimer.ticks = 0;
            revertVerifyTimer.restart();
        }
    }

    Component.onCompleted: root.refresh()

    function refresh(): bool {
        if (!query.running) {
            query.generation = root.operationGeneration;
            query.running = true;
            return true;
        }
        return false;
    }

    function parse(text: string, generation: int): void {
        try {
            const raw = JSON.parse(text);
            root.monitors = raw.map(monitor => {
                const modes = root.normaliseModes(monitor.availableModes ?? []);
                const width = monitor.width ?? 0;
                const height = monitor.height ?? 0;
                const refreshRate = monitor.refreshRate ?? 0;
                const current = modes
                    .filter(mode => mode.width === width && mode.height === height)
                    .sort((left, right) =>
                        Math.abs(left.refresh - refreshRate) - Math.abs(right.refresh - refreshRate))[0];
                return {
                    name: monitor.name ?? "",
                    description: monitor.description ?? monitor.model ?? "Display",
                    width: width,
                    height: height,
                    refreshRate: refreshRate,
                    mode: current?.mode ?? `${width}x${height}@${refreshRate}`,
                    scale: monitor.scale ?? 1,
                    transform: monitor.transform ?? 0,
                    currentFormat: monitor.currentFormat ?? "",
                    colorPreset: monitor.colorManagementPreset ?? "",
                    vrr: monitor.vrr === true,
                    modes: modes
                };
            });
            if (root.awaitingConfirmation && root.pendingRequested
                    && root.matchesRequest(root.monitorNamed(root.pendingOutput), root.pendingRequested)) {
                root.pendingVerified = true;
                verifyTimer.stop();
                root.lastError = "";
            } else if (root.revertVerificationActive
                    && generation === root.revertGeneration
                    && root.revertExpected
                    && root.matchesRequest(root.monitorNamed(root.revertExpected.output), root.revertExpected)) {
                revertVerifyTimer.stop();
                root.revertVerificationActive = false;
                root.revertGeneration = -1;
                root.revertExpected = null;
                if (root.revertReason === "")
                    root.lastError = "";
                else
                    root.lastError = root.revertReason;
                root.revertReason = "";
            } else if (!root.awaitingConfirmation && !root.revertExpected && (
                    root.lastError === "Could not read the connected displays."
                    || root.lastError === "The display list could not be read.")) {
                root.lastError = "";
            }
        } catch (error) {
            root.lastError = "The display list could not be read.";
        }
    }

    // "4500x3000@60.00Hz" -> a sortable record. The compositor reports the same
    // resolution at distinct rates such as 60.00 and 59.94. Those identities
    // remain separate because confirmation and recovery must read back the
    // exact mode the user chose, even when their rounded labels look similar.
    function normaliseModes(raw: var): var {
        const seen = {};
        const out = [];
        for (const entry of raw) {
            const match = String(entry).match(/^(\d+)x(\d+)@([\d.]+)Hz$/);
            if (!match)
                continue;
            const width = Number(match[1]);
            const height = Number(match[2]);
            const refreshText = match[3];
            const refresh = Number(refreshText);
            const roundedRefresh = Math.round(refresh);
            const key = `${width}x${height}@${refreshText}`;
            if (seen[key])
                continue;
            seen[key] = true;
            out.push({
                label: `${width} × ${height}`,
                refreshLabel: Math.abs(refresh - roundedRefresh) < 0.005
                    ? `${roundedRefresh} Hz`
                    : `${refresh.toFixed(2)} Hz`,
                mode: `${width}x${height}@${refreshText}`,
                width: width,
                height: height,
                refresh: refresh
            });
        }
        return out.sort((a, b) => (b.width * b.height) - (a.width * a.height) || b.refresh - a.refresh);
    }

    function monitorNamed(name: string): var {
        return root.monitors.find(monitor => monitor.name === name) ?? null;
    }

    function modeParts(mode: string): var {
        const match = String(mode).match(/^(\d+)x(\d+)@(\d+(?:\.\d+)?)$/);
        if (!match)
            return null;
        return {
            width: Number(match[1]),
            height: Number(match[2]),
            refresh: Number(match[3])
        };
    }

    function isScaleClean(mode: string, scale: real): bool {
        const parts = root.modeParts(mode);
        if (!parts || root.scales.indexOf(scale) < 0 || !Number.isFinite(scale) || scale <= 0)
            return false;
        const logicalWidth = parts.width / scale;
        const logicalHeight = parts.height / scale;
        return Math.abs(logicalWidth - Math.round(logicalWidth)) < 0.0001
            && Math.abs(logicalHeight - Math.round(logicalHeight)) < 0.0001;
    }

    function scalesForMode(mode: string): var {
        return root.scales.filter(scale => root.isScaleClean(mode, scale));
    }

    function nearestCleanScale(mode: string, preferred: real): real {
        const choices = root.scalesForMode(mode);
        if (choices.length === 0)
            return 1.0;
        return choices.reduce((best, candidate) =>
            Math.abs(candidate - preferred) < Math.abs(best - preferred) ? candidate : best,
            choices[0]);
    }

    function matchesRequest(monitor: var, requested: var): bool {
        if (!monitor || !requested || monitor.name !== requested.output)
            return false;
        const parts = root.modeParts(requested.mode);
        return !!parts
            && monitor.width === parts.width
            && monitor.height === parts.height
            && Math.abs(monitor.refreshRate - parts.refresh) < 0.01
            && Math.abs(monitor.scale - requested.scale) < 0.001
            && monitor.transform === requested.transform;
    }

    function modeIsCurrent(monitor: var, candidate: var): bool {
        return !!monitor && !!candidate
            && monitor.width === candidate.width
            && monitor.height === candidate.height
            && Math.abs(monitor.refreshRate - candidate.refresh) < 0.01;
    }

    // Applies immediately and starts the countdown. Nothing is stored yet: the
    // settings file is only written by confirm().
    function apply(output: string, mode: string, scale: real, transform: int): bool {
        if (root.externalChangeBlocked) {
            root.lastError = "Wait for Settings to finish restoring before changing a display.";
            return false;
        }
        if (root.busy) {
            root.lastError = "Wait for the current display operation to finish.";
            return false;
        }
        if (root.awaitingConfirmation) {
            root.lastError = "Finish the current display change first.";
            return false;
        }
        const monitor = root.monitorNamed(output);
        if (!monitor) {
            root.lastError = "That display is not connected.";
            return false;
        }
        if (!monitor.modes.some(candidate => candidate.mode === mode)) {
            root.lastError = "That display does not offer that mode.";
            return false;
        }
        if (!root.isScaleClean(mode, scale)) {
            root.lastError = "That scale does not divide this resolution cleanly.";
            return false;
        }
        if (!root.transforms.some(candidate => candidate.value === transform)) {
            root.lastError = "That rotation is not one Panama offers.";
            return false;
        }

        root.pendingPrevious = {
            output: output,
            mode: monitor.mode,
            scale: monitor.scale,
            transform: monitor.transform
        };
        root.operationGeneration++;
        root.pendingRequested = {
            output: output,
            mode: mode,
            scale: scale,
            transform: transform
        };
        root.pendingOutput = output;
        root.pendingVerified = false;
        root.revertQueued = false;
        root.secondsLeft = root.confirmSeconds;
        root.lastError = "";
        countdown.restart();

        root.push(output, mode, scale, transform);
        return true;
    }

    function push(output: string, mode: string, scale: real, transform: int): void {
        // Values are validated above and the output name comes from the
        // compositor's own list, so nothing user-authored reaches the payload.
        applyRun.exec(["hyprctl", "eval",
            `hl.monitor({ output = "${output}", mode = "${mode}", scale = ${scale}, transform = ${transform} })`]);
    }

    function confirm(): bool {
        if (!root.canConfirm || !root.matchesRequest(
                root.monitorNamed(root.pendingOutput), root.pendingRequested)) {
            if (root.awaitingConfirmation)
                root.lastError = "Wait for the display to finish applying before keeping it.";
            return false;
        }

        const stored = DesktopPreferences.get("displays");
        const next = Object.assign({}, (stored && typeof stored === "object") ? stored : {});
        next[root.pendingOutput] = {
            mode: root.pendingRequested.mode,
            scale: root.pendingRequested.scale,
            transform: root.pendingRequested.transform
        };
        if (!DesktopPreferences.set("displays", next)) {
            root.lastError = "That display setting could not be saved. Revert it and try again.";
            return false;
        }

        root.clearPending();
        root.lastError = "";
        return true;
    }

    function clearPending(): void {
        countdown.stop();
        verifyTimer.stop();
        root.pendingOutput = "";
        root.pendingPrevious = null;
        root.pendingRequested = null;
        root.pendingVerified = false;
        root.revertQueued = false;
        root.secondsLeft = 0;
    }

    function revert(): void {
        root.revertWithMessage("");
    }

    function revertWithMessage(message: string): void {
        if (!root.awaitingConfirmation)
            return;
        countdown.stop();
        verifyTimer.stop();
        root.pendingVerified = false;
        root.revertReason = message;
        if (message !== "")
            root.lastError = message;
        if (applyRun.running || query.running) {
            root.revertQueued = true;
            return;
        }
        root.performRevert();
    }

    function performRevert(): void {
        const previous = root.pendingPrevious;
        root.operationGeneration++;
        root.revertGeneration = root.operationGeneration;
        root.revertExpected = previous;
        root.revertVerificationActive = false;
        root.clearPending();
        if (previous) {
            revertRun.exec(["hyprctl", "eval",
                `hl.monitor({ output = "${previous.output}", mode = "${previous.mode}", scale = ${previous.scale}, transform = ${previous.transform} })`]);
        }
    }

    // Clears any stored override for an output so it returns to the value
    // shipped in hypr/monitors.lua on the next start.
    function forget(output: string): void {
        const stored = DesktopPreferences.get("displays");
        if (!stored || typeof stored !== "object" || stored[output] === undefined)
            return;
        const next = Object.assign({}, stored);
        delete next[output];
        DesktopPreferences.set("displays", next);
    }

    function isOverridden(output: string): bool {
        const stored = DesktopPreferences.get("displays");
        return !!(stored && typeof stored === "object" && stored[output] !== undefined);
    }

    Timer {
        id: verifyTimer
        property int attempts: 0
        property int ticks: 0
        interval: 120
        repeat: true
        onTriggered: {
            ticks++;
            if (ticks > 50) {
                root.revertWithMessage("The display did not apply that setting, so Panama restored the previous one.");
                return;
            }
            if (root.refresh())
                attempts++;
        }
    }

    Timer {
        id: revertVerifyTimer
        property int attempts: 0
        property int ticks: 0
        interval: 120
        repeat: true
        onTriggered: {
            ticks++;
            if (ticks > 50) {
                stop();
                root.revertVerificationActive = false;
                root.revertGeneration = -1;
                root.revertExpected = null;
                root.revertReason = "";
                root.lastError = "The previous display setting could not be verified. Open Displays and restore it manually.";
                return;
            }
            if (root.refresh())
                attempts++;
        }
    }

    Timer {
        id: countdown
        interval: 1000
        repeat: true
        onTriggered: {
            root.secondsLeft -= 1;
            if (root.secondsLeft <= 0)
                root.revert();
        }
    }
}
