/*
Trouble in Terror Town: Community Edition
Copyright (C) 2024 Mikolaj Wojciech Gorski

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

use bevy::prelude::*;
use bevy_xpbd_3d::{math::*, prelude::*};

pub struct CharacterControllerPlugin;

impl Plugin for CharacterControllerPlugin {
    fn build(&self, app: &mut App) {
        app.add_systems(Update, (keyboard_input, move_character));
        app.add_systems(FixedUpdate, (dampen_movement, update_grounded));
        app.add_event::<MovementAction>();
    }
}

#[derive(Event)]
pub enum MovementAction {
    Move(Vector2),
    Jump,
}

#[derive(Component)]
#[component(storage = "SparseSet")]
pub struct Grounded;

#[derive(Component)]
pub struct CharacterController {
    pub movement_acceleration: Scalar,
    pub air_control_factor: Scalar,
    pub movement_dampening_factor: Scalar,
    pub jump_impulse: Scalar,
    pub max_slope_angle: Scalar,
}

impl Default for CharacterController {
    fn default() -> Self {
        CharacterController {
            movement_acceleration: 20.0,
            air_control_factor: 0.5,
            movement_dampening_factor: 0.95,
            jump_impulse: 4.0,
            max_slope_angle: (30.0 as Scalar).to_radians(),
        }
    }
}

#[derive(Bundle)]
pub struct CharacterControllerBundle {
    character_controller: CharacterController,
    rigid_body: RigidBody,
    collider: Collider,
    ground_caster: ShapeCaster,
    locked_axis: LockedAxes,
    restitution: Restitution,
}

impl CharacterControllerBundle {
    pub fn new(controller_collider: Collider, controller: CharacterController) -> Self {
        let mut caster_shape = controller_collider.clone();
        caster_shape.set_scale(Vector::ONE * 0.99, 10);

        Self {
            character_controller: controller,
            rigid_body: RigidBody::Dynamic,
            collider: controller_collider,
            ground_caster: ShapeCaster::new(
                caster_shape,
                Vector::ZERO,
                Quaternion::default(),
                Direction3d::NEG_Y,
            )
            .with_max_time_of_impact(0.2),
            locked_axis: LockedAxes::ROTATION_LOCKED,
            restitution: Restitution::new(0.0),
        }
    }
}

impl Default for CharacterControllerBundle {
    fn default() -> Self {
        let collider = Collider::capsule(1.0, 0.5);
        let mut caster_shape = collider.clone();
        caster_shape.set_scale(Vector::ONE * 0.99, 10);

        CharacterControllerBundle::new(collider, CharacterController::default())
    }
}

pub fn update_grounded(
    mut commands: Commands,
    mut query: Query<(Entity, &CharacterController, &Rotation, &ShapeHits)>,
) {
    for (entity, character_controller, rotation, hits) in &mut query {
        let is_grounded = hits.iter().any(|hit| {
            let angle = rotation.rotate(-hit.normal2).angle_between(Vector::Y).abs();

            println!(
                "slope angle: {} / {}",
                angle, character_controller.max_slope_angle
            );

            angle <= character_controller.max_slope_angle
        });

        println!("is grounded: {}", is_grounded);

        if is_grounded {
            commands.entity(entity).insert(Grounded);
        } else {
            commands.entity(entity).remove::<Grounded>();
        }
    }
}

pub fn keyboard_input(
    mut movement_event_writer: EventWriter<MovementAction>,
    keyboard_input: Res<ButtonInput<KeyCode>>,
) {
    let up = keyboard_input.any_pressed([KeyCode::KeyW, KeyCode::ArrowUp]);
    let down = keyboard_input.any_pressed([KeyCode::KeyS, KeyCode::ArrowDown]);
    let left = keyboard_input.any_pressed([KeyCode::KeyA, KeyCode::ArrowLeft]);
    let right = keyboard_input.any_pressed([KeyCode::KeyD, KeyCode::ArrowRight]);

    let horizontal = right as i8 - left as i8;
    let vertical = up as i8 - down as i8;
    let direction = Vector2::new(horizontal as Scalar, vertical as Scalar).clamp_length_max(1.0);

    if direction != Vector2::ZERO {
        movement_event_writer.send(MovementAction::Move(direction));
    }

    if keyboard_input.just_pressed(KeyCode::Space) {
        movement_event_writer.send(MovementAction::Jump);
    }
}

pub fn move_character(
    time: Res<Time>,
    mut movement_event_reader: EventReader<MovementAction>,
    mut query: Query<(
        &mut LinearVelocity,
        &CharacterController,
        Has<Grounded>,
        &Rotation,
        &ShapeHits,
    )>,
) {
    let delta_time = time.delta_seconds_f64().adjust_precision();

    for event in movement_event_reader.read() {
        for (mut velocity, character_controller, is_grounded, rotation, shape_hits) in &mut query {
            match event {
                MovementAction::Move(direction) => {
                    if is_grounded {
                        let mut slope_factor = 1.0;
                        for hit in shape_hits.iter() {
                            let angle =
                                rotation.rotate(-hit.normal2).angle_between(Vector::Y).abs();
                            if angle <= character_controller.max_slope_angle {
                                slope_factor *=
                                    (angle / character_controller.max_slope_angle).cos();
                                velocity.x += direction.x
                                    * character_controller.movement_acceleration
                                    * slope_factor
                                    * delta_time;
                                velocity.z -= direction.y
                                    * character_controller.movement_acceleration
                                    * slope_factor
                                    * delta_time;
                            } else {
                                velocity.x = 0.0;
                                velocity.z = 0.0;
                            }
                        }
                    } else {
                        velocity.x += direction.x
                            * character_controller.movement_acceleration
                            * character_controller.air_control_factor
                            * delta_time;
                        velocity.z -= direction.y
                            * character_controller.movement_acceleration
                            * character_controller.air_control_factor
                            * delta_time;
                    }
                }
                MovementAction::Jump => {
                    if is_grounded {
                        velocity.y = character_controller.jump_impulse;
                    }
                }
            }
        }
    }
}

pub fn dampen_movement(mut qeury: Query<(&mut LinearVelocity, &CharacterController)>) {
    for (mut velocity, character_controller) in &mut qeury {
        // We could use `LinearDamping`, but we don't want to dampen movement along the Y axis
        velocity.x *= character_controller.movement_dampening_factor;
        velocity.z *= character_controller.movement_dampening_factor;
    }
}
