//! Deterministic fixed-step gameplay simulation.
//!
//! The sim crate owns gameplay state and movement. Client/editor crates feed it
//! protocol input intent and keep rendering, audio, UI, and tools outside this layer.
#![allow(clippy::type_complexity)]

use avian3d::prelude::*;
use bevy::prelude::*;
use bevy::time::Fixed;
use protocol::{PlayerInputIntent, ProtocolPlugin, SIM_DELTA_SECS, SIM_TICK_RATE_HZ};
use settings::SimTuning;
use shared::SharedTypesPlugin;

/// Tunable gameplay/movement constants, grouped for easy tweaking.
pub mod tuning {
    /// Capsule radius (meters).
    pub const PLAYER_RADIUS: f32 = 0.4;
    /// Length of the capsule's cylindrical section (meters).
    /// Total standing height is `PLAYER_LENGTH + 2 * PLAYER_RADIUS`.
    pub const PLAYER_LENGTH: f32 = 1.0;
    /// Camera (eye) offset above the capsule center while standing.
    pub const EYE_OFFSET_STAND: f32 = 0.6;
    /// Camera (eye) offset above the capsule center while crouching.
    pub const EYE_OFFSET_CROUCH: f32 = 0.1;

    /// Downward acceleration (m/s^2). Slightly stronger than Earth for snappy feel.
    pub const GRAVITY: f32 = 22.0;
    /// Ground walking speed (m/s).
    pub const WALK_SPEED: f32 = 6.0;
    /// Sprint speed (m/s).
    pub const RUN_SPEED: f32 = 9.5;
    /// Crouch speed (m/s).
    pub const CROUCH_SPEED: f32 = 3.0;
    /// Upward launch velocity on jump (m/s).
    pub const JUMP_SPEED: f32 = 7.5;

    /// Horizontal acceleration while grounded (m/s^2).
    pub const GROUND_ACCEL: f32 = 70.0;
    /// Horizontal acceleration while airborne (m/s^2).
    pub const AIR_ACCEL: f32 = 16.0;

    /// Distance below the feet used to detect ground (meters).
    pub const GROUND_CHECK_DIST: f32 = 0.2;
    /// Collision skin width kept between the capsule and surfaces (meters).
    pub const SKIN: f32 = 0.02;
    /// Maximum collide-and-slide iterations per fixed tick.
    pub const MAX_SLIDE_ITER: u32 = 4;
    /// A surface counts as ground when its normal.y exceeds this (roughly <60 degree slope).
    pub const GROUND_NORMAL_Y: f32 = 0.5;

    /// How long after leaving a ledge a jump is still allowed (seconds).
    pub const COYOTE_TIME: f32 = 0.12;
    /// How long a jump press is remembered before landing (seconds).
    pub const JUMP_BUFFER: f32 = 0.12;
}

/// Marks the player's root (body) entity.
#[derive(Component, Reflect, Default)]
#[reflect(Component, Default)]
pub struct Player;

/// The player's current world-space velocity (m/s).
#[derive(Component, Reflect, Default, Deref, DerefMut)]
#[reflect(Component, Default)]
pub struct PlayerVelocity(pub Vec3);

/// Whether the player is currently standing on walkable ground.
#[derive(Component, Reflect, Default)]
#[reflect(Component, Default)]
pub struct Grounded(pub bool);

/// Whether the player is currently crouching.
#[derive(Component, Reflect, Default)]
#[reflect(Component, Default)]
pub struct Crouching(pub bool);

/// Timers backing forgiving jump input (coyote-time and jump-buffering).
#[derive(Component, Reflect, Default)]
#[reflect(Component, Default)]
pub struct JumpState {
    /// Time remaining during which a late jump is still permitted.
    pub coyote: f32,
    /// Time remaining during which a buffered jump press is still valid.
    pub buffer: f32,
}

/// Horizontal look sensitivity consumed by the fixed-step sim.
#[derive(Component, Reflect, Deref, DerefMut)]
#[reflect(Component, Default)]
pub struct PlayerYawSensitivity(pub f32);

impl Default for PlayerYawSensitivity {
    fn default() -> Self {
        Self(0.003)
    }
}

/// Marks the player's first-person camera (a child of the [`Player`] body).
#[derive(Component, Reflect, Default)]
#[reflect(Component, Default)]
pub struct PlayerCamera;

/// Per-axis mouse sensitivity (radians of rotation per pixel of motion).
#[derive(Component, Reflect, Deref, DerefMut)]
#[reflect(Component, Default)]
pub struct CameraSensitivity(pub Vec2);

impl Default for CameraSensitivity {
    fn default() -> Self {
        Self(Vec2::new(0.003, 0.0022))
    }
}

/// Controls whether runtime player input/movement systems should run.
#[derive(Resource, Debug, Clone, Copy, Reflect)]
#[reflect(Resource)]
pub struct GameInputEnabled(pub bool);

impl Default for GameInputEnabled {
    fn default() -> Self {
        Self(true)
    }
}

/// When false, FPS look/movement intent is suppressed (e.g. pointer outside Game View).
#[derive(Resource, Debug, Clone, Copy, Reflect)]
#[reflect(Resource)]
pub struct GameInputFocused(pub bool);

impl Default for GameInputFocused {
    fn default() -> Self {
        Self(true)
    }
}

/// Controls whether fixed-step gameplay simulation is advancing.
///
/// The standalone game leaves this enabled. The in-process editor disables it
/// while editing so pressing Play is a clean gameplay boundary instead of
/// inheriting a player body that has been simulating off-camera.
#[derive(Resource, Debug, Clone, Copy, Reflect)]
#[reflect(Resource)]
pub struct SimEnabled(pub bool);

impl Default for SimEnabled {
    fn default() -> Self {
        Self(true)
    }
}

/// Fixed-step gameplay simulation plugin.
pub struct SimPlugin;

impl Plugin for SimPlugin {
    fn build(&self, app: &mut App) {
        app.add_plugins((ProtocolPlugin, PhysicsPlugins::default(), SharedTypesPlugin))
            .insert_resource(Time::<Fixed>::from_hz(SIM_TICK_RATE_HZ as f64))
            .init_resource::<SimEnabled>()
            .register_type::<Player>()
            .register_type::<PlayerVelocity>()
            .register_type::<Grounded>()
            .register_type::<Crouching>()
            .register_type::<JumpState>()
            .register_type::<PlayerCamera>()
            .register_type::<CameraSensitivity>()
            .register_type::<PlayerYawSensitivity>()
            .register_type::<SimEnabled>()
            .register_type::<GameInputEnabled>()
            .register_type::<GameInputFocused>();
    }
}

/// Deterministic no-collision state used by unit tests and future replay checks.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DeterminismState {
    pub position: Vec3,
    pub velocity: Vec3,
    pub yaw: f32,
    pub grounded: bool,
    pub crouching: bool,
    pub coyote: f32,
    pub jump_buffer: f32,
}

impl Default for DeterminismState {
    fn default() -> Self {
        Self {
            position: Vec3::ZERO,
            velocity: Vec3::ZERO,
            yaw: 0.0,
            grounded: true,
            crouching: false,
            coyote: tuning::COYOTE_TIME,
            jump_buffer: 0.0,
        }
    }
}

/// A compact, stable summary suitable for equality and hashing.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SimStateSummary {
    pub position: [u32; 3],
    pub velocity: [u32; 3],
    pub yaw: u32,
    pub grounded: bool,
    pub crouching: bool,
}

impl From<&DeterminismState> for SimStateSummary {
    fn from(state: &DeterminismState) -> Self {
        Self {
            position: vec3_bits(state.position),
            velocity: vec3_bits(state.velocity),
            yaw: state.yaw.to_bits(),
            grounded: state.grounded,
            crouching: state.crouching,
        }
    }
}

/// Steps a simplified no-collision controller with the same intent semantics as the ECS sim.
pub fn step_determinism_state(
    state: &mut DeterminismState,
    intent: &PlayerInputIntent,
    tuning: &SimTuning,
) {
    let dt = SIM_DELTA_SECS;
    state.yaw -= intent.look_delta.x * PlayerYawSensitivity::default().0;

    let (sin, cos) = state.yaw.sin_cos();
    let forward = Vec3::new(-sin, 0.0, -cos);
    let right = Vec3::new(cos, 0.0, -sin);
    let movement = intent.movement.clamp_length_max(1.0);
    let wish = (right * movement.x + forward * movement.y).normalize_or_zero();

    state.crouching = intent.crouch_held;
    let speed = if state.crouching {
        tuning.crouch_speed
    } else if intent.sprint_held {
        tuning.run_speed
    } else {
        tuning.walk_speed
    };
    accelerate_horizontal(&mut state.velocity, wish, speed, state.grounded, dt, tuning);

    if intent.jump_pressed {
        state.jump_buffer = tuning.jump_buffer;
    } else {
        state.jump_buffer = (state.jump_buffer - dt).max(0.0);
    }

    if state.grounded {
        state.coyote = tuning.coyote_time;
    } else {
        state.coyote = (state.coyote - dt).max(0.0);
    }

    if state.grounded && state.velocity.y <= 0.0 {
        state.velocity.y = -2.0;
    } else {
        state.velocity.y -= tuning.gravity * dt;
    }

    if state.jump_buffer > 0.0 && state.coyote > 0.0 {
        state.velocity.y = tuning.jump_speed;
        state.jump_buffer = 0.0;
        state.coyote = 0.0;
        state.grounded = false;
    }

    state.position += state.velocity * dt;
}

/// Stable FNV-1a hash of a state summary.
pub fn stable_state_hash(summary: &SimStateSummary) -> u64 {
    let mut hash = 0xcbf29ce484222325_u64;
    for value in summary
        .position
        .into_iter()
        .chain(summary.velocity)
        .chain([summary.yaw])
    {
        for byte in value.to_le_bytes() {
            hash ^= u64::from(byte);
            hash = hash.wrapping_mul(0x100000001b3);
        }
    }
    for flag in [summary.grounded, summary.crouching] {
        hash ^= u64::from(flag as u8);
        hash = hash.wrapping_mul(0x100000001b3);
    }
    hash
}

pub fn accelerate_horizontal(
    velocity: &mut Vec3,
    wish: Vec3,
    speed: f32,
    grounded: bool,
    dt: f32,
    tuning: &SimTuning,
) {
    let target = wish * speed;
    let accel = if grounded {
        tuning.ground_accel
    } else {
        tuning.air_accel
    };
    let horizontal = Vec3::new(velocity.x, 0.0, velocity.z);
    let next = horizontal.move_towards(target, accel * dt);
    velocity.x = next.x;
    velocity.z = next.z;
}

/// Drops the Y component and renormalizes a horizontal direction vector.
pub fn flatten(v: Vec3) -> Vec3 {
    Vec3::new(v.x, 0.0, v.z).normalize_or_zero()
}

fn vec3_bits(v: Vec3) -> [u32; 3] {
    [v.x.to_bits(), v.y.to_bits(), v.z.to_bits()]
}

#[cfg(test)]
mod tests {
    use super::*;
    use settings::{PhysicsSettings, SimTuning};

    fn test_tuning() -> SimTuning {
        SimTuning::from_physics(&PhysicsSettings::default())
    }

    #[test]
    fn same_inputs_over_same_ticks_produce_same_summary_and_hash() {
        let tuning = test_tuning();
        let inputs: Vec<PlayerInputIntent> = (0..160)
            .map(|tick| PlayerInputIntent {
                movement: if tick < 96 { Vec2::Y } else { Vec2::X },
                look_delta: Vec2::new(if tick % 8 == 0 { 2.0 } else { 0.0 }, 0.0),
                jump_pressed: tick == 12 || tick == 80,
                sprint_held: (24..72).contains(&tick),
                crouch_held: tick >= 120,
            })
            .collect();

        let run = |inputs: &[PlayerInputIntent]| {
            let mut state = DeterminismState::default();
            for intent in inputs {
                step_determinism_state(&mut state, intent, &tuning);
            }
            let summary = SimStateSummary::from(&state);
            (summary, stable_state_hash(&summary))
        };

        let first = run(&inputs);
        let second = run(&inputs);

        assert_eq!(first, second);
    }

    #[test]
    fn different_inputs_change_state_hash() {
        let tuning = test_tuning();
        let mut base = DeterminismState::default();
        let mut changed = DeterminismState::default();

        for tick in 0..64 {
            step_determinism_state(
                &mut base,
                &PlayerInputIntent {
                    movement: Vec2::Y,
                    ..default()
                },
                &tuning,
            );
            step_determinism_state(
                &mut changed,
                &PlayerInputIntent {
                    movement: Vec2::Y,
                    jump_pressed: tick == 4,
                    ..default()
                },
                &tuning,
            );
        }

        let base_hash = stable_state_hash(&SimStateSummary::from(&base));
        let changed_hash = stable_state_hash(&SimStateSummary::from(&changed));

        assert_ne!(base_hash, changed_hash);
    }
}
