pragma Singleton

// Gaming: what the machine is doing, and what it should do while you play.
//
// The reporting half is cheap -- GPU sensors come from sysfs, gamemode from its
// own daemon -- so this can poll while its page is open. It only polls then:
// a settings page nobody is looking at has no business waking the CPU twice a
// second.
//
// The acting half is not in this file at all. gamemode runs a script when a
// game starts and another when it exits, and that script reads the preferences
// directly, because the shell may have been restarted since the game launched.

import Quickshell
import Quickshell.Io
import QtQuick

Singleton {
    id: root

    readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-gaming"

    property var gameMode: ({})
    property var gpus: []
    property var overlay: ({})
    property var library: ({})
    property bool scanned: false
    property string lastError: ""

    // Set by the page while it is visible. Nothing polls otherwise.
    property bool watching: false

    readonly property bool busy: query.running || mutation.running

    readonly property bool active: root.gameMode?.active === true
    readonly property var primaryGpu: {
        for (const gpu of root.gpus) {
            if (gpu.discrete)
                return gpu;
        }
        return root.gpus.length > 0 ? root.gpus[0] : null;
    }

    // The honest version: gamemode's headline trick is switching the governor,
    // and it does nothing if the machine already runs that governor.
    readonly property bool governorAlreadyThere:
        String(root.gameMode?.governorNow ?? "") !== ""
        && root.gameMode?.governorNow === root.gameMode?.governorWhileGaming

    function formatBytes(bytes: real): string {
        if (!(bytes > 0))
            return "0 GB";
        return (bytes / 1073741824).toFixed(bytes < 10737418240 ? 1 : 0) + " GB";
    }

    function gpuSummary(gpu: var): string {
        if (!gpu)
            return "";
        const parts = [];
        if (gpu.temperatureC !== null && gpu.temperatureC !== undefined)
            parts.push(gpu.temperatureC + " °C");
        if (gpu.watts !== null && gpu.watts !== undefined && gpu.watts > 0)
            parts.push(gpu.watts + " W");
        if (Number(gpu.vramTotalBytes ?? 0) > 0)
            parts.push(root.formatBytes(gpu.vramUsedBytes) + " / "
                       + root.formatBytes(gpu.vramTotalBytes));
        return parts.join(" · ");
    }

    function refresh(): void {
        if (query.running)
            return;
        query.command = [root.helperPath, "snapshot"];
        query.running = true;
    }

    function absorb(text: string): void {
        try {
            const parsed = JSON.parse(text);
            root.gameMode = parsed.gameMode ?? ({});
            root.gpus = Array.isArray(parsed.gpus) ? parsed.gpus : [];
            root.overlay = parsed.overlay ?? ({});
            root.library = parsed.library ?? ({});
            root.lastError = String(parsed.error ?? "");
        } catch (error) {
            root.lastError = "Could not read the gaming helper's answer.";
            console.warn("Gaming: could not parse helper output:", error);
        }
        root.scanned = true;
    }

    function run(arguments: var): void {
        if (mutation.running)
            return;
        root.lastError = "";
        mutation.command = [root.helperPath].concat(arguments);
        mutation.running = true;
    }

    function setOverlay(enabled: bool): void { root.run(["set-overlay", enabled ? "true" : "false"]); }
    function setOverlayPreset(preset: string): void { root.run(["set-overlay-preset", preset]); }
    function installHooks(): void { root.run(["install-hooks"]); }
    function removeHooks(): void { root.run(["remove-hooks"]); }

    Process {
        id: query
        stdout: StdioCollector { onStreamFinished: root.absorb(this.text) }
        stderr: StdioCollector {
            onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim()
        }
    }

    Process {
        id: mutation
        stdout: StdioCollector { onStreamFinished: root.absorb(this.text) }
        stderr: StdioCollector {
            onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim()
        }
    }

    // Three seconds: fast enough that a temperature reading feels live, slow
    // enough that it is not a background task of its own.
    Timer {
        running: root.watching
        interval: 3000
        repeat: true
        onTriggered: root.refresh()
    }
}
