// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
use std::collections::BTreeMap;

use serde::Deserialize;
use glam::Vec2;

use super::{Config, AmmoCountIo, super::bots::{BehaviourConfig, Aggression}};

use crate::world::{player::{config::PlayerConfig, direction::Direction, ammo_count::AmmoCount}, colour::Colour};

pub(super) type Id = Box<str>;

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Bot<C> {
	pub pos: Vec2,
	#[serde(default)] pub dir: DirectionIo,
	pub config: C,
}

#[derive(Default, Clone, Copy, Deserialize)]
#[serde(from = "f32", deny_unknown_fields /* Don't think it's needed */)]
pub struct DirectionIo(pub Direction);

impl From<f32> for DirectionIo {
	fn from(degrees: f32) -> DirectionIo {
		DirectionIo(Direction::from_degrees(degrees))
	}
}

#[derive(Clone)]
pub struct BotConfig {
	pub player: PlayerConfig,
	pub colour: Colour,
	pub ammo: AmmoConfig,
	pub behaviour: BehaviourConfig,
	pub must_kill: bool,
}

#[derive(Clone)]
pub struct AmmoConfig {
	pub init: AmmoCountIo,
	pub crate_supply: u32,
}

pub(super) type BotConfigLayers = (Box<[Id]>, BotConfigLayer);

#[derive(Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub(super) struct BotConfigLayer {
	#[serde(default)] player: PlayerConfigLayer,
	#[serde(default)] colour: Option<Colour>,
	#[serde(default)] ammo: AmmoConfigLayer,
	#[serde(default)] behaviour: BehaviourConfigLayer,
	#[serde(default)] must_kill: Option<bool>,
}

#[derive(Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct PlayerConfigLayer {
	#[serde(default)] speed: Option<f32>,
	#[serde(default)] max_health: Option<f32>,
	#[serde(default)] regen_rate: Option<f32>,
	#[serde(default)] regen_time: Option<f32>,
	#[serde(default)] bullets: BulletConfigLayer,
}

#[derive(Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct BulletConfigLayer {
	#[serde(default)] reload_interval: Option<f32>,
	#[serde(default)] damage: Option<f32>,
	#[serde(default)] speed: Option<f32>,
	#[serde(default)] spread: Option<f32>,
	#[serde(default)] lifetime: Option<f32>,
}

#[derive(Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct AmmoConfigLayer {
	#[serde(default)] init: Option<AmmoCountIo>,
	#[serde(default)] crate_supply: Option<u32>,
}

#[derive(Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct BehaviourConfigLayer {
	#[serde(default)] planning: PlanningConfigLayer,
	#[serde(default)] targeting: TargetingConfigLayer,
}

#[derive(Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct PlanningConfigLayer {
	#[serde(default)] aggression: Option<Aggression>,
	#[serde(default)] return_on_idle: Option<bool>,
	#[serde(default)] can_retreat: Option<bool>,
	#[serde(default)] respond_on_hit: Option<bool>,
	#[serde(default)] seek_items: Option<bool>,
}

#[derive(Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct TargetingConfigLayer {
	#[serde(default)] max_shoot_dist: Option<f32>,
	#[serde(default)] max_path_dist: Option<u32>,
	#[serde(default)] can_move: Option<bool>,
}

const MAX_SHOOT_DIST_LIMIT: f32 = 250.0;
const MAX_PATH_DIST_LIMIT: u32 = 1024;

impl BotConfig {
	fn validate(&self) -> Result<(), String> {
		if let Aggression::DistanceRange { start_dist, continue_dist } = &self.behaviour.planning.aggression && start_dist > continue_dist {
			return Err(format!("planning config: start_dist ({start_dist}) > continue_dist ({continue_dist})"));
		}

		if self.behaviour.targeting.max_shoot_dist > MAX_SHOOT_DIST_LIMIT {
			return Err(format!("targeting config: max_shoot_dist ({}) exceeds limit of {MAX_SHOOT_DIST_LIMIT}", self.behaviour.targeting.max_shoot_dist));
		}

		if self.behaviour.targeting.max_path_dist > MAX_PATH_DIST_LIMIT {
			return Err(format!("targeting config: max_path_dist ({}) exceeds limit of {MAX_PATH_DIST_LIMIT}", self.behaviour.targeting.max_path_dist));
		}

		Ok(())
	}
}

// There must be a better way of doing this...
impl BotConfigLayer {
	fn stack(&mut self, top: &BotConfigLayer) {
		if let Some(speed) = top.player.speed { self.player.speed = Some(speed); }
		if let Some(max_health) = top.player.max_health { self.player.max_health = Some(max_health); }
		if let Some(regen_rate) = top.player.regen_rate { self.player.regen_rate = Some(regen_rate); }
		if let Some(regen_time) = top.player.regen_time { self.player.regen_time = Some(regen_time); }
		if let Some(reload_interval) = top.player.bullets.reload_interval { self.player.bullets.reload_interval = Some(reload_interval); }
		if let Some(damage) = top.player.bullets.damage { self.player.bullets.damage = Some(damage); }
		if let Some(speed) = top.player.bullets.speed { self.player.bullets.speed = Some(speed); }
		if let Some(spread) = top.player.bullets.spread { self.player.bullets.spread = Some(spread); }
		if let Some(lifetime) = top.player.bullets.lifetime { self.player.bullets.lifetime = Some(lifetime); }
		if let Some(colour) = top.colour { self.colour = Some(colour); }
		if let Some(init) = top.ammo.init { self.ammo.init = Some(init); }
		if let Some(crate_supply) = top.ammo.crate_supply { self.ammo.crate_supply = Some(crate_supply); }
		if let Some(aggression) = top.behaviour.planning.aggression.clone() { self.behaviour.planning.aggression = Some(aggression); }
		if let Some(return_on_idle) = top.behaviour.planning.return_on_idle { self.behaviour.planning.return_on_idle = Some(return_on_idle); }
		if let Some(can_retreat) = top.behaviour.planning.can_retreat { self.behaviour.planning.can_retreat = Some(can_retreat); }
		if let Some(respond_on_hit) = top.behaviour.planning.respond_on_hit { self.behaviour.planning.respond_on_hit = Some(respond_on_hit); }
		if let Some(seek_items) = top.behaviour.planning.seek_items { self.behaviour.planning.seek_items = Some(seek_items); }
		if let Some(max_shoot_dist) = top.behaviour.targeting.max_shoot_dist { self.behaviour.targeting.max_shoot_dist = Some(max_shoot_dist); }
		if let Some(max_path_dist) = top.behaviour.targeting.max_path_dist { self.behaviour.targeting.max_path_dist = Some(max_path_dist); }
		if let Some(can_move) = top.behaviour.targeting.can_move { self.behaviour.targeting.can_move = Some(can_move); }
		if let Some(must_kill) = top.must_kill { self.must_kill = Some(must_kill); }
	}

	fn create_config(&self) -> BotConfig {
		let mut config = BotConfig {
			player: PlayerConfig::default(),
			colour: Colour::default(),
			ammo: AmmoConfig {
				init: AmmoCountIo(AmmoCount::new(100)),
				crate_supply: 50,
			},
			behaviour: BehaviourConfig::default(),
			must_kill: true,
		};
		if let Some(speed) = self.player.speed { config.player.speed = speed; }
		if let Some(max_health) = self.player.max_health { config.player.max_health = max_health; }
		if let Some(regen_rate) = self.player.regen_rate { config.player.regen_rate = regen_rate; }
		if let Some(regen_time) = self.player.regen_time { config.player.regen_time = regen_time; }
		if let Some(reload_interval) = self.player.bullets.reload_interval { config.player.bullets.reload_interval = reload_interval; }
		if let Some(damage) = self.player.bullets.damage { config.player.bullets.damage = damage; }
		if let Some(speed) = self.player.bullets.speed { config.player.bullets.speed = speed; }
		if let Some(spread) = self.player.bullets.spread { config.player.bullets.spread = spread; }
		if let Some(lifetime) = self.player.bullets.lifetime { config.player.bullets.lifetime = lifetime; }
		if let Some(colour) = self.colour { config.colour = colour; }
		if let Some(init) = self.ammo.init { config.ammo.init = init; }
		if let Some(crate_supply) = self.ammo.crate_supply { config.ammo.crate_supply = crate_supply; }
		if let Some(aggression) = self.behaviour.planning.aggression.clone() { config.behaviour.planning.aggression = aggression; }
		if let Some(return_on_idle) = self.behaviour.planning.return_on_idle { config.behaviour.planning.return_on_idle = return_on_idle; }
		if let Some(can_retreat) = self.behaviour.planning.can_retreat { config.behaviour.planning.can_retreat = can_retreat; }
		if let Some(respond_on_hit) = self.behaviour.planning.respond_on_hit { config.behaviour.planning.respond_on_hit = respond_on_hit; }
		if let Some(seek_items) = self.behaviour.planning.seek_items { config.behaviour.planning.seek_items = seek_items; }
		if let Some(max_shoot_dist) = self.behaviour.targeting.max_shoot_dist { config.behaviour.targeting.max_shoot_dist = max_shoot_dist; }
		if let Some(max_path_dist) = self.behaviour.targeting.max_path_dist { config.behaviour.targeting.max_path_dist = max_path_dist; }
		if let Some(can_move) = self.behaviour.targeting.can_move { config.behaviour.targeting.can_move = can_move; }
		if let Some(must_kill) = self.must_kill { config.must_kill = must_kill; }
		config
	}
}

const MAX_RECURSION_DEPTH: usize = 16;

impl Config {
	pub fn resolve_bots(&self) -> Result<impl Iterator<Item = Bot<BotConfig>>, String> {
		let (mut resolved, mut bots) = (BTreeMap::new(), Vec::with_capacity(self.bots.len()));
		for bot in &self.bots {
			self.layer(&mut resolved, bot.config.as_ref(), 0, &mut |layer| bots.push(Bot { pos: bot.pos, dir: bot.dir, config: layer.create_config()}))?;
		}

		let unused_ids: Vec<&str> = self.bot_configs.keys().map(Box::as_ref).filter(|id| !resolved.contains_key(*id)).collect();
		if !unused_ids.is_empty() {
			log::warn!("unused bot config ids exist: {unused_ids:?}");
		}

		for bot in &bots {
			bot.config.validate().map_err(|err| format!("invalid bot config: {err}"))?;
		}

		Ok(bots.into_iter())
	}

	// Would like to return `Result<&BotConfigLayer, String>` but that has borrow checker problems, so instead taking the `on_layer` parameter
	// Also encountering recursion problems when using static dispatch
	fn layer(&self, resolved: &mut BTreeMap<Id, BotConfigLayer>, id: &str, depth: usize, on_layer: &mut dyn FnMut(&BotConfigLayer)) -> Result<(), String> {
		if depth > MAX_RECURSION_DEPTH {
			return Err(format!("recursion depth exceeded maximum of {MAX_RECURSION_DEPTH}, is there a cycle?"));
		}

		if let Some(layer) = resolved.get(id) {
			on_layer(layer);
			return Ok(());
		}

		let (layer_ids, final_layer) = self.bot_configs.get(id).ok_or_else(|| format!("bot with id \"{id}\" not found"))?;
		let mut layer = BotConfigLayer::default();
		for id in layer_ids {
			self.layer(resolved, id, depth + 1, &mut |top| layer.stack(top))?;
		}
		layer.stack(final_layer);
		on_layer(&layer);
		resolved.insert(Box::from(id), layer);
		Ok(())
	}
}
