#include <stdlib.h>
#include <stdio.h>
#include <time.h>

//defines
#include "./src/defines.hpp"

#include "./src/bmp.hpp"

//raytracer engine
#include "./src/vec3.hpp"
#include "./src/ray.hpp"
#include "./src/camera.hpp"
#include "./src/color.hpp"

//geom primitives
#include "./src/object.hpp"
#include "./src/plane.hpp"
#include "./src/sphere.hpp"
#include "./src/box.hpp"
#include "./src/sphereflake.hpp"
#include "./src/material.hpp"

using namespace raytracer;

//const
static const int max_scene_obj= 10;
static const vec3 I= vec3(1.0f, 1.0f, 1.0f);
static const double pi= 3.14;

double rand_01(){ return ((double)rand()/(RAND_MAX)); };

object* intersect(const ray& r, double* wt, vec3* wn, material* wmat);
color radiance(const ray &r, int depth);

//SCENE
camera* sceneCamera;

plane* floorPlane;
sphereflake* sphereFlake;
sphere** spheres;
box* box;
object** sceneObjects;

color* pixelColors;

//ESSENTIALS
int imWidth= 800, imHeight= 600;
double aspectRatio= imWidth/(double)imHeight;

static const double ambient_add= 0.2;

//vectors
vec3 origin= vec3(0.0f, 0.0f, 0.0f)
    , X= vec3(1.0f, 0.0f, 0.0f)
    , Y= vec3(0.0f, 1.0f, 0.0f)
    , Z= vec3(0.0f, 0.0f, 1.0f)    
    //light
    , lPos= vec3(1.0, 2.0, 4.0)
    //cameras
    , cPos= vec3(2.9f, 0.5f, 1.2f)
    , cLookAt= origin;

//colors
color background(135/255.0f, 206/255.0f, 250/255.0f, 0.0f);//135-206-250
color white(1.0f, 1.0f, 1.0f, 0.0f);
color wheatMilk(242/255.0f, 218/255.0f, 145/255.0f, 0.0f);
color maroon(0.5f, 0.25f, 0.25f, 0.0f);
color orang(1.0f, 0.5f, 0.0f, 0.0f);
color blue(0.0f, 0.0f, 0.7f, 0.0f);
color green(0.9f, 0.9f, 0.9f, 0.0f);
color darkGreen(0.0f, 0.80f, 0.0f, 0.0f);

material floorMat(maroon, material::diffuse);
material whiteMat(white, material::diffuse);
material milkyMat(wheatMilk, material::diffuse);
material whiteLight(5.0f, white);//emissive
material reflOrang(orang, material::specular);
material reflBlue(blue, material::specular);
material refrGreen(darkGreen, material::refractive, 1.52);

int main()
{
  srand(time(0));
  pixelColors= new color[imWidth*imHeight];

  //lights and camera
  sceneCamera= new camera(cPos, Y, cLookAt);
  double uncertLen= 1/((double)((imWidth> imHeight)? imWidth: imHeight));

  //scene objects
  floorPlane= new plane(Y, -1, floorMat);
  spheres= new sphere*[4];
  spheres[0]= new sphere(vec3(-2.0f, 0.0f, 0.75f), 1.0f, reflOrang);
  spheres[1]= new sphere(vec3(0.0f, -0.65f, 0.25f), 0.35f, refrGreen);
  spheres[2]= new sphere(vec3(-1.0f, 1.2f, -0.65f), 0.2f, whiteLight);
  //light source
  //spheres[3]= new sphere(vec3(0.0f,0.5f,0.0f), 0.05f, whiteLight);
  spheres[4]= new sphere(vec3(-2.0f, 0.0f, -2.75f), 1, whiteMat);
  spheres[3]= new sphere(vec3(-1.0f, -0.6f, -0.65f), 0.4, reflBlue);
  //box= new Box(vec3(-4.0f, 0.5f, -0.25f), 3.0f, reflorang);
  sphereFlake= new sphereflake(vec3(-2.0f, 0.0f, -2.75f), Y, uncertLen, 1, whiteMat);
  
  sceneObjects= new object*[::max_scene_obj];
  //sceneObjects[1]= //spheres[4];//sphereFlake;
  sceneObjects[0]= floorPlane;
  sceneObjects[2]= spheres[0];
  sceneObjects[3]= spheres[1];
  sceneObjects[4]= spheres[2];
  sceneObjects[1]= sphereFlake;//spheres[4];
  sceneObjects[5]= spheres[3];
  //sceneObjects[5]= box;  

  float jitter[4 * 2] = {
    -1.0/4.0, -1.0/4.0,
    3.0/4.0, -1.0/4.0,
    -1.0/4.0, 3.0/4.0,
    3.0/4.0, 3.0/4.0,
  };

  int numOfSamples= 3;
  
  vec3 camRayOrigin= sceneCamera->pos();
  vec3 camDir= sceneCamera->dir();
  vec3 camRight= sceneCamera->right();
  vec3 camDown= sceneCamera->down();
  double bigDim= (imWidth> imHeight)? imWidth: imHeight;
  for (int j= 0; j < imHeight; j++)
  {
    for (int i= 0; i < imWidth; i++)
    {
      for (int k = 0; k < 4; k++)
      {
        color sampledCol;
        for (int s= 0; s < numOfSamples; s++) {
          double l1= rand_01()*0.5;
          double l2= rand_01()*0.5;
          vec3 offsetVec= vec3(
            2.0 * (i + jitter[2*k]+l1)/bigDim - 1.0,
            2.0 * (imHeight-j + jitter[2*k+1]+l2)/bigDim - 1.0,
            0);
          
          //construct camera ray
          vec3 camRayDirection= camDir.add(camRight.scale(offsetVec.x).add(camDown.scale(offsetVec.y)));
          
          ray camRay(camRayOrigin, camRayDirection.normalize());
          sampledCol= sampledCol.add(radiance(camRay, 0).scale(1/(double)numOfSamples));
        }
        pixelColors[imWidth*j+i]= pixelColors[imWidth*j+i].add(sampledCol.clamp().scale(1/(4.0f)));
      }
     //.scale(255);
    }
    system("clear");
    printf("P: %d \% \n", 100*(j*imWidth)/(imWidth*imHeight));
  }

  outputbmp("./test.bmp", imWidth, imHeight, 72, pixelColors);
  
  delete[] pixelColors, sceneObjects, spheres;
  delete sceneCamera, floorPlane, sphereFlake;
  return 0;
};

object* intersect(const ray& r,double* wt= NULL, vec3* wn= NULL, material* wmat= NULL)
{
  double t= -1;
  double tMin= -1;
  object* winObject= NULL;
  vec3 n;
  material mat;
  bool first= true;
  int oi= 0;
  while(sceneObjects[oi]!= NULL){
    bool inters= sceneObjects[oi]->intersects(r, t, n, mat);   
    if(inters && (first || t - tMin < ACCURACY)){
      winObject= sceneObjects[oi];
      tMin= t;
      if(wt!= NULL) *wt= tMin;
      if(wn!= NULL) *wn= n;
      if(wmat!= NULL) *wmat= mat;
      first= false;
    }
    oi++;
  }
  return winObject;
};

color radiance(const ray &r, int depth)
{
  double t;
  vec3 n, x, nl;
  material mat;

  //intersects a scene object?
  object* winObject= intersect(r, &t, &n, &mat);
  if (winObject== NULL) return background;

  //calc intersect world coord
  x= r.origin.add(r.direction.scale(t));
  //normal vector on the side on the ray
  nl= (n.dot(r.direction)< 0)? n: n.scale(-1);
  color& col= mat.material_color;
  color emmitIntensity= col.scale(mat.mult_emission);
  //max intensity
  double maxIntensity = (col.red> col.green && col.red> col.blue)? col.red : (col.green> col.blue)? col.green : col.blue; // max refl

  //break the path tracing, with probability= maxIntensity, when the depth is large enough
  if(++depth> 5)
  {
    if(rand_01()<0.5){col= col.scale(1.0f/maxIntensity);}
    else return emmitIntensity;
  }

  //return col;
  if(mat.material_type == material::diffuse){
    //printf("here;\n");
    //gen uvw basis, w = nl
    vec3 w= nl;
    vec3 u= ((fabs(w.x)>0.1f? vec3(0.0f, 1.0f, 0.0f): vec3(1.0f, 0.0f, 0.0f)).cross(w)).normalize();
    vec3 v= w.cross(u);
    
    color diffcol;
    
    //generate new random direction vec3, based on a hemisphere distribution
    double r1= 2*pi*rand_01();
    double r2= rand_01();
    double r2s= sqrt(r2);
    vec3 d = (u.scale(cos(r1)*r2s)//u
              .add(v.scale(sin(r1)*r2s))//v
              .add(w.scale(sqrt(1-r2))))//w
             .normalize();

    color e;
    //for (int i=0; i<numSpheres; i++){
    int h= 0;
    object* s= NULL;
    
    while((s = sceneObjects[h])!= NULL){
      if(s->object_material()->mult_emission<= ACCURACY){h++; continue;}
      const material* sMat= s->object_material();
      vec3 sw= s->pos().add(x.negative());
      vec3 su= ((fabs(sw.x)>.1?vec3(0.0f, 1.0f, 0.0f): vec3(1.0f, 0.0f, 0.0f)).cross(sw)).normalize();
      vec3 sv= sw.cross(su);
      double cos_a_max = sqrt(1-0.25/(sw.negative().dot(sw.negative())));
      double eps1= rand_01(), eps2 = rand_01();
      double cos_a = 1-eps1+eps1*cos_a_max;
      double sin_a= sqrt(1-cos_a*cos_a);
      double phi= 2*pi*eps2;
      vec3 l= su.scale(cos(phi)*sin_a).add( sv.scale(sin(phi)*sin_a)).add(sw.scale(cos_a)).normalize();

      if(intersect(ray(x, l)) == s)
      {
        double omega = 2*pi*(1-cos_a_max);
        e= sMat->material_color.add(col.mult(e.scale(l.dot(nl)*omega))).scale(1.0f/pi);
      }
      h++;
    }
    return emmitIntensity.add(e).add(col.mult(radiance(ray(x, d), depth)).clamp());
  }
  else if(mat.material_type == material::specular){
    vec3 reflDir= r.direction.reflect(n);
    return emmitIntensity.add(col.mult(radiance(ray(x, reflDir),depth)));
  }
  vec3 reflDir= r.direction.reflect(n);
  ray reflRay(x, reflDir);
  bool into = n.dot(nl)>0;
  double nc=1;
  double nt= mat.refract_index;
  double nnt= into? nc/nt: nt/nc;
  double ddn=r.direction.dot(nl);
  double cos2t= 1-nnt*nnt*(1-ddn*ddn);
  
  if (cos2t<0)//total internal reflection 
    return emmitIntensity.add(col.mult(radiance(reflRay, depth))); 

  vec3 tDir = (r.direction.scale(nnt).add(n.scale(((into?1:-1)*(ddn*nnt+sqrt(cos2t)))).scale(-1.0f))).normalize();
  
  double a= nt-nc;
  double b= nt+nc;
  double R0=a*a/(b*b);
  double c = 1-(into? -ddn: tDir.dot(n)); 
  double Re= R0+(1-R0)*c*c*c*c*c;
  double Tr= 1-Re;
  double P= 0.25+ 0.5*Re;
  double RP= Re/P;
  double TP=Tr/(1-P);
  if(depth> 2)
    return emmitIntensity.add(col.mult(
        rand_01()< P? radiance(reflRay, depth).scale(RP)
        : radiance(ray(x, tDir), depth).scale(TP)));
  else
    return emmitIntensity.add(col.mult(radiance(reflRay, depth).scale(Re).add(radiance(ray(x, tDir), depth).scale(Tr))));
}
