use bone_text::{MaxWidth, ShapeRequest, ShapedText, Shaper};

use crate::strings::{StringKey, StringTable};
use crate::theme::{Theme, TypographyRole};

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum TextRole {
    Body,
    Label,
    Heading,
    Mono,
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub enum TextLayout {
    Line,
    Wrap(MaxWidth),
}

#[derive(Clone, Debug, PartialEq)]
pub struct TextPrimitive {
    pub role: TextRole,
    pub style: TypographyRole,
    pub layout: TextLayout,
    pub shaped: ShapedText,
}

impl TextPrimitive {
    #[must_use]
    pub fn for_role(
        shaper: &mut Shaper,
        theme: &Theme,
        table: &StringTable,
        key: StringKey,
        layout: TextLayout,
        role: TextRole,
    ) -> Self {
        let style = typography_for(theme, role);
        let max_width = match layout {
            TextLayout::Line => None,
            TextLayout::Wrap(w) => Some(w),
        };
        let out = shaper.shape(table.resolve(key), request_for(style, max_width));
        Self {
            role,
            style,
            layout,
            shaped: out,
        }
    }

    #[must_use]
    pub fn body(
        shaper: &mut Shaper,
        theme: &Theme,
        table: &StringTable,
        key: StringKey,
        layout: TextLayout,
    ) -> Self {
        Self::for_role(shaper, theme, table, key, layout, TextRole::Body)
    }

    #[must_use]
    pub fn label(
        shaper: &mut Shaper,
        theme: &Theme,
        table: &StringTable,
        key: StringKey,
        layout: TextLayout,
    ) -> Self {
        Self::for_role(shaper, theme, table, key, layout, TextRole::Label)
    }

    #[must_use]
    pub fn heading(
        shaper: &mut Shaper,
        theme: &Theme,
        table: &StringTable,
        key: StringKey,
        layout: TextLayout,
    ) -> Self {
        Self::for_role(shaper, theme, table, key, layout, TextRole::Heading)
    }

    #[must_use]
    pub fn mono(
        shaper: &mut Shaper,
        theme: &Theme,
        table: &StringTable,
        key: StringKey,
        layout: TextLayout,
    ) -> Self {
        Self::for_role(shaper, theme, table, key, layout, TextRole::Mono)
    }
}

#[must_use]
pub fn request_for(style: TypographyRole, max_width: Option<MaxWidth>) -> ShapeRequest {
    ShapeRequest {
        face: style.face,
        size_px: style.size.as_px_f32(),
        weight: style.weight,
        line_height_px: style.line_height.as_px_f32(),
        letter_spacing_px: style.letter_spacing.as_px_f32(),
        max_width,
    }
}

fn typography_for(theme: &Theme, role: TextRole) -> TypographyRole {
    match role {
        TextRole::Body => theme.typography.body,
        TextRole::Label => theme.typography.label,
        TextRole::Heading => theme.typography.heading,
        TextRole::Mono => theme.typography.mono,
    }
}

#[cfg(test)]
mod tests {
    use super::{TextLayout, TextPrimitive, TextRole, request_for};
    use crate::strings::{StringKey, StringTable};
    use crate::theme::{FontFace, Theme};
    use bone_text::{MaxWidth, Shaper};

    const GREETING: StringKey = StringKey::new("primitive.greeting");
    const LONG: StringKey = StringKey::new("primitive.long");
    const FI: StringKey = StringKey::new("primitive.fi");

    fn fixture() -> (Shaper, Theme, StringTable) {
        let table = StringTable::from_entries([
            (GREETING, "Hello world".to_owned()),
            (
                LONG,
                "the quick brown fox jumps over the lazy dog".to_owned(),
            ),
            (FI, "fi".to_owned()),
        ]);
        (Shaper::new(), Theme::light(), table)
    }

    fn cap_60px() -> MaxWidth {
        let Some(cap) = MaxWidth::new(60.0) else {
            panic!("60px must be a positive width")
        };
        cap
    }

    #[test]
    fn body_carries_body_role_and_style() {
        let (mut shaper, theme, table) = fixture();
        let p = TextPrimitive::body(&mut shaper, &theme, &table, GREETING, TextLayout::Line);
        assert_eq!(p.role, TextRole::Body);
        assert_eq!(p.style, theme.typography.body);
        assert_eq!(p.shaped.face, theme.typography.body.face);
    }

    #[test]
    fn label_carries_label_role_and_style() {
        let (mut shaper, theme, table) = fixture();
        let p = TextPrimitive::label(&mut shaper, &theme, &table, GREETING, TextLayout::Line);
        assert_eq!(p.role, TextRole::Label);
        assert_eq!(p.style, theme.typography.label);
    }

    #[test]
    fn heading_carries_heading_role_and_style() {
        let (mut shaper, theme, table) = fixture();
        let p = TextPrimitive::heading(&mut shaper, &theme, &table, GREETING, TextLayout::Line);
        assert_eq!(p.role, TextRole::Heading);
        assert_eq!(p.style, theme.typography.heading);
    }

    #[test]
    fn mono_carries_mono_role_and_uses_mono_face() {
        let (mut shaper, theme, table) = fixture();
        let p = TextPrimitive::mono(&mut shaper, &theme, &table, FI, TextLayout::Line);
        assert_eq!(p.role, TextRole::Mono);
        assert_eq!(p.style, theme.typography.mono);
        assert_eq!(p.shaped.face, FontFace::Mono);
        assert_eq!(p.shaped.glyph_count(), 2, "mono face must not ligate fi");
    }

    #[test]
    fn single_line_layout_does_not_wrap_long_text() {
        let (mut shaper, theme, table) = fixture();
        let p = TextPrimitive::body(&mut shaper, &theme, &table, LONG, TextLayout::Line);
        assert_eq!(p.layout, TextLayout::Line);
        assert_eq!(p.shaped.line_count(), 1);
    }

    #[test]
    fn wrap_layout_breaks_long_text_into_multiple_lines() {
        let (mut shaper, theme, table) = fixture();
        let cap = cap_60px();
        let p = TextPrimitive::body(&mut shaper, &theme, &table, LONG, TextLayout::Wrap(cap));
        assert_eq!(p.layout, TextLayout::Wrap(cap));
        assert!(
            p.shaped.line_count() >= 2,
            "expected wrap, got {}",
            p.shaped.line_count(),
        );
    }

    #[test]
    fn missing_string_key_falls_back_to_id_and_still_shapes() {
        let mut shaper = Shaper::new();
        let theme = Theme::light();
        let table = StringTable::new();
        let p = TextPrimitive::label(
            &mut shaper,
            &theme,
            &table,
            StringKey::new("absent.key"),
            TextLayout::Line,
        );
        assert!(p.shaped.glyph_count() > 0);
    }

    #[test]
    fn font_size_round_trips_from_theme_to_shaped_text() {
        let (mut shaper, theme, table) = fixture();
        let p = TextPrimitive::heading(&mut shaper, &theme, &table, GREETING, TextLayout::Line);
        assert!((p.shaped.font_size_px - theme.typography.heading.size.as_px_f32()).abs() < 0.01);
    }

    #[test]
    fn distinct_roles_select_distinct_styles() {
        let (mut shaper, theme, table) = fixture();
        let body = TextPrimitive::body(&mut shaper, &theme, &table, GREETING, TextLayout::Line);
        let heading =
            TextPrimitive::heading(&mut shaper, &theme, &table, GREETING, TextLayout::Line);
        assert_ne!(body.style, heading.style);
        assert_ne!(body.role, heading.role);
    }

    #[test]
    fn request_for_round_trips_typography_role() {
        let theme = Theme::light();
        let style = theme.typography.heading;
        let r = request_for(style, None);
        assert_eq!(r.face, style.face);
        assert!((r.size_px - style.size.as_px_f32()).abs() < 1e-3);
        assert_eq!(r.weight, style.weight);
        assert!((r.line_height_px - style.line_height.as_px_f32()).abs() < 1e-3);
        assert!((r.letter_spacing_px - style.letter_spacing.as_px_f32()).abs() < 1e-3);
        assert!(r.max_width.is_none());
    }
}
