use super::TriggerThrow;
use crate::{
    GameState,
    billboards::Billboard,
    heads_database::HeadsDatabase,
    hitpoints::Hit,
    loading_assets::GameAssets,
    physics_layers::GameLayer,
    sounds::PlaySound,
    utils::{global_observer, sprite_3d_animation::AnimationTimer},
};
use avian3d::prelude::*;
use bevy::{pbr::NotShadowCaster, prelude::*};
use bevy_ballistic::launch_velocity;
use bevy_sprite3d::{Sprite3dBuilder, Sprite3dParams};
use std::f32::consts::PI;

#[derive(Component)]
struct ThrownProjectile {
    impact_animation: bool,
    damage: u32,
}

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

#[derive(Event, Debug)]
struct Explosion {
    position: Vec3,
    radius: f32,
    damage: u32,
}

#[derive(Resource)]
struct ShotAssets {
    image: Handle<Image>,
    layout: Handle<TextureAtlasLayout>,
}

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

    app.add_systems(OnEnter(GameState::Playing), setup);
    app.add_systems(Update, shot_collision.run_if(in_state(GameState::Playing)));
    app.add_systems(FixedUpdate, update_auto_rotation);

    global_observer!(app, on_trigger_thrown);
    global_observer!(app, on_explosion);
}

fn on_explosion(
    explosion: Trigger<Explosion>,
    mut commands: Commands,
    spatial_query: SpatialQuery,
) {
    let explosion = explosion.event();
    let intersections = {
        spatial_query.shape_intersections(
            &Collider::sphere(explosion.radius),
            explosion.position,
            Quat::default(),
            &SpatialQueryFilter::default().with_mask(LayerMask(
                GameLayer::Npc.to_bits() | GameLayer::Player.to_bits(),
            )),
        )
    };

    for entity in intersections.iter() {
        if let Ok(mut e) = commands.get_entity(*entity) {
            e.trigger(Hit {
                damage: explosion.damage,
            });
        }
    }
}

fn setup(mut commands: Commands, assets: Res<GameAssets>, mut sprite_params: Sprite3dParams) {
    let layout = TextureAtlasLayout::from_grid(UVec2::splat(256), 7, 6, None, None);
    let texture_atlas_layout = sprite_params.atlas_layouts.add(layout);

    commands.insert_resource(ShotAssets {
        image: assets.impact_atlas.clone(),
        layout: texture_atlas_layout,
    });
}

fn on_trigger_thrown(
    trigger: Trigger<TriggerThrow>,
    mut commands: Commands,
    query_transform: Query<&Transform>,
    assets: Res<GameAssets>,
    gltf_assets: Res<Assets<Gltf>>,
    heads_db: Res<HeadsDatabase>,
) {
    let state = trigger.event().0;

    commands.trigger(PlaySound::Throw);

    const SPEED: f32 = 35.;

    let pos = state.pos;

    let vel = if let Some(target) = state.target {
        let t = query_transform
            .get(target)
            .expect("target must have transform");

        launch_velocity(pos, t.translation, SPEED, 9.81)
            .map(|(low, _)| low)
            .unwrap()
    } else {
        state.rot.mul_quat(Quat::from_rotation_y(-PI / 2.))
            * (Vec3::new(2., 1., 0.).normalize() * SPEED)
    };

    let head = heads_db.head_stats(state.head);
    let mesh = assets.projectiles[format!("{}.glb", head.projectile).as_str()].clone();
    let asset = gltf_assets.get(&mesh).unwrap();

    //TODO: projectile db?
    let explosion_animation = !matches!(state.head, 8 | 16);

    commands
        .spawn((
            Transform::from_translation(pos),
            Name::new("projectile-thrown"),
            ThrownProjectile {
                impact_animation: explosion_animation,
                damage: head.damage,
            },
            Collider::sphere(0.4),
            CollisionLayers::new(
                LayerMask(GameLayer::Projectile.to_bits()),
                LayerMask(state.target_layer.to_bits() | GameLayer::Level.to_bits()),
            ),
            RigidBody::Dynamic,
            CollisionEventsEnabled,
            Mass(0.01),
            LinearVelocity(vel),
            Visibility::default(),
        ))
        .with_child((
            AutoRotation(Quat::from_rotation_x(0.4) * Quat::from_rotation_z(0.3)),
            SceneRoot(asset.scenes[0].clone()),
        ));
}

fn shot_collision(
    mut commands: Commands,
    mut collision_event_reader: EventReader<CollisionStarted>,
    query_shot: Query<(&ThrownProjectile, &Transform)>,
    assets: Res<ShotAssets>,
    mut sprite_params: Sprite3dParams,
) {
    for CollisionStarted(e1, e2) in collision_event_reader.read() {
        if !query_shot.contains(*e1) && !query_shot.contains(*e2) {
            continue;
        }

        let shot_entity = if query_shot.contains(*e1) { *e1 } else { *e2 };

        let Ok((shot_pos, animation, damage)) =
            query_shot.get(shot_entity).map(|(projectile, t)| {
                (
                    t.translation,
                    projectile.impact_animation,
                    projectile.damage,
                )
            })
        else {
            continue;
        };

        if let Ok(mut entity) = commands.get_entity(shot_entity) {
            entity.try_despawn();
        } else {
            continue;
        }

        commands.trigger(PlaySound::ThrowHit);

        commands.trigger(Explosion {
            damage,
            position: shot_pos,
            //TODO: should be around 1 grid in distance
            radius: 5.,
        });

        //TODO: support different impact animations
        if animation {
            commands
                .spawn(
                    Sprite3dBuilder {
                        image: assets.image.clone(),
                        pixels_per_metre: 32.,
                        alpha_mode: AlphaMode::Blend,
                        unlit: true,
                        ..default()
                    }
                    .bundle_with_atlas(
                        &mut sprite_params,
                        TextureAtlas {
                            layout: assets.layout.clone(),
                            index: 0,
                        },
                    ),
                )
                .insert((
                    Billboard,
                    Transform::from_translation(shot_pos),
                    NotShadowCaster,
                    AnimationTimer::new(Timer::from_seconds(0.02, TimerMode::Repeating)),
                ));
        }
    }
}

fn update_auto_rotation(mut query: Query<(&AutoRotation, &mut Transform)>) {
    for (auto_rotation, mut transform) in query.iter_mut() {
        transform.rotate_local(auto_rotation.0);
    }
}
