// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
use std::ops::{Add, Sub, AddAssign};

use glam::Vec2;

use super::forcefield::LocalForcefield;

use crate::world::{World, DELTA_TIME, player::{Player, PlayerId, direction::{Direction, Angle}}, bullet::Bullet, effects::{Effect, Effects}};
use crate::app::playing::{view::{HealthBar, LoopingSounds, LoopingSfx}, controller::PlayerController};
use crate::utils::maths::{Circle, decay::Decay};

pub struct LocalPlayer {
	pub player: Player,
	pub last_hurt: f32,
	pub hbar: HealthBar,
	pub ffield: Option<LocalForcefield>,
	pub(super) delta: DPlayer,
	block_collision_time: f32,
	thrust_amplitude: f32,
	block_collision_amplitude: f32,
}

const BLOCK_COLLISION_SOUND_TIME: f32 = 0.125;

const FADE_IN_TIME: f32 = 0.03125;
const THRUST_FADE_OUT_TIME: f32 = 0.375;
const BLOCK_COLLISION_FADE_OUT_TIME: f32 = 0.5;

impl LocalPlayer {
	pub(super) fn new(id: PlayerId, player: Player, effects: &Effects) -> LocalPlayer {
		let hbar = HealthBar::new(player.get_norm_health());
		LocalPlayer {
			player,
			last_hurt: f32::INFINITY,
			hbar,
			ffield: effects.active(id, Effect::Forcefield).then(LocalForcefield::new_existing),
			delta: DPlayer::zero(),
			block_collision_time: f32::INFINITY,
			thrust_amplitude: 0.0,
			block_collision_amplitude: 0.0,
		}
	}

	pub fn inner(&self) -> &Player { &self.player }

	pub fn get_pos(&self, time_diff: f32) -> Vec2 {
		let pos = self.player.get_pos() + self.delta.get_pos();
		let vel = self.get_vel();
		pos + vel * time_diff
	}

	pub fn get_vel(&self) -> Vec2 { self.player.get_vel() + self.delta.get_vel() }
	pub fn get_dir(&self) -> Direction { self.player.get_dir() + self.delta.get_angle() }
	pub fn get_ammo_string(&self) -> String { self.player.get_ammo_count().to_string() }
	pub fn control(&mut self, pcontrol: &PlayerController) { self.player.set_update(pcontrol.get_update()); }

	pub(super) fn update(&mut self, world: &World, id: PlayerId) -> Option<Bullet> {
		let effects = world.get_effects();

		let mut shot = None;

		if self.player.update(DELTA_TIME, world.get_size(), world.get_blocks(), effects, id) {
			self.block_collision_time = 0.0;
		}
		self.player.decrease_cooldown(DELTA_TIME * effects.get_mul(id, Effect::Reload));
		if self.player.shoot() {
			shot = Some(Bullet::new(&self.player, id, effects.get_mul(id, Effect::Damage), 0.0));
		}
		if let Some(time) = effects.get_time(id, Effect::Forcefield) {
			let ffield = self.ffield.get_or_insert_with(LocalForcefield::new);
			ffield.update(time, DELTA_TIME);
		} else {
			self.ffield = None;
		}

		shot
	}

	pub fn play_looping_sounds(&mut self, dt: f32, time_diff: f32, sounds: &mut LoopingSounds) {
		let (pos, vel) = (self.get_pos(time_diff), self.get_vel());

		if self.player.get_input_state().thrusting() {
			self.thrust_amplitude = (self.thrust_amplitude + dt / FADE_IN_TIME).min(1.0);
		} else {
			self.thrust_amplitude = (self.thrust_amplitude - dt / THRUST_FADE_OUT_TIME).max(0.0);
		}

		if self.block_collision_time < BLOCK_COLLISION_SOUND_TIME {
			self.block_collision_amplitude = (self.block_collision_amplitude + dt / FADE_IN_TIME).min(1.0);
		} else {
			self.block_collision_amplitude = (self.block_collision_amplitude - dt / BLOCK_COLLISION_FADE_OUT_TIME).max(0.0);
		}

		sounds.play(LoopingSfx::Thrust, pos, vel, self.thrust_amplitude);
		sounds.play(LoopingSfx::BlockPlayer, pos, vel, self.block_collision_amplitude);

		self.block_collision_time += dt;
	}
}

#[derive(Default)]
pub struct DPlayer {
	pos: Vec2,
	vel: Vec2,
	angle: Angle,
}

impl DPlayer {
	pub fn zero() -> DPlayer { DPlayer::default() }

	pub fn get_pos(&self) -> Vec2 { self.pos }
	pub fn get_vel(&self) -> Vec2 { self.vel }
	pub fn get_angle(&self) -> Angle { self.angle }

	pub fn decay(&mut self, rate: f32) {
		/*
		 * Decays the direction faster as a jump in the position can feel very
		 * weird, but a jump in the direction can be normal gameplay.
		 */
		self.pos.decay(rate, DELTA_TIME);
		self.vel.decay(rate, DELTA_TIME);
		self.angle.decay(rate * 8.0, DELTA_TIME);
	}

	pub fn reset_angle(&mut self) {
		self.angle = Angle::default();
	}
}

impl Sub for &Player {
	type Output = DPlayer;

	fn sub(self, other: &Player) -> DPlayer {
		DPlayer {
			pos: self.get_pos() - other.get_pos(),
			vel: self.get_vel() - other.get_vel(),
			angle: self.get_dir() - other.get_dir(),
		}
	}
}

impl Add<&DPlayer> for &Player {
	type Output = Player;

	fn add(self, delta: &DPlayer) -> Player {
		let mut player = self.clone();
		player.set_pos(player.get_pos() + delta.pos);
		player.set_vel(player.get_vel() + delta.vel);
		player.set_dir(player.get_dir() + delta.angle);
		player
	}
}

impl AddAssign<&DPlayer> for DPlayer {
	fn add_assign(&mut self, rhs: &DPlayer) {
		self.pos += rhs.pos;
		self.vel += rhs.vel;
		self.angle += rhs.angle;
	}
}
