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

use glium::{
	Surface, VertexBuffer, Display, Program, DrawParameters,
	index::{NoIndices, PrimitiveType}, uniforms::SamplerWrapFunction,
	texture::{Texture2dArray, buffer_texture::{BufferTexture, BufferTextureType}},
	uniform, implement_vertex
};
use glutin::surface::WindowSurface;
use glam::Vec2;
use png::ColorType;

use super::{Camera, sky};

use crate::app::filesystem::{Filesystem, FsBase};
use crate::utils::{texture::Image, 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_radii: [f32; 2], i_layer: u32, i_int: f32 } // INVARIANT: i_int ∈ [0, 1]
implement_vertex!(Instance, i_pos, i_radii, i_layer, i_int);

const SCALE: f32 = 0.125;

pub struct Clouds {
	layers: Box<[Layer]>,

	v_vbo: VertexBuffer<Vertex>,
	i_vbo: VertexBuffer<Instance>,
	textures: Texture2dArray,
	gradients: BufferTexture<(f32, f32, f32)>,
	program: Program,
}

struct Layer {
	z: f32,
	pos: Vec2, // Centre in texture coordinates
	radii: Vec2,
}

impl Layer {
	fn new(i: u32) -> Layer {
		Layer {
			z: 24.0 * 2.5f32.powi(i as i32),
			pos: Vec2::ZERO,
			radii: Vec2::ZERO,
		}
	}

	fn update(&mut self, camera_size: Vec2, to_move: Vec2) {
		let to_move_hndc = sky::camera_to_half_ndc(to_move.extend(self.z), camera_size);
		self.radii = SCALE * camera_size / camera_size.min_element();
		self.pos += to_move_hndc * self.radii;
	}
}

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

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

		let image = Image::try_new(&fs.get(FsBase::Static, Path::new("textures/clouds.png")), ColorType::GrayscaleAlpha).unwrap();
		assert_eq!(image.height % image.width, 0, "Cannot get square texture array size");
		let count = image.height / image.width;

		let average_px = image.pixels.iter().map(|&x| x as f64).sum::<f64>() / (image.pixels.len() as f64 * 255.0);

		let textures = image.into_texture_array(display, count);
		let layers = (0..count).map(Layer::new).collect::<Box<_>>();

		(Clouds {
			layers,

			v_vbo: VertexBuffer::immutable(display, &VERTICES).unwrap(),
			i_vbo: VertexBuffer::empty_persistent(display, count as usize).unwrap(),
			textures,
			gradients: BufferTexture::empty_persistent(display, 512, BufferTextureType::Float).unwrap(),
			program: Program::from_source(display, &vsh, &fsh, None).unwrap(),
		}, average_px as f32)
	}

	pub fn reset(&mut self, colours: &[(f32, f32, f32)]) {
		self.gradients.write(colours);

		for layer in &mut self.layers {
			layer.pos = rand::random::<Vec2>();
		}
	}

	/**
	 * When the framebuffer is disabled and a fixed 1x1 texture for the star
	 * colour is used, I need to ensure that this colour decreases when the
	 * clouds are darker.
	 */
	pub fn get_star_attenuation_mul(&self, zoom: f32) -> f32 {
		self.layers.iter().map(|layer| 1.0 / sky::attenuation(zoom + layer.z)).sum::<f32>()
	}

	pub fn update(&mut self, camera: &Camera, to_move: Vec2) {
		let camera_size = camera.get_size();
		for layer in &mut self.layers {
			layer.update(camera_size, to_move);
		}
	}

	pub fn render<S>(&mut self, surface: &mut S, params: &mut DrawParameters, camera: &Camera, count: usize)
	where
		 S: Surface,
	{
		{
			let zoom = camera.get_size().min_element();
			let mut buf = self.i_vbo.map_write();
			for (i, layer) in self.layers.iter().enumerate().take(count) {
				// Invariant satisfied, as camera size and layer z position shouldn't have a NaN in it
				buf.set(i, Instance { i_pos: layer.pos.to_array(), i_radii: layer.radii.to_array(), i_layer: i as u32, i_int: 1.0 / sky::attenuation(zoom + layer.z) });
			}
		}

		let uniforms = uniform! { u_texture: self.textures.sampled().wrap_function(SamplerWrapFunction::Repeat), u_gradient: &self.gradients };
		params.blend.color = ADDITIVE_BLENDING_FUNC;
		surface.draw((&self.v_vbo, self.i_vbo.slice(..self.layers.len().min(count)).unwrap().per_instance().unwrap()), NoIndices(PrimitiveType::TriangleStrip), &self.program, &uniforms, params).unwrap();
		params.blend.color = ALPHA_BLENDING_FUNC;
	}
}
