use std::{collections::HashMap, fmt::Display, time::Duration};

use bevy::prelude::Reflect;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use super::prelude::*;

#[derive(
    Clone, Copy, Debug, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize, Reflect,
)]
pub struct SiteId(Uuid);

impl SiteId {
    pub const PLACEHOLDER: Self = SiteId(Uuid::from_u128(0));

    pub fn generate() -> Self {
        Self(Uuid::new_v4())
    }
}

impl Display for SiteId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "site-{}", self.0)
    }
}

#[derive(Debug, Serialize, Deserialize, Reflect)]
pub struct Site {
    id: SiteId,
    accumulated_time: Duration,
    pub name: String,
    pub definition: SiteDef,
    pub position: Coords,
    pub areas: Vec<SiteArea>,
}

impl Site {
    #[inline]
    const fn minimum_tick() -> Duration {
        Duration::from_secs(3600)
    }

    pub fn new(
        definition: SiteDef,
        name: impl Into<String>,
        position: Coords,
        areas: Vec<SiteArea>,
    ) -> Self {
        Self {
            id: SiteId::generate(),
            name: name.into(),
            definition,
            position,
            areas,
            accumulated_time: Duration::default(),
        }
    }

    pub fn id(&self) -> SiteId {
        self.id
    }

    fn inner_tick(&mut self) {
        // Maps where creatures are going during patrol
        let mut patrol_map: HashMap<CreatureId, (usize, usize)> = HashMap::new();
        let site_size = self.areas.len();
        for (area_index, area) in self.areas.iter_mut().enumerate() {
            // Resolve combat per area
            if let Some(results) = combat::resolve_combat(&area.population, 600) {
                for attack in results.attacks {
                    // println!(
                    //     "{attacker} [{attacker_id}] attacks {target} [{target_id}]: {attack}",
                    //     attacker = attack.attacker.map_or("Natural causes", |attacker| &area
                    //         .find_creature(attacker)
                    //         .unwrap()
                    //         .definition
                    //         .name_singular),
                    //     attacker_id = attack
                    //         .attacker
                    //         .map_or("<no id>".into(), |id| id.to_string()),
                    //     target = area
                    //         .find_creature(attack.target)
                    //         .unwrap()
                    //         .definition
                    //         .name_singular,
                    //     target_id = attack.target
                    // );
                    if let Some(damage) = attack.damage {
                        let target = match area
                            .population
                            .iter_mut()
                            .find(|creature| creature.id() == attack.target)
                        {
                            Some(target) => target,
                            None => continue,
                        };
                        target.state.damage = target.state.damage.saturating_add(damage);
                    }
                }
                for death in results.kills {
                    area.population
                        .retain(|creature| creature.id() != death.killed);
                }
            }

            // Resolve patrolling creatures
            if site_size > 1 {
                for creature in area.population.iter() {
                    if fastrand::f32() < creature.patrol_chance() {
                        // Make sure the creature patrols to a different area
                        let target_index = {
                            // Remove the current area from the range
                            let index = fastrand::usize(0..site_size - 1);
                            if index < area_index {
                                index
                            } else {
                                // Skip the current area
                                index + 1
                            }
                        };
                        patrol_map.insert(creature.id(), (area_index, target_index));
                    }
                }
            }
        }

        if !patrol_map.is_empty() {
            println!("Patrols:");
            for (id, (from, to)) in patrol_map.iter() {
                println!("\t[{id}] patrols [{from}] -> [{to}]")
            }
        }

        // Apply patrol map
        for (creature_id, (from_index, to_index)) in patrol_map {
            let creature = self
                .areas
                .get_mut(from_index)
                .expect("Patrol: getting the area where the patrolling creature is")
                .remove_creature(creature_id)
                .expect("Patrol: finding the creature inside the area");
            self.areas
                .get_mut(to_index)
                .expect("Patrol: getting the area where the creature is moving to")
                .population
                .push(creature);
        }

        println!("Tick! {self}");
    }

    pub fn generate_from_def(
        site_def: &SiteDef,
        name: impl Into<String>,
        position: Coords,
    ) -> Self {
        Self::new(
            site_def.clone(),
            name,
            position,
            vec![
                SiteArea::default();
                fastrand::usize(site_def.min_size as usize..=site_def.max_size as usize)
            ],
        )
    }

    pub fn populate_randomly(&mut self, creature_defs: &[&CreatureDef]) {
        let area_count = self.areas.len();
        {
            let min_count = area_count;
            let max_count = (5 + (area_count - 1) * 3).max(min_count);
            for _ in 0..fastrand::usize(min_count..=max_count) {
                let area_t = fastrand::f32().powi(2);
                let index = (area_t * (area_count - 1) as f32).round() as usize;
                let creature = Creature::generate_from_def(
                    (*fastrand::choice(creature_defs.iter()).unwrap()).clone(),
                );
                self.areas
                    .get_mut(index)
                    .expect("Creature generation index for depth is out of bounds")
                    .population
                    .push(creature)
            }
        }
    }

    pub fn advance_time(&mut self, time: Duration) {
        self.accumulated_time += time;
        while self.accumulated_time >= Self::minimum_tick() {
            self.inner_tick();
            self.accumulated_time -= Self::minimum_tick();
        }
    }

    /// Finds a creature with given id, removes it from the site and returns it with ownership
    pub fn remove_creature(&mut self, id: CreatureId) -> Option<Creature> {
        for area in self.areas.iter_mut() {
            if let Some(creature) = area.remove_creature(id) {
                return Some(creature);
            }
        }
        None
    }

    pub fn creatures(&self) -> Vec<&Creature> {
        self.areas
            .iter()
            .flat_map(|area| area.population.iter())
            .collect()
    }
}

impl Display for Site {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "site \"{name}\" [{id}] [",
            name = self.name,
            id = self.id
        )?;
        for (index, area) in self.areas.iter().enumerate() {
            write!(f, "\n\tarea {index} [")?;
            for creature in area.population.iter() {
                write!(f, "\n\t\t{creature}")?;
            }
            if !area.population.is_empty() {
                write!(f, "\n\t")?;
            }
            write!(f, "]")?;
        }
        if !self.areas.is_empty() {
            writeln!(f)?;
        }
        write!(f, "]")
    }
}

#[derive(Clone, Debug, Default, Serialize, Deserialize, Reflect)]
pub struct SiteArea {
    pub population: Vec<Creature>,
}

impl SiteArea {
    pub fn from_creatures(creatures: &[Creature]) -> Self {
        Self {
            population: creatures.to_vec(),
        }
    }

    pub fn find_creature(&self, id: CreatureId) -> Option<&Creature> {
        self.population.iter().find(|creature| creature.id() == id)
    }

    pub fn find_creature_mut(&mut self, id: CreatureId) -> Option<&mut Creature> {
        self.population
            .iter_mut()
            .find(|creature| creature.id() == id)
    }

    /// Finds a creature with given id, removes it from the area and returns it with ownership
    pub fn remove_creature(&mut self, id: CreatureId) -> Option<Creature> {
        for i in 0..self.population.len() {
            if self.population[i].id() == id {
                return Some(self.population.swap_remove(i));
            }
        }
        None
    }
}
