use glam::Vec3;
use kurbo::ParamCurve;
use rapier3d::math::Vector;

use crate::components::player_states::{
    FallingState, IdleState, JumpingState, LeapingState, RollingState, WalkingState,
};
use crate::entity::EntityHandle;
use crate::physics::PhysicsManager;
use crate::state::PlayerState;
use crate::world::World;

pub const LEAP_DURATION: f32 = 0.18;
pub const ROLL_DURATION: f32 = 0.42;
const LEAP_SPEED: f32 = 18.0;
const ROLL_SPEED: f32 = 14.0;

impl PlayerState for IdleState
{
    fn tick_time(&mut self, delta: f32)
    {
        self.time_in_state += delta;
    }

    fn on_enter(&mut self, world: &mut World, entity: EntityHandle)
    {
        world.physics.with(entity, |physics| {
            PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
                let current_velocity = *rigidbody.linvel();
                let idle_damping = world
                    .movements
                    .with(entity, |m| m.idle_damping)
                    .unwrap_or(0.1);

                let horizontal_velocity = Vec3::new(current_velocity.x, 0.0, current_velocity.z);
                let new_horizontal_velocity = horizontal_velocity * idle_damping;

                rigidbody.set_linvel(
                    Vector::new(
                        new_horizontal_velocity.x,
                        current_velocity.y,
                        new_horizontal_velocity.z,
                    ),
                    true,
                );
            });
        });
    }

    fn on_physics_update(&mut self, world: &mut World, entity: EntityHandle, _delta: f32)
    {
        const GROUND_CHECK_DISTANCE: f32 = 0.6;

        let current_translation = world
            .physics
            .with(entity, |physics| {
                PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
                    *rigidbody.translation()
                })
            })
            .flatten()
            .unwrap();

        let terrain_height =
            PhysicsManager::get_terrain_height_at(current_translation.x, current_translation.z);

        if let Some(height) = terrain_height
        {
            let target_y = height + 1.0;
            let distance_to_ground = current_translation.y - target_y;

            if distance_to_ground.abs() < GROUND_CHECK_DISTANCE
            {
                world.physics.with(entity, |physics| {
                    PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
                        let next_translation =
                            Vector::new(current_translation.x, target_y, current_translation.z);
                        rigidbody.set_next_kinematic_translation(next_translation);
                    });
                });

                world.movements.with_mut(entity, |movement| {
                    movement.movement_context.is_floored = true;
                });
            }
        }
    }
}

impl PlayerState for WalkingState
{
    fn tick_time(&mut self, delta: f32)
    {
        self.time_in_state += delta;
    }

    fn on_physics_update(&mut self, world: &mut World, entity: EntityHandle, delta: f32)
    {
        let (movement_input, walking_config) = world
            .movements
            .with(entity, |movement| {
                let input = world
                    .inputs
                    .with(entity, |input| input.move_direction)
                    .unwrap_or(Vec3::ZERO);
                (input, movement.clone())
            })
            .unwrap();

        let t = (self.time_in_state / walking_config.walking_acceleration_duration).clamp(0.0, 1.0);
        let acceleration_amount = walking_config.walking_acceleration_curve.eval(t as f64).y as f32;

        let current_translation = world
            .physics
            .with(entity, |physics| {
                PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
                    *rigidbody.translation()
                })
            })
            .flatten()
            .unwrap();

        let terrain_height =
            PhysicsManager::get_terrain_height_at(current_translation.x, current_translation.z);

        let target_y = if let Some(height) = terrain_height
        {
            height + 1.0
        }
        else
        {
            current_translation.y
        };

        let slope_angle = if movement_input.length_squared() > 0.01
        {
            PhysicsManager::get_terrain_slope_in_direction(
                current_translation.x,
                current_translation.z,
                movement_input.x,
                movement_input.z,
            )
        }
        else
        {
            0.0
        };

        let slope_multiplier = {
            const MAX_SLOPE_ANGLE: f32 = std::f32::consts::PI / 4.0;

            if slope_angle > 0.0
            {
                let uphill_factor = (slope_angle / MAX_SLOPE_ANGLE).min(1.0);
                1.0 - uphill_factor * 0.9
            }
            else
            {
                let downhill_factor = (slope_angle.abs() / MAX_SLOPE_ANGLE).min(1.0);
                1.0 + downhill_factor * 0.5
            }
        };

        world.physics.with(entity, |physics| {
            PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
                let current_velocity = *rigidbody.linvel();

                let horizontal_velocity = Vec3::new(current_velocity.x, 0.0, current_velocity.z);

                let walking_force = movement_input
                    * walking_config.walking_acceleration
                    * delta
                    * acceleration_amount;

                let new_horizontal_velocity = (walking_force
                    + horizontal_velocity * walking_config.walking_damping)
                    .clamp_length_max(walking_config.max_walking_speed * slope_multiplier);

                let next_translation = Vector::new(
                    current_translation.x + new_horizontal_velocity.x * delta,
                    target_y,
                    current_translation.z + new_horizontal_velocity.z * delta,
                );

                rigidbody.set_linvel(
                    Vector::new(new_horizontal_velocity.x, 0.0, new_horizontal_velocity.z),
                    true,
                );
                rigidbody.set_next_kinematic_translation(next_translation);
            });
        });

        world.movements.with_mut(entity, |movement| {
            movement.movement_context.is_floored = terrain_height.is_some();
        });

        if movement_input.length_squared() > 0.1
        {
            world.transforms.with_mut(entity, |transform| {
                let target_rotation = f32::atan2(movement_input.x, movement_input.z);
                transform.rotation.y = target_rotation;
            });
        }
    }
}

impl PlayerState for JumpingState
{
    fn tick_time(&mut self, delta: f32)
    {
        self.time_in_state += delta;
    }

    fn on_enter(&mut self, world: &mut World, entity: EntityHandle)
    {
        self.time_in_state = 0.0;

        let current_position = world.transforms.get(entity).unwrap().position;

        world.jumps.with_mut(entity, |jump| {
            jump.jump_context.in_progress = true;
            jump.jump_context.origin_height = current_position.y;
            jump.jump_context.duration = 0.0;
            jump.jump_context.normal = Vec3::Y;
        });
    }

    fn on_exit(&mut self, world: &mut World, entity: EntityHandle)
    {
        world.jumps.with_mut(entity, |jump| {
            jump.jump_context.in_progress = false;
            jump.jump_context.duration = 0.0;
        });
    }

    fn on_physics_update(&mut self, world: &mut World, entity: EntityHandle, delta: f32)
    {
        world.jumps.with_mut(entity, |jump| {
            jump.jump_context.duration = self.time_in_state;
        });

        let jump = world.jumps.with(entity, |jump| jump.clone()).unwrap();

        let normalized_time = (self.time_in_state / jump.jump_duration).min(1.0);
        let height_progress = jump.jump_curve.eval(normalized_time as f64).y as f32;

        let origin_height = jump.jump_context.origin_height;
        let target_height = origin_height + height_progress * jump.jump_height;

        let current_translation = world
            .physics
            .with(entity, |physics| {
                PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
                    *rigidbody.translation()
                })
            })
            .flatten()
            .unwrap();

        let height_diff = target_height - current_translation.y;
        let required_velocity = height_diff / delta;

        world.physics.with(entity, |physics| {
            PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
                let current_velocity = *rigidbody.linvel();
                let next_translation = Vector::new(
                    current_translation.x + current_velocity.x * delta,
                    current_translation.y + required_velocity * delta,
                    current_translation.z + current_velocity.z * delta,
                );

                rigidbody.set_linvel(
                    Vector::new(current_velocity.x, required_velocity, current_velocity.z),
                    true,
                );
                rigidbody.set_next_kinematic_translation(next_translation);
            });
        });

        world.movements.with_mut(entity, |movement| {
            movement.movement_context.is_floored = false;
        });
    }
}

impl PlayerState for FallingState
{
    fn tick_time(&mut self, delta: f32)
    {
        self.time_in_state += delta;
    }

    fn on_physics_update(&mut self, world: &mut World, entity: EntityHandle, delta: f32)
    {
        const GRAVITY: f32 = -9.81 * 5.0;
        const GROUND_CHECK_DISTANCE: f32 = 0.6;

        let (current_pos, velocity) = world
            .physics
            .with(entity, |physics| {
                PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
                    let mut vel = *rigidbody.linvel();
                    vel.y += GRAVITY * delta;
                    (*rigidbody.translation(), vel)
                })
            })
            .flatten()
            .unwrap();

        let next_pos = current_pos + velocity;
        let terrain_height = PhysicsManager::get_terrain_height_at(next_pos.x, next_pos.z);

        let is_grounded = if let Some(height) = terrain_height
        {
            let target_y = height + 1.0;
            let distance_to_ground = current_pos.y - target_y;

            if distance_to_ground < GROUND_CHECK_DISTANCE && velocity.y <= 0.01
            {
                world.physics.with(entity, |physics| {
                    PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
                        let next_pos = Vector::new(current_pos.x, target_y, current_pos.z);
                        rigidbody.set_next_kinematic_translation(next_pos);
                        rigidbody.set_linvel(Vector::new(velocity.x, 0.0, velocity.z), true);
                    });
                });
                true
            }
            else
            {
                world.physics.with(entity, |physics| {
                    PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
                        let next_pos = current_pos + velocity * delta;
                        rigidbody.set_next_kinematic_translation(next_pos);
                        rigidbody.set_linvel(velocity, true);
                    });
                });
                false
            }
        }
        else
        {
            world.physics.with(entity, |physics| {
                PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| {
                    let next_pos = current_pos + velocity * delta;
                    rigidbody.set_next_kinematic_translation(next_pos);
                    rigidbody.set_linvel(velocity, true);
                });
            });
            false
        };

        world.movements.with_mut(entity, |movement| {
            movement.movement_context.is_floored = is_grounded;
        });
    }
}

impl PlayerState for LeapingState
{
    fn tick_time(&mut self, delta: f32)
    {
        self.time_in_state += delta;
    }

    fn on_enter(&mut self, world: &mut World, entity: EntityHandle)
    {
        self.time_in_state = 0.0;

        let facing = world
            .transforms
            .with(entity, |t| {
                let yaw = t.rotation.y;
                Vec3::new(yaw.sin(), 0.0, yaw.cos())
            })
            .unwrap_or(Vec3::Z);

        let move_dir = world
            .inputs
            .with(entity, |i| i.move_direction)
            .unwrap_or(Vec3::ZERO);

        self.leap_direction = if move_dir.length_squared() > 0.01
        {
            move_dir
        }
        else
        {
            facing
        };
    }

    fn on_physics_update(&mut self, world: &mut World, entity: EntityHandle, delta: f32)
    {
        let current_translation = world
            .physics
            .with(entity, |physics| {
                PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rb| *rb.translation())
            })
            .flatten()
            .unwrap();

        let terrain_height =
            PhysicsManager::get_terrain_height_at(current_translation.x, current_translation.z);
        let target_y = terrain_height
            .map(|h| h + 1.0)
            .unwrap_or(current_translation.y);

        let velocity = self.leap_direction * LEAP_SPEED;

        world.physics.with(entity, |physics| {
            PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rb| {
                let next = Vector::new(
                    current_translation.x + velocity.x * delta,
                    target_y,
                    current_translation.z + velocity.z * delta,
                );
                rb.set_linvel(Vector::new(velocity.x, 0.0, velocity.z), true);
                rb.set_next_kinematic_translation(next);
            });
        });

        world.movements.with_mut(entity, |m| {
            m.movement_context.is_floored = terrain_height.is_some();
        });

        world.transforms.with_mut(entity, |t| {
            t.rotation.y = f32::atan2(self.leap_direction.x, self.leap_direction.z);
        });
    }
}

impl PlayerState for RollingState
{
    fn tick_time(&mut self, delta: f32)
    {
        self.time_in_state += delta;
    }

    fn on_enter(&mut self, world: &mut World, entity: EntityHandle)
    {
        self.time_in_state = 0.0;

        self.roll_direction = world
            .inputs
            .with(entity, |i| i.move_direction)
            .filter(|d| d.length_squared() > 0.01)
            .unwrap_or_else(|| {
                world
                    .transforms
                    .with(entity, |t| {
                        let yaw = t.rotation.y;
                        Vec3::new(yaw.sin(), 0.0, yaw.cos())
                    })
                    .unwrap_or(Vec3::Z)
            });
    }

    fn on_physics_update(&mut self, world: &mut World, entity: EntityHandle, delta: f32)
    {
        let t = (self.time_in_state / ROLL_DURATION).clamp(0.0, 1.0);
        let speed = ROLL_SPEED * (1.0 - t * 0.6);

        let current_translation = world
            .physics
            .with(entity, |physics| {
                PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rb| *rb.translation())
            })
            .flatten()
            .unwrap();

        let terrain_height =
            PhysicsManager::get_terrain_height_at(current_translation.x, current_translation.z);
        let target_y = terrain_height
            .map(|h| h + 1.0)
            .unwrap_or(current_translation.y);

        let velocity = self.roll_direction * speed;

        world.physics.with(entity, |physics| {
            PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rb| {
                let next = Vector::new(
                    current_translation.x + velocity.x * delta,
                    target_y,
                    current_translation.z + velocity.z * delta,
                );
                rb.set_linvel(Vector::new(velocity.x, 0.0, velocity.z), true);
                rb.set_next_kinematic_translation(next);
            });
        });

        world.movements.with_mut(entity, |m| {
            m.movement_context.is_floored = terrain_height.is_some();
        });
    }
}
