
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <cfloat>

#include "vec.h"
#include "mat.h"
#include "image.h"
#include "image_io.h"
#include "orbiter.h"
#include "ray.h"

//! representation d'une matiere
struct Material
{
    Color diffuse;
    Color specular;
    float ir;           //!< indice de refraction
    float ns;           //!< exposant blinn phong
    float kd;           //!< ks * diffuse + ks * speculaire, kd + ks == 1
    float ks;
    bool reflect;       //!< specualr reflect
    bool refract;       //!< specualr refract
    
    Material( ) : diffuse(make_white()), specular(make_black()), ir(1.f), ns(0.f), kd(1.f), ks(0.f), reflect(false), refract(false) {}
};

Material make_diffuse( const Color& color )
{
    Material m;
    m.diffuse= color;
    m.kd= 1;
    m.ks= 0;
    return m;
}

Material make_mirror( const Color& color, const float ir )
{
    Material m;
    m.diffuse= make_color(1, 1, 1);
    m.specular= color;
    m.kd= 0;
    m.ks= 1;
    m.ir= ir;
    m.reflect= true;
    m.reflect= false;
    return m;
}

Material make_glass( const Color& color, const float ir )
{
    Material m;
    m.diffuse= make_color(1, 0, 1);
    m.specular= color;
    m.kd= 0;
    m.ks= 1;
    m.ir= ir;
    m.reflect= true;
    m.refract= true;
    return m;
}


//! representation d'une sphere.
struct Sphere
{
    Material material;
    Transform model;
    Point center;
    float radius;  
    
    Sphere( const Point& c, const float r, const Color& cc ) : material(make_diffuse(cc)), model(make_identity()), center(c), radius(r) {}
    Sphere( const Point& c, const float r, const Material& m ) : material(m), model(make_identity()), center(c), radius(r) {}
};

//! intersection sphere / rayon. renvoie vrai si une intersection *valide* existe. 
bool intersect( const Sphere& sphere, const Ray& ray, float& hit )
{
    Point center= transform(sphere.model, sphere.center);
    Vector oc= ray.origin - center;
    float b= dot(oc, ray.direction);
    float c= dot(oc, oc) - sphere.radius*sphere.radius;
    // float d= dot(ray.direction, ray.direction) == 1, ray.direction == 1
    float h= b*b - c;
    if(h < 0.0) return false;
    
    h= sqrt(h);
    float t1= (-b - h);
    
/*  solution classique avec les 2 racines :
 */
    float t2= (-b + h);
    if(t1 <= 0 && t2 <= 0)
        return false;
    
    else if(t1 > 0 && t2 > 0)
        t1= std::min(t1, t2); // plus petite racine postive
    else 
        t1= std::max(t1, t2); // racine positive
    
    // intersection valide ? 
    if(t1 < hit)
    {
        hit= t1;
        return true;
    }
    else
        return false;
    
/*  solution courte : si l'origine du rayon est a l'exterieur de la sphere... 
    if(t1 < 0)
        return false;
    else if(t1 < hit)
    {
        hit= t1;
        return true;
    }
    else
        return false;
 */
}

//! representation d'un plan. point + normale.
struct Plane
{
    Material material;
    Point anchor;
    Vector normal;
    
    Plane( const Point& a, const Vector& n, const Color& c= make_white() ) : material(make_diffuse(c)), anchor(a), normal(n) {}
    Plane( const Point& a, const Vector& n, const Material& m ) : material(m), anchor(a), normal(n) {}
};

//! intersection rayon / plan. renvoie vrai si une intersection *valide* existe. 
bool intersect( const Plane& plane, const Ray& ray, float &hit )
{
    float t= dot(plane.anchor - ray.origin, plane.normal) / dot(ray.direction, plane.normal);
    if(t < 0) return false;
    if(t < hit)
    {
        hit= t;
        return true;
    }
    else
        return false;
}


//! representation d'une "scene", un ensemble de spheres et de plans...
std::vector<Plane> planes;
std::vector<Sphere> spheres;

//! intersection rayon / scene. meme convention que les autres fonctions d'intersection, renvoie egalement le point et la normale, si necessaire.
bool intersect( const Ray& ray, const float tmax, Hit& hit, Material& m )
{
    hit.hit= false;
    float t= tmax;
    for(unsigned int i= 0; i < (unsigned int) planes.size(); i++)
        if(intersect(planes[i], ray, t))
        {
            hit.t= t;
            hit.p= ray.origin + t * ray.direction;
            hit.n= planes[i].normal;
            hit.color= planes[i].material.diffuse;
            hit.hit= true;
            
            // damier
            Vector p= make_vector(planes[i].anchor, hit.p);
            int x= std::abs(std::floor(p.x));
            int y= std::abs(std::floor(p.y));
            int z= std::abs(std::floor(p.z));
            Color color= ((x%2) ^ (z%2)) ? planes[i].material.diffuse : make_white();
            hit.color= color;
            m= make_diffuse(color);
            //~ m= make_mirror(color, 1.f);
        }
    
    for(unsigned int i= 0; i < (unsigned int) spheres.size(); i++)
        if(intersect(spheres[i], ray, t))
        {
            hit.t= t;
            hit.p= ray.origin + t * ray.direction;
            hit.n= normalize(make_vector(transform(spheres[i].model, spheres[i].center), hit.p));
            m= spheres[i].material;
            hit.hit= true;
        }

    return (t < tmax);
}

float shadow( const Point& p, const Vector& n, const Point& l )
{
    Ray ray= make_ray(p + .001f * n, l);
    Hit hit; 
    Material m;
    intersect(ray, distance(p + .001f * n, l), hit, m);
    
    return hit.hit ? 0.f : 1.f; // s'il y a une intersection p et q ne se voient pas...
}


int main( int argc, char **argv )
{
    srand48(0);
    
    // creer quelques objets
    planes.push_back( Plane(make_point(0, 0, 0), normalize(make_vector(0, 1, 0)), make_red()) );
    planes.push_back( Plane(make_point(0, 0, 16), normalize(make_vector(0, 0, -1)), make_red()) );
    planes.push_back( Plane(make_point(0, 0, -16), normalize(make_vector(0, 0, 1)), make_white()) );
    
    spheres.push_back( Sphere(make_point(0, 0, 0), 1, make_white()) );
    //~ spheres.push_back( Sphere(make_point(0, 0, 0), 1, make_mirror(make_white(), 1.5f)) );
    spheres.push_back( Sphere(make_point(1, 0, 0), 0.5f, make_red()) );
    spheres.push_back( Sphere(make_point(0, 1, 0), 0.5f, make_green()) );
    spheres.push_back( Sphere(make_point(0, 0, 1), 0.5f, make_blue()) );

    // et quelques spheres miroir placees aleatoirement
    for(int i= 0; i < 10; i++)
    {
        float x= drand48() * 20 - 10;
        float y= drand48() * 5 + 2;
        float z= drand48() * 20 - 10;
        float r= drand48() * 2 + .25f;
        spheres.push_back( Sphere(make_point(x, y, z), r, make_glass(make_white(), 1.3f)) );
    }
    
    // et une source de lumiere
    Point light= make_point(10, 20, -2);
    Color emission= make_white() * 500;
    
    // reculer la camera pour observer les objets
    Orbiter camera= make_orbiter_lookat( make_point(0, 0, 0), 15 );
    orbiter_rotation(camera, 0, 45);
    
    // go
    Image image(1024, 640);
    
    Point d0;
    Vector dx, dy;
    Point o= orbiter_position(camera);
    orbiter_image_frame(camera, image.width(), image.height(), 0, 45, d0, dx, dy);
    
#pragma omp parallel for schedule(dynamic, 16)
    for(int y= 0; y < image.height(); y++)
    for(int x= 0; x < image.width(); x++)
    {
        Color pixel= make_black();
        
        const int samples= 16;
        for(int s= 0; s < samples; s++)
        {
            Point e= d0 + ((float) x + drand48() - 0.5f) * dx + ((float) y + drand48() - 0.5f) * dy;
            Ray ray= make_ray(o, e);
            
            Hit hit; 
            Material m;
            if(intersect(ray, FLT_MAX, hit, m))
            {
                Color color= make_black();
                
                if(m.reflect)
                {
                    Ray mray= reflect(ray, hit.p, hit.n);
                    Hit mhit;
                    Material mm;
                    if(intersect(mray, FLT_MAX, mhit, mm))
                    {
                        // reflexion miroir
                        float cos_theta= std::max( 0.f, dot(normalize(make_vector(mhit.p, light)), mhit.n));
                        color= color + m.specular * fresnel(ray, hit.p, hit.n, m.ir) * emission / distance2(mhit.p, light)  * mm.diffuse * shadow(mhit.p, mhit.n, light) * cos_theta;
                    }
                    else
                        color= color + m.specular * fresnel(ray, hit.p, hit.n, m.ir) * make_color(1, 1, 0) *2;
                }
                
                if(m.refract)
                {
                    // entree dans l'objet
                    if(fresnel_refract(ray, hit.p, hit.n, m.ir))
                    {
                        Ray iray= refract(ray, hit.p, hit.n, m.ir);
                        Hit ihit; 
                        Material im;
                        if(intersect(iray, FLT_MAX, ihit, im))
                        {
                            // sortie de l'objet
                            if(fresnel_refract(iray, ihit.p, ihit.n, im.ir))
                            {
                                Ray oray= refract(iray, ihit.p, ihit.n, im.ir);
                                Hit ohit; 
                                Material om;
                                if(intersect(oray, FLT_MAX, ohit, om))
                                {
                                    // transparence
                                    float cos_theta= std::max( 0.f, dot(normalize(make_vector(ohit.p, light)), ohit.n));
                                    color= color + m.specular * (1.f - fresnel(ray, hit.p, hit.n, m.ir)) * im.specular * (1.f - fresnel(iray, ihit.p, ihit.n, im.ir))
                                        * emission / distance2(ohit.p, light) * om.diffuse * shadow(ohit.p, ohit.n, light) * cos_theta;
                                }
                            }
                        }
                    }
                }
                
                else
                {
                    // diffus
                    float cos_theta= std::max( 0.f, dot(normalize(make_vector(hit.p, light)), hit.n));
                    color= emission / distance2(hit.p, light) * m.diffuse * shadow(hit.p, hit.n, light) * cos_theta;
                }
                
                pixel= pixel + color;
            }
        }
        
        image(x, y)= Color(pixel / (float) samples, 1);
    }
    
    write_image(image, "render.png");
    return 0;
}
