pragma Singleton

// The shipped theme catalog, read from config/themes.json — full palettes for
// every theme Panama ships, one file shared with the scripts (jq) and the
// Node halves of contracts (fs). QML reads it here with FileView rather than
// keeping a duplicated JS table: unlike palette.json's CURATED twin, nothing
// in this file needs to run under Node.
//
// The embedded fallback below carries only the two default themes (Moon and
// Day) so the shell renders correctly for the instant before the file loads —
// and forever if it is missing or malformed. theme-catalog-contract pins the
// fallback byte-identical to the catalog's moon and day records.

import Quickshell
import Quickshell.Io
import QtQuick

Singleton {
    id: root

    // [{ id, name, scheme, accent, secondary, palette: {19 tokens}, ansi: {16} }]
    property var themes: root.fallbackThemes
    property string defaultDark: "moon"
    property string defaultLight: "day"
    property bool loaded: false
    property string lastError: ""

    function byId(id: string): var {
        return root.themes.find(theme => theme.id === id) ?? null;
    }

    // The palette every resolution falls back to: the default theme for the
    // scheme. Always complete, even before the file loads.
    function defaultPalette(scheme: string): var {
        const id = scheme === "light" ? root.defaultLight : root.defaultDark;
        const theme = root.byId(id) ?? root.fallbackThemes[scheme === "light" ? 1 : 0];
        return theme.palette;
    }

    function themesFor(scheme: string): var {
        return root.themes.filter(theme => theme.scheme === scheme);
    }

    // A record is only accepted with every palette key present and valid, so
    // nothing downstream ever needs a per-token fallback.
    readonly property var paletteKeys: ["bg", "bgDark", "bgHighlight", "bgPanel",
        "bgPopover", "fg", "fgDim", "fgMuted", "gutter", "accentAlt",
        "cyan", "teal", "green", "yellow", "orange", "red", "redDeep",
        "magenta", "pink"]
    readonly property var ansiKeys: ["black", "red", "green", "yellow", "blue",
        "magenta", "cyan", "white", "brightBlack", "brightRed", "brightGreen",
        "brightYellow", "brightBlue", "brightMagenta", "brightCyan", "brightWhite"]

    function validHex(value: var): bool {
        return typeof value === "string" && /^#[0-9a-f]{6}$/.test(value);
    }

    function normalizeTheme(entry: var): var {
        if (!entry || typeof entry !== "object")
            return null;
        const id = String(entry.id ?? "");
        const name = String(entry.name ?? "");
        const scheme = String(entry.scheme ?? "");
        if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(id) || name === ""
                || (scheme !== "dark" && scheme !== "light"))
            return null;
        if (!root.validHex(entry.accent) || !root.validHex(entry.secondary))
            return null;
        const palette = {};
        for (const key of root.paletteKeys) {
            const value = entry.palette?.[key];
            if (!root.validHex(value))
                return null;
            palette[key] = value;
        }
        const ansi = {};
        for (const key of root.ansiKeys) {
            const value = entry.ansi?.[key];
            if (!root.validHex(value))
                return null;
            ansi[key] = value;
        }
        return {
            id: id, name: name, scheme: scheme, shipped: true,
            accent: entry.accent, secondary: entry.secondary,
            palette: palette, ansi: ansi
        };
    }

    function consume(text: string): void {
        try {
            const parsed = JSON.parse(text);
            const themes = (parsed.themes ?? [])
                .map(entry => root.normalizeTheme(entry))
                .filter(theme => theme !== null);
            if (themes.length === 0)
                throw new Error("no valid themes");
            root.themes = themes;
            root.defaultDark = String(parsed.defaultDark ?? "moon");
            root.defaultLight = String(parsed.defaultLight ?? "day");
            root.loaded = true;
            root.lastError = "";
        } catch (error) {
            root.lastError = "The theme catalog could not be read; using the built-in themes.";
        }
    }

    FileView {
        id: catalogFile
        path: Quickshell.shellDir + "/config/themes.json"
        printErrors: false
        onLoaded: root.consume(this.text())
        onLoadFailed: root.lastError =
            "The theme catalog is missing; using the built-in themes."
    }

    // Byte-identical to the catalog's moon and day records — the shell's
    // pre-theme literals, pinned by theme-catalog-contract.
    readonly property var fallbackThemes: [
        {
            id: "moon", name: "Tokyo Moon", scheme: "dark", shipped: true,
            accent: "#82aaff", secondary: "#b172b0",
            palette: {
                bg: "#222436", bgDark: "#1e2030", bgHighlight: "#2f334d",
                bgPanel: "#2e2f3d", bgPopover: "#21212f",
                fg: "#c8d3f5", fgDim: "#828bb8", fgMuted: "#636da6",
                gutter: "#3b4261", accentAlt: "#65bcff",
                cyan: "#86e1fc", teal: "#4fd6be", green: "#c3e88d",
                yellow: "#ffc777", orange: "#ff966c", red: "#ff757f",
                redDeep: "#c53b53", magenta: "#c099ff", pink: "#fca7ea"
            },
            ansi: {
                black: "#1b1d2b", red: "#ff757f", green: "#c3e88d",
                yellow: "#ffc777", blue: "#82aaff", magenta: "#c099ff",
                cyan: "#86e1fc", white: "#828bb8",
                brightBlack: "#444a73", brightRed: "#ff8d94", brightGreen: "#c7fb6d",
                brightYellow: "#ffd8ab", brightBlue: "#9ab8ff", brightMagenta: "#caabff",
                brightCyan: "#b2ebff", brightWhite: "#c8d3f5"
            }
        },
        {
            id: "day", name: "Tokyo Day", scheme: "light", shipped: true,
            accent: "#2e7de9", secondary: "#9854f1",
            palette: {
                bg: "#e1e2e7", bgDark: "#d3d5de", bgHighlight: "#c4c8da",
                bgPanel: "#d9dae3", bgPopover: "#eaeaee",
                fg: "#3760bf", fgDim: "#6172b0", fgMuted: "#848cb5",
                gutter: "#a8aecb", accentAlt: "#007197",
                cyan: "#007197", teal: "#118c74", green: "#587539",
                yellow: "#8c6c3e", orange: "#b15c00", red: "#f52a65",
                redDeep: "#c64343", magenta: "#9854f1", pink: "#d20065"
            },
            ansi: {
                black: "#b4b5b9", red: "#f52a65", green: "#587539",
                yellow: "#8c6c3e", blue: "#2e7de9", magenta: "#9854f1",
                cyan: "#007197", white: "#6172b0",
                brightBlack: "#a1a6c5", brightRed: "#ff4774", brightGreen: "#5c8524",
                brightYellow: "#a27629", brightBlue: "#358aff", brightMagenta: "#a463ff",
                brightCyan: "#007ea8", brightWhite: "#3760bf"
            }
        }
    ]
}
