pub mod camera;
pub mod color;
pub mod hittable;
pub mod hittable_list;
pub mod material;
pub mod ray;
pub mod rtweekend;
pub mod sphere;
pub mod vec3;

use std::{f64, thread::available_parallelism};

use image::RgbImage;
use indicatif::{ProgressBar, ProgressStyle};
use rayon::prelude::*;

use crate::{
    camera::*,
    color::*,
    hittable_list::*,
    material::*,
    material::{Dielectric, Lambertian},
    ray::*,
    rtweekend::*,
    sphere::*,
    vec3::*,
};

#[allow(dead_code)]
fn random_scene(aspect_ratio: f64) -> (Camera, HittableList) {
    let mut world = HittableList::new_empty();

    let ground_material: Lambertian = Lambertian::new(Color::new(0.5, 0.5, 0.5));
    world.add(Box::new(Sphere::new(
        Point3::new(0.0, -1000.0, 0.0),
        1000.0,
        Box::new(ground_material),
    )));

    for a in -12..12 {
        for b in -12..12 {
            let choose_mat = random_f64();
            let center = Point3::new(
                a as f64 + 0.9 * random_f64(),
                0.2,
                b as f64 + 0.9 * random_f64(),
            );

            if (center - Point3::new(4.0, 0.2, 0.0)).length() > 0.9 {
                if choose_mat < 0.3 {
                    //let albedo = Color::new(0.3, 0.8, 0.4);
                    let sphere_material: Lambertian = Lambertian::new(Color::random());
                    world.add(Box::new(Sphere::new(
                        center,
                        0.2,
                        Box::new(sphere_material),
                    )));
                } else if choose_mat < 0.7 {
                    let sphere_material: Metal = Metal::new(Color::random(), 0.2);
                    world.add(Box::new(Sphere::new(
                        center,
                        0.2,
                        Box::new(sphere_material),
                    )));
                } else {
                    let sphere_material: Dielectric = Dielectric::new(1.5);
                    world.add(Box::new(Sphere::new(
                        center,
                        0.2,
                        Box::new(sphere_material),
                    )));
                }
            }
        }
    }

    let material0  = Light::new(Color::new(15., 15., 15.));
    world.add(Box::new(Sphere::new(
        Point3::new(-1.0, 1.0, 0.0),
        1.0,
        Box::new(material0),
    )));

    let material1: Dielectric = Dielectric::new(1.5);
    world.add(Box::new(Sphere::new(
        Point3::new(0.0, 1.0, 0.0),
        1.0,
        Box::new(material1),
    )));

    let material2: Lambertian = Lambertian::new(Color::new(0.4, 0.2, 0.1));
    world.add(Box::new(Sphere::new(
        Point3::new(-4.0, 1.0, 0.0),
        1.0,
        Box::new(material2),
    )));

    let material3: Metal = Metal::new(Color::new(0.7, 0.6, 0.5), 0.0);
    world.add(Box::new(Sphere::new(
        Point3::new(4.0, 1.0, 0.0),
        1.0,
        Box::new(material3),
    )));

    // Camera
    let lookfrom = Point3::new(13.0, 2.0, 3.0);
    let lookat = Point3::new(0.0, 0.0, 0.0);
    let vup = Vec3::new(0.0, 1.0, 0.0);
    let dist_to_focus = (lookfrom - lookat).length();
    let focal_length = 50.0;
    let defocus_angle = 1.;

    let cam = Camera::new(
        lookfrom,
        lookat,
        vup,
        focal_length,
        aspect_ratio,
        defocus_angle,
        dist_to_focus,
    );

    (cam, world)
}

#[allow(dead_code)]
fn three_balls_scene(aspect_ratio: f64) -> (Camera, HittableList) {
    let mut world = HittableList::new_empty();

    let material_ground: Lambertian = Lambertian::new(Color::new(0.8, 0.8, 0.0));
    world.add(Box::new(Sphere::new(
        Point3::new(0.0, -100.5, -1.0),
        100.0,
        Box::new(material_ground),
    )));

    let material_center = Lambertian::new(Color::new(0.1, 0.2, 0.5));
    world.add(Box::new(Sphere::new(
        Point3::new(0.0, 0.0, -1.2),
        0.5,
        Box::new(material_center),
    )));

    let material_left = Dielectric::new(1.5);
    world.add(Box::new(Sphere::new(
        Point3::new(-1.0, 0.0, -1.0),
        0.5,
        Box::new(material_left),
    )));

    let material_bubble = Dielectric::new(1.0 / 1.5);
    world.add(Box::new(Sphere::new(
        Point3::new(-1.0, 0.0, -1.0),
        0.4,
        Box::new(material_bubble),
    )));

    let material_right = Metal::new(Color::new(0.8, 0.6, 0.2), 1.0);
    world.add(Box::new(Sphere::new(
        Point3::new(1.0, 0.0, -1.0),
        0.5,
        Box::new(material_right),
    )));

    // Camera
    let lookfrom = Point3::new(-2.0, 2.0, 1.0);
    let lookat = Point3::new(0.0, 0.0, -1.0);
    let vup = Vec3::new(0.0, 1.0, 0.0);
    let dist_to_focus = (lookfrom - lookat).length();
    let focal_length_mm = 20.0;
    let defocus_angle = 0.0;

    let cam = Camera::new(
        lookfrom,
        lookat,
        vup,
        focal_length_mm,
        aspect_ratio,
        defocus_angle,
        dist_to_focus,
    );

    (cam, world)
}

fn main() {
    // Image
    let aspect_ratio: f64 = 16. / 9.;
    let image_width: i32 = 1920;
    let image_height: i32 = (image_width as f64 / aspect_ratio as f64) as i32;
    let samples_per_pixel: i32 = 400;
    let max_depth: i32 = 10;

    let (cam, world) = random_scene(aspect_ratio);

    let num_threads: usize = match available_parallelism() {
        Ok(threads) => threads.into(),
        Err(_) => 1,
    };

    // Render
    rayon::ThreadPoolBuilder::new()
        .num_threads(num_threads)
        .build_global()
        .unwrap();

    let mut img_buffer = RgbImage::new(image_width as u32, image_height as u32);

    let total_pixels = (image_width * image_height) as u64;
    let bar = ProgressBar::new(total_pixels);

    bar.set_style(
        ProgressStyle::default_bar()
            .template(
                "{spinner:.green} [{elapsed_precise}] [{bar:40.green/black}] {percent}% ({eta})",
            )
            .unwrap()
            .progress_chars("#>-"),
    );

    img_buffer
        .par_enumerate_pixels_mut()
        .for_each(|(x, y, pixel)| {
            // goofy workaround :D
            let reversed_y = image_height as u32 - y;

            let mut pixel_color = Color::new(0.0, 0.0, 0.0);
            for _ in 0..samples_per_pixel {
                let u = (x as f64 + random_f64()) / (image_width - 1) as f64;
                let v = (reversed_y as f64 + random_f64()) / (image_height - 1) as f64;
                let r = cam.get_ray(u, v);
                pixel_color += cam.ray_color(r, &world, max_depth);
            }

            *pixel = get_scaled_color_components(pixel_color, samples_per_pixel);

            bar.inc(1);
        });

    bar.finish_with_message("Render Complete!");

    img_buffer.save("render.png").unwrap();
}
