use crate::{
    GameState, billboards::Billboard, global_observer, heads_database::HeadsDatabase,
    loading_assets::HeadDropAssets, physics_layers::GameLayer, player::Player, sounds::PlaySound,
    squish_animation::SquishAnimation, tb_entities::SecretHead,
};
use avian3d::prelude::*;
use bevy::prelude::*;
use std::f32::consts::PI;

#[derive(Event, Reflect)]
pub struct HeadDrops {
    pos: Vec3,
    head_id: usize,
    impulse: bool,
}

impl HeadDrops {
    pub fn new(pos: Vec3, head_id: usize) -> Self {
        Self {
            pos,
            head_id,
            impulse: true,
        }
    }

    fn new_static(pos: Vec3, head_id: usize) -> Self {
        Self {
            pos,
            head_id,
            impulse: false,
        }
    }
}

#[derive(Component, Reflect)]
#[reflect(Component)]
struct HeadDrop(pub usize);

#[derive(Component, Reflect)]
#[reflect(Component)]
struct SecretHeadMarker;

#[derive(Event, Reflect)]
pub struct HeadCollected(pub usize);

pub fn plugin(app: &mut App) {
    app.add_systems(Update, collect_head.run_if(in_state(GameState::Playing)));
    app.add_systems(OnEnter(GameState::Playing), spawn);

    global_observer!(app, on_head_drop);
}

fn spawn(mut commands: Commands, query: Query<(Entity, &GlobalTransform, &SecretHead)>) {
    for (e, t, head) in query {
        commands.trigger(HeadDrops::new_static(
            t.translation() + Vec3::new(0., 2., 0.),
            head.head_id,
        ));

        commands.entity(e).despawn();
    }
}

fn on_head_drop(
    trigger: Trigger<HeadDrops>,
    mut commands: Commands,
    assets: Res<HeadDropAssets>,
    heads_db: Res<HeadsDatabase>,
    gltf_assets: Res<Assets<Gltf>>,
) -> Result<(), BevyError> {
    let drop = trigger.event();

    let angle = rand::random::<f32>() * PI * 2.;
    let spawn_dir = Quat::from_rotation_y(angle) * Vec3::new(0.5, 0.6, 0.).normalize();

    if drop.impulse {
        commands.trigger(PlaySound::HeadDrop);
    }

    let ability = format!("{:?}.glb", heads_db.head_stats(drop.head_id).ability).to_lowercase();
    let mesh = if let Some(handle) = assets.meshes.get(ability.as_str()) {
        gltf_assets.get(handle)
    } else {
        gltf_assets.get(&assets.meshes["none.glb"])
    }
    .ok_or_else(|| "asset not found")?;

    commands
        .spawn((
            Name::new("headdrop"),
            HeadDrop(drop.head_id),
            Transform::from_translation(drop.pos),
            Visibility::default(),
            Collider::sphere(1.5),
            LockedAxes::ROTATION_LOCKED,
            RigidBody::Dynamic,
            CollisionLayers::new(LayerMask(GameLayer::Collectibles.to_bits()), LayerMask::ALL),
            CollisionEventsEnabled,
            Restitution::new(0.6),
        ))
        .insert_if(
            ExternalImpulse::new(spawn_dir * 180.).with_persistence(false),
            || drop.impulse,
        )
        .with_child((
            Billboard,
            SquishAnimation(2.6),
            SceneRoot(mesh.scenes[0].clone()),
        ));

    Ok(())
}

fn collect_head(
    mut commands: Commands,
    mut collision_event_reader: EventReader<CollisionStarted>,
    query_player: Query<&Player>,
    query_collectable: Query<&HeadDrop>,
    query_secret: Query<&SecretHeadMarker>,
) {
    for CollisionStarted(e1, e2) in collision_event_reader.read() {
        let collectable = if query_player.contains(*e1) && query_collectable.contains(*e2) {
            *e2
        } else if query_player.contains(*e2) && query_collectable.contains(*e1) {
            *e1
        } else {
            continue;
        };

        let key = query_collectable.get(collectable).unwrap();

        let is_secret = query_secret.contains(collectable);

        if is_secret {
            commands.trigger(PlaySound::SecretHeadCollect);
        } else {
            commands.trigger(PlaySound::HeadCollect);
        }
        commands.trigger(HeadCollected(key.0));
        commands.entity(collectable).despawn();
    }
}
