use termichart_core::Style;

use crate::cell::CellGrid;

/// The Unicode code-point for the empty braille pattern (U+2800).
const BRAILLE_OFFSET: u32 = 0x2800;

/// Bit position for each dot within a 2x4 braille cell.
///
/// A braille character represents a 2-column by 4-row dot grid. The mapping
/// from `(dx, dy)` within that cell to the bit index is:
///
/// ```text
///   (0,0) -> bit 0    (1,0) -> bit 3
///   (0,1) -> bit 1    (1,1) -> bit 4
///   (0,2) -> bit 2    (1,2) -> bit 5
///   (0,3) -> bit 6    (1,3) -> bit 7
/// ```
const DOT_BITS: [[u8; 4]; 2] = [
    [0, 1, 2, 6], // column 0 (left)
    [3, 4, 5, 7], // column 1 (right)
];

/// A sub-pixel canvas that uses Unicode braille characters for high-resolution
/// rendering.
///
/// Each character cell maps to a 2-wide by 4-tall grid of dots, giving an
/// effective resolution of `(width * 2, height * 4)` "pixels".
#[derive(Debug, Clone)]
pub struct BrailleCanvas {
    /// Width in character cells.
    pub width: u16,
    /// Height in character cells.
    pub height: u16,
    /// Sub-pixel dot grid. Outer index is the y coordinate (`height * 4`),
    /// inner index is the x coordinate (`width * 2`).
    pub dots: Vec<Vec<bool>>,
}

impl BrailleCanvas {
    /// Creates a new canvas with the given character-cell dimensions.
    ///
    /// The sub-pixel dot grid will be `(width * 2)` columns by `(height * 4)`
    /// rows, all initially unset.
    pub fn new(width: u16, height: u16) -> Self {
        let dot_w = (width as usize) * 2;
        let dot_h = (height as usize) * 4;
        Self {
            width,
            height,
            dots: vec![vec![false; dot_w]; dot_h],
        }
    }

    /// The width of the sub-pixel dot grid.
    #[inline]
    pub fn dot_width(&self) -> usize {
        (self.width as usize) * 2
    }

    /// The height of the sub-pixel dot grid.
    #[inline]
    pub fn dot_height(&self) -> usize {
        (self.height as usize) * 4
    }

    /// Sets a single dot at `(x, y)` in the sub-pixel grid.
    ///
    /// Out-of-bounds coordinates are silently ignored.
    pub fn set_dot(&mut self, x: usize, y: usize) {
        if y < self.dot_height() && x < self.dot_width() {
            self.dots[y][x] = true;
        }
    }

    /// Draws a line between two sub-pixel coordinates using Bresenham's line
    /// algorithm.
    pub fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32) {
        let mut x = x0;
        let mut y = y0;

        let dx = (x1 - x0).abs();
        let dy = -(y1 - y0).abs();
        let sx = if x0 < x1 { 1 } else { -1 };
        let sy = if y0 < y1 { 1 } else { -1 };
        let mut err = dx + dy;

        loop {
            if x >= 0 && y >= 0 {
                self.set_dot(x as usize, y as usize);
            }

            if x == x1 && y == y1 {
                break;
            }

            let e2 = 2 * err;

            if e2 >= dy {
                err += dy;
                x += sx;
            }
            if e2 <= dx {
                err += dx;
                y += sy;
            }
        }
    }

    /// Draws a smooth curve through a series of sub-pixel points using
    /// Catmull-Rom spline interpolation.
    ///
    /// Each point is `(x, y)` in the sub-pixel coordinate system. The curve
    /// passes through every point and transitions smoothly between them.
    pub fn draw_smooth_line(&mut self, points: &[(f64, f64)]) {
        if points.len() < 2 {
            return;
        }
        if points.len() == 2 {
            self.draw_line(
                points[0].0 as i32,
                points[0].1 as i32,
                points[1].0 as i32,
                points[1].1 as i32,
            );
            return;
        }

        // For each segment between point[i] and point[i+1], use Catmull-Rom
        // with the surrounding points as control points. Duplicate endpoints
        // for the first and last segment.
        let n = points.len();
        for i in 0..n - 1 {
            let p0 = if i == 0 { points[0] } else { points[i - 1] };
            let p1 = points[i];
            let p2 = points[i + 1];
            let p3 = if i + 2 < n { points[i + 2] } else { points[n - 1] };

            // Number of interpolation steps scales with segment length.
            let dx = p2.0 - p1.0;
            let dy = p2.1 - p1.1;
            let dist = (dx * dx + dy * dy).sqrt();
            let steps = (dist as usize).max(4);

            let mut prev_x = p1.0 as i32;
            let mut prev_y = p1.1 as i32;

            for step in 1..=steps {
                let t = step as f64 / steps as f64;
                let t2 = t * t;
                let t3 = t2 * t;

                // Catmull-Rom spline formula
                let x = 0.5
                    * ((2.0 * p1.0)
                        + (-p0.0 + p2.0) * t
                        + (2.0 * p0.0 - 5.0 * p1.0 + 4.0 * p2.0 - p3.0) * t2
                        + (-p0.0 + 3.0 * p1.0 - 3.0 * p2.0 + p3.0) * t3);
                let y = 0.5
                    * ((2.0 * p1.1)
                        + (-p0.1 + p2.1) * t
                        + (2.0 * p0.1 - 5.0 * p1.1 + 4.0 * p2.1 - p3.1) * t2
                        + (-p0.1 + 3.0 * p1.1 - 3.0 * p2.1 + p3.1) * t3);

                let cur_x = x as i32;
                let cur_y = y as i32;

                self.draw_line(prev_x, prev_y, cur_x, cur_y);

                prev_x = cur_x;
                prev_y = cur_y;
            }
        }
    }

    /// Converts the dot grid into braille characters and writes them into the
    /// given [`CellGrid`] at position `(offset_x, offset_y)` using the specified style.
    pub fn render_to_grid(
        &self,
        grid: &mut CellGrid,
        offset_x: u16,
        offset_y: u16,
        style: Style,
    ) {
        for cy in 0..self.height {
            for cx in 0..self.width {
                let mut bits: u32 = 0;

                // Each character cell covers a 2x4 block of dots.
                for dx in 0..2u16 {
                    for dy in 0..4u16 {
                        let dot_x = (cx as usize) * 2 + (dx as usize);
                        let dot_y = (cy as usize) * 4 + (dy as usize);

                        if dot_y < self.dot_height()
                            && dot_x < self.dot_width()
                            && self.dots[dot_y][dot_x]
                        {
                            bits |= 1 << DOT_BITS[dx as usize][dy as usize];
                        }
                    }
                }

                if bits != 0 {
                    if let Some(ch) = char::from_u32(BRAILLE_OFFSET + bits) {
                        grid.set(cx + offset_x, cy + offset_y, ch, style);
                    }
                }
            }
        }
    }
}
