pub mod util;
pub mod render;

use crate::{
	builder::RaytracerBuilder,
	hittable::{Hittable, HittableList, Object},
	interval::WORLD,
	material::Material,
	ray::Ray,
	vec3::{Color, Point3, Vec3},
};

pub struct Raytracer {
	world: HittableList,

	samples_per_pixel: usize,
	max_depth: usize,
	pixel_samples_scale: f64,

	image_width: usize,
	image_height: usize,

	center: Point3,
	pixel00_loc: Point3,
	pixel_delta_u: Vec3,
	pixel_delta_v: Vec3,

	defocus_angle: f64,
	defocus_disk_u: Vec3,
	defocus_disk_v: Vec3,
}

impl From<RaytracerBuilder> for Raytracer {
	fn from(config: RaytracerBuilder) -> Self {
		let aspect_ratio: f64 = config.image_width as f64 / config.image_height as f64;
		let theta = config.vertical_fov.to_radians();
		let h = (theta / 2.0).tan();
		let viewport_height = 2.0 * h * config.focus_dist;
		let viewport_width = viewport_height as f64 * aspect_ratio;

		let center = config.look_from;

		let w = (config.look_from - config.look_at).unit_vector();
		let u = config.vup.cross(&w).unit_vector();
		let v = w.cross(&u);

		let viewport_u = viewport_width * u;
		let viewport_v = viewport_height * -v;

		let pixel_delta_u = viewport_u / config.image_width as f64;
		let pixel_delta_v = viewport_v / config.image_height as f64;

		let viewport_upper_left =
			center - (config.focus_dist * w) - (viewport_u / 2.0) - (viewport_v / 2.0);
		let pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v);

		let defocus_radius = config.focus_dist * (config.defocus_angle / 2.0).to_radians().tan();
		let defocus_disk_u = u * defocus_radius;
		let defocus_disk_v = v * defocus_radius;

		Self {
			world: config.world,
			samples_per_pixel: config.samples_per_pixel,
			max_depth: config.max_depth,
			pixel_samples_scale: 1.0 / config.samples_per_pixel as f64,
			image_width: config.image_width,
			image_height: config.image_height,
			center,
			pixel00_loc,
			pixel_delta_u,
			pixel_delta_v,
			defocus_angle: config.defocus_angle,
			defocus_disk_u,
			defocus_disk_v,
		}
	}
}

impl Raytracer {
	pub fn builder() -> RaytracerBuilder {
		RaytracerBuilder::default()
	}

	pub fn add(&mut self, object: Object) {
		self.world.add(object);
	}

	fn sample_square() -> Vec3 {
		Vec3::new(random_f64() - 0.5, random_f64() - 0.5, 0.0)
	}

	fn defocus_disk_sample(&self) -> Point3 {
		let p = Point3::random_in_unit_disk();
		self.center + (p[0] * self.defocus_disk_u) + (p[1] * self.defocus_disk_v)
	}

	fn get_ray(&self, i: usize, j: usize) -> Ray {
		let offset = Self::sample_square();
		let pixel_sample = self.pixel00_loc
			+ ((i as f64 + offset.x()) * self.pixel_delta_u)
			+ ((j as f64 + offset.y()) * self.pixel_delta_v);

		let origin = match self.defocus_angle <= 0.0 {
			true => self.center,
			false => self.defocus_disk_sample(),
		};
		let direction = pixel_sample - origin;

		Ray::new(origin, direction)
	}

	fn ray_color(&self, depth: usize, ray: &Ray) -> Color {
		if depth == 0 {
			return Color::default();
		}

		if let Some(hit_record) = self.world.hit(ray, &WORLD) {
			if let Some((attenuation, scattered)) = hit_record.mat.scatter(ray, &hit_record) {
				return attenuation * self.ray_color(depth - 1, &scattered);
			}
			return Color::default();
		}

		let unit_direction = ray.direction().unit_vector();
		let a = 0.5 * (unit_direction.y() + 1.0);
		((1.0 - a) * Color::new(1.0, 1.0, 1.0)) + (a * Color::new(0.5, 0.7, 1.0))
	}
}

#[inline(always)]
pub fn random_f64() -> f64 {
	fastrand::f64()
}

#[inline(always)]
pub fn random_f64_range(min: f64, max: f64) -> f64 {
	min + (max - min) * random_f64()
}
