/*
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
package com.runicgateway.app.ui.theme

import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.font.FontFamily

/**
 * The shard's resolved type families — the `fonts` third of the admin's
 * Appearance page (THEMING_AND_NAV.md §5.3), alongside [ShardPalette] and
 * [ShardStructure].
 *
 * The three roles map onto the M5 type scale's three groups verbatim: `--display`
 * carries the Cinzel display/headline/title block, `--serif` the body block, and
 * `--sans` the label/meta/button block. Sizes, weights and tracking do not move —
 * only the family, which is why [shardTypography] is a one-field substitution and
 * an unthemed shard is a provable no-op (§2, AC-1).
 *
 * Resolution is pure, so the acceptance tests need no Compose rule.
 */
@Immutable
data class ShardTypeface(
    /** `--display`. */
    val display: FontFamily = Cinzel,
    /** `--serif`. */
    val serif: FontFamily = AppSerif,
    /** `--sans`. */
    val sans: FontFamily = AppSans,
) {
    companion object {
        /** The shipped app: the three M5 families. */
        val Shipped = ShardTypeface()

        /**
         * Resolve a `theme` token map into three families, **field by field** (§2):
         * an unreadable `--sans` must not cost the `--serif` beside it, exactly as
         * in [ShardPalette.resolve] and [ShardStructure.resolve].
         */
        fun resolve(theme: Map<String, String>): ShardTypeface {
            if (theme.isEmpty()) return Shipped
            return ShardTypeface(
                display = family(theme["--display"]) ?: Shipped.display,
                serif = family(theme["--serif"]) ?: Shipped.serif,
                sans = family(theme["--sans"]) ?: Shipped.sans,
            )
        }
    }
}

/**
 * Every family name the server can publish, keyed by the lowercased first family
 * of the stack.
 *
 * **This map is deliberately global rather than per-role**, and that is not a
 * simplification. The server validates an *admin-entered* font against
 * `FONT_OPTIONS[role]`, but a preset's tokens are copied verbatim by
 * `resolveThemeTokens` and never pass through that list — `modern` publishes
 * `--display: 'Work Sans', Arial, sans-serif`, which the display dropdown does
 * not offer, and `fantasy` publishes `--sans: 'EB Garamond', Georgia, serif`,
 * which the sans dropdown does not either. A per-role lookup would have missed
 * the display face of one preset and the label face of the other. It is the same
 * trap phase 2 hit with `--shadow-card`, in a different token group.
 *
 * The two system entries are the "shipped default" options: neither pulls in a
 * webfont on the web, and on Android both resolve to the platform family the app
 * has drawn with since M5.
 */
private val FamiliesByFirstName: Map<String, FontFamily> = mapOf(
    "cinzel" to Cinzel,
    "eb garamond" to EBGaramond,
    "merriweather" to Merriweather,
    "playfair display" to PlayfairDisplay,
    "im fell english" to IMFellEnglish,
    "inter" to Inter,
    "work sans" to WorkSans,
    "source sans 3" to SourceSans3,
    "georgia" to AppSerif,
    "helvetica neue" to AppSans,
)

/**
 * Resolve a CSS font stack to a family by **its first name**, which is how the
 * value is constructed server-side and the only part of it that carries the
 * admin's choice — everything after the first comma is the web's fallback chain,
 * which Android has no use for.
 *
 * Returns `null` for a stack this app cannot draw, so the caller falls back to
 * the role's shipped family rather than to some other role's.
 */
private fun family(stack: String?): FontFamily? {
    val first = stack?.substringBefore(',')?.trim()?.trim('\'', '"')?.trim()
    if (first.isNullOrEmpty()) return null
    return FamiliesByFirstName[first.lowercase()]
}
