#!/usr/bin/env bash

# System locale, via localectl.
#
#   panama-locale list  -> [{value, label, detail}]
#   panama-locale get   -> the current LANG, e.g. en_US.UTF-8
#   panama-locale set <locale>
#
# Locale codes are not names. "pt_BR.UTF-8" tells you what it means only if you
# already know, which defeats the point of a picker, so codes are resolved
# against the iso-codes database into "Portuguese (Brazil)" the way GNOME does.
# The code stays visible as the row's detail, because it is what actually gets
# written and someone choosing between two Spanish variants needs to see it.
#
# The join happens in a single jq pass. Doing it per locale meant 327 jq
# invocations, which took long enough to be visible when opening the page.
#
# Setting the locale is a privileged operation: localectl goes through polkit,
# which prompts. It also only takes effect for programs started afterwards, so
# the caller is responsible for saying a sign-out is needed -- this script does
# not pretend the running session changed.

set -uo pipefail

readonly ISO_LANG=/usr/share/iso-codes/json/iso_639-2.json
readonly ISO_COUNTRY=/usr/share/iso-codes/json/iso_3166-1.json

cmd_get() {
    localectl status 2>/dev/null \
        | awk -F'LANG=' '/System Locale:/ { print $2; exit }' \
        | tr -d '[:space:]'
}

cmd_list() {
    local locales
    locales="$(localectl list-locales 2>/dev/null)" || locales=""
    if [[ -z "$locales" ]]; then
        printf '[]\n'
        return 0
    fi

    # Without iso-codes installed the codes are still perfectly usable; they
    # just do not get friendly names. That is a degraded list, not a failure.
    if [[ ! -r "$ISO_LANG" || ! -r "$ISO_COUNTRY" ]]; then
        jq -Rn --rawfile raw /dev/stdin \
            '[$raw | split("\n")[] | select(length > 0) | {value: ., label: ., detail: ""}]' \
            <<<"$locales"
        return 0
    fi

    jq -Rn \
        --slurpfile languages "$ISO_LANG" \
        --slurpfile countries "$ISO_COUNTRY" \
        --rawfile raw /dev/stdin '
        # alpha_2 -> name, for both databases. Languages without a two-letter
        # code cannot appear in a locale name, so they are simply absent.
        ($languages[0]["639-2"] | map(select(.alpha_2)) | INDEX(.alpha_2) | map_values(.name)) as $lang
        | ($countries[0]["3166-1"] | INDEX(.alpha_2) | map_values(.name)) as $country
        | [ $raw
            | split("\n")[]
            | select(length > 0)
            | . as $value
            # en_US.UTF-8 -> ["en", "US"]; the codeset and any @modifier are
            # not part of the human name.
            | ($value | split(".")[0] | split("@")[0] | split("_")) as $parts
            | ($lang[$parts[0]] // $parts[0]) as $language
            | (if ($parts | length) > 1 then $country[$parts[1]] else null end) as $region
            | {
                value: $value,
                label: (if $region then "\($language) (\($region))" else $language end),
                detail: $value
              }
          ]
        | sort_by(.label)
    ' <<<"$locales"
}

cmd_set() {
    local locale="${1:-}"
    # Constrained rather than passed through: this reaches a privileged
    # command, and the set of legal locale names is narrow and well known.
    [[ "$locale" =~ ^[a-zA-Z0-9_@.-]+$ ]] || {
        printf 'panama-locale: refusing a locale name with unexpected characters\n' >&2
        return 2
    }
    localectl list-locales 2>/dev/null | grep -qxF "$locale" || {
        printf 'panama-locale: %s is not an installed locale\n' "$locale" >&2
        return 2
    }
    localectl set-locale "LANG=$locale"
}

case "${1:-list}" in
    list) cmd_list ;;
    get)  cmd_get ;;
    set)  shift; cmd_set "${1:-}" ;;
    *)    printf 'usage: panama-locale [list|get|set <locale>]\n' >&2; exit 2 ;;
esac
