// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
#![allow(clippy::future_not_send, clippy::mem_forget, clippy::ref_option)] // Suppresses clippy warnings in code generated by ouroboros

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

use ouroboros::self_referencing;
use glam::{UVec2, Vec2, Vec3, swizzles::Vec3Swizzles};
use glium::{
	Surface, Display, VertexBuffer, Frame, Program, DrawParameters, Texture2d,
	index::{NoIndices, PrimitiveType}, texture::{RawImage2d, ClientFormat, UncompressedFloatFormat, MipmapsOption},
	framebuffer::SimpleFrameBuffer, uniform, implement_vertex
};
use glutin::surface::WindowSurface;

use super::{Camera, stars::Stars, clouds::Clouds};

use crate::app::{config::{Config, SkyPerformance, sky::GRADIENT_COUNT}, filesystem::{Filesystem, FsBase}};

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

pub struct Sky {
	stars: Stars,
	clouds: Clouds,
	winsize: UVec2,

	perf: SkyPerformance,
	cloud_average_px: f32,

	framebuffer: Framebuffer,
	cloud_fb_vbo: VertexBuffer<Vertex>,
	cloud_fb_program: Program,
}

// Used https://github.com/glium/glium/blob/master/examples/deferred.rs for help with framebuffers
#[self_referencing]
struct Framebuffer {
	texture: Texture2d,

	#[borrows(texture)]
	#[covariant]
	fbo: Option<SimpleFrameBuffer<'this>>,
}

impl Framebuffer {
	fn new_texture(display: &Display<WindowSurface>, width: u32, height: u32) -> Texture2d {
		Texture2d::empty_with_format(display, UncompressedFloatFormat::U8U8U8, MipmapsOption::NoMipmap, width, height).unwrap()
	}

	fn empty(display: &Display<WindowSurface>) -> Framebuffer {
		FramebufferBuilder {
			texture: Framebuffer::new_texture(display, 0, 0),
			fbo_builder: |_| None,
		}.build()
	}

	fn new_pixel(display: &Display<WindowSurface>, pixel: [u8; 3]) -> Framebuffer {
		FramebufferBuilder {
			texture: Texture2d::new(display, RawImage2d {
				data: Cow::Owned(pixel.to_vec()),
				width: 1, height: 1, format: ClientFormat::U8U8U8,
			}).unwrap(),
			fbo_builder: |_| None,
		}.build()
	}

	fn new_canvas(display: &Display<WindowSurface>, winsize: UVec2) -> Framebuffer {
		FramebufferBuilder {
			texture: Texture2d::empty_with_format(display, UncompressedFloatFormat::U8U8U8, MipmapsOption::NoMipmap, winsize.x, winsize.y).unwrap(),
			fbo_builder: |texture| Some(SimpleFrameBuffer::new(display, texture).unwrap()),
		}.build()
	}
}

impl Sky {
	pub fn new(display: &Display<WindowSurface>, fs: &Filesystem) -> Sky {
		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/cloud_fb.vsh"))).unwrap();
		let fsh = fs::read_to_string(fs.get(FsBase::Static, Path::new("shaders/cloud_fb.fsh"))).unwrap();

		let (clouds, cloud_average_px) = Clouds::new(display, fs);

		Sky {
			stars: Stars::new(display, fs),
			clouds,
			winsize: UVec2::ZERO,
			perf: SkyPerformance::default(),
			cloud_average_px,
			framebuffer: Framebuffer::empty(display),
			cloud_fb_vbo: VertexBuffer::immutable(display, &VERTICES).unwrap(),
			cloud_fb_program: Program::from_source(display, &vsh, &fsh, None).unwrap(),
		}
	}

	pub fn reset(&mut self, display: &Display<WindowSurface>, winsize: UVec2, camera: &Camera, config: &Config) {
		self.winsize = winsize;

		let sky = &config.graphics.get_sky_config();
		let colours = sky.get_gradient_colours();

		self.clouds.reset(&colours);
		self.stars.reset(sky, camera);
		self.perf = config.graphics.sky_perf.clone();

		if !self.perf.framebuffer {
			let average = colours.iter().map(|&(r, g, b)| Vec3::new(r, g, b)).sum::<Vec3>()
				/ colours.len() as f32
				* GRADIENT_COUNT as f32 // Because each channel is added in the shader
				* self.cloud_average_px * 2.0; // The previous calculation assumes that the average pixel is halfway, this code corrects for that

			self.framebuffer = Framebuffer::new_pixel(display, average.to_array().map(|x| (x * 255.0) as u8));
		}
	}

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

	pub fn update(&mut self, dt: f32, camera: &Camera, moved_by: Vec2) {
		self.clouds.update(camera, moved_by);
		self.stars.update(dt, camera, -moved_by, self.winsize.as_vec2());
	}

	pub fn render(&mut self, display: &Display<WindowSurface>, frame: &mut Frame, params: &mut DrawParameters, camera: &Camera) {
		let texture = self.framebuffer.borrow_texture();
		let prev_size = UVec2::new(texture.width(), texture.height());
		if self.winsize != prev_size && self.perf.framebuffer {
			self.framebuffer = Framebuffer::new_canvas(display, self.winsize);
		}

		self.framebuffer.with_mut(|fb| {
			// 1. Render clouds to framebuffer
			if let Some(fbo) = fb.fbo && self.perf.stars {
				fbo.clear_color_srgb(0.0, 0.0, 0.0, 1.0);
				self.clouds.render(fbo, params, camera, self.perf.clouds);
			}

			// 2. Render cloud framebuffer to output framebuffer
			if self.perf.clouds > 0 && self.perf.stars && fb.fbo.is_some() {
				let uniforms = uniform! { u_cloud_fb: fb.texture };
				frame.clear_stencil(0);
				frame.draw(&self.cloud_fb_vbo, NoIndices(PrimitiveType::TriangleStrip), &self.cloud_fb_program, &uniforms, params).unwrap();
			} else {
				frame.clear_color_srgb_and_stencil((0.0, 0.0, 0.0, 1.0), 0);
				if self.perf.clouds > 0 {
					self.clouds.render(frame, params, camera, self.perf.clouds);
				}
			}

			// 3. Render stars to output frame buffer, using the cloud framebuffer for colour information
			if self.perf.stars {
				let brightness_mul = if fb.fbo.is_some() {
					1.0
				} else {
					self.clouds.get_star_attenuation_mul(camera.get_size().min_element())
				};

				self.stars.render(display, frame, fb.texture, params, self.winsize.as_vec2(), brightness_mul);
			}
		});
	}
}

/**
 * Calculates the reciprocal of the attenuation multipler with the given camera
 * distance.
 *
 * Returned value is guaranteed to be ≥1 if `camera_dist` isn't NaN.
 */
pub fn attenuation(camera_dist: f32) -> f32 {
	camera_dist.powi(2) * 5e-5 + 1.0
}

pub fn camera_to_half_ndc(pos: Vec3, camera_size: Vec2) -> Vec2 {
	let zoom = camera_size.min_element();
	pos.xy() * zoom / (camera_size * (zoom + pos.z))
}

pub fn half_ndc_to_camera(half_ndc: Vec2, z: f32, camera_size: Vec2) -> Vec3 {
	let zoom = camera_size.min_element();
	(half_ndc * (camera_size * (zoom + z)) / zoom).extend(z)
}

pub fn half_ndc_to_camera_component(half_ndc: f32, z: f32, camera_size: f32, zoom: f32) -> f32 {
	half_ndc * camera_size * (zoom + z) / zoom
}
