use crate::{
    Color,
    utils::{write_bg_color, write_fg_color, write_move_to},
};

/// renders to strings using braille characters
///
/// - sets colors of dots
///
///   NOTE: color works on a "last-wins" strategy. the last color
///   set for a dot within a single terminal cell dictates the
///   color of every dot in the cell
///
/// - renders to a string that the caller can write to their screen
pub struct BrailleCanvas {
    /// (cols, rows) in *cells*
    dimensions: (usize, usize),

    /// (col_offset, row_offset) in *cells*
    offset: (usize, usize),

    /// data is per *dot*
    buffer: Vec<bool>,

    /// data is per *cell*
    colors: Vec<Option<Color>>,

    /// data is per *cell*
    dirty: Vec<bool>,

    /// optional color used for the entire canvas background
    bg_color: Option<Color>,

    /// whether or not the buffer gets cleared after each render
    /// defaults to true
    clear_on_render: bool,
}

impl BrailleCanvas {
    pub fn new(dimensions: (usize, usize), offset: (usize, usize)) -> BrailleCanvas {
        let (rows, cols) = dimensions;

        let buffer = vec![false; 8 * rows * cols];
        let colors = vec![None; rows * cols];
        let dirty = vec![true; rows * cols];

        BrailleCanvas {
            dimensions,
            offset,
            buffer,
            colors,
            dirty,
            bg_color: None,
            clear_on_render: true,
        }
    }

    pub const fn width(&self) -> usize {
        2 * self.dimensions.0
    }

    pub const fn height(&self) -> usize {
        4 * self.dimensions.1
    }

    /// x and y are in canvas coordinates
    /// doesn't write anything if coordinates are outside of the canvas bounds
    pub fn set(&mut self, x: usize, y: usize, color: Color) {
        if x >= self.width() || y >= self.height() {
            return;
        }

        let buffer_idx = y * self.width() + x;
        self.buffer[buffer_idx] = true;

        let cell_idx = (y / 4) * self.dimensions.0 + (x / 2);
        self.colors[cell_idx] = Some(color);
    }

    /// x and y are in canvas coordinates
    /// doesn't write anything if coordinates are outside of the canvas bounds
    pub fn set_f(&mut self, x: f64, y: f64, color: Color) {
        if x < 0. || y < 0. {
            return;
        }

        self.set(x.round() as usize, y.round() as usize, color);
    }

    /// sets a background color for the entire canvas
    pub fn set_bg_color(&mut self, color: Color) {
        self.bg_color = Some(color);
    }

    fn clear_buffer(&mut self) {
        self.buffer.fill(false);
        self.colors.fill(None);
    }

    pub fn set_clear_on_render(&mut self, clear: bool) {
        self.clear_on_render = clear;
    }

    pub fn render_to(&mut self, buf: &mut String) {
        let width = self.width();

        if let Some(bg_color) = self.bg_color {
            write_bg_color(buf, bg_color);
        }

        let mut current_color = None;
        let mut skipping;

        for row in 0..self.dimensions.1 {
            skipping = true;

            for col in 0..self.dimensions.0 {
                // figure out which dots are set inside this cell
                let mut byte = 0;

                // the braille unicode character 0x28XX puts dots based on the bits of the
                // 'XX' bytes, according to this layout:
                //
                // 0 3
                // 1 4
                // 2 5
                // 6 7   <- annoying bottom row
                //
                // so we and in 1's offset by appropriate amounts based on which cells
                // we want turned on
                if self.buffer[4 * row * width + 2 * col] {
                    byte |= 1;
                }
                if self.buffer[4 * row * width + 2 * col + 1] {
                    byte |= 1 << 3;
                }
                if self.buffer[(4 * row + 1) * width + 2 * col] {
                    byte |= 1 << 1;
                }
                if self.buffer[(4 * row + 1) * width + 2 * col + 1] {
                    byte |= 1 << 4;
                }
                if self.buffer[(4 * row + 2) * width + 2 * col] {
                    byte |= 1 << 2;
                }
                if self.buffer[(4 * row + 2) * width + 2 * col + 1] {
                    byte |= 1 << 5;
                }
                if self.buffer[(4 * row + 3) * width + 2 * col] {
                    byte |= 1 << 6;
                }
                if self.buffer[(4 * row + 3) * width + 2 * col + 1] {
                    byte |= 1 << 7;
                }

                // if there are no particles to draw and this cell was *not*
                // rendered to last render, we can skip
                let cell_idx = row * width / 2 + col;
                if byte == 0 && !self.dirty[cell_idx] {
                    skipping = true;
                    continue;
                }

                // emit a move-to seq before writing if we've previously skipped some cells
                if skipping {
                    skipping = false;
                    write_move_to(buf, self.offset.0 + col, self.offset.1 + row);
                }

                // write a color byte if the color has changed in this cell
                let cell_color = self.colors[cell_idx];
                if cell_color != current_color
                    && let Some(cell_color) = cell_color
                {
                    write_fg_color(buf, cell_color);
                    current_color = Some(cell_color);
                }

                if byte == 0 {
                    self.dirty[cell_idx] = false;
                    buf.push(' ');
                } else {
                    self.dirty[cell_idx] = true;
                    buf.push(char::from_u32(0x2800 | byte as u32).unwrap());
                }
            }
        }

        // clear to prepare the next render
        if self.clear_on_render {
            self.clear_buffer();
        }
    }

    pub fn render(&mut self) -> String {
        let mut buf = String::new();
        self.render_to(&mut buf);
        buf
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // strips ANSI escape sequences, returning just the visible characters
    fn visible_chars(output: &str) -> Vec<char> {
        let mut chars = Vec::new();
        let mut in_escape = false;
        for c in output.chars() {
            if c == '\x1b' {
                in_escape = true;
            } else if in_escape {
                if c.is_ascii_alphabetic() {
                    in_escape = false;
                }
            } else {
                chars.push(c);
            }
        }
        chars
    }

    const WHITE: Color = Color::Rgb(255, 255, 255);

    #[test]
    fn empty_canvas_renders_spaces() {
        let mut canvas = BrailleCanvas::new((2, 3), (0, 0));
        let output = canvas.render();
        let chars = visible_chars(&output);
        assert_eq!(chars.len(), 6);
        assert!(chars.iter().all(|&c| c == ' '));
    }

    #[test]
    fn each_dot_maps_to_correct_braille_bit() {
        // dot positions within a cell and their expected braille bits:
        //   (0,0)->0  (1,0)->3
        //   (0,1)->1  (1,1)->4
        //   (0,2)->2  (1,2)->5
        //   (0,3)->6  (1,3)->7
        let expected: [(usize, usize, u32); 8] = [
            (0, 0, 0x01),
            (1, 0, 0x08),
            (0, 1, 0x02),
            (1, 1, 0x10),
            (0, 2, 0x04),
            (1, 2, 0x20),
            (0, 3, 0x40),
            (1, 3, 0x80),
        ];

        for (x, y, bit) in expected {
            let mut canvas = BrailleCanvas::new((1, 1), (0, 0));
            canvas.set(x, y, WHITE);
            let output = canvas.render();
            let chars = visible_chars(&output);
            assert_eq!(
                chars[0],
                char::from_u32(0x2800 | bit).unwrap(),
                "dot ({x}, {y}) should set bit {bit:#04x}"
            );
        }
    }

    #[test]
    fn set_out_of_bounds_does_not_panic() {
        let mut canvas = BrailleCanvas::new((1, 1), (0, 0));
        canvas.set(2, 0, WHITE);
        canvas.set(0, 4, WHITE);
        canvas.set(100, 100, WHITE);
        let chars = visible_chars(&canvas.render());
        assert!(chars.iter().all(|&c| c == ' '));
    }

    #[test]
    fn render_clears_buffer() {
        let mut canvas = BrailleCanvas::new((1, 1), (0, 0));
        canvas.set(0, 0, WHITE);
        let _ = canvas.render();

        // second render without any new sets should be empty
        let chars = visible_chars(&canvas.render());
        assert!(chars.iter().all(|&c| c == ' '));
    }

    #[test]
    fn multiple_dots_combine_in_one_cell() {
        let mut canvas = BrailleCanvas::new((1, 1), (0, 0));
        // top-left and bottom-right dots
        canvas.set(0, 0, WHITE); // bit 0
        canvas.set(1, 3, WHITE); // bit 7
        let chars = visible_chars(&canvas.render());
        assert_eq!(chars[0], char::from_u32(0x2800 | 0x01 | 0x80).unwrap());
    }
}
