use crate::{font::{FontProvider, Glyph}, grid::VoxelGrid, models::VoxelType};

enum GlyphToken {
    Char(Glyph),
    Space(u32),
}

pub enum OutlineMode {
    FourConnected,
    EightConnected,
}

pub struct GenerationOptions {
    pub extrusion_depth: u32,
    pub generate_outline: bool,
    pub outline_mode: OutlineMode,
    pub generate_shadow: bool,
    pub shadow_offset: (i32, i32),
    /// Overrides the font's default letter spacing (None = use font default).
    pub letter_spacing: Option<u32>,
    /// Width in blocks inserted for a space character.
    pub word_spacing: u32,
}

impl Default for GenerationOptions {
    fn default() -> Self {
        Self {
            extrusion_depth: 1,
            generate_outline: true,
            outline_mode: OutlineMode::EightConnected,
            generate_shadow: false,
            shadow_offset: (1, -1),
            letter_spacing: None,
            word_spacing: 4,
        }
    }
}

pub struct TextBuilder<'a> {
    font: &'a dyn FontProvider,
    options: GenerationOptions,
}

impl<'a> TextBuilder<'a> {
    pub fn new(font: &'a dyn FontProvider) -> Self {
        Self {
            font,
            options: GenerationOptions::default(),
        }
    }

    pub fn with_depth(mut self, depth: u32) -> Self {
        self.options.extrusion_depth = depth.max(1);
        self
    }

    pub fn with_outline(mut self, generate: bool) -> Self {
        self.options.generate_outline = generate;
        self
    }

    pub fn with_outline_mode(mut self, mode: OutlineMode) -> Self {
        self.options.outline_mode = mode;
        self
    }

    pub fn with_shadow(mut self, generate: bool) -> Self {
        self.options.generate_shadow = generate;
        self
    }

    pub fn with_shadow_offset(mut self, dx: i32, dy: i32) -> Self {
        self.options.shadow_offset = (dx, dy);
        self
    }

    pub fn with_letter_spacing(mut self, spacing: u32) -> Self {
        self.options.letter_spacing = Some(spacing);
        self
    }

    pub fn with_word_spacing(mut self, spacing: u32) -> Self {
        self.options.word_spacing = spacing;
        self
    }

    pub fn generate(&self, text: &str) -> VoxelGrid {
        let (tokens, text_width, max_height) = self.collect_glyphs(text);
        let (base_x, base_y, grid_w, grid_h) = self.compute_layout(text_width, max_height);
        let mut grid = VoxelGrid::new(grid_w, grid_h, self.options.extrusion_depth);
        self.place_glyphs(&mut grid, &tokens, base_x, base_y);
        if self.options.generate_shadow {
            self.apply_shadow(&mut grid);
        }
        if self.options.generate_outline {
            self.apply_outline(&mut grid);
        }
        grid
    }

    fn effective_letter_spacing(&self) -> u32 {
        self.options.letter_spacing.unwrap_or_else(|| self.font.letter_spacing())
    }

    fn collect_glyphs(&self, text: &str) -> (Vec<GlyphToken>, u32, u32) {
        let letter_spacing = self.effective_letter_spacing();
        let mut total_width = 0;
        let mut max_height = 0;
        let mut tokens = Vec::new();
        for c in text.chars() {
            if c == ' ' {
                total_width += self.options.word_spacing;
                tokens.push(GlyphToken::Space(self.options.word_spacing));
            } else if let Some(glyph) = self.font.get_glyph(c) {
                total_width += glyph.width + letter_spacing;
                max_height = max_height.max(glyph.height);
                tokens.push(GlyphToken::Char(glyph));
            }
        }
        (tokens, total_width, max_height)
    }

    fn compute_layout(&self, text_width: u32, max_height: u32) -> (u32, u32, u32, u32) {
        let outline_pad = if self.options.generate_outline { 1u32 } else { 0 };
        let (sdx, sdy) = self.options.shadow_offset;
        let shadow_x_extra = if self.options.generate_shadow { sdx.max(0) as u32 } else { 0 };
        let shadow_y_extra = if self.options.generate_shadow { (-sdy).max(0) as u32 } else { 0 };

        let grid_w = text_width + outline_pad * 2 + shadow_x_extra;
        let grid_h = max_height + outline_pad * 2 + shadow_y_extra;
        let base_x = outline_pad;
        let base_y = outline_pad + shadow_y_extra;

        (base_x, base_y, grid_w, grid_h)
    }

    fn place_glyphs(&self, grid: &mut VoxelGrid, tokens: &[GlyphToken], base_x: u32, base_y: u32) {
        let letter_spacing = self.effective_letter_spacing();
        let mut current_x = base_x;
        for token in tokens {
            match token {
                GlyphToken::Space(w) => current_x += w,
                GlyphToken::Char(glyph) => {
                    for gy in 0..glyph.height {
                        for gx in 0..glyph.width {
                            if glyph.data[(gy * glyph.width + gx) as usize] {
                                let wx = current_x + gx;
                                let wy = base_y + (glyph.height - 1 - gy); // flip vertically
                                for z in 0..self.options.extrusion_depth {
                                    if let Err(e) = grid.set(wx, wy, z, VoxelType::Body) {
                                        tracing::warn!("failed to set voxel: {}", e);
                                    }
                                }
                            }
                        }
                    }
                    current_x += glyph.width + letter_spacing;
                }
            }
        }
    }

    fn apply_shadow(&self, grid: &mut VoxelGrid) {
        let (dx, dy) = self.options.shadow_offset;
        let (w, h, d) = (grid.width, grid.height, grid.depth);
        let body_positions: Vec<(u32, u32, u32)> = (0..w)
            .flat_map(|x| (0..h).flat_map(move |y| (0..d).map(move |z| (x, y, z))))
            .filter(|&(x, y, z)| grid.get(x, y, z) == Some(VoxelType::Body))
            .collect();

        for (x, y, z) in body_positions {
            let sx = x as i32 + dx;
            let sy = y as i32 + dy;
            if sx >= 0 && sy >= 0 {
                let (sx, sy) = (sx as u32, sy as u32);
                if grid.get(sx, sy, z).is_none() {
                    if let Err(e) = grid.set(sx, sy, z, VoxelType::Shadow) {
                        tracing::warn!("failed to set shadow voxel: {}", e);
                    }
                }
            }
        }
    }

    fn apply_outline(&self, grid: &mut VoxelGrid) {
        let (w, h, d) = (grid.width, grid.height, grid.depth);
        let body_xy: Vec<(u32, u32)> = (0..w)
            .flat_map(|x| (0..h).map(move |y| (x, y)))
            .filter(|&(x, y)| (0..d).any(|z| grid.get(x, y, z) == Some(VoxelType::Body)))
            .collect();

        for (x, y) in body_xy {
            for (ox, oy) in Self::neighbor_offsets(&self.options.outline_mode) {
                let nx = x as i32 + ox;
                let ny = y as i32 + oy;
                if nx >= 0 && ny >= 0 {
                    let (nx, ny) = (nx as u32, ny as u32);
                    for z in 0..d {
                        if grid.get(nx, ny, z) != Some(VoxelType::Body) {
                            if let Err(e) = grid.set(nx, ny, z, VoxelType::Outline) {
                                tracing::warn!("failed to set outline voxel: {}", e);
                            }
                        }
                    }
                }
            }
        }
    }

    fn neighbor_offsets(mode: &OutlineMode) -> &'static [(i32, i32)] {
        match mode {
            OutlineMode::FourConnected => &[(-1, 0), (1, 0), (0, -1), (0, 1)],
            OutlineMode::EightConnected => &[
                (-1, -1), (0, -1), (1, -1),
                (-1,  0),          (1,  0),
                (-1,  1), (0,  1), (1,  1),
            ],
        }
    }
}
