pragma Singleton

// The theme resolver. Shipped themes come from ThemeCatalog (config/themes.json),
// customs from the themeProfiles preference; the active theme is one record with
// a full palette, resolved here for Theme.qml and the render pipeline.
//
// Selection is per-mode: themeDark and themeLight remember the chosen theme for
// each scheme, so flipping light/dark lands on *your* theme for that side —
// never a forced reset to the defaults. Every commit path ends in commitActive,
// which also keeps accentName (GNOME's enum, kitty's border, the lock screen)
// pointed at the nearest curated accent — the desync those surfaces used to
// suffer after a custom edit is structural now, not a convention.

import Quickshell
import QtQuick
import qs.config
import "ThemeProfileModel.js" as ThemeProfileModel

Singleton {
    id: root

    readonly property var shippedThemes: ThemeCatalog.themes
    readonly property var customProfiles: ThemeProfileModel.validCustomProfiles(
        DesktopPreferences.get("themeProfiles"), root.shippedThemes)
    readonly property var curatedAccents: ThemeProfileModel.curatedAccents()
    readonly property var profiles: ThemeProfileModel.profileCatalog(
        root.customProfiles, root.shippedThemes)

    readonly property string scheme: DesktopPreferences.get("colorScheme") === "light" ? "light" : "dark"
    readonly property string activeId: DesktopPreferences.get("themeProfileId") || ThemeCatalog.defaultDark
    readonly property var activeProfile: ThemeProfileModel.findProfile(
        root.customProfiles, root.activeId, root.shippedThemes)
        || ThemeProfileModel.shippedProfiles(root.shippedThemes)[0]
    readonly property string activeAccentName: ThemeProfileModel.curatedNameForProfile(
        root.activeProfile)

    // Always complete: the record's own palette, or the scheme default's.
    readonly property var activePalette: root.activeProfile.palette
        ?? ThemeCatalog.defaultPalette(root.activeProfile.scheme)
    readonly property var activeAnsi: root.activeProfile.ansi
        ?? (ThemeCatalog.byId(root.activeProfile.scheme === "light"
            ? ThemeCatalog.defaultLight : ThemeCatalog.defaultDark)?.ansi ?? null)

    property bool reconciling: false

    // The remembered theme for a scheme, falling back to the catalog default.
    function themeForScheme(target: string): string {
        const key = target === "light" ? "themeLight" : "themeDark";
        const fallback = target === "light" ? ThemeCatalog.defaultLight : ThemeCatalog.defaultDark;
        const stored = String(DesktopPreferences.get(key) || "");
        return ThemeProfileModel.findProfile(root.customProfiles, stored, root.shippedThemes)
            ? stored : fallback;
    }

    function commitActive(profile: var): bool {
        if (!profile)
            return false;

        // Scheme first: if another control changed it, the reconciliation hook
        // may briefly select the scheme default before this final id lands.
        const schemeAccepted = SystemSettings.commitPreference("colorScheme", profile.scheme);
        const profileAccepted = SystemSettings.commitPreference("themeProfileId", profile.id);
        SystemSettings.commitPreference(
            profile.scheme === "light" ? "themeLight" : "themeDark", profile.id);
        SystemSettings.commitPreference("accentName",
            ThemeProfileModel.nearestCuratedName(profile.scheme, profile.accent));
        root.applyProfileEffects(profile);
        return schemeAccepted && profileAccepted;
    }

    // A theme that carries an effects snapshot restores it; themes without one
    // (every shipped theme) leave the user's effects alone.
    function applyProfileEffects(profile: var): void {
        const effects = profile.effects ?? null;
        if (!effects)
            return;
        for (const key of Object.keys(effects))
            SystemSettings.commitPreference(key, effects[key]);
    }

    function selectProfile(id: string): bool {
        return root.commitActive(ThemeProfileModel.findProfile(
            root.customProfiles, id, root.shippedThemes));
    }

    // The Themes tab's light/dark hero: land on the remembered theme for the
    // requested scheme.
    function setScheme(target: string): bool {
        return root.selectProfile(root.themeForScheme(target === "light" ? "light" : "dark"));
    }

    // Editing a shipped profile creates a saved custom copy. Editing a custom
    // profile updates only that record. ThemeProfileModel enforces both rules,
    // leaving this singleton responsible only for validated preference routing.
    function updateActive(changes: var): bool {
        const result = ThemeProfileModel.editProfile(
            root.customProfiles, root.activeProfile, changes, root.shippedThemes);
        if (!result.profile)
            return false;
        if (!SystemSettings.commitPreference("themeProfiles", result.profiles))
            return false;
        return root.commitActive(result.profile);
    }

    function setAccentPair(accent: string, secondary: string): bool {
        return root.updateActive({ accent: accent, secondary: secondary });
    }

    // The editor's Background/Foreground wells: derive the surface and text
    // families from the two ground colors, keep everything else.
    function setGroundColors(bg: string, fg: string): bool {
        const palette = ThemeProfileModel.derivePalette(root.activePalette, {
            scheme: root.activeProfile.scheme,
            bg: bg,
            fg: fg,
            accent: root.activeProfile.accent
        });
        if (!palette)
            return false;
        return root.updateActive({
            palette: palette,
            ansi: root.activeProfile.ansi
                ?? ThemeProfileModel.deriveAnsi(palette, root.activeProfile.accent)
        });
    }

    // The editor's saturation slider. factor 1.0 is neutral.
    function setSaturation(factor: real): bool {
        const palette = ThemeProfileModel.resaturatePalette(root.activePalette, factor);
        if (!palette)
            return false;
        return root.updateActive({ palette: palette });
    }

    // Saving snapshots the whole look: palette, terminal colors, and the ten
    // effect values as they stand right now.
    function saveProfile(name: string): bool {
        const effects = {};
        for (const key of Object.keys(ThemeProfileModel.EFFECT_SPEC))
            effects[key] = DesktopPreferences.get(key);
        const result = ThemeProfileModel.createCustomProfile(root.customProfiles, {
            name: name,
            scheme: root.activeProfile.scheme,
            accent: root.activeProfile.accent,
            secondary: root.activeProfile.secondary,
            palette: root.activePalette,
            ansi: root.activeAnsi,
            effects: effects
        }, root.shippedThemes);
        if (!result.profile)
            return false;
        if (!SystemSettings.commitPreference("themeProfiles", result.profiles))
            return false;
        return root.commitActive(result.profile);
    }

    function deleteProfile(id: string): bool {
        const result = ThemeProfileModel.deleteProfile(
            root.customProfiles, id, root.shippedThemes);
        if (!result.removed)
            return false;
        if (!SystemSettings.commitPreference("themeProfiles", result.profiles))
            return false;
        if (root.activeId === id)
            return root.setScheme(root.scheme);
        return true;
    }

    // Accepts the curated name or the AccentPicker's option object — the
    // picker used to hand the object to a string parameter and silently fall
    // back to blue.
    function useCuratedAccent(entry: var): bool {
        const name = typeof entry === "string" ? entry : String(entry?.value ?? entry?.name ?? "");
        const pair = ThemeProfileModel.curatedPair(name, root.scheme);
        const shipped = ThemeProfileModel.matchingShippedProfile(
            root.scheme, pair.accent, pair.secondary, root.shippedThemes);
        const accepted = shipped
            ? root.commitActive(shipped)
            : root.setAccentPair(pair.accent, pair.secondary);
        if (accepted && ThemeProfileModel.curatedAccents()[name])
            SystemSettings.commitPreference("accentName", name);
        return accepted;
    }

    // A scheme flip from anywhere (the hero, IPC, a synced settings file)
    // lands on the remembered theme for that scheme — never a forced default.
    function reconcileScheme(): void {
        if (root.reconciling)
            return;
        if (root.activeProfile.scheme === root.scheme)
            return;
        root.reconciling = true;
        SystemSettings.commitPreference("themeProfileId", root.themeForScheme(root.scheme));
        root.reconciling = false;
    }

    Component.onCompleted: root.reconcileScheme()

    Connections {
        target: DesktopPreferences
        function onRevisionChanged(): void { root.reconcileScheme(); }
    }
}
