#include <iostream>

#include <GL/glew.h>
#include "gtk_win_main.hpp"
#include <gdk/x11/gdkglx.h> // X11 specific

// Macro to make things readable in main() function
#define EXIT_IF_FAIL(val, expr) do { \
	if ( ! (expr) ) { \
		std::cerr << "Init error " << val << std::endl; \
		exit(val); \
	} \
} while(0)

// For understanding the event sequences
#define CALL_TRACE do { \
	std::cout << "trace " <<__PRETTY_FUNCTION__ << std::endl; \
} while(0)

int main(int argc, char* argv[]) {
	CALL_TRACE;

	// Initialize GTK
	Gtk::Main gtkKit(argc, argv); // gtk itself
	Gtk::GL::init(argc, argv);    // gtkglextmm

	// Query and print OpenGL version
	int glVersionMajor, glVersionMinor;
	EXIT_IF_FAIL(1, Gdk::GL::query_version(glVersionMajor, glVersionMinor) );
	std::cout << "OpenGL extension version - " << glVersionMajor << "." << glVersionMinor << std::endl;

	// Initialize OpenGL
	Glib::RefPtr<Gdk::GL::Config> glconfig;
	Gdk::GL::ConfigMode glMode = Gdk::GL::MODE_RGB | Gdk::GL::MODE_DEPTH;
	EXIT_IF_FAIL(2, glconfig=Gdk::GL::Config::create(glMode) );

	// Initialize the OpenGL scene widget (realization came later, no RAII)
	MyGTKGLSceneWidget glScene(glconfig);

	// Instantiate the GTK app (and realize glScene)
	// Could exit() the program if problem with OpenGL or OpenCL
	GTKWinMain gtkwinmain(glScene);

	// Run the app
	gtkKit.run(gtkwinmain);

	return 0;
}

// MyGTKGLSceneWidget implementation (extends a Gtk::DrawingArea with Gtk::GL::Widget)
//  I want to keep all interesting code parts in this file, in natural reading order
MyGTKGLSceneWidget::MyGTKGLSceneWidget(Glib::RefPtr<Gdk::GL::Config> &glconfig):
	continuous_play(false), need_recompute(false), time(0.0f)
{
	CALL_TRACE;
	set_gl_capability(glconfig);
	Gdk::EventMask mask = Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_MOTION_MASK \
			      | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK \
			      | Gdk::SCROLL_MASK;
	set_events(mask); // The containing window should have those attributes too

	// Camera is always looking the center of the mesh
	// this->camera affects (only) the camera position
	// rx,ry : the camera rotate around the mesh. Those vars are angles in degrees
	// tz : distance between the center of the mesh and the camera (roughly)
	this->camera.rx = -64.0f; this->camera.ry = 16.0f; this->camera.tz = 2.0f;
}

MyGTKGLSceneWidget::~MyGTKGLSceneWidget() { }

void MyGTKGLSceneWidget::on_size_request(Gtk::Requisition* requisition) {
	CALL_TRACE; // Technical stuff, GTK call this to ask for widget minimal size 
 	*requisition = Gtk::Requisition();
	requisition->width = 320; requisition->height = 240;
}

void MyGTKGLSceneWidget::on_realize() {
	CALL_TRACE; // This one runs once at window creation time
	// It's time to setup GL things that don't change on each frame
	Gtk::DrawingArea::on_realize();
	Glib::RefPtr<Gdk::GL::Window> glwindow = get_gl_window();

	// *** OpenGL BEGIN ***
	GLenum gl_res;
	if (!glwindow->gl_begin(get_gl_context())) {
		std::cerr << "Oops : glwindow->gl_begin(get_gl_context())" << std::endl;
		return;
	}

	EXIT_IF_FAIL(3, Gdk::GL::query_gl_extension("GL_ARB_vertex_buffer_object") );
	EXIT_IF_FAIL(4, glewInit() == 0 );
	
	// TODO : mesh size should not be fixed here
	//size_t mesh_width=256, mesh_height=256, group_size=256;
	//size_t mesh_width=512, mesh_height=512, group_size=256;
	size_t mesh_width=1024, mesh_height=1024, group_size=256;
	//size_t mesh_width=2048, mesh_height=2048, group_size=256; 

	GLuint gl_vbo=0;
	GLsizeiptr gl_vbo_data_size = mesh_width * mesh_height * sizeof(cl_float4);
	//std::cout << "gl_vbo_data_size==" << gl_vbo_data_size << std::endl;

	glGenBuffers(1, &gl_vbo);
	glBindBuffer(GL_ARRAY_BUFFER, gl_vbo);
	glBufferData(GL_ARRAY_BUFFER, gl_vbo_data_size, NULL, GL_DYNAMIC_DRAW);
	gl_res=glGetError();
	if ( gl_res != GL_NO_ERROR ) {
		std::cerr << "glBufferData(). Unable to allocate " << gl_vbo_data_size << " bytes in VRAM" << std::endl;
		std::cerr << gluErrorString(gl_res);
		EXIT_IF_FAIL(5, false);
	}

//#ifdef X11
		intptr_t gl_context = (intptr_t)glXGetCurrentContext();
		intptr_t gl_display = (intptr_t)glXGetCurrentDisplay();
//#else
//	#error This code works only for X11 systems for now
//#endif

	int cl_res = this->clKit.initCL(gl_display, gl_context, gl_vbo, mesh_width, mesh_height, group_size);
	EXIT_IF_FAIL(cl_res, cl_res==0);
	this->clKit.resetVBO(); // Just for displaying a flat mesh at start

	const char source[]=STRINGIFY(
		/* This is OpenCL kernel code (Syntax like C but it's a different language) */

		__kernel void water1(__global float4 *pos, unsigned int width, unsigned int height, float time) {
			/* Getting current vertex indices (could be seen as a 2D array of float4) */
			unsigned int nx = get_global_id(0);
			unsigned int ny = get_global_id(1);
			/* A float4 vector that hold the output vertex */
			float4 out;

			/* Calculate centered mesh coordinates [-1.0;1.0]² */
			out.x = nx / (float) width * 2.0f - 1.0f;
			out.y = ny / (float) height * 2.0f - 1.0f;

			/* Set some constants (should be preprocessor macros...) */
			float freq = 8.0f;
			float speed = 1.0f;
			float amp = 1.0f / 10.0f; /* 0.1f does NOT works for me ! WTF !!! */

			/* Calculate some intermediate values */
			float dist = hypot(out.x,out.y); /* =sqrt(out.x*out.x+out.y*out.y); */

			/* Calculate the desirated value of the mesh point z=f(x,y,t) */
			out.z = amp * sinpi( freq * dist + speed * time ) / dist ;
			out.w = 1.0f;	/* We always use normalized quaterinons here */

			pos[ny*width+nx] = out; /* Write output vertex */
		}
	);

	// TODO : change API, use only one string and split it at each blanks
	//std::list<std::string> knames;
	//knames.push_back("water1");
	cl_res = this->clKit.compileKernels(/*knames,*/ source, sizeof(source)-1);
	//knames.clear();
	EXIT_IF_FAIL(6, cl_res==0);

	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	// Projection setup is done at on_configure_event
	// Camera setup (ie MODELVIEW matrix) is done at on_expose_event
	
	glwindow->gl_end();
	// *** OpenGL END ***
}

bool MyGTKGLSceneWidget::on_configure_event(GdkEventConfigure* event) {
	CALL_TRACE ; // This one runs mainly when GTK GL Widget is resized

	float h=this->get_height();
	float w=this->get_width();
	Glib::RefPtr<Gdk::GL::Window> glwindow = get_gl_window();

	// *** OpenGL BEGIN ***
	if (!glwindow->gl_begin(get_gl_context())) {
		std::cerr << "Oops : glwindow->gl_begin(get_gl_context())" << std::endl;
		return false;
	}

	glViewport(0, 0, w, h);
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(60.0, w/h, 0.1, 10.0);

	glwindow->gl_end();
	// *** OpenGL END ***

	return true;
}

bool MyGTKGLSceneWidget::on_expose_event(GdkEventExpose* event) {
//	CALL_TRACE ; // This one runs mainly when GTK GL Widget have to be redrawn
	std::cout << "e" << std::flush;

	Glib::RefPtr<Gdk::GL::Window> glwindow = get_gl_window();

	// *** OpenCL BEGIN ***
	if (this->need_recompute) {
		this->need_recompute=false;
		//std::cout << "execKernel(\"water1\", " << this->time << ");" << std::endl;
		//TODO : Dynamic kernel usage ?
		int res = this->clKit.execKernel("water1", this->time);
		if ( res !=0 ) std::cerr << "execKernel() has returned " << res << std::endl;
		//std::cout << " -> " << res << std::endl;
	}
	// *** OpenCL END ***

	// *** OpenGL BEGIN ***
	if (!glwindow->gl_begin(get_gl_context())) {
		std::cerr << "Oops : glwindow->gl_begin(get_gl_context())" << std::endl;
		return false;
	}

	//Camera position update
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	glTranslatef(0.0, 0.0, this->camera.tz * -1.0);
	glRotatef(this->camera.rx, 1.0, 0.0, 0.0);
	glRotatef(this->camera.ry, 0.0, 0.0, -1.0);

	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	// Drawing all the stuff
	//TODO : do this (and camera update) in configure event ?
	float m_w=this->clKit.getMeshWidth();
	float m_h=this->clKit.getMeshHeight();
	//float s_w=this->get_width();
	float s_h=this->get_height();
	float t_z=this->camera.tz;

	// Vertex alpha blending automatic tuning (if big density then high transparency)
	// The comprehensible one 
	//float c1=(1024*1024)/(m_w*m_h);	// coef 1 decreases with mesh point quantity
	//float c2=(s_h*s_h)/(1024*1024);	// coef 2 increases with viewport pixel quantity
	//float c3=(2*2)/(t_z*t_z);		// coef 3 decreases with mesh-camera distance
	//float alpha=0.5*c1*c2*c3;		// Combine it all
	//if (alpha < 0.01f) alpha = 0.01f;	// Prevent values outside acceptable range
	//if (alpha > 1.0f) alpha = 1.0f;
	//std::cout <<"c1="<<c1<<" c2="<<c2<<" c3="<<c3<<" alpha="<<alpha<<std::endl;

	// The compacted one
	float alpha=2.0f*(s_h*s_h)/(m_w*m_h)/(t_z*t_z);
	if (alpha < 0.01f) alpha = 0.01f;
	if (alpha > 1.0f) alpha = 1.0f;
	
	glColor4f(0.40f,0.78f,0.97f,alpha); // XXX Blue is great but it's fixed here and shouldn't
	glBindBuffer(GL_ARRAY_BUFFER, this->clKit.getGLVBO());
	glVertexPointer(4, GL_FLOAT, 0, (GLvoid *) 0);
	glEnableClientState(GL_VERTEX_ARRAY);
	glDrawArrays(GL_POINTS, 0, this->clKit.getMeshItemCount());
	glDisableClientState(GL_COLOR_ARRAY);
	glBindBuffer(GL_ARRAY_BUFFER, 0);

	glwindow->gl_end();
	// *** OpenGL END ***

	glwindow->swap_buffers(); // Display the rendered image

	return true;
}

// Update time (and flags) for continuous_play mode
void MyGTKGLSceneWidget::step() {
	this->time += 0.01f;
	this->need_recompute = true;
}

// Combine all kind of mouse event and redirect them to a unique method : do_mouse_logic()
bool MyGTKGLSceneWidget::on_motion_notify_event (GdkEventMotion *event) {
	return do_mouse_logic(event->type, event->state, event->x, event->y);
}
bool MyGTKGLSceneWidget::on_button_press_event(GdkEventButton *event) {
	return do_mouse_logic(event->type, event->state | 1<<(7+event->button) , event->x, event->y);
}
bool MyGTKGLSceneWidget::on_button_release_event(GdkEventButton *event) {
	return do_mouse_logic(event->type, event->state, event->x, event->y);
}
bool MyGTKGLSceneWidget::on_scroll_event(GdkEventScroll *event) {
	return do_mouse_logic(event->type, event->state | 1<<(7+event->direction) , event->x, event->y);
}

// Define helper macros to filter mouse events in do_mouse_logic()
#define ALL_KEYBOARD_MODIFIERS ( GDK_SHIFT_MASK | GDK_CONTROL_MASK \
	| GDK_SUPER_MASK | GDK_HYPER_MASK | GDK_META_MASK )

#define MOUSE_CLIC(button, required_modifier_mask, allowed_extra_modifier_mask) if ( \
	type == GDK_BUTTON_RELEASE \
	&& ( state & button ) == button \
	&& ( state & required_modifier_mask ) == required_modifier_mask \
	&& ( ( state & ALL_KEYBOARD_MODIFIERS ) & ~( required_modifier_mask | allowed_extra_modifier_mask ) ) == 0 \
)

#define MOUSE_WHEEL(direction, required_modifier_mask, allowed_extra_modifier_mask) if ( \
	type == GDK_SCROLL \
	&& ( state & (1<<(7+direction)) ) == (1<<(7+direction)) \
	&& ( state & required_modifier_mask ) == required_modifier_mask \
	&& ( ( state & ALL_KEYBOARD_MODIFIERS ) & ~( required_modifier_mask | allowed_extra_modifier_mask ) ) == 0 \
)

#define MOUSE_DOUBLECLIC(state_mask) if ( \
	type == GDK_BUTTON_RELEASE \
	&& ( state & state_mask ) == state_mask \
	&& prev_type == GDK_2BUTTON_PRESS \
)
#define MOUSE_DRAG_START(state_mask) if ( \
	type == GDK_BUTTON_PRESS \
	&& ( state & state_mask ) == state_mask \
)
#define MOUSE_DRAGING(state_mask) if ( \
	( type == GDK_MOTION_NOTIFY ) \
	&& ( state & state_mask ) == state_mask \
)
//FIXME find bug with multiple mouse button release
#define MOUSE_DRAG_END(state_mask) if ( \
	type == GDK_BUTTON_RELEASE \
	&& ( state & state_mask ) == state_mask \
)

bool MyGTKGLSceneWidget::do_mouse_logic(GdkEventType type, guint state, guint x, guint y) {
	//CALL_TRACE ; // This one runs when a mouse event is catched by the GTK GL Widget
	std::cout << "m" << std::flush;
	/*
	 * type : the type of the event.
	 *	Simple motion : GDK_MOTION_NOTIFY (3)
	 *	Simple clic : GDK_BUTTON_PRESS then GDK_BUTTON_RELEASE (4 then 7)
	 *	Double clic : GDK_BUTTON_PRESS, GDK_BUTTON_RELEASE, GDK_BUTTON_PRESS, GDK_2BUTTON_PRESS, GDK_BUTTON_RELEASE (4 7 4 5 7)
	 *	Scroll : GDK_SCROLL (31)
	 *
	 * state : a bit-mask representing the state of the modifier keys and the pointer buttons.
	 * 	GDK_BUTTON1_MASK, ... , GDK_BUTTON5_MASK (mouse buttons)
	 * 	GDK_SCROLL_UP, GDK_SCROLL_DOWN, GDK_SCROLL_LEFT, GDK_SCROLL_RIGHT (mouse wheel scroll)
	 * 	GDK_SHIFT_MASK, GDK_LOCK_MASK, GDK_CONTROL_MASK (keyboard standard modifier keys)
	 *	GDK_MOD1_MASK, ... (normally MOD1 it is the Alt key)
	 * 	GDK_SUPER_MASK, GDK_HYPER_MASK, GDK_META_MASK (extra keybord modifier keys)
	 */
	static GdkEventType prev_type = GDK_NOTHING; // Static variable to hold previous mouse button event
	static guint drag_x=0, drag_y=0; // Static for DRAGING displacement calculus
	bool recompute=false; // Setting it to true will call OpenCL on next redraw
	bool redraw=false; // Setting it to true will queue a redraw to the widget (invalidate)

	//XXX For event filter debug (display all events)
	//std::cout << "event type " << type << " state " << state << " on (" << x << "," << y << ") " << std::endl;

	/* *** BEGIN event filtering *** */ 
	MOUSE_DRAG_START(GDK_BUTTON2_MASK) {
		drag_x=x; drag_y=y;
	}

	// Carmera rotation
	MOUSE_DRAGING(GDK_BUTTON2_MASK) {
		float mouse_sensivity = 0.2f;
		gint dx = drag_x - x; // Delta movement (since last event)
		gint dy = drag_y - y; // Not unsigned !
		this->camera.rx -= mouse_sensivity * dy; // Camera position update
		this->camera.ry += mouse_sensivity * dx; // Yes dy for camera.rx, and -= operator :
		// GTK mouse coords and OpenGL ones are not on the same coords system
		drag_x = x; drag_y = y;
		redraw=true;
	}

	// Camera zoom-in
	MOUSE_WHEEL(GDK_SCROLL_UP, 0, 0) {
		
		if (this->camera.tz - 0.5f >= 0.5f) {
			this->camera.tz -= 0.5f;
			//std::cout << "camera.tz == " << this->camera.tz << std::endl;
			redraw=true;
		}
	}

	// Camera zoom-out
	MOUSE_WHEEL(GDK_SCROLL_DOWN, 0, 0) {
		if (this->camera.tz + 0.5f <= 9.0f) {
			this->camera.tz += 0.5f;
			//std::cout << "camera.tz == " << this->camera.tz << std::endl;
			redraw=true;
		}
	}

	/*
	MOUSE_CLIC(GDK_BUTTON1_MASK, 0, 0) {
		//TODO 
		this->clKit.execKernel("water1", 0.0f);
		redraw=true;
	}*/

	// Enabling continous_play mode
	MOUSE_DRAG_START(GDK_BUTTON1_MASK) {
		this->continuous_play=true;
		// The trick to have a perpetual redraw : generate a draw event in the idle signal handler
		Glib::signal_idle().connect( sigc::mem_fun(*this, &MyGTKGLSceneWidget::on_gtk_idle) );
	}

	// Disabling continous_play mode
	MOUSE_DRAG_END(GDK_BUTTON1_MASK) {
		this->continuous_play=false;
	}
	
	// Demo filters
	MOUSE_CLIC(GDK_BUTTON1_MASK, GDK_SHIFT_MASK, GDK_CONTROL_MASK) {
		std::cout << "Mouse 1 clic with shift or control-shift" << std::endl;
	}
	
	MOUSE_DOUBLECLIC(GDK_BUTTON3_MASK) {
		std::cout << "Mouse 1 double clic" << std::endl;
	}
	/* *** END event filtering *** */


	// Previous button event retention for double-clic filtering (used by the macros)
	if ( type == GDK_BUTTON_PRESS || type == GDK_2BUTTON_PRESS || type == GDK_BUTTON_RELEASE ) {
		prev_type=type;
	}

	if ( redraw ) { queue_draw(); std::cout << "q" << std::flush; }
	if ( recompute ) { this->need_recompute=true; }
	return true;
}

bool MyGTKGLSceneWidget::on_gtk_idle() {
	//CALL_TRACE ; // This one runs when there is no more GTK event in the current iteration
	
	// Note : This signal is not always connected.
	// Connected by Glib::signal_idle().connect() call
	// Disconnected automatically the first time it returns false
	if ( ! continuous_play ) return false;

	//TODO : compute and display FPS when in continuous_play mode
	this->step();
	this->queue_draw();
	return true;
}