mod arrow;
mod gun;
mod healing;
mod missile;
mod thrown;

pub use healing::Healing;

use crate::{
    GameState,
    aim::AimTarget,
    character::CharacterHierarchy,
    global_observer,
    heads::ActiveHeads,
    heads_database::HeadsDatabase,
    physics_layers::GameLayer,
    player::{Player, PlayerBodyMesh},
    sounds::PlaySound,
};
use bevy::prelude::*;
use healing::HealingStateChanged;
use serde::{Deserialize, Serialize};

#[derive(Event, Reflect)]
pub enum TriggerState {
    Active,
    Inactive,
}

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

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

#[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(Resource, Default)]
pub struct TriggerStateRes {
    cooldown: 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_systems(
        Update,
        (update, update_heal_ability).run_if(in_state(GameState::Playing)),
    );

    global_observer!(app, on_trigger_state);
}

fn on_trigger_state(trigger: Trigger<TriggerState>, mut res: ResMut<TriggerStateRes>) {
    res.active = matches!(trigger.event(), TriggerState::Active);
    if !res.active {
        res.cooldown = 0.0;
    }
}

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.cooldown < 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.cooldown = 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)),
            _ => 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);
        }
    }
}
