use std::fmt;
use std::ops::AddAssign;
use std::ops::Range;
use std::path::Path;
use std::path::PathBuf;
use std::str;
use std::sync;
use std::sync::mpsc::sync_channel;
use std::sync::mpsc::Receiver;
use std::sync::mpsc::SyncSender;
use std::sync::Arc;
use std::sync::Mutex;
use std::thread;
use std::time;

use core_affinity;
use log::info;
use log::trace;
use num_cpus;
use rand;
use rand::Rng;
use serde_derive::Serialize;
use structopt::StructOpt;

use crate::camera::Camera;
use crate::hitable::Hit;
use crate::human;
use crate::material::Lambertian;
use crate::output;
use crate::ray::Ray;
use crate::scenes;
use crate::sphere::Sphere;
use crate::texture::ConstantTexture;
use crate::texture::EnvMap;
use crate::vec3::Vec3;

#[derive(Debug)]
pub enum Model {
    BVH,
    Bench,
    Book,
    CornellBox,
    CornellSmoke,
    Final,
    Mandelbrot,
    PerlinDebug,
    Spheramid,
    Test,
    Tutorial,
}

impl Model {
    pub fn scene(&self, opt: &Opt) -> Scene {
        match self {
            Model::BVH => scenes::bvh::new(&opt),
            Model::Bench => scenes::bench::new(&opt),
            Model::Book => scenes::book::new(&opt),
            Model::CornellBox => scenes::cornell_box::new(&opt),
            Model::CornellSmoke => scenes::cornell_smoke::new(&opt),
            Model::Final => scenes::final_scene::new(&opt),
            Model::Mandelbrot => scenes::mandelbrot::new(&opt),
            Model::PerlinDebug => scenes::perlin_debug::new(&opt),
            Model::Spheramid => scenes::spheramid::new(&opt),
            Model::Test => scenes::test::new(&opt),
            Model::Tutorial => scenes::tutorial::new(&opt),
        }
    }
}

#[derive(Debug)]
pub struct ModelParseError(String);

impl fmt::Display for ModelParseError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "unknown model enum type '{}'", self.0)
    }
}

impl str::FromStr for Model {
    type Err = ModelParseError;
    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        match s {
            "bench" => Ok(Model::Bench),
            "book" => Ok(Model::Book),
            "bvh" => Ok(Model::BVH),
            "cornell_box" => Ok(Model::CornellBox),
            "cornell_smoke" => Ok(Model::CornellSmoke),
            "final" => Ok(Model::Final),
            "mandelbrot" => Ok(Model::Mandelbrot),
            "perlin_debug" => Ok(Model::PerlinDebug),
            "spheramid" => Ok(Model::Spheramid),
            "test" => Ok(Model::Test),
            "tutorial" => Ok(Model::Tutorial),
            _ => Err(ModelParseError(s.to_owned())),
        }
    }
}

impl std::string::ToString for Model {
    fn to_string(&self) -> String {
        match self {
            Model::BVH => "bvh".to_string(),
            Model::Bench => "bench".to_string(),
            Model::Book => "book".to_string(),
            Model::CornellBox => "cornell_box".to_string(),
            Model::CornellSmoke => "cornell_smoke".to_string(),
            Model::Final => "final".to_string(),
            Model::Mandelbrot => "mandelbrot".to_string(),
            Model::PerlinDebug => "perlin_debug".to_string(),
            Model::Spheramid => "spheramid".to_string(),
            Model::Test => "test".to_string(),
            Model::Tutorial => "tutorial".to_string(),
        }
    }
}

#[derive(Debug, StructOpt)]
#[structopt(name = "tracer", about = "An experimental ray tracer.")]
pub struct Opt {
    /// Image width
    #[structopt(short = "w", long = "width", default_value = "512")]
    pub width: usize,
    /// Image height
    #[structopt(short = "h", long = "height", default_value = "512")]
    pub height: usize,
    /// Number of threads
    #[structopt(short = "t", long = "num_threads")]
    pub num_threads: Option<usize>,
    /// Sub-samples per pixel
    #[structopt(short = "s", long = "subsample", default_value = "8")]
    pub subsamples: usize,
    /// Select scene to render, one of: "bench", "book", "tutorial", "bvh", "test", "cornell_box",
    /// "cornell_smoke", "perlin_debug", "final"
    #[structopt(long = "model", default_value = "book")]
    pub model: Model,
    /// Path to store pprof profile data, i.e. /tmp/cpuprofile.pprof
    #[structopt(long = "pprof", parse(from_os_str))]
    pub pprof: Option<PathBuf>,
    /// Use acceleration data structure, may be BVH or kd-tree depending on scene.
    #[structopt(long = "use_accel")]
    pub use_accel: bool,

    /// Output directory
    #[structopt(parse(from_os_str), default_value = "/tmp/tracer")]
    pub output: PathBuf,
}

pub fn opt_hash(opt: &Opt) -> String {
    // TODO(wathiede): add threads.
    format!(
        "w:{}-h:{}-s:{}-pprof:{}-model:{}-use_accel:{}-{}",
        opt.width,
        opt.height,
        opt.subsamples,
        opt.pprof.is_some(),
        opt.model.to_string(),
        opt.use_accel,
        opt.output.display().to_string().replace("/", "_")
    )
}

// TODO(wathiede): implement the skips and then the renderer could use json as an input file type.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Scene {
    #[serde(skip)]
    pub world: Box<dyn Hit>,
    #[serde(skip)]
    pub camera: Camera,
    pub subsamples: usize,
    /// overrides subsamples setting.
    pub adaptive_subsampling: Option<f32>,
    pub num_threads: Option<usize>,
    pub width: usize,
    pub height: usize,
    pub global_illumination: bool,
    #[serde(skip)]
    pub env_map: Option<EnvMap>,
}

impl Default for Scene {
    fn default() -> Scene {
        let lookfrom = Vec3::new(20., 20., 20.);
        let lookat = Vec3::new(0., 0., 0.);
        let dist_to_focus = (lookfrom - lookat).length();
        let aperture = 0.1;
        let time_min = 0.;
        let time_max = 1.;
        let camera = Camera::new(
            lookfrom,
            lookat,
            Vec3::new(0., 1., 0.),
            70.,
            1.,
            aperture,
            dist_to_focus,
            time_min,
            time_max,
        );
        Scene {
            world: Box::new(Sphere::new(
                Vec3::new(0., 0., 0.),
                1.0,
                Lambertian::new(ConstantTexture::new([0., 1., 0.])),
            )),
            camera,
            subsamples: 0,
            adaptive_subsampling: None,
            num_threads: None,
            width: 0,
            height: 0,
            global_illumination: false,
            env_map: None,
        }
    }
}

// color will trace ray up to 50 bounces deep accumulating color as it goes.  If
// global_illumination is true, a default light background color is assumed and will light the
// world.  If false, it is expected the scene has emissive light sources.
fn color(
    r: Ray,
    world: &dyn Hit,
    depth: usize,
    global_illumination: bool,
    env_map: &Option<EnvMap>,
) -> (Vec3, usize) {
    if let Some(rec) = world.hit(r, 0.001, std::f32::MAX) {
        let (u, v) = rec.uv;
        let emitted = rec.material.emitted(u, v, rec.p);
        let scatter_response = rec.material.scatter(&r, &rec);
        if depth < 50 && scatter_response.reflected {
            let (c, rays) = color(
                scatter_response.scattered,
                world,
                depth + 1,
                global_illumination,
                env_map,
            );
            return (emitted + scatter_response.attenutation * c, rays + 1);
        } else {
            return (emitted, 1);
        }
    }
    if global_illumination {
        return match env_map {
            Some(env_map) => (env_map.color(r.direction.unit_vector()), 1),
            None => {
                let unit_direction = r.direction.unit_vector();
                // No hit, choose color from background.
                let t = 0.5 * (unit_direction.y + 1.);
                (
                    Vec3::new(1., 1., 1.) * (1. - t) + Vec3::new(0.5, 0.7, 1.) * t,
                    1,
                )
            }
        };
    }
    // No global illumination, so background is black.
    (Vec3::new(0., 0., 0.), 1)
}

const MAX_ADAPTIVE_DEPTH: usize = 10;
fn trace_pixel_adaptive(
    depth: usize,
    threshold: f32,
    x: usize,
    y: usize,
    x_range: Range<f32>,
    y_range: Range<f32>,
    scene: &Scene,
) -> (Vec3, usize) {
    let w = scene.width as f32;
    let h = scene.height as f32;
    let x_mid = x_range.start + ((x_range.end - x_range.start) / 2.);
    let y_mid = y_range.start + ((y_range.end - y_range.start) / 2.);
    let mc = ((x_mid + x as f32) / w, (y_mid + y as f32) / h);
    let (center, rays) = color(
        scene.camera.get_ray(mc.0, mc.1),
        scene.world.as_ref(),
        0,
        scene.global_illumination,
        &scene.env_map,
    );
    if depth == 0 {
        output::set_pixel(output::ADAPTIVE_DEPTH, x, y, [1., 0., 0.].into());
        return (center, rays);
    }
    // t = top
    // m = middle
    // b = bottom
    // l = left
    // c = center
    // r = right
    let tl = (
        (x_range.start + x as f32) / w,
        (y_range.start + y as f32) / h,
    );
    let tr = ((x_range.end + x as f32) / w, (y_range.start + y as f32) / h);
    let bl = ((x_range.start + x as f32) / w, (y_range.end + y as f32) / h);
    let br = ((x_range.end + x as f32) / w, (y_range.end + y as f32) / h);

    let (corners, rays) = [tl, tr, mc, bl, br]
        .iter()
        .map(|(u, v)| {
            color(
                scene.camera.get_ray(*u, *v),
                scene.world.as_ref(),
                0,
                scene.global_illumination,
                &scene.env_map,
            )
        })
        .fold(
            ([0., 0., 0.].into(), 0),
            |(p1, r1): (Vec3, usize), (p2, r2): (Vec3, usize)| ((p1 + p2), (r1 + r2)),
        );
    let corners = corners / 5.;
    if (corners - center).length() > threshold {
        let tl = trace_pixel_adaptive(
            depth - 1,
            threshold,
            x,
            y,
            x_range.start..x_mid,
            y_range.start..y_mid,
            scene,
        );
        let tr = trace_pixel_adaptive(
            depth - 1,
            threshold,
            x,
            y,
            x_mid..x_range.end,
            y_range.start..y_mid,
            scene,
        );
        let bl = trace_pixel_adaptive(
            depth - 1,
            threshold,
            x,
            y,
            x_range.start..x_mid,
            y_mid..y_range.end,
            scene,
        );
        let br = trace_pixel_adaptive(
            depth - 1,
            threshold,
            x,
            y,
            x_mid..x_range.end,
            y_mid..y_range.end,
            scene,
        );
        let pixel = (tl.0 + tr.0 + bl.0 + br.0) / 4.;
        let rays = tl.1 + tr.1 + bl.1 + br.1;
        (pixel, rays)
    } else {
        if depth == MAX_ADAPTIVE_DEPTH {
            output::set_pixel(output::ADAPTIVE_DEPTH, x, y, [0., 1., 0.].into());
        }
        (corners, rays)
    }
}

fn trace_pixel_random(x: usize, y: usize, scene: &Scene) -> (Vec3, usize) {
    let mut rng = rand::thread_rng();
    let u = (rng.gen::<f32>() + x as f32) / scene.width as f32;
    let v = (rng.gen::<f32>() + y as f32) / scene.height as f32;
    let ray = scene.camera.get_ray(u, v);
    color(
        ray,
        scene.world.as_ref(),
        0,
        scene.global_illumination,
        &scene.env_map,
    )
}

#[derive(Clone, Copy)]
struct RenderStats {
    rays: usize,
    pixels: usize,
}

impl AddAssign for RenderStats {
    fn add_assign(&mut self, other: Self) {
        *self = Self {
            rays: self.rays + other.rays,
            pixels: self.pixels + other.pixels,
        }
    }
}

fn progress(
    last_stat: &RenderStats,
    current_stat: &RenderStats,
    time_diff: time::Duration,
    pixel_total: usize,
) -> String {
    let human = human::Formatter::new();
    let pixel_diff = current_stat.pixels - last_stat.pixels;
    let ray_diff = current_stat.rays - last_stat.rays;
    format!(
        "{:7} / {:7}pixels ({:2}%) {:7}pixels/s {:7}rays/s",
        human.format(current_stat.pixels as f64),
        human.format(pixel_total as f64),
        100 * current_stat.pixels / pixel_total,
        human.format(pixel_diff as f64 / time_diff.as_secs_f64()),
        human.format(ray_diff as f64 / time_diff.as_secs_f64())
    )
}

impl Default for RenderStats {
    fn default() -> Self {
        RenderStats { rays: 0, pixels: 0 }
    }
}

enum Request {
    Pixel { x: usize, y: usize },
    Line { width: usize, y: usize },
}

enum Response {
    Pixel {
        x: usize,
        y: usize,
        pixel: Vec3,
        rs: RenderStats,
    },
    Line {
        y: usize,
        pixels: Vec<Vec3>,
        rs: RenderStats,
    },
}

fn render_pixel(scene: &Scene, x: usize, y: usize) -> (Vec3, usize) {
    let (pixel, rays) = if let Some(threshold) = scene.adaptive_subsampling {
        trace_pixel_adaptive(
            MAX_ADAPTIVE_DEPTH,
            threshold,
            x,
            y,
            0.0..1.0,
            0.0..1.0,
            scene,
        )
    } else {
        let (pixel, rays) = (0..scene.subsamples)
            .map(|_| trace_pixel_random(x, y, scene))
            .fold(
                ([0., 0., 0.].into(), 0),
                |(p1, r1): (Vec3, usize), (p2, r2): (Vec3, usize)| ((p1 + p2), (r1 + r2)),
            );
        output::set_pixel_grey(output::RAYS_PER_PIXEL, x, y, rays as f32);
        (pixel / scene.subsamples as f32, rays)
    };
    // Gamma correct, use gamma 2 correction, which is 1/gamma where gamma=2 which is 1/2 or
    // sqrt.
    (
        Vec3::new(pixel[0].sqrt(), pixel[1].sqrt(), pixel[2].sqrt()),
        rays,
    )
}

fn render_worker(
    tid: usize,
    scene: &Scene,
    input_chan: Arc<Mutex<Receiver<Request>>>,
    output_chan: &SyncSender<Response>,
) {
    loop {
        let job = { input_chan.lock().unwrap().recv() };
        match job {
            Err(err) => {
                trace!("Shutting down render_worker {}: {}", tid, err);
                return;
            }
            Ok(req) => match req {
                Request::Line { width, y } => {
                    trace!("tid {} width {} y {}", tid, width, y);
                    let batch = false;
                    if batch {
                        let (pixels, rays): (Vec<Vec3>, Vec<usize>) = (0..width)
                            .map(|x| render_pixel(scene, x, y))
                            .collect::<Vec<(_, _)>>()
                            .into_iter()
                            .unzip();
                        let rays = rays.iter().sum();
                        output_chan
                            .send(Response::Line {
                                y,
                                pixels,
                                rs: RenderStats {
                                    rays,
                                    pixels: width,
                                },
                            })
                            .expect("failed to send pixel response");
                    } else {
                        (0..width).for_each(|x| {
                            let (pixel, rays) = render_pixel(scene, x, y);
                            output_chan
                                .send(Response::Pixel {
                                    x,
                                    y,
                                    pixel,
                                    rs: RenderStats { rays, pixels: 1 },
                                })
                                .expect("failed to send pixel response");
                        });
                    }
                }
                Request::Pixel { x, y } => {
                    trace!("tid {} x {} y {}", tid, x, y);
                    let (pixel, rays) = render_pixel(scene, x, y);
                    output_chan
                        .send(Response::Pixel {
                            x,
                            y,
                            pixel,
                            rs: RenderStats { rays, pixels: 1 },
                        })
                        .expect("failed to send line response");
                }
            },
        }
    }
}

pub fn render(scene: Scene, output_dir: &Path) -> std::result::Result<(), std::io::Error> {
    let num_threads = scene.num_threads.unwrap_or_else(num_cpus::get);
    let (pixel_req_tx, pixel_req_rx) = sync_channel(2 * num_threads);
    let (pixel_resp_tx, pixel_resp_rx) = sync_channel(2 * num_threads);

    let scene = Arc::new(scene);
    let pixel_req_rx = Arc::new(Mutex::new(pixel_req_rx));
    info!("Adaptive subsampling: {:?}", scene.adaptive_subsampling);
    // Retrieve the IDs of all active CPU cores.
    let core_ids = core_affinity::get_core_ids().unwrap();
    let core_ids = if core_ids.len() > num_threads {
        core_ids[..num_threads].to_vec()
    } else {
        core_ids
    };
    info!("Creating {} render threads", core_ids.len());
    output::register_image(
        output::MAIN_IMAGE.to_string(),
        (scene.width, scene.height),
        output::ImageType::RGB01,
    );
    if scene.adaptive_subsampling.is_some() {
        output::register_image(
            output::ADAPTIVE_DEPTH.to_string(),
            (scene.width, scene.height),
            output::ImageType::RGB01,
        );
    }
    output::register_image(
        output::RAYS_PER_PIXEL.to_string(),
        (scene.width, scene.height),
        output::ImageType::GreyNormalized,
    );
    // Create a thread for each active CPU core.
    let mut handles = core_ids
        .into_iter()
        .enumerate()
        .filter(|(i, _id)| *i < num_threads)
        .map(|(i, id)| {
            let s = sync::Arc::clone(&scene);
            let pixel_req_rx = pixel_req_rx.clone();
            let pixel_resp_tx = pixel_resp_tx.clone();
            thread::spawn(move || {
                core_affinity::set_for_current(id);
                render_worker(i, &s, pixel_req_rx, &pixel_resp_tx);
            })
        })
        .collect::<Vec<_>>();
    drop(pixel_req_rx);
    drop(pixel_resp_tx);

    let start_time = time::Instant::now();
    let (w, h) = (scene.width, scene.height);
    handles.push(thread::spawn(move || {
        let batch_line_requests = true;
        if batch_line_requests {
            for y in 0..h {
                pixel_req_tx
                    .send(Request::Line { width: w, y })
                    .expect("failed to send line request");
            }
        } else {
            for y in 0..h {
                for x in 0..w {
                    pixel_req_tx
                        .send(Request::Pixel { x, y })
                        .expect("failed to send pixel request");
                }
            }
        }
        drop(pixel_req_tx);
    }));

    info!("Rendering with {} subsamples", scene.subsamples);

    let pixel_total = scene.width * scene.height;
    let mut last_time = time::Instant::now();
    let mut last_stat: RenderStats = Default::default();
    let mut current_stat: RenderStats = Default::default();
    for resp in pixel_resp_rx {
        match resp {
            Response::Pixel { x, y, pixel, rs } => {
                current_stat += rs;
                output::set_pixel(output::MAIN_IMAGE, x, y, pixel);
            }
            Response::Line { y, pixels, rs } => {
                current_stat += rs;
                for (x, pixel) in pixels.iter().enumerate() {
                    output::set_pixel(output::MAIN_IMAGE, x, y, *pixel);
                }
            }
        }

        let now = time::Instant::now();
        let time_diff = now - last_time;
        if time_diff > time::Duration::from_secs(1) {
            info!(
                "{}",
                progress(&last_stat, &current_stat, time_diff, pixel_total)
            );
            last_stat = current_stat;
            last_time = now;
        }
    }
    for thr in handles {
        thr.join().expect("thread join");
    }
    let time_diff = time::Instant::now() - start_time;
    info!(
        "Runtime {} seconds {}",
        time_diff.as_secs_f32(),
        progress(&Default::default(), &current_stat, time_diff, pixel_total)
    );

    output::write_images(&scene, time_diff, output_dir)
}
