//! Física de jugador en primera persona sobre un [`VoxelGrid`] — el otro
//! ingrediente núcleo de un juego voxel (además del picking de [`raycast`]):
//! **caminar el mundo con gravedad y colisión**. Reusable por cualquier juego
//! voxel de la suite; no toca la GPU ni el render.
//!
//! Coordenadas en **espacio de grilla** (voxel = 1, mundo en `[0, dim]`), las
//! mismas que [`raycast`](crate::raycast) y `eye_mundo + dim/2`. El jugador es
//! una caja AABB parada sobre el terreno; `pos` es el **centro de los pies**
//! (x/z centrados, y en la base). La colisión se resuelve eje por eje
//! (move-and-resolve), el método clásico y robusto para voxels.
//!
//! La mirada (`yaw`/`pitch`) la lleva la app (compartida con la cámara
//! `orbit`/`fly`); acá viven sólo las helpers puras de base
//! ([`forward_h`]/[`right_h`]/[`look_dir`]) para derivar el vector de avance.

use llimphi_3d::glam::Vec3;
use llimphi_3d::VoxelGrid;

/// Semi-ancho del jugador en X/Z (la caja mide `2·HALF_W` de lado).
const HALF_W: f32 = 0.3;
/// Altura total de la caja (de los pies a la coronilla).
const HEIGHT: f32 = 1.7;
/// Altura del ojo sobre los pies (la cámara va acá).
const EYE: f32 = 1.55;
/// Aceleración de gravedad (voxels/s²).
const GRAVITY: f32 = 30.0;
/// Velocidad horizontal de caminata (voxels/s).
const MOVE_SPEED: f32 = 8.0;
/// Velocidad vertical inicial del salto (voxels/s).
const JUMP_SPEED: f32 = 9.0;
/// Margen para no “tocar” el voxel del plano siguiente por error de redondeo.
const EPS: f32 = 1e-3;

/// Avance horizontal unitario para `yaw` (en grilla): `yaw=0` mira a `+Z`.
pub fn forward_h(yaw: f32) -> Vec3 {
    let (s, c) = yaw.sin_cos();
    Vec3::new(s, 0.0, c)
}

/// "Derecha" horizontal unitaria para `yaw` (perpendicular a [`forward_h`],
/// con la convención de mano derecha del motor: a `yaw=0`, derecha = `+X`).
pub fn right_h(yaw: f32) -> Vec3 {
    let (s, c) = yaw.sin_cos();
    Vec3::new(c, 0.0, -s)
}

/// Dirección de mirada completa (incluye `pitch`), misma convención que
/// [`Camera3d::fly`](llimphi_3d::Camera3d::fly). Útil como `dir` del raycast.
pub fn look_dir(yaw: f32, pitch: f32) -> Vec3 {
    let (sy, cy) = yaw.sin_cos();
    let (sp, cp) = pitch.sin_cos();
    Vec3::new(cp * sy, sp, cp * cy)
}

/// Cuerpo físico AABB con velocidad, parado sobre el terreno. Lo usa tanto el
/// jugador (input directo) como los agentes ([`Critter`](crate::Critter), wander
/// automático) — misma física, distinta voluntad.
#[derive(Debug, Clone, Copy)]
pub struct Player {
    /// Centro de los pies (espacio de grilla).
    pub pos: Vec3,
    /// Velocidad actual (voxels/s).
    pub vel: Vec3,
    /// `true` si el último paso terminó apoyado en suelo (habilita el salto).
    pub on_ground: bool,
    /// Velocidad de caminata horizontal (voxels/s). Editable por cuerpo: el
    /// jugador anda rápido, un bicho puede pastar lento.
    pub speed: f32,
}

impl Player {
    /// Jugador parado con los pies en `feet` (centro de los pies, grilla).
    pub fn new(feet: Vec3) -> Self {
        Self {
            pos: feet,
            vel: Vec3::ZERO,
            on_ground: false,
            speed: MOVE_SPEED,
        }
    }

    /// Crea un jugador posado sobre la columna `(x, z)` del grid: pies un voxel
    /// por encima del suelo más alto (o sobre el piso `y=0` si la columna está
    /// vacía). Centrado en el voxel (`+0.5`).
    pub fn spawn_on(grid: &VoxelGrid, x: u32, z: u32) -> Self {
        let top = grid.height_at(x, z).map(|y| y as f32 + 1.0).unwrap_or(0.0);
        Self::new(Vec3::new(x as f32 + 0.5, top + EPS, z as f32 + 0.5))
    }

    /// Posición del ojo (cámara) en espacio de grilla.
    pub fn eye(&self) -> Vec3 {
        self.pos + Vec3::new(0.0, EYE, 0.0)
    }

    /// Avanza la física un paso `dt` segundos. `wish` es la dirección de
    /// caminata **horizontal** deseada (no necesita estar normalizada; se usa
    /// su dirección × [`MOVE_SPEED`]); `jump` solicita un salto (sólo prende si
    /// `on_ground`). Resuelve colisión contra `grid` eje por eje.
    pub fn step(&mut self, grid: &VoxelGrid, wish: Vec3, jump: bool, dt: f32) {
        let dt = dt.clamp(0.0, 0.05); // nunca un salto de física monstruoso

        // Velocidad horizontal: directa desde el deseo (sin inercia, simple).
        let flat = Vec3::new(wish.x, 0.0, wish.z);
        let h = if flat.length_squared() > 1e-6 {
            flat.normalize() * self.speed
        } else {
            Vec3::ZERO
        };
        self.vel.x = h.x;
        self.vel.z = h.z;

        // Gravedad + salto.
        self.vel.y -= GRAVITY * dt;
        if jump && self.on_ground {
            self.vel.y = JUMP_SPEED;
        }

        // Mover y resolver eje por eje. on_ground se re-evalúa cada paso.
        self.on_ground = false;
        self.move_axis(grid, 0, self.vel.x * dt);
        self.move_axis(grid, 1, self.vel.y * dt);
        self.move_axis(grid, 2, self.vel.z * dt);
    }

    /// AABB `[min, max]` del jugador en `pos`.
    fn aabb_at(&self, pos: Vec3) -> (Vec3, Vec3) {
        (
            Vec3::new(pos.x - HALF_W, pos.y, pos.z - HALF_W),
            Vec3::new(pos.x + HALF_W, pos.y + HEIGHT, pos.z + HALF_W),
        )
    }

    /// Intenta desplazar `amount` en `axis` (0=X,1=Y,2=Z); si la caja chocaría
    /// con un sólido, cancela el movimiento en ese eje y anula la velocidad
    /// (marcando `on_ground` si fue al bajar).
    fn move_axis(&mut self, grid: &VoxelGrid, axis: usize, amount: f32) {
        if amount == 0.0 {
            return;
        }
        let mut np = self.pos;
        np[axis] += amount;
        let (min, max) = self.aabb_at(np);
        if aabb_hits_solid(grid, min, max) {
            if axis == 1 && amount < 0.0 {
                self.on_ground = true;
            }
            self.vel[axis] = 0.0;
        } else {
            self.pos = np;
        }
    }
}

/// `true` si la caja `[min, max]` solapa algún voxel sólido del grid. Recorre
/// las celdas enteras que toca la caja (cada voxel `(i,j,k)` ocupa
/// `[i,i+1)³`).
fn aabb_hits_solid(grid: &VoxelGrid, min: Vec3, max: Vec3) -> bool {
    let x0 = min.x.floor() as i32;
    let y0 = min.y.floor() as i32;
    let z0 = min.z.floor() as i32;
    // `-EPS` para que tocar exacto el plano del voxel siguiente no lo cuente.
    let x1 = (max.x - EPS).floor() as i32;
    let y1 = (max.y - EPS).floor() as i32;
    let z1 = (max.z - EPS).floor() as i32;
    for z in z0..=z1 {
        for y in y0..=y1 {
            for x in x0..=x1 {
                if grid.is_solid(x, y, z) {
                    return true;
                }
            }
        }
    }
    false
}

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

    /// Piso sólido en `y=0` (una capa) sobre un grid 8³.
    fn grid_con_piso() -> VoxelGrid {
        let mut g = VoxelGrid::new([8, 8, 8]);
        for z in 0..8 {
            for x in 0..8 {
                g.set(x, 0, z, [100, 100, 100]);
            }
        }
        g
    }

    #[test]
    fn cae_y_se_apoya_en_el_piso() {
        let g = grid_con_piso();
        // Arranca flotando bien arriba.
        let mut p = Player::new(Vec3::new(4.5, 5.0, 4.5));
        for _ in 0..240 {
            p.step(&g, Vec3::ZERO, false, 1.0 / 60.0);
        }
        // El piso ocupa [0,1); los pies deben quedar apoyados a y≈1.
        assert!(p.on_ground, "debería terminar en el suelo");
        assert!((p.pos.y - 1.0).abs() < 0.05, "pies en y≈1, dio {}", p.pos.y);
    }

    #[test]
    fn una_pared_frena_el_avance() {
        let mut g = grid_con_piso();
        // Muro en x=6, toda la altura.
        for y in 0..8 {
            for z in 0..8 {
                g.set(6, y, z, [80, 80, 80]);
            }
        }
        let mut p = Player::new(Vec3::new(4.5, 1.0, 4.5));
        // Camina hacia +X contra el muro un buen rato.
        for _ in 0..120 {
            p.step(&g, Vec3::X, false, 1.0 / 60.0);
        }
        // No puede atravesar: su borde derecho (pos.x + HALF_W) queda en x<6.
        assert!(p.pos.x + HALF_W <= 6.0 + EPS, "atravesó el muro: x={}", p.pos.x);
        assert!(p.pos.x > 4.5, "debería haber avanzado algo hasta el muro");
    }

    #[test]
    fn salta_solo_desde_el_suelo() {
        let g = grid_con_piso();
        let mut p = Player::new(Vec3::new(4.5, 1.0, 4.5));
        // Un paso para asentar on_ground.
        p.step(&g, Vec3::ZERO, false, 1.0 / 60.0);
        assert!(p.on_ground);
        // Salta: la velocidad vertical se vuelve positiva y despega.
        p.step(&g, Vec3::ZERO, true, 1.0 / 60.0);
        assert!(p.vel.y > 0.0, "el salto debe dar velocidad ascendente");
        assert!(p.pos.y > 1.0, "debe despegar del piso");
        // En el aire, un segundo intento de salto no reinyecta velocidad.
        let antes = p.vel.y;
        p.step(&g, Vec3::ZERO, true, 1.0 / 60.0);
        assert!(p.vel.y < antes, "no debe re-saltar en el aire");
    }
}
