pragma Singleton

// Persistent privacy state is separate from transient Signal Glass events.
// The low-frequency PipeWire probe only assigns when state changes, so the bar
// remains completely idle when nothing is capturing.

import Quickshell
import Quickshell.Io
import QtQuick

Singleton {
    id: root

    property bool microphoneActive: false
    property bool cameraActive: false
    property bool screenSharingActive: false
    readonly property bool recordingActive: Capture.recording
    property string microphoneApp: ""
    property string cameraApp: ""
    property string screenSharingApp: ""

    property bool initialized: false
    property bool fixtureMode: false
    property bool warned: false

    readonly property bool anyActive: root.microphoneActive || root.cameraActive
        || root.screenSharingActive || root.recordingActive
    readonly property var activeKinds: {
        const kinds = [];
        if (root.recordingActive)
            kinds.push("Recording");
        if (root.screenSharingActive)
            kinds.push("Screen sharing");
        if (root.cameraActive)
            kinds.push("Camera");
        if (root.microphoneActive)
            kinds.push("Microphone");
        return kinds;
    }

    function applyStates(microphone: bool, camera: bool, screen: bool,
                         micApp: string, cameraApp: string, screenApp: string,
                         announce: bool): void {
        root.updateKind("microphone", microphone, micApp, announce);
        root.updateKind("camera", camera, cameraApp, announce);
        root.updateKind("screen", screen, screenApp, announce);
        root.initialized = true;
    }

    function updateKind(kind: string, active: bool, app: string, announce: bool): void {
        let previous = false;
        let label = "";
        if (kind === "microphone") {
            previous = root.microphoneActive;
            root.microphoneActive = active;
            root.microphoneApp = app;
            label = "Microphone";
        } else if (kind === "camera") {
            previous = root.cameraActive;
            root.cameraActive = active;
            root.cameraApp = app;
            label = "Camera";
        } else {
            previous = root.screenSharingActive;
            root.screenSharingActive = active;
            root.screenSharingApp = app;
            label = "Screen sharing";
        }

        if (!announce || previous === active)
            return;

        StatusEvents.publish({
            key: "privacy-" + kind,
            glyph: kind === "microphone" ? "\u{F036C}" : (kind === "camera" ? "\u{F0100}" : "\u{F0379}"),
            title: active ? label + " in use" : label + " released",
            detail: active ? (app || "An application is using " + label.toLowerCase()) : "No application is using " + label.toLowerCase(),
            tone: active ? "danger" : "ok",
            priority: StatusEvents.criticalPriority,
            durationMs: active ? 3600 : 2400,
            actionId: active ? "open-activity" : ""
        });
    }

    function applyProbe(text: string): void {
        if (root.fixtureMode)
            return;

        let nodes = [];
        try {
            nodes = JSON.parse(text);
        } catch (error) {
            if (!root.warned) {
                console.warn("PrivacyState: could not parse PipeWire state");
                root.warned = true;
            }
            return;
        }

        let microphone = false;
        let camera = false;
        let screen = false;
        let micApp = "";
        let cameraApp = "";
        let screenApp = "";

        for (const object of nodes) {
            if (object?.type !== "PipeWire:Interface:Node" || object?.info?.state !== "running")
                continue;
            const props = object.info.props ?? {};
            const mediaClass = String(props["media.class"] ?? "");
            const nodeName = String(props["node.name"] ?? "").toLowerCase();
            const role = String(props["media.role"] ?? "").toLowerCase();
            const app = String(props["application.name"] ?? props["node.description"] ?? "");

            const looksLikeScreen = role.includes("screen") || nodeName.includes("screencast")
                || nodeName.includes("screen-cast") || nodeName.includes("xdg-desktop-portal");
            if (mediaClass === "Stream/Input/Audio") {
                microphone = true;
                micApp = micApp || app;
            } else if (mediaClass === "Stream/Input/Video" && !looksLikeScreen) {
                camera = true;
                cameraApp = cameraApp || app;
            } else if ((mediaClass === "Stream/Input/Video" || mediaClass === "Stream/Output/Video") && looksLikeScreen) {
                screen = true;
                screenApp = screenApp || app;
            }
        }

        root.applyStates(microphone, camera, screen, micApp, cameraApp, screenApp, root.initialized);
    }

    function applyFixture(name: string): void {
        root.fixtureMode = true;
        if (name === "microphone-on")
            root.applyStates(true, false, false, "Contract fixture", "", "", true);
        else if (name === "camera-on")
            root.applyStates(false, true, false, "", "Contract fixture", "", true);
        else if (name === "screen-on")
            root.applyStates(false, false, true, "", "", "Contract fixture", true);
        else if (name === "all-on")
            root.applyStates(true, true, true, "Contract fixture", "Contract fixture", "Contract fixture", true);
        else if (name === "all-off")
            root.applyStates(false, false, false, "", "", "", true);
    }

    function clearFixture(): void {
        root.fixtureMode = false;
        // The next real probe is discovery, not a transition away from test
        // data; otherwise contract cleanup would flash a bogus release event.
        root.initialized = false;
        probe.running = false;
        probe.running = true;
    }

    Timer {
        interval: 2500
        repeat: true
        running: true
        triggeredOnStart: true
        onTriggered: {
            if (!root.fixtureMode && !probe.running)
                probe.running = true;
        }
    }

    Process {
        id: probe
        command: ["pw-dump"]
        stdout: StdioCollector {
            onStreamFinished: root.applyProbe(this.text)
        }
        onExited: (code, status) => {
            if (code !== 0 && !root.warned) {
                console.warn("PrivacyState: pw-dump exited with", code);
                root.warned = true;
            }
        }
    }
}
