//! Application state and lifecycle management.
//!
//! This module contains the main `App` struct that coordinates all
//! subsystems: physics, rendering, UI, event handling, and shape management.

use std::time::Instant;

use rand::Rng;
use rapier2d::prelude::{nalgebra, vector};
use ratatui::{
    layout::{Constraint, Direction, Layout, Rect},
    style::Style,
    text::Span,
    widgets::Paragraph,
    Frame,
};

use crate::error::AppResult;
use crate::event::{AppEvent, EventContext};
use crate::physics::{BallColor, PhysicsConfig, PhysicsWorld};
use crate::render::{density_to_color, density_to_foreground, physics_to_subpixel, BrailleCanvas};
use crate::save::{apply_options_to_menu, options_from_menu, LevelConfig, SavedShape};
use crate::shapes::{
    ascii_art::{get_ascii_art, get_rotated_ascii_art},
    ShapeManager,
};
use crate::timing::FrameTimer;
use crate::ui::{HelpMenu, OptionsMenu, ShapeMenu, StatusBar, StatusBarInfo};

/// Physics timestep for fixed-step integration.
const PHYSICS_DT: f32 = 1.0 / 60.0;

/// Maximum physics steps per frame to prevent spiral of death.
const MAX_PHYSICS_STEPS: u32 = 5;

/// Number of balls to spawn per click.
const BALLS_PER_SPAWN: usize = 10;

/// Burst effect strength (reduced by 25% from original 50.0).
const BURST_STRENGTH: f32 = 37.5;

/// Burst effect radius.
const BURST_RADIUS: f32 = 5.0;

/// Maximum number of balls allowed in the simulation.
pub const MAX_BALLS: usize = 15000;

/// Key release detection timeout in milliseconds.
/// If no key repeat event is received within this time, consider the key released.
/// This is needed because terminals don't send key-up events.
const KEY_RELEASE_TIMEOUT_MS: u128 = 100;

/// Calculates spawn rate (balls per second) based on hold duration.
/// Ramps smoothly from 5/sec initially to 300/sec max.
fn calculate_spawn_rate(elapsed_secs: f32) -> f32 {
    if elapsed_secs < 0.5 {
        5.0
    } else if elapsed_secs < 1.5 {
        5.0 + (elapsed_secs - 0.5) * 25.0
    } else if elapsed_secs < 3.0 {
        30.0 + (elapsed_secs - 1.5) * (70.0 / 1.5)
    } else {
        (100.0 * (1.3_f32).powf(elapsed_secs - 3.0)).min(300.0)
    }
}

/// Spawns balls in a grid pattern to avoid physics solver instability from overlap.
fn spawn_balls_in_grid(
    physics_world: &mut PhysicsWorld,
    count: usize,
    world_width: f32,
    world_height: f32,
    spawn_color_percent: i32,
) {
    if count == 0 {
        return;
    }

    let ball_radius = physics_world.config().ball_radius;
    let ideal_spacing = ball_radius * 1.5;
    let cols_ideal = (world_width / ideal_spacing).floor() as usize;
    let rows_ideal = (world_height / ideal_spacing).floor() as usize;
    let max_ideal = cols_ideal * rows_ideal;

    let (cols, rows, spacing_x, spacing_y) = if count <= max_ideal {
        let aspect = world_width / world_height;
        let rows = ((count as f32 / aspect).sqrt().ceil() as usize).max(1);
        let cols = ((count as f32 / rows as f32).ceil() as usize).max(1);
        let spacing_x = world_width / (cols as f32 + 1.0);
        let spacing_y = world_height / (rows as f32 + 1.0);
        (cols, rows, spacing_x, spacing_y)
    } else {
        let aspect = world_width / world_height;
        let rows = ((count as f32 / aspect).sqrt().ceil() as usize).max(1);
        let cols = ((count as f32 / rows as f32).ceil() as usize).max(1);
        let spacing_x = (world_width / (cols as f32 + 1.0)).max(ball_radius * 2.0);
        let spacing_y = (world_height / (rows as f32 + 1.0)).max(ball_radius * 2.0);
        (cols, rows, spacing_x, spacing_y)
    };

    let mut spawned = 0;
    let mut rng = rand::thread_rng();
    for row in 0..rows {
        if spawned >= count {
            break;
        }
        for col in 0..cols {
            if spawned >= count {
                break;
            }
            let x = spacing_x * (col as f32 + 0.5);
            let y = spacing_y * (row as f32 + 0.5);
            let color = if spawn_color_percent > 0 && rng.gen_range(0..100) < spawn_color_percent {
                BallColor::random_color()
            } else {
                BallColor::White
            };

            let _ = physics_world.spawn_ball_with_velocity_and_color(x, y, 0.0, 0.0, color);
            spawned += 1;
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UiState {
    Simulation,
    OptionsMenu,
    ShapeMenu,
    FileExplorer,
    HelpMenu,
}

/// Main application state coordinating physics, rendering, UI, and events.
pub struct App {
    physics_world: PhysicsWorld,
    terminal_size: (u16, u16),
    last_resize_time: Instant,
    prev_terminal_size: (u16, u16),
    running: bool,
    ui_state: UiState,
    options_menu: OptionsMenu,
    shape_menu: ShapeMenu,
    help_menu: HelpMenu,
    shape_manager: ShapeManager,
    frame_timer: FrameTimer,
    canvas: BrailleCanvas,
    physics_config: PhysicsConfig,
    target_ball_count: usize,
    ball_count_on_menu_open: usize,
    mouse_held: bool,
    mouse_position: Option<(f32, f32)>,
    is_in_spawn_zone: bool,
    spawn_hold_start: Option<Instant>,
    space_held: bool,
    space_hold_start: Option<Instant>,
    mouse_spawn_accumulator: f32,
    space_spawn_accumulator: f32,
    /// Terminals don't send key-up events, so we detect release by timeout.
    last_space_event: Option<Instant>,
    held_spawn_section: Option<u8>,
    section_hold_start: Option<Instant>,
    last_section_event: Option<Instant>,
    section_spawn_accumulator: f32,
    /// Each element holds the activation time if that geyser (1-6) is active.
    active_geysers: [Option<Instant>; 6],
    shape_dragging: bool,
    drag_offset: Option<(f32, f32)>,
    last_click_time: Option<Instant>,
    last_click_position: Option<(f32, f32)>,
    file_explorer: crate::ui::FileExplorer,
    /// Indexed as: [Up, Down, Left, Right]; 500ms rate limiting to prevent performance issues.
    last_arrow_key_press: [Option<Instant>; 4],
}

impl App {
    pub fn new(
        width: u16,
        height: u16,
        initial_balls: usize,
        place_shapes: bool,
        color_mode: bool,
    ) -> AppResult<Self> {
        let mut options_menu = OptionsMenu::default();
        options_menu.set_ball_count(initial_balls.min(MAX_BALLS));
        options_menu.set_color_mode(color_mode);
        let target_ball_count = options_menu.ball_count();

        let canvas_height = height.saturating_sub(StatusBar::HEIGHT);

        let world_width = f32::from(width);
        let world_height = f32::from(canvas_height);

        let physics_config = PhysicsConfig {
            gravity: vector![0.0, -options_menu.gravity()],
            friction: options_menu.friction(),
            ..Default::default()
        };

        let mut physics_world =
            PhysicsWorld::new(world_width, world_height, physics_config.clone());

        let spawn_color_percent = options_menu.spawn_color_percent();
        spawn_balls_in_grid(
            &mut physics_world,
            target_ball_count,
            world_width,
            world_height,
            spawn_color_percent,
        );

        let canvas = BrailleCanvas::new(width, canvas_height);
        let frame_timer = FrameTimer::new();
        let shape_menu = ShapeMenu::new();
        let help_menu = HelpMenu::new();
        let mut shape_manager = ShapeManager::new();

        if place_shapes {
            let (rigid_bodies, colliders, _, _, _) = physics_world.shape_components_mut();
            shape_manager.place_initial_shapes(world_width, world_height, rigid_bodies, colliders);

            // Displace balls away from newly placed shapes to prevent trapping
            for shape in shape_manager.shapes() {
                let ascii_art = get_ascii_art(shape.shape_type());
                let displacement_radius =
                    (ascii_art.width().max(ascii_art.height()) as f32 / 2.0) + 2.0;
                let (x, y) = shape.position();
                physics_world.displace_balls_from_shape(x, y, displacement_radius);
            }
        }

        Ok(Self {
            physics_world,
            terminal_size: (width, height),
            last_resize_time: Instant::now(),
            prev_terminal_size: (width, height),
            running: true,
            ui_state: UiState::Simulation,
            options_menu,
            shape_menu,
            help_menu,
            shape_manager,
            frame_timer,
            canvas,
            physics_config,
            target_ball_count,
            ball_count_on_menu_open: target_ball_count,
            mouse_held: false,
            mouse_position: None,
            is_in_spawn_zone: false,
            spawn_hold_start: None,
            space_held: false,
            space_hold_start: None,
            mouse_spawn_accumulator: 0.0,
            space_spawn_accumulator: 0.0,
            last_space_event: None,
            held_spawn_section: None,
            section_hold_start: None,
            last_section_event: None,
            section_spawn_accumulator: 0.0,
            active_geysers: [None; 6],
            shape_dragging: false,
            drag_offset: None,
            last_click_time: None,
            last_click_position: None,
            file_explorer: crate::ui::FileExplorer::new(),
            last_arrow_key_press: [None; 4],
        })
    }

    pub fn is_running(&self) -> bool {
        self.running
    }

    pub fn handle_event(&mut self, event: AppEvent) -> AppResult<()> {
        match event {
            AppEvent::Quit => {
                self.running = false;
            }
            AppEvent::ToggleOptions => {
                if !self.options_menu.visible {
                    self.shape_menu.hide();
                    self.ball_count_on_menu_open = self.options_menu.ball_count();
                    self.options_menu.show();
                    self.ui_state = UiState::OptionsMenu;
                } else {
                    self.options_menu.hide();
                    self.ui_state = UiState::Simulation;
                    self.check_auto_reset()?;
                }
            }
            AppEvent::ToggleShapes => {
                if !self.shape_menu.visible {
                    self.options_menu.hide();
                    self.shape_menu.show();
                    self.ui_state = UiState::ShapeMenu;
                } else {
                    self.shape_menu.hide();
                    self.ui_state = UiState::Simulation;
                }
            }
            AppEvent::ToggleColorMode => {
                self.options_menu.toggle_color_mode();
            }
            AppEvent::ToggleHelp => {
                if !self.help_menu.visible {
                    self.options_menu.hide();
                    self.shape_menu.hide();
                    self.file_explorer.hide();
                    self.help_menu.show();
                    self.ui_state = UiState::HelpMenu;
                } else {
                    self.help_menu.hide();
                    self.ui_state = UiState::Simulation;
                }
            }
            AppEvent::CloseHelpMenu => {
                self.help_menu.hide();
                self.ui_state = UiState::Simulation;
            }
            AppEvent::HelpMenuScrollUp => {
                self.help_menu.scroll_up(1);
            }
            AppEvent::HelpMenuScrollDown => {
                self.help_menu.scroll_down(1);
            }
            AppEvent::HelpMenuPageUp => {
                self.help_menu.page_up();
            }
            AppEvent::HelpMenuPageDown => {
                self.help_menu.page_down();
            }
            AppEvent::HelpMenuScrollToTop => {
                self.help_menu.scroll_to_top();
            }
            AppEvent::HelpMenuScrollToBottom => {
                self.help_menu.scroll_to_bottom();
            }
            AppEvent::CloseShapeMenu => {
                self.shape_menu.hide();
                self.ui_state = UiState::Simulation;
            }
            AppEvent::ShapeMenuUp => {
                self.shape_menu.select_up();
            }
            AppEvent::ShapeMenuDown => {
                self.shape_menu.select_down();
            }
            AppEvent::ShapeMenuLeft => {
                self.shape_menu.select_left();
            }
            AppEvent::ShapeMenuRight => {
                self.shape_menu.select_right();
            }
            AppEvent::PlaceSelectedShape => {
                self.place_selected_shape()?;
            }
            AppEvent::ClearShapes => {
                self.clear_all_shapes();
            }
            AppEvent::RotateShapeClockwise => {
                self.shape_manager
                    .rotate_selected_clockwise(self.physics_world.rigid_body_set_mut());
            }
            AppEvent::RotateShapeCounterClockwise => {
                self.shape_manager
                    .rotate_selected_counter_clockwise(self.physics_world.rigid_body_set_mut());
            }
            AppEvent::MoveSelectedShape { dx, dy } => {
                self.shape_manager
                    .move_selected(dx, dy, self.physics_world.rigid_body_set_mut());
            }
            AppEvent::DeleteSelectedShape => {
                self.delete_selected_shape();
            }
            AppEvent::DeselectShape => {
                self.shape_manager.deselect();
                self.shape_dragging = false;
                self.drag_offset = None;
            }
            AppEvent::CycleColorForward => {
                self.shape_manager.cycle_selected_color_forward();
            }
            AppEvent::CycleColorBackward => {
                self.shape_manager.cycle_selected_color_backward();
            }
            AppEvent::ShapeMenuClick { x, y } => {
                self.handle_shape_menu_click(x, y)?;
            }
            AppEvent::CloseMenu => {
                self.options_menu.hide();
                self.shape_menu.hide();
                self.ui_state = UiState::Simulation;
                self.check_auto_reset()?;
            }
            AppEvent::Reset => {
                self.reset_simulation()?;
            }
            AppEvent::MenuUp => {
                self.options_menu.select_previous();
            }
            AppEvent::MenuDown => {
                self.options_menu.select_next();
            }
            AppEvent::MenuIncrease => {
                self.options_menu.increase_value();
                self.apply_menu_changes();
            }
            AppEvent::MenuDecrease => {
                self.options_menu.decrease_value();
                self.apply_menu_changes();
            }
            AppEvent::MenuStartEdit => {
                self.options_menu.start_editing();
            }
            AppEvent::MenuEditChar(c) => {
                self.options_menu.handle_edit_char(c);
            }
            AppEvent::MenuEditBackspace => {
                self.options_menu.handle_edit_backspace();
            }
            AppEvent::MenuConfirmEdit => {
                if self.options_menu.confirm_edit() {
                    self.apply_menu_changes();
                }
            }
            AppEvent::MenuCancelEdit => {
                self.options_menu.cancel_edit();
            }
            AppEvent::SpawnBalls { x, y } => {
                // Spawn without burst effect in spawn zone
                self.spawn_balls_at(x, y)?;
            }
            AppEvent::ApplyBurst { x, y } => {
                let force_mult = self.options_menu.force_percent();
                self.physics_world
                    .apply_burst(x, y, BURST_STRENGTH * force_mult, BURST_RADIUS);
            }
            AppEvent::MouseDown {
                x,
                y,
                in_spawn_zone,
            } => {
                let now = Instant::now();

                // Check for double-click (within 400ms and 2 physics units)
                let is_double_click = if let (Some(last_time), Some((last_x, last_y))) =
                    (self.last_click_time, self.last_click_position)
                {
                    let elapsed = now.duration_since(last_time).as_millis();
                    let distance = ((x - last_x).powi(2) + (y - last_y).powi(2)).sqrt();
                    elapsed < 400 && distance < 2.0
                } else {
                    false
                };

                // Update last click tracking
                self.last_click_time = Some(now);
                self.last_click_position = Some((x, y));

                // First, try to select a shape at the click position
                let shape_selected = self
                    .shape_manager
                    .select_at(x, y, self.physics_world.collider_set())
                    .is_some();

                if shape_selected {
                    if is_double_click {
                        // Double-click on selected shape: delete it
                        self.delete_selected_shape();
                    } else {
                        // Single click: start dragging the shape
                        self.shape_dragging = true;
                        // Calculate offset from shape center for smooth dragging
                        if let Some(shape) = self.shape_manager.selected_shape() {
                            let (sx, sy) = shape.position();
                            self.drag_offset = Some((x - sx, y - sy));
                        }
                    }
                } else {
                    // No shape selected, proceed with normal behavior
                    self.mouse_held = true;
                    self.mouse_position = Some((x, y));
                    self.is_in_spawn_zone = in_spawn_zone;
                    self.spawn_hold_start = Some(Instant::now());
                    // Reset accumulator and start with enough to spawn immediately
                    self.mouse_spawn_accumulator = 1.0;

                    // Initial action on mouse down
                    if in_spawn_zone {
                        // Spawn initial batch without burst
                        self.spawn_balls_at(x, y)?;
                    } else {
                        // Apply burst on initial click
                        let force_mult = self.options_menu.force_percent();
                        self.physics_world.apply_burst(
                            x,
                            y,
                            BURST_STRENGTH * force_mult,
                            BURST_RADIUS,
                        );
                    }
                }
            }
            AppEvent::MouseDrag { x, y } => {
                if self.shape_dragging {
                    // Move shape while dragging
                    let (target_x, target_y) = if let Some((ox, oy)) = self.drag_offset {
                        (x - ox, y - oy)
                    } else {
                        (x, y)
                    };
                    self.shape_manager.move_selected_to(
                        target_x,
                        target_y,
                        self.physics_world.rigid_body_set_mut(),
                    );
                } else if self.mouse_held {
                    self.mouse_position = Some((x, y));
                }
            }
            AppEvent::MouseUp => {
                self.mouse_held = false;
                self.mouse_position = None;
                self.is_in_spawn_zone = false;
                self.spawn_hold_start = None;
                self.mouse_spawn_accumulator = 0.0;
                self.shape_dragging = false;
                self.drag_offset = None;
            }
            AppEvent::StartShapeDrag { x, y } => {
                if self.shape_manager.selected_id().is_some() {
                    self.shape_dragging = true;
                    if let Some(shape) = self.shape_manager.selected_shape() {
                        let (sx, sy) = shape.position();
                        self.drag_offset = Some((x - sx, y - sy));
                    }
                }
            }
            AppEvent::DragShape { x, y } => {
                if self.shape_dragging {
                    let (target_x, target_y) = if let Some((ox, oy)) = self.drag_offset {
                        (x - ox, y - oy)
                    } else {
                        (x, y)
                    };
                    self.shape_manager.move_selected_to(
                        target_x,
                        target_y,
                        self.physics_world.rigid_body_set_mut(),
                    );
                }
            }
            AppEvent::EndShapeDrag => {
                self.shape_dragging = false;
                self.drag_offset = None;
            }
            AppEvent::DoubleClick { x, y } => {
                // Try to select and delete shape at position
                if self
                    .shape_manager
                    .select_at(x, y, self.physics_world.collider_set())
                    .is_some()
                {
                    self.delete_selected_shape();
                }
            }
            AppEvent::NumberBurst { digit } => {
                self.apply_number_burst(digit);
                // Set active geyser for visual feedback (digit 1-6 maps to index 0-5)
                if (1..=6).contains(&digit) {
                    self.active_geysers[(digit - 1) as usize] = Some(Instant::now());
                }
            }
            AppEvent::Nudge { dx, dy } => {
                // Rate limit arrow key nudges to prevent performance issues from spam
                // Each direction has independent 500ms cooldown
                if self.check_arrow_key_cooldown(dx, dy) {
                    self.physics_world.nudge_all(dx, dy);
                }
            }
            AppEvent::SpawnAtSection { digit } => {
                let now = Instant::now();
                // Check if this is a new key or continuing hold
                let is_new_key = self.held_spawn_section != Some(digit);
                if is_new_key {
                    // New section key pressed
                    self.held_spawn_section = Some(digit);
                    self.section_hold_start = Some(now);
                    self.section_spawn_accumulator = 1.0; // Spawn immediately
                    self.spawn_at_section(digit)?;
                }
                // Always update timestamp for release detection
                self.last_section_event = Some(now);
            }
            AppEvent::SpaceDown => {
                let now = Instant::now();
                // Check if this is a new press or continuing hold
                if !self.space_held {
                    self.space_held = true;
                    self.space_hold_start = Some(now);
                    self.space_spawn_accumulator = 1.0;
                    // Initial spawn across full width
                    self.spawn_across_full_width()?;
                }
                // Always update timestamp for release detection
                self.last_space_event = Some(now);
            }
            AppEvent::SpaceUp => {
                // This event may not be received in terminals, but handle it anyway
                self.space_held = false;
                self.space_hold_start = None;
                self.space_spawn_accumulator = 0.0;
                self.last_space_event = None;
            }
            AppEvent::Resize {
                new_width,
                new_height,
                delta_width,
                delta_height,
            } => {
                self.handle_resize(new_width, new_height, delta_width, delta_height)?;
            }
            AppEvent::SaveLevel => {
                self.save_level()?;
            }
            AppEvent::LoadLevel => {
                self.load_level()?;
            }
            AppEvent::OpenSaveExplorer => {
                self.file_explorer.show_save();
                self.ui_state = UiState::FileExplorer;
            }
            AppEvent::OpenLoadExplorer => {
                self.file_explorer.show_load();
                self.ui_state = UiState::FileExplorer;
            }
            AppEvent::CloseFileExplorer => {
                self.file_explorer.hide();
                self.ui_state = UiState::Simulation;
            }
            AppEvent::FileExplorerUp => {
                self.file_explorer.select_previous();
            }
            AppEvent::FileExplorerDown => {
                self.file_explorer.select_next();
            }
            AppEvent::FileExplorerConfirm => {
                self.handle_file_explorer_confirm()?;
            }
            AppEvent::FileExplorerChar(c) => {
                self.file_explorer.handle_char(c);
            }
            AppEvent::FileExplorerBackspace => {
                self.file_explorer.handle_backspace();
            }
            AppEvent::None => {}
        }
        Ok(())
    }

    fn apply_menu_changes(&mut self) {
        self.frame_timer
            .set_fps_cap(self.options_menu.fps_cap_enabled());

        self.physics_config.gravity = vector![0.0, -self.options_menu.gravity()];
        self.physics_config.friction = self.options_menu.friction();
        self.physics_world
            .update_config(self.physics_config.clone());

        self.target_ball_count = self.options_menu.ball_count();
    }

    fn check_auto_reset(&mut self) -> AppResult<()> {
        let new_ball_count = self.options_menu.ball_count();
        if new_ball_count != self.ball_count_on_menu_open {
            // Ball count changed, update target and reset
            self.target_ball_count = new_ball_count;
            self.reset_simulation()?;
        }
        Ok(())
    }

    /// Resets balls to initial positions; shapes are preserved.
    fn reset_simulation(&mut self) -> AppResult<()> {
        self.physics_world.clear_balls();

        let (world_width, world_height) = self.physics_world.dimensions();
        let spawn_color_percent = self.options_menu.spawn_color_percent();
        spawn_balls_in_grid(
            &mut self.physics_world,
            self.target_ball_count,
            world_width,
            world_height,
            spawn_color_percent,
        );

        Ok(())
    }

    /// Places shape at valid position; displaces nearby balls to prevent trapping.
    fn place_selected_shape(&mut self) -> AppResult<()> {
        let shape_type = self.shape_menu.selected_shape_type();
        let (world_width, world_height) = self.physics_world.dimensions();

        // Try to find a valid position that respects all placement restrictions
        let position = self
            .shape_manager
            .find_random_position(shape_type, world_width, world_height, 100)
            .unwrap_or_else(|| {
                // Fallback: place at center of the non-spawn area if no valid position found
                // Spawn zone is top 1/4 of world, so place in center of bottom 3/4
                let center_x = world_width / 2.0;
                let center_y = world_height * 0.375; // Center of bottom 3/4
                (center_x, center_y)
            });

        // Always place the shape (even if using fallback position)
        let (rigid_bodies, colliders, _, _, _) = self.physics_world.shape_components_mut();
        self.shape_manager
            .add_shape(shape_type, position.0, position.1, rigid_bodies, colliders);

        // Displace balls away from the newly placed shape to prevent trapping
        let ascii_art = get_ascii_art(shape_type);
        let displacement_radius = (ascii_art.width().max(ascii_art.height()) as f32 / 2.0) + 2.0;
        self.physics_world
            .displace_balls_from_shape(position.0, position.1, displacement_radius);

        // Close the shape menu after placing
        self.shape_menu.hide();
        self.ui_state = UiState::Simulation;

        Ok(())
    }

    /// Returns `true` if the 500ms cooldown has passed for this direction.
    fn check_arrow_key_cooldown(&mut self, dx: f32, dy: f32) -> bool {
        const ARROW_KEY_COOLDOWN_MS: u128 = 500;

        // Determine which direction index to use
        // Index: 0=Up, 1=Down, 2=Left, 3=Right
        let dir_index = if dy > 0.0 {
            0 // Up
        } else if dy < 0.0 {
            1 // Down
        } else if dx < 0.0 {
            2 // Left
        } else {
            3 // Right
        };

        let now = Instant::now();

        // Check if enough time has passed since the last press of this direction
        let can_proceed = match self.last_arrow_key_press[dir_index] {
            Some(last_time) => now.duration_since(last_time).as_millis() >= ARROW_KEY_COOLDOWN_MS,
            None => true, // First press, always allow
        };

        if can_proceed {
            self.last_arrow_key_press[dir_index] = Some(now);
        }

        can_proceed
    }

    fn clear_all_shapes(&mut self) {
        let (rigid_bodies, colliders, islands, impulse_joints, multibody_joints) =
            self.physics_world.shape_components_mut();
        self.shape_manager.clear_all(
            rigid_bodies,
            colliders,
            islands,
            impulse_joints,
            multibody_joints,
        );
    }

    fn delete_selected_shape(&mut self) {
        let (rigid_bodies, colliders, islands, impulse_joints, multibody_joints) =
            self.physics_world.shape_components_mut();
        self.shape_manager.remove_selected(
            rigid_bodies,
            colliders,
            islands,
            impulse_joints,
            multibody_joints,
        );
        self.shape_dragging = false;
        self.drag_offset = None;
    }

    fn handle_shape_menu_click(&mut self, x: u16, y: u16) -> AppResult<()> {
        // The shape menu handles click internally and may place a shape
        let area = ratatui::layout::Rect {
            x: 0,
            y: 0,
            width: self.terminal_size.0,
            height: self.terminal_size.1,
        };

        // Calculate popup area (must match ShapeMenu::render)
        let popup_width = (area.width * 60 / 100).max(30);
        let popup_height = (area.height * 50 / 100).max(12);
        let popup_x = (area.width.saturating_sub(popup_width)) / 2;
        let popup_y = (area.height.saturating_sub(popup_height)) / 2;

        // Check if click is inside the popup
        if x >= popup_x && x < popup_x + popup_width && y >= popup_y && y < popup_y + popup_height {
            let local_x = x - popup_x;
            let local_y = y - popup_y;

            if let Some(_shape_type) =
                self.shape_menu
                    .handle_click(local_x, local_y, popup_width, popup_height)
            {
                // Shape was clicked - place it
                self.place_selected_shape()?;
            }
        } else {
            // Click outside popup - close menu
            self.shape_menu.hide();
            self.ui_state = UiState::Simulation;
        }

        Ok(())
    }

    /// Applies zone-spanning upward burst; colors affected balls when Color Mode is ON.
    fn apply_number_burst(&mut self, digit: u8) {
        let (world_width, _world_height) = self.physics_world.dimensions();
        let term_width = self.terminal_size.0;

        // Calculate zone layout (must match StatusBar::build_number_line)
        let max_digits = 6.min((term_width / 6) as u8).max(1);

        // Only process if digit is within displayed range
        if digit > max_digits || digit == 0 {
            return;
        }

        // Calculate zone width in physics units
        let zone_width_physics = world_width / max_digits as f32;

        // Calculate X position as center of the zone
        // Zone 1 spans from 0 to zone_width, center at zone_width/2
        // Zone n spans from (n-1)*zone_width to n*zone_width, center at (n-0.5)*zone_width
        let x = (digit as f32 - 0.5) * zone_width_physics;

        // Y position: near the bottom of the canvas (in physics coords, Y-up)
        let y = 1.0; // Just above the floor

        // Calculate burst radius to cover the full zone width
        // Radius should be half the zone width to cover edge-to-edge
        let burst_radius = zone_width_physics / 2.0;

        // Apply upward directional burst with zone-spanning radius
        let force_mult = self.options_menu.force_percent();
        let effective_radius = burst_radius.max(BURST_RADIUS);

        // Check if Color Mode is enabled
        if self.options_menu.color_mode() {
            // Color Mode ON: apply burst and color affected balls
            let color = BallColor::from_geyser(digit);
            self.physics_world.apply_directional_burst_with_color(
                x,
                y,
                0.0, // No horizontal bias
                1.0, // Upward bias
                BURST_STRENGTH * force_mult,
                effective_radius,
                color,
            );
        } else {
            // Color Mode OFF: just apply the burst without coloring
            self.physics_world.apply_directional_burst(
                x,
                y,
                0.0, // No horizontal bias
                1.0, // Upward bias
                BURST_STRENGTH * force_mult,
                effective_radius,
            );
        }
    }

    fn spawn_balls_at(&mut self, x: f32, y: f32) -> AppResult<()> {
        let spawn_color_percent = self.options_menu.spawn_color_percent();

        // Spawn a cluster of balls with slight random offset
        for i in 0..BALLS_PER_SPAWN {
            let offset_x = (i % 3) as f32 - 1.0;
            let offset_y = (i / 3) as f32 * 0.5;
            let color = Self::choose_spawn_color(spawn_color_percent);
            let _ = self.physics_world.spawn_ball_with_velocity_and_color(
                x + offset_x,
                y + offset_y,
                0.0,
                0.0,
                color,
            );
        }
        Ok(())
    }

    fn choose_spawn_color(spawn_color_percent: i32) -> BallColor {
        if spawn_color_percent <= 0 {
            return BallColor::White;
        }

        let mut rng = rand::thread_rng();
        if rng.gen_range(0..100) < spawn_color_percent {
            BallColor::random_color()
        } else {
            BallColor::White
        }
    }

    fn spawn_at_section(&mut self, digit: u8) -> AppResult<()> {
        let (world_width, world_height) = self.physics_world.dimensions();
        let term_width = self.terminal_size.0;
        let spawn_color_percent = self.options_menu.spawn_color_percent();

        // Calculate zone layout (must match StatusBar::build_number_line)
        let max_digits = 6.min((term_width / 6) as u8).max(1);

        // Only process if digit is within displayed range
        if digit > max_digits || digit == 0 {
            return Ok(());
        }

        // Calculate zone width in physics units
        let zone_width_physics = world_width / max_digits as f32;

        // Calculate X range for the zone
        let zone_start_x = (digit as f32 - 1.0) * zone_width_physics;

        // Y position: top of spawn zone (top 1/4 of canvas)
        let y = world_height - 1.0;

        // Spawn balls across the section width
        let num_balls = BALLS_PER_SPAWN;
        let spacing = zone_width_physics / (num_balls as f32 + 1.0);

        for i in 0..num_balls {
            let x = zone_start_x + spacing * (i as f32 + 1.0);
            let color = Self::choose_spawn_color(spawn_color_percent);
            let _ = self
                .physics_world
                .spawn_ball_with_velocity_and_color(x, y, 0.0, 0.0, color);
        }

        Ok(())
    }

    fn spawn_across_full_width(&mut self) -> AppResult<()> {
        let (world_width, world_height) = self.physics_world.dimensions();
        let spawn_color_percent = self.options_menu.spawn_color_percent();

        // Y position: top of spawn zone
        let y = world_height - 1.0;

        // Spawn balls evenly across the full width
        let num_balls = BALLS_PER_SPAWN * 2; // More balls for full width
        let spacing = world_width / (num_balls as f32 + 1.0);

        for i in 0..num_balls {
            let x = spacing * (i as f32 + 1.0);
            let color = Self::choose_spawn_color(spawn_color_percent);
            let _ = self
                .physics_world
                .spawn_ball_with_velocity_and_color(x, y, 0.0, 0.0, color);
        }

        Ok(())
    }

    /// Applies inward forces to balls near shrinking boundaries.
    fn handle_resize(
        &mut self,
        new_width: u16,
        new_height: u16,
        delta_width: i32,
        delta_height: i32,
    ) -> AppResult<()> {
        let now = Instant::now();
        let elapsed = now
            .duration_since(self.last_resize_time)
            .as_secs_f32()
            .max(0.001);

        // Get old dimensions before updating
        let (old_world_width, old_world_height) = self.physics_world.dimensions();

        // Calculate new dimensions
        let canvas_height = new_height.saturating_sub(StatusBar::HEIGHT);
        let new_world_width = f32::from(new_width);
        let new_world_height = f32::from(canvas_height);

        // Calculate resize velocity (units per second)
        let velocity_x = delta_width as f32 / elapsed;
        let velocity_y = delta_height as f32 / elapsed;

        // Apply force only to balls near shrinking boundaries
        self.physics_world.apply_boundary_shrink_force(
            old_world_width,
            old_world_height,
            new_world_width,
            new_world_height,
            velocity_x,
            velocity_y,
        );

        // Update physics world boundaries
        self.physics_world
            .update_boundaries(new_world_width, new_world_height);

        // Resize canvas
        self.canvas.resize(new_width, canvas_height);

        // Update state
        self.prev_terminal_size = self.terminal_size;
        self.terminal_size = (new_width, new_height);
        self.last_resize_time = now;

        Ok(())
    }

    /// Uses fixed timestep with accumulator for frame-rate-independent physics.
    pub fn update_physics(&mut self, delta: std::time::Duration) {
        self.frame_timer.add_physics_time(delta.as_secs_f32());

        let mut steps = 0;
        while self.frame_timer.physics_accumulator() >= PHYSICS_DT && steps < MAX_PHYSICS_STEPS {
            self.physics_world.step();
            self.frame_timer.consume_physics_step(PHYSICS_DT);
            steps += 1;
        }

        // Clamp accumulator to prevent spiral of death
        self.frame_timer
            .clamp_accumulator(MAX_PHYSICS_STEPS, PHYSICS_DT);

        // Process continuous hold effects
        self.update_hold_effects();

        // Clear geyser visual after a short duration
        self.update_geyser_visual();
    }

    fn update_hold_effects(&mut self) {
        // Check for key release timeouts first
        self.check_key_release_timeouts();

        // Then update spawning for held keys
        self.update_mouse_hold();
        self.update_space_hold();
        self.update_section_hold();
    }

    fn check_key_release_timeouts(&mut self) {
        let now = Instant::now();

        // Check space bar release
        if let Some(last_event) = self.last_space_event {
            if now.duration_since(last_event).as_millis() > KEY_RELEASE_TIMEOUT_MS {
                // No space key event received recently - consider it released
                self.space_held = false;
                self.space_hold_start = None;
                self.space_spawn_accumulator = 0.0;
                self.last_space_event = None;
            }
        }

        // Check section key release
        if let Some(last_event) = self.last_section_event {
            if now.duration_since(last_event).as_millis() > KEY_RELEASE_TIMEOUT_MS {
                // No section key event received recently - consider it released
                self.held_spawn_section = None;
                self.section_hold_start = None;
                self.section_spawn_accumulator = 0.0;
                self.last_section_event = None;
            }
        }
    }

    fn update_section_hold(&mut self) {
        let Some(digit) = self.held_spawn_section else {
            return;
        };

        let Some(start_time) = self.section_hold_start else {
            return;
        };

        // Check if we're at the ball limit
        if self.physics_world.ball_count() >= MAX_BALLS {
            return;
        }

        let (world_width, world_height) = self.physics_world.dimensions();
        let term_width = self.terminal_size.0;
        let spawn_color_percent = self.options_menu.spawn_color_percent();

        // Calculate zone layout (must match StatusBar::build_number_line)
        let max_digits = 6.min((term_width / 6) as u8).max(1);

        // Only process if digit is within displayed range
        if digit > max_digits || digit == 0 {
            return;
        }

        // Calculate zone width in physics units
        let zone_width_physics = world_width / max_digits as f32;
        let zone_start_x = (digit as f32 - 1.0) * zone_width_physics;
        let y = world_height - 1.0;

        let elapsed = start_time.elapsed().as_secs_f32();

        // Calculate spawn rate based on hold duration (shared with mouse/space)
        let spawn_rate = calculate_spawn_rate(elapsed);

        // Use accumulator pattern for smooth spawning
        self.section_spawn_accumulator += spawn_rate * PHYSICS_DT;

        // Spawn whole balls when accumulator >= 1
        let balls_to_spawn = self.section_spawn_accumulator as usize;
        if balls_to_spawn > 0 {
            self.section_spawn_accumulator -= balls_to_spawn as f32;

            let remaining_capacity = MAX_BALLS.saturating_sub(self.physics_world.ball_count());
            let actual_spawn = balls_to_spawn.min(remaining_capacity);

            // Spread balls across the section width
            let spacing = zone_width_physics / (actual_spawn as f32 + 1.0);
            for i in 0..actual_spawn {
                let x = zone_start_x + spacing * (i as f32 + 1.0);
                let color = Self::choose_spawn_color(spawn_color_percent);
                let _ = self
                    .physics_world
                    .spawn_ball_with_velocity_and_color(x, y, 0.0, 0.0, color);
            }
        }
    }

    /// Clears each active geyser indicator after 150ms.
    fn update_geyser_visual(&mut self) {
        for geyser in &mut self.active_geysers {
            if let Some(activation_time) = geyser {
                if activation_time.elapsed().as_millis() > 150 {
                    *geyser = None;
                }
            }
        }
    }

    fn update_space_hold(&mut self) {
        if !self.space_held {
            return;
        }
        let Some(start_time) = self.space_hold_start else {
            return;
        };
        if self.physics_world.ball_count() >= MAX_BALLS {
            return;
        }

        let (world_width, world_height) = self.physics_world.dimensions();
        let y = world_height - 1.0;
        let spawn_color_percent = self.options_menu.spawn_color_percent();
        let elapsed = start_time.elapsed().as_secs_f32();
        let spawn_rate = calculate_spawn_rate(elapsed);

        self.space_spawn_accumulator += spawn_rate * PHYSICS_DT;
        let balls_to_spawn = self.space_spawn_accumulator as usize;
        if balls_to_spawn > 0 {
            self.space_spawn_accumulator -= balls_to_spawn as f32;
            let remaining_capacity = MAX_BALLS.saturating_sub(self.physics_world.ball_count());
            let actual_spawn = balls_to_spawn.min(remaining_capacity);
            let spacing = world_width / (actual_spawn as f32 + 1.0);
            for i in 0..actual_spawn {
                let x = spacing * (i as f32 + 1.0);
                let color = Self::choose_spawn_color(spawn_color_percent);
                let _ = self
                    .physics_world
                    .spawn_ball_with_velocity_and_color(x, y, 0.0, 0.0, color);
            }
        }
    }

    fn update_mouse_hold(&mut self) {
        if !self.mouse_held {
            return;
        }
        let Some((x, y)) = self.mouse_position else {
            return;
        };

        if self.is_in_spawn_zone {
            let Some(start_time) = self.spawn_hold_start else {
                return;
            };
            if self.physics_world.ball_count() >= MAX_BALLS {
                return;
            }

            let spawn_color_percent = self.options_menu.spawn_color_percent();
            let elapsed = start_time.elapsed().as_secs_f32();
            let spawn_rate = calculate_spawn_rate(elapsed);

            self.mouse_spawn_accumulator += spawn_rate * PHYSICS_DT;
            let balls_to_spawn = self.mouse_spawn_accumulator as usize;
            if balls_to_spawn > 0 {
                self.mouse_spawn_accumulator -= balls_to_spawn as f32;
                let remaining_capacity = MAX_BALLS.saturating_sub(self.physics_world.ball_count());
                let actual_spawn = balls_to_spawn.min(remaining_capacity);
                for i in 0..actual_spawn {
                    let offset_x = (i % 3) as f32 - 1.0;
                    let offset_y = (i / 3) as f32 * 0.5;
                    let color = Self::choose_spawn_color(spawn_color_percent);
                    let _ = self.physics_world.spawn_ball_with_velocity_and_color(
                        x + offset_x,
                        y + offset_y,
                        0.0,
                        0.0,
                        color,
                    );
                }
            }
        } else {
            let force_mult = self.options_menu.force_percent();
            self.physics_world
                .apply_burst(x, y, BURST_STRENGTH * force_mult * 0.3, BURST_RADIUS);
        }
    }

    pub fn render(&mut self, frame: &mut Frame) {
        let area = frame.area();
        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints([Constraint::Min(0), Constraint::Length(StatusBar::HEIGHT)])
            .split(area);
        let canvas_area = chunks[0];
        let status_area = chunks[1];

        self.update_canvas();
        self.render_canvas(frame, canvas_area);
        self.render_shapes(frame, canvas_area);

        let active_geysers_bool: [bool; 6] = [
            self.active_geysers[0].is_some(),
            self.active_geysers[1].is_some(),
            self.active_geysers[2].is_some(),
            self.active_geysers[3].is_some(),
            self.active_geysers[4].is_some(),
            self.active_geysers[5].is_some(),
        ];
        let status_info = StatusBarInfo {
            fps: if self.options_menu.show_fps() {
                Some(self.frame_timer.current_fps())
            } else {
                None
            },
            ball_count: self.physics_world.ball_count(),
            gravity_percent: self.options_menu.gravity_percent(),
            force_percent: (self.options_menu.force_percent() * 100.0) as i32,
            active_geysers: active_geysers_bool,
            color_mode: self.options_menu.color_mode(),
        };
        StatusBar::render(frame, status_area, &status_info);

        // Render options menu if visible
        if self.ui_state == UiState::OptionsMenu {
            self.options_menu.render(frame, area);
        }

        // Render shape menu if visible
        if self.ui_state == UiState::ShapeMenu {
            self.shape_menu.render(frame, area);
        }

        // Render file explorer if visible
        if self.ui_state == UiState::FileExplorer {
            self.file_explorer.render(frame, area);
        }

        // Render help menu if visible
        if self.ui_state == UiState::HelpMenu {
            self.help_menu.render(frame, area);
        }
    }

    fn render_shapes(&self, frame: &mut Frame, canvas_area: Rect) {
        let (world_width, world_height) = self.physics_world.dimensions();

        for shape in self.shape_manager.shapes() {
            let (phys_x, phys_y) = shape.position();

            // Convert physics coordinates to terminal coordinates
            // Physics: Y-up (0 at bottom), Terminal: Y-down (0 at top)
            let norm_x = phys_x / world_width;
            let norm_y = 1.0 - (phys_y / world_height);

            // Get ASCII art for this shape (with rotation)
            let ascii_art = get_rotated_ascii_art(shape.shape_type(), shape.rotation_degrees());

            // Calculate terminal position (center of shape)
            let term_x =
                (norm_x * canvas_area.width as f32) as i16 - (ascii_art.width() as i16 / 2);
            let term_y =
                (norm_y * canvas_area.height as f32) as i16 - (ascii_art.height() as i16 / 2);

            // Render each character of the shape
            let color = shape.render_color();
            let style = Style::default().fg(color);

            for (dy, line) in ascii_art.lines().iter().enumerate() {
                let row = term_y + dy as i16;

                // Skip if row is outside canvas
                if row < 0 || row >= canvas_area.height as i16 {
                    continue;
                }

                let row_u16 = row as u16;

                for (dx, ch) in line.chars().enumerate() {
                    if ch == ' ' {
                        continue;
                    }

                    let col = term_x + dx as i16;

                    // Skip if column is outside canvas
                    if col < 0 || col >= canvas_area.width as i16 {
                        continue;
                    }

                    let col_u16 = col as u16;

                    // Render the character at this position
                    let char_area = Rect {
                        x: canvas_area.x + col_u16,
                        y: canvas_area.y + row_u16,
                        width: 1,
                        height: 1,
                    };

                    let para = Paragraph::new(Span::styled(ch.to_string(), style));
                    frame.render_widget(para, char_area);
                }
            }
        }
    }

    /// Color Mode ON uses color-aware plotting; OFF uses faster non-color plotting.
    fn update_canvas(&mut self) {
        self.canvas.clear();

        let (world_width, world_height) = self.physics_world.dimensions();
        let (canvas_width, canvas_height) = self.canvas.dimensions();

        if self.options_menu.color_mode() {
            // Color Mode ON: collect positions with colors
            let positions: Vec<(u32, u32, BallColor)> = self
                .physics_world
                .ball_positions_with_colors()
                .map(|(px, py, color)| {
                    let (sx, sy) = physics_to_subpixel(
                        px,
                        py,
                        world_width,
                        world_height,
                        canvas_width,
                        canvas_height,
                    );
                    (sx, sy, color)
                })
                .collect();

            // Batch plot with colors using parallel iteration
            self.canvas.plot_batch_parallel_with_colors(&positions);
        } else {
            // Color Mode OFF: use faster non-color plotting
            let positions: Vec<(u32, u32)> = self
                .physics_world
                .ball_positions()
                .map(|(px, py)| {
                    physics_to_subpixel(
                        px,
                        py,
                        world_width,
                        world_height,
                        canvas_width,
                        canvas_height,
                    )
                })
                .collect();

            // Batch plot using parallel iteration
            self.canvas.plot_batch_parallel(&positions);
        }

        self.canvas.sync_from_atomic();
    }

    /// Color Mode ON uses dominant ball color; OFF uses density-based coloring.
    fn render_canvas(&self, frame: &mut Frame, area: Rect) {
        let color_mode = self.options_menu.color_mode();

        // Build each row as a styled string
        for row in 0..area.height {
            let mut line_spans = Vec::with_capacity(area.width as usize);

            for col in 0..area.width {
                let ch = self.canvas.get_char(col, row);
                let ball_count = self.canvas.get_ball_count(col, row);
                let bg = density_to_color(ball_count);

                // Determine foreground color based on mode
                let fg = if color_mode && ball_count > 0 {
                    // Color Mode ON: use dominant ball color
                    self.canvas.get_dominant_color(col, row).to_ratatui_color()
                } else {
                    // Color Mode OFF: use density-based foreground
                    density_to_foreground(ball_count)
                };

                let style = match bg {
                    Some(bg_color) => Style::default().fg(fg).bg(bg_color),
                    None => Style::default().fg(fg),
                };

                line_spans.push(ratatui::text::Span::styled(ch.to_string(), style));
            }

            let line = ratatui::text::Line::from(line_spans);
            let para = Paragraph::new(line);
            let row_area = Rect {
                x: area.x,
                y: area.y + row,
                width: area.width,
                height: 1,
            };
            frame.render_widget(para, row_area);
        }
    }

    /// Returns the event context for event handling.
    pub fn event_context(&self) -> EventContext {
        let (world_width, world_height) = self.physics_world.dimensions();
        EventContext {
            terminal_width: self.terminal_size.0,
            terminal_height: self.terminal_size.1,
            menu_open: self.ui_state == UiState::OptionsMenu,
            menu_editing: self.options_menu.is_editing(),
            shape_menu_open: self.ui_state == UiState::ShapeMenu,
            file_explorer_open: self.ui_state == UiState::FileExplorer,
            file_explorer_editing: self.file_explorer.is_editing_filename(),
            help_menu_open: self.ui_state == UiState::HelpMenu,
            world_width,
            world_height,
            shape_selected: self.shape_manager.selected_id().is_some(),
            shape_dragging: self.shape_dragging,
        }
    }

    /// Begins a new frame and returns delta time.
    pub fn begin_frame(&mut self) -> std::time::Duration {
        self.frame_timer.begin_frame()
    }

    /// Ends the current frame (handles timing/sleep).
    pub fn end_frame(&mut self) {
        self.frame_timer.end_frame();
    }

    /// Saves the current level configuration to a JSON file.
    ///
    /// Saves shapes (positions, rotations, colors) and options settings
    /// to "level.json" in the current directory.
    fn save_level(&self) -> AppResult<()> {
        let mut config = LevelConfig::new();

        // Collect all shapes
        for shape in self.shape_manager.shapes() {
            config.shapes.push(SavedShape::from_shape(shape));
        }

        // Collect options
        config.options = options_from_menu(&self.options_menu);

        // Save to file
        config.save_to_file("level.json")?;

        Ok(())
    }

    /// Loads a level configuration from a JSON file.
    ///
    /// Loads shapes and options from "level.json" in the current directory.
    /// Gracefully handles missing or malformed files.
    fn load_level(&mut self) -> AppResult<()> {
        self.load_level_from_path("level.json")
    }

    /// Loads a level configuration from a specified path.
    ///
    /// This is also used by the --open CLI argument.
    ///
    /// # Arguments
    ///
    /// * `path` - Path to the JSON file to load
    pub fn load_level_from_path(&mut self, path: &str) -> AppResult<()> {
        // Load the configuration (returns error if file is missing or malformed)
        let config = LevelConfig::load_from_file(path)?;

        self.clear_all_shapes();

        apply_options_to_menu(&config.options, &mut self.options_menu);
        self.apply_menu_changes();
        self.target_ball_count = self.options_menu.ball_count();
        let (world_width, world_height) = self.physics_world.dimensions();

        for saved_shape in &config.shapes {
            let (shape_type, x, y, rotation_degrees, color) = saved_shape.to_shape_params();
            let clamped_x = x.clamp(1.0, world_width - 1.0);
            let clamped_y = y.clamp(1.0, world_height - 1.0);

            // Add the shape
            let (rigid_bodies, colliders, _, _, _) = self.physics_world.shape_components_mut();
            let id = self.shape_manager.add_shape(
                shape_type,
                clamped_x,
                clamped_y,
                rigid_bodies,
                colliders,
            );

            // Apply rotation and color
            if let Some(shape) = self.shape_manager.get_shape_mut(id) {
                // Set rotation by rotating the appropriate number of times
                let rotations = (rotation_degrees / 90).rem_euclid(4);
                for _ in 0..rotations {
                    shape.rotate_clockwise();
                }
                // Update physics transform for rotation
                if let Some(handle) = shape.rigid_body_handle() {
                    if let Some(body) = self.physics_world.rigid_body_set_mut().get_mut(handle) {
                        let rotation = shape.rotation_radians();
                        let isometry = rapier2d::prelude::Isometry::new(
                            vector![clamped_x, clamped_y],
                            rotation,
                        );
                        body.set_position(isometry, true);
                    }
                }
                // Set color
                shape.set_color(color);
            }
        }

        // Reset balls with new settings
        self.reset_simulation()?;

        // Displace balls away from loaded shapes to prevent trapping
        for shape in self.shape_manager.shapes() {
            let ascii_art = get_ascii_art(shape.shape_type());
            let displacement_radius =
                (ascii_art.width().max(ascii_art.height()) as f32 / 2.0) + 2.0;
            let (x, y) = shape.position();
            self.physics_world
                .displace_balls_from_shape(x, y, displacement_radius);
        }

        Ok(())
    }

    /// Handles file explorer confirmation (Enter key).
    ///
    /// Saves or loads based on the current file explorer mode.
    fn handle_file_explorer_confirm(&mut self) -> AppResult<()> {
        use crate::ui::FileExplorerMode;

        let mode = self.file_explorer.mode();

        if let Some(path) = self.file_explorer.get_target_path() {
            match mode {
                FileExplorerMode::Save => {
                    // Save to the selected/typed path
                    self.save_level_to_path(&path)?;
                }
                FileExplorerMode::Load => {
                    // Load from the selected path
                    self.load_level_from_path(&path)?;
                }
            }
        }

        // Close the file explorer
        self.file_explorer.hide();
        self.ui_state = UiState::Simulation;

        Ok(())
    }

    /// Saves the current level configuration to a specified path.
    ///
    /// # Arguments
    ///
    /// * `path` - Path to save the JSON file
    fn save_level_to_path(&self, path: &str) -> AppResult<()> {
        let mut config = LevelConfig::new();

        // Collect all shapes
        for shape in self.shape_manager.shapes() {
            config.shapes.push(SavedShape::from_shape(shape));
        }

        // Collect options
        config.options = options_from_menu(&self.options_menu);

        // Save to file
        config.save_to_file(path)?;

        Ok(())
    }
}
