use super::{ControlState, ControllerSet, Controls, controller_common::MovementSpeedFactor};
use crate::{GameState, abilities::TriggerStateRes, player::PlayerBodyMesh};
use avian3d::{math::*, prelude::*};
use bevy::{ecs::query::Has, prelude::*};

use super::controller_common::{
    CharacterController, ControllerGravity, Grounded, JumpImpulse, MaxSlopeAngle, MovementSpeed,
    PlayerMovement, reset_upon_switch,
};

pub struct CharacterControllerPlugin;

impl Plugin for CharacterControllerPlugin {
    fn build(&self, app: &mut App) {
        app.add_systems(
            Update,
            (
                reset_upon_switch,
                set_movement_flag,
                update_grounded,
                apply_gravity,
                rotate_view,
                movement,
            )
                .chain()
                .in_set(ControllerSet::ApplyControlsRun)
                .run_if(in_state(GameState::Playing)), // todo check if we can make this less viral
        );
    }
}

/// Updates the [`Grounded`] status for character controllers.
fn update_grounded(
    mut commands: Commands,
    mut query: Query<
        (Entity, &ShapeHits, &Rotation, Option<&MaxSlopeAngle>),
        With<CharacterController>,
    >,
) {
    for (entity, hits, rotation, max_slope_angle) in &mut query {
        // The character is grounded if the shape caster has a hit with a normal
        // that isn't too steep.
        let is_grounded = hits.iter().any(|hit| {
            if let Some(angle) = max_slope_angle {
                (rotation * -hit.normal2).angle_between(Vector::Y).abs() <= angle.0
            } else {
                true
            }
        });

        if is_grounded {
            debug!("grounded");
            commands.entity(entity).insert(Grounded);
        } else {
            debug!("not grounded");
            commands.entity(entity).remove::<Grounded>();
        }
    }
}

/// Sets the movement flag, which is an indicator for the rig animation and the braking system.
fn set_movement_flag(
    mut player_movement: ResMut<PlayerMovement>,
    controls: Res<Controls>,
    trigger: Res<TriggerStateRes>,
) {
    let mut direction = controls.keyboard_state.move_dir;
    let deadzone = 0.2;

    if let Some(gamepad) = controls.gamepad_state {
        direction += gamepad.move_dir;
    }

    if player_movement.any_direction {
        if direction.length_squared() < deadzone {
            player_movement.any_direction = false;
        }
    } else if direction.length_squared() > deadzone {
        player_movement.any_direction = true;
    }

    if player_movement.shooting != trigger.is_active() {
        player_movement.shooting = trigger.is_active();
    }
}

fn rotate_view(
    controls: Res<ControlState>,
    mut player: Query<&mut Transform, With<PlayerBodyMesh>>,
) {
    if controls.view_mode {
        return;
    }

    for mut tr in &mut player {
        tr.rotate_y(controls.look_dir.x * -0.001);
    }
}

/// Responds to [`MovementAction`] events and moves character controllers accordingly.
fn movement(
    controls: Res<Controls>,
    mut controllers: Query<(
        &MovementSpeed,
        &MovementSpeedFactor,
        &JumpImpulse,
        &mut LinearVelocity,
        Has<Grounded>,
    )>,
    rig_transform_q: Option<Single<&GlobalTransform, With<PlayerBodyMesh>>>,
    mut jump_used: Local<bool>,
) {
    let mut direction = controls.keyboard_state.move_dir;

    let mut jump_requested = controls.keyboard_state.jump;

    if let Some(gamepad) = controls.gamepad_state {
        direction += gamepad.move_dir;

        jump_requested |= gamepad.jump;
    }

    direction = direction.normalize_or_zero();

    for (movement_speed, factor, jump_impulse, mut linear_velocity, is_grounded) in &mut controllers
    {
        let mut direction = direction.extend(0.0).xzy();

        if let Some(ref rig_transform) = rig_transform_q {
            direction =
                (rig_transform.forward() * direction.z) + (rig_transform.right() * direction.x);
        }

        linear_velocity.x = -direction.x * movement_speed.0 * factor.0;
        linear_velocity.z = -direction.z * movement_speed.0 * factor.0;

        if is_grounded && jump_requested && !*jump_used {
            linear_velocity.y = jump_impulse.0;
            debug!("jump");
            *jump_used = true;
        }

        if !controls.keyboard_state.jump
            && !controls
                .gamepad_state
                .map(|state| state.jump)
                .unwrap_or_default()
        {
            *jump_used = false;
        }
    }
}

/// Applies [`ControllerGravity`] to character controllers.
fn apply_gravity(
    time: Res<Time>,
    mut controllers: Query<(&ControllerGravity, &mut LinearVelocity, Option<&Grounded>)>,
) {
    let delta_time = time.delta_secs();

    for (gravity, mut linear_velocity, grounded) in &mut controllers {
        if grounded.is_none() {
            linear_velocity.0 += gravity.0 * delta_time;
        }
    }
}
