// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
use std::{fs::{self, File}, io::BufReader, path::Path};

use bincode::{Options, DefaultOptions};
use glium::{Surface, VertexBuffer, Display, Program, Frame, DrawParameters, Texture2d, index::{NoIndices, PrimitiveType}, uniforms::SamplerWrapFunction, texture::Texture2dArray, uniform, implement_vertex};
use glutin::surface::WindowSurface;
use glam::{UVec2, Vec2};
use png::ColorType;

use super::{Camera, star::Star, sky};

use crate::app::{config::sky::SkyConfig, filesystem::{Filesystem, FsBase}};
use crate::utils::{texture, glium_resize, blending::{ALPHA_BLENDING_FUNC, ADDITIVE_BLENDING_FUNC}};

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

#[derive(Clone, Copy)]
struct Instance { i_pos: [f32; 2], i_alpha: f32, i_layer: u32 }
implement_vertex!(Instance, i_pos, i_alpha, i_layer);

pub(super) struct Stars {
	stars: Vec<Star>,
	star_px_radii: Box<[Vec2]>,
	density: f32,
	c_size: Vec2,

	intensity: f32,
	colour_brightness: f32,
	colour_offset: f32,

	v_vbo: VertexBuffer<Vertex>,
	i_vbo: VertexBuffer<Instance>,
	textures: Texture2dArray,
	program: Program,
}

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

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

		let textures = texture::load_square_array(display, &fs.get(FsBase::Static, Path::new("textures/stars.png")), ColorType::Grayscale);
		let star_px_radii: Vec<Vec2> = DefaultOptions::new().with_varint_encoding().deserialize_from(BufReader::new(File::open(fs.get(FsBase::Static, Path::new("textures/star_radii"))).unwrap())).unwrap();
		assert_eq!(star_px_radii.len(), textures.array_size() as usize);

		Stars {
			stars: Vec::new(),
			star_px_radii: star_px_radii.into_boxed_slice(),
			density: 0.0,
			c_size: Vec2::ZERO,

			intensity: 0.0,
			colour_brightness: 0.0,
			colour_offset: 0.0,

			v_vbo: VertexBuffer::immutable(display, &VERTICES).unwrap(),
			i_vbo: VertexBuffer::empty_persistent(display, 0).unwrap(),
			textures,
			program: Program::from_source(display, &vsh, &fsh, None).unwrap(),
		}
	}

	pub fn reset(&mut self, sky: &SkyConfig, camera: &Camera) {
		self.stars.clear();
		self.density = sky.star_density;
		self.c_size = camera.get_size();
		self.intensity = sky.star_intensity;
		self.colour_brightness = sky.star_colour_brightness / sky.cloud_brightness.max(1.0 / 256.0); // Ensures the star brightness is independent of the cloud_brightness parameter
		self.colour_offset = sky.star_colour_offset;
	}

	pub fn update(&mut self, dt: f32, camera: &Camera, to_move: Vec2, winsize: Vec2) {
		let new_c_size = camera.get_size();

		let moved_vel = to_move / dt;

		for star in &mut self.stars {
			if new_c_size != self.c_size {
				star.resize(self.c_size, new_c_size);
			}
			star.move_by(to_move);
			star.update(dt);
			star.out_of_bounds(camera, moved_vel, self.star_px_radii[star.get_layer() as usize] / winsize, || Stars::get_layer(&self.textures, &self.star_px_radii, winsize));
		}

		self.c_size = new_c_size;

		/*
		 * Adds or removes stars if needed to keep the star density constant as
		 * the window area changes.
		 *
		 * Note that this must be called after moving the stars to prevent
		 * `Star::resize` being called on an already resized star, which results
		 * in the new stars being spawned in a non-uniform distribution.
		 */
		let pixels = camera.get_window_size().element_product();
		let expected_len = (self.density / 1e6 * pixels as f32).round() as usize;

		if self.stars.len() < expected_len {
			let count = expected_len - self.stars.len();
			self.stars.reserve(count);
			for _ in 0..count {
				let (layer, star_radii_hndc) = Stars::get_layer(&self.textures, &self.star_px_radii, winsize);
				self.stars.push(Star::new(camera, layer, star_radii_hndc));
			}
		} else {
			self.stars.truncate(expected_len);
		}
	}

	pub fn render(&mut self, display: &Display<WindowSurface>, frame: &mut Frame, cloud_fb: &Texture2d, params: &mut DrawParameters, winsize: Vec2, brightness_mul: f32) {
		glium_resize::vbo_persistent(&mut self.i_vbo, display, self.stars.len());

		{
			let mut buf = self.i_vbo.map_write();
			let zoom = self.c_size.min_element();
			for (i, star) in self.stars.iter().enumerate() {
				let fade_out = ((1.0 - i as f32 / self.stars.len() as f32) * 3.0).min(1.0);
				buf.set(i, Instance { i_pos: (sky::camera_to_half_ndc(star.get_pos(), self.c_size) * 2.0).into(), i_alpha: star.get_alpha(zoom) * fade_out, i_layer: star.get_layer() });
			}
		}

		let texture_size = UVec2::new(self.textures.width(), self.textures.height()).as_vec2();
		let uniforms = uniform! {
			u_size: (2.0 / winsize * texture_size).to_array(),
			u_texture: self.textures.sampled().wrap_function(SamplerWrapFunction::Clamp),
			u_cloud_fb: cloud_fb,
			u_intensity: self.intensity,
			u_colour_brightness: self.colour_brightness * brightness_mul,
			u_colour_offset: self.colour_offset,
		};
		params.blend.color = ADDITIVE_BLENDING_FUNC;
		frame.draw((&self.v_vbo, self.i_vbo.slice(..self.stars.len()).unwrap().per_instance().unwrap()), NoIndices(PrimitiveType::TriangleStrip), &self.program, &uniforms, params).unwrap();
		params.blend.color = ALPHA_BLENDING_FUNC;
	}

	fn get_layer(textures: &Texture2dArray, star_radii_px: &[Vec2], winsize: Vec2) -> (u32, Vec2) {
		let layer = rand::random::<u32>() % textures.array_size();
		(layer, star_radii_px[layer as usize] / winsize)
	}
}
