// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
mod pathfinding;
mod aiming;
mod input;
mod targeting;
mod planning;

pub use planning::Aggression;

use std::collections::BTreeMap;

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

use pathfinding::Pathfinding;
use input::{Input, Config};
use targeting::{Targeting, Config as TargetingConfig};
use planning::{Planning, Config as PlanningConfig};

use super::{config::BotConfig, super::common::changes::ServerChanges};

use crate::world::{World, DELTA_TIME, player::{PlayerId, update::PlayerUpdate, direction::Direction}};

pub struct Bots {
	init_players: BTreeMap<PlayerId, BotPlayer>,
	players: BTreeMap<PlayerId, BotPlayer>,
	pathfinding: Pathfinding,
	config: Config,
}

#[derive(Clone)]
pub struct BotPlayer {
	planning: Planning,
	targeting: Targeting,
	config: BotConfig,
}

#[derive(Default, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BehaviourConfig {
	pub planning: PlanningConfig,
	pub targeting: TargetingConfig,
}

impl BotPlayer {
	pub fn new(init_pos: Vec2, dir: Direction, config: BotConfig) -> BotPlayer {
		BotPlayer {
			planning: Planning::new(init_pos),
			targeting: Targeting::new(dir),
			config,
		}
	}
}

impl Bots {
	pub fn new(players: BTreeMap<PlayerId, BotPlayer>, world: &World) -> Bots {
		let block_size = world.get_blocks().get_block_size().get() as f32;
		Bots {
			init_players: players.clone(),
			players,
			pathfinding: Pathfinding::new(world),
			config: Config {
				block_size,
				block_size_squared: block_size.powi(2),
			},
		}
	}

	pub fn reset(&mut self) {
		self.players = self.init_players.clone();
	}

	pub fn get_ammo_crate_supply(&self, id: PlayerId) -> Option<u32> {
		self.players.get(&id).map(|bot| bot.config.ammo.crate_supply)
	}

	pub fn remove_player(&mut self, id: PlayerId) {
		self.players.remove(&id);
	}

	pub fn must_kill_remaining(&self) -> usize {
		self.players.values().filter(|bot| bot.config.must_kill).count()
	}

	pub fn get_player_updates(&mut self, world: &World, changes: &ServerChanges, mut on_update: impl FnMut(PlayerId, PlayerUpdate)) {
		for (&id, player) in world.get_players() {
			if player.is_human() { continue; }
			let Some(bot) = self.players.get_mut(&id) else { continue; };

			let input = Input {
				world,
				changes,
				dt: DELTA_TIME,
				player,
				player_id: id,
				pathfinding: &self.pathfinding,
				config: &self.config,
			};

			let (moving, shooting) = bot.planning.update(&input, &bot.config);
			on_update(id, bot.targeting.update(&input, &bot.config, moving, shooting));
		}
	}
}
