use std::time::Instant;

use anyhow::Result;
use structopt::StructOpt;

use rtchallenge::prelude::*;

use rtchallenge::{
    camera::RenderStrategy,
    float::consts::PI,
    patterns::{test_pattern, BLACK_PAT, WHITE_PAT},
    WHITE,
};

/// End of chapter 11 challenge.
#[derive(StructOpt, Debug)]
#[structopt(name = "eoc11")]
struct Opt {
    /// Strategy for casting rays into image.
    #[structopt(long, default_value = "rayon")]
    render_strategy: RenderStrategy,
    /// Number of samples per pixel.  0 renders from the center of the pixel, 1 or more samples N
    /// times randomly across the pixel.
    #[structopt(short, long, default_value = "0")]
    samples: usize,
    /// Rendered image width in pixels.
    #[structopt(short, long, default_value = "2560")]
    width: usize,
    /// Rendered image height in pixels.
    #[structopt(short, long, default_value = "1440")]
    height: usize,
}

fn main() -> Result<()> {
    let start = Instant::now();
    let opt = Opt::from_args();

    let light1 = PointLightBuilder::default()
        .position(point(-5., 5., -5.))
        .intensity(WHITE)
        .build()?;
    let light2 = PointLightBuilder::default()
        .position(point(5., 5., -5.))
        .intensity([0.2, 0.2, 0.6])
        .build()?;
    let light3 = PointLightBuilder::default()
        .position(point(0., 2., -5.))
        .intensity([0.2, 0.2, 0.1])
        .build()?;

    let from = point(2., 8., -10.);
    let to = point(2., 1., -1.);
    let up = point(0., 1., 0.);
    let camera = CameraBuilder::default()
        .hsize(opt.width)
        .vsize(opt.height)
        .field_of_view(PI / 6.)
        .transform(view_transform(from, to, up))
        .render_strategy(opt.render_strategy)
        .samples_per_pixel(opt.samples)
        .build()?;

    let floor = plane()
        .material(
            MaterialBuilder::default()
                .color(
                    checkers_pattern(
                        ring_pattern([0.8, 0.8, 0.2].into(), [0.8, 0.2, 0.8].into())
                            .transform(scaling(1. / 8., 1. / 8., 1. / 8.))
                            .build()?,
                        ring_pattern([0.2, 0.8, 0.2].into(), [0.8, 0.2, 0.2].into())
                            .transform(scaling(1. / 4., 1. / 4., 1. / 4.))
                            .build()?,
                    )
                    .transform(translation(0., 1., 0.) * scaling(2., 2., 2.))
                    .build()?,
                )
                .specular(0.)
                .reflective(0.5)
                .build()?,
        )
        .build()?;
    let sphere_size = scaling(0.5, 0.5, 0.5);

    let x1y1 = sphere()
        .transform(translation(1., 1., 0.) * sphere_size)
        .material(
            MaterialBuilder::default()
                .color(
                    gradient_pattern([0., 0., 1.].into(), [1., 1., 0.].into())
                        .transform(scaling(2., 1., 1.) * translation(-0.5, 0., 0.))
                        .build()?,
                )
                .diffuse(0.7)
                .specular(0.3)
                .reflective(0.5)
                .build()?,
        )
        .build()?;

    let x2y1 = sphere()
        .transform(translation(2., 1., 0.) * sphere_size)
        .material(
            MaterialBuilder::default()
                .color(stripe_pattern(WHITE_PAT, BLACK_PAT).build()?)
                .diffuse(0.7)
                .specular(0.3)
                .build()?,
        )
        .build()?;

    let x3y1 = sphere()
        .transform(translation(3., 1., 0.) * sphere_size)
        .material(
            MaterialBuilder::default()
                .color(
                    stripe_pattern(WHITE_PAT, BLACK_PAT)
                        .transform(scaling(0.2, 1., 1.))
                        .build()?,
                )
                .diffuse(0.7)
                .specular(0.0)
                .build()?,
        )
        .build()?;

    let x1y2 = sphere()
        .transform(translation(1., 2., 0.) * sphere_size)
        .material(
            MaterialBuilder::default()
                .color(test_pattern().build()?)
                .diffuse(0.7)
                .specular(0.3)
                .build()?,
        )
        .build()?;

    let x2y2 = sphere()
        .transform(translation(2., 2., 0.) * sphere_size)
        .material(
            MaterialBuilder::default()
                .color(
                    ring_pattern(WHITE_PAT, BLACK_PAT)
                        .transform(scaling(0.2, 0.2, 0.2))
                        .build()?,
                )
                .diffuse(0.7)
                .specular(0.3)
                .build()?,
        )
        .build()?;

    let x3y2 = sphere()
        .transform(translation(3., 2., 0.) * sphere_size)
        .material(
            MaterialBuilder::default()
                .color(
                    checkers_pattern(WHITE_PAT, BLACK_PAT)
                        .transform(scaling(0.5, 0.5, 0.5))
                        .build()?,
                )
                .diffuse(0.7)
                .specular(0.3)
                .build()?,
        )
        .build()?;

    let x1y2z1 = sphere()
        .transform(translation(1., 2., -1.) * sphere_size)
        .material(
            MaterialBuilder::default()
                .color([0.8, 0.2, 0.2])
                .diffuse(0.7)
                .specular(0.3)
                .transparency(0.5)
                .build()?,
        )
        .build()?;

    let x2y2z1 = sphere()
        .transform(translation(2., 2., -1.) * sphere_size)
        .material(
            MaterialBuilder::default()
                .color([0.8, 0.2, 0.2])
                .diffuse(0.7)
                .specular(0.3)
                .transparency(0.9)
                .refractive_index(1.5)
                .build()?,
        )
        .build()?;

    let x2y4z1 = sphere()
        .transform(translation(2., 4., 0.) * sphere_size)
        .material(
            MaterialBuilder::default()
                .color([0.2, 0.2, 0.8])
                .diffuse(0.7)
                .specular(0.3)
                .build()?,
        )
        .build()?;

    let world = WorldBuilder::default()
        .lights(vec![light1, light2, light3])
        .objects(vec![
            floor, x1y1, x2y1, x3y1, x1y2, x2y2, x3y2, x1y2z1, x2y2z1, x2y4z1,
        ])
        .build()?;

    let image = camera.render(&world);

    let path = "/tmp/eoc11.png";
    println!("saving output to {}", path);
    image.write_to_file(path)?;
    println!("Render time {:.3} seconds", start.elapsed().as_secs_f32());
    Ok(())
}
