/* daveray.cpp */

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

#define frand(a) (float(rand()) * float(a) / float(RAND_MAX))

int samps, rsamps, ssamps;
float anim;
int intersects;

typedef struct {
   char id_len, map_type, img_type;
   int  map_first, map_len;
   char map_entry_size;
   int  x, y, width, height;
   char bpp, misc;
} targa_header;

void set_tgaheader (targa_header &header, int wd, int ht){
  header.id_len = 0;          /* no ID field */
  header.map_type = 0;        /* no colormap */
  header.img_type = 2;        /* trust me */
  header.map_first = 0;       /* not used */
  header.map_len = 0;         /* not used */
  header.map_entry_size = 0;  /* not used */
  header.x = 0;               /* image starts at (0,0) */
  header.y = 0;
  header.width = wd;         /* image is 256x128 */
  header.height = ht;
  header.bpp = 24;            /* 24 bits per pixel */
  header.misc = 0x20;         /* scan from upper left corner */
}

void writeheader(targa_header h, FILE *tga){
   fputc(h.id_len, tga);          // Write chars for ID, map, and image type
   fputc(h.map_type, tga);
   fputc(h.img_type, tga);
   fputc(h.map_first % 256, tga); // Write integer as two bytes, low order first
   fputc(h.map_first / 256, tga); // Write second byte of integer, high order next
   fputc(h.map_len % 256, tga);   // Another integer 
   fputc(h.map_len / 256, tga);
   fputc(h.map_entry_size, tga);  // Write a char - only one byte
   fputc(h.x % 256, tga);         // More integers
   fputc(h.x / 256, tga);
   fputc(h.y % 256, tga);
   fputc(h.y / 256, tga);
   fputc(h.width % 256, tga);     // Even more integers
   fputc(h.width / 256, tga);
   fputc(h.height % 256, tga);
   fputc(h.height / 256, tga);
   fputc(h.bpp, tga);             // Write two chars
   fputc(h.misc, tga);
}

struct vector{
  float x, y, z;
  vector(){};
  vector(float _x, float _y, float _z) : x(_x), y(_y), z(_z) {}
  void printout() { printf("Vector is: <%f,%f,%f>\n", x, y, z); };
  void operator+=(vector& v) { x+=v.x; y+=v.y; z+=v.z; }
  void operator-=(vector& v) { x-=v.x; y-=v.y; z-=v.z; }
  void operator*=(float v) { x*=v; y*=v; z*=v; }
  void operator/=(float v) { x/=v; y/=v; z/=v; }
  
  vector operator+(vector& v){return vector(v.x+x, v.y+y, v.z+z);}
  vector operator-(vector& v){return vector(x-v.x, y-v.y, z-v.z);}
  vector operator*(float v){return vector(x*v, y*v, z*v);}
  vector operator/(float v){return vector(x/v, y/v, z/v);}
  
  float dot(vector &v){return v.x*x+v.y*y+v.z*z;}
  float mag(){return sqrt(x*x+y*y+z*z);}
  float mag2(){return x*x+y*y+z*z;}
  void norm(){
    float d=1.0/mag();
    x*=d; y*=d; z*=d;
  }
};

typedef vector v3f;
#include "tmatrix.h"
#include "ply.h"

vector cross(vector &a, vector &b){
  return vector(a.y*b.z-a.z*b.y, a.z*b.x-a.x*b.z, a.x*b.y-a.y*b.x);
}

struct color{
  float r, g, b;
  color(){};
  color(float _r, float _g, float _b) : r(_r), g(_g), b(_b) {};
  void operator+=(color &c) {r+=c.r; g+=c.g; b+=c.b;};
  color operator+(color &c) {return color(r+c.r, g+c.g, b+c.b);};
  color operator*(color &c) {return color(r*c.r, g*c.g, b*c.b);};
  color operator*(float i) {return color(r*i, g*i, b*i);};
};

void outcolor(FILE* outfile, color &c){
  int r = c.r*256.0;
  int g = c.g*256.0;
  int b = c.b*256.0;
  if (r<0) r=0;
  if (r>255) r=255;
  if (g<0) g=0;
  if (g>255) g=255;
  if (b<0) g=0;
  if (b>255) b=255;
  fputc(char(b), outfile);
  fputc(char(g), outfile);
  fputc(char(r), outfile);
}

void rotate(vector &v, vector& rot){
}

struct camera {
  vector pos;
  vector forward;
  vector up;
  vector right;
  camera(){
    vector position(0,0,0);
    vector look_at(0,0,5);
    camera(position, look_at);
  }
  void setpos(vector &p, vector &l){
    pos = p;
    forward = l-p;
    forward.norm();
    vector down(0,-1,0);
    right=cross(forward, down);
    right.norm();
    right *= 0.6;
    up=cross(forward, right);
    up.norm();
    up *= 0.6;
    
  }
  camera(vector &p, vector &l){
    setpos(p,l);
  }
  
};

struct light {
  vector position;
  color c;
  float radius;
  light(){
    position = vector(0,5,0);
    c = color(1,1,1);
    radius = 0.25;
  };
  light(vector &r, color &col){
    position = r;
    c = col;
    radius = 0.25;
  }
  light(vector &r, color &col, float _radius){
    radius = _radius;
    position = r;
    c = col;
  }
};

struct shader {
  virtual color diffusecolor(vector &r){
    return color(1, 1, 1);
  }
  virtual color specularcolor(vector &r){
    return color(0, 0, 0);
  }
  virtual float getreflect(vector &r){
    return 0;
  }
  virtual float getroughness(){
    return 150;
  }
  virtual ~shader() {};
};

struct chrome : shader{
  virtual color diffusecolor(vector &r){
    return color (0.0,0.0,0.0);
  }
  virtual color specularcolor(vector &r){
    return color(1, 1, 1);
  }
  virtual float getreflect(vector &r){
    return 0.6;
  }
};

struct shiny : shader{
  virtual color diffusecolor(vector &r){
    return color (1,1,1);
  }
  virtual color specularcolor(vector &r){
    return color(.5, .5, .5);
  }
  virtual float getreflect(vector &r){
    return 0.2;
  }
  virtual float getroughness(){
    return 50;
  }
};

struct matte : shader{
};

struct halfhalf : shader{
  virtual color diffusecolor(vector &r){
    return color (.7,.7,.7);
  }
  virtual float getreflect(vector &r){
    return 0.3;
  }
  virtual color specularcolor(vector &r){
    return color(1, 1, 1);
  }
  
};

struct checker : shader{
  virtual color diffusecolor(vector &r){
    if ( (int(r.z)+int(r.x)+(r.x<0)+(r.z<0))&1 )
      return color (1,1,1);
    else
      return color (0,0,0);
  }
  virtual float getreflect(vector &r){
    if ( (int(r.z)+int(r.x)+(r.x<0)+(r.z<0))&1 )
      return 0;
    else
      return 0.7;
  }
  virtual color specularcolor(vector &r){
    return color(1, 1, 1);
  }
};

struct obj {
  shader *surfprop;
  obj(shader* _surfprop=0){
    surfprop = _surfprop;
  };
  virtual ~obj() {
    if (surfprop) delete surfprop;
  };
  virtual float rayhit(vector &p, vector &d){
    return 0;
  }
  virtual vector getnormal(vector &r){
    return vector(0,0,0);
  }
};

struct plane : obj{
  vector n;
  float c;
  plane(){
    n = vector(0,1,0);
    c = 0;
    surfprop = new checker();
  }
  plane(vector &a, float b, shader * _surfprop=0) : obj(_surfprop) {
    n = a;
    n.norm();
    c = b;
  }
  float rayhit(vector &p, vector &d){
    intersects++;
    if (n.dot(d)>0)
      return 0;
    else
      return -(n.dot(p)+c) / (n.dot(d));
  }
  vector getnormal(vector &r){
    return n;
  }
};

struct sphere : obj{
  vector center;
  float r;
  sphere(){
    center = vector(0,0,0);
    r = 1;
    surfprop = new chrome();
  }
  sphere(vector &a, float b, shader* _surfprop=0) : obj(_surfprop) {
    center = a;
    r = b;
  }
  float rayhit(vector &p, vector &d){
    intersects++;
    vector eo = center-p;
    float v = eo.dot(d);
    if (v<0) return 0;
    float disc = r*r - ((eo.dot(eo)) - v*v);
    if (disc<0) return 0;
    else{
      float d = sqrt(disc);
      return v-d;
    }
  }
  vector getnormal(vector &pos){
    vector foo;
    foo=pos-center;
    foo.norm();
    return foo;
  }
};

struct polymodel : obj {
  ply_file* mdl;
  obj *bsphere;
  polymodel(char* fname, shader *_shader) : obj(_shader), mdl(0), bsphere(0) {
    FILE *f=fopen(fname,"rb");
    if (!f) return;
    fseek(f,0,SEEK_END);
    int len=ftell(f);
    fseek(f,0,SEEK_SET);
    mdl=(ply_file*)new char[len];
    fread(mdl,len,1,f);
    fclose(f);
//    printf("%s %d.%d\n",mdl->header,mdl->hi_ver,mdl->lo_ver);
//    printf("ALMOST DONE!!!\n");
//    printf("Model %s: Total size %d, model offset %d\n",fname,len,mdl->offset_models);
    bsphere = new sphere(vector(0, 0, 0), mdl->model().bound_sphere/10.0);
  };
  float rayhit(vector &p, vector &d){
    intersects++;
    return bsphere?bsphere->rayhit(p,d):0.0;
  }
  vector getnormal(vector &pos){
    return bsphere?bsphere->getnormal(pos):vector(1,0,0);
  }  
  
  ~polymodel() {
    if (mdl) delete mdl;
  };
};

struct scene{
  int stat_rays;
  int stat_shadows;

  int width, height;
  camera cam;
  obj *objs[1000];
  light *lights[100];
  int numobjects, numlights;
  
  scene(){
    stat_rays=0;
    stat_shadows=0;
    width=320;
    height=240;
    numobjects=0;
    numlights=0;
  }
  void operator += (obj* o) {
    objs[numobjects++]=o;
  };
  void operator += (light* l) {
    lights[numlights++]=l;
  };
  ~scene() {
    for(int o=0;o<numobjects;o++)
      delete objs[o];
    for(int l=0;l<numlights;l++)
      delete lights[l];
  };
  void sceneinfo(){
    printf("\n  SCENE\n");
    printf("Using %d objects\n", numobjects);
    printf("Using %d lights\n", numlights);
    printf("\n  RENDERER\n");
    printf("Using %d sample(s) per pixel\n", samps);
    printf("Using %d shadow sample(s) per ray\n", ssamps);
    printf("Using %d reflection sample(s) per ray\n", rsamps);
  }
  void after_stats(){
    printf("\n  STATS\n");
    printf("Did %d rays\n", stat_rays);
    printf("Did %d shadow checks\n", stat_shadows);
    printf("Total of %d intersect tests\n", intersects);
  }
  
  color getlocalphong(obj &o, vector &position, vector &normal, vector& rd){
    color dif = o.surfprop->diffusecolor(position);
    color spec = o.surfprop->specularcolor(position);
    color lcolor(0,0,0);
    color scolor(0,0,0);
    
    for (int i=0;i<numlights;i++){
      float ins=0;
      vector jitter, lv, ldis;
      for (int jn=0;jn<ssamps;jn++){
        if (ssamps>1){
          jitter=vector(float(rand())/RAND_MAX-0.5, 
                      float(rand())/RAND_MAX-0.5, 
                      float(rand())/RAND_MAX-0.5);
          jitter *= lights[i]->radius/jitter.mag();
        }
        else jitter = vector(0,0,0);
        lv = lights[i]->position + jitter - position;
        ldis = lv;
        lv.norm();
        float feeler = rayfeel(position, lv); // check shadow
        if ((feeler*feeler>ldis.mag2())||(feeler==0)) ins += 1;
      }
      ins /= ssamps;
      if (ins>0){
        lv = lights[i]->position - position;
        ldis = lv;
        lv.norm();
        float il = lv.dot(normal);
        if (il>0){
          lcolor += lights[i]->c * il * ins;
        }
        rd.norm();
        float sl = lv.dot(rd);
        if (sl>0){
          sl = sl;
          sl = pow(sl,o.surfprop->getroughness());
          scolor += lights[i]->c * sl * ins;
        }
      }
    }
    return (dif*lcolor)+(spec*scolor);
  }

  float rayfeel(vector &p, vector &d){
    int objhit=-1;
    float minz=1E30, currz;
    
    stat_shadows++;
    
    for (int o=0;o<numobjects;o++){
      currz = objs[o]->rayhit(p, d);
      if ((currz != 0) && (currz < minz)){
        minz = currz;
        objhit = o;
      }
    }

    if (objhit!=-1)
      return minz;
    else
      return 0;
  }

  color traceray(vector &p, vector &d, int depth){
    if (depth>4) return color(0.5,0.5,0.5);
    int objhit=-1;
    float minz=1E30, currz;
    color objcolor;
    
    stat_rays++;
    
    for (int o=0;o<numobjects;o++){
      currz = objs[o]->rayhit(p, d);
      if ((currz != 0) && (currz < minz) && (currz > 0.01)){
        objhit = o;
        minz = currz;
      }
    }

    if (objhit!=-1){
      color total(0,0,0);
      vector ipoint = p + (d*minz);
      vector norm = objs[objhit]->getnormal(ipoint);
      vector rd;
      rd = d-(norm*norm.dot(d)*2);
      total += getlocalphong(*objs[objhit], ipoint, norm, rd);
      float rfl = objs[objhit]->surfprop->getreflect(ipoint);
      if (rfl){
        color rtotal(0,0,0);
        for (int foo=0;foo<rsamps;foo++){
          vector prd = rd;
          if (rsamps>1){
            prd += vector(float(rand())/RAND_MAX*0.1, float(rand())/RAND_MAX*0.1, float(rand())/RAND_MAX*0.1) + vector(-0.05,-0.05,-0.05);
            prd.norm();
          }
          rtotal += traceray(ipoint,prd, depth+1) * rfl;
        }
        total += rtotal * (1.0 / rsamps);
      }
      return total;
    }
    else
      return color(0,0,0);
  }
  void render(FILE *outfile){
    printf("\n  RENDERING at %dx%d\n", width, height);
    color col;
    int wsize = (width + height) / 2;
    
    for (int j=0;j<height;j++){
      printf("%d ", height-j);
      fflush(stdout);
      for (int i=0;i<width;i++){
        
        float up_fac=-float(j-(height>>1))/wsize*2;
        float right_fac=float(i-(width>>1))/wsize*2;
        
        vector direction;
        direction = cam.forward;
        direction += cam.up*up_fac;
        direction += cam.right*right_fac;
        
        col = color(0,0,0);
        vector newdir;
        for (int foo=0;foo<samps;foo++){
          newdir = direction;
          if (samps>1){
            newdir += cam.up * (2.2/wsize * float(rand()) / RAND_MAX);
            newdir += cam.right * (2.2/wsize * float(rand()) / RAND_MAX);
          }
          newdir.norm();
          col += traceray(cam.pos, newdir, 0);
        }
        float oos = 1.0/samps;
        col = col*oos;
        outcolor(outfile, col); 
      }
    }
    after_stats();
  }
};

scene mainscene;

void ssetup(scene& s){
  float li=0.6;
  s.cam.setpos(vector(3,1.5,4), vector(-.5,.5,0));
    
  s += new plane(vector(0,1,0), 0, new checker);
  s += new sphere(vector(0,1,0), 1, new shiny);
  s += new sphere(vector(-1,.5,1.5), 0.5, new shiny);

  s += new light(vector(-2, 2.5, 0), color(.7, .6, .3)*li );
  s += new light(vector(1.5, 2.5, 1.5), color(.45, .5, .6)*li );
  s += new light(vector(1.5, 2.5, -1.5), color(.7, .8, .7)*li );
  s += new light(vector(0, 3.5, 0), color(.3, .3, .3)*li );

//  s += new polymodel("BIRD.PLY", new halfhalf);
//  printf("DONE!!!\n");
}
  
 
int main( int argc, char **argv){  
  samps = 1;
  rsamps = 1;
  ssamps = 1;
  intersects = 0;
  char* fn=0;
  anim=0;
  
  for (int i=1;i<argc;i++){
    if (argv[i][1]=='a'){
      samps=atoi(&argv[i][2]); 
    }
    if (argv[i][1]=='w'){
      mainscene.width=atoi(&argv[i][2]); 
    }
    if (argv[i][1]=='h'){
      mainscene.height=atoi(&argv[i][2]); 
    }
    if (argv[i][1]=='r'){
      rsamps=atoi(&argv[i][2]);
    }
    if (argv[i][1]=='s'){
      ssamps=atoi(&argv[i][2]);
    }
    if (argv[i][1]=='k'){
      anim=float(atoi(&argv[i][2]));
    }
    if (argv[i][1]=='o'){
      fn=&argv[i][2];
    }
  }
  FILE *tga;               
  targa_header header;     
  set_tgaheader(header, mainscene.width, mainscene.height);
  if (fn==0)
    tga = fopen("dr_out.tga", "wb");
  else
    tga = fopen(fn, "wb");
    
  writeheader(header, tga);  
  
  printf(" |================| \n");
  printf(" |  DAVERAY V1.0  | \n");
  printf(" |================| \n");
  
  ssetup(mainscene);
  mainscene.sceneinfo();
  mainscene.render(tga);

  fclose(tga);
  
  return 0;
}
