pragma Singleton

// Which applications may use the camera and microphone.
//
// Backed by xdg-desktop-portal's permission store, which records the answer an
// application got when it asked through the portal. That is the whole of what
// this controls, and the limit belongs on the page rather than in a comment: a
// native binary opens /dev/video0 directly and no desktop setting stands in its
// way. What this covers is Flatpaks and anything else going through the portal.

import Quickshell
import Quickshell.Io
import QtQuick

Singleton {
    id: root

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

    property bool available: false
    property var devices: []
    property bool scanned: false
    property string lastError: ""

    // Guards read the Processes directly rather than a derived binding, which
    // returns its cached value inside the handler that changes its dependency.
    readonly property bool busy: query.running || mutation.running

    // Devices something has actually asked for. A device nothing has asked for
    // is still reported, so the page can say so rather than omit it.
    readonly property var recorded: root.devices.filter(
        device => (device.applications ?? []).length > 0)

    readonly property int grantedCount: root.devices.reduce(
        (total, device) => total + (device.applications ?? []).filter(app => app.allowed).length, 0)

    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.available = parsed.available === true;
            root.devices = Array.isArray(parsed.devices) ? parsed.devices : [];
            root.lastError = String(parsed.error ?? "");
        } catch (error) {
            root.lastError = "Could not read the portal's permissions.";
            console.warn("Permissions: 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 setAllowed(device: string, app: string, allowed: bool): void {
        root.run(["set", device, app, allowed ? "allow" : "deny"]);
    }

    // Drops the recorded answer entirely, so the application is asked again the
    // next time it wants the device.
    function forget(device: string, app: string): void {
        root.run(["forget", device, app]);
    }

    Component.onCompleted: root.refresh()

    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()
        }
    }
}
