#!/usr/bin/env bash

# The accent palette and the theme resolver, for the shell scripts that need
# them.
#
# Sourced, not run:
#
#   source "$(dirname "$0")/panama-palette"
#   accent_hex orchid dark      # -> c099ff
#   hex_to_rgb c099ff           # -> 192, 153, 255
#   eval "$(theme_vars "$(resolve_theme dark)")"   # -> pal_bg, ansi_red, ...
#
# Why this exists: the same eight accents were written out by hand in five
# places -- Theme.qml through ThemeProfileModel.js, hypr/looks.lua,
# panama-theme-apps and panama-lock -- and panama-theme-apps said so in a
# comment admitting "there is no shared source between QML and a shell script".
# Adding a ninth accent meant editing five files, and the one most likely to be
# missed was the lock screen, which fails silently: the machine locks with a
# stale color and nothing says why.
#
# config/palette.json is now that shared source. QML still carries the table in
# ThemeProfileModel.js, because a .js module imported into QML cannot read a
# file -- but the two are checked against each other by
# tests/quickshell/palette-contract, so drift is a failing test rather than a
# lock screen in last season's colour.

# The palette lives beside the shell config. Resolved from this script's own
# location so it is correct wherever the repository is checked out.
PANAMA_PALETTE="${PANAMA_PALETTE:-$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)/config/palette.json}"

# The hex for one accent in one scheme, without the leading '#'.
#
# Falls back to blue for an unknown or omitted name, matching Theme.qml's own
# accentPair fallback: a caller that only ever knew about the scheme still gets
# a real accent rather than an empty string reaching sed.
accent_hex() {
    local name="${1:-}" scheme="${2:-dark}" key value
    [[ "$scheme" == "light" ]] && key="light" || key="dark"

    value="$(jq -r --arg n "$name" --arg k "$key" \
        '.accents[$n][$k] // empty' "$PANAMA_PALETTE" 2>/dev/null)"
    if [[ -z "$value" ]]; then
        value="$(jq -r --arg k "$key" \
            '.accents[.default][$k] // empty' "$PANAMA_PALETTE" 2>/dev/null)"
    fi
    # Last resort, for a palette file that is missing or unreadable. Shipping
    # blue is better than shipping an empty string into a config generator.
    [[ -n "$value" ]] || value="$([[ "$key" == light ]] && echo 2e7de9 || echo 82aaff)"
    printf '%s' "$value"
}

# The second half of the pair, for the two-stop gradient the window border uses.
accent_secondary_hex() {
    local name="${1:-}" scheme="${2:-dark}" key value
    [[ "$scheme" == "light" ]] && key="lightSecondary" || key="darkSecondary"

    value="$(jq -r --arg n "$name" --arg k "$key" \
        '.accents[$n][$k] // empty' "$PANAMA_PALETTE" 2>/dev/null)"
    if [[ -z "$value" ]]; then
        value="$(jq -r --arg k "$key" \
            '.accents[.default][$k] // empty' "$PANAMA_PALETTE" 2>/dev/null)"
    fi
    [[ -n "$value" ]] || value="$([[ "$key" == lightSecondary ]] && echo 9854f1 || echo b172b0)"
    printf '%s' "$value"
}

# GNOME's accent-color is a FIXED enum of nine names -- there is no hex to
# match, only a nearest member to pick -- so palette.json records which one
# each Panama accent maps to. libadwaita applications are told this, so
# choosing an accent here recolours Files and Papers too.
accent_gnome() {
    local name="${1:-}" value
    value="$(jq -r --arg n "$name" '.accents[$n].gnome // empty' "$PANAMA_PALETTE" 2>/dev/null)"
    [[ -n "$value" ]] || value="$(jq -r '.accents[.default].gnome // empty' "$PANAMA_PALETTE" 2>/dev/null)"
    [[ -n "$value" ]] || value="blue"
    printf '%s' "$value"
}

# hyprlock wants "R, G, B" decimal, not hex.
hex_to_rgb() {
    local hex="${1#\#}"
    printf '%d, %d, %d' "0x${hex:0:2}" "0x${hex:2:2}" "0x${hex:4:2}"
}

# ── Themes ───────────────────────────────────────────────────────────────────
#
# An accent is one colour; a THEME is the whole palette plus that accent pair.
# Shipped themes live in config/themes.json, custom ones in settings.json's
# themeProfiles array, and the two shell generators that render application
# configuration -- panama-theme-apps and panama-lock -- both have to resolve
# the same active theme from those two files. Doing that twice is how the
# hyprlock colours came to be written out in three places; the resolution
# lives here for the same reason accent_hex does.

# The catalog lives beside the shell config, resolved from this script's own
# location so it is correct wherever the repository is checked out.
PANAMA_THEMES="${PANAMA_THEMES:-$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)/config/themes.json}"

# Read at call time rather than baked in at source time, so a caller (or a
# test) that sets XDG_CONFIG_HOME after sourcing still gets the right file.
panama_settings_file() {
    printf '%s' "${PANAMA_SETTINGS:-${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json}"
}

# The complete theme record for one scheme, as compact JSON:
#
#   { id, scheme, accent, secondary, palette: {19 keys}, ansi: {16 keys} }
#
# Resolution order, per scheme: the per-mode preference (themeDark/themeLight),
# then themeProfileId when it names a theme of that scheme, then the catalog's
# default for the scheme. A record found in either place wins only where it
# actually carries a value -- palette and ansi are MERGED over the scheme
# default's, so a custom theme saved with only an accent (every profile stored
# before themes existed) still resolves to a complete palette rather than to
# nothing.
resolve_theme() {
    local scheme="${1:-dark}" settings settings_json='{}' catalog_json='{}'
    [[ "$scheme" == "light" ]] || scheme="dark"

    settings="$(panama_settings_file)"
    if [[ -r "$settings" ]] && jq -e 'type == "object"' "$settings" >/dev/null 2>&1; then
        settings_json="$(cat "$settings")"
    fi
    if [[ -r "$PANAMA_THEMES" ]] && jq -e 'type == "object"' "$PANAMA_THEMES" >/dev/null 2>&1; then
        catalog_json="$(cat "$PANAMA_THEMES")"
    fi

    jq -cn --arg scheme "$scheme" --argjson settings "$settings_json" \
        --argjson catalog "$catalog_json" '
        def shipped: ($catalog.themes // []) | map(select(type == "object"));
        def customs: ($settings.themeProfiles // []) | map(select(type == "object"));
        def defaultId($s): if $s == "light" then ($catalog.defaultLight // "day")
                           else ($catalog.defaultDark // "moon") end;
        def byId($id): (shipped | map(select(.id == $id)) | first)
                    // (customs | map(select(.id == $id)) | first);
        def forScheme($id): byId($id) | select(. != null and ((.scheme // "dark") == $scheme));

        (byId(defaultId($scheme)) // {}) as $base
        | ( forScheme($settings[if $scheme == "light" then "themeLight" else "themeDark" end] // "")
            // forScheme($settings.themeProfileId // "")
            // $base ) as $theme
        | { id:        ($theme.id // defaultId($scheme)),
            scheme:    $scheme,
            accent:    ($theme.accent // $base.accent // ""),
            secondary: ($theme.secondary // $base.secondary // ""),
            palette:   (($base.palette // {}) + ($theme.palette // {})),
            ansi:      (($base.ansi // {}) + ($theme.ansi // {})) }
    ' 2>/dev/null \
        || printf '{"id":"","scheme":"%s","accent":"","secondary":"","palette":{},"ansi":{}}' "$scheme"
}

# A theme record as eval-able shell assignments, so a generator reads a colour
# as "$pal_bg" rather than paying a jq invocation per token:
#
#   eval "$(theme_vars "$(resolve_theme dark)")"
#   printf 'background #%s\n' "$pal_bg"
#
# The optional prefix lets one script hold two themes at once -- Firefox needs
# both schemes in a single stylesheet so the browser can flip on its own.
#
# Every value is validated to six lowercase hex digits before it is emitted and
# anything else becomes the empty string, so the eval can never run something
# a settings file said. A caller must therefore treat "" as "not themed" rather
# than substituting it into a config.
theme_vars() {
    local json="${1:-\{\}}" prefix="${2:-}"
    jq -r --arg p "$prefix" '
        def hex: if type == "string"
                     and (ascii_downcase | ltrimstr("#") | test("^[0-9a-f]{6}$"))
                 then (ascii_downcase | ltrimstr("#")) else "" end;
        ((.palette // {}) | to_entries[]
            | select(.key | test("^[A-Za-z][A-Za-z0-9]*$"))
            | "\($p)pal_\(.key)=\(.value | hex)"),
        ((.ansi // {}) | to_entries[]
            | select(.key | test("^[A-Za-z][A-Za-z0-9]*$"))
            | "\($p)ansi_\(.key)=\(.value | hex)"),
        "\($p)theme_accent=\(.accent | hex)",
        "\($p)theme_secondary=\(.secondary | hex)",
        "\($p)theme_id=\(((.id // "") | if test("^[A-Za-z0-9._-]+$") then . else "" end))"
    ' <<<"$json" 2>/dev/null || true
}

# First non-empty of the two. Written out because `[[ -n $a ]] && printf $a`
# returns 1 when it is empty, which `set -e` treats as a failure.
hex_or() {
    if [[ -n "${1:-}" ]]; then printf '%s' "$1"; else printf '%s' "${2:-}"; fi
}

# Blend two colours: mix_hex <base> <tint> <percent of tint>. Used to derive
# the in-between surfaces (a selected row, a grid tile) that no palette token
# names but every application theme wants.
mix_hex() {
    local base="${1#\#}" tint="${2#\#}" percent="${3:-50}" offset out=""
    local from to blended
    for offset in 0 2 4; do
        from=$((16#${base:offset:2}))
        to=$((16#${tint:offset:2}))
        blended=$(( (from * (100 - percent) + to * percent) / 100 ))
        out+="$(printf '%02x' "$blended")"
    done
    printf '%s' "$out"
}

# Perceived luminance, 0-255. Not the arithmetic mean: green reads far brighter
# than blue at the same value, and the mean puts white text on a yellow accent.
luma_hex() {
    local hex="${1#\#}" red green blue
    red=$((16#${hex:0:2})); green=$((16#${hex:2:2})); blue=$((16#${hex:4:2}))
    printf '%d' $(( (red * 299 + green * 587 + blue * 114) / 1000 ))
}

# Whether dark text belongs on top of this colour.
is_light_hex() {
    (( $(luma_hex "$1") >= 140 ))
}

# The colour to draw text in on top of a filled surface -- an accent button, a
# selected row. Takes two of the theme's own colours rather than black and
# white, so the result keeps the theme's cast.
#
# The two candidates are compared by their own luminance rather than trusted to
# arrive in a fixed order. Passing "the dark one" first only works on a dark
# theme: on a light one the foreground IS the dark colour, which is how a light
# theme ended up with blue label text on a blue button.
on_hex() {
    local surface="${1:-}" first="${2:-}" second="${3:-}" first_luma second_luma
    [[ -n "$first" ]] || { printf '%s' "$second"; return; }
    [[ -n "$second" && -n "$surface" ]] || { printf '%s' "$first"; return; }

    first_luma="$(luma_hex "$first")"
    second_luma="$(luma_hex "$second")"
    if is_light_hex "$surface"; then
        (( first_luma <= second_luma )) && printf '%s' "$first" || printf '%s' "$second"
    else
        (( first_luma >= second_luma )) && printf '%s' "$first" || printf '%s' "$second"
    fi
}

# The lock screen, rendered from a theme into hyprlock's config language.
#
# This lives here rather than in panama-theme-apps because two other callers
# need the same eight substitutions: setup/scripts/link-dotfiles seeds the
# fallback config at install time, and panama-lock renders the generated one.
# All three used to carry their own table of literal rgba triples, and the copy
# most likely to be missed was the lock screen's -- which fails silently, with
# the machine locked in last season's colour and nothing to say why.
#
# hyprlock takes rgba(r, g, b, a) in DECIMAL rather than hex, which is why the
# template carries "R, G, B" triples. The two _HEX placeholders are the
# exception: they sit inside Pango markup, which wants ##rrggbb.
#
#   render_hyprlock <dark|light> <template> <destination> [accent-name]
#
# Returns non-zero without touching the destination when the template is
# unreadable or the theme resolves without a palette -- a half-substituted
# config is not a parse error to hyprlock, it is a set of colours it quietly
# ignores in favour of its own bright grey defaults.
render_hyprlock() {
    local scheme="${1:-dark}" template="${2:-}" destination="${3:-}" accent_name="${4:-blue}"
    [[ -r "$template" && -n "$destination" ]] || return 1
    (
        set -euo pipefail
        eval "$(theme_vars "$(resolve_theme "$scheme")")"

        local accent
        accent="$(hex_or "${theme_accent:-}" "$(accent_hex "$accent_name" "$scheme")")"
        for value in "${pal_fg:-}" "${pal_fgMuted:-}" "${pal_red:-}" "${pal_bg:-}" \
                     "${pal_bgPanel:-}" "$accent"; do
            [[ -n "$value" ]] || exit 1
        done

        # Written atomically: a lock triggered mid-write would otherwise read a
        # truncated config and fall back to hyprlock's own defaults, which is a
        # bright grey screen with none of this desktop's identity.
        local temporary="$destination.tmp.$$"
        sed -e "s/@FG@/$(hex_to_rgb "$pal_fg")/g" \
            -e "s/@MUTED@/$(hex_to_rgb "$pal_fgMuted")/g" \
            -e "s/@ACCENT@/$(hex_to_rgb "$accent")/g" \
            -e "s/@ERROR@/$(hex_to_rgb "$pal_red")/g" \
            -e "s/@BG@/$(hex_to_rgb "$pal_bg")/g" \
            -e "s/@FIELD@/$(hex_to_rgb "$pal_bgPanel")/g" \
            -e "s/@MUTED_HEX@/$pal_fgMuted/g" \
            -e "s/@ERROR_HEX@/$pal_red/g" \
            "$template" >"$temporary" 2>/dev/null \
            && mv "$temporary" "$destination" 2>/dev/null \
            || { rm -f "$temporary" 2>/dev/null || true; exit 1; }
    )
}
