// renderer.rs

use std::f32;
use nalgebra::*;

use crate::camera::*;

use crate::elements::*;

const BUMPMAP_SCALE: f64 = 1.0;

const BLACK: Color = Color {
    red: 0.0,
    green: 0.0,
    blue: 0.0,
};

pub struct Ray {
  pub pos: Vec3<f64>,
  pub dir: Vec3<f64>
}

impl Ray {
  fn new(pos: Vec3<f64>, dir: Vec3<f64>) -> Ray {
    Ray {
      pos: pos,
      dir: dir
    }
  }

  fn at(&self, t: f64) -> Vec3<f64> {
    self.pos + t * self.dir
  }
}

pub struct Intersection<'a> {
  pub distance: f64,
  pub object: &'a Element
}

impl<'a> Intersection<'a> {
  pub fn new<'b>(distance: f64, object: &'b Element) -> Intersection<'b> {
    Intersection {
      distance: distance,
      object: & object
    }
  }
}


fn create_reflection(normal: Vec3<f64>, incident: Vec3<f64>, hit_point: Vec3<f64>, bias: f64) -> Ray {
    Ray {
        pos: hit_point + (normal.normalize()),
        dir: incident - (2.0 * incident.dot(&normal) * normal),
    }
}

fn get_color(camera: &PerspectiveCamera, ray: &Ray, intersection: &Intersection, depth: u32) -> Color {
    let hit_point = ray.pos + (ray.dir * intersection.distance);
    let texture_coords = intersection.object.texture_coords(&hit_point);
    //let material = intersection.object.material();
    //let heightmap_value = material.coloration.height(&texture_coords);

    //let surface_normal = intersection.object.normal(hit_point) * heightmap_value as f64;
    //let bump_hit_point = hit_point + surface_normal;
    let surface_normal = intersection.object.normal(hit_point);
    //println!("heightmap_value: {}", heightmap_value);

    let material = intersection.object.material();

    let mut color = shade_diffuse(camera, intersection.object, hit_point, surface_normal);

    if let SurfaceType::Reflective { reflectivity } = material.surface {
      let reflection_ray = create_reflection(surface_normal, ray.dir, hit_point, camera.shadow_bias);
      color = color * (1.0 - reflectivity);
      color = color + (cast_ray(&camera, &reflection_ray, depth + 1) * reflectivity);
    }
    color
}

fn shade_diffuse(camera: &PerspectiveCamera, object: &Element, hit_point: Vec3<f64>, surface_normal: Vec3<f64>) -> Color {
    let mut color = BLACK;

    // Light processing
    // TODO: Support multiple lights
    for light in camera.lights.iter() {
        let direction_to_light = light.pos - hit_point;
      
        let material = object.material();
        // TODO: Change light intensity to take hit_point for some reason (read source)
        // https://github.com/bheisler/raytracer/blob/7130556181de7fc59eaa29346f5d4134db3e720e/src/rendering.rs#L195
        let texture_coords = object.texture_coords(&hit_point);

        let heightmap = material.coloration.height(&texture_coords);

        // Shadow stuff
        let shadow_ray = Ray {
          pos: hit_point + surface_normal.normalize(),
          // Can be used for actual bumpy light calculations
          //pos: bump_point,
          dir: direction_to_light.normalize(),
        };

        let shadow_intersection = camera.trace(&shadow_ray);
        let in_light = shadow_intersection.is_none()
                    || shadow_intersection.unwrap().distance > light.distance(hit_point);
        let light_intensity = if in_light { light.intensity } else { 0.0 };

        let light_power = heightmap * (surface_normal.normalize().dot(&direction_to_light.normalize()) as f32).max(0.0);
        let light_reflected = material.albedo / f32::consts::PI;

        let light_color = light_intensity * light_power * light_reflected;
        color = color + (material.coloration.color(&texture_coords) * light_color);
    }

    return color;
}

pub fn cast_ray(camera: &PerspectiveCamera, ray: &Ray, depth: u32) -> Color {
    if depth >= camera.max_recursion_depth {
        return BLACK;
    }

    let intersection = camera.trace(&ray);
    intersection.map(|i| get_color(camera, &ray, &i, depth)).unwrap_or(BLACK)
}

