use crate::{
    GameState,
    camera::{CameraArmRotation, CameraTarget},
    cash::{Cash, CashCollectEvent},
    character::{AnimatedCharacter, CharacterAnimations},
    control::controller_common::{CharacterControllerBundle, PlayerMovement},
    global_observer,
    head::ActiveHead,
    heads::{ActiveHeads, HeadChanged, HeadState},
    heads_database::{HeadControls, HeadsDatabase},
    hitpoints::Hitpoints,
    loading_assets::AudioAssets,
    physics_layers::GameLayer,
    sounds::PlaySound,
    tb_entities::SpawnPoint,
};
use avian3d::{math::Vector, prelude::*};
use bevy::{
    input::common_conditions::input_just_pressed,
    prelude::*,
    window::{CursorGrabMode, PrimaryWindow},
};
use std::time::Duration;

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

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

#[derive(Component, Default)]
#[require(Transform, Visibility)]
pub struct PlayerBodyMesh;

pub fn plugin(app: &mut App) {
    app.add_systems(Startup, (toggle_cursor_system, cursor_recenter));
    app.add_systems(OnEnter(GameState::Playing), spawn);
    app.add_systems(
        Update,
        (
            collect_cash,
            toggle_animation,
            setup_animations_marker_for_player,
            toggle_cursor_system.run_if(input_just_pressed(KeyCode::Escape)),
        )
            .run_if(in_state(GameState::Playing)),
    );

    global_observer!(app, on_update_head);
}

fn spawn(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    query: Query<&Transform, With<SpawnPoint>>,
    heads_db: Res<HeadsDatabase>,
) {
    let Some(spawn) = query.iter().next() else {
        return;
    };

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

    let gravity = Vector::NEG_Y * 40.0;
    let collider = Collider::capsule(0.9, 1.2);

    commands.spawn((
        Name::from("player"),
        Player,
        ActiveHead(0),
        ActiveHeads::new([
            Some(HeadState::new(0, heads_db.as_ref())),
            Some(HeadState::new(3, heads_db.as_ref())),
            Some(HeadState::new(6, heads_db.as_ref())),
            Some(HeadState::new(10, heads_db.as_ref())),
            Some(HeadState::new(9, heads_db.as_ref())),
        ]),
        Hitpoints::new(100),
        CameraTarget,
        transform,
        Visibility::default(),
        // LockedAxes::ROTATION_LOCKED, todo
        CollisionLayers::new(LayerMask(GameLayer::Player.to_bits()), LayerMask::ALL),
        CharacterControllerBundle::new(collider, gravity),
        children![(
            Name::new("player-rig"),
            PlayerBodyMesh,
            CameraArmRotation,
            children![AnimatedCharacter::new(0)]
        )],
    ));

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

fn cursor_recenter(q_windows: Single<&mut Window, With<PrimaryWindow>>) {
    let mut primary_window = q_windows;
    let center = Vec2::new(
        primary_window.resolution.width() / 2.,
        primary_window.resolution.height() / 2.,
    );
    primary_window.set_cursor_position(Some(center));
}

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 toggle_cursor_system(mut window: Single<&mut Window, With<PrimaryWindow>>) {
    toggle_grab_cursor(&mut window);
}

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>>,
    children: Query<&ChildOf>,
    player: Query<&PlayerBodyMesh>,
) {
    for entity in animation_handles.iter() {
        for ancestor in children.iter_ancestors(entity) {
            if player.contains(ancestor) {
                commands.entity(entity).insert(PlayerAnimations);
                return;
            }
        }
    }
}

fn toggle_animation(
    mut transitions: Query<
        (
            &mut AnimationTransitions,
            &mut AnimationPlayer,
            &CharacterAnimations,
        ),
        With<PlayerAnimations>,
    >,
    movement: Res<PlayerMovement>,
) {
    if movement.is_changed() {
        for (mut transition, mut player, animations) in &mut transitions {
            let index = if movement.any_direction {
                animations.run
            } else {
                animations.idle
            };

            let (mut index, mut duration) = (index, Duration::from_millis(100));

            if movement.shooting {
                if let Some(shoot_anim) = animations.shooting {
                    (index, duration) = (shoot_anim, Duration::from_millis(10));
                }
            }

            if transition
                .get_main_animation()
                .map(|playing| playing != index)
                .unwrap_or_default()
            {
                transition.play(&mut player, index, duration).repeat();
            }
        }
    }
}

fn on_update_head(
    trigger: Trigger<HeadChanged>,
    mut commands: Commands,
    body_mesh: Single<Entity, With<PlayerBodyMesh>>,
    mut player: Single<&mut ActiveHead, With<Player>>,
    head_db: Res<HeadsDatabase>,
    audio_assets: Res<AudioAssets>,
) {
    let body_mesh = *body_mesh;

    player.0 = trigger.0;

    let head_str = head_db.head_key(trigger.0);

    commands.trigger(PlaySound::Head(head_str.to_string()));

    commands.entity(body_mesh).despawn_related::<Children>();

    commands
        .entity(body_mesh)
        .with_child(AnimatedCharacter::new(trigger.0));

    //TODO: make part of full character mesh later
    if head_db.head_stats(trigger.0).controls == HeadControls::Plane {
        commands.entity(body_mesh).with_child((
            Name::new("sfx"),
            AudioPlayer::new(audio_assets.jet.clone()),
            PlaybackSettings {
                mode: bevy::audio::PlaybackMode::Loop,
                ..Default::default()
            },
        ));
    }
}
