#pragma once
#include "../includes.h"
#include "../scene/scene.h"
#include "tile_renderer.h"

class Renderer {
	friend class TileRenderer;
	private:
		bool render_stop;
		bool render_finished;

		#ifdef DEBUG_CLICK
		bool render_debug;
		#endif

		TileRenderer* tile_renderers[NUM_RENDER_THREADS];

		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_pixel, _numof_samples_aperture;

		int num_tiles_rendered; //Rendered.  Not loaded from cache
		#ifdef SAVE_INTERMEDIARY
		char loading_or_rendering;
		#endif

		Scene* _scene;
		DT_COL _background_color[3];

		map<string,Light*>* _lights;

		deque<Tile*>* schedule;

		SDL_mutex* schedule_lock;
		SDL_mutex* static_lock;

		DT_FP vec_eye_x[3],vec_eye_y[3]; //x, y
		DT_FP vec_eye_v[3],vec_eye_z[3],vec_img_center[3],vec_camera_position[3]; //view, scaled view, image center

		glLib::Time::timer* _timer_total;
		glLib::Time::timer* _timer_render;

		WeightedBVH<DT_FP>* _bvh;

		PoissonPoint** _poisson_samples_pixel;
		PoissonPoint** _poisson_samples_aperture;

	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_pixel = _scene->image->samples;
			_numof_samples_aperture = _scene->camera->aperture_samples;
		}
		void init_scene_attributes(void) {
			glLib::Math::Vec::copy3(_scene->background_color,_background_color);
			_lights = _scene->lights;
		}
		void init_vectors(void) {
			glLib::Math::Vec::copy3(_scene->camera->position,vec_camera_position);

			glLib::Math::Vec::subtract3(_scene->camera->center,vec_camera_position,vec_eye_v);
			glLib::Math::Vec::normalize3(vec_eye_v);

			glLib::Math::Vec::cross(vec_eye_v,_scene->camera->up,vec_eye_x);
			glLib::Math::Vec::normalize3(vec_eye_x);

			glLib::Math::Vec::cross(vec_eye_x,vec_eye_v,vec_eye_y); //vec_eye_y is normalized

			glLib::Math::Vec::scale3(_scene->camera->image_plane,vec_eye_v,vec_eye_z);
			glLib::Math::Vec::subtract3(vec_camera_position,vec_eye_z,vec_img_center);
		}
		void init_threads(void) {
			static_lock = SDL_CreateMutex();
			schedule_lock = SDL_CreateMutex();

			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();
					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());

			for (int i=0;i<NUM_RENDER_THREADS;++i) {
				tile_renderers[i] = new TileRenderer(this,i);
			}
		}
		void init_random_samples(void) {
			int i;
			list<PoissonPoint*>* s;

			_poisson_samples_pixel = new PoissonPoint*[_numof_samples_pixel];
			i = 0;
			s = generate_poisson_square(_numof_samples_pixel);
			for (list<PoissonPoint*>::const_iterator iter=s->begin();iter!=s->end();++iter) {
				_poisson_samples_pixel[i++] = *iter;
			}
			delete s;

			_poisson_samples_aperture = new PoissonPoint*[_numof_samples_aperture*NUM_RANDOM_DISTR];
			i = 0;
			for (int j=0;j<NUM_RANDOM_DISTR;++j) {
				s = generate_poisson_disk(_numof_samples_aperture);
				for (list<PoissonPoint*>::const_iterator iter=s->begin();iter!=s->end();++iter) {
					PoissonPoint* p = *iter;
					p->x = _scene->camera->aperture*(p->x-0.5);
					p->y = _scene->camera->aperture*(p->y-0.5);
					_poisson_samples_aperture[i++] = p;
				}
				delete s;
			}
		}

		void deinit_threads(void) {
			stop();

			for (int i=0;i<NUM_RENDER_THREADS;++i) {
				delete tile_renderers[i];
			}

			for (deque<Tile*>::iterator iter=schedule->begin();iter!=schedule->end();++iter) {
				delete *iter;
			}
			delete schedule;

			SDL_DestroyMutex(schedule_lock);
			SDL_DestroyMutex(static_lock);
		}
		void deinit_random_samples(void) {
			for (int i=0;i<_numof_samples_pixel;++i) {
				delete _poisson_samples_pixel[i];
			}
			delete [] _poisson_samples_pixel;

			for (int i=0;i<_numof_samples_aperture*NUM_RANDOM_DISTR;++i) {
				delete _poisson_samples_aperture[i];
			}
			delete [] _poisson_samples_aperture;
		}

	public:
		Renderer(Scene* scene, SDL_Surface* image) {
			_scene = scene;
			_image = image;

			#ifndef USING_BVH
			_bvh = NULL;
			#else
			_bvh = new WeightedBVH<DT_FP>(_scene->objects,16);
			printf("  Bounding volume hierarchy has:\n");
			printf("    Number of objects:  %u\n",_bvh->get_num_objects());
			printf("      Triangles:        %u\n",_bvh->get_num_objects_triangles());
			printf("      Spheres:          %u\n",_bvh->get_num_objects_spheres());
			printf("      Planes:           %u\n",_bvh->get_num_objects_planes());
			printf("    Leaf depth:         max %u, average %2.3f, possible %u\n",_bvh->get_max_depth(),_bvh->get_avg_depth(),_bvh->get_max_possible_depth());
			printf("    Leaf contents:      max %u, average %2.3f\n",_bvh->get_max_in_leaf(),_bvh->get_avg_in_leaf());
			#endif

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

			render_finished = false;
			//render_stop = false; //set in start/stop
			#ifdef DEBUG_CLICK
			render_debug = false;
			#endif
			num_tiles_rendered = 0;
			#ifdef SAVE_INTERMEDIARY
			loading_or_rendering = 0;
			#endif

			_timer_total = new glLib::Time::timer();
			_timer_render = new glLib::Time::timer();
		}
		~Renderer() {
			deinit_threads();

			delete _timer_total;
			delete _timer_render;

			if (_bvh!=NULL) delete _bvh;

			deinit_random_samples();
		}

		#ifdef DEBUG_CLICK
		inline void trace_debug(int pixel_x, int pixel_y) {
			stop();

			render_debug = true;
			printf("\n");

			//DT_COL color[3];
			//tile_renderers[0]->generate_trace_rays(pixel_x,pixel_y,color);

			printf("\n");
			render_debug = false;

			start();
		}
		#endif

		bool is_finished(void) {
			return render_finished;
		}
		void start(void) {
			render_stop = false;
			_timer_total->mark();
			for (int i=0;i<NUM_RENDER_THREADS;++i) {
				tile_renderers[i]->thread_start();
			}
		}
		void stop(void) {
			render_stop = true;
			for (int i=0;i<NUM_RENDER_THREADS;++i) {
				tile_renderers[i]->thread_wait();
			}
		}

		int get_image_width(void) { return _img_w; }
		int get_image_height(void) { return _img_h; }

		Tile* get_next_tile(void) {
			Tile* to_render = NULL;

			SDL_mutexP(schedule_lock);

			if (!(render_finished||render_stop)) {
				#ifdef SAVE_INTERMEDIARY
				while (schedule->size()>0) {
					to_render = schedule->front();
					schedule->pop_front();
					tile_name = RENDER_TEMPDIR_NAME + "/" + to_string(to_render.x) + "_" + to_string(to_render.y) + ".bmp";
					//printf("Checking %d,%d = %s\n",to_render.x,to_render.y,tile_name.c_str());
					if (exists_file(tile_name)) {
						printf("\r  Loading tile (%d,%d) from cache.                ",to_render.x,to_render.y);
						SDL_Surface* temp = SDL_LoadBMP(tile_name.c_str());
						SDL_SemPost(schedule_lock);
						for (int y=0;y<to_render.h;++y) {
							for (int x=0;x<to_render.w;++x) {
								set_pixel_rgb24(
									to_render.x+x,
									_img_h-(to_render.y+y)-1,
									get_pixel_rgb24(x,TILE_SIZE-y-1,temp)
								);
							}
						}
						SDL_SemWait(schedule_lock);
						SDL_FreeSurface(temp);
					}
					else {
						if (loading_or_rendering==0) {
							loading_or_rendering = 1;
							_timer_render->mark();
						}
						break;
					}
					//printf("  Exists!  %d %d\n",is_directory(tile_name.c_str()),is_file(tile_name.c_str()));
				}
				#else
				if (schedule->size()==0) {
					render_finished = true;
					//printf("\n");
				}
				else {
					to_render = schedule->front();
					schedule->pop_front();

					printf("\r  Rendering tile (%d,%d); expected: ",to_render->x,to_render->y);
					print_time(  _timer_total->get() / (double)(num_tiles_rendered) * schedule->size(),  0  );
					printf("                ");
				}
				#endif
			}

			SDL_mutexV(schedule_lock);

			return to_render;
		}
		void complete_tile(Tile* tile) {
			delete tile;
			++num_tiles_rendered;
		}

		inline SDL_Surface* get_image(void) {
			return _image;
		}

		inline void print_time(void) {
			print_time(_timer_total->get(),2);
		}
		void print_time(double seconds, int precision, const char* label_d="d", const char* label_h="h", const char* label_m="m", const char* label_s="s") {
			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;
			}

			SDL_mutexP(static_lock);

			if (days>1) printf("%d%s",days,label_d);
			if (hours>1) printf("%d%s",hours,label_h);
			if (minutes>1) printf("%d%s",minutes,label_m);
			printf(("%."+to_string(precision)+"f"+label_s).c_str(),seconds);

			SDL_mutexV(static_lock);
		}
};