pub mod arrow;
pub mod curver;
pub mod gun;
pub mod healing;
pub mod missile;
pub mod thrown;

use crate::{
    GameState,
    aim::AimTarget,
    character::CharacterHierarchy,
    global_observer,
    heads::ActiveHeads,
    heads_database::HeadsDatabase,
    loading_assets::GameAssets,
    physics_layers::GameLayer,
    player::{Player, PlayerBodyMesh},
    protocol::PlaySound,
    utils::{billboards::Billboard, sprite_3d_animation::AnimationTimer},
};
#[cfg(feature = "server")]
use crate::{control::ControlState, head::ActiveHead};
use bevy::{pbr::NotShadowCaster, prelude::*};
use bevy_sprite3d::{Sprite3dBuilder, Sprite3dParams};
pub use healing::Healing;
use healing::HealingStateChanged;
#[cfg(feature = "server")]
use lightyear::prelude::input::native::ActionState;
use serde::{Deserialize, Serialize};

#[derive(Debug, Copy, Clone, PartialEq, Reflect, Default, Serialize, Deserialize)]
pub enum HeadAbility {
    #[default]
    None,
    Arrow,
    Thrown,
    Gun,
    Missile,
    Medic,
    Curver,
    Boat,
    Turbo,
    Spray,
}

#[derive(Debug, Reflect, Clone, Copy)]
pub struct TriggerData {
    target: Option<Entity>,
    dir: Dir3,
    rot: Quat,
    pos: Vec3,
    target_layer: GameLayer,
    head: usize,
}

impl TriggerData {
    pub fn new(
        target: Option<Entity>,
        dir: Dir3,
        rot: Quat,
        pos: Vec3,
        target_layer: GameLayer,
        head: usize,
    ) -> Self {
        Self {
            target,
            dir,
            rot,
            pos,
            target_layer,
            head,
        }
    }
}

#[derive(Event, Reflect)]
pub struct TriggerGun(pub TriggerData);
#[derive(Event, Reflect)]
pub struct TriggerArrow(pub TriggerData);
#[derive(Event, Reflect)]
pub struct TriggerThrow(pub TriggerData);
#[derive(Event, Reflect)]
pub struct TriggerMissile(pub TriggerData);
#[derive(Event, Reflect)]
pub struct TriggerCurver(pub TriggerData);

#[derive(Resource, Default)]
pub struct TriggerStateRes {
    next_trigger_timestamp: f32,
    active: bool,
}

impl TriggerStateRes {
    pub fn is_active(&self) -> bool {
        self.active
    }
}

pub fn plugin(app: &mut App) {
    app.init_resource::<TriggerStateRes>();

    app.add_plugins(gun::plugin);
    app.add_plugins(thrown::plugin);
    app.add_plugins(arrow::plugin);
    app.add_plugins(missile::plugin);
    app.add_plugins(healing::plugin);
    app.add_plugins(curver::plugin);

    app.add_systems(OnEnter(GameState::Playing), setup);
    app.add_systems(
        Update,
        (update, update_heal_ability).run_if(in_state(GameState::Playing)),
    );
    #[cfg(feature = "server")]
    app.add_systems(
        FixedUpdate,
        on_trigger_state.run_if(in_state(GameState::Playing)),
    );

    global_observer!(app, build_explosion_sprite);
}

#[cfg(feature = "server")]
fn on_trigger_state(
    mut res: ResMut<TriggerStateRes>,
    player: Query<(&ActiveHead, &ActionState<ControlState>), With<Player>>,
    headdb: Res<HeadsDatabase>,
    time: Res<Time>,
) {
    for (player_head, controls) in player.iter() {
        res.active = controls.trigger;
        if controls.just_triggered {
            let head_stats = headdb.head_stats(player_head.0);
            res.next_trigger_timestamp = time.elapsed_secs() + head_stats.shoot_offset;
        }
    }
}

fn update(
    mut res: ResMut<TriggerStateRes>,
    mut commands: Commands,
    player_rot: Query<&Transform, With<PlayerBodyMesh>>,
    player_query: Query<(Entity, &AimTarget), With<Player>>,
    mut active_heads: Single<&mut ActiveHeads, With<Player>>,
    heads_db: Res<HeadsDatabase>,
    time: Res<Time>,
    character: CharacterHierarchy,
) {
    if res.active && res.next_trigger_timestamp < time.elapsed_secs() {
        let Some(state) = active_heads.current() else {
            return;
        };

        if !state.has_ammo() {
            commands.trigger(PlaySound::Invalid);
            return;
        }

        let Some((player, target)) = player_query.iter().next() else {
            return;
        };

        let Some(projectile_origin) = character
            .projectile_origin(player)
            .map(|origin| origin.translation())
        else {
            return;
        };

        let Some((rot, dir)) = player_rot.iter().next().map(|t| (t.rotation, t.forward())) else {
            return;
        };

        let head = heads_db.head_stats(state.head);

        if matches!(head.ability, HeadAbility::None | HeadAbility::Medic) {
            return;
        }

        active_heads.use_ammo(time.elapsed_secs());

        res.next_trigger_timestamp = time.elapsed_secs() + (1. / head.aps);

        let trigger_state = TriggerData {
            dir,
            rot,
            pos: projectile_origin,
            target: target.0,
            target_layer: GameLayer::Npc,
            head: state.head,
        };

        match head.ability {
            HeadAbility::Thrown => commands.trigger(TriggerThrow(trigger_state)),
            HeadAbility::Gun => commands.trigger(TriggerGun(trigger_state)),
            HeadAbility::Missile => commands.trigger(TriggerMissile(trigger_state)),
            HeadAbility::Arrow => commands.trigger(TriggerArrow(trigger_state)),
            HeadAbility::Curver => commands.trigger(TriggerCurver(trigger_state)),
            _ => panic!("Unhandled head ability"),
        };
    }
}

fn update_heal_ability(
    res: Res<TriggerStateRes>,
    mut commands: Commands,
    active_heads: Single<(Entity, &ActiveHeads), With<Player>>,
    heads_db: Res<HeadsDatabase>,
) {
    if res.is_changed() {
        let Some(state) = active_heads.1.current() else {
            return;
        };

        let player = active_heads.0;

        let head = heads_db.head_stats(state.head);

        if !matches!(head.ability, HeadAbility::Medic) {
            return;
        }

        if res.active {
            commands.trigger_targets(HealingStateChanged::Started, player);
        } else {
            commands.trigger_targets(HealingStateChanged::Stopped, player);
        }
    }
}

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

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,
    });
}

#[derive(Clone, Copy, Event, Serialize, Deserialize, PartialEq)]
pub struct BuildExplosionSprite {
    pos: Vec3,
    pixels_per_meter: f32,
    time: f32,
}

fn build_explosion_sprite(
    trigger: Trigger<BuildExplosionSprite>,
    mut commands: Commands,
    assets: Res<ShotAssets>,
    mut sprite_params: Sprite3dParams,
) {
    commands.spawn((
        Transform::from_translation(trigger.event().pos),
        Sprite3dBuilder {
            image: assets.image.clone(),
            pixels_per_metre: trigger.event().pixels_per_meter,
            alpha_mode: AlphaMode::Blend,
            unlit: true,
            ..default()
        }
        .bundle_with_atlas(
            &mut sprite_params,
            TextureAtlas {
                layout: assets.layout.clone(),
                index: 0,
            },
        ),
        Billboard::All,
        NotShadowCaster,
        AnimationTimer::new(Timer::from_seconds(
            trigger.event().time,
            TimerMode::Repeating,
        )),
    ));
}
