pragma Singleton

// Verified wallpaper policy application. hyprpaper 0.8 applies one output per
// IPC call, so Panama queues every connected output and persists a policy only
// after listactive confirms the complete map.

import Quickshell
import Quickshell.Io
import QtQuick

import "WallpaperPolicy.js" as WallpaperPolicy
import qs.config

Singleton {
    id: root

    property var available: []
    property var activeByOutput: ({})
    property string lastError: ""
    property bool scanning: false
    property var transaction: null
    property bool startupRestoreEnabled: true
    property var outputOverride: null
    property var candidateOverride: null
    property string slideshowPath: ""
    property int slideshowIndex: -1
    property var shuffleBag: []
    property int slideshowIntervalOverrideMs: 0
    property bool pendingHotplugReapply: false

    readonly property string configured: DesktopPreferences.get("wallpaperPath")
    readonly property string shippedPath: `${Quickshell.env("HOME")}/Pictures/Wallpapers/faroe_islands.jpg`
    readonly property string active: {
        const outputs = root.outputNames();
        if (outputs.length > 0 && root.activeByOutput[outputs[0]])
            return root.activeByOutput[outputs[0]];
        const paths = Object.values(root.activeByOutput);
        return paths.length > 0 ? paths[0] : "";
    }
    readonly property bool busy: root.transaction !== null
        || applyProcess.running || verifyProcess.running
    readonly property var slideshowCollection: WallpaperPolicy.validCollection(
        DesktopPreferences.get("wallpaperSlideshowPaths") ?? [], root.candidates([]))
    readonly property bool slideshowTimerRunning: slideshowTimer.running
    readonly property string outputSignature: root.outputNames().slice().sort().join("|")

    property var outputNames: function() {
        if (Array.isArray(root.outputOverride))
            return root.outputOverride.slice();
        return Quickshell.screens.map(screen => screen.name).filter(name => !!name);
    }

    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
        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: {
                root.available = this.text.split("\n")
                    .map(line => line.trim()).filter(line => line.length > 0);
                root.scanning = false;
            }
        }
    }

    Process {
        id: activeQuery
        command: ["hyprctl", "hyprpaper", "listactive"]
        stdout: StdioCollector {
            onStreamFinished: {
                const parsed = root.parseActive(this.text);
                if (parsed !== null)
                    root.activeByOutput = parsed;
            }
        }
    }

    Process {
        id: applyProcess
        onExited: (exitCode, exitStatus) => {
            if (root.transaction === null)
                return;
            if (exitCode !== 0) {
                root.lastError = "Hyprpaper did not apply that background.";
                root.transaction = null;
                root.schedulePendingHotplug();
                return;
            }
            root.drainTransaction();
        }
    }

    Process {
        id: verifyProcess
        property string outputText: ""
        onStarted: outputText = ""
        stdout: StdioCollector {
            onStreamFinished: verifyProcess.outputText = this.text
        }
        onExited: (exitCode, exitStatus) => root.finishVerification(exitCode)
    }

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

    Timer {
        id: restore
        interval: 1500
        onTriggered: {
            if (root.startupRestoreEnabled)
                root.applyCurrentPolicy(false);
        }
    }

    Timer {
        id: slideshowTimer
        interval: root.slideshowIntervalOverrideMs > 0
            ? root.slideshowIntervalOverrideMs
            : DesktopPreferences.get("wallpaperIntervalMinutes") * 60000
        repeat: true
        running: DesktopPreferences.get("wallpaperMode") === "slideshow"
            && root.slideshowCollection.length >= 2 && !root.busy
        onTriggered: root.advanceSlideshow()
    }

    Timer {
        id: outputSettle
        interval: 350
        onTriggered: {
            if (root.busy) {
                root.pendingHotplugReapply = true;
                return;
            }
            root.applyCurrentPolicy(false);
        }
    }

    onOutputSignatureChanged: {
        if (Object.keys(root.activeByOutput).length > 0)
            outputSettle.restart();
    }

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

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

    function candidates(extra: var): var {
        const result = [];
        const discovered = Array.isArray(root.candidateOverride)
            ? root.candidateOverride : root.available;
        for (const path of discovered.concat([root.shippedPath, root.configured]).concat(extra || [])) {
            if (typeof path === "string" && /^\/[^,\n]+$/.test(path) && !result.includes(path))
                result.push(path);
        }
        return result;
    }

    function currentPolicy(): var {
        return {
            mode: DesktopPreferences.get("wallpaperMode") ?? "single",
            globalPath: root.configured === "" ? root.shippedPath : root.configured,
            collection: DesktopPreferences.get("wallpaperSlideshowPaths") ?? [],
            intervalMinutes: DesktopPreferences.get("wallpaperIntervalMinutes") ?? 30,
            shuffle: DesktopPreferences.get("wallpaperShuffle") !== false,
            assignments: DesktopPreferences.get("wallpaperPerMonitor") ?? ({}),
            slideshowPath: root.slideshowPath !== "" ? root.slideshowPath : root.active
        };
    }

    function normalisePolicy(policy: var): var {
        if (!policy || typeof policy !== "object")
            return null;
        const mode = ["single", "slideshow", "per-monitor"].includes(policy.mode)
            ? policy.mode : "single";
        const rawGlobal = policy.globalPath === "" ? root.shippedPath : policy.globalPath;
        const candidatePaths = root.candidates([rawGlobal]
            .concat(policy.collection || [])
            .concat(Object.values(policy.assignments || {}))
            .concat([policy.slideshowPath || ""]));
        if (!WallpaperPolicy.validPath(rawGlobal, candidatePaths))
            return null;
        return {
            mode,
            globalPath: rawGlobal,
            storedPath: rawGlobal === root.shippedPath ? "" : rawGlobal,
            collection: WallpaperPolicy.validCollection(policy.collection || [], candidatePaths),
            intervalMinutes: Math.max(5, Math.min(1440, Number(policy.intervalMinutes) || 30)),
            shuffle: policy.shuffle !== false,
            assignments: WallpaperPolicy.validAssignments(policy.assignments || {}, candidatePaths),
            slideshowPath: WallpaperPolicy.validPath(policy.slideshowPath, candidatePaths)
                ? policy.slideshowPath : rawGlobal,
            nextShuffleBag: Array.isArray(policy.nextShuffleBag)
                ? policy.nextShuffleBag.slice() : root.shuffleBag.slice(),
            nextSlideshowIndex: Number.isInteger(policy.nextSlideshowIndex)
                ? policy.nextSlideshowIndex : root.slideshowIndex,
            candidates: candidatePaths
        };
    }

    function applyPolicy(policy: var, persist: bool, automatic: bool): bool {
        if (root.busy)
            return false;
        const normalised = root.normalisePolicy(policy);
        if (normalised === null) {
            root.lastError = "That wallpaper policy is not valid.";
            return false;
        }
        const outputs = root.outputNames();
        if (outputs.length === 0) {
            root.lastError = "No display to set a wallpaper on.";
            return false;
        }
        const expected = WallpaperPolicy.effectiveMap(
            normalised.mode, normalised.globalPath, normalised.slideshowPath,
            normalised.assignments, outputs, normalised.candidates);
        if (Object.keys(expected).length !== outputs.length
                || Object.values(expected).some(path => path === "")) {
            root.lastError = "That wallpaper policy is not valid.";
            return false;
        }
        root.lastError = "";
        root.transaction = {
            expected,
            remaining: outputs.slice(),
            policy: normalised,
            persist: persist === true,
            automatic: automatic === true
        };
        root.drainTransaction();
        return true;
    }

    function drainTransaction(): void {
        if (root.transaction === null || applyProcess.running || verifyProcess.running)
            return;
        if (root.transaction.remaining.length === 0) {
            verifyProcess.exec(["hyprctl", "hyprpaper", "listactive"]);
            return;
        }
        const output = root.transaction.remaining[0];
        root.transaction.remaining = root.transaction.remaining.slice(1);
        applyProcess.exec([
            "hyprctl", "hyprpaper", "wallpaper",
            `${output},${root.transaction.expected[output]}`
        ]);
    }

    function parseActive(text: string): var {
        const result = {};
        const lines = String(text).split("\n").map(line => line.trim()).filter(line => line !== "");
        for (const line of lines) {
            const match = line.match(/^([A-Za-z0-9_.-]+): (\/[^,\n]+)$/);
            if (!match || result[match[1]] !== undefined)
                return null;
            result[match[1]] = match[2];
        }
        return result;
    }

    function finishVerification(exitCode: int): void {
        if (root.transaction === null)
            return;
        const observed = exitCode === 0 ? root.parseActive(verifyProcess.outputText) : null;
        const expected = root.transaction.expected;
        const matches = observed !== null
            && Object.keys(observed).length === Object.keys(expected).length
            && Object.keys(expected).every(output => observed[output] === expected[output]);
        if (!matches) {
            root.lastError = "Hyprpaper did not confirm that background.";
            root.transaction = null;
            root.schedulePendingHotplug();
            return;
        }

        const completed = root.transaction;
        root.activeByOutput = observed;
        root.transaction = null;
        root.lastError = "";
        if (completed.automatic) {
            root.slideshowPath = completed.policy.slideshowPath;
            root.shuffleBag = completed.policy.nextShuffleBag;
            root.slideshowIndex = completed.policy.nextSlideshowIndex;
        }
        if (completed.persist)
            root.persistPolicy(completed.policy);
        root.schedulePendingHotplug();
    }

    function persistPolicy(policy: var): void {
        DesktopPreferences.set("wallpaperMode", policy.mode);
        DesktopPreferences.set("wallpaperPath", policy.storedPath);
        DesktopPreferences.set("wallpaperSlideshowPaths", policy.collection);
        DesktopPreferences.set("wallpaperIntervalMinutes", policy.intervalMinutes);
        DesktopPreferences.set("wallpaperShuffle", policy.shuffle);
        DesktopPreferences.set("wallpaperPerMonitor", policy.assignments);
    }

    function applyCurrentPolicy(persist: bool): bool {
        return root.applyPolicy(root.currentPolicy(), persist === true, false);
    }

    function schedulePendingHotplug(): void {
        if (!root.pendingHotplugReapply)
            return;
        root.pendingHotplugReapply = false;
        outputSettle.restart();
    }

    function advanceSlideshow(): bool {
        if (root.busy)
            return false;
        const collection = root.slideshowCollection;
        if (collection.length < 2)
            return false;

        const current = root.slideshowPath !== ""
            ? root.slideshowPath
            : (root.active !== "" ? root.active : collection[0]);
        const policy = root.currentPolicy();
        policy.mode = "slideshow";
        policy.collection = collection;

        if (DesktopPreferences.get("wallpaperShuffle") !== false) {
            const next = WallpaperPolicy.shuffledNext(
                collection, root.shuffleBag, current, Math.random);
            policy.slideshowPath = next.path;
            policy.nextShuffleBag = next.bag;
            policy.nextSlideshowIndex = collection.indexOf(next.path);
        } else {
            policy.slideshowPath = WallpaperPolicy.orderedNext(collection, current);
            policy.nextShuffleBag = [];
            policy.nextSlideshowIndex = collection.indexOf(policy.slideshowPath);
        }
        return root.applyPolicy(policy, false, true);
    }

    function setSingle(path: string): bool {
        const effectivePath = path === "" ? root.shippedPath : path;
        const allowed = root.candidates([]);
        if (!WallpaperPolicy.validPath(effectivePath, allowed)) {
            root.lastError = "That file path cannot be used as a wallpaper.";
            return false;
        }
        const policy = root.currentPolicy();
        policy.mode = "single";
        policy.globalPath = effectivePath;
        policy.slideshowPath = effectivePath;
        return root.applyPolicy(policy, true, false);
    }

    function setMode(mode: string): bool {
        if (!["single", "slideshow", "per-monitor"].includes(mode))
            return false;
        const policy = root.currentPolicy();
        policy.mode = mode;
        if (mode === "slideshow") {
            const collection = WallpaperPolicy.validCollection(
                policy.collection, root.candidates([]));
            if (!collection.includes(policy.slideshowPath))
                policy.slideshowPath = collection[0] || policy.globalPath;
        }
        return root.applyPolicy(policy, true, false);
    }

    function setAssignment(output: string, path: string): bool {
        if (!root.outputNames().includes(output)
                || !WallpaperPolicy.validPath(path, root.candidates([])))
            return false;
        const policy = root.currentPolicy();
        policy.mode = "per-monitor";
        policy.assignments = Object.assign({}, policy.assignments);
        policy.assignments[output] = path;
        return root.applyPolicy(policy, true, false);
    }

    function toggleSlideshowPath(path: string): bool {
        if (!WallpaperPolicy.validPath(path, root.candidates([])))
            return false;
        const policy = root.currentPolicy();
        const collection = WallpaperPolicy.validCollection(policy.collection, root.candidates([]));
        policy.collection = collection.includes(path)
            ? collection.filter(candidate => candidate !== path)
            : collection.concat([path]);
        policy.mode = "slideshow";
        if (!policy.collection.includes(policy.slideshowPath))
            policy.slideshowPath = policy.collection[0] || policy.globalPath;
        return root.applyPolicy(policy, true, false);
    }

    function setIntervalMinutes(minutes: int): bool {
        const value = PreferenceSchema.coerce("wallpaperIntervalMinutes", minutes);
        return value !== undefined
            && DesktopPreferences.set("wallpaperIntervalMinutes", value);
    }

    function setShuffle(enabled: bool): bool {
        return DesktopPreferences.set("wallpaperShuffle", enabled === true);
    }

    // Compatibility boundary used by backup/reset and the existing picker.
    function set(path: string): bool {
        return root.setSingle(path);
    }

    function titleFor(path: string): string {
        const file = String(path).split("/").pop();
        return file.replace(/\.[^.]+$/, "").replace(/[_-]+/g, " ");
    }
}
