// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
mod star;
mod stars;
mod clouds;
mod sky;
mod particle;
mod death_effect;

use std::{rc::Rc, cell::RefCell, f32::consts::TAU};

use glium::{Display, Frame, DrawParameters};
use glutin::surface::WindowSurface;
use glam::{UVec2, Vec2, Mat4};

use sky::Sky;
use particle::{ParticleSystem, hit::{Hit, HitArgs}, thrust::Thrust, forcefield::ForcefieldDeflection, checkpoint::Checkpoint, powerup::{self as powerup_particle, particle_clock::ParticleClock}};
use death_effect::DeathEffect;

use super::{Camera, ResetType, world::{SmoothWorld, RenderedForcefield}};

use crate::app::{config::Config, filesystem::Filesystem, player_gfx::PlayerGfx, playing::model::changes::Hit as ChangesHit};
use crate::world::{World, powerup::Powerup, player::forcefield::Forcefield};
use crate::utils::{Clock, maths::{Circle, glam_fix::Fix}, blending::{ALPHA_BLENDING_FUNC, ADDITIVE_BLENDING_FUNC}};

pub(super) struct Decorations {
	config: Rc<RefCell<Config>>,

	sky: Sky,
	particles: ParticleSystem,
	death_effect: DeathEffect,

	powerup_particle_clock: ParticleClock,
	thrust_particle_clock: Clock,
	forcefield_particle_clock: Clock,
	checkpoint_particle_clock: Clock,
	checkpoint_particle_angle: f32,
	checkpoint_particle_hue: f32,
}

impl Decorations {
	pub fn new(config: Rc<RefCell<Config>>, display: &Display<WindowSurface>, fs: &Filesystem, player_gfx: Rc<PlayerGfx>) -> Decorations {
		Decorations {
			config,
			sky: Sky::new(display, fs),
			particles: ParticleSystem::new(display, fs),
			death_effect: DeathEffect::new(display, fs, player_gfx),
			powerup_particle_clock: ParticleClock::new(),
			thrust_particle_clock: Clock::new(1.0 / 200.0),
			forcefield_particle_clock: Clock::new(1.0 / 200.0),
			checkpoint_particle_clock: Clock::new(1.0 / 60.0),
			checkpoint_particle_angle: rand::random::<f32>() * TAU,
			checkpoint_particle_hue: rand::random::<f32>() * 6.0,
		}
	}

	pub fn reset(&mut self, display: &Display<WindowSurface>, winsize: UVec2, camera: &Camera, typ: ResetType) {
		if typ == ResetType::Full {
			self.sky.reset(display, winsize, camera, &self.config.borrow());
		}
		self.clear_particles();
	}

	pub fn resize_event(&mut self, winsize: UVec2) {
		self.sky.resize_event(winsize);
	}

	pub fn clear_particles(&mut self) {
		self.particles.reset();
		self.death_effect.reset();
		self.checkpoint_particle_angle = rand::random::<f32>() * TAU;
		self.checkpoint_particle_hue = rand::random::<f32>() * 6.0;
	}

	pub fn on_hit(&mut self, hit: &ChangesHit) {
		let particle_config = &self.config.borrow().graphics.particles;
		let args = HitArgs::new(hit);

		if particle_config.hit {
			self.particles.add(|| Hit::new(hit, &args), 8);
		}

		if particle_config.death && hit.killed {
			self.death_effect.add(hit, &mut self.particles);
		}
	}

	pub fn render_sky(&mut self, dt: f32, display: &Display<WindowSurface>, camera: &Camera, moved_by: Vec2, frame: &mut Frame, params: &mut DrawParameters) {
		self.sky.update(dt, camera, moved_by);
		self.sky.render(display, frame, params, camera);
	}

	pub fn update_particles(&mut self, dt: f32) {
		self.particles.update(dt);
	}

	pub fn add_particles(&mut self, dt: f32, time_diff: f32, world: &World, smooth_world: &SmoothWorld, forcefields: &[RenderedForcefield]) {
		let (thrust, powerup, forcefield, checkpoint) = {
			let particle_config = &self.config.borrow().graphics.particles;
			(particle_config.thrust, particle_config.powerup, particle_config.forcefield_deflection, particle_config.checkpoint)
		};

		if thrust { self.add_thrust_particles(dt, time_diff, smooth_world); }
		if powerup { self.add_powerup_particles(dt, world.get_powerups()); }
		if forcefield { self.add_forcefield_particles(dt, time_diff, smooth_world, forcefields); }
		if checkpoint { self.add_checkpoint_particles(dt, world.get_active_checkpoint_pos()); }
	}

	fn add_thrust_particles(&mut self, dt: f32, time_diff: f32, smooth_world: &SmoothWorld) {
		let to_add = self.thrust_particle_clock.advance(dt);
		if to_add == 0 { return; }

		for player in smooth_world.player_iter() {
			if player.inner().get_input_state().thrusting() {
				let pos = player.get_pos(time_diff);
				let vel = player.get_vel();
				let dir = player.get_dir();
				self.particles.add(|| Thrust::new(pos, vel, dir), to_add as usize);
			}
		}
	}

	fn add_powerup_particles(&mut self, dt: f32, powerups: &[Powerup]) {
		if self.powerup_particle_clock.advance(dt) {
			for powerup in powerups {
				let typ = powerup.get_type();
				self.particles.add(|| powerup_particle::new(typ, powerup.get_pos()), self.powerup_particle_clock.get_count(typ) as usize);
			}
		}
	}

	fn add_forcefield_particles(&mut self, dt: f32, time_diff: f32, smooth_world: &SmoothWorld, forcefields: &[RenderedForcefield]) {
		let to_add = self.forcefield_particle_clock.advance(dt);
		if to_add == 0 { return; }

		for bullet in smooth_world.get_bullets() {
			let mut bullet_dir = None;
			for ffield in forcefields.iter().filter(|ffield| bullet.should_deflect(ffield.player_id, ffield.team_id)) {
				let bullet_pos = bullet.get_smooth_pos(time_diff);
				if let Some(strength) = Forcefield::strength(bullet_pos, ffield.pos) {
					let dir = *bullet_dir.get_or_insert_with(|| bullet.get_vel().normalise_or_zero());
					self.particles.add(|| ForcefieldDeflection::new(bullet_pos, dir, strength), to_add as usize);
				}
			}
		}
	}

	fn add_checkpoint_particles(&mut self, dt: f32, pos: Option<Vec2>) {
		self.checkpoint_particle_angle = (self.checkpoint_particle_angle + dt) % TAU;
		self.checkpoint_particle_hue = (self.checkpoint_particle_hue + dt * 2.5) % 6.0;

		let to_add = self.checkpoint_particle_clock.advance(dt);
		if to_add == 0 { return; }

		if let Some(pos) = pos {
			self.particles.add(|| Checkpoint::new(pos, self.checkpoint_particle_angle, self.checkpoint_particle_hue), to_add as usize);
		}
	}

	pub fn render_particles(&mut self, dt: f32, display: &Display<WindowSurface>, frame: &mut Frame, params: &mut DrawParameters, matrix: &Mat4) {
		// Uses additive blending for the particles to avoid a white particle being rendered over by a darker particle, resulting in things appearing less bright
		params.blend.color = ADDITIVE_BLENDING_FUNC;
		self.particles.render(display, frame, params, matrix);
		params.blend.color = ALPHA_BLENDING_FUNC;

		self.death_effect.update(dt);
		self.death_effect.render(display, frame, params, matrix);
	}
}
