use std::sync::{Arc, Mutex};
use std::time::Instant;

use crate::camera::Camera;
use crate::hittable::{Sphere, HittableList, HittableObject};
use crate::output::{Output, PNG};
use crate::ray::Ray;
use crate::vec3::{Color, Point3, Vec3};
use hittable::MovableSphere;
use rand::Rng;
use rand::distributions::{Distribution, Uniform};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use crate::material::{Dielectric, Lambertian, Material, Metal};
use crate::aabb::Aabb;
use crate::image_texture::ImageTexture;
use crate::noise::NoiseTexture;
use crate::perlin::Perlin;
use crate::texture::CheckerTexture;

mod vec3;
mod ray;
mod hittable;
mod material;
mod camera;
mod output;
mod aabb;
mod bvh;
mod texture;
mod perlin;
mod noise;
mod image_texture;

fn earth() -> HittableList {
    let mut world:HittableList = Vec::new();
    let earth_texture = ImageTexture::new("textures/earthmap.jpg");
    let earth_material = Arc::new(
        Material::Lambertian(Lambertian::textured(Arc::new(earth_texture))));
    world.push(Arc::new(Sphere {
        center: Point3::new(0.0, 0.0, 0.0),
        radius: 2.0,
        material: earth_material
    }));
    world
}

fn two_spheres() -> HittableList {
    let mut world:HittableList = Vec::new();
    let checker = CheckerTexture::colored(
        Color::new(0.2, 0.3, 0.1),
        Color::new(0.9, 0.9, 0.9));
    let checker_material = Arc::new(Material::Lambertian(Lambertian::textured(Arc::new(checker))));
    world.push(Arc::new(Sphere {
        center: Point3::new(0.0, -10.0, 0.0),
        radius: 10.0,
        material: Arc::clone(&checker_material)
    }));
    world.push(Arc::new(Sphere {
        center: Point3::new(0.0, 10.0, 0.0),
        radius: 10.0,
        material: checker_material
    }));
    world
}
fn two_perlin_spheres() -> HittableList {
    let mut world:HittableList = Vec::new();
    let noise = NoiseTexture { noise: Perlin::new(), scale: 4.0 };
    let noise_material = Arc::new(Material::Lambertian(Lambertian::textured(Arc::new(noise))));
    world.push(Arc::new(Sphere {
        center: Point3::new(0.0, -1000.0, 0.0),
        radius: 1000.0,
        material: Arc::clone(&noise_material)
    }));
    world.push(Arc::new(Sphere {
        center: Point3::new(1.0, 2.0, 1.0),
        radius: 2.0,
        material: noise_material
    }));
    world
}

fn random_scene() -> HittableList {
    let mut world: HittableList = Vec::new();

    let checker = CheckerTexture::colored(
        Color::new(0.2, 0.3, 0.1),
        Color::new(0.9, 0.9, 0.9));
    let material_ground = Arc::new(Material::Lambertian(Lambertian::textured(Arc::new(checker))));
    let ground = Sphere {
        center: Point3::new(0.0, -1000.0, 0.0),
        radius: 1000.0,
        material: material_ground
    };
    world.push(Arc::new(ground));

    let unit_range = Uniform::from(0.0..1.0);
    let fuzz_range = Uniform::from(0.0..0.5);
    let mut rng = rand::thread_rng();
    let p = Point3::new(4.0, 0.2, 0.0);
    for a in -1..11 {
        for b in -11..11 {
            let choose_material = unit_range.sample(&mut rng);
            let center = Point3::new(
                (a as f64) + 0.9*unit_range.sample(&mut rng),
                0.2,
                (b as f64) + 0.9*unit_range.sample(&mut rng));
            if (center - p).length() < 0.9 {
                continue;
            }
            let material = match choose_material {
                _ if choose_material < 0.8 => Arc::new(Material::Lambertian(Lambertian::new(
                    Color::random(0.0, 1.0) * Color::random(0.0, 1.0)))),
                _ if choose_material < 0.95 => Arc::new(Material::Metal(Metal::new(
                    Color::random(0.5, 1.0),
                    fuzz_range.sample(&mut rng)))),
                _ => Arc::new(Material::Dielectric(Dielectric::new(1.5))),
            };
            let sphere: HittableObject = match rng.gen_bool(1.0 / 3.0) {
                true => {
                    let center1 = center + Vec3::new(0.0, fuzz_range.sample(&mut rng) / 2.0, 0.0);
                    Arc::new(MovableSphere {
                        center0: center,
                        center1,
                        radius: 0.2,
                        material,
                        time0: 0.0,
                        time1: 1.0
                    })
                }
                false => Arc::new(Sphere {
                    center,
                    radius: 0.2,
                    material
                })
            };
            world.push(sphere);
        }
    }

    let material1 = Arc::new(Material::Dielectric(Dielectric::new(1.5)));
    world.push(Arc::new(Sphere {
        center: Point3::new(0.0, 1.0, 0.0),
        radius: 1.0,
        material: material1
    }));
    let material2 = Arc::new(Material::Lambertian(Lambertian::new(Color::new(0.4, 0.2, 0.1))));
    world.push(Arc::new(Sphere {
        center: Point3::new(-4.0, 1.0, 0.0),
        radius: 1.0,
        material: material2
    }));
    let material3 = Arc::new(Material::Metal(Metal::new(Color::new(0.7, 0.6, 0.5), 0.0)));
    world.push(Arc::new(Sphere {
        center: Point3::new(4.0, 1.0, 0.0),
        radius: 1.0,
        material: material3
    }));

    world
}

fn main() {
    // Image
    const ASPECT_RATIO: f64 = 3.0 / 2.0;
    const IMAGE_WIDTH: usize = 400;
    const IMAGE_HEIGHT: usize = (IMAGE_WIDTH as f64 / ASPECT_RATIO) as usize;
    const SAMPLES_PER_PIXEL: i32 = 1;
    const MAX_DEPTH: i32 = 50;

    let look_from = Point3::new(13.0, 2.0, 3.0);
    let look_at = Point3::new(0.0, 0.0, 0.0);
    let focus_dist = 2.0;

    // Camera
    let cam = Camera::new(
        look_from,
        look_at,
        Vec3::new(0.0, 1.0, 0.0),
        ASPECT_RATIO,
        20.0,
        0.0,
        focus_dist,
    0.0,
    1.0);

    // World
    let scene: u8 = 2;
    let world = match scene {
        0 => two_spheres(),
        1 => two_perlin_spheres(),
        2 => earth(),
        _ => random_scene()
    };

    let between = Uniform::from(0.0..1.0);

    let start = Instant::now();
    let mut pixels = vec![0; IMAGE_WIDTH * IMAGE_HEIGHT * 3];
    let bands: Vec<(usize, &mut [u8])> = pixels.chunks_mut(3).enumerate().collect();
    let count = Mutex::new(0);
    bands.into_par_iter().for_each(|(i, pixel)| {
        let row = IMAGE_HEIGHT - (i / IMAGE_WIDTH) - 1;
        let col = i % IMAGE_WIDTH;
        let mut rng = rand::thread_rng();
        let mut color = Color::default();
        (0..SAMPLES_PER_PIXEL).for_each(|_s| {
            let random_number = between.sample(&mut rng);
            let u = (col as f64 + random_number) / (IMAGE_WIDTH - 1) as f64;
            let v = (row as f64 + random_number) / (IMAGE_HEIGHT - 1) as f64;
            let ray = cam.get_ray(u, v);
            color += ray.pixel_color(&world, MAX_DEPTH);
        });
        let bytes = color.into_bytes(SAMPLES_PER_PIXEL);
        pixel[0] = bytes[0];
        pixel[1] = bytes[1];
        pixel[2] = bytes[2];
        if i % 100 == 0 {
            let mut rem = count.lock().unwrap();
            let percent_done_before = 100 * *rem / (IMAGE_WIDTH * IMAGE_HEIGHT);
            *rem += 100;
            let percent_done_after = 100 * *rem / (IMAGE_WIDTH * IMAGE_HEIGHT);
            if percent_done_before != percent_done_after {
                eprint!("\rProgress: {}% ", percent_done_after);
            }
        }
    });
    PNG::write("imc.png", &pixels, IMAGE_WIDTH, IMAGE_HEIGHT).expect("Error writing image: {}");
    eprintln!("\nDone. Time: {}ms", start.elapsed().as_millis());
}

fn world_bounding_box(time0: f64, time1: f64, world: &HittableList) -> Option<Aabb> {
    if world.is_empty() {
        return None;
    }
    let mut is_first = true;
    let mut output_box: Aabb = Aabb {
        minimum: Point3::default(),
        maximum: Point3::default()
    };
    for object in world {
        if let Some(bb) = object.bounding_box(time0, time1) {
            output_box = match is_first {
                true => bb,
                false => output_box.surrounding(&bb)
            };
            is_first = false;
        } else {
            return None;
        }
    }
    Some(output_box)
}