#include "raytracer.h"
#include <algorithm>
#include <cfloat>

//some helper functions
inline double clamp(double x){ return x<0 ? 0 : x>1 ? 1 : x; }


//convert a double between 0~1 to int between 0~255
inline int toInt(double x){ return int( clamp(x)*255 + .5); }


//constructor
RayTracer::RayTracer(list<object3D*>& models, float ambient_light[3], unsigned int light_sample_size) : m_models(models)
{
    //find out where lights are
    m_lights.clear();
    for (list<object3D*>::iterator i = models.begin(); i != models.end(); i++)
    {
        object3D* obj = *i;
        if (obj->mat_emission.normsqr() != 0) m_lights.push_back(*i);
    }

    if (m_lights.empty())
    {
        cerr << "! Error: There is no light defined. Make sure that at least one object has non-zero emission." << endl;
        exit(0);
    }

	//set ambient light
	m_ambient_light.set(ambient_light[0], ambient_light[1], ambient_light[2]);

	//sample points from lights
	sample(m_lights, light_sample_size);
}

inline void show_progress_bar(double ratio)
{
    // Show the load bar.
    static const int w = 50;
    int   c = (int)(ratio * w);

    cout << setw(3) << (int)(ratio * 100) << "% [";
    for (int x = 0; x<c; x++) cout << "=";
    for (int x = c; x<w; x++) cout << " ";
    cout << "]\r" << flush;
}

//render an image with "nray_per_pixel" rays created per pixel
void RayTracer::render(const Camera& cam, unsigned int img_w, unsigned int img_h, unsigned int nray_per_pixel, unsigned int max_depth)
{
    //create a black image of size img_w X img_h
    m_img = vector< vector< Vector3d > >(img_h, vector< Vector3d >(img_w, Vector3d(0,0,0) ));
    
	//ray control
	this->m_max_ray_depth = max_depth;

    //initialize the bar
    show_progress_bar(0);

    //generate rays
    for (unsigned int y = 0; y < img_h; y++)
    {

        for (unsigned int x = 0; x < img_w; x++)
        {
            Vector3d color;
            for (unsigned int n = 0; n < nray_per_pixel; n++)
            {
				Ray r = create_a_random_ray(cam, x, y);
                Vector3d rc = raycolor(r, 0);
                color = rc + color;
            }

            m_img[y][x] = color / nray_per_pixel;
        }//end of x

        show_progress_bar(y*1.0 / img_h);

    }//end of y
    show_progress_bar(1.0f);
    cout << endl;
}

// create a ray randomly in the given pixel (x,y)
Ray RayTracer::create_a_random_ray(const Camera& cam, unsigned int x, unsigned int y)
{
    Ray r;

    //TODO: implement this using settings in cam and (x,y)
    //hint: see slides on generating rays for perspective views
	int h = m_img.size(); //image height
	int w = m_img.front().size(); //image width

	double h_res = cam.viewport[3] * 1.0 / h;
	double w_res = cam.viewport[2] * 1.0 / w;

	double rnd_x = (x + drand48())*w_res;
	double rnd_y = (y + drand48())*h_res;
	rnd_y = cam.viewport[3] - rnd_y;

	//find camera local vectors
	const Vector3d & camera_v = cam.up; //up vector
	Vector3d camera_w = (cam.lookat - cam.pos).normalize();
	Vector3d camera_u = (camera_w%cam.up).normalize(); //right vector

	//get the world coorindates
	auto u = camera_u * (cam.viewport[0] + rnd_x);
	auto v = camera_v * (cam.viewport[1] + rnd_y);

    if (cam.type == Camera::ORTHOGONAL_CAM)
    {
        //setup an orthogonal cam
		r.o = cam.pos + (u + v) + camera_w*cam.near;
		r.v = camera_w;
    }
    else //if (cam.type == Camera::PERSPECTIVE_CAM)
    {
        //setup a perspective cam
		r.o = cam.pos;
		r.v = (u + v + camera_w*cam.near).normalize();
    }

    return r;
}



//
//returns true if the sphere intersects ray r
//return false if no intersection is found
//x is the location of intersection if true is returned.
//
bool RayTracer::intersect(sphere& s, Ray r, Point3d& x)
{
    //
    // TODO: implement this
    // 

	Vector3d vec = s.center - r.o;
	double a = r.v.normsqr();
	double b = -2 * (r.v*vec);
	double c = vec.normsqr() - s.radius*s.radius;

	double b2_4ac = b*b - 4 * a*c;
	if (b2_4ac < 0) return false;

	double sqrt_b2_4ac = sqrt(b2_4ac);
	double t1 = (-b - sqrt_b2_4ac) / (a * 2);
	double t2 = (-b + sqrt_b2_4ac) / (a * 2);

	if (t1 < 0 && t2<0) return false;

	double t = 0;

	if (t1 >= 0 && t2 >= 0)
	{
		if (t1 <= t2) t = t1;
		else t = t2;
	}
	else if (t1 >= 0)
	{
		t = t1;
	}
	else //t2>0
		t = t2;

	x = r.o + r.v*t;
	return true;
}


//returns a triangle in model m that intersect ray r
//return NULL if no intersection is found
triangle * RayTracer::intersect(model& m, Ray r)
{
    for (unsigned int i = 0; i < m.t_size; i++)
    {
        Point3d x;
        if ( intersect(m, &m.tris[i], r, x) )
            return &m.tris[i];
    }

    return NULL;
}


//returns a model and triangle that intersect ray r
//return pair<NULL,NULL> if no intersection is found
pair<object3D *, triangle *> RayTracer::intersect(Ray r)
{
	double min_dist = FLT_MAX;
	object3D * closest_M = NULL; //keep track of the closest sphere
	triangle * closest_T = NULL;

	for (list<object3D*>::iterator i = m_models.begin(); i != m_models.end(); i++)
	{
		object3D* obj = *i;

		if (dynamic_cast<model*>(obj)) //this object is a mesh
		{
			model* mesh = dynamic_cast<model*>(obj);
			triangle * t = closest_intersect(*mesh, r);
			if (t != NULL)
			{
				Point3d x;
				intersect(*mesh, t, r, x);
				double dist = (x - r.o).normsqr();
				if (dist < min_dist)
				{
					min_dist = dist;
					closest_T = t;
					closest_M = mesh;
				}
			}
		}
		//--------------------------------------------------------------------------
		else if (dynamic_cast<sphere*>(obj)) //this object is a sphere
		{
			sphere* ball = dynamic_cast<sphere*>(obj);
			Point3d x;
			if (intersect(*ball, r, x))
			{
				double dist = (x - r.o).normsqr();
				if (dist < min_dist)
				{
					min_dist = dist;
					closest_T = NULL;
					closest_M = ball;
				}
			}
		}//--------------------------------------------------------------------------
	}

	return make_pair(closest_M, closest_T);
}

// determine if there is an intersection between triangle t and ray r
// return true if there is an intersection and store the intersection in x
// return false otherwise and x is undefined in this case
bool RayTracer::intersect(model& m, triangle * t, Ray r, Point3d& x)
{
	//
	//TODO: implement this
	//
	// Read model class first to understand how the models is represented
	//

    return false;
}

//returns a triangle in model m that make closest intersection with ray r
//return NULL if no intersection is found
triangle * RayTracer::closest_intersect(model& m, Ray r)
{
	double min_dist = FLT_MAX;
	triangle * closest_T = NULL;
	for (unsigned int i = 0; i < m.t_size; i++)
	{
		Point3d x;
		if (intersect(m, &m.tris[i], r, x))
		{
			double dist = (x - r.o).normsqr();
			if (dist < min_dist)
			{
				closest_T = &m.tris[i];
				min_dist = dist;
			}
		}//end if
	}//end for i

	return closest_T;
}

//
// determine the color of ray r
// 
Vector3d RayTracer::raycolor(const Ray& r, int depth)
{
	if (depth >= this->m_max_ray_depth) return Vector3d(0, 0, 0);

    Vector3d color;
    pair<object3D *, triangle *> X = intersect(r);

    //determine the color of this ray 
    if (X.first != NULL && X.second != NULL) //this is mesh intersection
    {
        Vector3d rc = raycolor(*((model*)X.first), X.second, r,depth);
        color = rc + color;
    }
    else if (X.first != NULL) //this is spehere intersection
    {
        Vector3d rc = raycolor(*((sphere*)X.first), r, depth);
        color = rc + color;
    }

    return color;
}


//
// determine the color of ray r, by analizing the intersection between t and r 
// 

inline bool isLight(object3D * obj)
{
	return (obj->mat_emission.normsqr() != 0);
}

Vector3d RayTracer::raycolor(sphere& s, const Ray& r, int depth)
{
	if (isLight(&s)) return s.mat_emission; //s is a light source...

	Vector3d color; // = Vector3d(m_light_ambient[0] * s.mat_color[0], m_light_ambient[1] * s.mat_color[1], m_light_ambient[2] * s.mat_color[2]);


	//  determine the direct color
	//
	//hint: (1) what is the normal direction at the intersection between s and r
	//      (2) what is the color of this sphere? 
	//      (3) where is the light?
	//      (4) is the intersection in shadow? (call inshadow(const Point3d& p))

	Point3d x;
	intersect(s, r, x);
	Vector3d normal = (x - s.center).normalize();

	//loop through each light source
	typedef list< pair<object3D*, Point3d> >::iterator IT;
	double light_sample_contribution = 1.0 / m_light_samples.size();

	for (IT i = m_light_samples.begin(); i != m_light_samples.end(); i++)
	{
		//get light properties
		const Point3d& light_pos = i->second;
		const Vector3d & light_specular = i->first->mat_specular;
		const Vector3d & light_diffuse = i->first->mat_emission;

		//detemine the color from this light source...
		Vector3d mycolor;

		Vector3d light_dir = (light_pos - x).normalize();
		double dot = normal*light_dir;

		if (dot > 0 && inshadow(x + normal*1e-5, light_pos) == false) //not in shadow
		{
			double NdotHV = max(normal * (-r.v + light_dir).normalize(), 0.0);

			for (int d = 0; d < 3; d++)
			{
				mycolor[d] += dot*s.mat_color[d] * light_diffuse[d];
				mycolor[d] += s.mat_specular[d] * light_specular[d] * pow(NdotHV, s.mat_shininess);
			}
		}

		//accumulate the color...
		color = color + mycolor*light_sample_contribution;

	}//end for i

	//
	// TODO: compute reflection color
	//
	Vector3d reflection_color;
	if (s.reflectiveness.normsqr() > 0)
	{
		//TODO: determine the reflection color
		//
		//      (1) generate reflection ray 
		//      (2) determine the color of the ray (remember of increase depth by 1)
	}

	//
	// TODO: compute refraction color
	//
	Vector3d refraction_color;
	if (s.transparency.normsqr() > 0)
	{
		// TODO: determine the refraction color
		//
		//      (1) generate refraction ray 
		//      (2) determine the color of the ray (remember of increase depth by 1)
		//
	}

	//finally gather all the colors
	for (int i = 0; i < 3; i++)
	{
		color[i] = clamp(color[i] + s.reflectiveness[i] * reflection_color[i] + s.transparency[i] * refraction_color[i]);
	}


    return color;
}


//
// determine the color of ray r, by analizing the intersection between t and r 
// 
Vector3d RayTracer::raycolor(model& m, triangle * t, const Ray& r, int depth)
{
	if (isLight(&m)) return m.mat_emission; //s is a light source...

	Vector3d color;

	//
    //  TODO: implement this
	//  determine the direct color (diffuse + sepcular)
	//
	//  hint:
	//
	//      (1) what is the normal direction at the intersection between s and r
	//      (2) what is the color of this sphere? 
	//      (3) where is the light?
	//      (4) is the intersection in shadow? (call inshadow(const Point3d& p))
	//

	//
	// Note: You do not need to implement reflection or refraction for this function
	// Bonux +10 pts if you provide reflection + refraction implementation
	//

    return color;
}


//return true if the point p is in the shadow cast by a light at light_pos
bool RayTracer::inshadow(const Point3d& p, const Point3d& light_pos)
{
	//
	// TODO: implement this
	//

	return false;
}


//save rendered image to file
bool RayTracer::save2file(const string& filename)
{
    FILE *f = fopen(filename.c_str(), "w");         // Write image to PPM file.

    int h = m_img.size();
    if (h == 0) return true; //nothing to save...

    int w = m_img.front().size();
    
    fprintf(f, "P3\n%d %d\n%d\n", w, h, 255);

    for (int i = 0; i < h; i++)
    {
        for (int j = 0; j < w; j++)
            fprintf(f, "%d %d %d ", toInt(m_img[i][j][0]), toInt(m_img[i][j][1]), toInt(m_img[i][j][2]));

        show_progress_bar(i*1.0 / h);
    }
    show_progress_bar(1.0f);
    fclose(f);
    
    return true;
}



//sample points from all area lights
void RayTracer::sample(list<object3D*>& lights, int sample_size_per_obj)
{
	for (list<object3D*>::iterator i = lights.begin(); i != lights.end(); i++)
	{
		object3D* obj = *i;

		if (dynamic_cast<model*>(obj)) //this object is a mesh
		{
			model* mesh = dynamic_cast<model*>(obj);
			sample(*mesh, sample_size_per_obj);
		}//--------------------------------------------------------------------------
		else if (dynamic_cast<sphere*>(obj)) //this object is a sphere
		{
			sphere* ball = dynamic_cast<sphere*>(obj);
			sample(ball, sample_size_per_obj);
		}//--------------------------------------------------------------------------
	}

}

void RayTracer::sample(sphere* ball, int size)
{
	for (int i = 0; i < size; i++)
	{
		Vector3d vec(drand48(), drand48(), drand48());
		for (int d = 0; d<3; d++) if (drand48()>0.5) vec[d] = -vec[d];
		vec = vec.normalize();
		m_light_samples.push_back(make_pair(ball, ball->center + vec*(ball->radius + 1e-5)));
	}
}

void RayTracer::sample(model& m, int size)
{

	//compute total area
	double total_area = 0;
	for (unsigned int i = 0; i < m.t_size; i++)
	{
		triangle& t = m.tris[i];
		Point3d& p1 = m.vertices[t.v[0]].p;
		Point3d& p2 = m.vertices[t.v[1]].p;
		Point3d& p3 = m.vertices[t.v[2]].p;

		//compute triangle area
		Vector3d v1 = p2 - p1;
		Vector3d v2 = p3 - p1;
		double area = (v1%v2).norm() / 2;
		total_area += area;
	}

	//sample from facets
	double r[2];
	for (unsigned int i = 0; i<m.t_size; i++){
		triangle& t = m.tris[i];
		Point3d& p1 = m.vertices[t.v[0]].p;
		Point3d& p2 = m.vertices[t.v[1]].p;
		Point3d& p3 = m.vertices[t.v[2]].p;

		//compute triangle area
		Vector3d v1 = p2 - p1;
		Vector3d v2 = p3 - p1;
		Vector3d v3 = p3 - p2;
		Vector3d v4 = p1 - p3;
		double area = (v1%v2).norm() / 2;
		int t_size = (int)ceil(size*(area / total_area));

		//randomly generate these points
		while (t_size>0)
		{
			r[0] = drand48();
			r[1] = drand48();
			Point3d pos = p1 + v1*r[0] + v2*r[1];

			//check if inside the triangle
			if ((v1 % (pos - p1))*t.n <= 0) continue;
			if ((v3 % (pos - p2))*t.n <= 0) continue;
			if ((v4 % (pos - p3))*t.n <= 0) continue;

			//in tri
			pos = pos + t.n*1e-5;

			m_light_samples.push_back(make_pair(&m, pos));
			t_size--;
		}//j

	}//end for i
}

