pragma Singleton

// The desktop background.
//
// hyprpaper owns the actual painting; this owns choosing. Two things are worth
// knowing about hyprpaper 0.8:
//
//   * Its IPC is much smaller than the documentation for older versions
//     suggests. `wallpaper <output>,<path>` and `listactive` work; `preload`,
//     `listloaded`, `unload`, and `reload` all answer "invalid hyprpaper
//     request". So there is no preload step -- setting is a single call.
//   * hyprpaper.conf lives in the Panama repo via the ~/.config/hypr symlink,
//     so it cannot be rewritten at runtime without dirtying a tracked file.
//     The chosen wallpaper therefore lives in the shared settings store like
//     every other preference, and is re-applied when the shell starts.
//
// The argument is "<output>,<path>", so a path containing a comma would be
// parsed as a different request. The schema's pattern rejects those, and the
// value is passed as a single argv element rather than through a shell.

import Quickshell
import Quickshell.Io
import QtQuick
import qs.config

Singleton {
    id: root

    // Absolute paths of candidate images, newest first.
    property var available: []
    property string active: ""
    property string lastError: ""
    property bool scanning: false

    readonly property string configured: DesktopPreferences.get("wallpaperPath")
    readonly property string shippedPath: `${Quickshell.env("HOME")}/Pictures/Wallpapers/faroe_islands.jpg`

    // Directories searched for wallpapers, in order. Screenshots are
    // deliberately excluded: a folder of 300 screenshots is not a wallpaper
    // picker, and including it made the grid useless on this machine.
    readonly property var searchRoots: [
        `${Quickshell.env("HOME")}/Pictures/Wallpapers`,
        `${Quickshell.env("HOME")}/Pictures/Backgrounds`,
        `${Quickshell.env("HOME")}/.local/share/backgrounds`,
        "/usr/share/backgrounds"
    ]

    Process {
        id: scan

        // -print0 would be safer against odd filenames, but the schema already
        // rejects paths containing commas or newlines, and this list is only
        // ever offered as candidates -- the value that gets stored is validated
        // again on the way in.
        command: ["bash", "-lc",
            "find " + root.searchRoots.map(dir => `'${dir}'`).join(" ")
            + " -maxdepth 2 -type f \\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \\)"
            + " -printf '%T@ %p\\n' 2>/dev/null | sort -rn | cut -d' ' -f2- | head -60"]

        stdout: StdioCollector {
            onStreamFinished: {
                const paths = this.text.split("\n").map(line => line.trim()).filter(line => line.length > 0);
                root.available = paths;
                root.scanning = false;
            }
        }
    }

    Process {
        id: activeQuery
        command: ["hyprctl", "hyprpaper", "listactive"]
        stdout: StdioCollector {
            onStreamFinished: {
                // "DP-2: /path/to/image.jpg", one line per output.
                const first = this.text.split("\n").find(line => line.indexOf(":") > 0);
                root.active = first ? first.slice(first.indexOf(":") + 1).trim() : "";
            }
        }
    }

    // hyprpaper requires an explicit output name: the "<empty>,<path>" form that
    // older versions accepted as "all outputs" is silently ignored by 0.8, so a
    // wallpaper set that way appears to succeed and never changes. Outputs are
    // therefore walked one at a time.
    Process {
        id: apply

        property string requested: ""
        property string storedValue: ""
        property var remaining: []

        onExited: (exitCode, exitStatus) => {
            if (exitCode !== 0) {
                root.lastError = "hyprpaper could not load that image.";
                apply.remaining = [];
                return;
            }
            if (apply.remaining.length > 0) {
                const next = apply.remaining[0];
                apply.remaining = apply.remaining.slice(1);
                apply.exec(["hyprctl", "hyprpaper", "wallpaper", `${next},${apply.requested}`]);
                return;
            }
            root.lastError = "";
            DesktopPreferences.set("wallpaperPath", apply.storedValue);
            root.refreshActive();
        }
    }

    Component.onCompleted: {
        root.rescan();
        root.refreshActive();
        restore.restart();
    }

    // hyprpaper is started by the compositor's autostart, so it may not be
    // listening yet when the shell comes up. Re-applying the stored choice
    // after a short delay makes the wallpaper survive a reboot without needing
    // hyprpaper.conf to know about it.
    Timer {
        id: restore
        interval: 1500
        onTriggered: {
            const stored = root.configured;
            if (stored !== "" && stored !== root.active)
                root.set(stored);
        }
    }

    function rescan(): void {
        if (scan.running)
            return;
        root.scanning = true;
        scan.running = true;
    }

    function refreshActive(): void {
        if (!activeQuery.running)
            activeQuery.running = true;
    }

    // Applies to every connected output. Returns false when the path is not one
    // the schema will accept, so a caller can report the refusal.
    function set(path: string): bool {
        const effectivePath = path === "" ? root.shippedPath : path;
        if (PreferenceSchema.coerce("wallpaperPath", effectivePath) === undefined) {
            root.lastError = "That file path cannot be used as a wallpaper.";
            return false;
        }
        if (apply.running)
            return false;

        apply.requested = effectivePath;
        apply.storedValue = path;

        const outputs = Quickshell.screens.map(screen => screen.name).filter(name => !!name);
        if (outputs.length === 0) {
            root.lastError = "No display to set a wallpaper on.";
            return false;
        }

        apply.remaining = outputs.slice(1);
        apply.exec(["hyprctl", "hyprpaper", "wallpaper", `${outputs[0]},${effectivePath}`]);
        return true;
    }

    // The display name for a path: the file's own name, without extension,
    // with separators turned into spaces.
    function titleFor(path: string): string {
        const file = String(path).split("/").pop();
        return file.replace(/\.[^.]+$/, "").replace(/[_-]+/g, " ");
    }
}
