// elements.rs

use crate::renderer::Ray;

use std::ops::{Add,Mul};
use nalgebra::*;
//use image::{DynamicImage,GenericImage,Pixel,Rgba};
use image::*;

// Gamma functions
const GAMMA: f32 = 2.2;

fn gamma_decode(encoded: f32) -> f32 {
    encoded.powf(GAMMA)
}

// Materials
pub struct Material {
  pub coloration: Coloration,
  pub albedo: f32,
  pub surface: SurfaceType
}

impl Material {
  pub fn new(coloration: Coloration, albedo: f32, surface: SurfaceType) -> Material {
    Material {
      coloration: coloration,
      albedo: albedo,
      surface: surface
    }
  }
}

#[derive(Copy, Clone)]
pub struct Color {
  pub red: f32,
  pub green: f32,
  pub blue: f32,
}

impl Color {
  pub fn new(red: f32, green: f32, blue: f32) -> Color {
    Color {
      red: red,
      green: green,
      blue: blue
    }
  }

  pub fn from_rgba(rgba: Rgba<u8>) -> Color {
      Color {
          red: rgba.0[0] as f32,//gamma_decode((rgba.0[0] as f32)),
          green: rgba.0[1] as f32,//gamma_decode((rgba.0[1] as f32)),
          blue: rgba.0[2] as f32,//gamma_decode((rgba.0[2] as f32)),
      }
  }
}

impl Mul for Color {
  type Output = Color;

  fn mul(self, other: Color) -> Color {
    Color {
      red: self.red * other.red,
      green: self.green * other.green,
      blue: self.blue * other.blue,
    }
  }
}

impl Mul<f32> for Color {
  type Output = Color;

  fn mul(self, other: f32) -> Color {
    Color {
      red: self.red * other,
      green: self.green * other,
      blue: self.blue * other,
    }
  }
}

impl Add for Color {
  type Output = Color;

  fn add(self, other: Color) -> Color {
    Color {
      red: self.red + other.red,
      green: self.green + other.green,
      blue: self.blue + other.blue,
    }
  }
}

impl Mul<Color> for f32 {
  type Output = Color;

  fn mul(self, other: Color) -> Color {
    other * self
  }
}

pub enum Coloration {
    Color(Color),
    Texture(Texture)
}

impl Coloration {
    pub fn color(&self, coords: &TextureCoords) -> Color {
        match *self {
            Coloration::Color(ref c) => c.clone(),
            Coloration::Texture(ref texture) => {
                let tex_x = wrap(coords.x, texture.texture.width());
                let tex_y = wrap(coords.y, texture.texture.height());

                Color::from_rgba((&texture.texture).get_pixel(tex_x, tex_y))
            }
        }
    }

    pub fn height(&self, coords: &TextureCoords) -> f32 {
        match *self {
            Coloration::Color(ref c) => {
                1.0
            },
            Coloration::Texture(ref texture) => {
                let delta_u = (1.0 / texture.heightmap.width() as f32) * 0.5;

                let delta_height = texture.get_height(coords.x + delta_u, coords.y);
                let height = texture.get_height(coords.x, coords.y);
                
                //println!("delta_height : height : {} : {}", delta_height, height);

                let result = (delta_height - height) / delta_u;

                //result

                (height - 100.0) / 60.0
                //(delta_height - height) + 1.0
    
                //Color::from_rgba(texture.heightmap.get_pixel(tex_x, tex_y))
            }
        }
    }
}

// T)extures
#[derive(Clone)]
pub struct Texture {
    pub texture: DynamicImage,
    pub heightmap: DynamicImage,
}

impl Texture {
    // TODO: Might need to take more channels into account
    pub fn get_height(&self, x: f32, y: f32) -> f32 {
        let tex_x = wrap(x, self.heightmap.width());
        let tex_y = wrap(y, self.heightmap.height());

        self.heightmap.get_pixel(tex_x, tex_y).0[0] as f32
    }
}

pub fn dummy_texture() -> DynamicImage {
    DynamicImage::new_rgb8(1, 1)
}

fn wrap(val: f32, bound: u32) -> u32 {
    let signed_bound = bound as i32;
    let float_coord = val * bound as f32;
    let wrapped_coord = (float_coord as i32) % signed_bound;
    if wrapped_coord < 0 {
        (wrapped_coord + signed_bound) as u32
    } else {
        wrapped_coord as u32
    }
}


pub struct TextureCoords {
    pub x: f32,
    pub y: f32,
}

pub enum SurfaceType {
  Diffuse,
  Reflective { reflectivity: f32 },
}

// Element root class
pub enum Element {
  Sphere(Sphere),
  Plane(Plane),
}

impl Element {
  pub fn pos(&self) -> Vec3<f64> {
    match *self {
      Element::Sphere(ref s) => s.pos,
      Element::Plane(ref p) => p.pos,
    }
  }

//  pub fn color(&self) -> &Color {
//    match *self {
//      Element::Sphere(ref s) => &s.material.coloration,
//      Element::Plane(ref p) => &p.material.coloration,
//    }
//  }

  pub fn normal(&self, pos: Vec3<f64>) -> Vec3<f64> {
    match *self {
      Element::Sphere(ref s) => pos - s.pos,
      Element::Plane(ref p) => -p.normal,
    }
  }

  pub fn material(&self) -> &Material {
      match *self {
          Element::Sphere(ref s) => &s.material,
          Element::Plane(ref p) => &p.material,
      }
  }
}

// Lights

pub struct LightSrc {
  pub pos: Vec3<f64>,
  pub intensity: f32,
}

impl LightSrc {
  pub fn new(pos: Vec3<f64>, intensity: f32) -> LightSrc {
    LightSrc {
      pos: pos,
      intensity: intensity
    }
  }

  pub fn distance(&self, hit_point: Vec3<f64>) -> f64 {
    let difference = self.pos - hit_point;
    difference.norm()
  }
}

// Specific Elements

pub trait Intersectable {
  fn intersect(&self, ray: &Ray) -> Option<f64>;

  //fn surface_normal(&self, hit_point: &Vec3<f64>) -> Vec3<f64>;
  fn texture_coords(&self, hit_point: &Vec3<f64>) -> TextureCoords;
}

impl Intersectable for Element {
  fn intersect(&self, ray: &Ray) -> Option<f64> {
    match *self {
      Element::Sphere(ref s) => s.intersect(ray),
      Element::Plane(ref p) => p.intersect(ray),
    }
  }

  fn texture_coords(&self, hit_point: &Vec3<f64>) -> TextureCoords {
      match *self {
          Element::Sphere(ref s) => s.texture_coords(hit_point),
          Element::Plane(ref p) => p.texture_coords(hit_point),
      }
  }
}


pub struct Sphere {
  pub pos: Vec3<f64>,
  pub radius: f64,
  pub material: Material,

}

impl Intersectable for Sphere {
  // Implemented from
  // http://kylehalladay.com/blog/tutorial/math/2013/12/24/Ray-Sphere-Intersection.html
  fn intersect(&self, ray: &Ray) -> Option<f64> {
    let l = self.pos - ray.pos;
    let adj = l.dot(&ray.dir);
    let d2 = l.dot(&l) - (adj * adj);
    let radius2 = self.radius * self.radius;

    if d2 > radius2 {
      return None;
    }

    let thc = (radius2 - d2).sqrt();
    let t0 = adj - thc;
    let t1 = adj + thc;

    if t0 < 0.0 && t1 < 0.0 {
      None
    } else if t0 < 0.0 {
      Some(t1)
    } else if t1 < 0.0 {
      Some(t0)
    } else {
      let distance = if t0 < t1 { t0 } else { t1 };
      Some(distance)
    }
  }

  fn texture_coords(&self, hit_point: &Vec3<f64>) -> TextureCoords {
    let hit_vec = *hit_point - self.pos;
    TextureCoords {
        x: (1.0 + (hit_vec.z.atan2(hit_vec.x) as f32) / std::f32::consts::PI) * 0.5,
        y: (hit_vec.y / self.radius).acos() as f32 / std::f32::consts::PI,
    }
  }

}

pub struct Plane {
  pub pos: Vec3<f64>,
  pub normal: Vec3<f64>,
  //color: Color,
  pub material: Material,
}

impl Intersectable for Plane {
  fn intersect(&self, ray: &Ray) -> Option<f64> {
    let normal = &self.normal;
    let denom = normal.dot(&ray.dir);
    if denom > 1e-6 {
      let v = self.pos - ray.pos;
      let distance = v.dot(&normal) / denom;
      if distance >= 0.0 {
        return Some(distance);
      }
    }
    None
  }

  // TODO: Implement this
  fn texture_coords(&self, hit_point: &Vec3<f64>) -> TextureCoords {
      let mut x_axis = self.normal.cross(&Vec3 {
          x: 0.0,
          y: 0.0,
          z: 1.0,
      });
      if x_axis.norm() == 0.0 {
          x_axis = self.normal.cross(&Vec3 {
              x: 0.0,
              y: 1.0,
              z: 0.0,
          });
      }
      let y_axis = self.normal.cross(&x_axis);
      let hit_vec = *hit_point - self.pos;

      TextureCoords {
          x: hit_vec.dot(&x_axis) as f32,
          y: hit_vec.dot(&y_axis) as f32,
      }
  }
}
