use crate::{
    GameState,
    animation::AnimationFlags,
    character::{CharacterAnimations, HasCharacterAnimations},
    protocol::PlaySound,
};
use bevy::prelude::*;
use serde::{Deserialize, Serialize};

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

#[derive(Event, Reflect)]
pub struct Hit {
    pub damage: u32,
}

#[derive(Component, Reflect, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Hitpoints {
    max: u32,
    current: u32,
    last_hit_timestamp: f32,
}

impl Hitpoints {
    pub fn new(v: u32) -> Self {
        Self {
            max: v,
            current: v,
            last_hit_timestamp: f32::NEG_INFINITY,
        }
    }

    pub fn with_health(mut self, v: u32) -> Self {
        self.current = v;
        self
    }

    pub fn health(&self) -> f32 {
        self.current as f32 / self.max as f32
    }

    pub fn set_health(&mut self, v: u32) {
        self.current = v;
    }

    pub fn heal(&mut self, v: u32) {
        self.current += v;
    }

    pub fn get(&self) -> (u32, u32) {
        (self.current, self.max)
    }

    pub fn max(&self) -> bool {
        self.current == self.max
    }

    pub fn time_since_hit(&self, time: &Time) -> f32 {
        time.elapsed_secs() - self.last_hit_timestamp
    }
}

pub fn plugin(app: &mut App) {
    app.add_systems(Update, on_hp_added).add_systems(
        PreUpdate,
        reset_hit_animation_flag.run_if(in_state(GameState::Playing)),
    );
}

fn on_hp_added(mut commands: Commands, query: Query<Entity, Added<Hitpoints>>) {
    for e in query.iter() {
        commands.entity(e).observe(on_hit);
    }
}

fn on_hit(
    trigger: Trigger<Hit>,
    mut commands: Commands,
    mut query: Query<(&mut Hitpoints, Option<&mut AnimationFlags>)>,
    time: Res<Time>,
) {
    let Hit { damage } = trigger.event();

    let Ok((mut hp, flags)) = query.get_mut(trigger.target()) else {
        return;
    };

    commands.trigger(PlaySound::Hit);

    if let Some(mut flags) = flags {
        flags.hit = true;
    }

    hp.current = hp.current.saturating_sub(*damage);
    hp.last_hit_timestamp = time.elapsed_secs();

    if hp.current == 0 {
        commands.trigger_targets(Kill, trigger.target());
    }
}

fn reset_hit_animation_flag(
    mut query: Query<(&Hitpoints, &HasCharacterAnimations, &mut AnimationFlags)>,
    animations: Query<(&AnimationGraphHandle, &CharacterAnimations)>,
    graphs: Res<Assets<AnimationGraph>>,
    clips: Res<Assets<AnimationClip>>,
    time: Res<Time>,
) {
    for (hp, anims, mut flags) in query.iter_mut() {
        let (graph_handle, anims) = animations.get(*anims.collection()).unwrap();
        let graph = graphs.get(graph_handle.id()).unwrap();
        let hit_anim = match graph.get(anims.hit).unwrap().node_type {
            AnimationNodeType::Clip(ref handle) => clips.get(handle.id()).unwrap(),
            _ => unreachable!(),
        };
        flags.hit = hp.time_since_hit(&time) < hit_anim.duration();
    }
}
