//! Fixed-step player movement systems.

use avian3d::math::{AdjustPrecision, AsF32};
use avian3d::prelude::*;
use bevy::prelude::*;
use bevy::time::Fixed;
use protocol::PlayerInputIntent;
use settings::SimTuning;
use sim::{
    accelerate_horizontal, flatten, Crouching, Grounded, JumpState, Player, PlayerVelocity,
    PlayerYawSensitivity,
};

/// Applies protocol input intent to deterministic player state.
#[unsafe(no_mangle)]
pub fn apply_player_intent(
    time: Res<Time<Fixed>>,
    tuning: Res<SimTuning>,
    player: Single<
        (
            &mut Transform,
            &mut PlayerInputIntent,
            &PlayerYawSensitivity,
            &mut PlayerVelocity,
            &Grounded,
            &mut Crouching,
            &mut JumpState,
        ),
        With<Player>,
    >,
) {
    let dt = time.delta_secs();
    let (
        mut transform,
        mut intent,
        yaw_sensitivity,
        mut velocity,
        grounded,
        mut crouching,
        mut jump,
    ) = player.into_inner();

    if intent.look_delta.x != 0.0 {
        transform.rotate_y(-intent.look_delta.x * yaw_sensitivity.0);
        intent.look_delta.x = 0.0;
    }

    let forward = flatten(transform.forward().as_vec3());
    let right = flatten(transform.right().as_vec3());
    let movement = intent.movement.clamp_length_max(1.0);
    let wish = (right * movement.x + forward * movement.y).normalize_or_zero();

    crouching.0 = intent.crouch_held;
    let speed = if crouching.0 {
        tuning.crouch_speed
    } else if intent.sprint_held {
        tuning.run_speed
    } else {
        tuning.walk_speed
    };

    accelerate_horizontal(&mut velocity.0, wish, speed, grounded.0, dt, &tuning);

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

/// Applies gravity, resolves jumps, and moves the capsule with Avian move-and-slide.
#[unsafe(no_mangle)]
pub fn move_and_slide(
    time: Res<Time<Fixed>>,
    tuning: Res<SimTuning>,
    move_and_slide: MoveAndSlide,
    player: Single<
        (
            Entity,
            &Collider,
            &mut Transform,
            &mut PlayerVelocity,
            &mut Grounded,
            &mut JumpState,
        ),
        With<Player>,
    >,
) {
    let dt = time.delta_secs();
    if dt <= 0.0 {
        return;
    }

    let (entity, collider, mut transform, mut velocity, mut grounded, mut jump) =
        player.into_inner();

    let filter = SpatialQueryFilter::default().with_excluded_entities([entity]);

    let ground_hit = move_and_slide.spatial_query.cast_shape(
        collider,
        transform.translation,
        transform.rotation,
        Dir3::NEG_Y,
        &ShapeCastConfig::from_max_distance(tuning.ground_check_dist),
        &filter,
    );
    let is_grounded = ground_hit.is_some_and(|hit| hit.normal1.y > tuning.ground_normal_y);
    grounded.0 = is_grounded;

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

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

    if jump.buffer > 0.0 && jump.coyote > 0.0 {
        velocity.0.y = tuning.jump_speed;
        jump.buffer = 0.0;
        jump.coyote = 0.0;
        grounded.0 = false;
    }

    let config = MoveAndSlideConfig {
        move_and_slide_iterations: tuning.max_slide_iter as usize,
        skin_width: tuning.skin.adjust_precision(),
        ..default()
    };

    let output = move_and_slide.move_and_slide(
        collider,
        transform.translation.adjust_precision(),
        transform.rotation.adjust_precision(),
        velocity.0.adjust_precision(),
        time.delta(),
        &config,
        &filter,
        |_| MoveAndSlideHitResponse::Accept,
    );

    transform.translation = output.position.f32();
    velocity.0 = output.projected_velocity.f32();
}

/// Keeps Avian [`Position`] aligned with the transform written by [`move_and_slide`].
///
/// Must run in a separate system from [`move_and_slide`] because [`MoveAndSlide`] already
/// accesses [`Position`] internally (Bevy error B0001).
#[unsafe(no_mangle)]
pub fn sync_player_physics_position(player: Single<(&Transform, &mut Position), With<Player>>) {
    let (transform, mut position) = player.into_inner();
    position.0 = transform.translation.adjust_precision();
}
