//! Shape manager for tracking and manipulating shapes.
//!
//! The `ShapeManager` is the central coordinator for all shape operations:
//! - Creating and removing shapes
//! - Selection and manipulation
//! - Random placement with collision avoidance
//! - Physics synchronization

use rand::Rng;
use rapier2d::prelude::*;

use super::ascii_art::get_ascii_art;
use super::colliders::{create_shape_collider, remove_shape_collider, shapes_overlap};
use super::types::{Shape, ShapeType};

/// Minimum separation distance between shapes (in character cells).
const MIN_SHAPE_SEPARATION: f32 = 4.0;

/// Edge margin to avoid placing shapes too close to boundaries.
const EDGE_MARGIN: f32 = 3.0;

/// Manager for all shapes in the simulation.
///
/// Handles creation, removal, selection, and physics synchronization
/// for all shape objects.
#[derive(Debug)]
pub struct ShapeManager {
    /// All shapes in the simulation.
    shapes: Vec<Shape>,

    /// Next shape ID to assign.
    next_id: u32,

    /// Currently selected shape ID, if any.
    selected_id: Option<u32>,
}

impl Default for ShapeManager {
    fn default() -> Self {
        Self::new()
    }
}

impl ShapeManager {
    /// Creates a new empty shape manager.
    pub fn new() -> Self {
        Self {
            shapes: Vec::new(),
            next_id: 1,
            selected_id: None,
        }
    }

    /// Returns the number of shapes.
    pub fn shape_count(&self) -> usize {
        self.shapes.len()
    }

    /// Returns an iterator over all shapes.
    pub fn shapes(&self) -> impl Iterator<Item = &Shape> {
        self.shapes.iter()
    }

    /// Returns a mutable iterator over all shapes.
    pub fn shapes_mut(&mut self) -> impl Iterator<Item = &mut Shape> {
        self.shapes.iter_mut()
    }

    /// Returns a reference to the currently selected shape, if any.
    pub fn selected_shape(&self) -> Option<&Shape> {
        self.selected_id
            .and_then(|id| self.shapes.iter().find(|s| s.id() == id))
    }

    /// Returns a mutable reference to the currently selected shape, if any.
    pub fn selected_shape_mut(&mut self) -> Option<&mut Shape> {
        let selected_id = self.selected_id?;
        self.shapes.iter_mut().find(|s| s.id() == selected_id)
    }

    /// Returns the ID of the currently selected shape, if any.
    pub fn selected_id(&self) -> Option<u32> {
        self.selected_id
    }

    /// Adds a shape at the specified position.
    ///
    /// Creates the shape and its physics collider.
    ///
    /// # Arguments
    ///
    /// * `shape_type` - The type of shape to create
    /// * `x` - X position in physics coordinates
    /// * `y` - Y position in physics coordinates
    /// * `rigid_body_set` - Physics rigid body set
    /// * `collider_set` - Physics collider set
    ///
    /// # Returns
    ///
    /// The ID of the newly created shape.
    pub fn add_shape(
        &mut self,
        shape_type: ShapeType,
        x: f32,
        y: f32,
        rigid_body_set: &mut RigidBodySet,
        collider_set: &mut ColliderSet,
    ) -> u32 {
        let id = self.next_id;
        self.next_id += 1;

        let mut shape = Shape::new(id, shape_type, x, y);

        let (body_handle, collider_handle) =
            create_shape_collider(&shape, rigid_body_set, collider_set);
        shape.set_physics_handles(body_handle, collider_handle);

        self.shapes.push(shape);
        id
    }

    /// Removes a shape by ID.
    ///
    /// Also removes the physics collider.
    ///
    /// # Arguments
    ///
    /// * `id` - The shape ID to remove
    /// * `rigid_body_set` - Physics rigid body set
    /// * `collider_set` - Physics collider set
    /// * `island_manager` - Physics island manager
    /// * `impulse_joint_set` - Physics impulse joint set
    /// * `multibody_joint_set` - Physics multibody joint set
    ///
    /// # Returns
    ///
    /// `true` if the shape was found and removed.
    #[allow(clippy::too_many_arguments)]
    pub fn remove_shape(
        &mut self,
        id: u32,
        rigid_body_set: &mut RigidBodySet,
        collider_set: &mut ColliderSet,
        island_manager: &mut IslandManager,
        impulse_joint_set: &mut ImpulseJointSet,
        multibody_joint_set: &mut MultibodyJointSet,
    ) -> bool {
        if let Some(idx) = self.shapes.iter().position(|s| s.id() == id) {
            let shape = &self.shapes[idx];

            remove_shape_collider(
                shape,
                rigid_body_set,
                collider_set,
                island_manager,
                impulse_joint_set,
                multibody_joint_set,
            );

            if self.selected_id == Some(id) {
                self.selected_id = None;
            }

            self.shapes.remove(idx);
            true
        } else {
            false
        }
    }

    /// Removes all shapes.
    ///
    /// # Arguments
    ///
    /// * `rigid_body_set` - Physics rigid body set
    /// * `collider_set` - Physics collider set
    /// * `island_manager` - Physics island manager
    /// * `impulse_joint_set` - Physics impulse joint set
    /// * `multibody_joint_set` - Physics multibody joint set
    pub fn clear_all(
        &mut self,
        rigid_body_set: &mut RigidBodySet,
        collider_set: &mut ColliderSet,
        island_manager: &mut IslandManager,
        impulse_joint_set: &mut ImpulseJointSet,
        multibody_joint_set: &mut MultibodyJointSet,
    ) {
        for shape in &self.shapes {
            remove_shape_collider(
                shape,
                rigid_body_set,
                collider_set,
                island_manager,
                impulse_joint_set,
                multibody_joint_set,
            );
        }
        self.shapes.clear();
        self.selected_id = None;
    }

    /// Selects the shape at the given position.
    ///
    /// Uses physics collision detection to find the shape under the click.
    ///
    /// # Arguments
    ///
    /// * `x` - X position in physics coordinates
    /// * `y` - Y position in physics coordinates
    /// * `collider_set` - Physics collider set for hit testing
    ///
    /// # Returns
    ///
    /// The ID of the selected shape, or None if no shape was hit.
    pub fn select_at(&mut self, x: f32, y: f32, collider_set: &ColliderSet) -> Option<u32> {
        // Clear previous selection
        if let Some(old_id) = self.selected_id {
            if let Some(shape) = self.shapes.iter_mut().find(|s| s.id() == old_id) {
                shape.set_selected(false);
            }
        }

        // Find shape under click
        // Use a simple distance-based check to the shape center
        let mut best_match: Option<(u32, f32)> = None;

        for shape in &self.shapes {
            let (cx, cy) = shape.position();
            let dx = x - cx;
            let dy = y - cy;
            let dist_sq = dx * dx + dy * dy;

            // Get shape size for hit radius
            let ascii_art = get_ascii_art(shape.shape_type());
            let hit_radius = (ascii_art.width().max(ascii_art.height()) as f32 / 2.0) + 1.0;

            if dist_sq < hit_radius * hit_radius {
                match best_match {
                    Some((_, best_dist)) if dist_sq < best_dist => {
                        best_match = Some((shape.id(), dist_sq));
                    }
                    None => {
                        best_match = Some((shape.id(), dist_sq));
                    }
                    _ => {}
                }
            }
        }

        // Also check using collider point containment
        for shape in &self.shapes {
            if let Some(collider_handle) = shape.collider_handle() {
                if let Some(collider) = collider_set.get(collider_handle) {
                    let point = point![x, y];
                    let local_point = collider.position().inverse_transform_point(&point);

                    if collider.shape().contains_local_point(&local_point) {
                        // Prefer exact collider hit over distance-based
                        let (cx, cy) = shape.position();
                        let dx = x - cx;
                        let dy = y - cy;
                        let dist_sq = dx * dx + dy * dy;

                        match best_match {
                            Some((_, best_dist)) if dist_sq < best_dist => {
                                best_match = Some((shape.id(), dist_sq));
                            }
                            None => {
                                best_match = Some((shape.id(), dist_sq));
                            }
                            _ => {}
                        }
                    }
                }
            }
        }

        // Update selection
        if let Some((id, _)) = best_match {
            if let Some(shape) = self.shapes.iter_mut().find(|s| s.id() == id) {
                shape.set_selected(true);
            }
            self.selected_id = Some(id);
            Some(id)
        } else {
            self.selected_id = None;
            None
        }
    }

    /// Deselects the current shape.
    pub fn deselect(&mut self) {
        if let Some(id) = self.selected_id {
            if let Some(shape) = self.shapes.iter_mut().find(|s| s.id() == id) {
                shape.set_selected(false);
            }
        }
        self.selected_id = None;
    }

    /// Rotates the selected shape clockwise by 45 degrees.
    ///
    /// # Arguments
    ///
    /// * `rigid_body_set` - Physics rigid body set for updating transform
    ///
    /// # Returns
    ///
    /// `true` if a shape was rotated.
    pub fn rotate_selected_clockwise(&mut self, rigid_body_set: &mut RigidBodySet) -> bool {
        if let Some(shape) = self.selected_shape_mut() {
            shape.rotate_clockwise();

            // Update physics transform
            if let Some(handle) = shape.rigid_body_handle() {
                if let Some(body) = rigid_body_set.get_mut(handle) {
                    let (x, y) = shape.position();
                    let rotation = shape.rotation_radians();
                    let isometry = Isometry::new(vector![x, y], rotation);
                    body.set_position(isometry, true);
                }
            }
            true
        } else {
            false
        }
    }

    /// Rotates the selected shape counter-clockwise by 90 degrees.
    ///
    /// # Arguments
    ///
    /// * `rigid_body_set` - Physics rigid body set for updating transform
    ///
    /// # Returns
    ///
    /// `true` if a shape was rotated.
    pub fn rotate_selected_counter_clockwise(&mut self, rigid_body_set: &mut RigidBodySet) -> bool {
        if let Some(shape) = self.selected_shape_mut() {
            shape.rotate_counter_clockwise();

            // Update physics transform
            if let Some(handle) = shape.rigid_body_handle() {
                if let Some(body) = rigid_body_set.get_mut(handle) {
                    let (x, y) = shape.position();
                    let rotation = shape.rotation_radians();
                    let isometry = Isometry::new(vector![x, y], rotation);
                    body.set_position(isometry, true);
                }
            }
            true
        } else {
            false
        }
    }

    /// Moves the selected shape by the given delta.
    ///
    /// # Arguments
    ///
    /// * `dx` - X movement in physics units
    /// * `dy` - Y movement in physics units
    /// * `rigid_body_set` - Physics rigid body set for updating transform
    ///
    /// # Returns
    ///
    /// `true` if a shape was moved.
    pub fn move_selected(&mut self, dx: f32, dy: f32, rigid_body_set: &mut RigidBodySet) -> bool {
        if let Some(shape) = self.selected_shape_mut() {
            let (x, y) = shape.position();
            shape.set_position(x + dx, y + dy);

            // Update physics transform
            if let Some(handle) = shape.rigid_body_handle() {
                if let Some(body) = rigid_body_set.get_mut(handle) {
                    let (new_x, new_y) = shape.position();
                    let rotation = shape.rotation_radians();
                    let isometry = Isometry::new(vector![new_x, new_y], rotation);
                    body.set_position(isometry, true);
                }
            }
            true
        } else {
            false
        }
    }

    /// Moves the selected shape to an absolute position.
    ///
    /// # Arguments
    ///
    /// * `x` - New X position in physics units
    /// * `y` - New Y position in physics units
    /// * `rigid_body_set` - Physics rigid body set for updating transform
    ///
    /// # Returns
    ///
    /// `true` if a shape was moved.
    pub fn move_selected_to(&mut self, x: f32, y: f32, rigid_body_set: &mut RigidBodySet) -> bool {
        if let Some(shape) = self.selected_shape_mut() {
            shape.set_position(x, y);

            // Update physics transform
            if let Some(handle) = shape.rigid_body_handle() {
                if let Some(body) = rigid_body_set.get_mut(handle) {
                    let rotation = shape.rotation_radians();
                    let isometry = Isometry::new(vector![x, y], rotation);
                    body.set_position(isometry, true);
                }
            }
            true
        } else {
            false
        }
    }

    /// Cycles the selected shape's color forward.
    ///
    /// # Returns
    ///
    /// `true` if a shape's color was changed.
    pub fn cycle_selected_color_forward(&mut self) -> bool {
        if let Some(shape) = self.selected_shape_mut() {
            shape.cycle_color_forward();
            true
        } else {
            false
        }
    }

    /// Cycles the selected shape's color backward.
    ///
    /// # Returns
    ///
    /// `true` if a shape's color was changed.
    pub fn cycle_selected_color_backward(&mut self) -> bool {
        if let Some(shape) = self.selected_shape_mut() {
            shape.cycle_color_backward();
            true
        } else {
            false
        }
    }

    /// Removes the currently selected shape.
    ///
    /// # Arguments
    ///
    /// * `rigid_body_set` - Physics rigid body set
    /// * `collider_set` - Physics collider set
    /// * `island_manager` - Physics island manager
    /// * `impulse_joint_set` - Physics impulse joint set
    /// * `multibody_joint_set` - Physics multibody joint set
    ///
    /// # Returns
    ///
    /// `true` if a shape was removed.
    #[allow(clippy::too_many_arguments)]
    pub fn remove_selected(
        &mut self,
        rigid_body_set: &mut RigidBodySet,
        collider_set: &mut ColliderSet,
        island_manager: &mut IslandManager,
        impulse_joint_set: &mut ImpulseJointSet,
        multibody_joint_set: &mut MultibodyJointSet,
    ) -> bool {
        if let Some(id) = self.selected_id {
            self.remove_shape(
                id,
                rigid_body_set,
                collider_set,
                island_manager,
                impulse_joint_set,
                multibody_joint_set,
            )
        } else {
            false
        }
    }

    /// Finds a valid random position for a shape.
    ///
    /// Avoids:
    /// - Spawn zone (top 1/4 of canvas)
    /// - Edges (within EDGE_MARGIN)
    /// - Other shapes (within MIN_SHAPE_SEPARATION)
    ///
    /// # Arguments
    ///
    /// * `shape_type` - The type of shape to place
    /// * `world_width` - Physics world width
    /// * `world_height` - Physics world height
    /// * `max_attempts` - Maximum placement attempts before giving up
    ///
    /// # Returns
    ///
    /// `Some((x, y))` if a valid position was found, `None` otherwise.
    pub fn find_random_position(
        &self,
        shape_type: ShapeType,
        world_width: f32,
        world_height: f32,
        max_attempts: u32,
    ) -> Option<(f32, f32)> {
        let mut rng = rand::thread_rng();

        // Get shape dimensions for placement bounds
        let ascii_art = get_ascii_art(shape_type);
        let half_width = ascii_art.width() as f32 / 2.0;
        let half_height = ascii_art.height() as f32 / 2.0;

        // Spawn zone is top 1/4 of world (in physics coords, Y-up)
        let spawn_zone_bottom = world_height * 0.75;

        // Valid placement range (avoiding edges and spawn zone)
        let min_x = EDGE_MARGIN + half_width;
        let max_x = world_width - EDGE_MARGIN - half_width;
        let min_y = EDGE_MARGIN + half_height;
        let max_y = spawn_zone_bottom - half_height;

        // Check if there's any valid space
        if min_x >= max_x || min_y >= max_y {
            return None;
        }

        for _ in 0..max_attempts {
            let x = rng.gen_range(min_x..max_x);
            let y = rng.gen_range(min_y..max_y);

            // Create temporary shape for overlap testing
            let temp_shape = Shape::new(0, shape_type, x, y);

            // Check overlap with existing shapes
            let overlaps = self
                .shapes
                .iter()
                .any(|existing| shapes_overlap(&temp_shape, existing, MIN_SHAPE_SEPARATION));

            if !overlaps {
                return Some((x, y));
            }
        }

        None
    }

    /// Places a random shape at a valid position.
    ///
    /// # Arguments
    ///
    /// * `world_width` - Physics world width
    /// * `world_height` - Physics world height
    /// * `rigid_body_set` - Physics rigid body set
    /// * `collider_set` - Physics collider set
    ///
    /// # Returns
    ///
    /// The ID of the placed shape, or `None` if no valid position was found.
    pub fn place_random_shape(
        &mut self,
        world_width: f32,
        world_height: f32,
        rigid_body_set: &mut RigidBodySet,
        collider_set: &mut ColliderSet,
    ) -> Option<u32> {
        let mut rng = rand::thread_rng();

        let shape_types = ShapeType::all();
        let shape_type = shape_types[rng.gen_range(0..shape_types.len())];

        let position = self.find_random_position(shape_type, world_width, world_height, 100)?;

        let id = self.add_shape(
            shape_type,
            position.0,
            position.1,
            rigid_body_set,
            collider_set,
        );
        Some(id)
    }

    /// Places multiple random shapes on app load.
    ///
    /// Places 2-4 shapes with proper separation.
    ///
    /// # Arguments
    ///
    /// * `world_width` - Physics world width
    /// * `world_height` - Physics world height
    /// * `rigid_body_set` - Physics rigid body set
    /// * `collider_set` - Physics collider set
    ///
    /// # Returns
    ///
    /// The number of shapes successfully placed.
    pub fn place_initial_shapes(
        &mut self,
        world_width: f32,
        world_height: f32,
        rigid_body_set: &mut RigidBodySet,
        collider_set: &mut ColliderSet,
    ) -> usize {
        let mut rng = rand::thread_rng();
        let count = rng.gen_range(2..=4);
        let mut placed = 0;

        for _ in 0..count {
            if self
                .place_random_shape(world_width, world_height, rigid_body_set, collider_set)
                .is_some()
            {
                placed += 1;
            }
        }

        placed
    }

    /// Gets a shape by ID.
    pub fn get_shape(&self, id: u32) -> Option<&Shape> {
        self.shapes.iter().find(|s| s.id() == id)
    }

    /// Gets a mutable shape by ID.
    pub fn get_shape_mut(&mut self, id: u32) -> Option<&mut Shape> {
        self.shapes.iter_mut().find(|s| s.id() == id)
    }
}

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

    #[test]
    fn test_new_manager() {
        let manager = ShapeManager::new();
        assert_eq!(manager.shape_count(), 0);
        assert!(manager.selected_id().is_none());
    }

    #[test]
    fn test_find_random_position() {
        let manager = ShapeManager::new();

        // Should find a position in a large world
        let pos = manager.find_random_position(ShapeType::Square, 100.0, 100.0, 10);
        assert!(pos.is_some());

        let (x, y) = pos.unwrap();
        // Should be within bounds
        assert!(x > 0.0 && x < 100.0);
        assert!(y > 0.0 && y < 75.0); // Below spawn zone
    }

    #[test]
    fn test_find_random_position_small_world() {
        let manager = ShapeManager::new();

        // Should fail in a tiny world
        let pos = manager.find_random_position(ShapeType::Square, 5.0, 5.0, 10);
        assert!(pos.is_none());
    }
}
