//! Visual style — colors, borders, fonts. The *what does it look like* layer.
//!
//! [`VisualStyle`] is orthogonal to the Stage-8 [`LayoutStyle`](super::style::LayoutStyle):
//! layout decides where a widget *is*; visual decides what it *looks like*.
//! Every field is `Option<T>`. `None` means **inherit** — from a [`Theme`](super::theme::Theme)
//! when present, otherwise from the renderer's hard-coded fallback in piece 4.
//! `Some` means **override**: this widget (or this named theme style) wants
//! exactly this value, regardless of what the theme provides.
//!
//! Why optional fields instead of full values: it lets a tiny per-widget
//! override stay tiny in RON (one line for "button-pressed has a brighter
//! background") without re-stating every color/border/font the theme already
//! provides. The same merging rule works equally well for theme cascades
//! (default → named style → per-instance) and for runtime state changes
//! (hover/focus/press overlays in piece 5).

use serde::{Deserialize, Serialize};

use crate::asset::AssetRef;
use crate::math::Color;

use super::text::Font;

/// Optional per-widget visual properties. `None` on a field means "inherit";
/// `Some` means "override".
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct VisualStyle {
    /// Filled background color drawn behind the widget's `content_rect`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub background: Option<Color>,
    /// Foreground color — text, icons, anything drawn *on top of* the
    /// background.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub foreground: Option<Color>,
    /// Border drawn around the widget's `rect`. `Some(border)` with a
    /// `width <= 0.0` is treated as "no border" by the renderer, the same as
    /// `None`, but the value still serializes — useful for theme overrides
    /// that explicitly *suppress* a border.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub border: Option<Border>,
    /// Corner radius in logical pixels (zero means square). Applies to both
    /// background fill and border.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub corner_radius: Option<f32>,
    /// Font family + weight + italic flag. Piece 3 turns this into a
    /// shaped glyph stream.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub font: Option<FontRef>,
    /// A specific font **asset** to draw with, chosen in the editor's UI canvas
    /// from the project's `fonts/`. When set it takes precedence over the
    /// portable [`font`](Self::font) descriptor (the renderer resolves the
    /// [`AssetRef`] to a loaded face via the asset database); when `None` the
    /// descriptor / theme path applies as before. This is the engine's first
    /// `AssetRef<T>` field — the asset-picker's end-to-end target.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub font_asset: Option<AssetRef<Font>>,
    /// Font size in logical pixels.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub font_size: Option<f32>,
}

impl VisualStyle {
    /// Empty style — every field `None`. Equivalent to [`Default::default`];
    /// `EMPTY` exists as a `const` for places that want it as an associated
    /// constant.
    pub const EMPTY: Self = Self {
        background: None,
        foreground: None,
        border: None,
        corner_radius: None,
        font: None,
        font_asset: None,
        font_size: None,
    };

    /// Returns a style where every `Some` field in `override_with` replaces
    /// the corresponding field in `self`.
    ///
    /// This is the merge primitive themes and runtime state use: build a
    /// resolved style by cascading default → named-style → per-instance →
    /// state-overlay, each call replacing only the fields the caller cared
    /// about.
    pub fn merged(&self, override_with: &VisualStyle) -> VisualStyle {
        VisualStyle {
            background: override_with.background.or(self.background),
            foreground: override_with.foreground.or(self.foreground),
            border: override_with.border.or(self.border),
            corner_radius: override_with.corner_radius.or(self.corner_radius),
            font: override_with.font.clone().or_else(|| self.font.clone()),
            font_asset: override_with.font_asset.or(self.font_asset),
            font_size: override_with.font_size.or(self.font_size),
        }
    }

    /// True if every field is `None`. Handy as a `skip_serializing_if` test
    /// when embedding a `VisualStyle` in a host struct that wants the empty
    /// case to vanish from RON entirely.
    pub fn is_empty(&self) -> bool {
        *self == Self::EMPTY
    }
}

/// Border drawn around a widget's `rect`.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Border {
    pub color: Color,
    pub width: f32,
}

impl Border {
    pub const fn new(color: Color, width: f32) -> Self {
        Self { color, width }
    }
}

/// Reference to a font face the renderer will load and shape with.
///
/// Piece 2 stores the descriptor only; piece 3 (text shaping & glyph atlas)
/// resolves it to an actual loaded face. Keeping the descriptor as plain
/// `family` + `weight` + `italic` (rather than a path or a handle) means UI
/// documents are portable: a theme can ask for `"Inter"` and the runtime can
/// pick the platform's best match for that name without rewriting the
/// document.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct FontRef {
    pub family: String,
    #[serde(default, skip_serializing_if = "FontWeight::is_default")]
    pub weight: FontWeight,
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub italic: bool,
}

impl FontRef {
    /// Regular-weight, upright font of the given family.
    pub fn regular(family: impl Into<String>) -> Self {
        Self {
            family: family.into(),
            weight: FontWeight::Regular,
            italic: false,
        }
    }

    /// Bold-weight, upright font of the given family.
    pub fn bold(family: impl Into<String>) -> Self {
        Self {
            family: family.into(),
            weight: FontWeight::Bold,
            italic: false,
        }
    }
}

/// Font weight — the named buckets the OpenType weight axis snaps to.
///
/// Stored as a discrete enum (rather than a `u16` 100–900) because the
/// editor's style inspector and a hand-edited RON file both want
/// `weight: Bold` to round-trip exactly. Renderers can map each variant to
/// its OpenType weight value in piece 3.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
pub enum FontWeight {
    Thin,
    Light,
    #[default]
    Regular,
    Medium,
    Bold,
    Black,
}

impl FontWeight {
    /// OpenType weight value (100..=900) for this bucket.
    pub fn opentype_value(self) -> u16 {
        match self {
            Self::Thin => 100,
            Self::Light => 300,
            Self::Regular => 400,
            Self::Medium => 500,
            Self::Bold => 700,
            Self::Black => 900,
        }
    }

    fn is_default(&self) -> bool {
        *self == Self::default()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn empty_style_has_no_set_fields() {
        let s = VisualStyle::default();
        assert!(s.is_empty());
        assert_eq!(s, VisualStyle::EMPTY);
    }

    #[test]
    fn merge_overrides_only_set_fields() {
        let base = VisualStyle {
            background: Some(Color::WHITE),
            foreground: Some(Color::BLACK),
            border: Some(Border::new(Color::BLACK, 1.0)),
            corner_radius: Some(4.0),
            font: Some(FontRef::regular("Inter")),
            font_asset: None,
            font_size: Some(14.0),
        };
        let overlay = VisualStyle {
            background: Some(Color::rgb(0.9, 0.9, 0.9)),
            font_size: Some(16.0),
            ..VisualStyle::EMPTY
        };
        let merged = base.merged(&overlay);
        assert_eq!(merged.background, Some(Color::rgb(0.9, 0.9, 0.9))); // overlaid
        assert_eq!(merged.foreground, Some(Color::BLACK)); // kept from base
        assert_eq!(merged.font_size, Some(16.0)); // overlaid
        assert_eq!(merged.corner_radius, Some(4.0)); // kept from base
        assert_eq!(merged.font, Some(FontRef::regular("Inter")));
    }

    #[test]
    fn merge_with_empty_overlay_is_identity() {
        let base = VisualStyle {
            background: Some(Color::WHITE),
            foreground: Some(Color::BLACK),
            ..VisualStyle::EMPTY
        };
        assert_eq!(base.merged(&VisualStyle::EMPTY), base);
    }

    #[test]
    fn merge_into_empty_base_takes_overlay() {
        let overlay = VisualStyle {
            background: Some(Color::RED),
            ..VisualStyle::EMPTY
        };
        assert_eq!(VisualStyle::EMPTY.merged(&overlay), overlay);
    }

    #[test]
    fn font_ref_helpers_match_fields() {
        let r = FontRef::regular("Inter");
        assert_eq!(r.family, "Inter");
        assert_eq!(r.weight, FontWeight::Regular);
        assert!(!r.italic);

        let b = FontRef::bold("Inter");
        assert_eq!(b.weight, FontWeight::Bold);
    }

    #[test]
    fn font_weight_opentype_value() {
        assert_eq!(FontWeight::Thin.opentype_value(), 100);
        assert_eq!(FontWeight::Regular.opentype_value(), 400);
        assert_eq!(FontWeight::Bold.opentype_value(), 700);
        assert_eq!(FontWeight::Black.opentype_value(), 900);
    }

    #[test]
    fn visual_style_round_trips_through_ron_compactly() {
        let s = VisualStyle {
            background: Some(Color::WHITE),
            corner_radius: Some(8.0),
            font: Some(FontRef::bold("Inter")),
            ..VisualStyle::EMPTY
        };
        let text = ron::ser::to_string(&s).unwrap();
        // Fields that are `None` must not appear in the serialized form.
        assert!(!text.contains("foreground"));
        assert!(!text.contains("border"));
        assert!(!text.contains("font_size"));
        let decoded: VisualStyle = ron::de::from_str(&text).unwrap();
        assert_eq!(s, decoded);
    }

    #[test]
    fn font_asset_overrides_and_round_trips() {
        use crate::asset::{AssetRef, AssetUid};

        // An overlay's font_asset replaces the base's, like the other fields.
        let base = VisualStyle {
            font_asset: Some(AssetRef::new(AssetUid(1))),
            ..VisualStyle::EMPTY
        };
        let overlay = VisualStyle {
            font_asset: Some(AssetRef::new(AssetUid(2))),
            ..VisualStyle::EMPTY
        };
        assert_eq!(
            base.merged(&overlay).font_asset,
            Some(AssetRef::new(AssetUid(2)))
        );
        // An empty overlay keeps the base reference (inherit semantics).
        assert_eq!(base.merged(&VisualStyle::EMPTY).font_asset, base.font_asset);

        // Round-trips compactly and is skipped when unset.
        let text = ron::ser::to_string(&base).unwrap();
        assert!(text.contains("font_asset"));
        assert_eq!(ron::de::from_str::<VisualStyle>(&text).unwrap(), base);
        let empty_text = ron::ser::to_string(&VisualStyle::EMPTY).unwrap();
        assert!(!empty_text.contains("font_asset"));
    }

    #[test]
    fn empty_style_round_trips_to_empty_ron() {
        let text = ron::ser::to_string(&VisualStyle::EMPTY).unwrap();
        // No fields set → the struct should serialize to its empty form.
        let decoded: VisualStyle = ron::de::from_str(&text).unwrap();
        assert_eq!(decoded, VisualStyle::EMPTY);
    }

    #[test]
    fn font_ref_defaults_skip_in_ron() {
        let f = FontRef::regular("Inter");
        let text = ron::ser::to_string(&f).unwrap();
        // Regular weight and non-italic should be skipped.
        assert!(!text.contains("Regular"));
        assert!(!text.contains("italic"));
        let decoded: FontRef = ron::de::from_str(&text).unwrap();
        assert_eq!(f, decoded);
    }
}
