// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
pub mod temperature;
pub mod thrust;
pub mod hit;
pub mod explosion;
pub mod powerup;
pub mod forcefield;
pub mod checkpoint;

use std::{fs, path::Path};

use glium::{Surface, VertexBuffer, Display, Program, Frame, DrawParameters, index::{NoIndices, PrimitiveType}, uniform, implement_vertex};
use glutin::surface::WindowSurface;
use glam::{Vec2, Vec4, Mat4};
use enum_dispatch::enum_dispatch;

use thrust::Thrust;
use hit::Hit;
use explosion::Explosion;
use forcefield::ForcefieldDeflection;
use checkpoint::Checkpoint;
use powerup::{speed::Speed, reload::Reload, health::Health, damage::Damage, forcefield::Forcefield, teleportation::Teleportation};

use crate::app::filesystem::{Filesystem, FsBase};
use crate::utils::{glium_resize, SwapRemoveIterator};

#[enum_dispatch]
pub trait Particle {
	fn get_pos(&self) -> Vec2;
	fn get_size(&self) -> f32;

	/*
	 * Note that alpha is redundant right now as all particles are rendered using
	 * additive rendering. Too lazy to change things and in the future I might
	 * change the blending.
	 */
	fn get_colour(&self) -> Vec4;

	// Returns false if the particle should be removed
	fn update(&mut self, dt: f32) -> bool;
}

#[enum_dispatch(Particle)]
pub enum AnyParticle {
	Thrust,
	Hit,
	Explosion,
	ForcefieldDeflection,
	Checkpoint,

	// Powerup particles
	Speed,
	Reload,
	Health,
	Damage,
	Forcefield,
	Teleportation,
}

#[derive(Clone, Copy)]
struct Vertex { v_pos: [f32; 2] }
implement_vertex!(Vertex, v_pos);

#[derive(Clone, Copy)]
struct Instance { i_pos: [f32; 2], i_size: f32, i_colour: [f32; 4] }
implement_vertex!(Instance, i_pos, i_size, i_colour);

pub struct ParticleSystem {
	particles: Vec<AnyParticle>,
	v_vbo: VertexBuffer<Vertex>,
	i_vbo: VertexBuffer<Instance>,
	program: Program,
}

impl ParticleSystem {
	pub fn new(display: &Display<WindowSurface>, fs: &Filesystem) -> ParticleSystem {
		static VERTICES: [Vertex; 4] = [
			Vertex { v_pos: [-0.5, -0.5] },
			Vertex { v_pos: [0.5, -0.5] },
			Vertex { v_pos: [-0.5, 0.5] },
			Vertex { v_pos: [0.5, 0.5] },
		];

		let vsh = fs::read_to_string(fs.get(FsBase::Static, Path::new("shaders/particle.vsh"))).unwrap();
		let fsh = fs::read_to_string(fs.get(FsBase::Static, Path::new("shaders/particle.fsh"))).unwrap();

		ParticleSystem {
			particles: Vec::new(),
			v_vbo: VertexBuffer::immutable(display, &VERTICES).unwrap(),
			i_vbo: VertexBuffer::empty_persistent(display, 0).unwrap(),
			program: Program::from_source(display, &vsh, &fsh, None).unwrap(),
		}
	}

	pub fn reset(&mut self) {
		self.particles.clear();
	}

	pub fn add<P, F>(&mut self, mut f: F, count: usize)
	where
		P: Particle + Into<AnyParticle>,
		F: FnMut() -> P,
	{
		self.particles.extend((0..count).map(|_| f().into()));
	}

	pub fn update(&mut self, dt: f32) {
		// Additive blending is used so changing the order of particles won't produce any visual problems
		let mut iter = SwapRemoveIterator::new(&mut self.particles);
		while let Some(pt) = iter.next() {
			if !pt.update(dt) {
				iter.remove();
			}
		}
	}

	pub fn render(&mut self, display: &Display<WindowSurface>, frame: &mut Frame, params: &DrawParameters, proj_matrix: &Mat4) {
		glium_resize::vbo_persistent(&mut self.i_vbo, display, self.particles.len());

		{
			let mut buf = self.i_vbo.map_write();
			for (i, pt) in self.particles.iter().enumerate() {
				buf.set(i, Instance { i_pos: pt.get_pos().into(), i_size: pt.get_size(), i_colour: pt.get_colour().into() });
			}
		}

		let uniforms = uniform! { u_matrix: proj_matrix.to_cols_array_2d() };
		frame.draw((&self.v_vbo, self.i_vbo.slice(..self.particles.len()).unwrap().per_instance().unwrap()), NoIndices(PrimitiveType::TriangleStrip), &self.program, &uniforms, params).unwrap();
	}
}
