use std::borrow::Cow;

use bone_render::{ChromeInstance, ConvexInstance, IconInstance, SdfGlyphInstance, StrokeInstance};
use bone_text::{FontFace, FontWeight, ShapeRequest, ShapedLine, ShapedText, Shaper};
use bone_types::IconTile;
use bone_ui::layout::LayoutRect;
use bone_ui::strings::StringTable;
use bone_ui::text::{MaskAtlas, MaskAtlasKey};
use bone_ui::theme::{Border, Color, StrokeWidth, Theme};
use bone_ui::widgets::{
    ConvexPoly, HorizontalAlign, IconTint, PaintPrim, PolyPath, WidgetPaint, lower_paint,
};
use swash::FontRef;

const MARK_FONT_SCALE: f32 = 0.7;

const _: () = assert!(bone_ui::widgets::MAX_CONVEX_VERTS == bone_render::MAX_PLANES);
const _: () = assert!(bone_ui::widgets::MAX_PATH_POINTS < bone_render::MAX_STROKE_POINTS);

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum SpanVertical {
    Baseline,
    RectCenter,
}

#[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,
    pub vertical: SpanVertical,
    pub horizontal: HorizontalAlign,
}

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

#[must_use]
pub fn paint_to_icon_instances(paints: &[WidgetPaint]) -> Vec<IconInstance> {
    paints
        .iter()
        .filter_map(|p| match p {
            WidgetPaint::Icon { rect, icon, tint } => Some(IconInstance::new(
                rect_to_xywh(*rect),
                tile_index(icon.tile()),
                icon_tint_premul(*tint),
            )),
            _ => None,
        })
        .collect()
}

const DISABLED_TINT_VALUE: f32 = 0.75;
const DISABLED_TINT_ALPHA: f32 = 0.5;

fn icon_tint_premul(tint: IconTint) -> [f32; 4] {
    match tint {
        IconTint::Normal => [1.0, 1.0, 1.0, 1.0],
        IconTint::Disabled => {
            let premul = DISABLED_TINT_VALUE * DISABLED_TINT_ALPHA;
            [premul, premul, premul, DISABLED_TINT_ALPHA]
        }
        IconTint::Solid(color) => color.linear_rgba_premul(),
    }
}

fn tile_index(tile: IconTile) -> u32 {
    let Ok(index) = u32::try_from(tile.as_usize()) else {
        panic!("icon tile index {} exceeds u32", tile.as_usize());
    };
    index
}

#[must_use]
pub fn paint_to_convex_instances(paints: &[WidgetPaint]) -> Vec<ConvexInstance> {
    paints
        .iter()
        .filter_map(|p| match p {
            WidgetPaint::ConvexFill { poly, fill, border } => {
                convex_to_instance(poly, *fill, *border)
            }
            _ => None,
        })
        .collect()
}

#[must_use]
pub fn paint_to_stroke_instances(paints: &[WidgetPaint]) -> Vec<StrokeInstance> {
    paints
        .iter()
        .filter_map(|p| match p {
            WidgetPaint::Stroke { path, width, color } => stroke_to_instance(path, *width, *color),
            _ => None,
        })
        .collect()
}

fn convex_to_instance(
    poly: &ConvexPoly,
    fill: Color,
    border: Option<Border>,
) -> Option<ConvexInstance> {
    let bounds = poly.bounds();
    let rect = [
        bounds.origin.x.value() - 1.0,
        bounds.origin.y.value() - 1.0,
        bounds.size.width.value() + 2.0,
        bounds.size.height.value() + 2.0,
    ];
    let (border_premul, border_width) = border.map_or(([0.0; 4], 0.0), |b| {
        (b.color.linear_rgba_premul(), b.width.value_px())
    });
    ConvexInstance::new(
        rect,
        fill.linear_rgba_premul(),
        border_premul,
        border_width,
        &poly.edge_planes(),
    )
}

fn stroke_to_instance(path: &PolyPath, width: StrokeWidth, color: Color) -> Option<StrokeInstance> {
    let half = width.value_px().max(1.0) * 0.5;
    let open: Vec<[f32; 2]> = path
        .points()
        .iter()
        .map(|p| [p.x.value(), p.y.value()])
        .collect();
    let closing = path.is_closed().then(|| open.first().copied()).flatten();
    let points: Vec<[f32; 2]> = open.into_iter().chain(closing).collect();
    StrokeInstance::new(&points, color.linear_rgba_premul(), half)
}

#[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,
                vertical: SpanVertical::Baseline,
                horizontal: HorizontalAlign::Center,
            }),
            WidgetPaint::AlignedLabel {
                rect,
                text,
                color,
                role,
                align,
            } => 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,
                vertical: SpanVertical::Baseline,
                horizontal: *align,
            }),
            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,
                vertical: SpanVertical::RectCenter,
                horizontal: HorizontalAlign::Center,
            }),
            _ => 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 MaskAtlas,
    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
    })
}

const ELLIPSIS: &str = "\u{2026}";

fn push_span_instances(
    out: &mut Vec<SdfGlyphInstance>,
    item: &ChromeTextSpan<'_>,
    atlas: &mut MaskAtlas,
    shaper: &mut Shaper,
    fonts: &ChromeFonts<'_>,
) {
    let request = 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 [rx, ry, rw, rh] = item.rect_px;
    let (_text_for_paint, layout) = fit_with_ellipsis(item.text, rw, shaper, request);
    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 start_x = match item.horizontal {
        HorizontalAlign::Center => rx + ((rw - visible_advance) * 0.5).max(0.0),
        HorizontalAlign::Start => rx,
        HorizontalAlign::End => rx + (rw - visible_advance).max(0.0),
    };
    let line = LineLayout {
        face: item.face,
        size_px: item.font_size_px,
        color: item.color_premul_rgba,
        start_x,
        baseline_y: ry + (rh + metrics.ascent - metrics.descent) * 0.5,
        vertical: item.vertical,
        rect_center_y: ry + rh * 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);
                }
            });
        });
}

fn shape_advance(shaper: &mut Shaper, text: &str, request: ShapeRequest) -> f32 {
    shaper
        .shape(text, request)
        .lines
        .first()
        .map_or(0.0, ShapedLine::visible_advance_px)
}

fn fit_with_ellipsis<'a>(
    text: &'a str,
    max_width_px: f32,
    shaper: &mut Shaper,
    request: ShapeRequest,
) -> (Cow<'a, str>, ShapedText) {
    let initial = shaper.shape(text, request);
    let visible = initial
        .lines
        .first()
        .map_or(0.0, ShapedLine::visible_advance_px);
    if max_width_px <= 0.0 || text.is_empty() || visible <= max_width_px {
        return (Cow::Borrowed(text), initial);
    }
    let ellipsis_advance = shape_advance(shaper, ELLIPSIS, request);
    let target = (max_width_px - ellipsis_advance).max(0.0);
    if target <= 0.0 {
        let layout = shaper.shape(ELLIPSIS, request);
        return (Cow::Owned(ELLIPSIS.to_owned()), layout);
    }
    let char_offsets: Vec<usize> = text
        .char_indices()
        .map(|(i, _)| i)
        .chain(core::iter::once(text.len()))
        .collect();
    let n_chars = char_offsets.len().saturating_sub(1);
    let mut lo = 0usize;
    let mut hi = n_chars;
    while lo < hi {
        let mid = (lo + hi).div_ceil(2);
        let prefix = &text[..char_offsets[mid]];
        let prefix_advance = shape_advance(shaper, prefix, request);
        if prefix_advance <= target {
            lo = mid;
        } else {
            hi = mid.saturating_sub(1);
        }
    }
    let prefix = &text[..char_offsets[lo]];
    let truncated = format!("{prefix}{ELLIPSIS}");
    let layout = shaper.shape(truncated.as_str(), request);
    (Cow::Owned(truncated), layout)
}

#[derive(Copy, Clone)]
struct LineLayout {
    face: FontFace,
    size_px: f32,
    color: [f32; 4],
    start_x: f32,
    baseline_y: f32,
    vertical: SpanVertical,
    rect_center_y: f32,
}

fn build_instance(
    atlas: &mut MaskAtlas,
    line: &LineLayout,
    glyph_id: bone_text::GlyphId,
    advance_x: f32,
) -> Option<SdfGlyphInstance> {
    let size_px = atlas_size_px(line.size_px);
    let key = MaskAtlasKey::new(line.face, glyph_id, size_px);
    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.glyph_size;
    let bearing = entry.bearing;
    let glyph_top_y = match line.vertical {
        SpanVertical::Baseline => line.baseline_y - bearing.dy.value(),
        SpanVertical::RectCenter => line.rect_center_y - extent.height.value() * 0.5,
    };
    let pixel_x = (line.start_x + advance_x + bearing.dx.value()).round();
    let pixel_y = glyph_top_y.round();
    Some(SdfGlyphInstance {
        rect_xywh_px: [
            pixel_x,
            pixel_y,
            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 atlas_size_px(font_size_px: f32) -> u16 {
    #[allow(
        clippy::cast_possible_truncation,
        clippy::cast_sign_loss,
        reason = "chrome font sizes are well within u16 after rounding"
    )]
    let rounded = font_size_px.round().clamp(1.0, f32::from(u16::MAX)) as u16;
    rounded
}

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 = MaskAtlas::new(bone_ui::MaskAtlasParams::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 icon_paints_route_to_icon_instances() {
        let paints = vec![WidgetPaint::Icon {
            rect: rect(4.0, 6.0, 16.0, 16.0),
            icon: bone_types::IconId::Point,
            tint: IconTint::Normal,
        }];
        let instances = paint_to_icon_instances(&paints);
        assert_eq!(instances.len(), 1);
        assert!(rgba_close(
            instances[0].rect_xywh_px,
            [4.0, 6.0, 16.0, 16.0]
        ));
        assert_eq!(instances[0].tile_index, 0);
        assert!(rgba_close(
            instances[0].tint_premul_rgba,
            [1.0, 1.0, 1.0, 1.0]
        ));
    }

    fn rgba_close(a: [f32; 4], b: [f32; 4]) -> bool {
        a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-6)
    }

    #[test]
    fn disabled_icon_tint_dims_and_fades_while_staying_premultiplied() {
        let paints = vec![WidgetPaint::Icon {
            rect: rect(0.0, 0.0, 16.0, 16.0),
            icon: bone_types::IconId::Point,
            tint: IconTint::Disabled,
        }];
        let [r, g, b, a] = paint_to_icon_instances(&paints)[0].tint_premul_rgba;
        assert!(a < 1.0, "disabled icon fades");
        assert!(r < a && g < a && b < a, "disabled icon dims below identity");
    }

    #[test]
    fn icon_paints_drop_from_chrome_rect_path() {
        let theme = Theme::light();
        let paints = vec![
            surface(&theme),
            WidgetPaint::Icon {
                rect: rect(0.0, 0.0, 16.0, 16.0),
                icon: bone_types::IconId::Point,
                tint: IconTint::Normal,
            },
        ];
        let instances = paint_to_instances(&theme, &paints);
        assert_eq!(
            instances.len(),
            1,
            "icon must route to the icon pass, not a chrome placeholder"
        );
    }

    #[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 = MaskAtlas::new(bone_ui::MaskAtlasParams::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 = MaskAtlas::new(bone_ui::MaskAtlasParams::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());
    }
}
