use ecs::component::local::Local;
use ecs::phase::UPDATE as UPDATE_PHASE;
use ecs::sole::Single;
use ecs::system::{Into, System};
use ecs::{Component, Query};

use crate::camera::{Active as ActiveCamera, Camera};
use crate::delta_time::DeltaTime;
use crate::input::{Cursor, CursorFlags, Key, KeyState, Keys};
use crate::transform::Position;
use crate::util::builder;
use crate::vector::{Vec2, Vec3};

builder! {
/// A fly camera.
#[builder(name = Builder, derives = (Debug))]
#[derive(Debug, Component)]
#[non_exhaustive]
pub struct Fly {
    pub current_pitch: f64,
    pub current_yaw: f64,
    pub speed: f32,
}
}

impl Fly
{
    #[must_use]
    pub fn builder() -> Builder
    {
        Builder::default()
    }
}

impl Default for Fly
{
    fn default() -> Self
    {
        Self::builder().build()
    }
}

impl Default for Builder
{
    fn default() -> Self
    {
        Self {
            current_yaw: -90.0,
            current_pitch: 0.0,
            speed: 3.0,
        }
    }
}

/// Fly camera extension.
pub struct Extension(pub Options);

impl ecs::extension::Extension for Extension
{
    fn collect(self, mut collector: ecs::extension::Collector<'_>)
    {
        collector.add_system(
            *UPDATE_PHASE,
            update
                .into_system()
                .initialize((CursorState::default(), self.0)),
        );
    }
}

#[derive(Debug, Component)]
pub struct Options
{
    pub mouse_sensitivity: f32,
}

fn update(
    camera_query: Query<(&mut Camera, &mut Position, &mut Fly, &ActiveCamera)>,
    keys: Single<Keys>,
    cursor: Single<Cursor>,
    cursor_flags: Single<CursorFlags>,
    delta_time: Single<DeltaTime>,
    mut cursor_state: Local<CursorState>,
    options: Local<Options>,
)
{
    for (mut camera, mut camera_pos, mut fly_camera, _) in &camera_query {
        if cursor.has_moved && cursor_flags.is_first_move.flag {
            tracing::debug!("First cursor move");

            cursor_state.last_pos = cursor.position;
        }

        let delta_time = delta_time.duration;

        let mut x_offset = cursor.position.x - cursor_state.last_pos.x;
        let mut y_offset = cursor_state.last_pos.y - cursor.position.y;

        cursor_state.last_pos = cursor.position;

        x_offset *= f64::from(options.mouse_sensitivity);
        y_offset *= f64::from(options.mouse_sensitivity);

        fly_camera.current_yaw += x_offset;
        fly_camera.current_pitch += y_offset;

        fly_camera.current_pitch = fly_camera.current_pitch.clamp(-89.0, 89.0);

        // TODO: This casting to a f32 from a f64 is horrible. fix it
        #[allow(clippy::cast_possible_truncation)]
        let direction = Vec3 {
            x: (fly_camera.current_yaw.to_radians().cos()
                * fly_camera.current_pitch.to_radians().cos()) as f32,
            y: fly_camera.current_pitch.to_radians().sin() as f32,
            z: (fly_camera.current_yaw.to_radians().sin()
                * fly_camera.current_pitch.to_radians().cos()) as f32,
        }
        .normalize();

        let cam_right = direction.cross(&Vec3::UP).normalize();

        camera.global_up = cam_right.cross(&direction).normalize();

        if keys.get_key_state(Key::W) == KeyState::Pressed {
            camera_pos.position +=
                direction * fly_camera.speed * delta_time.as_secs_f32();
        }

        if keys.get_key_state(Key::S) == KeyState::Pressed {
            camera_pos.position -=
                direction * fly_camera.speed * delta_time.as_secs_f32();
        }

        if keys.get_key_state(Key::A) == KeyState::Pressed {
            let cam_left = -direction.cross(&Vec3::UP).normalize();

            camera_pos.position += cam_left * fly_camera.speed * delta_time.as_secs_f32();
        }

        if keys.get_key_state(Key::D) == KeyState::Pressed {
            let cam_right = direction.cross(&Vec3::UP).normalize();

            camera_pos.position +=
                cam_right * fly_camera.speed * delta_time.as_secs_f32();
        }

        camera.target = camera_pos.position + direction;
    }
}

#[derive(Debug, Default, Component)]
struct CursorState
{
    last_pos: Vec2<f64>,
}