use bone_render::{ChromeInstance, SdfGlyphInstance};
use bone_text::{FontFace, FontWeight, ShapeRequest, ShapedLine, Shaper};
use bone_ui::layout::LayoutRect;
use bone_ui::strings::StringTable;
use bone_ui::text::{GlyphAtlasKey, SdfAtlas};
use bone_ui::theme::{Color, FontSize, Theme};
use bone_ui::widgets::{PaintPrim, WidgetPaint, lower_paint};
use swash::FontRef;

const MARK_FONT_SCALE: f32 = 0.7;

#[derive(Clone, Debug)]
pub struct ChromeTextSpan<'a> {
    pub rect_px: [f32; 4],
    pub text: &'a str,
    pub color_premul_rgba: [f32; 4],
    pub font_size_px: f32,
    pub face: FontFace,
    pub weight: FontWeight,
}

#[must_use]
pub fn paint_to_instances(theme: &Theme, paints: &[WidgetPaint]) -> Vec<ChromeInstance> {
    paints
        .iter()
        .filter(|p| !matches!(p, WidgetPaint::Label { .. } | WidgetPaint::Mark { .. }))
        .map(|p| prim_to_instance(&lower_paint(theme, p)))
        .collect()
}

#[must_use]
pub fn paint_to_text_spans<'a>(
    paints: &'a [WidgetPaint],
    strings: &'a StringTable,
) -> Vec<ChromeTextSpan<'a>> {
    paints
        .iter()
        .filter_map(|p| match p {
            WidgetPaint::Label {
                rect,
                text,
                color,
                role,
            } => Some(ChromeTextSpan {
                rect_px: rect_to_xywh(*rect),
                text: text.resolve(strings),
                color_premul_rgba: color.linear_rgba_premul(),
                font_size_px: role.size.as_px_f32(),
                face: role.face,
                weight: role.weight,
            }),
            WidgetPaint::Mark { rect, kind, color } => Some(ChromeTextSpan {
                rect_px: rect_to_xywh(*rect),
                text: kind.glyph(),
                color_premul_rgba: color.linear_rgba_premul(),
                font_size_px: rect.size.height.value() * MARK_FONT_SCALE,
                face: FontFace::Sans,
                weight: FontWeight::Regular,
            }),
            _ => None,
        })
        .collect()
}

struct ChromeFonts<'a> {
    sans_font: &'a FontRef<'static>,
    mono_font: &'a FontRef<'static>,
}

impl<'a> ChromeFonts<'a> {
    fn for_face(&self, face: FontFace) -> &'a FontRef<'static> {
        match face {
            FontFace::Sans => self.sans_font,
            FontFace::Mono => self.mono_font,
        }
    }
}

#[must_use]
pub fn build_glyph_instances(
    spans: &[ChromeTextSpan<'_>],
    atlas: &mut SdfAtlas,
    shaper: &mut Shaper,
    sans_font: &FontRef<'static>,
    mono_font: &FontRef<'static>,
) -> Vec<SdfGlyphInstance> {
    let fonts = ChromeFonts {
        sans_font,
        mono_font,
    };
    spans.iter().fold(Vec::new(), |mut acc, item| {
        push_span_instances(&mut acc, item, atlas, shaper, &fonts);
        acc
    })
}

fn push_span_instances(
    out: &mut Vec<SdfGlyphInstance>,
    item: &ChromeTextSpan<'_>,
    atlas: &mut SdfAtlas,
    shaper: &mut Shaper,
    fonts: &ChromeFonts<'_>,
) {
    let layout = shaper.shape(
        item.text,
        ShapeRequest {
            face: item.face,
            size_px: item.font_size_px,
            weight: item.weight,
            line_height_px: 0.0,
            letter_spacing_px: 0.0,
            max_width: None,
        },
    );
    let metrics = fonts
        .for_face(item.face)
        .metrics(&[])
        .scale(item.font_size_px);
    let visible_advance = layout
        .lines
        .first()
        .map_or(0.0, ShapedLine::visible_advance_px);
    let [rx, ry, rw, rh] = item.rect_px;
    let line = LineLayout {
        face: item.face,
        target: FontSize::from_px(f64::from(item.font_size_px)),
        color: item.color_premul_rgba,
        start_x: rx + ((rw - visible_advance) * 0.5).max(0.0),
        baseline_y: ry + (rh + metrics.cap_height) * 0.5,
    };
    layout
        .lines
        .iter()
        .flat_map(|l| l.runs.iter())
        .for_each(|run| {
            let run_origin = run.origin_x_px;
            run.glyphs.iter().for_each(|g| {
                if let Some(instance) = build_instance(atlas, &line, g.id, run_origin + g.x_px) {
                    out.push(instance);
                }
            });
        });
}

#[derive(Copy, Clone)]
struct LineLayout {
    face: FontFace,
    target: FontSize,
    color: [f32; 4],
    start_x: f32,
    baseline_y: f32,
}

fn build_instance(
    atlas: &mut SdfAtlas,
    line: &LineLayout,
    glyph_id: bone_text::GlyphId,
    advance_x: f32,
) -> Option<SdfGlyphInstance> {
    let key = GlyphAtlasKey::new(line.face, glyph_id);
    let entry = match atlas.ensure(key) {
        Ok(e) => e,
        Err(e) => {
            tracing::warn!(error = %e, face = ?line.face, "glyph atlas ensure failed");
            return None;
        }
    };
    let extent = entry.display_extent(line.target);
    let bearing = entry.display_bearing(line.target);
    Some(SdfGlyphInstance {
        rect_xywh_px: [
            line.start_x + advance_x + bearing.dx.value(),
            line.baseline_y - bearing.dy.value(),
            extent.width.value(),
            extent.height.value(),
        ],
        uv_min: entry.uv_min,
        uv_max: entry.uv_max,
        color_premul_rgba: line.color,
    })
}

fn prim_to_instance(prim: &PaintPrim) -> ChromeInstance {
    let (border_color, thickness_px) = prim
        .border
        .map_or((Color::TRANSPARENT, 0.0), |b| (b.color, b.width.value_px()));
    ChromeInstance::new(
        rect_to_xywh(prim.rect),
        prim.fill.linear_rgba_premul(),
        border_color.linear_rgba_premul(),
        [thickness_px, prim.radius.value_px()],
    )
}

fn rect_to_xywh(rect: LayoutRect) -> [f32; 4] {
    [
        rect.origin.x.value(),
        rect.origin.y.value(),
        rect.size.width.value(),
        rect.size.height.value(),
    ]
}

#[cfg(test)]
mod tests {
    use super::*;
    use bone_ui::layout::{LayoutPos, LayoutPx, LayoutSize};
    use bone_ui::strings::StringKey;
    use bone_ui::widgets::{GlyphMark, LabelText};

    fn rect(x: f32, y: f32, w: f32, h: f32) -> LayoutRect {
        LayoutRect::new(
            LayoutPos::new(LayoutPx::new(x), LayoutPx::new(y)),
            LayoutSize::new(LayoutPx::new(w), LayoutPx::new(h)),
        )
    }

    fn label(text: LabelText, theme: &Theme) -> WidgetPaint {
        WidgetPaint::Label {
            rect: rect(10.0, 20.0, 80.0, 14.0),
            text,
            color: theme.colors.text_secondary(),
            role: theme.typography.caption,
        }
    }

    fn surface(theme: &Theme) -> WidgetPaint {
        WidgetPaint::Surface {
            rect: rect(0.0, 0.0, 100.0, 100.0),
            fill: theme.colors.surface(theme.elevation.level1.surface),
            border: None,
            radius: theme.radius.none,
            elevation: None,
        }
    }

    #[test]
    fn rect_path_drops_label_paints() {
        let theme = Theme::light();
        let paints = vec![
            surface(&theme),
            label(LabelText::Owned("hello".to_owned()), &theme),
        ];
        let instances = paint_to_instances(&theme, &paints);
        assert_eq!(instances.len(), 1);
    }

    #[test]
    fn text_span_path_skips_non_label_paints() {
        let theme = Theme::light();
        let paints = vec![
            surface(&theme),
            label(LabelText::Owned("hello".to_owned()), &theme),
        ];
        let spans = paint_to_text_spans(&paints, StringTable::empty());
        assert_eq!(spans.len(), 1);
        assert_eq!(spans[0].text, "hello");
    }

    #[test]
    fn text_span_resolves_string_keys_through_table() {
        let theme = Theme::light();
        let key = StringKey::new("test.label");
        let paints = vec![label(LabelText::Key(key), &theme)];
        let table = StringTable::from_entries([(key, "Resolved".to_owned())]);
        let spans = paint_to_text_spans(&paints, &table);
        assert_eq!(spans.len(), 1);
        assert_eq!(spans[0].text, "Resolved");
    }

    #[test]
    fn text_span_carries_face_and_weight_from_role() {
        let theme = Theme::light();
        let paints = vec![label(LabelText::Owned("x".to_owned()), &theme)];
        let spans = paint_to_text_spans(&paints, StringTable::empty());
        assert_eq!(spans.len(), 1);
        assert_eq!(spans[0].face, theme.typography.caption.face);
        assert_eq!(spans[0].weight, theme.typography.caption.weight);
        assert!((spans[0].font_size_px - theme.typography.caption.size.as_px_f32()).abs() < 1e-6);
    }

    #[test]
    fn build_glyph_instances_emits_one_quad_per_visible_glyph() {
        let theme = Theme::light();
        let paints = vec![label(LabelText::Owned("Hi".to_owned()), &theme)];
        let spans = paint_to_text_spans(&paints, StringTable::empty());
        let mut atlas = SdfAtlas::new(bone_ui::SdfAtlasParams::STANDARD);
        let mut shaper = Shaper::new();
        let sans_font = bone_text::load_font(FontFace::Sans);
        let mono_font = bone_text::load_font(FontFace::Mono);
        let instances =
            build_glyph_instances(&spans, &mut atlas, &mut shaper, &sans_font, &mono_font);
        assert_eq!(instances.len(), 2, "Hi has two glyphs");
    }

    fn mark(kind: GlyphMark, theme: &Theme) -> WidgetPaint {
        WidgetPaint::Mark {
            rect: rect(40.0, 60.0, 16.0, 16.0),
            kind,
            color: theme.colors.text_secondary(),
        }
    }

    #[test]
    fn rect_path_drops_mark_paints() {
        let theme = Theme::light();
        let paints = vec![surface(&theme), mark(GlyphMark::Ellipsis, &theme)];
        let instances = paint_to_instances(&theme, &paints);
        assert_eq!(
            instances.len(),
            1,
            "Mark must not render as a placeholder square"
        );
    }

    #[test]
    fn text_span_path_includes_mark_glyphs() {
        let theme = Theme::light();
        let paints = vec![mark(GlyphMark::Ellipsis, &theme)];
        let spans = paint_to_text_spans(&paints, StringTable::empty());
        assert_eq!(spans.len(), 1);
        assert_eq!(spans[0].text, "\u{2026}");
    }

    #[test]
    fn build_glyph_instances_renders_mark_glyphs_via_atlas() {
        let theme = Theme::light();
        let paints = vec![mark(GlyphMark::Ellipsis, &theme)];
        let spans = paint_to_text_spans(&paints, StringTable::empty());
        let mut atlas = SdfAtlas::new(bone_ui::SdfAtlasParams::STANDARD);
        let mut shaper = Shaper::new();
        let sans_font = bone_text::load_font(FontFace::Sans);
        let mono_font = bone_text::load_font(FontFace::Mono);
        let instances =
            build_glyph_instances(&spans, &mut atlas, &mut shaper, &sans_font, &mono_font);
        assert_eq!(
            instances.len(),
            1,
            "Ellipsis is one BMP codepoint, expected one quad"
        );
    }

    #[test]
    fn build_glyph_instances_is_empty_for_no_spans() {
        let mut atlas = SdfAtlas::new(bone_ui::SdfAtlasParams::STANDARD);
        let mut shaper = Shaper::new();
        let sans_font = bone_text::load_font(FontFace::Sans);
        let mono_font = bone_text::load_font(FontFace::Mono);
        let instances = build_glyph_instances(&[], &mut atlas, &mut shaper, &sans_font, &mono_font);
        assert!(instances.is_empty());
    }
}
