pragma Singleton

// ─────────────────────────────────────────────────────────────────────────────
// Night light — a thin wrapper around hyprsunset (0.4.0).
//
// hyprsunset is a daemon: `hyprsunset -t <kelvin>` grabs wlr-gamma-control and
// holds it until it exits, and `hyprctl hyprsunset <request>` re-tunes the
// running instance over its socket. So the shape here is:
//   * `active` drives whether the daemon process runs at all,
//   * temperature changes are pushed to the *running* daemon rather than
//     restarting it, which would flash the screen back to 6500K.
//
// Gamma is restored by the compositor when the daemon's gamma-control object
// dies, so stopping the Process is a complete "off" — no `-i` pass needed.
//
// Only works under Hyprland; there is no gamma protocol on GNOME, so the
// daemon exits immediately there. That is reported once, not retried.
// ─────────────────────────────────────────────────────────────────────────────

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

Singleton {
    id: root

    property bool initialized: false
    property bool suppressStatusEvents: false

    // The manual switch. Ignored while `automatic` is on.
    property bool enabled: Settings.nightLightEnabledByDefault
    property int temperature: Settings.nightLightTemperature

    // Follow Settings.nightLightFrom .. nightLightTo instead of the manual
    // switch. Off by default because GNOME's schedule was disabled.
    property bool automatic: DesktopPreferences.get("nightLightAutomatic")

    // What is actually applied right now.
    readonly property bool active: root.automatic ? root.scheduled : root.enabled

    // True while the wall clock is inside the scheduled window.
    readonly property bool scheduled: root.inWindow(clock.hours + clock.minutes / 60)

    // A manual toggle always wins: it drops out of the schedule rather than
    // being silently reverted a minute later. Same as GNOME's behavior when
    // you flip night light off during a scheduled evening.
    function toggle(): void {
        if (root.automatic) {
            root.automatic = false;
            root.enabled = !root.scheduled;
        } else {
            root.enabled = !root.enabled;
        }
    }

    // Launcher actions already provide immediate Prism OSD feedback. Keep the
    // ambient Signal Glass channel quiet for that path so one choice produces
    // one confirmation instead of two competing transients.
    function toggleQuietly(): void {
        root.suppressStatusEvents = true;
        root.toggle();
        root.suppressStatusEvents = false;
    }

    function restore(manualEnabled: bool, automaticEnabled: bool): void {
        root.suppressStatusEvents = true;
        root.enabled = manualEnabled;
        root.automatic = automaticEnabled;
        root.suppressStatusEvents = false;
    }

    // The window wraps midnight (17:00 → 10:00), so the comparison flips when
    // `from` is later in the day than `to`.
    function inWindow(hour: real): bool {
        const from = Settings.nightLightFrom;
        const to = Settings.nightLightTo;
        return from <= to ? (hour >= from && hour < to) : (hour >= from || hour < to);
    }

    // Only ticks while the schedule is in charge — no idle work otherwise.
    SystemClock {
        id: clock
        precision: SystemClock.Minutes
        enabled: root.automatic
    }

    Process {
        id: daemon
        command: ["hyprsunset", "-t", String(root.temperature)]
        running: root.active

        onExited: (code, status) => {
            if (root.active)
                console.warn("NightLight: hyprsunset exited with", code, "- is Hyprland running?");
        }
    }

    // Re-tune in place; restarting the daemon would flash the display.
    onTemperatureChanged: {
        DesktopPreferences.set("nightLightTemperature", root.temperature);
        if (daemon.running)
            Quickshell.execDetached(["hyprctl", "hyprsunset", "temperature", String(root.temperature)]);
    }

    onEnabledChanged: DesktopPreferences.set("nightLightEnabled", root.enabled)
    onAutomaticChanged: DesktopPreferences.set("nightLightAutomatic", root.automatic)

    // `enabled`, `temperature`, and `automatic` are declared as bindings on the
    // store, but a binding is destroyed the moment anything assigns to the
    // property -- which toggle() does. Without this, the service would write to
    // the store and never read from it again: Quick Settings would keep working
    // while the same settings on the Displays page silently did nothing, which
    // is worse than not offering them there at all.
    //
    // No loop: set() is a no-op when the value is unchanged, and assigning a
    // property its current value emits nothing, so this converges immediately.
    Connections {
        target: DesktopPreferences
        function onRevisionChanged(): void { root.syncFromStore(); }
    }

    function syncFromStore(): void {
        const storedEnabled = DesktopPreferences.get("nightLightEnabled") === true;
        if (root.enabled !== storedEnabled)
            root.enabled = storedEnabled;

        const storedAutomatic = DesktopPreferences.get("nightLightAutomatic") === true;
        if (root.automatic !== storedAutomatic)
            root.automatic = storedAutomatic;

        const storedTemperature = DesktopPreferences.get("nightLightTemperature");
        if (typeof storedTemperature === "number" && root.temperature !== storedTemperature)
            root.temperature = storedTemperature;
    }

    onActiveChanged: {
        if (!root.initialized || root.suppressStatusEvents)
            return;
        StatusEvents.publish({
            key: "display-night-light",
            glyph: "\u{F0F31}",
            title: root.active ? "Night Light on" : "Night Light off",
            detail: root.active ? String(root.temperature) + " K" : "Display colors restored",
            tone: root.active ? "warn" : "accent",
            priority: StatusEvents.ambientPriority
        });
    }

    Component.onCompleted: Qt.callLater(() => root.initialized = true)
}
