// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
use glam::{Vec2, Vec3};

use super::{Camera, sky};

const FADE_IN_TIME: f32 = 4.0;
const Z_MAX: f32 = 400.0;
const Z_FADE_DIST: f32 = 4.0;

pub(super) struct Star {
	pos: Vec3,
	vel: Vec3,
	int: f32,
	time: f32,
	layer: u32,
}

impl Star {
	pub fn new(camera: &Camera, layer: u32, star_radii_hndc: Vec2) -> Star {
		Star::reset(camera, f32::INFINITY, None, layer, star_radii_hndc)
	}

	fn reset(camera: &Camera, init_time: f32, z: Option<f32>, layer: u32, star_radii_hndc: Vec2) -> Star {
		let pos = (rand::random::<Vec2>() - 0.5) * (Vec2::ONE + star_radii_hndc * 2.0);
		let z = z.unwrap_or_else(|| rand::random::<f32>() * Z_MAX);

		Star {
			pos: sky::half_ndc_to_camera(pos, z, camera.get_size()),
			vel: (rand::random::<Vec3>() - 0.5) * 0.75,
			int: rand::random::<f32>() * 0.375 + 0.625,
			time: init_time,
			layer,
		}
	}

	pub fn get_pos(&self) -> Vec3 {
		self.pos
	}

	pub fn get_alpha(&self, zoom: f32) -> f32 {
		let camera_dist = self.pos.z + zoom;

		let mut alpha = self.int // Intrinsic intensity of a star
		/ sky::attenuation(camera_dist) // Attenuation due to distance
		* (self.time / FADE_IN_TIME).min(1.0); // Fade in for new star

		// Fades out a star when its z coordinate is about to move out of bounds, to avoid it suddenly disappearing
		if self.pos.z < Z_FADE_DIST {
			alpha *= self.pos.z / Z_FADE_DIST;
		} else if self.pos.z > Z_MAX - Z_FADE_DIST {
			alpha *= (Z_MAX - self.pos.z) / Z_FADE_DIST;
		}

		alpha
	}

	pub fn get_layer(&self) -> u32 {
		self.layer
	}

	pub fn resize(&mut self, old_size: Vec2, new_size: Vec2) {
		let old_pos = sky::camera_to_half_ndc(self.pos, old_size);
		self.pos = sky::half_ndc_to_camera(old_pos, self.pos.z, new_size);
	}

	pub fn move_by(&mut self, off: Vec2) {
		self.pos.x += off.x;
		self.pos.y += off.y;
	}

	pub fn update(&mut self, dt: f32) {
		self.pos += self.vel * dt;
		self.time += dt;
	}

	/**
	 * Checks if the position is out of bounds, in which it is wrapped around or
	 * reset.
	 *
	 * If the z coordinate is out of bounds, the star is reset.
	 *
	 * If the x or y coordinate is out of bounds, the star is attempted to be
	 * wrapped around, but it might be reset.
	 *
	 * If the star being wrapped around will result in it being "stuck" at the
	 * edges/corner, then it is reset. A situation in which this could happen
	 * for example is if the velocity is something like (0, 0, -z) in which the
	 * star hitting the edge is due to it going out of the camera's view as it
	 * moves closer towards it. If the star was wrapped around in this case, it
	 * wouldn't move around like normal but instead move in the opposite
	 * direction (away from the centre again) resulting in another wrap.
	 *
	 * To detect this, I am checking if the sign star's "effective velocity",
	 * v_i for i = x or y for the components, has the same sign as the new
	 * position. If it does (or there's a zero), the star will be reset.
	 */
	pub fn out_of_bounds(&mut self, camera: &Camera, moved_vel: Vec2, star_radii_hndc: Vec2, mut get_layer: impl FnMut() -> (u32, Vec2)) {
		if !(0.0..=Z_MAX).contains(&self.pos.z) {
			let (layer, star_radii_hndc) = get_layer();
			*self = Star::reset(camera, 0.0, None, layer, star_radii_hndc);
			return;
		}

		let camera_size = camera.get_size();
		let zoom = camera_size.min_element();

		let mut half_ndc = sky::camera_to_half_ndc(self.pos, camera_size);
		for i in 0..2 {
			if half_ndc[i].abs() > 0.5 + star_radii_hndc[i] {
				let x = half_ndc[i] + 0.5;
				half_ndc[i] = x - x.floor() - 0.5 - star_radii_hndc[i] * 2.0 * half_ndc[i].signum();
				self.pos[i] = sky::half_ndc_to_camera_component(half_ndc[i], self.pos.z, camera_size[i], zoom);

				/*
				 * Effective velocity is calculated as follows:
				 *
				 * ev = d/dt ep
				 *    = d/dt (p_i / (p_z + zoom))
				 *    = (v_i * (p_z + zoom) - p_i * v_z) / (p_z + zoom)² (quotient rule)
				 *
				 * Note that the denominator is always non-negative so it can be
				 * removed when calculating the sign.
				 */
				let eff_vel = (self.vel[i] + moved_vel[i]) * (self.pos.z + zoom) - self.pos[i] * self.vel.z; // A value with the same sign as the effective velocity
				if eff_vel * self.pos[i] >= 0.0 {
					let (layer, star_radii_hndc) = get_layer();
					*self = Star::reset(camera, 0.0, Some(self.pos.z), layer, star_radii_hndc);
					return;
				}
			}
		}
	}
}
