//! Theme — reusable named [`VisualStyle`]s plus a default fallback.
//!
//! A [`Theme`] is what a project ships to give every UI document a consistent
//! look without hand-styling every widget. The resolution rule is a strict
//! left-to-right cascade:
//!
//! 1. Start with `theme.default` (a `VisualStyle` whose `Some` fields are the
//!    project-wide defaults — body text color, border weight, …).
//! 2. If the widget specifies `theme_style: Some("button")` and the theme
//!    contains a `"button"` entry, merge that on top.
//! 3. Merge the widget's per-instance `visual` on top.
//!
//! Each merge is field-by-field: a `Some` on the right replaces the field;
//! a `None` keeps what was there. The result is a single [`VisualStyle`]
//! where any field that's still `None` means "the renderer's own hard-coded
//! fallback applies" — that fallback lives in piece 4 (the 2D overlay pass).
//!
//! Why named styles instead of CSS-like selectors: it makes per-widget
//! attribution explicit in the UI document (`theme_style: "button-primary"`)
//! and keeps theme resolution constant-time per widget. CSS selectors and
//! cascading rules are a richer model but their authoring cost dwarfs what
//! Stage-8 game UIs actually need.

use std::collections::BTreeMap;

use serde::{Deserialize, Serialize};

use super::visual::VisualStyle;

/// A named-style theme. Holds a `default` style applied to every widget plus
/// a map of named styles widgets can opt into by their `theme_style` field.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Theme {
    /// Project-wide defaults — applied first to every widget before its
    /// `theme_style` and per-instance overrides.
    #[serde(default, skip_serializing_if = "VisualStyle::is_empty")]
    pub default: VisualStyle,
    /// Named style buckets — `theme_style: "button"` on a widget pulls the
    /// `"button"` entry here on top of `default`.
    ///
    /// Stored as a `BTreeMap` (not `HashMap`) so RON output is in a
    /// deterministic order — important for diff-friendly UI documents and
    /// reproducible RON snapshots in tests.
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub styles: BTreeMap<String, VisualStyle>,
}

impl Theme {
    /// Empty theme — no default fields, no named styles. Every widget under
    /// this theme inherits only the renderer's hard-coded fallback.
    pub const fn new() -> Self {
        Self {
            default: VisualStyle::EMPTY,
            styles: BTreeMap::new(),
        }
    }

    /// Insert (or replace) a named style. Chainable for builder-style theme
    /// construction in tests and examples.
    pub fn with_style(mut self, name: impl Into<String>, style: VisualStyle) -> Self {
        self.styles.insert(name.into(), style);
        self
    }

    /// Replace the project-wide default style.
    pub fn with_default(mut self, default: VisualStyle) -> Self {
        self.default = default;
        self
    }

    /// Resolve the effective visual style for a widget that opts into
    /// `style_ref` (if any) and provides its own `override_with` per-instance
    /// fields.
    ///
    /// Cascade: `self.default` → (`self.styles[style_ref]` if present) →
    /// `override_with`. A missing named style is treated as empty (no
    /// contribution) rather than an error — UI documents stay valid when a
    /// theme is swapped for a smaller one mid-development.
    pub fn resolve(&self, style_ref: Option<&str>, override_with: &VisualStyle) -> VisualStyle {
        let mut resolved = self.default.clone();
        if let Some(name) = style_ref {
            if let Some(named) = self.styles.get(name) {
                resolved = resolved.merged(named);
            }
        }
        resolved.merged(override_with)
    }

    /// Serialize this theme to a pretty-printed RON string.
    pub fn to_ron(&self) -> Result<String, ron::Error> {
        ron::ser::to_string_pretty(self, ron::ser::PrettyConfig::default())
    }

    /// Parse a theme from a RON string.
    pub fn from_ron(text: &str) -> Result<Self, ron::de::SpannedError> {
        ron::de::from_str(text)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::math::Color;
    use crate::ui::visual::{Border, FontRef};

    fn theme_with_three_styles() -> Theme {
        Theme::new()
            .with_default(VisualStyle {
                foreground: Some(Color::BLACK),
                background: Some(Color::WHITE),
                font: Some(FontRef::regular("Inter")),
                font_size: Some(14.0),
                ..VisualStyle::EMPTY
            })
            .with_style(
                "button",
                VisualStyle {
                    background: Some(Color::rgb(0.85, 0.85, 0.9)),
                    border: Some(Border::new(Color::rgb(0.6, 0.6, 0.7), 1.0)),
                    corner_radius: Some(4.0),
                    ..VisualStyle::EMPTY
                },
            )
            .with_style(
                "button-primary",
                VisualStyle {
                    background: Some(Color::rgb(0.2, 0.4, 0.8)),
                    foreground: Some(Color::WHITE),
                    ..VisualStyle::EMPTY
                },
            )
            .with_style(
                "label",
                VisualStyle {
                    foreground: Some(Color::rgb(0.2, 0.2, 0.2)),
                    ..VisualStyle::EMPTY
                },
            )
    }

    #[test]
    fn resolve_returns_default_for_no_style_or_overrides() {
        let theme = theme_with_three_styles();
        let resolved = theme.resolve(None, &VisualStyle::EMPTY);
        assert_eq!(resolved.foreground, Some(Color::BLACK));
        assert_eq!(resolved.background, Some(Color::WHITE));
        assert_eq!(resolved.font_size, Some(14.0));
    }

    #[test]
    fn named_style_overrides_default() {
        let theme = theme_with_three_styles();
        let resolved = theme.resolve(Some("button"), &VisualStyle::EMPTY);
        assert_eq!(resolved.background, Some(Color::rgb(0.85, 0.85, 0.9)));
        // Foreground not set on "button" → kept from default.
        assert_eq!(resolved.foreground, Some(Color::BLACK));
        assert_eq!(resolved.corner_radius, Some(4.0));
    }

    #[test]
    fn per_instance_override_takes_final_precedence() {
        let theme = theme_with_three_styles();
        let overlay = VisualStyle {
            background: Some(Color::RED),
            ..VisualStyle::EMPTY
        };
        let resolved = theme.resolve(Some("button-primary"), &overlay);
        // Per-instance background wins over the named style.
        assert_eq!(resolved.background, Some(Color::RED));
        // The named style's foreground (WHITE) still beats the default (BLACK).
        assert_eq!(resolved.foreground, Some(Color::WHITE));
    }

    #[test]
    fn unknown_named_style_falls_back_to_default() {
        let theme = theme_with_three_styles();
        let resolved = theme.resolve(Some("does-not-exist"), &VisualStyle::EMPTY);
        // Same as resolve(None, &EMPTY).
        assert_eq!(resolved, theme.resolve(None, &VisualStyle::EMPTY));
    }

    #[test]
    fn theme_round_trips_through_ron() {
        let theme = theme_with_three_styles();
        let text = theme.to_ron().unwrap();
        let decoded = Theme::from_ron(&text).unwrap();
        assert_eq!(theme, decoded);
        // Named styles are alphabetised by BTreeMap, so "button" precedes
        // "button-primary" precedes "label" in the serialized form.
        let button_pos = text.find("\"button\"").unwrap();
        let primary_pos = text.find("\"button-primary\"").unwrap();
        let label_pos = text.find("\"label\"").unwrap();
        assert!(button_pos < primary_pos);
        assert!(primary_pos < label_pos);
    }

    #[test]
    fn empty_theme_round_trips_to_empty_ron() {
        let empty = Theme::new();
        let text = empty.to_ron().unwrap();
        let decoded = Theme::from_ron(&text).unwrap();
        assert_eq!(empty, decoded);
        // The empty theme should not mention either field.
        assert!(!text.contains("default:"));
        assert!(!text.contains("styles:"));
    }

    #[test]
    fn builder_chaining_inserts_styles_in_order() {
        let t = Theme::new()
            .with_style("a", VisualStyle::EMPTY)
            .with_style("b", VisualStyle::EMPTY);
        assert_eq!(t.styles.len(), 2);
        assert!(t.styles.contains_key("a"));
        assert!(t.styles.contains_key("b"));
    }
}
