#pragma once
#include "includes.h"

#define NUM_RENDER_THREADS 2
#define PIXEL_STEP 1
#define TILE_SIZE 50
#define MAX_DEPTH 4
#define EPSILON 0.00001

struct Tile {
	int x,y,w,h;
};

double intersect_sphere(double ray_direction[3], double ray_origin[3], double sphere_center[3], double sphere_radius) {
	double delta[3];
	double a = glLibMathVec::dot(ray_direction,ray_direction,3);

	glLibMathVec::subtract(ray_origin,sphere_center,delta,3);
	double b = 2.0*glLibMathVec::dot(ray_direction,delta,3);

	double c = glLibMathVec::dot(delta,delta,3) - sphere_radius*sphere_radius;

	double radicand = b*b - 4*a*c;
	if (radicand<0) return 0.0;

	return (-b-pow(radicand,0.5))/(2.0*a);
}
double intersect_plane(double ray_direction[3], double ray_origin[3], double plane_point[3], double plane_normal[3]) {
	//http://en.wikipedia.org/wiki/Line-plane_intersection
	double n_dot_l = glLibMathVec::dot(ray_direction,plane_normal,3);
	if (n_dot_l==0.0) return 0.0;
	double delta[3];
	glLibMathVec::subtract(plane_point,ray_origin,delta,3);
	return glLibMathVec::dot(delta,plane_normal,3) / n_dot_l;
}
double intersect_triangle(double ray_direction[3], double ray_origin[3], double p1[3], double p2[3], double p3[3]) {
	//http://softsurfer.com/Archive/algorithm_0105/algorithm_0105.htm#intersect_RayTriangle()

	double u[3],v[3],n[3];
	//Get triangle edge vectors and plane normal
	glLibMathVec::subtract(p2,p1,u,3);
	glLibMathVec::subtract(p3,p1,v,3);
	glLibMathVec::cross(u,v,n);

	if (n[0]==0.0&&n[1]==0.0&&n[2]==0.0) return 0.0; //degenerate triangle

	double w0[3];
	glLibMathVec::subtract(ray_origin,p1,w0,3);
	double a = -glLibMathVec::dot(n,w0,3);
	double b = glLibMathVec::dot(n,ray_direction,3);
	if (b==0.0) return 0.0; //ray is parallel to triangle plane ((a==0.0)?in:disjoint)

	//Get intersection point of ray with triangle plane
	double r = a / b;
	if (r<0.0) return 0.0; //Ray goes away from triangle
	//For a segment, also test if (r>1.0) = no intersect

	//distance to plane is r
	double I[3], temp[3];
	glLibMathVec::scale(r,ray_direction,temp,3);
	glLibMathVec::add(ray_origin,temp,I,3); //I is point of intersection

	// is I inside T?
	double uu = glLibMathVec::dot(u,u,3);
	double uv = glLibMathVec::dot(u,v,3);
	double vv = glLibMathVec::dot(v,v,3);
	double w[3];
	glLibMathVec::subtract(I,p1,w,3);
	double wu = glLibMathVec::dot(w,u,3);
	double wv = glLibMathVec::dot(w,v,3);
	double D = uv * uv - uu * vv;

	// get and test parametric coords
	double s = (uv * wv - vv * wu) / D;
	if (s<0.0||s>1.0) return 0.0;
	double t = (uv * wu - uu * wv) / D;
	if (t<0.0||(s+t)>1.0) return 0.0;

	return r;
}

class Renderer;
Renderer* instance__renderer;
int wrapper__render_tile(void*);
class Renderer {
	friend int wrapper__render_tile(void* arg);
	public:
		bool finished;
		bool stop;
	private:
		SDL_Surface* _image;
		int _img_w,_img_h;
		int _num_tiles_x,_num_tiles_y;
		double img_aspect;
		SDL_PixelFormat* _pixel_format;
		int _numof_samples;

		Scene* _scene;
		double _background_color[3];

		map<string,Light*>* _lights;

		deque<Tile>* schedule;

		SDL_sem* schedule_lock;

		list<SDL_Thread*>* render_threads;

		double vec_eye_x[3],vec_eye_y[3],vec_eye_v[3],vec_eye_z[3]; //x, y, view, scaled view

		double* random_samples;

		glLibTimer* _timer;

	private:
		void init_image_attributes(void) {
			_img_w = _scene->image->width;
			_img_h = _scene->image->height;
			img_aspect = (double)_img_w / (double)_img_h;

			if (TILE_SIZE*(_img_w/TILE_SIZE)==_img_w) { _num_tiles_x = _img_w/TILE_SIZE; }
			else _num_tiles_x = _img_w/TILE_SIZE+1;
			if (TILE_SIZE*(_img_h/TILE_SIZE)==_img_h) { _num_tiles_y = _img_h/TILE_SIZE; }
			else _num_tiles_y = _img_h/TILE_SIZE+1;

			_pixel_format = _image->format;
			_numof_samples = _scene->image->samples;
		}
		void init_scene_attributes(void) {
			for (int i=0;i<3;++i) _background_color[i] = _scene->background_color[i];

			_lights = _scene->lights;
		}
		void init_vectors(void) {
			glLibMathVec::subtract(_scene->camera->center,_scene->camera->position,vec_eye_v,3);
			glLibMathVec::normalize(vec_eye_v,3);
			glLibMathVec::cross(vec_eye_v,_scene->camera->up,vec_eye_x);
			glLibMathVec::normalize(vec_eye_x,3);
			glLibMathVec::cross(vec_eye_v,vec_eye_x,vec_eye_y);
			glLibMathVec::scale(-1.0,vec_eye_y,3);

			glLibMathVec::scale(_scene->camera->image_plane,vec_eye_v,vec_eye_z,3);
		}
		void init_threads(void) {
			schedule_lock = SDL_CreateSemaphore(1);

			schedule = new deque<Tile>();
			for (int x=0;x<_num_tiles_x;++x) {
				for (int y=0;y<_num_tiles_y;++y) {
					Tile new_tile;
					new_tile.x = x*TILE_SIZE;
					new_tile.y = y*TILE_SIZE;
					new_tile.w = TILE_SIZE;
					new_tile.h = TILE_SIZE;
					if (new_tile.x+new_tile.w>_img_w) new_tile.w = _img_w - new_tile.x;
					if (new_tile.y+new_tile.h>_img_h) new_tile.h = _img_h - new_tile.y;
					schedule->push_back(new_tile);
				}
			}
			//random_shuffle(schedule->begin(),schedule->end());

			render_threads = new list<SDL_Thread*>();
		}
		void init_random_samples(void) {
			random_samples = new double[2*_numof_samples];
			for (int i=0;i<2*_numof_samples;++i) {
				random_samples[i] =(double)rand()/(double)RAND_MAX;
			}
		}
	public:
		Renderer(Scene* scene, SDL_Surface* image) {
			_scene = scene;
			_image = image;

			init_image_attributes();
			init_scene_attributes();
			init_vectors();
			init_threads();
			init_random_samples();

			finished = false;
			stop = false;

			_timer = new glLibTimer();

			instance__renderer = this;
		}
		~Renderer() {
			stop = true;
			for (list<SDL_Thread*>::iterator iter=render_threads->begin();iter!=render_threads->end();++iter) {
				int status;
				SDL_WaitThread(*iter,&status);
			}
			delete render_threads;

			delete random_samples;
			delete schedule;
			delete _timer;

			SDL_DestroySemaphore(schedule_lock);
		}
		
		void begin(void) {
			_timer->mark();
			for (int i=0;i<NUM_RENDER_THREADS;++i) {
				render_threads->push_front(SDL_CreateThread(wrapper__render_tile,NULL));
			}
		}

		SDL_Surface* get_image(void) {
			return _image;
		}
		void save(const char* path) {
			SDL_SaveBMP(_image,path);
		}
		void print_time(void) {
			double seconds = _timer->get();
			int minutes = 0, hours = 0, days = 0;
			if (seconds/60.0>=1.0) {
				minutes = floor(seconds/60.0);
				seconds = fmod(seconds,60.0);
			}
			if (minutes/60>=1) {
				hours = minutes / 60;
				minutes = minutes % 60;
			}
			if (hours/60>=1) {
				days = hours / 24;
				hours = hours % 24;
			}
			if      (days==1) { printf(   "1 day,"     ); }
			else if (days >1) { printf("%d days, ",days); }
			if      (hours==1) { printf(  "1 hour, "      ); }
			else if (hours >1) { printf("%d hours, ",hours); }
			if      (minutes==1) { printf(  "1 minute, "        ); }
			else if (minutes >1) { printf("%d minutes, ",minutes); }
			if      (seconds==1.0) { printf("1 second"); }
			else                   { printf("%.2f seconds",seconds); }
		}
	private:
		void set_pixel_rgb24(int x, int y, Uint32 pixel) {
			static Uint8* pixels_pointer = (Uint8*)_image->pixels;
			static int pitch = _image->pitch;
			Uint8 *p = pixels_pointer + y*pitch + x*3;
			#if SDL_BYTEORDER==SDL_BIG_ENDIAN
			p[0] = (pixel >> 16) & 0xff;
			p[1] = (pixel >> 8) & 0xff;
			p[2] = pixel & 0xff;
			#else
			p[0] = pixel & 0xff;
			p[1] = (pixel >> 8) & 0xff;
			p[2] = (pixel >> 16) & 0xff;
			#endif
		}

		void trace_get_intersect(double ray[3], double origin[3], double* distance, Object** object_hit) {
			list<Object*>* objs = _scene->objects;
			double closest = MAX_DOUBLE;
			*object_hit = NULL;
			for (list<Object*>::iterator iter=objs->begin(); iter!=objs->end(); ++iter) {
				double intersect = 0.0;
				switch ((*iter)->type) {
					case    SCENE_OBJECT_TYPE_POINT: { break; }
					case   SCENE_OBJECT_TYPE_SPHERE: { Sphere* object = (Sphere*)(*iter); intersect = intersect_sphere(ray,origin,object->position,object->radius); break; }
					case    SCENE_OBJECT_TYPE_PLANE: { Plane* object = (Plane*)(*iter); intersect = intersect_plane(ray,origin,object->position,object->normal); break; }
					case SCENE_OBJECT_TYPE_TRIANGLE: { Triangle* object = (Triangle*)(*iter); intersect = intersect_triangle(ray,origin,object->position,object->position2,object->position3); break; }
					default: { throw glLibError("Not implemented!"); break; }
				}
				if (intersect<=0.0) continue;
				if (intersect<closest) {
					closest = intersect;
					*object_hit = *iter;
				}
			}
			*distance = closest;
		}
		double trace_shadow_ray(double ray[3], double origin[3]) {
			Object* object_hit;
			double closest;
			trace_get_intersect(ray,origin,&closest,&object_hit);
			return closest;
		}
		bool trace_ray(double ray[3], double origin[3], double color[3], int depth) {
			//Determine intersect
			Object* object_hit;
			double closest;
			trace_get_intersect(ray,origin,&closest,&object_hit);
			//Shade
			if (object_hit!=NULL) {
				Material* material = object_hit->material;
				//double plane_near = 4.5;
				//double plane_far = 11.5;
				//color[1] += 255.0*(closest-plane_near)/(plane_far-plane_near);

				//Ambient
				glLibMathVec::add(material->ambient,color,color,3);

				double position[3];
				glLibMathVec::scale(closest,ray,position,3);
				glLibMathVec::add(position,origin,position,3);

				double l[3],n[3],r[3],v[3], temp[3];

				glLibMathVec::negate(ray,v,3);

				bool can_flip_normal = false;
				//Get normal
				switch (object_hit->type) {
					case    SCENE_OBJECT_TYPE_POINT: { break; } //point, not possible, wouldn't have hit above
					case   SCENE_OBJECT_TYPE_SPHERE: { //sphere
						glLibMathVec::subtract(position,object_hit->position,n,3);
						glLibMathVec::normalize(n,3);
						break;
					}
					case    SCENE_OBJECT_TYPE_PLANE: {
						glLibMathVec::copy( ((Plane*)(object_hit))->normal, n, 3);
						can_flip_normal = true;
						break;
					}
					case SCENE_OBJECT_TYPE_TRIANGLE: {
						Triangle* t = ((Triangle*)(object_hit));
						double v1[3],v2[3];
						glLibMathVec::subtract(t->position2,t->position,v1,3);
						glLibMathVec::subtract(t->position3,t->position,v2,3);
						glLibMathVec::cross(v1,v2,n);
						glLibMathVec::normalize(n,3);
						can_flip_normal = true;
						break;
					}
					default: { //not possible, would've thrown above
						throw glLibError("Not implemented!");
						break;
					}
				}

				for (map<string,Light*>::const_iterator iter=_lights->begin();iter!=_lights->end();++iter) {
					Light* light = iter->second;
					glLibMathVec::subtract(light->primitive->position,position,l,3);
					glLibMathVec::normalize(l,3);

					double n_dot_l = glLibMathVec::dot(n,l,3);
					if (n_dot_l<0.0) {
						if (can_flip_normal) {
							glLibMathVec::negate(n,3);
							n_dot_l = -n_dot_l;
						}
						else continue;
					}

					double light_dependent_color[3];

					//Diffuse
					double scaled_diffuse[3]; glLibMathVec::scale(n_dot_l,material->diffuse,scaled_diffuse,3);
					glLibMathVec::copy(scaled_diffuse,light_dependent_color,3);

					//Specular
					//	Not using glLibMathVec::reflect, because can take advantage of the fact that dot(n,l) has already been computed.
					glLibMathVec::scale(2.0*n_dot_l,n,temp,3);
					glLibMathVec::subtract(temp,l,r,3);

					double spec = glLibMathVec::dot(r,v,3);
					if (spec<0.0) spec = 0.0; //when raising small negative numbers to large powers, sometimes it becomes positive.
					spec = pow(spec,material->shininess);

					double scaled_specular[3]; glLibMathVec::scale(spec,material->specular,scaled_specular,3);
					glLibMathVec::add(scaled_specular,light_dependent_color,light_dependent_color,3);

					//Shadow
					double occlusion_scalar = 1.0;
					map<Light*,int>::const_iterator location = material->to_shadow->find(light);
					if (location!=material->to_shadow->end()) {
						occlusion_scalar = 0.0;
						for (int i=0;i<location->second;++i) {
							double light_point[3];
							light->get_random_point(light_point);

							double shadow_ray_direction[3];
							glLibMathVec::subtract(light_point,position,shadow_ray_direction,3);

							double distance_to_light_point = glLibMathVec::length(shadow_ray_direction,3);
							glLibMathVec::scale(1.0/distance_to_light_point,shadow_ray_direction,3);

							double offset_ray[3], offset_position[3];
							glLibMathVec::scale(EPSILON,shadow_ray_direction,offset_ray,3);
							glLibMathVec::add(position,offset_ray,offset_position,3);
							double intersect_distance = trace_shadow_ray(shadow_ray_direction,offset_position);
							occlusion_scalar += ((intersect_distance==0.0)||(intersect_distance>distance_to_light_point)) ? 1.0 : 0.0;
						}
						occlusion_scalar /= (double)(location->second);
					}

					//Add (possibly scaled) diffuse and specular
					glLibMathVec::scale(occlusion_scalar,light_dependent_color,3);
					glLibMathVec::add(light_dependent_color,color,color,3);
				}

				//Reflection
				if (material->reflect&&depth<MAX_DEPTH) {
					double reflection[3] = {0.0,0.0,0.0};

					//Change to glLibMathVec::reflect
					double temp[3];
					double n_dot_v = glLibMathVec::dot(v,n,3);
					glLibMathVec::scale(2.0*n_dot_v,n,temp,3);
					glLibMathVec::subtract(temp,v,r,3);

					double offset_ray[3], offset_position[3];
					glLibMathVec::scale(EPSILON,r,offset_ray,3);
					glLibMathVec::add(position,offset_ray,offset_position,3);

					if (material->samples_reflection>1) {
						for (int i=0;i<material->samples_reflection;++i) {
							double rand_sphere_vec[3] = {glLibMathRandom::get_gaussian(1,1),glLibMathRandom::get_gaussian(1,1),glLibMathRandom::get_gaussian(1,1)};
							double l = glLibMathVec::length(rand_sphere_vec,3);
							glLibMathVec::scale((0.5-EPSILON)*powf(0.95,material->shininess)/l,rand_sphere_vec,3);
							double perturbed_reflection[3];
							glLibMathVec::add(r,rand_sphere_vec,perturbed_reflection,3);
							glLibMathVec::normalize(perturbed_reflection,3);
							trace_ray(perturbed_reflection,offset_position,reflection,depth+1);
						}
						glLibMathVec::scale(1.0/(double)material->samples_reflection,reflection,3);
					}
					else {
						trace_ray(r,offset_position,reflection,depth+1);
					}
					glLibMathVec::scale(material->coefficient_reflection,reflection,3);
					glLibMathVec::add(reflection,color,color,3);
				}

				return true;
			}
			return false;
		}
		template <typename type_num> void clamp(type_num vec[], type_num low, type_num high, int vec_length) {
			for (int i=0;i<vec_length;++i) vec[i] = max(min(vec[i],high),low);
		}
		int render_tile(void* data) {
			Tile to_render;
			while (true) {
				SDL_SemWait(schedule_lock);
				if (schedule->size()==0) finished = true;
				else {
					to_render = schedule->front();
					schedule->pop_front();
				}
				SDL_SemPost(schedule_lock);
				if (finished||stop) return 1;

				for (int pix_x=to_render.x;pix_x<to_render.x+to_render.w;pix_x+=PIXEL_STEP) {
					for (int pix_y=to_render.y;pix_y<to_render.y+to_render.h;pix_y+=PIXEL_STEP) {
						double color[3] = {0.0,0.0,0.0};
						for (int i=0;i<_numof_samples;++i) {
							double x_part = ((double)(pix_x)+random_samples[  i]) / (double)_img_w * 2.0 - 1.0;
							double y_part = ((double)(pix_y)+random_samples[2*i]) / (double)_img_h * 2.0 - 1.0;
							y_part /= img_aspect;

							double temp1[3],temp2[3],img_plane_vec[3],direction[3];
							glLibMathVec::scale(x_part,vec_eye_x,temp1,3);
							glLibMathVec::scale(y_part,vec_eye_y,temp2,3);
							glLibMathVec::add(temp1,temp2,img_plane_vec,3);
							glLibMathVec::add(img_plane_vec,vec_eye_z,direction,3);
							glLibMathVec::normalize(direction,3);

							if (!trace_ray(direction,_scene->camera->position,color,1)) {
								color[0] += _background_color[0];
								color[1] += _background_color[1];
								color[2] += _background_color[2];
							}
						}
						glLibMathVec::scale(255.0/_numof_samples,color,3);
						clamp(color,0.0,255.0,3);

						Uint32 final_color = SDL_MapRGB(_pixel_format,(int)(color[0]),(int)(color[1]),(int)(color[2]));
						set_pixel_rgb24(pix_x,_img_h-pix_y-1,final_color);
					}
				}
			}
		}
};
int wrapper__render_tile(void* arg) {
	return instance__renderer->render_tile(arg);
}