use std::collections::HashMap;

use bone_text::{FontFace, GlyphId, load_font, outline_to_path, tessellate_path};
use lyon_tessellation::FillTessellator;
use swash::{
    FontRef,
    scale::{ScaleContext, outline::Outline},
};

use crate::layout::{LayoutOffset, LayoutPos, LayoutPx, LayoutRect, LayoutSize};
use crate::theme::FontSize;

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct GlyphTessellationKey {
    face: FontFace,
    glyph: GlyphId,
    size_px_bits: u32,
}

impl GlyphTessellationKey {
    #[must_use]
    pub fn new(face: FontFace, glyph: GlyphId, size: FontSize) -> Self {
        Self {
            face,
            glyph,
            size_px_bits: size.as_px_f32().to_bits(),
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct TessellatedGlyph {
    pub vertices_px: Vec<LayoutOffset>,
    pub indices: Vec<u32>,
    pub bbox_px: LayoutRect,
}

impl TessellatedGlyph {
    #[must_use]
    pub fn empty() -> Self {
        Self {
            vertices_px: Vec::new(),
            indices: Vec::new(),
            bbox_px: LayoutRect::new(LayoutPos::ORIGIN, LayoutSize::ZERO),
        }
    }

    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.indices.is_empty()
    }
}

pub struct OutlineTessellator {
    sans: FontRef<'static>,
    mono: FontRef<'static>,
    scale_ctx: ScaleContext,
    fill: FillTessellator,
    cache: HashMap<GlyphTessellationKey, TessellatedGlyph>,
}

impl OutlineTessellator {
    #[must_use]
    pub fn new() -> Self {
        Self {
            sans: load_font(FontFace::Sans),
            mono: load_font(FontFace::Mono),
            scale_ctx: ScaleContext::new(),
            fill: FillTessellator::new(),
            cache: HashMap::new(),
        }
    }

    pub fn tessellate(
        &mut self,
        face: FontFace,
        glyph: GlyphId,
        size: FontSize,
    ) -> &TessellatedGlyph {
        let key = GlyphTessellationKey::new(face, glyph, size);
        let font = match face {
            FontFace::Sans => self.sans,
            FontFace::Mono => self.mono,
        };
        self.cache.entry(key).or_insert_with(|| {
            tessellate_glyph(&font, glyph, size, &mut self.scale_ctx, &mut self.fill)
        })
    }
}

impl Default for OutlineTessellator {
    fn default() -> Self {
        Self::new()
    }
}

impl core::fmt::Debug for OutlineTessellator {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("OutlineTessellator")
            .field("cache_len", &self.cache.len())
            .finish_non_exhaustive()
    }
}

fn tessellate_glyph(
    font: &FontRef<'_>,
    glyph: GlyphId,
    size: FontSize,
    scale_ctx: &mut ScaleContext,
    fill: &mut FillTessellator,
) -> TessellatedGlyph {
    let Ok(glyph_id_u16) = u16::try_from(glyph.raw()) else {
        return TessellatedGlyph::empty();
    };
    let mut scaler = scale_ctx.builder(*font).size(size.as_px_f32()).build();
    let mut outline = Outline::new();
    if !scaler.scale_outline_into(glyph_id_u16, &mut outline) {
        return TessellatedGlyph::empty();
    }
    if outline.is_empty() {
        return TessellatedGlyph::empty();
    }
    let bbox_px = outline_bounds(&outline);
    let path = outline_to_path(&outline);
    let Ok(raw) = tessellate_path(&path, fill) else {
        return TessellatedGlyph::empty();
    };
    let vertices_px = raw
        .vertices_px
        .into_iter()
        .map(|[x, y]| LayoutOffset::new(LayoutPx::saturating(x), LayoutPx::saturating(y)))
        .collect();
    TessellatedGlyph {
        vertices_px,
        indices: raw.indices,
        bbox_px,
    }
}

fn outline_bounds(outline: &Outline) -> LayoutRect {
    let bounds = outline.bounds();
    LayoutRect::new(
        LayoutPos::new(
            LayoutPx::saturating(bounds.min.x),
            LayoutPx::saturating(bounds.min.y),
        ),
        LayoutSize::new(
            LayoutPx::saturating_nonneg(bounds.max.x - bounds.min.x),
            LayoutPx::saturating_nonneg(bounds.max.y - bounds.min.y),
        ),
    )
}

#[cfg(test)]
mod tests {
    use super::{GlyphTessellationKey, OutlineTessellator, TessellatedGlyph};
    use crate::theme::FontSize;
    use bone_text::{FontFace, GlyphId, load_font};

    fn glyph_for(face: FontFace, ch: char) -> GlyphId {
        let font = load_font(face);
        GlyphId::new(u32::from(font.charmap().map(u32::from(ch))))
    }

    fn px(v: f64) -> FontSize {
        FontSize::from_px(v)
    }

    #[test]
    fn tessellation_key_hashes_distinct_sizes_independently() {
        let a = GlyphTessellationKey::new(FontFace::Sans, GlyphId::new(7), px(16.0));
        let b = GlyphTessellationKey::new(FontFace::Sans, GlyphId::new(7), px(17.0));
        let c = GlyphTessellationKey::new(FontFace::Sans, GlyphId::new(7), px(16.0));
        assert_ne!(a, b);
        assert_eq!(a, c);
    }

    #[test]
    fn ascii_glyph_produces_triangles_and_positive_bounds() {
        let mut tess = OutlineTessellator::new();
        let glyph = glyph_for(FontFace::Sans, 'A');
        let result = tess.tessellate(FontFace::Sans, glyph, px(32.0)).clone();
        assert!(!result.indices.is_empty(), "A must tessellate to triangles");
        assert!(
            result.indices.len().is_multiple_of(3),
            "lyon emits triangle list"
        );
        assert!(result.bbox_px.size.width.value() > 0.0);
        assert!(result.bbox_px.size.height.value() > 0.0);
    }

    #[test]
    fn space_glyph_returns_empty_geometry() {
        let mut tess = OutlineTessellator::new();
        let glyph = glyph_for(FontFace::Sans, ' ');
        let result = tess.tessellate(FontFace::Sans, glyph, px(32.0)).clone();
        assert!(result.is_empty(), "space has no fill geometry");
    }

    #[test]
    fn unknown_glyph_returns_empty() {
        let mut tess = OutlineTessellator::new();
        let result = tess
            .tessellate(FontFace::Sans, GlyphId::new(0xFFFF), px(32.0))
            .clone();
        assert!(result.is_empty());
    }

    #[test]
    fn glyph_id_outside_u16_returns_empty() {
        let mut tess = OutlineTessellator::new();
        let result = tess
            .tessellate(FontFace::Sans, GlyphId::new(0x0001_0000), px(32.0))
            .clone();
        assert!(result.is_empty());
    }

    #[test]
    fn cache_returns_same_geometry_for_repeated_calls() {
        let mut tess = OutlineTessellator::new();
        let glyph = glyph_for(FontFace::Sans, 'B');
        let first = tess.tessellate(FontFace::Sans, glyph, px(24.0)).clone();
        let second = tess.tessellate(FontFace::Sans, glyph, px(24.0)).clone();
        assert_eq!(first, second);
    }

    #[test]
    fn distinct_sizes_yield_distinct_geometry() {
        let mut tess = OutlineTessellator::new();
        let glyph = glyph_for(FontFace::Mono, 'O');
        let small = tess.tessellate(FontFace::Mono, glyph, px(12.0)).clone();
        let large = tess.tessellate(FontFace::Mono, glyph, px(24.0)).clone();
        assert_ne!(small, large);
    }

    #[test]
    fn larger_size_yields_proportionally_larger_bounds() {
        let mut tess = OutlineTessellator::new();
        let glyph = glyph_for(FontFace::Sans, 'M');
        let small = tess
            .tessellate(FontFace::Sans, glyph, px(16.0))
            .bbox_px
            .size;
        let large = tess
            .tessellate(FontFace::Sans, glyph, px(32.0))
            .bbox_px
            .size;
        assert!(large.width.value() > small.width.value());
        assert!(large.height.value() > small.height.value());
        let ratio_w = large.width.value() / small.width.value();
        assert!(
            (ratio_w - 2.0).abs() < 0.1,
            "scaling 2x must preserve width ratio: got {ratio_w}",
        );
    }

    #[test]
    fn vertex_count_matches_index_consumers() {
        let mut tess = OutlineTessellator::new();
        let glyph = glyph_for(FontFace::Sans, 'g');
        let TessellatedGlyph {
            vertices_px,
            indices,
            ..
        } = tess.tessellate(FontFace::Sans, glyph, px(32.0)).clone();
        let max_idx = indices.iter().copied().max().unwrap_or(0) as usize;
        assert!(
            max_idx < vertices_px.len(),
            "every index must reference a real vertex",
        );
    }

    #[test]
    fn empty_glyph_helper_is_default_state() {
        let g = TessellatedGlyph::empty();
        assert!(g.is_empty());
        assert!(g.vertices_px.is_empty());
        assert!(g.indices.is_empty());
    }

    #[test]
    fn descender_glyph_bbox_extends_below_baseline_in_font_y_up_space() {
        let mut tess = OutlineTessellator::new();
        let glyph = glyph_for(FontFace::Sans, 'g');
        let bbox = tess.tessellate(FontFace::Sans, glyph, px(64.0)).bbox_px;
        assert!(
            bbox.origin.y.value() < 0.0,
            "swash returns Y-up font coords: descender of 'g' must have negative origin.y, got {}",
            bbox.origin.y.value(),
        );
    }

    #[test]
    fn ascender_glyph_bbox_lies_above_baseline_in_font_y_up_space() {
        let mut tess = OutlineTessellator::new();
        let glyph = glyph_for(FontFace::Sans, 'l');
        let bbox = tess.tessellate(FontFace::Sans, glyph, px(64.0)).bbox_px;
        assert!(
            bbox.max_y().value() > 0.0,
            "ascender of 'l' must reach above baseline (max y > 0) in Y-up space, got {}",
            bbox.max_y().value(),
        );
        assert!(
            bbox.min_y().value() >= -1.0,
            "ascender-only glyph 'l' should not extend significantly below baseline, got min y {}",
            bbox.min_y().value(),
        );
    }
}
