ballin 0.1.2

A colorful interactive physics simulator with thousands of balls, but in your terminal.
Documentation
//! Shape type definitions and core structures.
//!
//! This module defines the fundamental types for the shape system including
//! the shape type enum, color abstraction, and the Shape struct itself.

use rapier2d::prelude::{ColliderHandle, RigidBodyHandle};
use ratatui::style::Color;

/// Available shape types for placement in the simulation.
///
/// Each shape has a unique ASCII art representation and corresponding
/// physics collider. The 6 shapes are displayed in a 3x2 grid:
/// ```text
/// Circle    Triangle  Square
/// Star      Line      VertLine
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ShapeType {
    /// Circular shape rendered with curved ASCII characters.
    Circle,
    /// Equilateral triangle pointing upward.
    Triangle,
    /// Four-sided square shape.
    Square,
    /// Five-pointed star shape.
    Star,
    /// Horizontal line (5 wide x 2 tall).
    LineStraight,
    /// Vertical line (2 wide x 5 tall).
    /// Internally implemented as LineStraight rotated 90 degrees.
    LineVertical,
}

impl ShapeType {
    /// Returns all available shape types in display order.
    ///
    /// Order matches the 3x2 grid layout:
    /// ```text
    /// Circle    Triangle  Square
    /// Star      Line      VertLine
    /// ```
    pub fn all() -> [ShapeType; 6] {
        [
            ShapeType::Circle,
            ShapeType::Triangle,
            ShapeType::Square,
            ShapeType::Star,
            ShapeType::LineStraight,
            ShapeType::LineVertical,
        ]
    }

    pub fn name(&self) -> &'static str {
        match self {
            ShapeType::Circle => "Circle",
            ShapeType::Triangle => "Triangle",
            ShapeType::Square => "Square",
            ShapeType::Star => "Star",
            ShapeType::LineStraight => "Line",
            ShapeType::LineVertical => "VertLine",
        }
    }

    pub fn short_name(&self) -> char {
        match self {
            ShapeType::Circle => 'O',
            ShapeType::Triangle => 'A',
            ShapeType::Square => '#',
            ShapeType::Star => '*',
            ShapeType::LineStraight => '-',
            ShapeType::LineVertical => '|',
        }
    }

    /// Returns the shape type at the given grid index (0-5).
    ///
    /// Grid layout (3x2):
    /// ```text
    /// 0  1  2
    /// 3  4  5
    /// ```
    pub fn from_grid_index(index: usize) -> Option<ShapeType> {
        ShapeType::all().get(index).copied()
    }
}

/// Color abstraction for shapes.
///
/// Provides a layer of abstraction for shape colors to support
/// color customization features. Colors cycle in order:
/// Red, Green, Yellow, Blue, Magenta, Cyan.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ShapeColor {
    /// The ratatui color for rendering.
    color: Color,
}

impl ShapeColor {
    /// The ordered list of colors that shapes can cycle through.
    /// Order: Red, Green, Yellow, Blue, Magenta, Cyan.
    pub const COLORS: [Color; 6] = [
        Color::Red,
        Color::Green,
        Color::Yellow,
        Color::Blue,
        Color::Magenta,
        Color::Cyan,
    ];

    pub fn new(color: Color) -> Self {
        Self { color }
    }

    pub fn random() -> Self {
        use rand::Rng;
        let mut rng = rand::thread_rng();
        let idx = rng.gen_range(0..Self::COLORS.len());
        Self {
            color: Self::COLORS[idx],
        }
    }

    pub fn green() -> Self {
        Self {
            color: Color::Green,
        }
    }

    /// Returns a brighter variant for selected shapes.
    pub fn highlighted(&self) -> Color {
        match self.color {
            Color::Green => Color::LightGreen,
            Color::Red => Color::LightRed,
            Color::Blue => Color::LightBlue,
            Color::Yellow => Color::LightYellow,
            Color::Magenta => Color::LightMagenta,
            Color::Cyan => Color::LightCyan,
            other => other,
        }
    }

    pub fn color(&self) -> Color {
        self.color
    }

    /// Order: Red -> Green -> Yellow -> Blue -> Magenta -> Cyan -> Red...
    pub fn cycle_forward(&mut self) {
        let current_idx = Self::COLORS
            .iter()
            .position(|&c| c == self.color)
            .unwrap_or(0);
        let next_idx = (current_idx + 1) % Self::COLORS.len();
        self.color = Self::COLORS[next_idx];
    }

    /// Order: Red -> Cyan -> Magenta -> Blue -> Yellow -> Green -> Red...
    pub fn cycle_backward(&mut self) {
        let current_idx = Self::COLORS
            .iter()
            .position(|&c| c == self.color)
            .unwrap_or(0);
        let prev_idx = if current_idx == 0 {
            Self::COLORS.len() - 1
        } else {
            current_idx - 1
        };
        self.color = Self::COLORS[prev_idx];
    }
}

impl Default for ShapeColor {
    fn default() -> Self {
        Self {
            color: Color::Green,
        }
    }
}

/// Polygonal collider that interacts with balls.
/// Maintains absolute position independent of window resizing.
#[derive(Debug, Clone)]
pub struct Shape {
    id: u32,
    shape_type: ShapeType,
    /// Position in physics coordinates (center of shape).
    position: (f32, f32),
    /// Rotation angle in degrees (0, 90, 180, 270).
    rotation_degrees: i32,
    color: ShapeColor,
    selected: bool,
    rigid_body_handle: Option<RigidBodyHandle>,
    collider_handle: Option<ColliderHandle>,
}

impl Shape {
    /// Creates a new shape at the given position with a random color.
    ///
    /// # Arguments
    ///
    /// * `id` - Unique identifier for this shape
    /// * `shape_type` - The type of shape to create
    /// * `x` - X position in physics coordinates
    /// * `y` - Y position in physics coordinates
    pub fn new(id: u32, shape_type: ShapeType, x: f32, y: f32) -> Self {
        Self {
            id,
            shape_type,
            position: (x, y),
            rotation_degrees: 0,
            color: ShapeColor::random(),
            selected: false,
            rigid_body_handle: None,
            collider_handle: None,
        }
    }

    pub fn id(&self) -> u32 {
        self.id
    }

    pub fn shape_type(&self) -> ShapeType {
        self.shape_type
    }

    pub fn position(&self) -> (f32, f32) {
        self.position
    }

    pub fn set_position(&mut self, x: f32, y: f32) {
        self.position = (x, y);
    }

    pub fn rotation_degrees(&self) -> i32 {
        self.rotation_degrees
    }

    pub fn rotation_radians(&self) -> f32 {
        (self.rotation_degrees as f32).to_radians()
    }

    /// Rotates clockwise by 90 degrees.
    pub fn rotate_clockwise(&mut self) {
        self.rotation_degrees = (self.rotation_degrees + 90) % 360;
    }

    /// Rotates counter-clockwise by 90 degrees.
    pub fn rotate_counter_clockwise(&mut self) {
        self.rotation_degrees = (self.rotation_degrees - 90 + 360) % 360;
    }

    pub fn color(&self) -> &ShapeColor {
        &self.color
    }

    pub fn set_color(&mut self, color: ShapeColor) {
        self.color = color;
    }

    pub fn cycle_color_forward(&mut self) {
        self.color.cycle_forward();
    }

    pub fn cycle_color_backward(&mut self) {
        self.color.cycle_backward();
    }

    pub fn is_selected(&self) -> bool {
        self.selected
    }

    pub fn set_selected(&mut self, selected: bool) {
        self.selected = selected;
    }

    pub fn rigid_body_handle(&self) -> Option<RigidBodyHandle> {
        self.rigid_body_handle
    }

    pub fn collider_handle(&self) -> Option<ColliderHandle> {
        self.collider_handle
    }

    pub fn set_physics_handles(
        &mut self,
        body_handle: RigidBodyHandle,
        collider_handle: ColliderHandle,
    ) {
        self.rigid_body_handle = Some(body_handle);
        self.collider_handle = Some(collider_handle);
    }

    pub fn clear_physics_handles(&mut self) {
        self.rigid_body_handle = None;
        self.collider_handle = None;
    }

    /// Returns rendering color based on selection state (highlighted if selected).
    pub fn render_color(&self) -> Color {
        if self.selected {
            self.color.highlighted()
        } else {
            self.color.color()
        }
    }
}

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

    #[test]
    fn test_shape_type_all() {
        let types = ShapeType::all();
        assert_eq!(types.len(), 6);
        assert_eq!(types[0], ShapeType::Circle);
        assert_eq!(types[5], ShapeType::LineVertical);
    }

    #[test]
    fn test_shape_rotation() {
        let mut shape = Shape::new(1, ShapeType::Square, 10.0, 10.0);
        assert_eq!(shape.rotation_degrees(), 0);

        shape.rotate_clockwise();
        assert_eq!(shape.rotation_degrees(), 90);

        shape.rotate_clockwise();
        assert_eq!(shape.rotation_degrees(), 180);

        shape.rotate_counter_clockwise();
        assert_eq!(shape.rotation_degrees(), 90);

        // Test wrap-around
        shape.rotation_degrees = 270;
        shape.rotate_clockwise();
        assert_eq!(shape.rotation_degrees(), 0);
    }

    #[test]
    fn test_shape_color_default() {
        let color = ShapeColor::default();
        assert_eq!(color.color(), Color::Green);
    }

    #[test]
    fn test_shape_selection() {
        let mut shape = Shape::new(1, ShapeType::Circle, 5.0, 5.0);
        assert!(!shape.is_selected());
        // Shape has a random color, just verify it's one of the valid colors
        let color = shape.render_color();
        assert!(ShapeColor::COLORS.contains(&color));

        // Set to known color for selection test
        shape.set_color(ShapeColor::green());
        assert_eq!(shape.render_color(), Color::Green);

        shape.set_selected(true);
        assert!(shape.is_selected());
        assert_eq!(shape.render_color(), Color::LightGreen);
    }

    #[test]
    fn test_shape_color_cycling() {
        let mut color = ShapeColor::new(Color::Red);
        assert_eq!(color.color(), Color::Red);

        color.cycle_forward();
        assert_eq!(color.color(), Color::Green);

        color.cycle_forward();
        assert_eq!(color.color(), Color::Yellow);

        color.cycle_backward();
        assert_eq!(color.color(), Color::Green);

        // Test wrap around
        color = ShapeColor::new(Color::Cyan);
        color.cycle_forward();
        assert_eq!(color.color(), Color::Red);

        color.cycle_backward();
        assert_eq!(color.color(), Color::Cyan);
    }
}