mod heads_ui;

use crate::{
    GameState,
    backpack::{BackbackSwapEvent, Backpack},
    global_observer,
    heads_database::HeadsDatabase,
    hitpoints::Hitpoints,
    player::Player,
    sounds::PlaySound,
};
use bevy::prelude::*;

pub static HEAD_COUNT: usize = 18;
pub static HEAD_SLOTS: usize = 5;

#[derive(Resource, Default)]
pub struct HeadsImages {
    pub heads: Vec<Handle<Image>>,
}

#[derive(Clone, Copy, Debug, PartialEq, Reflect)]
pub struct HeadState {
    pub head: usize,
    pub health: u32,
    pub health_max: u32,
    pub ammo: u32,
    pub ammo_max: u32,
    pub reload_duration: f32,
    pub last_use: f32,
}

impl HeadState {
    pub fn new(head: usize, heads_db: &HeadsDatabase) -> Self {
        let ammo = heads_db.head_stats(head).ammo;
        Self {
            head,
            health: 100,
            health_max: 100,
            ammo,
            ammo_max: ammo,
            reload_duration: 5.,
            last_use: 0.,
        }
    }

    pub fn has_ammo(&self) -> bool {
        self.ammo > 0
    }
}

#[derive(Component, Default, Reflect, Debug)]
#[reflect(Component)]
pub struct ActiveHeads {
    heads: [Option<HeadState>; 5],
    current_slot: usize,
}

impl ActiveHeads {
    pub fn new(heads: [Option<HeadState>; 5]) -> Self {
        Self {
            heads,
            current_slot: 0,
        }
    }

    pub fn current(&self) -> Option<HeadState> {
        self.heads[self.current_slot]
    }

    pub fn use_ammo(&mut self, time: f32) {
        let Some(head) = &mut self.heads[self.current_slot] else {
            error!("cannot use ammo of empty head");
            return;
        };

        head.last_use = time;
        head.ammo = head.ammo.saturating_sub(1);
    }

    pub fn medic_heal(&mut self, heal_amount: u32, time: f32) -> Option<u32> {
        let mut healed = false;
        for (index, head) in self.heads.iter_mut().enumerate() {
            if index == self.current_slot {
                continue;
            }

            if let Some(head) = head {
                if head.health < head.health_max {
                    head.health = head
                        .health
                        .saturating_add(heal_amount)
                        .clamp(0, head.health_max);
                    healed = true;
                }
            }
        }

        if healed {
            let Some(head) = &mut self.heads[self.current_slot] else {
                error!("cannot heal with empty head");
                return None;
            };

            head.last_use = time;
            head.health = head.health.saturating_sub(1);

            Some(head.health)
        } else {
            None
        }
    }

    pub fn slot(&self) -> usize {
        self.current_slot
    }

    pub fn head(&self, slot: usize) -> Option<HeadState> {
        self.heads[slot]
    }

    pub fn reloading(&self) -> bool {
        for head in self.heads {
            let Some(head) = head else {
                continue;
            };

            if head.ammo == 0 {
                return true;
            }
        }

        false
    }

    pub fn hp(&self) -> Hitpoints {
        if let Some(head) = &self.heads[self.current_slot] {
            Hitpoints::new(head.health_max).with_health(head.health)
        } else {
            Hitpoints::new(0)
        }
    }

    pub fn set_hitpoint(&mut self, hp: &Hitpoints) {
        let Some(head) = &mut self.heads[self.current_slot] else {
            error!("cannot use ammo of empty head");
            return;
        };

        (head.health, head.health_max) = hp.get()
    }
}

#[derive(Event, Reflect)]
pub enum SelectActiveHead {
    Left,
    Right,
}

#[derive(Event)]
pub struct HeadChanged(pub usize);

pub fn plugin(app: &mut App) {
    app.add_plugins(heads_ui::plugin);

    app.add_systems(OnEnter(GameState::Playing), setup);
    app.add_systems(
        Update,
        (reload, sync_hp).run_if(in_state(GameState::Playing)),
    );

    global_observer!(app, on_select_active_head);
    global_observer!(app, on_swap_backpack);
}

fn setup(mut commands: Commands, asset_server: Res<AssetServer>, heads: Res<HeadsDatabase>) {
    // TODO: load via asset loader
    let heads = (0usize..HEAD_COUNT)
        .map(|i| asset_server.load(format!("ui/heads/{}.png", heads.head_key(i))))
        .collect();

    commands.insert_resource(HeadsImages { heads });
}

fn sync_hp(mut query: Query<(&mut ActiveHeads, &Hitpoints)>) {
    for (mut active_heads, hp) in query.iter_mut() {
        if active_heads.hp() != *hp {
            active_heads.set_hitpoint(hp);
        }
    }
}

fn reload(mut commands: Commands, mut active: Query<&mut ActiveHeads>, time: Res<Time>) {
    for mut active in active.iter_mut() {
        if !active.reloading() {
            continue;
        }

        for head in active.heads.iter_mut() {
            let Some(head) = head else {
                continue;
            };

            if !head.has_ammo() && (head.last_use + head.reload_duration <= time.elapsed_secs()) {
                // only for player?
                commands.trigger(PlaySound::Reloaded);
                head.ammo = head.ammo_max;
            }
        }
    }
}

fn on_select_active_head(
    trigger: Trigger<SelectActiveHead>,
    mut commands: Commands,
    mut query: Query<(&mut ActiveHeads, &mut Hitpoints), With<Player>>,
) {
    let Ok((mut active_heads, mut hp)) = query.single_mut() else {
        return;
    };

    match trigger.event() {
        SelectActiveHead::Right => {
            active_heads.current_slot = (active_heads.current_slot + 1) % HEAD_SLOTS;
        }
        SelectActiveHead::Left => {
            active_heads.current_slot = (active_heads.current_slot + (HEAD_SLOTS - 1)) % HEAD_SLOTS;
        }
    }

    hp.set_health(active_heads.current().unwrap().health);

    commands.trigger(PlaySound::Selection);
    commands.trigger(HeadChanged(
        active_heads.heads[active_heads.current_slot].unwrap().head,
    ));
}

fn on_swap_backpack(
    trigger: Trigger<BackbackSwapEvent>,
    mut commands: Commands,
    mut query: Query<(&mut ActiveHeads, &mut Hitpoints), With<Player>>,
    mut backpack: ResMut<Backpack>,
) {
    let backpack_slot = trigger.event().0;

    let head = backpack.heads.get(backpack_slot).unwrap();

    let Ok((mut active_heads, mut hp)) = query.single_mut() else {
        return;
    };

    let current_active_slot = active_heads.current_slot;

    let current_active_head = active_heads.heads[current_active_slot];
    active_heads.heads[current_active_slot] = Some(*head);

    if let Some(old_active) = current_active_head {
        backpack.heads[backpack_slot] = old_active;
    } else {
        backpack.heads.remove(backpack_slot);
    }

    hp.set_health(active_heads.current().unwrap().health);

    commands.trigger(HeadChanged(
        active_heads.heads[active_heads.current_slot].unwrap().head,
    ));
}
