// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
mod id;
pub mod update;
pub mod input_state;
pub mod ammo_count;
pub mod forcefield;
mod collision;
pub mod direction;
pub mod config;
mod style;

pub use id::PlayerId;
pub use style::{PlayerStyle, PlayerName, PlayerTexture, HumanStyle};
#[cfg(feature = "client")] pub use style::{PlayerNameRef, PlayerNameMut};

use glam::Vec2;
use serde::{Serialize, Deserialize};

use input_state::InputState;
use ammo_count::AmmoCount;
use update::PlayerUpdate;
use collision::RESTITUTION_K;
use direction::Direction;
use config::PlayerConfig;

use super::{blocks::Blocks, bullet::Bullet, effects::{Effect, Effects}, team::TeamId};
#[cfg(feature = "client")] use super::colour::Colour;

use crate::utils::maths::Circle;

// The hitbox of the spaceship is a circle to make things easy
// Annoyingly, square root isn't a constant function so I need to hard-code this value in
pub const RADIUS: f32 = 0.44603103; // (0.625 / PI).sqrt(), using 0.625 from scripts/area.py which calculates the area of the model
#[cfg(feature = "client")] pub const RADIUS_SQUARED: f32 = RADIUS * RADIUS;
#[cfg(feature = "client")] pub const MIN_DIST_TO_EDGE: f32 = 0.25;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Player {
	pos: Vec2, vel: Vec2,
	dir: Direction,
	health: f32,
	regen_time: f32,
	cooldown: f32,
	ammo: AmmoCount,
	input_state: InputState,
	style: PlayerStyle,
	team: Option<TeamId>,
	config: PlayerConfig,
}

impl Player {
	pub fn new(config: PlayerConfig, style: PlayerStyle, team: Option<TeamId>) -> Player {
		Player {
			pos: Vec2::ZERO, vel: Vec2::ZERO, dir: Direction::default(),
			health: config.max_health, regen_time: config.regen_time, cooldown: 0.0, ammo: AmmoCount::default(), input_state: InputState::default(),
			style, team, config,
		}
	}

	pub fn get_vel(&self) -> Vec2 { self.vel }
	#[cfg(feature = "client")] pub fn get_norm_health(&self) -> f32 { (self.health / self.config.max_health).clamp(0.0, 2.0) }
	pub fn get_dir(&self) -> Direction { self.dir }
	pub fn get_input_state(&self) -> InputState { self.input_state }
	pub fn get_ammo_count(&self) -> AmmoCount { self.ammo }
	#[cfg(feature = "client")] pub fn get_style(&self) -> PlayerStyle { self.style.clone() }
	pub fn get_style_mut(&mut self) -> &mut PlayerStyle { &mut self.style }
	#[cfg(feature = "client")] pub fn get_colour(&self) -> Colour { self.style.get_colour() }
	#[cfg(feature = "client")] pub fn get_texture(&self) -> PlayerTexture { self.style.get_texture() }
	pub fn get_team(&self) -> Option<TeamId> { self.team }
	pub fn get_config(&self) -> &PlayerConfig { &self.config }

	pub fn is_human(&self) -> bool {
		match self.style.get_texture() {
			PlayerTexture::Human => true,
			PlayerTexture::Bot => false,
		}
	}

	#[cfg(feature = "client")]
	pub fn is_bot(&self) -> bool {
		!self.is_human()
	}

	pub fn set_pos(&mut self, pos: Vec2) { self.pos = pos; }
	pub fn set_vel(&mut self, vel: Vec2) { self.vel = vel; }
	pub fn set_dir(&mut self, dir: Direction) { self.dir = dir; }
	pub fn set_health(&mut self, health: f32) { self.health = health; }
	pub fn set_ammo_count(&mut self, ammo: AmmoCount) { self.ammo = ammo; }
	pub fn set_team(&mut self, team: Option<TeamId>) { self.team= team; }

	pub fn set_update(&mut self, update: PlayerUpdate) {
		self.dir = update.dir;
		self.input_state = update.input_state;
	}

	pub fn add_health(&self, amount: f32) -> f32 {
		(self.health + amount).min(self.config.max_health * 2.0)
	}

	pub fn on_bullet_hit(&mut self, bullet: &Bullet) {
		self.health -= bullet.get_damage();
	}

	pub fn is_dead(&self) -> bool { self.health <= 0.0 }

	pub fn reset(&mut self) {
		self.health = self.config.max_health;
		self.regen_time = self.config.regen_time;
		self.cooldown = 0.0;
	}

	pub fn update(&mut self, dt: f32, world_size: Vec2, blocks: &Blocks, effects: &Effects, id: PlayerId) -> bool {
		let speed_mul = effects.get_mul(id, Effect::Speed);
		let prev_pos = self.pos;

		let acc = self.get_acc(dt, speed_mul);
		self.vel += acc * dt;
		self.pos += self.vel * dt;

		let regen_rate = self.config.regen_rate * effects.get_mul(id, Effect::Regeneration);
		let max_health = self.health.max(self.config.max_health);
		let regen_time = self.regen_time.clamp(0.0, dt);
		let prev_health = self.health;
		self.health = (self.health + regen_rate * regen_time).min(max_health);
		let dhealth = self.health - prev_health;
		if regen_rate != 0.0 {
			self.regen_time -= dhealth / regen_rate;
		}

		/*
		 * Prevents the player from leaving the world. If the player does leave,
		 * then put them back and negate one component of their velocity.
		 */
		for i in 0..2 {
			if self.pos[i] < -world_size[i] / 2.0 + RADIUS { self.pos[i] = -world_size[i] / 2.0 + RADIUS; self.vel[i] *= -RESTITUTION_K; }
			else if self.pos[i] > world_size[i] / 2.0 - RADIUS { self.pos[i] = world_size[i] / 2.0 - RADIUS; self.vel[i] *= -RESTITUTION_K; }
		}

		self.collide_blocks(world_size, blocks, prev_pos, dt)
	}

	fn get_acc(&self, dt: f32, speed_mul: f32) -> Vec2 {
		const MASS: f32 = 1.0;
		const DRAG_A: f32 = 0.5;
		const DRAG_B: f32 = 1.0;

		let move_dir = self.input_state.get_move_dir();

		let max_speed = self.config.speed * speed_mul;
		let thrust_size = DRAG_A * max_speed * (max_speed + DRAG_B); // The thrust size required to exactly oppose the drag at the max speed
		let mut force = move_dir * thrust_size;

		/*
		 * The vel * speed factor is equivalent to dir * speed² (where dir is the
		 * direction of the velocity), which is based on the air resistance
		 * equation. The vel factor without the speed has the force proportional
		 * to the speed directly, which makes the spaceship slow down faster when
		 * at slower speeds.
		 */
		let speed = self.vel.length();
		let part_drag_size = DRAG_A * (speed + DRAG_B);
		let mut drag = -self.vel * part_drag_size;
		let drag_size = speed * part_drag_size;

		/*
		 * If the drag magnitude exceeds this limit, then the "instantaneous"
		 * change in velocity would result in the velocity being negated, which
		 * if the drag coefficient or delta time is sufficiently large could
		 * result in the new velocity being larger than it initially was, just on
		 * the opposite sign. This can easily spiral into being infinity and then
		 * NaN.
		 */
		let drag_limit = speed * MASS / dt.abs(); // Delta time can be negative if reversing physics
		if drag_size > drag_limit {
			drag *= drag_limit / drag_size;
		}

		force += drag;

		force / MASS
	}

	pub fn decrease_cooldown(&mut self, dt: f32) {
		self.cooldown -= dt;
	}

	// Shoots at most one bullet
	pub fn shoot(&mut self) -> bool {
		if self.cooldown <= 0.0 {
			if self.input_state.shooting() && self.ammo.is_positive() {
				self.cooldown += self.config.bullets.reload_interval;
				self.ammo.decrease();
				true
			} else {
				self.cooldown = 0.0;
				false
			}
		} else { false }
	}
}

impl Circle for Player {
	fn get_pos(&self) -> Vec2 { self.pos }
	fn get_radius(&self) -> f32 { RADIUS }
}
