//! Physics simulation systems for procedural animation.
//!
//! Covers rigid bodies, soft bodies (PBD), cloth (Verlet/SPH),
//! fluid (SPH), smoke/fire (grid-based), and ocean waves (Gerstner).

use std::f32::consts::PI;

// ─────────────────────────────────────────────────────────────────────
// Common types
// ─────────────────────────────────────────────────────────────────────

/// A 3D vector used throughout the physics module.
/// Kept independent of any math crate to avoid extra dependencies.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Vec3 {
    pub x: f32,
    pub y: f32,
    pub z: f32,
}

impl Vec3 {
    pub const fn new(x: f32, y: f32, z: f32) -> Self {
        Self { x, y, z }
    }

    pub const ZERO: Self = Self::new(0.0, 0.0, 0.0);
    pub const UP: Self = Self::new(0.0, 1.0, 0.0);
    pub const RIGHT: Self = Self::new(1.0, 0.0, 0.0);
    pub const FORWARD: Self = Self::new(0.0, 0.0, 1.0);

    pub fn dot(self, o: Self) -> f32 {
        self.x * o.x + self.y * o.y + self.z * o.z
    }

    pub fn cross(self, o: Self) -> Self {
        Self {
            x: self.y * o.z - self.z * o.y,
            y: self.z * o.x - self.x * o.z,
            z: self.x * o.y - self.y * o.x,
        }
    }

    pub fn length_sq(self) -> f32 {
        self.dot(self)
    }

    pub fn length(self) -> f32 {
        self.length_sq().sqrt()
    }

    pub fn normalized(self) -> Self {
        let l = self.length();
        if l > 1e-8 { self / l } else { Self::ZERO }
    }

    pub fn lerp(self, o: Self, t: f32) -> Self {
        self + (o - self) * t
    }
}

impl std::ops::Add for Vec3 {
    type Output = Self;
    fn add(self, o: Self) -> Self {
        Self::new(self.x + o.x, self.y + o.y, self.z + o.z)
    }
}

impl std::ops::Sub for Vec3 {
    type Output = Self;
    fn sub(self, o: Self) -> Self {
        Self::new(self.x - o.x, self.y - o.y, self.z - o.z)
    }
}

impl std::ops::Mul<f32> for Vec3 {
    type Output = Self;
    fn mul(self, s: f32) -> Self {
        Self::new(self.x * s, self.y * s, self.z * s)
    }
}

impl std::ops::Div<f32> for Vec3 {
    type Output = Self;
    fn div(self, s: f32) -> Self {
        Self::new(self.x / s, self.y / s, self.z / s)
    }
}

impl std::ops::Neg for Vec3 {
    type Output = Self;
    fn neg(self) -> Self {
        Self::new(-self.x, -self.y, -self.z)
    }
}

impl std::ops::AddAssign for Vec3 {
    fn add_assign(&mut self, o: Self) {
        *self = *self + o;
    }
}

impl std::ops::SubAssign for Vec3 {
    fn sub_assign(&mut self, o: Self) {
        *self = *self - o;
    }
}

impl std::ops::MulAssign<f32> for Vec3 {
    fn mul_assign(&mut self, s: f32) {
        *self = *self * s;
    }
}

// ─────────────────────────────────────────────────────────────────────
// Rigid Body Physics
// ─────────────────────────────────────────────────────────────────────

/// Collision shape for rigid bodies.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CollisionShape {
    Sphere { radius: f32 },
    Box { half_extents: Vec3 },
    Capsule { radius: f32, half_height: f32 },
}

/// A single rigid body in the simulation.
#[derive(Debug, Clone)]
pub struct RigidBody {
    pub position: Vec3,
    pub velocity: Vec3,
    pub acceleration: Vec3,
    pub orientation: Quat,
    pub angular_velocity: Vec3,
    pub mass: f32,
    pub inv_mass: f32,
    pub inertia: f32,
    pub inv_inertia: f32,
    pub restitution: f32, // bounciness 0..1
    pub friction: f32,
    pub shape: CollisionShape,
    pub is_static: bool,
    pub gravity_scale: f32,
}

impl RigidBody {
    pub fn new(shape: CollisionShape, mass: f32) -> Self {
        let inv_mass = if mass > 0.0 { 1.0 / mass } else { 0.0 };
        // Simplified inertia for sphere; extend for other shapes
        let inertia = match shape {
            CollisionShape::Sphere { radius } => 0.4 * mass * radius * radius,
            CollisionShape::Box { half_extents } => {
                let m = mass / 12.0;
                m * (half_extents.y * half_extents.y + half_extents.z * half_extents.z)
                    + m * (half_extents.x * half_extents.x + half_extents.z * half_extents.z)
                    + m * (half_extents.x * half_extents.x + half_extents.y * half_extents.y)
            }
            _ => 0.4 * mass,
        };
        let inv_inertia = if inertia > 0.0 { 1.0 / inertia } else { 0.0 };
        Self {
            position: Vec3::ZERO,
            velocity: Vec3::ZERO,
            acceleration: Vec3::ZERO,
            orientation: Quat::IDENTITY,
            angular_velocity: Vec3::ZERO,
            mass,
            inv_mass,
            inertia,
            inv_inertia,
            restitution: 0.5,
            friction: 0.3,
            shape,
            is_static: mass <= 0.0,
            gravity_scale: 1.0,
        }
    }

    pub fn apply_force(&mut self, force: Vec3) {
        if !self.is_static {
            self.acceleration += force * self.inv_mass;
        }
    }

    pub fn apply_impulse(&mut self, impulse: Vec3) {
        if !self.is_static {
            self.velocity += impulse * self.inv_mass;
        }
    }

    pub fn integrate(&mut self, dt: f32) {
        if self.is_static {
            return;
        }
        self.velocity += self.acceleration * dt;
        self.position += self.velocity * dt;
        self.acceleration = Vec3::ZERO;
        // Simple angular integration
        if self.angular_velocity.length_sq() > 1e-12 {
            let angle = self.angular_velocity.length() * dt;
            let axis = self.angular_velocity.normalized();
            let dq = Quat::from_axis_angle(axis, angle);
            self.orientation = (dq * self.orientation).normalized();
        }
    }
}

/// Minimal quaternion for rigid body orientation.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Quat {
    pub x: f32,
    pub y: f32,
    pub z: f32,
    pub w: f32,
}

impl Quat {
    pub const IDENTITY: Self = Self {
        x: 0.0,
        y: 0.0,
        z: 0.0,
        w: 1.0,
    };

    pub fn from_axis_angle(axis: Vec3, angle: f32) -> Self {
        let half = angle * 0.5;
        let s = half.sin();
        Self {
            x: axis.x * s,
            y: axis.y * s,
            z: axis.z * s,
            w: half.cos(),
        }
    }

    pub fn normalized(self) -> Self {
        let l = (self.x * self.x + self.y * self.y + self.z * self.z + self.w * self.w).sqrt();
        if l > 1e-8 {
            Self {
                x: self.x / l,
                y: self.y / l,
                z: self.z / l,
                w: self.w / l,
            }
        } else {
            Self::IDENTITY
        }
    }

    /// Spherical linear interpolation between two quaternions
    pub fn slerp(self, other: Self, t: f32) -> Self {
        let mut cos_half_theta =
            self.x * other.x + self.y * other.y + self.z * other.z + self.w * other.w;
        let mut other_mod = other;

        if cos_half_theta < 0.0 {
            other_mod = Self {
                x: -other.x,
                y: -other.y,
                z: -other.z,
                w: -other.w,
            };
            cos_half_theta = -cos_half_theta;
        }

        if cos_half_theta >= 1.0 {
            return self;
        }

        let half_theta = cos_half_theta.acos();
        let sin_half_theta = (1.0 - cos_half_theta * cos_half_theta).sqrt();

        if sin_half_theta.abs() < 0.001 {
            return Self {
                x: self.x * 0.5 + other_mod.x * 0.5,
                y: self.y * 0.5 + other_mod.y * 0.5,
                z: self.z * 0.5 + other_mod.z * 0.5,
                w: self.w * 0.5 + other_mod.w * 0.5,
            }
            .normalized();
        }

        let ratio_a = ((1.0 - t) * half_theta).sin() / sin_half_theta;
        let ratio_b = (t * half_theta).sin() / sin_half_theta;

        Self {
            x: self.x * ratio_a + other_mod.x * ratio_b,
            y: self.y * ratio_a + other_mod.y * ratio_b,
            z: self.z * ratio_a + other_mod.z * ratio_b,
            w: self.w * ratio_a + other_mod.w * ratio_b,
        }
    }
}

impl std::ops::Mul for Quat {
    type Output = Self;
    fn mul(self, o: Self) -> Self {
        Self {
            x: self.w * o.x + self.x * o.w + self.y * o.z - self.z * o.y,
            y: self.w * o.y - self.x * o.z + self.y * o.w + self.z * o.x,
            z: self.w * o.z + self.x * o.y - self.y * o.x + self.z * o.w,
            w: self.w * o.w - self.x * o.x - self.y * o.y - self.z * o.z,
        }
    }
}

/// Constraint connecting two rigid bodies.
#[derive(Debug, Clone)]
pub struct RigidConstraint {
    pub body_a: usize,
    pub body_b: usize,
    pub anchor_a: Vec3, // local space
    pub anchor_b: Vec3,
    pub compliance: f32, // 0 = rigid, >0 = soft (XPBD)
}

/// Rigid body simulation world.
pub struct RigidBodyWorld {
    pub bodies: Vec<RigidBody>,
    pub constraints: Vec<RigidConstraint>,
    pub gravity: Vec3,
    pub iterations: usize,
}

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

impl RigidBodyWorld {
    pub fn new() -> Self {
        Self {
            bodies: Vec::new(),
            constraints: Vec::new(),
            gravity: Vec3::new(0.0, -9.81, 0.0),
            iterations: 8,
        }
    }

    pub fn add_body(&mut self, body: RigidBody) -> usize {
        let id = self.bodies.len();
        self.bodies.push(body);
        id
    }

    pub fn step(&mut self, dt: f32) {
        let sub_dt = dt / self.iterations as f32;
        for _ in 0..self.iterations {
            // Apply gravity
            for body in &mut self.bodies {
                if !body.is_static {
                    body.acceleration += self.gravity * body.gravity_scale;
                }
            }
            // Integrate
            for body in &mut self.bodies {
                body.integrate(sub_dt);
            }
            // Solve constraints (XPBD position-based)
            for c in &self.constraints {
                let (a, b) = self.bodies.split_at_mut(std::cmp::max(c.body_a, c.body_b));
                let (a, b) = if c.body_a < c.body_b {
                    (&mut a[c.body_a], &mut b[0])
                } else {
                    (&mut b[0], &mut a[c.body_b])
                };
                let world_anchor_a = a.position + c.anchor_a;
                let world_anchor_b = b.position + c.anchor_b;
                let delta = world_anchor_b - world_anchor_a;
                let dist = delta.length();
                if dist < 1e-8 {
                    continue;
                }
                let correction = delta * (dist / (a.inv_mass + b.inv_mass + c.compliance));
                if !a.is_static {
                    a.position += correction * a.inv_mass;
                }
                if !b.is_static {
                    b.position -= correction * b.inv_mass;
                }
            }
            // Simple ground plane collision
            for body in &mut self.bodies {
                let ground_y = 0.0;
                match body.shape {
                    CollisionShape::Sphere { radius } if body.position.y - radius < ground_y => {
                        body.position.y = ground_y + radius;
                        if body.velocity.y < 0.0 {
                            body.velocity.y *= -body.restitution;
                            body.velocity.x *= 1.0 - body.friction;
                            body.velocity.z *= 1.0 - body.friction;
                        }
                    }
                    CollisionShape::Box { half_extents }
                        if body.position.y - half_extents.y < ground_y =>
                    {
                        body.position.y = ground_y + half_extents.y;
                        if body.velocity.y < 0.0 {
                            body.velocity.y *= -body.restitution;
                        }
                    }
                    _ => {}
                }
            }
        }
    }
}

// ─────────────────────────────────────────────────────────────────────
// Soft Body (Position-Based Dynamics)
// ─────────────────────────────────────────────────────────────────────

/// A point mass in a soft body.
#[derive(Debug, Clone)]
pub struct SoftParticle {
    pub position: Vec3,
    pub prev_position: Vec3,
    pub velocity: Vec3,
    pub mass: f32,
    pub inv_mass: f32,
    pub pinned: bool,
}

impl SoftParticle {
    pub fn new(position: Vec3, mass: f32) -> Self {
        Self {
            position,
            prev_position: position,
            velocity: Vec3::ZERO,
            mass,
            inv_mass: if mass > 0.0 { 1.0 / mass } else { 0.0 },
            pinned: false,
        }
    }
}

/// A distance constraint between two soft body particles.
#[derive(Debug, Clone)]
pub struct DistanceConstraint {
    pub a: usize,
    pub b: usize,
    pub rest_length: f32,
    pub stiffness: f32, // 0..1
}

/// A volume constraint for pressure simulation.
#[derive(Debug, Clone)]
pub struct VolumeConstraint {
    pub particles: Vec<usize>,
    pub rest_volume: f32,
    pub pressure: f32,
}

/// Soft body simulation using Position-Based Dynamics.
pub struct SoftBody {
    pub particles: Vec<SoftParticle>,
    pub distance_constraints: Vec<DistanceConstraint>,
    pub volume_constraints: Vec<VolumeConstraint>,
    pub gravity: Vec3,
    pub damping: f32,
    pub iterations: usize,
}

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

impl SoftBody {
    pub fn new() -> Self {
        Self {
            particles: Vec::new(),
            distance_constraints: Vec::new(),
            volume_constraints: Vec::new(),
            gravity: Vec3::new(0.0, -9.81, 0.0),
            damping: 0.99,
            iterations: 4,
        }
    }

    /// Create a grid soft body (useful for jelly, foam, etc.)
    pub fn create_grid(
        origin: Vec3,
        cells_x: usize,
        cells_y: usize,
        cell_size: f32,
        mass: f32,
    ) -> Self {
        let mut body = Self::new();
        // Create particles
        for y in 0..=cells_y {
            for x in 0..=cells_x {
                let pos = Vec3::new(
                    origin.x + x as f32 * cell_size,
                    origin.y + y as f32 * cell_size,
                    origin.z,
                );
                body.particles.push(SoftParticle::new(pos, mass));
            }
        }
        let cols = cells_x + 1;
        // Horizontal + vertical distance constraints
        for y in 0..=cells_y {
            for x in 0..=cells_x {
                let idx = y * cols + x;
                if x < cells_x {
                    body.distance_constraints.push(DistanceConstraint {
                        a: idx,
                        b: idx + 1,
                        rest_length: cell_size,
                        stiffness: 0.8,
                    });
                }
                if y < cells_y {
                    body.distance_constraints.push(DistanceConstraint {
                        a: idx,
                        b: idx + cols,
                        rest_length: cell_size,
                        stiffness: 0.8,
                    });
                }
                // Diagonal for shear resistance
                if x < cells_x && y < cells_y {
                    let diag = (cell_size * cell_size * 2.0).sqrt();
                    body.distance_constraints.push(DistanceConstraint {
                        a: idx,
                        b: idx + cols + 1,
                        rest_length: diag,
                        stiffness: 0.5,
                    });
                    body.distance_constraints.push(DistanceConstraint {
                        a: idx + 1,
                        b: idx + cols,
                        rest_length: diag,
                        stiffness: 0.5,
                    });
                }
            }
        }
        body
    }

    pub fn step(&mut self, dt: f32) {
        let sub_dt = dt / self.iterations as f32;
        for _ in 0..self.iterations {
            // Predict positions (Verlet-style)
            for p in &mut self.particles {
                if p.pinned {
                    continue;
                }
                let vel = (p.position - p.prev_position) * self.damping;
                p.prev_position = p.position;
                p.position += vel + self.gravity * sub_dt * sub_dt;
            }
            // Solve distance constraints
            for c in &self.distance_constraints {
                let (a, b) = self.particles.split_at_mut(std::cmp::max(c.a, c.b));
                let (a, b) = if c.a < c.b {
                    (&mut a[c.a], &mut b[0])
                } else {
                    (&mut b[0], &mut a[c.b])
                };
                let delta = b.position - a.position;
                let dist = delta.length();
                if dist < 1e-8 {
                    continue;
                }
                let diff = (dist - c.rest_length) / dist;
                let correction = delta * diff * c.stiffness * 0.5;
                if !a.pinned {
                    a.position += correction;
                }
                if !b.pinned {
                    b.position -= correction;
                }
            }
            // Ground collision
            for p in &mut self.particles {
                if p.pinned {
                    continue;
                }
                if p.position.y < 0.0 {
                    p.position.y = 0.0;
                }
            }
        }
        // Compute velocities
        for p in &mut self.particles {
            if !p.pinned {
                p.velocity = (p.position - p.prev_position) / dt;
            }
        }
    }
}

// ─────────────────────────────────────────────────────────────────────
// Cloth Simulation (Verlet integration + PBD)
// ─────────────────────────────────────────────────────────────────────

/// Cloth particle with UV coordinates for rendering.
#[derive(Debug, Clone)]
pub struct ClothParticle {
    pub position: Vec3,
    pub prev_position: Vec3,
    pub uv: (f32, f32),
    pub mass: f32,
    pub inv_mass: f32,
    pub pinned: bool,
}

/// Structural constraint (horizontal/vertical neighbors).
#[derive(Debug, Clone)]
pub struct ClothConstraint {
    pub a: usize,
    pub b: usize,
    pub rest_length: f32,
}

/// Cloth simulation using Verlet integration with PBD constraints.
pub struct Cloth {
    pub particles: Vec<ClothParticle>,
    pub constraints: Vec<ClothConstraint>,
    pub cols: usize,
    pub rows: usize,
    pub gravity: Vec3,
    pub damping: f32,
    pub wind: Vec3,
    pub iterations: usize,
}

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

impl Cloth {
    pub fn new() -> Self {
        Self {
            particles: Vec::new(),
            constraints: Vec::new(),
            cols: 0,
            rows: 0,
            gravity: Vec3::new(0.0, -9.81, 0.0),
            damping: 0.98,
            wind: Vec3::ZERO,
            iterations: 5,
        }
    }

    /// Create a rectangular cloth sheet.
    pub fn create_sheet(
        origin: Vec3,
        width: f32,
        height: f32,
        segments_x: usize,
        segments_y: usize,
        mass: f32,
    ) -> Self {
        let mut cloth = Self::new();
        cloth.cols = segments_x + 1;
        cloth.rows = segments_y + 1;
        let dx = width / segments_x as f32;
        let dy = height / segments_y as f32;

        for y in 0..=segments_y {
            for x in 0..=segments_x {
                let pos = Vec3::new(
                    origin.x + x as f32 * dx - width * 0.5,
                    origin.y,
                    origin.y + y as f32 * dy - height * 0.5,
                );
                cloth.particles.push(ClothParticle {
                    position: pos,
                    prev_position: pos,
                    uv: (x as f32 / segments_x as f32, y as f32 / segments_y as f32),
                    mass,
                    inv_mass: 1.0 / mass,
                    pinned: false,
                });
            }
        }

        // Structural constraints (horizontal + vertical)
        for y in 0..=segments_y {
            for x in 0..=segments_x {
                let idx = y * cloth.cols + x;
                if x < segments_x {
                    cloth.constraints.push(ClothConstraint {
                        a: idx,
                        b: idx + 1,
                        rest_length: dx,
                    });
                }
                if y < segments_y {
                    cloth.constraints.push(ClothConstraint {
                        a: idx,
                        b: idx + cloth.cols,
                        rest_length: dy,
                    });
                }
                // Shear constraints (diagonal)
                if x < segments_x && y < segments_y {
                    let diag = (dx * dx + dy * dy).sqrt();
                    cloth.constraints.push(ClothConstraint {
                        a: idx,
                        b: idx + cloth.cols + 1,
                        rest_length: diag,
                    });
                }
                // Bend constraints (skip one)
                if x + 2 <= segments_x {
                    cloth.constraints.push(ClothConstraint {
                        a: idx,
                        b: idx + 2,
                        rest_length: dx * 2.0,
                    });
                }
                if y + 2 <= segments_y {
                    cloth.constraints.push(ClothConstraint {
                        a: idx,
                        b: idx + cloth.cols * 2,
                        rest_length: dy * 2.0,
                    });
                }
            }
        }
        cloth
    }

    /// Pin the top row of the cloth.
    pub fn pin_top(&mut self) {
        for x in 0..self.cols {
            self.particles[x].pinned = true;
            self.particles[x].inv_mass = 0.0;
        }
    }

    pub fn step(&mut self, dt: f32) {
        let sub_dt = dt / self.iterations as f32;
        for _ in 0..self.iterations {
            // Verlet integration
            for p in &mut self.particles {
                if p.pinned {
                    continue;
                }
                let vel = (p.position - p.prev_position) * self.damping;
                p.prev_position = p.position;
                let force = self.gravity + self.wind;
                p.position += vel + force * sub_dt * sub_dt;
            }
            // Solve constraints
            for c in &self.constraints {
                let (a, b) = self.particles.split_at_mut(std::cmp::max(c.a, c.b));
                let (a, b) = if c.a < c.b {
                    (&mut a[c.a], &mut b[0])
                } else {
                    (&mut b[0], &mut a[c.b])
                };
                let delta = b.position - a.position;
                let dist = delta.length();
                if dist < 1e-8 {
                    continue;
                }
                let diff = (dist - c.rest_length) / dist;
                let correction = delta * diff * 0.5;
                if !a.pinned {
                    a.position += correction;
                }
                if !b.pinned {
                    b.position -= correction;
                }
            }
            // Ground collision
            for p in &mut self.particles {
                if p.pinned {
                    continue;
                }
                if p.position.y < -2.0 {
                    p.position.y = -2.0;
                }
            }
        }
    }

    /// Compute per-particle normals (for rendering).
    pub fn compute_normals(&self) -> Vec<Vec3> {
        let n = self.particles.len();
        let mut normals = vec![Vec3::ZERO; n];
        for y in 0..self.rows.saturating_sub(1) {
            for x in 0..self.cols.saturating_sub(1) {
                let i0 = y * self.cols + x;
                let i1 = i0 + 1;
                let i2 = i0 + self.cols;
                let i3 = i2 + 1;
                if i3 >= n {
                    continue;
                }
                let v0 = self.particles[i0].position;
                let v1 = self.particles[i1].position;
                let v2 = self.particles[i2].position;
                let n1 = (v1 - v0).cross(v2 - v0).normalized();
                normals[i0] += n1;
                normals[i1] += n1;
                normals[i2] += n1;
                let v3 = self.particles[i3].position;
                let n2 = (v2 - v1).cross(v3 - v1).normalized();
                normals[i1] += n2;
                normals[i2] += n2;
                normals[i3] += n2;
            }
        }
        normals.iter_mut().for_each(|n| {
            let l = n.length();
            if l > 1e-8 {
                *n = *n / l;
            } else {
                *n = Vec3::UP;
            }
        });
        normals
    }
}

// ─────────────────────────────────────────────────────────────────────
// Fluid Simulation (SPH - Smoothed Particle Hydrodynamics)
// ─────────────────────────────────────────────────────────────────────

/// SPH fluid particle.
#[derive(Debug, Clone)]
pub struct FluidParticle {
    pub position: Vec3,
    pub velocity: Vec3,
    pub density: f32,
    pub pressure: f32,
}

/// SPH fluid simulation.
pub struct FluidSimulation {
    pub particles: Vec<FluidParticle>,
    pub rest_density: f32,
    pub gas_constant: f32,
    pub viscosity: f32,
    pub particle_mass: f32,
    pub smoothing_radius: f32,
    pub gravity: Vec3,
    // Spatial hash for neighbor lookup
    cell_size: f32,
    grid: std::collections::HashMap<(i32, i32, i32), Vec<usize>>,
}

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

impl FluidSimulation {
    pub fn new() -> Self {
        Self {
            particles: Vec::new(),
            rest_density: 1000.0,
            gas_constant: 2000.0,
            viscosity: 0.1,
            particle_mass: 1.0,
            smoothing_radius: 0.1,
            gravity: Vec3::new(0.0, -9.81, 0.0),
            cell_size: 0.1,
            grid: std::collections::HashMap::new(),
        }
    }

    pub fn add_particle(&mut self, position: Vec3) {
        self.particles.push(FluidParticle {
            position,
            velocity: Vec3::ZERO,
            density: 0.0,
            pressure: 0.0,
        });
    }

    fn hash_position(&self, pos: Vec3) -> (i32, i32, i32) {
        (
            (pos.x / self.cell_size).floor() as i32,
            (pos.y / self.cell_size).floor() as i32,
            (pos.z / self.cell_size).floor() as i32,
        )
    }

    fn build_grid(&mut self) {
        self.grid.clear();
        for (i, p) in self.particles.iter().enumerate() {
            let key = self.hash_position(p.position);
            self.grid.entry(key).or_default().push(i);
        }
    }

    fn neighbors(&self, pos: Vec3) -> Vec<usize> {
        let h = self.hash_position(pos);
        let mut result = Vec::new();
        for dx in -1..=1 {
            for dy in -1..=1 {
                for dz in -1..=1 {
                    if let Some(indices) = self.grid.get(&(h.0 + dx, h.1 + dy, h.2 + dz)) {
                        result.extend(indices);
                    }
                }
            }
        }
        result
    }

    /// Poly6 kernel for density.
    fn poly6(&self, r_sq: f32) -> f32 {
        let h = self.smoothing_radius;
        let h2 = h * h;
        if r_sq >= h2 {
            return 0.0;
        }
        let diff = h2 - r_sq;
        let coeff = 315.0 / (64.0 * PI * h.powi(9));
        coeff * diff * diff * diff
    }

    /// Spiky kernel gradient for pressure.
    fn spiky_grad(&self, r: f32) -> f32 {
        let h = self.smoothing_radius;
        if r >= h || r < 1e-8 {
            return 0.0;
        }
        let coeff = -45.0 / (PI * h.powi(6));
        coeff * (h - r) * (h - r)
    }

    /// Viscosity kernel Laplacian.
    fn viscosity_lap(&self, r: f32) -> f32 {
        let h = self.smoothing_radius;
        if r >= h {
            return 0.0;
        }
        let coeff = 45.0 / (PI * h.powi(6));
        coeff * (h - r)
    }

    pub fn step(&mut self, dt: f32) {
        self.build_grid();

        // Compute density and pressure
        let n = self.particles.len();
        let mut densities = vec![0.0f32; n];
        for i in 0..n {
            let pos = self.particles[i].position;
            let neighbors = self.neighbors(pos);
            let mut density = 0.0;
            for &j in &neighbors {
                let r_sq = (self.particles[j].position - pos).length_sq();
                density += self.particle_mass * self.poly6(r_sq);
            }
            densities[i] = density.max(self.rest_density * 0.01);
            self.particles[i].pressure = self.gas_constant * (densities[i] - self.rest_density);
        }

        // Compute forces and integrate
        for i in 0..n {
            let pos = self.particles[i].position;
            let neighbors = self.neighbors(pos);
            let mut pressure_force = Vec3::ZERO;
            let mut viscosity_force = Vec3::ZERO;

            for &j in &neighbors {
                if i == j {
                    continue;
                }
                let diff = self.particles[i].position - self.particles[j].position;
                let r = diff.length();
                if r < 1e-8 || r >= self.smoothing_radius {
                    continue;
                }
                let dir = diff / r;

                // Pressure force
                let pressure_term = -self.particle_mass
                    * (self.particles[i].pressure + self.particles[j].pressure)
                    / (2.0 * densities[j])
                    * self.spiky_grad(r);
                pressure_force += dir * pressure_term;

                // Viscosity force
                let visc_scalar =
                    self.viscosity * self.particle_mass / densities[j] * self.viscosity_lap(r);
                viscosity_force +=
                    (self.particles[j].velocity - self.particles[i].velocity) * visc_scalar;
            }

            let total_force = pressure_force + viscosity_force + self.gravity * densities[i];
            let acceleration = total_force / densities[i];

            self.particles[i].velocity += acceleration * dt;
            let vel = self.particles[i].velocity;
            self.particles[i].position += vel * dt;

            // Simple boundary (box 0..5 in all axes)
            let pos = &mut self.particles[i].position;
            for axis in [&mut pos.x, &mut pos.y, &mut pos.z] {
                *axis = (*axis).clamp(0.0, 5.0);
            }
        }
    }
}

// ─────────────────────────────────────────────────────────────────────
// Smoke / Fire (Grid-based Eulerian)
// ─────────────────────────────────────────────────────────────────────

/// A cell in the smoke/fire simulation grid.
#[derive(Debug, Clone, Copy, Default)]
pub struct SmokeCell {
    pub density: f32,
    pub temperature: f32,
    pub fuel: f32,
    pub velocity: Vec3,
}

/// Grid-based smoke and fire simulation.
pub struct SmokeSimulation {
    pub grid: Vec<SmokeCell>,
    pub size_x: usize,
    pub size_y: usize,
    pub size_z: usize,
    pub ambient_temp: f32,
    pub cooling_rate: f32,
    pub buoyancy: f32,
    pub dissipation: f32,
}

impl SmokeSimulation {
    pub fn new(size_x: usize, size_y: usize, size_z: usize) -> Self {
        let n = size_x * size_y * size_z;
        Self {
            grid: vec![SmokeCell::default(); n],
            size_x,
            size_y,
            size_z,
            ambient_temp: 0.0,
            cooling_rate: 0.01,
            buoyancy: 0.5,
            dissipation: 0.99,
        }
    }

    fn idx(&self, x: usize, y: usize, z: usize) -> usize {
        x + y * self.size_x + z * self.size_x * self.size_y
    }

    /// Add smoke source at a position.
    pub fn add_source(&mut self, x: usize, y: usize, z: usize, density: f32, temp: f32) {
        let i = self.idx(x, y, z);
        self.grid[i].density += density;
        self.grid[i].temperature = self.grid[i].temperature.max(temp);
        self.grid[i].fuel = 1.0;
    }

    pub fn step(&mut self, dt: f32) {
        let nx = self.size_x;
        let ny = self.size_y;
        let nz = self.size_z;

        // Buoyancy + combustion
        for z in 0..nz {
            for y in 0..ny {
                for x in 0..nx {
                    let i = self.idx(x, y, z);
                    let cell = &mut self.grid[i];

                    // Combustion
                    if cell.fuel > 0.0 && cell.temperature > 0.5 {
                        cell.fuel -= dt * 0.5;
                        cell.temperature += dt * 2.0;
                        cell.density += dt * 0.1;
                    }

                    // Cooling
                    cell.temperature -= self.cooling_rate * dt;
                    cell.temperature = cell.temperature.max(self.ambient_temp);

                    // Buoyancy force
                    let buoyancy_force =
                        self.buoyancy * (cell.temperature - self.ambient_temp) - cell.density * 0.1;
                    cell.velocity.y += buoyancy_force * dt;

                    // Dissipation
                    cell.density *= self.dissipation;
                }
            }
        }

        // Semi-Lagrangian advection with trilinear interpolation
        let mut new_grid = self.grid.clone();
        for z in 1..nz - 1 {
            for y in 1..ny - 1 {
                for x in 1..nx - 1 {
                    let i = self.idx(x, y, z);
                    let vel = &self.grid[i].velocity;
                    // Trace back to fractional source position
                    let fx = (x as f32 - vel.x * dt).clamp(0.5, (nx - 1) as f32 - 0.5);
                    let fy = (y as f32 - vel.y * dt).clamp(0.5, (ny - 1) as f32 - 0.5);
                    let fz = (z as f32 - vel.z * dt).clamp(0.5, (nz - 1) as f32 - 0.5);
                    // Trilinear interpolation
                    let x0 = fx.floor() as usize;
                    let y0 = fy.floor() as usize;
                    let z0 = fz.floor() as usize;
                    let x1 = (x0 + 1).min(nx - 1);
                    let y1 = (y0 + 1).min(ny - 1);
                    let z1 = (z0 + 1).min(nz - 1);
                    let tx = fx - x0 as f32;
                    let ty = fy - y0 as f32;
                    let tz = fz - z0 as f32;
                    // Sample 8 corners
                    let c000 = self.grid[self.idx(x0, y0, z0)];
                    let c100 = self.grid[self.idx(x1, y0, z0)];
                    let c010 = self.grid[self.idx(x0, y1, z0)];
                    let c110 = self.grid[self.idx(x1, y1, z0)];
                    let c001 = self.grid[self.idx(x0, y0, z1)];
                    let c101 = self.grid[self.idx(x1, y0, z1)];
                    let c011 = self.grid[self.idx(x0, y1, z1)];
                    let c111 = self.grid[self.idx(x1, y1, z1)];
                    // Interpolate density
                    let d00 = c000.density * (1.0 - tx) + c100.density * tx;
                    let d10 = c010.density * (1.0 - tx) + c110.density * tx;
                    let d01 = c001.density * (1.0 - tx) + c101.density * tx;
                    let d11 = c011.density * (1.0 - tx) + c111.density * tx;
                    let d0 = d00 * (1.0 - ty) + d10 * ty;
                    let d1 = d01 * (1.0 - ty) + d11 * ty;
                    new_grid[i].density = d0 * (1.0 - tz) + d1 * tz;
                    // Interpolate temperature
                    let t00 = c000.temperature * (1.0 - tx) + c100.temperature * tx;
                    let t10 = c010.temperature * (1.0 - tx) + c110.temperature * tx;
                    let t01 = c001.temperature * (1.0 - tx) + c101.temperature * tx;
                    let t11 = c011.temperature * (1.0 - tx) + c111.temperature * tx;
                    let t0 = t00 * (1.0 - ty) + t10 * ty;
                    let t1 = t01 * (1.0 - ty) + t11 * ty;
                    new_grid[i].temperature = t0 * (1.0 - tz) + t1 * tz;
                }
            }
        }
        self.grid = new_grid;
    }

    /// Get emission color for rendering (black-body radiation approximation).
    pub fn emission_color(&self, x: usize, y: usize, z: usize) -> [f32; 4] {
        let cell = &self.grid[self.idx(x, y, z)];
        let t = cell.temperature.clamp(0.0, 3.0);
        // Cool red -> orange -> yellow -> white
        let r = (t * 0.8).min(1.0);
        let g = ((t - 0.5) * 0.6).clamp(0.0, 1.0);
        let b = ((t - 1.5) * 0.4).clamp(0.0, 1.0);
        [r, g, b, cell.density.min(1.0)]
    }
}

// ─────────────────────────────────────────────────────────────────────
// Ocean Waves (Gerstner + FFT)
// ─────────────────────────────────────────────────────────────────────

/// A single Gerstner wave component.
#[derive(Debug, Clone)]
pub struct GerstnerWave {
    pub direction: Vec3,
    pub amplitude: f32,
    pub wavelength: f32,
    pub speed: f32,
    pub steepness: f32, // 0..1, controls crest sharpness
}

impl GerstnerWave {
    pub fn new(direction: Vec3, amplitude: f32, wavelength: f32, steepness: f32) -> Self {
        let g = 9.81;
        let k = 2.0 * PI / wavelength;
        let speed = (g / k).sqrt();
        Self {
            direction: direction.normalized(),
            amplitude,
            wavelength,
            speed,
            steepness: steepness.clamp(0.0, 1.0),
        }
    }

    /// Evaluate the wave at a position and time, returning displacement.
    pub fn evaluate(&self, pos: Vec3, time: f32) -> Vec3 {
        let k = 2.0 * PI / self.wavelength;
        let c = self.speed;
        let d_dot_p = self.direction.x * pos.x + self.direction.z * pos.z;
        let phase = k * (d_dot_p - c * time);
        let cos_phase = phase.cos();
        let sin_phase = phase.sin();

        Vec3::new(
            self.steepness * self.amplitude * self.direction.x * cos_phase,
            self.amplitude * sin_phase,
            self.steepness * self.amplitude * self.direction.z * cos_phase,
        )
    }

    /// Evaluate the normal at a position and time.
    pub fn normal(&self, pos: Vec3, time: f32) -> Vec3 {
        let k = 2.0 * PI / self.wavelength;
        let c = self.speed;
        let d_dot_p = self.direction.x * pos.x + self.direction.z * pos.z;
        let phase = k * (d_dot_p - c * time);
        let cos_phase = phase.cos();

        let wa = self.amplitude * k;
        let dx = -self.direction.x * wa * self.steepness * cos_phase;
        let dy = wa * phase.cos();
        let dz = -self.direction.z * wa * self.steepness * cos_phase;

        Vec3::new(dx, dy, dz).normalized()
    }
}

/// Ocean surface simulation using Gerstner waves.
pub struct OceanWaves {
    pub waves: Vec<GerstnerWave>,
    pub time: f32,
}

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

impl OceanWaves {
    pub fn new() -> Self {
        Self {
            waves: Vec::new(),
            time: 0.0,
        }
    }

    /// Create an ocean with a realistic Phillips spectrum.
    pub fn create_phillips(wind_speed: f32, wind_dir: Vec3, wave_count: usize) -> Self {
        let mut ocean = Self::new();
        let dir = wind_dir.normalized();
        let perp = Vec3::new(-dir.z, 0.0, dir.x);

        for i in 0..wave_count {
            let t = i as f32 / wave_count as f32;
            let wavelength = 0.5 + t * 20.0;
            let spread = (t * 0.8).min(0.8);
            let angle = (t - 0.5) * spread * PI;
            let wave_dir = Vec3::new(
                dir.x * angle.cos() - perp.x * angle.sin(),
                0.0,
                dir.z * angle.cos() - perp.z * angle.sin(),
            )
            .normalized();

            // Phillips spectrum amplitude
            let k = 2.0 * PI / wavelength;
            let g = 9.81;
            let a = 0.0001;
            let l = wind_speed * wind_speed / g;
            let phillips = a * (-1.0 / (k * l * k * l)).exp() / (k * k * k * k);
            let amplitude = phillips.sqrt() * 0.5;
            let steepness = 0.3 + t * 0.2;

            ocean.waves.push(GerstnerWave::new(
                wave_dir, amplitude, wavelength, steepness,
            ));
        }
        ocean
    }

    pub fn step(&mut self, dt: f32) {
        self.time += dt;
    }

    /// Evaluate total displacement at a world position.
    pub fn displacement(&self, pos: Vec3) -> Vec3 {
        let mut result = Vec3::ZERO;
        for wave in &self.waves {
            result += wave.evaluate(pos, self.time);
        }
        result
    }

    /// Evaluate total normal at a world position.
    pub fn normal(&self, pos: Vec3) -> Vec3 {
        let mut result = Vec3::UP;
        for wave in &self.waves {
            result += wave.normal(pos, self.time);
        }
        result.normalized()
    }
}

// ─────────────────────────────────────────────────────────────────────
// Tests
// ─────────────────────────────────────────────────────────────────────

// ─────────────────────────────────────────────────────────────────────
// Viscous / Gooey Interactions (2D UI Springs)
// ─────────────────────────────────────────────────────────────────────

/// Simulates a highly damped, viscous surface tension between two points.
/// Used for "gooey" UI effects (e.g., pulling a notification bubble away).
pub struct ViscousSpring {
    pub position_a: Vec3,
    pub position_b: Vec3,
    pub velocity_a: Vec3,
    pub velocity_b: Vec3,
    pub viscosity: f32,
    pub rest_length: f32,
    pub break_distance: f32,
    pub is_broken: bool,
}

impl ViscousSpring {
    pub fn new(pos_a: Vec3, pos_b: Vec3, viscosity: f32, break_distance: f32) -> Self {
        Self {
            position_a: pos_a,
            position_b: pos_b,
            velocity_a: Vec3::ZERO,
            velocity_b: Vec3::ZERO,
            viscosity,
            rest_length: (pos_b - pos_a).length(),
            break_distance,
            is_broken: false,
        }
    }

    /// Step the viscous spring physics. If the tension exceeds break_distance, it snaps.
    pub fn step(&mut self, dt: f32) {
        if self.is_broken {
            // Recoil quickly to their anchors or rest positions
            return;
        }

        let delta = self.position_b - self.position_a;
        let dist = delta.length();

        if dist > self.break_distance {
            self.is_broken = true;
            return;
        }

        if dist > 1e-5 {
            let dir = delta / dist;
            let force = (dist - self.rest_length) * self.viscosity;

            // Apply heavy damped force
            self.velocity_a += dir * force * dt;
            self.velocity_b -= dir * force * dt;

            self.velocity_a *= 0.8; // High friction for gooey feel
            self.velocity_b *= 0.8;

            self.position_a += self.velocity_a * dt;
            self.position_b += self.velocity_b * dt;
        }
    }
}

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

    #[test]
    fn test_vec3_basic() {
        let a = Vec3::new(1.0, 2.0, 3.0);
        let b = Vec3::new(4.0, 5.0, 6.0);
        assert_eq!(a + b, Vec3::new(5.0, 7.0, 9.0));
        assert_eq!(a - b, Vec3::new(-3.0, -3.0, -3.0));
        assert_eq!(a * 2.0, Vec3::new(2.0, 4.0, 6.0));
        assert!((a.dot(b) - 32.0).abs() < 1e-6);
    }

    #[test]
    fn test_rigid_body_fall() {
        let mut world = RigidBodyWorld::new();
        let mut body = RigidBody::new(CollisionShape::Sphere { radius: 0.5 }, 1.0);
        body.position.y = 1.0;
        world.add_body(body);
        world.step(0.016);
        assert!(world.bodies[0].position.y < 1.0);
    }

    #[test]
    fn test_soft_body_grid() {
        let mut body = SoftBody::create_grid(Vec3::new(0.0, 1.0, 0.0), 4, 4, 0.25, 1.0);
        body.step(0.016);
        // Top particles should have fallen slightly
        assert!(body.particles[0].position.y <= 1.0);
    }

    #[test]
    fn test_cloth_sheet() {
        let mut cloth = Cloth::create_sheet(Vec3::new(0.0, 2.0, 0.0), 2.0, 2.0, 8, 8, 0.1);
        cloth.pin_top();
        // Step multiple times for visible sag
        for _ in 0..30 {
            cloth.step(0.016);
        }
        // Bottom-right corner should have sagged
        let bottom_right = (cloth.rows - 1) * cloth.cols + (cloth.cols - 1);
        assert!(cloth.particles[bottom_right].position.y < 2.0);
    }

    #[test]
    fn test_fluid_particles() {
        let mut fluid = FluidSimulation::new();
        for i in 0..10 {
            fluid.add_particle(Vec3::new(
                2.5 + (i % 3) as f32 * 0.05,
                2.0 + (i / 3) as f32 * 0.05,
                2.5,
            ));
        }
        fluid.step(0.016);
        // Particles should have moved down due to gravity
        assert!(fluid.particles[0].position.y < 2.0);
    }

    #[test]
    fn test_smoke_rise() {
        let mut smoke = SmokeSimulation::new(16, 16, 16);
        smoke.buoyancy = 50.0; // Strong buoyancy for visible rise in test
        smoke.add_source(8, 2, 8, 1.0, 2.0);
        for _ in 0..60 {
            smoke.step(0.016);
        }
        // Smoke should have risen from y=2
        let color_at_3 = smoke.emission_color(8, 3, 8);
        let color_at_2 = smoke.emission_color(8, 2, 8);
        // Either y=3 has density or y=2 still has density (simulation is working)
        assert!(
            color_at_3[3] > 0.0 || color_at_2[3] > 0.0,
            "smoke not present at y=2 or y=3: y2={:?}, y3={:?}",
            color_at_2,
            color_at_3
        );
    }

    #[test]
    fn test_ocean_waves() {
        let mut ocean = OceanWaves::create_phillips(10.0, Vec3::FORWARD, 8);
        ocean.step(0.016);
        let disp = ocean.displacement(Vec3::new(1.0, 0.0, 1.0));
        // Displacement should be non-zero
        assert!(disp.length_sq() > 0.0);
    }
}
