pragma Singleton

// Printers, through CUPS' own API.
//
// Driverless only: this adds printers that describe their own capabilities over
// IPP. Choosing PPDs and downloading vendor drivers is most of what the panel
// this replaces does, and getting it wrong yields a printer that accepts jobs
// and silently prints nothing -- so the page says a printer needs a driver
// rather than guessing one.
//
// Discovery is separate from the snapshot because it takes seconds of network
// waiting, and a settings page should open immediately.

import Quickshell
import Quickshell.Io
import QtQuick

Singleton {
    id: root

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

    property var printers: []
    property var jobs: []
    property var service: ({})
    property var discovered: []
    property bool scanned: false
    property bool searching: false
    property bool searched: false
    property string lastError: ""

    // Guards read the Process objects directly. A derived binding is stale
    // inside the handler that changes it, which silently drops the refresh
    // after a successful write. See DefaultApps.qml.
    readonly property bool busy: query.running || mutation.running

    readonly property var defaultPrinter: {
        for (const printer of root.printers) {
            if (printer.isDefault)
                return printer;
        }
        return null;
    }

    readonly property bool anyPrinters: root.printers.length > 0

    // Printers found on the network that are not already set up here.
    readonly property var addable: root.discovered.filter(entry => {
        for (const printer of root.printers) {
            if (String(printer.uri ?? "") === String(entry.uri ?? ""))
                return false;
        }
        return true;
    })

    function stateSummary(printer: var): string {
        const message = String(printer?.stateMessage ?? "").trim();
        const state = String(printer?.state ?? "");
        if (state === "stopped")
            return message !== "" ? "Paused — " + message : "Paused";
        if (state === "printing")
            return message !== "" ? "Printing — " + message : "Printing";
        if (printer?.accepting === false)
            return "Not accepting jobs";
        return message !== "" ? message : "Ready";
    }

    function jobsFor(printerName: string): int {
        let count = 0;
        for (const job of root.jobs) {
            if (job.printer === printerName)
                count += 1;
        }
        return count;
    }

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

    function search(): void {
        if (root.searching)
            return;
        root.searching = true;
        discovery.command = [root.helperPath, "discover"];
        discovery.running = true;
    }

    function absorb(text: string): void {
        try {
            const parsed = JSON.parse(text);
            root.printers = Array.isArray(parsed.printers) ? parsed.printers : [];
            root.jobs = Array.isArray(parsed.jobs) ? parsed.jobs : [];
            root.service = parsed.service ?? ({});
            root.lastError = String(parsed.error ?? "");
        } catch (error) {
            root.lastError = "Could not read the printing service's answer.";
            console.warn("Printers: 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 add(uri: string, name: string): void { root.run(["add", uri, name]); }
    function remove(name: string): void { root.run(["remove", name]); }
    function setDefault(name: string): void { root.run(["set-default", name]); }
    function pause(name: string): void { root.run(["pause", name]); }
    function resume(name: string): void { root.run(["resume", name]); }
    function cancel(jobId: int): void { root.run(["cancel", String(jobId)]); }
    function testPage(name: string): void { root.run(["test-page", name]); }

    // A queue name CUPS will accept, derived from what the printer calls itself.
    function suggestedName(label: string): string {
        const cleaned = String(label).replace(/[^A-Za-z0-9_.-]+/g, "_").replace(/^_+|_+$/g, "");
        return cleaned === "" ? "printer" : cleaned.slice(0, 60);
    }

    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
        // Answers with the fresh state, so the page updates from the change
        // itself rather than asking again afterwards.
        stdout: StdioCollector { onStreamFinished: root.absorb(this.text) }
        stderr: StdioCollector {
            onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim()
        }
    }

    Process {
        id: discovery
        stdout: StdioCollector {
            onStreamFinished: {
                try {
                    const parsed = JSON.parse(this.text);
                    root.discovered = Array.isArray(parsed.found) ? parsed.found : [];
                    if (String(parsed.error ?? "") !== "")
                        root.lastError = String(parsed.error);
                } catch (error) {
                    root.lastError = "Could not read the discovery result.";
                }
                root.searched = true;
            }
        }
        onExited: root.searching = false
    }
}
