pragma Singleton

// Storage: what is in this machine, how full it is, and what is filling it.
//
// Two reads with very different costs, kept apart on purpose:
//
//   refresh()  layout, usage, and drive health. Around 90ms -- lsblk plus one
//              udisks call -- so the page opens with it and re-reads freely.
//   scan()     what is using the space. Measuring a folder means walking it,
//              and a Steam library alone can be a terabyte, so this happens
//              only when asked and the answer is kept until asked again.
//
// Not a stored preference: every value here is the machine's, not the user's.
//
// Deliberately absent: partitioning and formatting. Those stay in GNOME Disks,
// which the page can launch.

import Quickshell
import Quickshell.Io
import QtQuick

Singleton {
    id: root

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

    property var drives: []
    property var filesystems: []
    property var swap: []
    property bool scanned: false
    property string lastError: ""

    // The expensive half.
    property var folders: []
    property var containers: null
    property bool scanning: false
    property bool folderScanTruncated: false
    // Empty until a scan has completed, which is what the page shows a prompt
    // for rather than an empty list -- "nothing here" and "not measured yet"
    // are different answers.
    property bool foldersMeasured: false

    readonly property var primaryDrive: root.drives.length > 0 ? root.drives[0] : null

    // The filesystem the user means when they ask how full the machine is.
    readonly property var rootFilesystem: {
        for (const filesystem of root.filesystems) {
            if ((filesystem.mountpoints ?? []).includes("/"))
                return filesystem;
        }
        return root.filesystems.length > 0 ? root.filesystems[0] : null;
    }

    // Bytes, in the units people actually read. Binary units with decimal-style
    // names would be a lie in the other direction; this matches what df -h and
    // the drive's own packaging say.
    function formatBytes(bytes: real): string {
        if (!(bytes > 0))
            return "0 B";
        const units = ["B", "KB", "MB", "GB", "TB"];
        let value = bytes;
        let index = 0;
        while (value >= 1000 && index < units.length - 1) {
            value /= 1000;
            index += 1;
        }
        const decimals = value < 10 && index > 1 ? 1 : 0;
        return value.toFixed(decimals) + " " + units[index];
    }

    function usedFraction(filesystem: var): real {
        const size = Number(filesystem?.sizeBytes ?? 0);
        if (!(size > 0))
            return 0;
        return Math.max(0, Math.min(1, Number(filesystem.usedBytes ?? 0) / size));
    }

    // "/ and /home" rather than two rows: btrfs subvolumes share one pool of
    // free space, and showing them separately doubles it on screen.
    function mountLabel(filesystem: var): string {
        const points = filesystem?.mountpoints ?? [];
        if (points.length === 0)
            return String(filesystem?.device ?? "");
        if (points.length === 1)
            return points[0];
        return points.slice(0, -1).join(", ") + " and " + points[points.length - 1];
    }

    function healthSummary(drive: var): string {
        if (!drive)
            return "";
        if (drive.healthy === false)
            return (drive.warnings ?? []).length > 0
                ? "Reporting " + drive.warnings.join(", ")
                : "Reporting a failure";
        if (drive.healthy === true)
            return "No warnings";
        return "Health not reported";
    }

    function refresh(): void {
        if (!query.running)
            query.running = true;
    }

    function scan(): void {
        if (root.scanning)
            return;
        root.scanning = true;
        folderScan.running = true;
    }

    function unmount(devicePath: string): void {
        root.runMedia(["unmount", devicePath]);
    }

    function eject(devicePath: string): void {
        root.runMedia(["eject", devicePath]);
    }

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

    Process {
        id: query
        command: [root.helperPath, "snapshot"]
        stdout: StdioCollector {
            onStreamFinished: {
                try {
                    const parsed = JSON.parse(this.text);
                    root.drives = Array.isArray(parsed.drives) ? parsed.drives : [];
                    root.filesystems = Array.isArray(parsed.filesystems) ? parsed.filesystems : [];
                    root.swap = Array.isArray(parsed.swap) ? parsed.swap : [];
                    root.lastError = "";
                } catch (error) {
                    root.drives = [];
                    root.filesystems = [];
                    root.lastError = "Could not read the storage helper's output.";
                    console.warn("Disks: could not parse helper output:", error);
                }
                root.scanned = true;
            }
        }
        stderr: StdioCollector {
            onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim()
        }
    }

    Process {
        id: folderScan
        command: [root.helperPath, "scan"]
        stdout: StdioCollector {
            onStreamFinished: {
                try {
                    const parsed = JSON.parse(this.text);
                    root.folders = Array.isArray(parsed.folders) ? parsed.folders : [];
                    root.containers = parsed.containers ?? null;
                    root.folderScanTruncated = parsed.truncated === true;
                    root.foldersMeasured = true;
                } catch (error) {
                    root.lastError = "Could not measure what is using the drive.";
                    console.warn("Disks: could not parse scan output:", error);
                }
            }
        }
        onExited: root.scanning = false
    }

    Process {
        id: media
        stderr: StdioCollector {
            onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim()
        }
        // Re-read rather than assuming: a device may refuse to unmount because
        // something still has a file open on it.
        onExited: root.refresh()
    }
}
