use super::{ControllerSet, ControllerSwitchEvent};
use crate::{
    GameState,
    abilities::TriggerStateRes,
    animation::AnimationFlags,
    control::{ControlState, SelectedController, controls::ControllerSettings},
    head::ActiveHead,
    heads_database::{HeadControls, HeadsDatabase},
    physics_layers::GameLayer,
    player::{Player, PlayerBodyMesh},
};
use avian3d::{math::*, prelude::*};
use bevy::prelude::*;
use happy_feet::prelude::{
    Character, CharacterDrag, CharacterGravity, CharacterMovement, CharacterPlugins,
    GroundFriction, Grounding, GroundingConfig, KinematicVelocity, MoveInput, SteppingBehaviour,
    SteppingConfig,
};
use lightyear::prelude::{Replicated, input::native::ActionState};
use serde::{Deserialize, Serialize};

pub fn plugin(app: &mut App) {
    app.add_plugins(CharacterPlugins::default());

    app.register_type::<MovementSpeedFactor>();

    app.add_systems(
        PreUpdate,
        reset_upon_switch.run_if(in_state(GameState::Playing)),
    );

    app.add_systems(
        PreUpdate,
        set_animation_flags.run_if(in_state(GameState::Playing)),
    );

    app.add_systems(
        FixedUpdate,
        decelerate
            .after(ControllerSet::ApplyControlsRun)
            .after(ControllerSet::ApplyControlsFly)
            .run_if(in_state(GameState::Playing)),
    );

    app.add_systems(Update, add_controller_bundle);
}

fn set_animation_flags(
    trigger: Res<TriggerStateRes>,
    mut player: Query<
        (&Grounding, &mut AnimationFlags, &ActionState<ControlState>),
        (With<Player>, Without<Replicated>),
    >,
) {
    for (grounding, mut flags, controls) in player.iter_mut() {
        let direction = controls.move_dir;
        let deadzone = 0.2;

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

        flags.shooting = trigger.is_active();

        // `apply_controls` sets the jump flag when the player actually jumps.
        // Unset the flag on hitting the ground
        if grounding.is_grounded() {
            flags.jumping = false;
        }
    }
}

/// Reset the pitch and velocity of the character if the controller was switched.
pub fn reset_upon_switch(
    mut c: Commands,
    mut event_controller_switch: EventReader<ControllerSwitchEvent>,
    controller: Res<SelectedController>,
    mut rig_transform_q: Option<Single<&mut Transform, With<PlayerBodyMesh>>>,
    mut velocity: Single<&mut KinematicVelocity, With<Character>>,
    character: Single<Entity, With<Character>>,
) {
    for _ in event_controller_switch.read() {
        velocity.0 = Vec3::ZERO;

        // Reset pitch but keep yaw the same
        if let Some(ref mut rig_transform) = rig_transform_q {
            let euler_rot = rig_transform.rotation.to_euler(EulerRot::YXZ);
            let yaw = euler_rot.0;
            rig_transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, 0.0, 0.0);
        }

        match controller.0 {
            ControllerSet::ApplyControlsFly => {
                c.entity(*character).insert(FLYING_MOVEMENT_CONFIG);
            }
            ControllerSet::ApplyControlsRun => {
                c.entity(*character).insert(RUNNING_MOVEMENT_CONFIG);
            }
            _ => unreachable!(),
        }
    }
}

/// Decelerates the player in the directions of "undesired velocity"; velocity that is not aligned
/// with the movement input direction. This makes it quicker to reverse direction, and prevents
/// sliding around, even with low friction, without slowing down the player globally like high
/// friction or drag would.
fn decelerate(
    mut character: Query<(
        &mut KinematicVelocity,
        &MoveInput,
        Option<&Grounding>,
        &ControllerSettings,
    )>,
) {
    for (mut velocity, input, grounding, settings) in character.iter_mut() {
        let direction = input.value.normalize();
        let ground_normal = grounding
            .and_then(|it| it.normal())
            .unwrap_or(Dir3::Y)
            .as_vec3();

        let velocity_within_90_degrees = direction.dot(velocity.0) > 0.0;
        let desired_velocity = if direction != Vec3::ZERO && velocity_within_90_degrees {
            // project velocity onto direction to extract the component directly aligned with direction
            velocity.0.project_onto(direction)
        } else {
            // if velocity isn't within 90 degrees of direction then the projection would be in the
            // exact opposite direction of `direction`; so just zero it
            Vec3::ZERO
        };
        let undesired_velocity = velocity.0 - desired_velocity;
        let vertical_undesired_velocity = undesired_velocity.project_onto(ground_normal);
        // only select the velocity along the ground plane; that way the character can't decelerate
        // while falling or jumping, but will decelerate along slopes properly
        let undesired_velocity = undesired_velocity - vertical_undesired_velocity;
        let deceleration =
            Vec3::ZERO.move_towards(undesired_velocity, settings.deceleration_factor);
        velocity.0 -= deceleration;
    }
}

#[derive(Component, Reflect, PartialEq, Serialize, Deserialize)]
#[reflect(Component)]
pub struct MovementSpeedFactor(pub f32);

#[derive(Component, Reflect, Default, Debug, Serialize, Deserialize, PartialEq)]
#[reflect(Component)]
pub struct PlayerCharacterController;

fn add_controller_bundle(
    mut commands: Commands,
    controllers: Query<
        (Entity, &ActiveHead),
        (With<PlayerCharacterController>, Without<Character>),
    >,
    db: Option<Res<HeadsDatabase>>,
) {
    let Some(db) = db else { return };

    for (controller, head) in controllers {
        let controls = db.head_stats(head.0).controls;
        commands
            .entity(controller)
            .insert(CharacterControllerBundle::new(controls));
    }
}

/// A bundle that contains the components needed for a basic
/// kinematic character controller.
#[derive(Bundle)]
pub struct CharacterControllerBundle {
    character_controller: Character,
    collider: Collider,
    move_input: MoveInput,
    movement_factor: MovementSpeedFactor,
    collision_events: CollisionEventsEnabled,
    movement_config: MovementConfig,
    interpolation: TransformInterpolation,
    layers: CollisionLayers,
}

impl CharacterControllerBundle {
    pub fn new(controls: HeadControls) -> Self {
        // Create shape caster as a slightly smaller version of collider
        let collider = Collider::capsule(0.9, 1.2);
        let mut caster_shape = collider.clone();
        caster_shape.set_scale(Vector::ONE * 0.98, 10);

        let config = match controls {
            HeadControls::Plane => FLYING_MOVEMENT_CONFIG,
            HeadControls::Walk => RUNNING_MOVEMENT_CONFIG,
        };

        Self {
            character_controller: Character,
            collider,
            move_input: MoveInput::default(),
            movement_factor: MovementSpeedFactor(1.0),
            collision_events: CollisionEventsEnabled,
            movement_config: config,
            interpolation: TransformInterpolation,
            layers: CollisionLayers::new(
                LayerMask(GameLayer::Player.to_bits()),
                LayerMask::ALL & !GameLayer::CollectiblePhysics.to_bits(),
            ),
        }
    }
}

#[derive(Bundle)]
struct MovementConfig {
    movement: CharacterMovement,
    step: SteppingConfig,
    ground: GroundingConfig,
    gravity: CharacterGravity,
    friction: GroundFriction,
    drag: CharacterDrag,
    settings: ControllerSettings,
}

const RUNNING_MOVEMENT_CONFIG: MovementConfig = MovementConfig {
    movement: CharacterMovement {
        target_speed: 15.0,
        acceleration: 40.0,
    },
    step: SteppingConfig {
        max_vertical: 0.25,
        max_horizontal: 0.4,
        behaviour: SteppingBehaviour::Grounded,
        max_substeps: 8,
    },
    ground: GroundingConfig {
        max_angle: PI / 4.0,
        max_distance: 0.2,
        snap_to_surface: true,
        up_direction: Dir3::Y,
        max_iterations: 2,
    },
    gravity: CharacterGravity(Some(vec3(0.0, -60.0, 0.0))),
    friction: GroundFriction(10.0),
    drag: CharacterDrag(0.0),
    settings: ControllerSettings {
        jump_force: 25.0,
        deceleration_factor: 1.0,
    },
};

const FLYING_MOVEMENT_CONFIG: MovementConfig = MovementConfig {
    movement: CharacterMovement {
        target_speed: 20.0,
        acceleration: 300.0,
    },
    step: SteppingConfig {
        max_vertical: 0.25,
        max_horizontal: 0.4,
        behaviour: SteppingBehaviour::Never,
        max_substeps: 8,
    },
    ground: GroundingConfig {
        max_angle: 0.0,
        max_distance: -1.0,
        snap_to_surface: false,
        up_direction: Dir3::Y,
        max_iterations: 2,
    },
    gravity: CharacterGravity(Some(Vec3::ZERO)),
    friction: GroundFriction(0.0),
    drag: CharacterDrag(10.0),
    settings: ControllerSettings {
        jump_force: 0.0,
        deceleration_factor: 0.0,
    },
};
