//! Braille character rendering for ball visualization.
//!
//! This module provides the core rendering functionality using Unicode Braille
//! characters to achieve sub-pixel resolution. Each terminal character cell
//! represents a 2x4 grid of dots, allowing 8 balls to be displayed per cell.
//!
//! # Braille Character Layout
//!
//! A Braille character represents a 2x4 grid of dots:
//! ```text
//! [0,0] [1,0]   (dots 1, 4)
//! [0,1] [1,1]   (dots 2, 5)
//! [0,2] [1,2]   (dots 3, 6)
//! [0,3] [1,3]   (dots 7, 8)
//! ```
//!
//! The Unicode codepoint is calculated as:
//! `U+2800 + (bit pattern where bit N corresponds to dot N+1)`
//!
//! # Bit-to-Dot Mapping (0-indexed bits, 1-indexed dots)
//!
//! - Bit 0 -> Dot 1 (position [0,0])
//! - Bit 1 -> Dot 2 (position [0,1])
//! - Bit 2 -> Dot 3 (position [0,2])
//! - Bit 3 -> Dot 4 (position [1,0])
//! - Bit 4 -> Dot 5 (position [1,1])
//! - Bit 5 -> Dot 6 (position [1,2])
//! - Bit 6 -> Dot 7 (position [0,3])
//! - Bit 7 -> Dot 8 (position [1,3])

use std::sync::atomic::{AtomicU16, AtomicU8, Ordering};

use rayon::prelude::*;

use crate::physics::BallColor;

/// Braille character base codepoint (U+2800 = empty Braille pattern).
const BRAILLE_BASE: u32 = 0x2800;

/// Lookup table mapping (sub_x, sub_y) position to bit mask.
///
/// The index is calculated as: `sub_y * 2 + sub_x`
/// This gives us direct O(1) lookup for any sub-pixel position.
///
/// Grid layout with corresponding bit values:
/// ```text
/// 0b0000_0001  0b0000_1000   (bits 0, 3)
/// 0b0000_0010  0b0001_0000   (bits 1, 4)
/// 0b0000_0100  0b0010_0000   (bits 2, 5)
/// 0b0100_0000  0b1000_0000   (bits 6, 7)
/// ```
const BRAILLE_DOT_BITS: [u8; 8] = [
    0b0000_0001, // [0,0] -> bit 0 (dot 1)
    0b0000_0010, // [0,1] -> bit 1 (dot 2)
    0b0000_0100, // [0,2] -> bit 2 (dot 3)
    0b0100_0000, // [0,3] -> bit 6 (dot 7)
    0b0000_1000, // [1,0] -> bit 3 (dot 4)
    0b0001_0000, // [1,1] -> bit 4 (dot 5)
    0b0010_0000, // [1,2] -> bit 5 (dot 6)
    0b1000_0000, // [1,3] -> bit 7 (dot 8)
];

/// Number of ball colors (White + 6 geyser colors).
pub const NUM_BALL_COLORS: usize = 7;

/// Data for a single Braille character cell.
///
/// Stores both the dot pattern (which sub-pixels have balls) and
/// the total ball count (for density-based coloring), plus color
/// counts for Color Mode rendering.
#[derive(Debug, Clone, Copy, Default)]
pub struct CellData {
    /// Bit pattern for dots (0-255).
    /// Each bit corresponds to one of the 8 possible dot positions.
    pub dot_bits: u8,

    /// Total count of balls in this cell.
    /// Used for density-based background coloring.
    /// May exceed 8 if multiple balls occupy the same sub-pixel.
    pub ball_count: u16,

    /// Count of balls per color in this cell.
    /// Index 0 = White, 1 = Red, 2 = Green, 3 = Yellow, 4 = Blue, 5 = Magenta, 6 = Cyan.
    /// Used for Color Mode to determine the dominant color.
    pub color_counts: [u16; NUM_BALL_COLORS],
}

impl CellData {
    /// Returns the dominant (most common) ball color in this cell.
    ///
    /// If the cell is empty or all balls are white, returns White.
    /// Otherwise, returns the non-white color with the highest count.
    pub fn dominant_color(&self) -> BallColor {
        // Find the color with the maximum count
        // Prefer non-white colors when there's a tie
        let mut max_count = 0u16;
        let mut max_index = 0usize;

        for (i, &count) in self.color_counts.iter().enumerate() {
            // Prefer non-white colors (index > 0) by using > for white and >= for others
            if i == 0 {
                if count > max_count {
                    max_count = count;
                    max_index = i;
                }
            } else if count >= max_count && count > 0 {
                max_count = count;
                max_index = i;
            }
        }

        BallColor::from_index(max_index)
    }
}

/// Atomic version of CellData for thread-safe parallel updates.
///
/// Used during parallel batch plotting to allow multiple threads
/// to update cells concurrently without data races.
struct AtomicCellData {
    dot_bits: AtomicU8,
    ball_count: AtomicU16,
    /// Atomic color counts for each ball color.
    color_counts: [AtomicU16; NUM_BALL_COLORS],
}

impl Default for AtomicCellData {
    fn default() -> Self {
        Self {
            dot_bits: AtomicU8::new(0),
            ball_count: AtomicU16::new(0),
            color_counts: [
                AtomicU16::new(0),
                AtomicU16::new(0),
                AtomicU16::new(0),
                AtomicU16::new(0),
                AtomicU16::new(0),
                AtomicU16::new(0),
                AtomicU16::new(0),
            ],
        }
    }
}

/// Canvas for rendering balls as Braille characters.
///
/// Maintains a grid of cells where each cell tracks:
/// - Which of the 8 sub-positions contain balls (dot_bits)
/// - Total ball count for density coloring (ball_count)
///
/// The canvas provides both single-threaded and parallel batch
/// plotting methods for efficient rendering of thousands of balls.
pub struct BrailleCanvas {
    /// Width in terminal columns.
    width: u16,

    /// Height in terminal rows (excluding status bar rows).
    height: u16,

    /// Cell data stored in row-major order: index = y * width + x.
    cells: Vec<CellData>,

    /// Atomic cells for parallel updates.
    /// Lazily initialized when plot_batch_parallel is called.
    atomic_cells: Vec<AtomicCellData>,
}

impl BrailleCanvas {
    /// Creates a new canvas with the given terminal dimensions.
    ///
    /// # Arguments
    ///
    /// * `width` - Canvas width in terminal columns
    /// * `height` - Canvas height in terminal rows
    ///
    /// # Returns
    ///
    /// A new `BrailleCanvas` with all cells cleared.
    pub fn new(width: u16, height: u16) -> Self {
        let size = (width as usize) * (height as usize);
        Self {
            width,
            height,
            cells: vec![CellData::default(); size],
            atomic_cells: (0..size).map(|_| AtomicCellData::default()).collect(),
        }
    }

    /// Clears all cells for a new frame.
    ///
    /// Resets dot patterns, ball counts, and color counts to zero.
    /// Must be called at the start of each render frame.
    pub fn clear(&mut self) {
        for cell in &mut self.cells {
            cell.dot_bits = 0;
            cell.ball_count = 0;
            cell.color_counts = [0; NUM_BALL_COLORS];
        }
        // Also clear atomic cells for parallel operations
        for cell in &self.atomic_cells {
            cell.dot_bits.store(0, Ordering::Relaxed);
            cell.ball_count.store(0, Ordering::Relaxed);
            for color_count in &cell.color_counts {
                color_count.store(0, Ordering::Relaxed);
            }
        }
    }

    /// Plots a ball at the given sub-pixel coordinates (single-threaded).
    ///
    /// # Arguments
    ///
    /// * `sub_x` - X coordinate in sub-pixels (0 to width*2 - 1)
    /// * `sub_y` - Y coordinate in sub-pixels (0 to height*4 - 1)
    ///
    /// Sub-pixel resolution is 2x horizontal, 4x vertical per character.
    /// Coordinates outside the canvas bounds are ignored.
    pub fn plot(&mut self, sub_x: u32, sub_y: u32) {
        self.plot_with_color(sub_x, sub_y, BallColor::White);
    }

    /// Plots a ball at the given sub-pixel coordinates with a specific color.
    ///
    /// # Arguments
    ///
    /// * `sub_x` - X coordinate in sub-pixels (0 to width*2 - 1)
    /// * `sub_y` - Y coordinate in sub-pixels (0 to height*4 - 1)
    /// * `color` - The ball's color for Color Mode tracking
    ///
    /// Sub-pixel resolution is 2x horizontal, 4x vertical per character.
    /// Coordinates outside the canvas bounds are ignored.
    pub fn plot_with_color(&mut self, sub_x: u32, sub_y: u32, color: BallColor) {
        // Calculate terminal cell coordinates
        let cell_x = (sub_x / 2) as u16;
        let cell_y = (sub_y / 4) as u16;

        // Bounds check
        if cell_x >= self.width || cell_y >= self.height {
            return;
        }

        // Calculate position within the 2x4 cell grid
        let local_x = (sub_x % 2) as usize;
        let local_y = (sub_y % 4) as usize;

        // Get bit mask from lookup table
        let bit_index = local_y * 2 + local_x;
        let dot_bit = BRAILLE_DOT_BITS[bit_index];

        // Update cell
        let idx = (cell_y as usize) * (self.width as usize) + (cell_x as usize);
        self.cells[idx].dot_bits |= dot_bit;
        self.cells[idx].ball_count += 1;
        self.cells[idx].color_counts[color.index()] += 1;
    }

    /// Batch plots multiple balls using parallel iteration.
    ///
    /// More efficient than individual `plot()` calls for thousands of balls.
    /// Uses atomic operations for thread-safe concurrent cell updates.
    /// All balls are plotted with white color.
    ///
    /// # Arguments
    ///
    /// * `positions` - Slice of (sub_x, sub_y) positions to plot
    pub fn plot_batch_parallel(&self, positions: &[(u32, u32)]) {
        let width = self.width as u32;
        let height = self.height as u32;
        let stride = self.width as usize;

        positions.par_iter().for_each(|&(sub_x, sub_y)| {
            // Calculate terminal cell coordinates
            let cell_x = sub_x / 2;
            let cell_y = sub_y / 4;

            // Bounds check
            if cell_x >= width || cell_y >= height {
                return;
            }

            // Calculate position within the 2x4 cell grid
            let local_x = (sub_x % 2) as usize;
            let local_y = (sub_y % 4) as usize;

            // Get bit mask from lookup table
            let bit_index = local_y * 2 + local_x;
            let dot_bit = BRAILLE_DOT_BITS[bit_index];

            // Atomic update
            let idx = (cell_y as usize) * stride + (cell_x as usize);
            self.atomic_cells[idx]
                .dot_bits
                .fetch_or(dot_bit, Ordering::Relaxed);
            self.atomic_cells[idx]
                .ball_count
                .fetch_add(1, Ordering::Relaxed);
            // Default white color
            self.atomic_cells[idx].color_counts[0].fetch_add(1, Ordering::Relaxed);
        });
    }

    /// Batch plots multiple balls with colors using parallel iteration.
    ///
    /// More efficient than individual `plot_with_color()` calls for thousands of balls.
    /// Uses atomic operations for thread-safe concurrent cell updates.
    ///
    /// # Arguments
    ///
    /// * `positions` - Slice of (sub_x, sub_y, color) positions with colors to plot
    pub fn plot_batch_parallel_with_colors(&self, positions: &[(u32, u32, BallColor)]) {
        let width = self.width as u32;
        let height = self.height as u32;
        let stride = self.width as usize;

        positions.par_iter().for_each(|&(sub_x, sub_y, color)| {
            // Calculate terminal cell coordinates
            let cell_x = sub_x / 2;
            let cell_y = sub_y / 4;

            // Bounds check
            if cell_x >= width || cell_y >= height {
                return;
            }

            // Calculate position within the 2x4 cell grid
            let local_x = (sub_x % 2) as usize;
            let local_y = (sub_y % 4) as usize;

            // Get bit mask from lookup table
            let bit_index = local_y * 2 + local_x;
            let dot_bit = BRAILLE_DOT_BITS[bit_index];

            // Atomic update
            let idx = (cell_y as usize) * stride + (cell_x as usize);
            self.atomic_cells[idx]
                .dot_bits
                .fetch_or(dot_bit, Ordering::Relaxed);
            self.atomic_cells[idx]
                .ball_count
                .fetch_add(1, Ordering::Relaxed);
            self.atomic_cells[idx].color_counts[color.index()].fetch_add(1, Ordering::Relaxed);
        });
    }

    /// Copies atomic cell data to regular cells after parallel plotting.
    ///
    /// Must be called after `plot_batch_parallel` or `plot_batch_parallel_with_colors`
    /// and before reading cells.
    pub fn sync_from_atomic(&mut self) {
        for (cell, atomic) in self.cells.iter_mut().zip(self.atomic_cells.iter()) {
            cell.dot_bits = atomic.dot_bits.load(Ordering::Relaxed);
            cell.ball_count = atomic.ball_count.load(Ordering::Relaxed);
            for (i, count) in cell.color_counts.iter_mut().enumerate() {
                *count = atomic.color_counts[i].load(Ordering::Relaxed);
            }
        }
    }

    /// Gets the Braille character for a cell.
    ///
    /// # Arguments
    ///
    /// * `col` - Column index (0 to width-1)
    /// * `row` - Row index (0 to height-1)
    ///
    /// # Returns
    ///
    /// The Unicode Braille character representing the cell's dot pattern.
    /// Returns an empty Braille character (U+2800) for out-of-bounds cells.
    pub fn get_char(&self, col: u16, row: u16) -> char {
        if col >= self.width || row >= self.height {
            return char::from_u32(BRAILLE_BASE).unwrap_or(' ');
        }

        let idx = (row as usize) * (self.width as usize) + (col as usize);
        let dot_bits = self.cells[idx].dot_bits;

        // Convert bit pattern to Unicode Braille character
        // Safety: BRAILLE_BASE + any u8 value is always a valid Unicode codepoint
        char::from_u32(BRAILLE_BASE + u32::from(dot_bits)).unwrap_or(' ')
    }

    /// Gets the ball count for a cell (for density coloring).
    ///
    /// # Arguments
    ///
    /// * `col` - Column index (0 to width-1)
    /// * `row` - Row index (0 to height-1)
    ///
    /// # Returns
    ///
    /// The number of balls in the cell, or 0 for out-of-bounds.
    pub fn get_ball_count(&self, col: u16, row: u16) -> u16 {
        if col >= self.width || row >= self.height {
            return 0;
        }

        let idx = (row as usize) * (self.width as usize) + (col as usize);
        self.cells[idx].ball_count
    }

    /// Gets the dominant (most common) ball color for a cell.
    ///
    /// # Arguments
    ///
    /// * `col` - Column index (0 to width-1)
    /// * `row` - Row index (0 to height-1)
    ///
    /// # Returns
    ///
    /// The dominant ball color, or White if out-of-bounds or no colored balls.
    pub fn get_dominant_color(&self, col: u16, row: u16) -> BallColor {
        if col >= self.width || row >= self.height {
            return BallColor::White;
        }

        let idx = (row as usize) * (self.width as usize) + (col as usize);
        self.cells[idx].dominant_color()
    }

    /// Gets the cell data at the specified position.
    ///
    /// # Arguments
    ///
    /// * `col` - Column index
    /// * `row` - Row index
    ///
    /// # Returns
    ///
    /// Reference to the cell data, or None if out of bounds.
    pub fn get_cell(&self, col: u16, row: u16) -> Option<&CellData> {
        if col >= self.width || row >= self.height {
            return None;
        }
        let idx = (row as usize) * (self.width as usize) + (col as usize);
        self.cells.get(idx)
    }

    /// Resizes the canvas to new dimensions.
    ///
    /// All existing cell data is cleared.
    ///
    /// # Arguments
    ///
    /// * `width` - New width in terminal columns
    /// * `height` - New height in terminal rows
    pub fn resize(&mut self, width: u16, height: u16) {
        let new_size = (width as usize) * (height as usize);

        // Resize vectors
        self.cells.resize(new_size, CellData::default());
        self.atomic_cells
            .resize_with(new_size, AtomicCellData::default);

        self.width = width;
        self.height = height;

        // Clear all cells
        self.clear();
    }

    /// Returns the canvas dimensions.
    ///
    /// # Returns
    ///
    /// `(width, height)` in terminal cells.
    pub fn dimensions(&self) -> (u16, u16) {
        (self.width, self.height)
    }

    /// Returns the sub-pixel resolution of the canvas.
    ///
    /// # Returns
    ///
    /// `(sub_width, sub_height)` - total sub-pixel dimensions.
    /// sub_width = width * 2, sub_height = height * 4
    pub fn subpixel_dimensions(&self) -> (u32, u32) {
        ((self.width as u32) * 2, (self.height as u32) * 4)
    }
}

/// Converts physics coordinates to sub-pixel coordinates.
///
/// Handles the coordinate system transformation:
/// - Physics uses Y-up convention (0 at bottom)
/// - Terminal uses Y-down convention (0 at top)
/// - Sub-pixel resolution is 2x horizontal, 4x vertical
///
/// # Arguments
///
/// * `physics_x`, `physics_y` - Position in physics world coordinates
/// * `world_width`, `world_height` - Physics world dimensions
/// * `canvas_width`, `canvas_height` - Canvas dimensions in terminal cells
///
/// # Returns
///
/// `(sub_x, sub_y)` in sub-pixel coordinates suitable for `BrailleCanvas::plot()`.
pub fn physics_to_subpixel(
    physics_x: f32,
    physics_y: f32,
    world_width: f32,
    world_height: f32,
    canvas_width: u16,
    canvas_height: u16,
) -> (u32, u32) {
    // Sub-pixel resolution: 2x width, 4x height
    let sub_width = (canvas_width as u32) * 2;
    let sub_height = (canvas_height as u32) * 4;

    // Normalize to 0.0-1.0 range
    let norm_x = physics_x / world_width;
    // Flip Y: physics has Y-up, terminal has Y-down
    let norm_y = 1.0 - (physics_y / world_height);

    // Scale to sub-pixel coordinates and clamp to valid range
    let sub_x = (norm_x * sub_width as f32).clamp(0.0, (sub_width.saturating_sub(1)) as f32) as u32;
    let sub_y =
        (norm_y * sub_height as f32).clamp(0.0, (sub_height.saturating_sub(1)) as f32) as u32;

    (sub_x, sub_y)
}

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

    #[test]
    fn test_braille_dot_mapping() {
        // Verify the lookup table produces correct bit patterns
        assert_eq!(BRAILLE_DOT_BITS[0], 0b0000_0001); // [0,0] -> dot 1
        assert_eq!(BRAILLE_DOT_BITS[4], 0b0000_1000); // [1,0] -> dot 4
        assert_eq!(BRAILLE_DOT_BITS[3], 0b0100_0000); // [0,3] -> dot 7
        assert_eq!(BRAILLE_DOT_BITS[7], 0b1000_0000); // [1,3] -> dot 8
    }

    #[test]
    fn test_braille_character_generation() {
        let mut canvas = BrailleCanvas::new(10, 10);

        // Plot a single dot at [0,0] of first cell
        canvas.plot(0, 0);

        let ch = canvas.get_char(0, 0);
        // Should produce U+2801 (Braille pattern with dot 1)
        assert_eq!(ch, '\u{2801}');
    }

    #[test]
    fn test_full_braille_character() {
        let mut canvas = BrailleCanvas::new(10, 10);

        // Plot all 8 dots in first cell
        for sub_x in 0..2 {
            for sub_y in 0..4 {
                canvas.plot(sub_x, sub_y);
            }
        }

        let ch = canvas.get_char(0, 0);
        // Should produce U+28FF (all 8 dots)
        assert_eq!(ch, '\u{28FF}');
    }

    #[test]
    fn test_physics_to_subpixel_origin() {
        // Bottom-left of physics world should map to bottom of canvas
        let (sub_x, sub_y) = physics_to_subpixel(0.0, 0.0, 10.0, 10.0, 10, 10);
        assert_eq!(sub_x, 0);
        // Y is flipped, so physics y=0 maps to bottom of canvas (sub_y = max)
        assert_eq!(sub_y, 39); // 10 * 4 - 1 = 39
    }

    #[test]
    fn test_physics_to_subpixel_top() {
        // Top of physics world should map to top of canvas
        let (sub_x, sub_y) = physics_to_subpixel(0.0, 10.0, 10.0, 10.0, 10, 10);
        assert_eq!(sub_x, 0);
        assert_eq!(sub_y, 0);
    }

    #[test]
    fn test_ball_count() {
        let mut canvas = BrailleCanvas::new(10, 10);

        // Plot same position multiple times
        canvas.plot(0, 0);
        canvas.plot(0, 0);
        canvas.plot(0, 0);

        // Ball count should be 3, even though only 1 dot is visible
        assert_eq!(canvas.get_ball_count(0, 0), 3);
    }
}
