pragma Singleton

// Local screen recognition. Capture owns geometry; this service owns the
// Tesseract/ZBar process boundary, result state, explicit follow-up actions,
// and cleanup of temporary screen images.

import Quickshell
import Quickshell.Io
import QtQuick

Singleton {
    id: root

    readonly property string helperPath: Quickshell.shellDir + "/scripts/screen-intelligence"

    // "idle" | "capturing" | "reading" | "ready" | "error"
    property string phase: "idle"
    // Do not map the result surface until grim has finished, or the overlay
    // can appear in the very pixels it is trying to analyze.
    readonly property bool visible: phase !== "idle" && phase !== "capturing"
    readonly property bool busy: phase === "capturing" || phase === "reading"

    property bool ocrReady: false
    property bool codeReady: false
    property bool englishReady: false
    property string text: ""
    property string codeType: ""
    property string codeValue: ""
    property string error: ""
    property string imagePath: ""
    property bool ownsImage: false

    readonly property string primaryUrl: root._firstWebUrl(root.codeValue) || root._firstWebUrl(root.text)

    function refresh(): void {
        probeProc.running = false;
        probeProc.running = true;
    }

    function analyzeRegion(geometry: string, outputName: string): void {
        root._prepare();
        root.imagePath = Quickshell.cachePath("screen-intelligence-" + Date.now() + ".png");
        root.ownsImage = true;
        root.phase = "capturing";

        const command = ["grim"];
        if (geometry !== "")
            command.push("-g", geometry);
        else if (outputName !== "")
            command.push("-o", outputName);
        command.push(root.imagePath);

        captureProc.command = command;
        captureProc.running = false;
        captureProc.running = true;
    }

    function analyzeFile(path: string): void {
        root._prepare();
        root.imagePath = path;
        root.ownsImage = false;
        root._startRecognition();
    }

    function copyText(): void {
        if (root.text !== "")
            Quickshell.execDetached(["wl-copy", root.text]);
    }

    function copyCode(): void {
        if (root.codeValue !== "")
            Quickshell.execDetached(["wl-copy", root.codeValue]);
    }

    function search(): void {
        if (root.text === "")
            return;
        Quickshell.execDetached(["xdg-open", "https://www.google.com/search?q=" + encodeURIComponent(root.text.slice(0, 4000))]);
    }

    function translate(): void {
        if (root.text === "")
            return;
        Quickshell.execDetached(["xdg-open", "https://translate.google.com/?sl=auto&tl=en&text=" + encodeURIComponent(root.text.slice(0, 4000))]);
    }

    function openDetected(): void {
        if (root.primaryUrl !== "")
            Quickshell.execDetached(["xdg-open", root.primaryUrl]);
    }

    function close(): void {
        captureProc.running = false;
        recognizeProc.running = false;
        root._dropOwnedImage();
        root.phase = "idle";
        root.text = "";
        root.codeType = "";
        root.codeValue = "";
        root.error = "";
        root.imagePath = "";
        root.ownsImage = false;
    }

    function _prepare(): void {
        captureProc.running = false;
        recognizeProc.running = false;
        root._dropOwnedImage();
        root.text = "";
        root.codeType = "";
        root.codeValue = "";
        root.error = "";
        root.imagePath = "";
        root.ownsImage = false;
    }

    function _startRecognition(): void {
        root.phase = "reading";
        recognizeProc.sawOutput = false;
        recognizeProc.command = [root.helperPath, "analyze-file", root.imagePath];
        recognizeProc.running = false;
        recognizeProc.running = true;
    }

    function _consume(raw: string): void {
        try {
            const result = JSON.parse(raw);
            if (!result.ok) {
                root.error = result.error || "Panama could not read that capture.";
                root.phase = "error";
                return;
            }
            root.text = (result.text || "").trim();
            root.codeType = result.codeType || "";
            root.codeValue = result.codeValue || "";
            root.error = "";
            root.phase = "ready";
        } catch (e) {
            root.error = "The local recognition engine returned an unreadable result.";
            root.phase = "error";
        }
    }

    function _dropOwnedImage(): void {
        if (root.ownsImage && root.imagePath !== "")
            Quickshell.execDetached(["rm", "-f", root.imagePath]);
    }

    function _firstWebUrl(value: string): string {
        if (!value)
            return "";
        const match = value.match(/https?:\/\/[^\s<>'\"]+/i);
        if (!match)
            return "";
        return match[0].replace(/[),.;!?]+$/, "");
    }

    Process {
        id: probeProc
        command: [root.helperPath, "probe"]
        stdout: StdioCollector {
            onStreamFinished: {
                try {
                    const result = JSON.parse(this.text);
                    root.ocrReady = result.tesseract === true;
                    root.codeReady = result.zbar === true;
                    root.englishReady = result.english === true;
                } catch (e) {
                    root.ocrReady = false;
                    root.codeReady = false;
                    root.englishReady = false;
                }
            }
        }
    }

    Process {
        id: captureProc
        onExited: (code, status) => {
            if (root.phase !== "capturing")
                return;
            if (code !== 0) {
                root.error = "Panama could not capture the selected area.";
                root.phase = "error";
                return;
            }
            root._startRecognition();
        }
    }

    Process {
        id: recognizeProc
        property bool sawOutput: false
        stdout: StdioCollector {
            onStreamFinished: {
                recognizeProc.sawOutput = this.text !== "";
                root._consume(this.text);
            }
        }
        onExited: (code, status) => {
            if (!recognizeProc.sawOutput && root.phase === "reading") {
                root.error = "The local recognition engine did not return a result.";
                root.phase = "error";
            }
        }
    }

    Component.onCompleted: root.refresh()
}
