// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
use glam::Vec2;
use serde::Deserialize;

use super::{PlayerUpdate, Input, BotConfig, aiming::{self, Aim}, pathfinding::DEFAULT_MAX_DIST};

use crate::world::{player::{RADIUS_SQUARED as PLAYER_RADIUS_SQUARED, direction::Direction, input_state::{InputState, InputMovement}}};
use crate::utils::maths::{Circle, decay::Decay};

/**
 * The targeting layer, a layer of abstraction above directly setting the
 * movement state, player direction and whether the player is shooting. This
 * layer takes information about where in the world to move to and what to shoot
 * at, and handles path following, aiming and shooting.
 */
#[derive(Clone)]
pub struct Targeting {
	path: Option<Path>,
	next_path: f32,
	dir: Direction,
	rendered_dir: Direction,
}

#[derive(Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
	#[serde(default = "max_shoot_dist_default")] pub max_shoot_dist: f32,
	#[serde(default = "max_path_dist_default")] pub max_path_dist: u32,
	#[serde(default = "can_move_default")] pub can_move: bool,
}

impl Default for Config {
	fn default() -> Config {
		Config {
			max_shoot_dist: max_shoot_dist_default(),
			max_path_dist: max_path_dist_default(),
			can_move: can_move_default(),
		}
	}
}

fn max_shoot_dist_default() -> f32 { 40.0 }
fn max_path_dist_default() -> u32 { DEFAULT_MAX_DIST }
fn can_move_default() -> bool { true }

#[derive(Clone)]
pub enum Moving {
	Towards(Target),
	Away(Vec2),
}

#[derive(Clone, Copy)]
pub struct Target {
	pub pos: Vec2,
	pub vel: Vec2,
}

impl Target {
	pub fn new_static(pos: Vec2) -> Target {
		Target { pos, vel: Vec2::ZERO }
	}

	pub fn new_moving(pos: Vec2, vel: Vec2) -> Target {
		Target { pos, vel }
	}
}

#[derive(Clone)]
struct Path {
	points: Vec<Vec2>,
	target: Option<Target>,
	index: usize,
	retreating: bool,
}

impl Targeting {
	pub fn new(dir: Direction) -> Targeting {
		Targeting { path: None, next_path: 0.0, dir, rendered_dir: dir }
	}

	pub fn update(&mut self, input: &Input, config: &BotConfig, mut moving: impl Iterator<Item = Option<Moving>>, shooting: Option<Target>) -> PlayerUpdate {
		self.next_path -= input.dt;
		let player_pos = input.player.get_pos();

		if input.changes.dead.contains(&input.player_id) {
			self.path = None; // Don't want to follow the previously defined path that's no longer useful
		}

		if self.next_path <= 0.0 && config.behaviour.targeting.can_move {
			loop {
				if let Some(moving) = moving.next().flatten() {
					let (path, target, retreating) = match moving {
						Moving::Towards(target) => (input.pathfinding.find_path_to_goal(player_pos, target.pos, input.world.get_blocks(), config.behaviour.targeting.max_path_dist), Some(target), false),
						Moving::Away(avoid_pos) => (input.pathfinding.find_path_avoiding(player_pos, avoid_pos, input.world.get_blocks()), None, true),
					};

					if let Some(points) = path {
						self.path = Some(Path { points, target, index: 0, retreating });
						break;
					}
				} else {
					self.path = None;
					break;
				}
			}

			/*
			 * Only updates paths occasionally because pathfinding is (relatively)
			 * expensive to perform. I could set `next_path` to a constant amount,
			 * but if there are a lot of bot players, the updating of their paths
			 * might be synchronised and all paths would update in a single update,
			 * which is more likely to produce a lag spike.
			 *
			 * Introducing randomness more evenly distributes this computation,
			 * reducing the chance of lag spikes.
			 */
			self.next_path = rand::random::<f32>() * 0.09375 + 0.15625;
		}

		let mut movement = InputMovement::default();

		if let Some(path) = &mut self.path {
			if let Some(target) = &mut path.target {
				target.pos += target.vel * input.dt;
			}

			let follow_pos = loop {
				if let Some(&point) = path.points.get(path.index) {
					if player_pos.distance_squared(point) < input.config.block_size_squared {
						path.index += 1;
					} else {
						break Some(point);
					}
				} else if let Some(target) = path.target {
					break if player_pos.distance_squared(target.pos) < PLAYER_RADIUS_SQUARED {
						None
					} else {
						Some(target.pos)
					};
				} else {
					break None;
				}
			};

			if let Some(pos) = follow_pos {
				let mut dir = Direction::from(pos - player_pos);
				movement = dir.into();

				/*
				 * When the bot is attacking another player, it switches between
				 * looking in the direction of the path and looking at the player to
				 * aim. If there's a block in the way and the players are quickly
				 * moving, then this switching can happen rapidly.
				 *
				 * This isn't any problem when the bot is chasing the player, but
				 * when the bot is retreating from the player the path direction is
				 * likely very different from the aim direction. This rapid
				 * switching between very different directions can look weird. To
				 * avoid this, the path direction is negated if retreating so that
				 * the oscillations are smaller.
				 */
				if path.retreating {
					dir = -dir;
				}

				self.dir = dir;
			} else {
				self.path = None;
			}
		}

		let shooting = if
			let Some(target) = shooting &&
			let aim = aiming::aim(target.pos - player_pos, target.vel - input.player.get_vel(), input.player.get_config().bullets.speed) &&
			aim != Aim::Unreachable &&
			let dist = player_pos.distance(target.pos) && dist < config.behaviour.targeting.max_shoot_dist
		{
			let dir = match aim {
				Aim::Direction(dir) => dir,
				Aim::Anywhere | Aim::Unreachable /* Impossible */ => self.dir,
			};

			let step_size = input.config.block_size / 2.0;
			let step = Vec2::from(dir) * step_size;
			let step_count = (dist / step_size).floor() as usize;

			let mut bullet_pos = player_pos;
			let mut colliding = false;
			for _ in 0..step_count {
				bullet_pos += step;
				let blocks = input.world.get_blocks();
				if blocks.get(blocks.world_to_block_pos(bullet_pos)).unwrap_or_default() {
					colliding = true;
					break;
				}
			}

			if !colliding {
				self.dir = dir;
				true
			} else { false }
		} else { false };

		// Smoothly changes the player's direction
		// Actually has a huge impact on the difficulty because the intended direction is slightly behind
		// Might try to correct that to make smoothness and difficulty independent
		let mut angle = self.rendered_dir - self.dir;
		angle.decay(32.0, input.dt);
		self.rendered_dir = self.dir + angle;

		PlayerUpdate {
			input_state: InputState::new(movement, shooting, true /* Not interested in picking up any flags */),
			dir: self.rendered_dir,
		}
	}
}
