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

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

use super::{PlayerId, Input, BotConfig, targeting::{Moving, Target}};

use crate::world::{player::Player, effects::Effect, powerup::PowerupType, team::TeamId};
use crate::utils::maths::{Circle, RectCorners, CollidingRect};

/**
 * The planning layer, above the targeting layer and responsible for the
 * high-level decisions about how the player targets and retreats from other
 * players.
 */
#[derive(Clone)]
pub struct Planning {
	attacking: Attacking,
	init_pos: Vec2,
}

#[derive(Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
	#[serde(default)] pub aggression: Aggression,

	/**
	 * Whether a player should return to its initial position while idle.
	 */
	#[serde(default)] pub return_on_idle: bool,
	#[serde(default = "can_retreat_default")] pub can_retreat: bool,
	#[serde(default = "respond_on_hit_default")] pub respond_on_hit: bool,
	#[serde(default = "seek_items_default")] pub seek_items: bool,
}

fn can_retreat_default() -> bool { true }
fn respond_on_hit_default() -> bool { true }
fn seek_items_default() -> bool { true }

impl Default for Config {
	fn default() -> Config {
		Config {
			aggression: Aggression::default(),
			return_on_idle: false,
			can_retreat: can_retreat_default(),
			respond_on_hit: respond_on_hit_default(),
			seek_items: seek_items_default(),
		}
	}
}

#[derive(Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum Aggression {
	DistanceRange {
		/// If in the peaceful state and the distance between the bot and a player
		/// is strictly less than this, then enter the aggressing state.
		#[serde(rename = "start")]
		start_dist: f32,

		/// If in the aggressing state and the distance between the bot and a
		/// player is strictly greater than this, then enter the peaceful state.
		#[serde(rename = "continue")]
		continue_dist: f32,
	},

	// Maintains aggression forever after the condition is met
	Rect(RectCorners),
}

impl Default for Aggression {
	fn default() -> Aggression {
		Aggression::DistanceRange { start_dist: 20.0, continue_dist: 40.0 }
	}
}

impl Aggression {
	fn should_start(&self, player: &Player, other: &Player) -> bool {
		match self {
			Aggression::DistanceRange { start_dist, continue_dist: _ } => Self::distance_squared(player, other) < start_dist.powi(2),
			Aggression::Rect(rect) => other.colliding_rect(rect),
		}
	}

	fn should_continue(&self, player: &Player, other: &Player) -> bool {
		match self {
			Aggression::DistanceRange { start_dist: _, continue_dist } => Self::distance_squared(player, other) < continue_dist.powi(2),
			Aggression::Rect(_) => true,
		}
	}

	fn distance_squared(a: &Player, b: &Player) -> f32 {
		a.get_pos().distance_squared(b.get_pos())
	}
}

const RETURN_ON_IDLE_MIN_DIST: f32 = 4.0;
const RETURN_ON_IDLE_MIN_DIST_2: f32 = RETURN_ON_IDLE_MIN_DIST * RETURN_ON_IDLE_MIN_DIST;

#[derive(Clone, Copy)]
enum Attacking {
	/// The initial state for new players. Transitioned from the `Aggressing`
	/// state if the bot or targeted player dies or if the targeted player is
	/// sufficiently far (and there are no other nearby players). Transitioned
	/// from the `Responding` state if the targeted player leaves.
	///
	/// The bool field is for whether the player should seek ammo crates and
	/// powerups. This is initially false when the player spawns and becomes true
	/// when the player becomes peaceful after transitioning from another state.
	///
	/// The purpose of not seeking items when the player hasn't yet been involved
	/// with combat is to prevent every single bot player in the level from
	/// swarming an item once it has been spawned, moving away from their initial
	/// positions and potentially overwhelming the player.
	Peaceful(bool),

	/// Entered when in the peaceful state and a player is sufficiently close. If
	/// there are multiple players in range, the player is randomly chosen.
	Aggressing(PlayerId),

	/// Entered when in the peaceful state and a player's bullets hit the bot.
	/// The difference between this and the `Aggressing` state is that the bot
	/// doesn't leave this state if the player is sufficiently far away.
	Responding(PlayerId),
}

impl Default for Attacking {
	fn default() -> Attacking {
		Attacking::Peaceful(false)
	}
}

impl Attacking {
	fn get_aggressing(self) -> Option<PlayerId> {
		match self {
			Attacking::Peaceful(_) | Attacking::Responding(_) => None,
			Attacking::Aggressing(id) => Some(id),
		}
	}
}

impl Planning {
	pub fn new(init_pos: Vec2) -> Planning {
		Planning { attacking: Attacking::default(), init_pos }
	}

	pub fn update(&mut self, input: &Input, config: &BotConfig) -> (impl Iterator<Item = Option<Moving>>, Option<Target>) {
		let players = input.world.get_players();

		if input.changes.dead.contains(&input.player_id) {
			self.attacking = Attacking::default();
		} else if self.attacking.get_aggressing().is_some_and(|id| input.changes.dead.contains(&id)) {
			self.attacking = Attacking::Peaceful(true);
		}

		let attacked = match self.attacking {
			Attacking::Peaceful(_) => {
				if let Some(attackers) = input.changes.hit.get(&input.player_id) && !attackers.is_empty() /* Shouldn't happen */ && config.behaviour.planning.respond_on_hit {
					let id = attackers[rand::random::<usize>() % attackers.len()];
					if let Some(player) = players.get(&id) {
						self.attacking = Attacking::Responding(id); // Fight back against the other player
						Some((id, player))
					} else { // Shouldn't happen
						log::warn!("attacker not in world");
						None
					}
				} else if let Some((id, player)) = Self::start_aggression(input, players, &config.behaviour.planning.aggression) {
					self.attacking = Attacking::Aggressing(id); // Close player found to aggress
					Some((id, player))
				} else {
					None // No player found to attack
				}
			},
			Attacking::Aggressing(attacked_id) => {
				if let Some(player) = players.get(&attacked_id) && config.behaviour.planning.aggression.should_continue(input.player, player) {
					Some((attacked_id, player)) // Still aggressing since player exists and is still in range
				} else if let Some((id, player)) = Self::start_aggression(input, players, &config.behaviour.planning.aggression) {
					self.attacking = Attacking::Aggressing(id);
					Some((id, player)) // Otherwise finds another player at random, if any
				} else {
					self.attacking = Attacking::Peaceful(true); // No other players found
					None
				}
			},
			Attacking::Responding(attacked_id) => {
				if let Some(player) = players.get(&attacked_id) {
					Some((attacked_id, player)) // Player still exists, so still responding
				} else if let Some((id, player)) = Self::start_aggression(input, players, &config.behaviour.planning.aggression) {
					self.attacking = Attacking::Aggressing(id); // Close player found to aggress
					Some((id, player))
				} else {
					self.attacking = Attacking::Peaceful(true); // No players found, so be peaceful
					None
				}
			},
		};

		/*
		 * There are many possible movement states the bot player can be in. These
		 * are idle (not moving), chasing (moving towards players), retreating
		 * (moving away from players) and collecting (moving towards an item).
		 *
		 * Each of these possible states is assigned a value based on how
		 * *desired* the state is. These values are calculated based on
		 * information about the world.
		 */
		let mut movements = Vec::new();

		// Idle
		movements.push((f32::NEG_INFINITY, (config.behaviour.planning.return_on_idle && input.player.get_pos().distance_squared(self.init_pos) > RETURN_ON_IDLE_MIN_DIST_2).then_some(Moving::Towards(Target::new_static(self.init_pos)))));

		let shooting = if let Some((attacked_id, attacked_player)) = attacked {
			let target = Target::new_moving(attacked_player.get_pos(), attacked_player.get_vel());

			let (health, other_health) = (input.player.get_norm_health(), attacked_player.get_norm_health());
			let all_effects = input.world.get_effects();
			let (effects, other_effects) = (all_effects.get_all_info(input.player_id), all_effects.get_all_info(attacked_id));
			let damage_rate_boost = effects.map(|effects| effects.get(Effect::Damage).get_mul() * effects.get(Effect::Reload).get_mul() - 1.0).unwrap_or_default();
			let ammo_security = Self::ammo_security(f32::from(input.player.get_ammo_count()), input.player);
			let has_forcefield = effects.is_some_and(|effects| effects.get(Effect::Forcefield).active()) as u32 as f32;
			let other_has_forcefield = other_effects.is_some_and(|effects| effects.get(Effect::Forcefield).active()) as u32 as f32;

			// Chasing
			let chasing_desire = health + (1.0 - other_health) + damage_rate_boost + ammo_security * 0.5 + has_forcefield + 0.5 - other_has_forcefield * 0.5;
			movements.push((chasing_desire, Some(Moving::Towards(target))));

			// Retreating
			if config.behaviour.planning.can_retreat {
				let retreating_desire = (1.0 - health) * 1.5;
				movements.push((retreating_desire, Some(Moving::Away(target.pos))));
			}

			Some(target)
		} else { None };

		// Collecting
		if !matches!(self.attacking, Attacking::Peaceful(false)) && config.behaviour.planning.seek_items {
			for ammo_crate in input.world.get_ammo_crates() {
				let ammo_count = f32::from(input.player.get_ammo_count());
				let value = (Self::ammo_security(ammo_count + config.ammo.crate_supply as f32, input.player) - Self::ammo_security(ammo_count, input.player)) * 1.5;
				let desire = value * Self::item_desire_mul(input.player.get_pos().distance(ammo_crate.get_pos()));
				movements.push((desire, Some(Moving::Towards(Target::new_static(ammo_crate.get_pos())))));
			}

			for powerup in input.world.get_powerups() {
				let intrinsic_value = match powerup.get_type() {
					PowerupType::Speed => 1.0,
					PowerupType::Reload => 0.8,
					PowerupType::Health => 0.9,
					PowerupType::Damage => 0.85,
					PowerupType::Forcefield => 1.25,
					PowerupType::Teleportation => 0.5,
				} * 2.5;

				let desire = intrinsic_value * Self::item_desire_mul(input.player.get_pos().distance(powerup.get_pos()));
				movements.push((desire, Some(Moving::Towards(Target::new_static(powerup.get_pos())))));
			}
		}

		movements.sort_by(|a, b| a.0.total_cmp(&b.0).reverse());

		let moving = movements.into_iter().map(|(_desire, moving)| moving);
		(moving, shooting)
	}

	fn start_aggression<'a>(input: &Input, players: &'a BTreeMap<PlayerId, Player>, aggression: &Aggression) -> Option<(PlayerId, &'a Player)> {
		let team_id = players.get(&input.player_id).and_then(Player::get_team);
		let close_players = players
			.iter()
			.filter(|(other_id, other_player)| input.player_id != **other_id && TeamId::opponents(team_id, other_player.get_team()) && aggression.should_start(input.player, other_player))
			.map(|(other_id, other_player)| (*other_id, other_player))
			.collect::<Vec<_>>();

		(!close_players.is_empty()).then(|| close_players[rand::random::<usize>() % close_players.len()])
	}

	/**
	 * A multiplier to the desire of an item so that it is less desirable if you
	 * need to travel further to get it.
	 *
	 * The Euclidean distance is used rather than performing pathfinding and
	 * adding up the distance, which would be much more expensive.
	 */
	fn item_desire_mul(dist: f32) -> f32 {
		1.0 / (0.05 * dist + 1.0)
	}

	/**
	 * A measure for how much ammo the player has to perform an attack.
	 *
	 * This is a non-linear function because if the player doesn't have much
	 * ammo, then a small increase of ammo can make a huge difference in the
	 * ability to perform an attack.
	 *
	 * Compare this to if a player has plenty of ammo, in which the addition of
	 * more ammo would provide very little value.
	 *
	 * The ammo security is first calculated from the time taken to completely
	 * exhaust ammo supplies from always shooting. This doesn't depend on the
	 * reload effect because I don't want the ammo security to decrease when a
	 * reload powerup is collected.
	 */
	fn ammo_security(ammo_count: f32, player: &Player) -> f32 {
		let ammo_time = ammo_count * player.get_config().bullets.reload_interval;
		-1.0 / (0.2 * ammo_time + 0.2) + 1.0
	}
}
