use std::collections::HashMap;

use bone_text::{FontFace, GlyphId, load_font};
use swash::{
    FontRef,
    scale::{Render, ScaleContext, Source, image::Image},
    zeno::Format,
};
use thiserror::Error;

use crate::layout::{LayoutOffset, LayoutPx, LayoutSize};

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct MaskAtlasKey {
    pub face: FontFace,
    pub glyph: GlyphId,
    pub size_px: u16,
}

impl MaskAtlasKey {
    #[must_use]
    pub const fn new(face: FontFace, glyph: GlyphId, size_px: u16) -> Self {
        Self {
            face,
            glyph,
            size_px,
        }
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct MaskAtlasEntry {
    pub uv_min: [f32; 2],
    pub uv_max: [f32; 2],
    pub atlas_origin: [u32; 2],
    pub atlas_size: [u32; 2],
    pub bearing: LayoutOffset,
    pub glyph_size: LayoutSize,
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct MaskAtlasParams {
    pub atlas_extent: u32,
}

impl MaskAtlasParams {
    pub const STANDARD: Self = Self { atlas_extent: 2048 };
}

#[derive(Debug, Error, PartialEq, Eq)]
pub enum MaskAtlasError {
    #[error("glyph id {0} exceeds the swash u16 range")]
    GlyphOutOfRange(u32),
    #[error("tile {tile_width}x{tile_height} exceeds atlas extent {atlas_extent}")]
    TileExceedsAtlas {
        tile_width: u32,
        tile_height: u32,
        atlas_extent: u32,
    },
    #[error("atlas full: no shelf space for {tile_width}x{tile_height} tile")]
    Full { tile_width: u32, tile_height: u32 },
}

pub struct MaskAtlas {
    params: MaskAtlasParams,
    pixels: Vec<u8>,
    sans: FontRef<'static>,
    mono: FontRef<'static>,
    scale_ctx: ScaleContext,
    entries: HashMap<MaskAtlasKey, MaskAtlasEntry>,
    packer: ShelfPacker,
    version: u64,
}

impl MaskAtlas {
    #[must_use]
    pub fn new(params: MaskAtlasParams) -> Self {
        let extent = params.atlas_extent as usize;
        Self {
            params,
            pixels: vec![0u8; extent * extent],
            sans: load_font(FontFace::Sans),
            mono: load_font(FontFace::Mono),
            scale_ctx: ScaleContext::new(),
            entries: HashMap::new(),
            packer: ShelfPacker::new(params.atlas_extent),
            version: 0,
        }
    }

    #[must_use]
    pub fn extent(&self) -> u32 {
        self.params.atlas_extent
    }

    #[must_use]
    pub fn pixels(&self) -> &[u8] {
        &self.pixels
    }

    #[must_use]
    pub fn version(&self) -> u64 {
        self.version
    }

    #[must_use]
    pub fn entries_len(&self) -> usize {
        self.entries.len()
    }

    pub fn ensure(&mut self, key: MaskAtlasKey) -> Result<MaskAtlasEntry, MaskAtlasError> {
        if let Some(entry) = self.entries.get(&key) {
            return Ok(*entry);
        }
        let glyph_id_u16 = u16::try_from(key.glyph.raw())
            .map_err(|_| MaskAtlasError::GlyphOutOfRange(key.glyph.raw()))?;
        let font = match key.face {
            FontFace::Sans => self.sans,
            FontFace::Mono => self.mono,
        };
        let tile = rasterise_glyph_alpha(&font, glyph_id_u16, key.size_px, &mut self.scale_ctx);
        let placed = self
            .packer
            .place(tile.width.max(1), tile.height.max(1))
            .ok_or(MaskAtlasError::Full {
                tile_width: tile.width,
                tile_height: tile.height,
            })?;
        if placed.width > self.params.atlas_extent || placed.height > self.params.atlas_extent {
            return Err(MaskAtlasError::TileExceedsAtlas {
                tile_width: placed.width,
                tile_height: placed.height,
                atlas_extent: self.params.atlas_extent,
            });
        }
        blit_tile(&mut self.pixels, self.params.atlas_extent, placed, &tile);
        let entry = atlas_entry(self.params, placed, &tile);
        self.entries.insert(key, entry);
        self.version = self.version.wrapping_add(1);
        Ok(entry)
    }
}

impl core::fmt::Debug for MaskAtlas {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("MaskAtlas")
            .field("entries", &self.entries.len())
            .field("extent", &self.params.atlas_extent)
            .field("version", &self.version)
            .finish_non_exhaustive()
    }
}

struct GlyphTile {
    pixels: Vec<u8>,
    width: u32,
    height: u32,
    bearing_left: f32,
    bearing_top: f32,
}

fn rasterise_glyph_alpha(
    font: &FontRef<'_>,
    glyph: u16,
    size_px: u16,
    scale_ctx: &mut ScaleContext,
) -> GlyphTile {
    let mut scaler = scale_ctx
        .builder(*font)
        .size(f32::from(size_px))
        .hint(true)
        .build();
    let mut image = Image::new();
    if !Render::new(&[Source::Outline])
        .format(Format::Alpha)
        .render_into(&mut scaler, glyph, &mut image)
    {
        return empty_tile();
    }
    let placement = image.placement;
    #[allow(
        clippy::cast_precision_loss,
        reason = "glyph bearings stay well within f32 mantissa"
    )]
    let bearing_left = placement.left as f32;
    #[allow(
        clippy::cast_precision_loss,
        reason = "glyph bearings stay well within f32 mantissa"
    )]
    let bearing_top = placement.top as f32;
    GlyphTile {
        pixels: image.data,
        width: placement.width,
        height: placement.height,
        bearing_left,
        bearing_top,
    }
}

fn empty_tile() -> GlyphTile {
    GlyphTile {
        pixels: Vec::new(),
        width: 0,
        height: 0,
        bearing_left: 0.0,
        bearing_top: 0.0,
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
struct PlacedTile {
    x: u32,
    y: u32,
    width: u32,
    height: u32,
}

struct ShelfPacker {
    extent: u32,
    cursor_x: u32,
    shelf_y: u32,
    shelf_height: u32,
}

impl ShelfPacker {
    fn new(extent: u32) -> Self {
        Self {
            extent,
            cursor_x: 0,
            shelf_y: 0,
            shelf_height: 0,
        }
    }

    fn place(&mut self, tile_w: u32, tile_h: u32) -> Option<PlacedTile> {
        if tile_w > self.extent || tile_h > self.extent {
            return None;
        }
        if self.cursor_x.saturating_add(tile_w) > self.extent {
            self.shelf_y = self.shelf_y.checked_add(self.shelf_height)?;
            self.cursor_x = 0;
            self.shelf_height = 0;
        }
        if self.shelf_y.saturating_add(tile_h) > self.extent {
            return None;
        }
        let placed = PlacedTile {
            x: self.cursor_x,
            y: self.shelf_y,
            width: tile_w,
            height: tile_h,
        };
        self.cursor_x += tile_w;
        if tile_h > self.shelf_height {
            self.shelf_height = tile_h;
        }
        Some(placed)
    }
}

fn blit_tile(dst: &mut [u8], extent: u32, placed: PlacedTile, tile: &GlyphTile) {
    if tile.pixels.is_empty() {
        return;
    }
    (0..tile.height).for_each(|row| {
        let src_off = (row as usize) * (tile.width as usize);
        let dst_off = ((placed.y + row) as usize) * (extent as usize) + placed.x as usize;
        let len = tile.width as usize;
        dst[dst_off..dst_off + len].copy_from_slice(&tile.pixels[src_off..src_off + len]);
    });
}

fn atlas_entry(
    params: MaskAtlasParams,
    placed: PlacedTile,
    tile: &GlyphTile,
) -> MaskAtlasEntry {
    #[allow(clippy::cast_precision_loss, reason = "extent fits f32 mantissa")]
    let extent = params.atlas_extent as f32;
    #[allow(clippy::cast_precision_loss, reason = "placed coords fit f32 mantissa")]
    let uv_min = [placed.x as f32 / extent, placed.y as f32 / extent];
    #[allow(clippy::cast_precision_loss, reason = "placed coords fit f32 mantissa")]
    let uv_max = [
        (placed.x + placed.width) as f32 / extent,
        (placed.y + placed.height) as f32 / extent,
    ];
    let bearing = LayoutOffset::new(
        LayoutPx::saturating(tile.bearing_left),
        LayoutPx::saturating(tile.bearing_top),
    );
    #[allow(clippy::cast_precision_loss, reason = "tile size fits f32 mantissa")]
    let glyph_size = LayoutSize::new(
        LayoutPx::saturating_nonneg(tile.width as f32),
        LayoutPx::saturating_nonneg(tile.height as f32),
    );
    MaskAtlasEntry {
        uv_min,
        uv_max,
        atlas_origin: [placed.x, placed.y],
        atlas_size: [placed.width, placed.height],
        bearing,
        glyph_size,
    }
}

#[cfg(test)]
mod tests {
    use super::{MaskAtlas, MaskAtlasKey, MaskAtlasParams};
    use bone_text::{FontFace, load_font};

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

    #[test]
    fn fresh_atlas_has_zero_entries() {
        let atlas = MaskAtlas::new(MaskAtlasParams::STANDARD);
        assert_eq!(atlas.entries_len(), 0);
        assert_eq!(atlas.version(), 0);
    }

    fn ensure_or_panic(atlas: &mut MaskAtlas, key: MaskAtlasKey) -> super::MaskAtlasEntry {
        let Ok(entry) = atlas.ensure(key) else {
            panic!("ensure failed for {key:?}");
        };
        entry
    }

    #[test]
    fn ensure_inserts_entry_and_bumps_version() {
        let mut atlas = MaskAtlas::new(MaskAtlasParams::STANDARD);
        let key = MaskAtlasKey::new(FontFace::Sans, glyph_for(FontFace::Sans, 'a'), 12);
        let _ = ensure_or_panic(&mut atlas, key);
        assert_eq!(atlas.entries_len(), 1);
        assert_eq!(atlas.version(), 1);
    }

    #[test]
    fn ensure_idempotent_for_same_key() {
        let mut atlas = MaskAtlas::new(MaskAtlasParams::STANDARD);
        let key = MaskAtlasKey::new(FontFace::Sans, glyph_for(FontFace::Sans, 'a'), 12);
        let first = ensure_or_panic(&mut atlas, key);
        let second = ensure_or_panic(&mut atlas, key);
        assert_eq!(first, second);
        assert_eq!(atlas.version(), 1);
    }

    #[test]
    fn different_sizes_produce_distinct_entries() {
        let mut atlas = MaskAtlas::new(MaskAtlasParams::STANDARD);
        let small = MaskAtlasKey::new(FontFace::Sans, glyph_for(FontFace::Sans, 'a'), 12);
        let large = MaskAtlasKey::new(FontFace::Sans, glyph_for(FontFace::Sans, 'a'), 48);
        let small_entry = ensure_or_panic(&mut atlas, small);
        let large_entry = ensure_or_panic(&mut atlas, large);
        assert!(large_entry.glyph_size.width.value() > small_entry.glyph_size.width.value());
    }
}
