use std::{f32::consts::PI, time::Duration};

use crate::{
    DebugVisuals,
    alien::{ALIEN_ASSET_PATH, Animations},
    camera::GameCameraRig,
    cash::{Cash, CashCollectEvent},
    heads_ui::HeadChanged,
    tb_entities::SpawnPoint,
};
use avian3d::prelude::*;
use bevy::{
    input::mouse::MouseMotion,
    prelude::*,
    window::{CursorGrabMode, PrimaryWindow},
};
use bevy_dolly::prelude::Rig;
use bevy_tnua::{TnuaUserControlsSystemSet, prelude::*};
use bevy_tnua_avian3d::TnuaAvian3dSensorShape;

#[derive(Component, Default)]
pub struct Player;

#[derive(Component, Default)]
struct PlayerAnimations;

#[derive(Component, Default)]
pub struct PlayerHead;

#[derive(Component, Default)]
pub struct PlayerRig;

#[derive(Resource, Default)]
struct PlayerSpawned {
    spawned: bool,
}

#[derive(Resource, Default)]
struct PlayerMovement {
    any_direction: bool,
}

pub fn plugin(app: &mut App) {
    app.init_resource::<PlayerSpawned>();
    app.init_resource::<PlayerMovement>();
    app.add_systems(Startup, initial_grab_cursor);
    app.add_systems(
        Update,
        (
            spawn,
            update_camera,
            mouse_rotate,
            collect_cash,
            toggle_animation,
            setup_animations_marker_for_player,
        ),
    );
    app.add_systems(
        FixedUpdate,
        apply_controls.in_set(TnuaUserControlsSystemSet),
    );

    app.add_observer(updaate_head);
}

fn spawn(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    query: Query<&Transform, With<SpawnPoint>>,
    mut player_spawned: ResMut<PlayerSpawned>,
) {
    if player_spawned.spawned {
        return;
    }

    let Some(spawn) = query.iter().next() else {
        return;
    };

    let transform = Transform::from_translation(spawn.translation + Vec3::new(0., 3., 0.));

    let mesh = asset_server
        .load(GltfAssetLabel::Scene(0).from_asset("models/heads/angry demonstrator.glb"));

    commands
        .spawn((
            Name::from("player"),
            Player,
            transform,
            TransformInterpolation,
            Visibility::default(),
            RigidBody::Dynamic,
            Collider::capsule(1.2, 1.5),
            LockedAxes::ROTATION_LOCKED,
            TnuaController::default(),
            TnuaAvian3dSensorShape(Collider::cylinder(0.8, 0.0)),
        ))
        .with_child((
            Name::from("head"),
            PlayerHead,
            Transform::from_translation(Vec3::new(0., -0.5, 0.))
                .with_rotation(Quat::from_rotation_y(std::f32::consts::PI)),
            SceneRoot(mesh),
        ))
        .with_child((
            Name::from("body rig"),
            PlayerRig,
            Transform::from_translation(Vec3::new(0., -3., 0.))
                .with_rotation(Quat::from_rotation_y(std::f32::consts::PI))
                .with_scale(Vec3::splat(1.5)),
            SceneRoot(asset_server.load(GltfAssetLabel::Scene(0).from_asset(ALIEN_ASSET_PATH))),
        ));

    commands.spawn((
        AudioPlayer::new(asset_server.load("sfx/heads/angry demonstrator.ogg")),
        PlaybackSettings::DESPAWN,
    ));

    player_spawned.spawned = true;
}

fn mouse_rotate(
    mut mouse: EventReader<MouseMotion>,
    // todo: Put the player head as a child of the rig to avoid this mess:
    mut player: Query<&mut Transform, Or<(With<PlayerRig>, With<PlayerHead>)>>,
) {
    for ev in mouse.read() {
        for mut tr in &mut player {
            tr.rotate_y(ev.delta.x * -0.001);
        }
    }
}

fn toggle_grab_cursor(window: &mut Window) {
    match window.cursor_options.grab_mode {
        CursorGrabMode::None => {
            window.cursor_options.grab_mode = CursorGrabMode::Confined;
            window.cursor_options.visible = false;
        }
        _ => {
            window.cursor_options.grab_mode = CursorGrabMode::None;
            window.cursor_options.visible = true;
        }
    }
}

fn initial_grab_cursor(mut primary_window: Query<&mut Window, With<PrimaryWindow>>) {
    if let Ok(mut window) = primary_window.get_single_mut() {
        toggle_grab_cursor(&mut window);
    } else {
        warn!("Primary window not found for `initial_grab_cursor`!");
    }
}

fn apply_controls(
    keyboard: Res<ButtonInput<KeyCode>>,
    mut query: Query<&mut TnuaController>,
    player: Query<&Transform, With<PlayerRig>>,
    mut controls: ResMut<PlayerMovement>,
) {
    let Ok(mut controller) = query.get_single_mut() else {
        return;
    };

    let Ok(player) = player.get_single() else {
        return;
    };

    let mut direction = Vec3::ZERO;

    if keyboard.pressed(KeyCode::KeyW) {
        direction = player.forward().as_vec3() * -1.;
    }
    if keyboard.pressed(KeyCode::KeyS) {
        direction = player.forward().as_vec3();
    }
    if keyboard.pressed(KeyCode::KeyA) {
        direction += player.right().as_vec3();
    }
    if keyboard.pressed(KeyCode::KeyD) {
        direction += player.left().as_vec3();
    }

    if controls.any_direction != (direction != Vec3::ZERO) {
        controls.any_direction = direction != Vec3::ZERO;
    }

    controller.basis(TnuaBuiltinWalk {
        // The `desired_velocity` determines how the character will move.
        desired_velocity: direction.normalize_or_zero() * 10.0,
        spring_strengh: 1000.,
        spring_dampening: 0.5,
        // The `float_height` must be greater (even if by little) from the distance between the
        // character's center and the lowest point of its collider.
        float_height: 3.0,
        // `TnuaBuiltinWalk` has many other fields for customizing the movement - but they have
        // sensible defaults. Refer to the `TnuaBuiltinWalk`'s documentation to learn what they do.
        ..Default::default()
    });

    // Feed the jump action every frame as long as the player holds the jump button. If the player
    // stops holding the jump button, simply stop feeding the action.
    if keyboard.pressed(KeyCode::Space) {
        controller.action(TnuaBuiltinJump {
            // The height is the only mandatory field of the jump button.
            height: 4.0,
            // `TnuaBuiltinJump` also has customization fields with sensible defaults.
            ..Default::default()
        });
    }
}

fn update_camera(
    player: Query<&GlobalTransform, With<PlayerRig>>,
    mut rig: Single<&mut Rig>,
    res: Res<DebugVisuals>,
) {
    let Some(player) = player.iter().next() else {
        return;
    };

    if !res.cam_follow {
        return;
    }

    rig.driver_mut::<GameCameraRig>().set_position_target(
        player.translation(),
        player.rotation() * Quat::from_rotation_y(PI),
    );
}

fn collect_cash(
    mut commands: Commands,
    mut collision_event_reader: EventReader<CollisionStarted>,
    query_player: Query<&Player>,
    query_cash: Query<&Cash>,
) {
    for CollisionStarted(e1, e2) in collision_event_reader.read() {
        let collect = if query_player.contains(*e1) && query_cash.contains(*e2) {
            Some(*e2)
        } else if query_player.contains(*e2) && query_cash.contains(*e1) {
            Some(*e1)
        } else {
            None
        };

        if let Some(cash) = collect {
            commands.trigger(CashCollectEvent);
            commands.entity(cash).despawn();
        }
    }
}

fn setup_animations_marker_for_player(
    mut commands: Commands,
    animation_handles: Query<Entity, Added<AnimationGraphHandle>>,
    parent_query: Query<&Parent>,
    player: Query<&Player>,
) {
    for entity in animation_handles.iter() {
        for ancestor in parent_query.iter_ancestors(entity) {
            if player.contains(ancestor) {
                commands.entity(entity).insert(PlayerAnimations);
            }
        }
    }
}

fn toggle_animation(
    animations: Res<Animations>,
    mut transitions: Query<
        (&mut AnimationTransitions, &mut AnimationPlayer),
        With<PlayerAnimations>,
    >,
    movement: Res<PlayerMovement>,
) {
    if movement.is_changed() {
        let index = if movement.any_direction { 0 } else { 1 };

        for (mut transition, mut player) in &mut transitions {
            transition
                .play(
                    &mut player,
                    animations.animations[index],
                    Duration::from_millis(100),
                )
                .repeat();
        }
    }
}

fn updaate_head(
    trigger: Trigger<HeadChanged>,
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    head: Query<Entity, With<PlayerHead>>,
) {
    let Ok(head) = head.get_single() else {
        return;
    };

    let head_str = match trigger.0 {
        0 => "angry demonstrator",
        1 => "commando",
        2 => "goblin",
        3 => "highland hammer thrower",
        _ => "legionnaire",
    };

    commands.spawn((
        AudioPlayer::new(asset_server.load(format!("sfx/heads/{}.ogg", head_str))),
        PlaybackSettings::DESPAWN,
    ));

    let mesh = asset_server
        .load(GltfAssetLabel::Scene(0).from_asset(format!("models/heads/{}.glb", head_str)));

    commands.entity(head).insert(SceneRoot(mesh));
}
