pragma Singleton

// Freedesktop default handlers and session autostart entries.
//
// The helper owns parsing and atomic desktop-file writes. This singleton keeps
// the QML side typed and reactive, and every external command crosses Process
// as an argument array.

import Quickshell
import Quickshell.Io
import QtQuick

Singleton {
    id: root

    property var handlers: ({})
    property var autostartEntries: []
    property var luaAutostartEntries: []
    property string lastError: ""

    // For the UI, which wants one answer to "is anything happening".
    //
    // Guards inside this file do NOT use it. `busy` is a binding, and a binding
    // hands back its cached value until the change notification that feeds it
    // has been delivered. Inside a process's own onExited handler that has not
    // happened yet, so `busy` still reads true there -- which silently turned
    // the refresh after every successful write into a no-op. The write landed
    // and the settings page never noticed, which looks exactly like a settings
    // page that cannot change anything. Guards below read the Process objects
    // directly, where the value is current.
    readonly property bool busy: snapshotProcess.running || mutationProcess.running
    readonly property string helper: Quickshell.shellDir + "/scripts/panama-default-apps"
    // Derived from the snapshot, never restated. This was a hardcoded list of
    // seven, and when the helper and the page grew documents, text and archives
    // it stayed at seven -- so choosing a PDF viewer set lastError and did
    // nothing, which reads exactly like a settings page that does not work.
    //
    // The snapshot already reports one handler per role the helper supports, so
    // that IS the list. An empty one means no snapshot has landed yet; the
    // helper validates the role itself and reports a failure, so there is
    // nothing for this guard to add before then.
    readonly property var supportedRoles: Object.keys(root.handlers ?? ({}))

    Process {
        id: snapshotProcess

        stdout: StdioCollector {
            onStreamFinished: root.applySnapshot(this.text)
        }
        onExited: (exitCode, exitStatus) => {
            if (exitCode !== 0)
                root.lastError = "Default applications could not be read. Try refreshing."
        }
    }

    Process {
        id: mutationProcess

        onExited: (exitCode, exitStatus) => {
            if (exitCode !== 0) {
                root.lastError = "That application setting could not be changed."
                return;
            }
            root.refresh();
        }
    }

    function applySnapshot(text: string): void {
        try {
            const payload = JSON.parse(text);
            root.handlers = payload.handlers ?? ({});
            root.autostartEntries = payload.autostartEntries ?? [];
            root.luaAutostartEntries = payload.luaAutostartEntries ?? [];
            root.lastError = "";
        } catch (error) {
            root.lastError = "Default applications returned an unreadable response."
        }
    }

    function refresh(): void {
        // Only a snapshot already in flight is a reason not to start another.
        // A mutation finishing is the single best reason TO refresh.
        if (snapshotProcess.running)
            return;
        root.lastError = "";
        snapshotProcess.exec([root.helper, "snapshot"]);
    }

    function knownDesktopId(desktopId: string): bool {
        if (!/^[A-Za-z0-9][A-Za-z0-9._+-]*\.desktop$/.test(desktopId))
            return false;
        const entries = DesktopEntries.applications.values;
        return entries.some(entry => {
            const entryId = String(entry.id ?? "");
            return entryId === desktopId || entryId + ".desktop" === desktopId;
        });
    }

    function setDefault(role: string, desktopId: string): void {
        if (mutationProcess.running)
            return;
        const roleIsKnown = root.supportedRoles.length === 0
            || root.supportedRoles.includes(role);
        if (!roleIsKnown || !root.knownDesktopId(desktopId)) {
            root.lastError = "Choose an application from the available list."
            return;
        }
        root.lastError = "";
        mutationProcess.exec([root.helper, "set-default", role, desktopId]);
    }

    function setAutostart(desktopId: string, enabled: bool): void {
        if (mutationProcess.running)
            return;
        const known = root.autostartEntries.some(entry => entry.id === desktopId);
        if (!known) {
            root.lastError = "That user autostart entry is no longer available."
            return;
        }
        root.lastError = "";
        mutationProcess.exec([root.helper, "set-autostart", desktopId, String(enabled)]);
    }

    // Deletes the entry rather than hiding it. Disabling writes Hidden=true and
    // is reversible; this is not, so the page confirms before calling it.
    function removeAutostart(desktopId: string): void {
        if (mutationProcess.running)
            return;
        const known = root.autostartEntries.some(entry => entry.id === desktopId);
        if (!known) {
            root.lastError = "That user autostart entry is no longer available."
            return;
        }
        root.lastError = "";
        mutationProcess.exec([root.helper, "remove-autostart", desktopId]);
    }

    function addAutostart(desktopId: string): void {
        if (mutationProcess.running)
            return;
        if (!root.knownDesktopId(desktopId)) {
            root.lastError = "Choose an installed application.";
            return;
        }
        root.lastError = "";
        mutationProcess.exec([root.helper, "add-autostart", desktopId]);
    }

    Component.onCompleted: root.refresh()
}
