pragma Singleton

// ─────────────────────────────────────────────────────────────────────────────
// Durable user choices, derived entirely from config/PreferenceSchema.qml.
//
// This object owns reading, validating, and writing. It knows nothing about
// which settings exist -- that is the schema's job -- so adding a setting never
// requires touching this file. That is the point: the previous implementation
// restated every key four times (a property alias, a JSON adapter property, a
// change handler, and a line in reset), and each omission failed silently.
//
// Reads go through get(), writes through set(). Typed accessors for the values
// the shell reads on every frame live in Settings.qml, which remains the stable
// public surface.
//
// The store lives at ~/.config/panama/settings.json rather than inside
// Quickshell's per-shell state directory, because the Hyprland Lua config reads
// the same file (see config/dot/hypr/prefs.lua) and because a user should be
// able to back it up, diff it, or keep it in a dotfiles repo.
// ─────────────────────────────────────────────────────────────────────────────

import Quickshell
import Quickshell.Io
import QtQuick

Singleton {
    id: root

    readonly property string path: (Quickshell.env("XDG_CONFIG_HOME") || `${Quickshell.env("HOME")}/.config`) + "/panama/settings.json"

    // Bumped on every accepted change. get() reads it so bindings built on get()
    // have something to invalidate; a bare function call would otherwise capture
    // no dependency and every reader would silently go stale.
    property int revision: 0

    // Everything currently on disk, including keys this build does not know
    // about. Unknown keys are carried through untouched so that rolling back to
    // an older Panama does not discard a newer version's settings.
    property var values: ({})

    property bool loaded: false

    function get(key: string): var {
        root.revision;
        const stored = root.values[key];
        if (stored === undefined)
            return PreferenceSchema.defaultFor(key);
        const coerced = PreferenceSchema.coerce(key, stored);
        return coerced === undefined ? PreferenceSchema.defaultFor(key) : coerced;
    }

    // Returns false when the key is unknown or the value cannot be represented,
    // so a caller can surface the rejection instead of assuming it took.
    function set(key: string, value: var): bool {
        if (!PreferenceSchema.has(key))
            return false;
        const coerced = PreferenceSchema.coerce(key, value);
        if (coerced === undefined)
            return false;
        if (root.values[key] === coerced)
            return true;

        // Reassign rather than mutate: QML does not notify on in-place changes
        // to a var property's contents.
        const next = Object.assign({}, root.values);
        next[key] = coerced;
        root.values = next;
        root.revision++;
        persistTimer.restart();
        return true;
    }

    // Restores every schema default in one write. Complete by construction --
    // there is no hand-maintained list to fall out of sync with the schema.
    function resetDesktopDefaults(): void {
        const next = Object.assign({}, root.values, PreferenceSchema.defaults());
        root.values = next;
        root.revision++;
        persistTimer.restart();
    }

    // Re-read the file from disk. Used after something outside the shell has
    // rewritten it -- restoring a snapshot, or a hand edit -- so the running
    // desktop reflects the new contents without waiting for the next change.
    function reload(): void {
        preferencesFile.reload();
        root.load();
    }

    function load(): void {
        let parsed = {};
        try {
            const text = preferencesFile.text();
            if (text && text.trim().length > 0)
                parsed = JSON.parse(text);
        } catch (error) {
            // A corrupt file must not cost the user a working desktop. Fall
            // back to shipped defaults and let the next write replace it.
            parsed = {};
        }
        const raw = (parsed && typeof parsed === "object") ? parsed : {};

        // Upgrade before anything reads a value. A stored key the current
        // schema no longer recognises is carried through untouched and silently
        // stops taking effect, so the conversion has to happen here rather than
        // being noticed later by whoever owns that setting.
        const result = Migrations.apply(raw);
        root.values = result.values;

        if (result.migrated)
            console.info("Settings migrated from version", result.from, "to", result.to + ":",
                         result.applied.join("; "));

        // Write whenever the version moved, which includes stamping a file
        // written before versioning existed. Left unwritten, the stamp lives
        // only in memory and is redone on every launch, and a migration that is
        // not idempotent would compound.
        if (result.changed)
            persistTimer.restart();

        root.revision++;
        root.loaded = true;
    }

    FileView {
        id: preferencesFile

        path: root.path
        blockLoading: true
        printErrors: false
        atomicWrites: true

        onLoaded: root.load()
        // No file yet is the normal first-run case, not an error.
        onLoadFailed: root.load()
    }

    Component.onCompleted: {
        migration.adopt();
        root.load();
    }

    // Coalesce a burst of changes into one atomic write. Writing on every
    // assignment races a second assignment and can reload the older value.
    Timer {
        id: persistTimer
        interval: 0
        onTriggered: preferencesFile.setText(JSON.stringify(root.values, null, 2) + "\n")
    }

    // One-time move from the pre-Stage-1 location inside Quickshell's state
    // directory. Reads the old file only when the new one does not exist yet, so
    // it can never overwrite newer settings, and never deletes the original.
    QtObject {
        id: migration

        function adopt(): void {
            if (preferencesFile.text())
                return;
            try {
                const legacy = legacyFile.text();
                if (legacy && legacy.trim().length > 0)
                    preferencesFile.setText(legacy);
            } catch (error) {
                // Nothing to migrate.
            }
        }
    }

    FileView {
        id: legacyFile

        path: Quickshell.stateDir + "/panama-settings.json"
        blockLoading: true
        printErrors: false
    }
}
