#pragma once
#include "../../stdafx.h"
#include "../glLibLocals.h"
#include "../glLibMisc.h"
#include "../glLibView.h"
#include "../glLibFBO.h"
#include "../glLibObjects/glLibObjectsMisc.h"
#include "../glLibMath/glLibMathVec.h"
#include "../glLibMath/glLibMathDistrib.h"

#ifdef GLLIB_ERROR_MODE_REPORT_ERRORS
void check_filter_type(glLibFlagFilter filter_type, bool separable);
#endif

class glLibInternal_post_process {
	public:
		glLibFlagFilter filter_type;
		glLibTexture2D *source_tex, *dest_tex;
	protected:
		glLibView2D *_view2D, *_view2D_draw;
		glLibFBO *_fbo1, *_fbo2;
		glLibTexture2D* intermediate_tex;
		char _mode;
		int _kernel_size;
		int _radius;
		int _precision;
		bool _separable;
		int _filter_mode; //FBO, ACCUM, CONV, COPY
	private:
		float *_filter, *_filter_x, *_filter_y;
		bool _same_separable;
		bool _created;
		int _step;

	protected:
		glLibInternal_post_process(int screen_size[2], glLibFlagFilter filter_mode, int downsample, int step, int precision, bool separable) {
			int rect[] = {0,0,screen_size[0],screen_size[1]};
			int downsampled_rect[] = {0,0,screen_size[0]/downsample,screen_size[1]/downsample};
			_view2D = new glLibView2D(downsampled_rect);
			_view2D_draw = new glLibView2D(rect);

			if (filter_mode&GLLIB_FILTER_USE_FBO) {
				_fbo1 = new glLibFBO(_view2D->size);
				_fbo2 = new glLibFBO(_view2D->size);
				_precision = precision;
				_fbo1->add_render_target(1,GLLIB_RGBA,false,true,precision);
				_fbo2->add_render_target(1,GLLIB_RGBA,false,true,precision);
				_filter_mode = 1;
			}
			else if (filter_mode&GLLIB_FILTER_USE_ACCUM) {
				#ifdef GLLIB_ERROR_MODE_REPORT_ERRORS
				if (downsample!=1) throw glLibError("The accumulation buffer cannot be downsampled!");
				#endif
				GLint bits_r; glGetIntegerv(  GL_ACCUM_RED_BITS,&bits_r);
				GLint bits_g; glGetIntegerv(GL_ACCUM_GREEN_BITS,&bits_g);
				GLint bits_b; glGetIntegerv( GL_ACCUM_BLUE_BITS,&bits_b);
				GLint bits_a; glGetIntegerv(GL_ACCUM_ALPHA_BITS,&bits_a);
				_precision = (bits_r+bits_g+bits_b+bits_a) / 4;
				#ifdef GLLIB_ERROR_MODE_REPORT_WARNINGS
				if (_precision!=precision) {
					glLibWarning("Accumulation buffer does not have the precision requested (has precision \""+to_string(_precision)+"\").");
				}
				#endif
				_filter_mode = 2;
			}
			else if (filter_mode&GLLIB_FILTER_USE_CONV) {
				_filter_mode = 3;
			}
			else if (filter_mode&GLLIB_FILTER_USE_COPY) {
				_filter_mode = 4;
			}
			#ifdef GLLIB_ERROR_MODE_REPORT_ERRORS
			else throw glLibError("Invalid filter mode!");
			#endif

			_separable = separable;

			_filter = _filter_x = _filter_y = NULL;

			_created = false;

			source_tex = dest_tex = NULL;
			if ( _filter_mode==2 || _filter_mode==3 || _filter_mode==4 ) {
				unsigned short rect[] = {0,0,downsampled_rect[2],downsampled_rect[3]};
				intermediate_tex = new glLibTexture2D(rect,GLLIB_RGBA,false,true);
			}

			_step = step;
			#ifdef GLLIB_ERROR_MODE_REPORT_ERRORS
			if (step<1) throw glLibError("Step must be a positive number!");
			#endif
		}
		~glLibInternal_post_process() {
			delete _view2D;
			delete _view2D_draw;

			if (_filter_mode==1) {
				delete _fbo1;
				delete _fbo2;
			}
			else if (_filter_mode==2 || _filter_mode==3 || _filter_mode==4) {
				delete intermediate_tex;
			}

			glLibInternal_cleanup_filters();
		}
	
	private:
		void glLibInternal_cleanup_filters(void) {
			if (_separable) {
				if (_same_separable) {
					delete [] _filter_x;
					//filter_y = _filter_x, so don't delete the data again
				}
				else {
					delete [] _filter_x;
					delete [] _filter_y;
				}
			}
			else {
				delete [] _filter;
			}
		}
	public:
		void set_filter(glLibFlagFilter filter_type, int kernel_size, float parameters=-1.0) {
			#ifdef GLLIB_ERROR_MODE_REPORT_ERRORS
			if (kernel_size<1) {
				throw glLibError("Invalid kernel size \""+to_string(kernel_size)+"\"!  Size must be at least 1.");
			}
			if (kernel_size%2!=1) {
				throw glLibError("Invalid kernel size \""+to_string(kernel_size)+"\"!  Size must be an odd number.");
			}
			if (kernel_size>32767) {
				throw glLibError("Invalid kernel size \""+to_string(kernel_size)+"\"!  Size too large!");
			}
			#endif
			_kernel_size = kernel_size;
			_radius = _kernel_size / 2;

			#ifdef GLLIB_ERROR_MODE_REPORT_ERRORS
			check_filter_type(filter_type,false);
			#endif

			this->filter_type = filter_type;

			if (_created) glLibInternal_cleanup_filters();

			if (_separable) {
				if      (filter_type &  GLLIB_FILTER_BILINEAR) { throw glLibError("Unimplemented!"); }
				else if (filter_type &       GLLIB_FILTER_BOX) {
					_same_separable = true; //_filter_x == _filter_y

					_filter_x = new float[_kernel_size]; _filter_y = _filter_x;

					float value = 1.0f/_kernel_size;
					for (int i=0;i<_kernel_size;++i) {
						_filter_x[i] = value;
					}
				}
				else if (filter_type &  GLLIB_FILTER_GAUSSIAN) {
					_same_separable = true; //_filter_x == _filter_y

					_filter_x = new float[_kernel_size]; _filter_y = _filter_x;

					float variance = parameters;
					if (variance==-1.0) throw glLibError("Automatically sized Gaussians are unimplemented!");

					float sum = 0.0f;
					for (int i=-_radius;i<=_radius;++i) {
						float value = glLibMathDistrib::gaussian((float)i,variance);
						sum += value;
						_filter_x[i+_radius] = value;
					}
					for (int i=0;i<_kernel_size;++i) {
						_filter_x[i] /= sum;
					}
				}
				else if (filter_type &   GLLIB_FILTER_SOBEL_X) { throw glLibError("Unimplemented!"); }
				else if (filter_type &   GLLIB_FILTER_SOBEL_Y) { throw glLibError("Unimplemented!"); }
			}
			else {
				_filter = new float[_kernel_size*_kernel_size];

				if      (filter_type &  GLLIB_FILTER_BILINEAR) { throw glLibError("Unimplemented!"); }
				else if (filter_type &       GLLIB_FILTER_BOX) {
					float value = 1.0f/(_kernel_size*_kernel_size);
					for (int i=0;i<_kernel_size;++i) {
						for (int j=0;j<_kernel_size;++j) {
							_filter[i*_kernel_size+j] = value;
						}
					}
				}
				else if (filter_type &  GLLIB_FILTER_GAUSSIAN) {
					float variance = parameters;
					if (variance==-1.0) throw glLibError("Automatically sized Gaussians are unimplemented!");
					float sum = 0.0;
					for (int i=-_radius;i<=_radius;++i) {
						for (int j=-_radius;j<=_radius;++j) {
							float value = glLibMathDistrib::gaussian((float)i,variance) * glLibMathDistrib::gaussian((float)j,variance);
							sum += value;
							_filter[(i+_radius)*_kernel_size+(j+_radius)] = value;
						}
					}
					for (int i=0;i<_kernel_size;++i) {
						for (int j=0;j<_kernel_size;++j) {
							_filter[i*_kernel_size+j] /= sum;
						}
					}
				}
				else if (filter_type & GLLIB_FILTER_LAPLACIAN) { throw glLibError("Unimplemented!"); }
				else if (filter_type &   GLLIB_FILTER_SHARPEN) { throw glLibError("Unimplemented!"); }
				else if (filter_type &   GLLIB_FILTER_SOBEL_X) { throw glLibError("Unimplemented!"); }
				else if (filter_type &   GLLIB_FILTER_SOBEL_Y) { throw glLibError("Unimplemented!"); }
			}

			_created = true;
		}
		void set_source(glLibTexture2D* texture) {
			#ifdef GLLIB_ERROR_MODE_REPORT_ERRORS
			if (texture==NULL) throw glLibError("Cannot use a null 2D texture as a source!");
			#endif

			source_tex = texture;
		}
		void set_source(glLibInternal_post_process* post_process) {
			#ifdef GLLIB_ERROR_MODE_REPORT_ERRORS
			if (post_process==NULL) throw glLibError("Cannot use a null post process as a source!");
			#endif

			source_tex = post_process->dest_tex;
		}
	protected:
		void begin_apply(void) {
			#ifdef GLLIB_ERROR_MODE_REPORT_ERRORS
			if (!_created) throw glLibError("Post process does not have a filter!  Use the set_filter(...) method.");
			#endif

			glLibPushView();
			glPushAttrib(GL_ENABLE_BIT);

			glDisable(GL_DEPTH_TEST);
			glDisable(GL_LIGHTING);
			if (_filter_mode==1 || _filter_mode==4) {
				glEnable(GL_BLEND);
				glBlendFunc(GL_ONE,GL_ONE);
			}
		}
		void end_apply(void) {
			glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
			glPopAttrib();
			glLibPopView();
		}
		
		void begin_pass(int pass) {
			if (_filter_mode==1) {
				if (pass==1) _fbo1->enable(1);
				else         _fbo2->enable(1);
				glClear(GL_COLOR_BUFFER_BIT);
			}
			else if (_filter_mode==2) glClear(GL_ACCUM_BUFFER_BIT);
			else if (_filter_mode==3) glClear(GL_COLOR_BUFFER_BIT);
			else if (_filter_mode==4) glClear(GL_COLOR_BUFFER_BIT);
			_view2D->set_view();
		}
		void end_pass(int pass) {
			if (_filter_mode==1) {
				if (pass==1) _fbo1->disable();
				else         _fbo2->disable();
			}
			else if (_filter_mode==2) {
				if (pass==1) glAccum(GL_RETURN,1.0);
			}
		}

		void apply_pass(glLibTexture2D* texture) {
			glLibSelectTexture(texture);

			int rect[4]; rect[2]=_view2D->width; rect[3]=_view2D->height;
			if      (_filter_mode==1) {
				for (int i=0;i<_kernel_size;++i) { rect[0] = (i-_radius)*_step; for (int j=0;j<_kernel_size;++j) { rect[1] = (j-_radius)*_step;
					float value = _filter[i*_kernel_size+j];
					glColor3f(value,value,value);
					glLibDrawScreenQuad(rect,true);
				}}
				glColor3f(1.0f,1.0f,1.0f);
			}
			else if (_filter_mode==2) {
				for (int i=0;i<_kernel_size;++i) { rect[0] = (i-_radius)*_step; for (int j=0;j<_kernel_size;++j) { rect[1] = (j-_radius)*_step;
					glLibDrawScreenQuad(rect,true);
					glAccum(GL_ACCUM,_filter[i*_kernel_size+j]);
				}}
			}
			else if (_filter_mode==3) {
				throw glLibError("Unimplemented!");
				rect[0] = rect[1] = 0;
				//glLibDrawScreenQuad(rect,true);
				float ninth = 1.0f/9.0f;
				//float kernel[3][3] = {{ninth,ninth,ninth},{ninth,ninth,ninth},{ninth,ninth,ninth}};
				float kernel[] = { ninth,ninth,ninth, ninth,ninth,ninth, ninth,ninth,ninth };
				//int support = glLibCheckExtension("GL_ARB_imaging");
				glEnable(GL_CONVOLUTION_2D);
				glConvolutionFilter2D(GL_CONVOLUTION_2D,GL_RGBA,_kernel_size,_kernel_size,GL_RGBA,GL_FLOAT,kernel);
				//glDisable(GL_DEPTH_TEST);
				//glBlitFramebuffer(_view2D->x,_view2D->y,_view2D->width,_view2D->height, _view2D->x,_view2D->y,_view2D->width,_view2D->height, GL_COLOR_BUFFER_BIT,GL_NEAREST);
				//glCopyPixels(_view2D->x,_view2D->y,_view2D->width,_view2D->height,GL_COLOR);
				glDisable(GL_CONVOLUTION_2D);
				//glDrawPixels(_view2D->width,_view2D->height,GL_RGBA,GL_UNSIGNED_BYTE,source_tex->);
			}
			else if (_filter_mode==4) {
				for (int i=0;i<_kernel_size;++i) { rect[0] = (i-_radius)*_step; for (int j=0;j<_kernel_size;++j) { rect[1] = (j-_radius)*_step;
					GLfloat value = _filter[i*_kernel_size+j];
					glColor3f(value,value,value);
					glLibDrawScreenQuad(rect,true);
				}}
				glColor3f(1,1,1);
			}
		}
		void apply_pass_horizontal(glLibTexture2D* texture) {
			glLibSelectTexture(texture);

			int rect[4]; rect[1]=0; rect[2]=_view2D->width; rect[3]=_view2D->height;
			if      (_filter_mode==1) {
				for (int i=0;i<_kernel_size;++i) { rect[0] = (i-_radius)*_step;
					float value = _filter_x[i]; glColor3f(value,value,value);
					glLibDrawScreenQuad(rect,true);
				}
				glColor3f(1.0f,1.0f,1.0f);
			}
			else if (_filter_mode==2) {
				for (int i=0;i<_kernel_size;++i) { rect[0] = (i-_radius)*_step;
					glLibDrawScreenQuad(rect,true);
					glAccum(GL_ACCUM,_filter_x[i]);
				}
			}
			#ifdef GLLIB_ERROR_MODE_REPORT_ERRORS
			else if (_filter_mode==3) {
				throw glLibError("Cannot execute only horizontal pass on an OpenGL convolution filter!");
			}
			#endif
			else if (_filter_mode==4) {
				for (int i=0;i<_kernel_size;++i) { rect[0] = (i-_radius)*_step;
					GLfloat value = _filter_x[i];
					glColor3f(value,value,value);	
					glLibDrawScreenQuad(rect,true);
				}
				glColor3f(1,1,1);
			}
		}
		void apply_pass_vertical(glLibTexture2D* texture) {
			glLibSelectTexture(texture);

			int rect[4]; rect[0]=0; rect[2]=_view2D->width; rect[3]=_view2D->height;
			if      (_filter_mode==1) {
				for (int j=0;j<_kernel_size;++j) { rect[1] = (j-_radius)*_step;
					float value = _filter_y[j]; glColor3f(value,value,value);
					glLibDrawScreenQuad(rect,true);
				}
				glColor3f(1.0f,1.0f,1.0f);
			}
			else if (_filter_mode==2) {
				for (int j=0;j<_kernel_size;++j) { rect[1] = (j-_radius)*_step;
					glLibDrawScreenQuad(rect,true);
					glAccum(GL_ACCUM,_filter_y[j]);
				}
			}
			#ifdef GLLIB_ERROR_MODE_REPORT_ERRORS
			else if (_filter_mode==3) {
				throw glLibError("Cannot execute only vertical pass on an OpenGL convolution filter!");
			}
			#endif
			else if (_filter_mode==4) {
				for (int j=0;j<_kernel_size;++j) { rect[1] = (j-_radius)*_step;
					GLfloat value = _filter_y[j];
					glColor3f(value,value,value);	
					glLibDrawScreenQuad(rect,true);
				}
				glColor3f(1,1,1);
			}
		}
	public:
		void draw_result(void) {
			glPushAttrib(GL_ENABLE_BIT);
			glDisable(GL_DEPTH_TEST);
			if ( _filter_mode==1 || _filter_mode==3 || _filter_mode==4 ) {
				glLibPushView();
				_view2D_draw->set_view();
				glLibDrawScreenQuad(dest_tex);
				glLibPopView();
			}
			else if (_filter_mode==2) {
				glAccum(GL_RETURN,1.0);
			}
			glPopAttrib();
		}
};

class glLibPostProcess : public glLibInternal_post_process {
	public:
	public:
		glLibPostProcess(int screen_size[2], glLibFlagFilter filter_mode, int downsample, int step, int precision) : glLibInternal_post_process(screen_size,filter_mode,downsample,step,precision,false) {}
		~glLibPostProcess() {}

		void apply(void) {
			begin_apply();

			begin_pass(1);
			apply_pass(source_tex);
			end_pass(1);

			if ( _filter_mode==3 || _filter_mode==4 ) {
				glLibSceneToTexture(GLLIB_ALL,intermediate_tex);
			}

			end_apply();

			if      (_filter_mode==1) dest_tex = (glLibTexture2D*)_fbo1->textures[1];
			else if (_filter_mode==2) dest_tex = NULL;
			else if (_filter_mode==3) dest_tex = intermediate_tex;
			else if (_filter_mode==4) dest_tex = intermediate_tex;
		}
};

class glLibPostProcessSeparable : public glLibInternal_post_process {
	public:
	public:
		glLibPostProcessSeparable(int screen_size[2], glLibFlagFilter filter_mode, int downsample, int step, int precision) : glLibInternal_post_process(screen_size,filter_mode,downsample,step,precision,true) {}
		~glLibPostProcessSeparable() {}

		void apply(void) {
			begin_apply();

			begin_pass(1);
			apply_pass_horizontal(source_tex);
			end_pass(1);

			if (_filter_mode==2) {
				glAccum(GL_RETURN,1.0);
				glLibSceneToTexture(GLLIB_ALL,intermediate_tex);
			}
			else if (_filter_mode==4) {
				glLibSceneToTexture(GLLIB_ALL,intermediate_tex);
			}

			begin_pass(2);
			if      (_filter_mode==1) apply_pass_vertical( (glLibTexture2D*)_fbo1->textures[1] );
			else if (_filter_mode==2) apply_pass_vertical(intermediate_tex);
			else if (_filter_mode==3) throw glLibError("Not implemented!");
			else if (_filter_mode==4) apply_pass_vertical(intermediate_tex);
			end_pass(2);

			end_apply();

			if (_filter_mode==4) {
				glLibSceneToTexture(GLLIB_ALL,intermediate_tex);
			}

			if      (_filter_mode==1) dest_tex = (glLibTexture2D*)_fbo2->textures[1];
			else if (_filter_mode==2) dest_tex = NULL;
			else if (_filter_mode==3) throw glLibError("Not implemented!");
			else if (_filter_mode==4) dest_tex = intermediate_tex;
		}
		void apply_horizontal(void) {
			begin_apply();

			begin_pass(1);
			apply_pass_horizontal(source_tex);
			end_pass(1);

			end_apply();

			if (_filter_mode==4) {
				glLibSceneToTexture(GLLIB_ALL,intermediate_tex);
			}

			if      (_filter_mode==1) dest_tex = (glLibTexture2D*)_fbo1->textures[1];
			else if (_filter_mode==2) dest_tex = NULL;
			else if (_filter_mode==3) throw glLibError("Not implemented!");
			else if (_filter_mode==4) dest_tex = intermediate_tex;
		}
		void apply_vertical(void) {
			begin_apply();

			begin_pass(2);
			apply_pass_vertical(source_tex);
			end_pass(2);

			end_apply();

			if (_filter_mode==4) {
				glLibSceneToTexture(GLLIB_ALL,intermediate_tex);
			}

			if      (_filter_mode==1) dest_tex = (glLibTexture2D*)_fbo2->textures[1];
			else if (_filter_mode==2) dest_tex = NULL;
			else if (_filter_mode==3) throw glLibError("Not implemented!");
			else if (_filter_mode==4) dest_tex = intermediate_tex;
		}
};