use avian3d::prelude::*;
use bevy::prelude::*;

use crate::{
    GameState, control::ControlState, loading_assets::UIAssets, physics_layers::GameLayer,
};

#[derive(Component, Reflect, Debug)]
pub struct CameraTarget;

#[derive(Component, Reflect, Debug)]
pub struct CameraArmRotation;

/// Requested camera rotation based on various input sources (keyboard, gamepad)
#[derive(Component, Reflect, Debug, Default, Deref, DerefMut)]
#[reflect(Component)]
pub struct CameraRotationInput(pub Vec2);

#[derive(Resource, Reflect, Debug, Default)]
#[reflect(Resource)]
pub struct CameraState {
    pub cutscene: bool,
    pub look_around: bool,
}

#[derive(Component, Reflect, Debug, Default)]
struct CameraUi;

#[derive(Component, Reflect, Debug)]
#[reflect(Component)]
pub struct MainCamera {
    dir: Dir3,
    distance: f32,
}

impl MainCamera {
    fn new(arm: Vec3) -> Self {
        let (dir, distance) = Dir3::new_and_length(arm).expect("invalid arm length");
        Self { dir, distance }
    }
}

pub fn plugin(app: &mut App) {
    app.register_type::<CameraRotationInput>();
    app.register_type::<CameraState>();
    app.register_type::<MainCamera>();

    app.init_resource::<CameraState>();
    app.add_systems(OnEnter(GameState::Playing), startup);
    app.add_systems(
        Update,
        (update, update_ui, update_look_around, rotate_view).run_if(in_state(GameState::Playing)),
    );
}

fn startup(mut commands: Commands) {
    commands.spawn((
        Camera3d::default(),
        MainCamera::new(Vec3::new(0., 2.5, -13.)),
        CameraRotationInput::default(),
    ));
}

fn update_look_around(controls: Res<ControlState>, mut cam_state: ResMut<CameraState>) {
    let look_around = controls.view_mode;

    if look_around != cam_state.look_around {
        cam_state.look_around = look_around;
    }
}

fn update_ui(
    mut commands: Commands,
    cam_state: Res<CameraState>,
    assets: Res<UIAssets>,
    query: Query<Entity, With<CameraUi>>,
) {
    if cam_state.is_changed() {
        let show_ui = cam_state.look_around || cam_state.cutscene;

        if show_ui {
            commands.spawn((
                CameraUi,
                Node {
                    margin: UiRect::top(Val::Px(20.))
                        .with_left(Val::Auto)
                        .with_right(Val::Auto),
                    justify_content: JustifyContent::Center,
                    ..default()
                },
                children![(
                    Node {
                        display: Display::Block,
                        position_type: PositionType::Absolute,
                        ..default()
                    },
                    ImageNode::new(assets.camera.clone()),
                )],
            ));
        } else {
            for entity in query.iter() {
                commands.entity(entity).despawn();
            }
        }
    }
}

fn update(
    mut cam: Query<
        (&MainCamera, &mut Transform, &CameraRotationInput),
        (Without<CameraTarget>, Without<CameraArmRotation>),
    >,
    target: Single<&GlobalTransform, (With<CameraTarget>, Without<CameraArmRotation>)>,
    arm_rotation: Single<&Transform, With<CameraArmRotation>>,
    spatial_query: SpatialQuery,
    cam_state: Res<CameraState>,
) {
    if cam_state.cutscene {
        return;
    }

    let target = target.translation();

    let arm_tf = arm_rotation;

    let Ok((camera, mut cam_transform, cam_rotation_input)) = cam.single_mut() else {
        return;
    };

    let direction = arm_tf.rotation * Quat::from_rotation_y(cam_rotation_input.x) * camera.dir;

    let max_distance = camera.distance;

    let filter = SpatialQueryFilter::from_mask(LayerMask(GameLayer::Level.to_bits()));
    let cam_pos = if let Some(first_hit) = spatial_query.cast_shape(
        &Collider::sphere(0.5),
        target,
        Quat::IDENTITY,
        direction,
        &ShapeCastConfig::from_max_distance(max_distance),
        &filter,
    ) {
        let distance = first_hit.distance;
        target + (direction * distance)
    } else {
        target + (direction * camera.distance)
    };

    *cam_transform = Transform::from_translation(cam_pos).looking_at(target, Vec3::Y);
}

fn rotate_view(controls: Res<ControlState>, mut cam: Single<&mut CameraRotationInput>) {
    if !controls.view_mode {
        cam.x = 0.;
        return;
    }

    cam.0 += controls.look_dir * -0.001;
}
