// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
/**
 * 2D Perlin noise that wraps around (wrapping needed to tile seamlessly).
 *
 * Following this guide for generation:
 * https://en.wikipedia.org/wiki/Perlin_noise
 */
use std::f32::consts::TAU;

use arrayvec::ArrayVec;
use rand_chacha::ChaCha20Rng;
use glam::{IVec2, Vec2};

use crate::utils::{self, Float};

pub struct PerlinNoiseMap {
	width: usize,
	height: usize,
	gradients: Box<[Vec2]>,
}

impl PerlinNoiseMap {
	pub fn new(width: usize, height: usize, mut rng: ChaCha20Rng) -> PerlinNoiseMap {
		// Step 1: Creating the grid
		let gradients = (0..width * height).map(|_| {
			let angle = rng.next_f32() * TAU;
			let (sin, cos) = angle.sin_cos();
			Vec2::new(cos, sin)
		}).collect();

		PerlinNoiseMap { width, height, gradients }
	}

	pub fn with_config<'a>(&'a self, config: &'a PerlinConfig) -> PerlinGenerator<'a> {
		PerlinGenerator(self, config)
	}

	/**
	 * Gets the gradient at the given position (wrapped around if necessary).
	 */
	fn get(&self, pos: IVec2) -> Vec2 {
		fn pmod(mut x: i32, d: usize) -> usize { // Always returns a positive integer
			x %= d as i32;
			if x < 0 { x += d as i32; }
			x as usize
		}

		let (x, y) = (pmod(pos.x, self.width), pmod(pos.y, self.height));
		self.gradients[x + y * self.width]
	}
}

pub struct PerlinConfig {
	pub scale: u32,
}

#[derive(Clone, Copy)]
pub struct PerlinGenerator<'a>(&'a PerlinNoiseMap, &'a PerlinConfig);

impl PerlinGenerator<'_> {
	pub fn get(self, x: f32, y: f32) -> f32 {
		let PerlinGenerator(map, config) = self;

		let pos = Vec2::new(x, y);
		let fscale = config.scale as f32;
		let corner00 = (pos / fscale).floor() * fscale;

		// Step 2: Getting the dot product
		let mut dots = ArrayVec::<f32, 4>::new();
		for iy in 0..=1 {
			for ix in 0..=1 {
				let corner = corner00 + IVec2::new(ix, iy).as_vec2() * fscale;
				let offset = (pos - corner) / fscale;
				let gradient = map.get(corner.as_ivec2());
				dots.push(offset.dot(gradient));
			}
		}

		// Step 3: Interpolation
		let [x00, x10, x01, x11] = dots.into_inner().unwrap();
		let t = (pos - corner00) / fscale;
		utils::bilerp(x00, x10, x01, x11, utils::smoothstep(t.x), utils::smoothstep(t.y))
	}
}
