package com.ponzischeme89.memby.ui.theme

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

/**
 * The one vocabulary of colour, shape and punctuation shared by the launcher and the detail
 * pages.
 *
 * These two surfaces sit next to each other the moment a detail page opens from a home row,
 * and they had drifted into four near-blacks, four greens, two secondary-text greys and
 * eight corner radii. Nothing here is new design — it is the values that were already
 * winning, named once so a change lands on both screens at the same time.
 *
 * **The colours are now the server's answer, not constants.** A theme is decided by the
 * gateway (see `server/internal/api/themes.go`) and handed down as a [MembyPalette]; these
 * names are reads of whichever one is in force. Which is why every one of them is a `get()`
 * rather than a value: a token captured once at class-init time would be the palette that
 * happened to be loaded when the first screen composed, and would never change again.
 *
 * The shape and punctuation below are *not* themeable and are still constants. A theme
 * changes colour and nothing else — a palette that could move a corner radius or a
 * separator would be able to make a layout wrong from the server, and the whole safety of
 * this feature is that the worst a bad theme can do is look bad.
 */

/**
 * Every colour a theme sets, and the complete list of them. It matches `themePalette` on
 * the gateway field for field: a palette carrying a colour with no slot here would be a
 * promise the app cannot keep, and one missing a slot is a theme that half-applies, which
 * reads as a rendering fault rather than as a colour scheme.
 *
 * The defaults are Midnight — the palette the app shipped with before themes existed — so a
 * television with no gateway, no network or no theme yet looks exactly as it always did.
 */
data class MembyPalette(
    val surface: Color = Color(0xFF090B0D),
    val surfaceRaised: Color = Color(0xFF101418),
    val accent: Color = Color(0xFF52B54B),
    val onSurface: Color = Color(0xFFE2E5E8),
    val mutedText: Color = Color(0xFFD0D6DB),
    val quietText: Color = Color(0xFFAEB7BF),
    val hairline: Color = Color(0x28FFFFFF),
    val ratingsSurface: Color = Color(0xFF20252A),
)

/**
 * The palette in force, as snapshot state.
 *
 * Process-wide rather than a `CompositionLocal`, and that is deliberate. One television has
 * one signed-in viewer and therefore one theme, and the surfaces that have to obey it are
 * not all inside one composition: the launcher, the detail overlays, the settings sheet, the
 * player's Compose islands and the screensaver's `DreamService` are five separate roots. A
 * local would have to be provided at each of them and would be silently missing from the
 * next one somebody added.
 *
 * Being snapshot state is what makes the tokens below work without a `@Composable`
 * annotation: a read inside composition or a draw scope is recorded, so assigning here
 * repaints exactly the scopes that use the colour that changed.
 */
private var activePalette by mutableStateOf(MembyPalette())

/**
 * Repaints the app. Called by `ThemeSync` when the gateway's answer changes — at sign-in,
 * when the viewer picks a scheme, and at the midnight a season begins or ends.
 *
 * Setting the same palette twice is free: `mutableStateOf` compares with `equals`, and
 * [MembyPalette] is a data class, so a poll that resolves to what is already on screen
 * invalidates nothing.
 */
fun applyMembyPalette(palette: MembyPalette) {
    activePalette = palette
}

/** The palette in force, for the few places that need it whole rather than one colour. */
val membyPalette: MembyPalette get() = activePalette

/** The near-black every full-screen surface is drawn on. */
val MembySurface: Color get() = activePalette.surface

/** One step up, for panels and sheets that need to read as raised off [MembySurface]. */
val MembySurfaceRaised: Color get() = activePalette.surfaceRaised

/** The single accent. Emby's green on the default theme; a theme may make it anything. */
val MembyAccent: Color get() = activePalette.accent

/** Primary body copy. Not pure white — that vibrates on a TV panel at this size. */
val MembyOnSurface: Color get() = activePalette.onSurface

/**
 * Secondary and tertiary copy, raised for TV viewing distance: quiet still reads as
 * secondary without falling into low-contrast grey-on-black.
 */
val MembyMutedText: Color get() = activePalette.mutedText
val MembyQuietText: Color get() = activePalette.quietText

/** Hairline rules and unfocused borders. */
val MembyHairline: Color get() = activePalette.hairline

/** A quiet neutral capsule behind third-party ratings. */
val MembyRatingsSurface: Color get() = activePalette.ratingsSurface

// --- Derived colour ---------------------------------------------------------------------
//
// The eight slots above are what a theme *sends*. What the screens actually paint is a
// wider vocabulary than that — a lighter accent for a label, a near-black ink for text on
// an accent fill, three neutral steps for controls — and every one of those was a literal
// hex typed into a screen. Which is why picking a colour scheme visibly changed almost
// nothing: the palette reached the tokens and the tokens were not what most of the launcher
// was drawn with.
//
// They are *derived* rather than added to the wire on purpose. A theme sends the decisions
// (what is the surface, what is the accent) and the app works out the shades around them,
// so a scheme invented on the gateway tomorrow arrives complete rather than half-applied —
// which is the same reason the palette carries no radii.

/** Mixes [other] into this colour by [fraction] (0 = unchanged, 1 = entirely [other]). */
private fun Color.mix(other: Color, fraction: Float): Color {
    val t = fraction.coerceIn(0f, 1f)
    return Color(
        red = red + (other.red - red) * t,
        green = green + (other.green - green) * t,
        blue = blue + (other.blue - blue) * t,
        alpha = alpha,
    )
}

private fun Color.lighten(fraction: Float): Color = mix(Color.White, fraction)

private fun Color.darken(fraction: Float): Color = mix(Color.Black, fraction)

/**
 * The accent as a *label* — a step brighter than the fill, for accent-coloured text and
 * icons sitting directly on [MembySurface]. The fill colour used as type is a shade too
 * heavy at small sizes on a TV panel.
 */
val MembyAccentBright: Color get() = activePalette.accent.lighten(0.22f)

/**
 * Text and glyphs drawn *on* an accent fill: the accent's own hue taken almost to black, so
 * a focused chip reads as one object rather than as white type on a coloured plate. Pure
 * black would be the safe choice and is the flatter one.
 */
val MembyAccentInk: Color get() = activePalette.accent.darken(0.90f)

/** A quiet accent wash for a plate that is *selected* rather than focused. */
val MembyAccentMuted: Color get() = activePalette.surfaceRaised.mix(activePalette.accent, 0.20f)

/**
 * The three neutral steps between [MembySurfaceRaised] and the text on it: an unfocused
 * control, the same control one step up, and the hairline border around it. Named by their
 * job rather than by their lightness, so a light theme could invert them without every
 * screen that uses one reading backwards.
 */
val MembyControlSurface: Color get() = activePalette.surfaceRaised.lighten(0.06f)
val MembyControlSurfaceRaised: Color get() = activePalette.surfaceRaised.lighten(0.14f)
val MembyOutline: Color get() = activePalette.surfaceRaised.lighten(0.24f)

/** Copy on a control that cannot be pressed. Quiet text taken down, never grey-on-grey. */
val MembyDisabledText: Color get() = activePalette.quietText.darken(0.42f)

/**
 * The wash a full-screen splash is drawn on: the surface with a breath of the accent in it,
 * falling away to the surface itself. It is what makes the cold-start screen belong to the
 * theme in force rather than being the one screen that is always Midnight green.
 */
val MembySplashTint: Color get() = activePalette.surface.mix(activePalette.accent, 0.12f)

// --- Shape ---------------------------------------------------------------------------
// Three steps, largest last. Anything that needs a radius picks the nearest one rather
// than inventing a fourth. Not themeable; see the note at the top of this file.

/** Chips, badges and small controls. */
val MembyChipCorner = 8.dp

/** Cards: posters, episode rows, buttons. */
val MembyCardCorner = 10.dp

/** Full panels: heroes, overlays, confirmation toasts. */
val MembyPanelCorner = 14.dp

// --- Punctuation ---------------------------------------------------------------------

/**
 * Between one fact and the next — year, runtime, certificate. One separator everywhere,
 * because the same line is drawn by the home hero, the metadata panel and the detail page.
 */
const val FactSeparator = "  •  "

/** Within a single fact that happens to hold a list: genres, codecs, audio attributes. */
const val ValueSeparator = " · "
