#include <GL/glew.h>
#include <stdio.h>

// All headers are automatically included from "iglu.h"
#include "iglu.h"
using namespace iglu;

// These are handles to the two windows, should you need to modify their
//   behavior after window creation.
IGLUWindow::Ptr         myWin = 0;
IGLUWidgetWindow::Ptr   uiWin = 0;

// Variables controllable by the ui-widget
IGLUBool				visualizeShadowmap ( false,                                     "Visualize shadowmap" );
IGLUBool				useLambertian      ( false,                                     "Use Lambertian shading" );
IGLUFloat               zBias              ( -0.005, IGLURange<float>(-0.02,0), 0.0002, "Shadow map bias" );
IGLUInt                 smapRes            ( 1024,   IGLURange<int>(128,4096),  1,      "Shadow map resolution" );
IGLUFloat               lightMovement      ( 3.5,    IGLURange<float>(0,4), 0.04,       "Light movement" );
IGLUFloat               lightNear          ( 0.1,    IGLURange<float>(0.1,3),  0.1,     "Light near plane" );
IGLUFloat               lightFar           ( 9.3,    IGLURange<float>(5.0,10), 0.1,     "Light far plane" );
IGLUInt                 visualizeLayer     ( 0,      IGLURange<int>(0,1),      1,       "Displayed Shadow Map Layer" );

//The scene geometry consists of two objects: a wavefront obj file and a square 
IGLUOBJReader::Ptr      objCow     = 0;
IGLUOBJReader::Ptr      boxReader  = 0;

//We use two shaders--one to create the shadow map, another to query the shadow map
//These are accessed using the enum below
IGLUShaderProgram::Ptr  shaders[3] = {0, 0, 0};
enum ShaderType{ CREATE = 0, QUERY = 1, DISPLAY = 2 };

//Trackball for interaction
IGLUTrackball::Ptr		ball = 0;

//The framebuffer holding our shadow map
IGLUFramebuffer::Ptr    parabSMapFBO;

// Location of the light and the eye
vec3            lightPos = vec3(2.78,5.25,2.795);
vec3            eyePos   = vec3(0.0,0.0,2.0);

// Matrices for setting up the view from the eye
IGLUMatrix4x4   eyeProj = IGLUMatrix4x4::Perspective( 38.5, 1.0, 5, 20 );
IGLUMatrix4x4   eyeView = IGLUMatrix4x4::LookAt( vec3(2.78, 2.73, -8.00), 
												vec3(2.78, 2.73, 2.795), 
												vec3::YAxis() );

// Matrices for positioning our geometry relative to the world origin
IGLUMatrix4x4   cowPosition   = IGLUMatrix4x4::Translate( 2.78, 1.2, 2.795 ) * IGLUMatrix4x4::Scale( 2.0f ) * 
                                IGLUMatrix4x4::Rotate( -45, vec3::YAxis() );
IGLUMatrix4x4   planePosition = IGLUMatrix4x4::Translate( -0.45*vec3::YAxis() ) * IGLUMatrix4x4::Scale( 2.0f ) *
                                IGLUMatrix4x4::Rotate( 90, vec3::XAxis() );


void display ( void )	
{
	glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

	// Compute the eye-space light position (used multiple times per frame)
	vec4 esLightPos = eyeView * vec4( lightPos.X(), lightPos.Y(), lightPos.Z(), 1.0f );
	
	//Render into the shadow map.  This can be done with or without a color mask.
	//   When no color channels are enabled for rendering, creating the shadow map
	//   is significantly faster (usually 2-4x), so color masking is usually a good
	//   idea for when rendering only to a depth buffer.
	parabSMapFBO->Bind();
	glColorMask( GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE );
	parabSMapFBO->Clear();

		// Draw our objects in the correct position
		shaders[CREATE]->Enable(); 
		    shaders[CREATE]["eyeView"]    = eyeView;
		    shaders[CREATE]["esLightPos"] = esLightPos;
			shaders[CREATE]["model"]      = cowPosition * ball->GetMatrix();
			objCow->Draw( shaders[CREATE] ); 
			// We don't draw the Cornell-box walls into the dual-paraboloid shadow map 
			//     for two reasons:  (1) They don't cast shadows on anything, and more
			//     importantly (2) large planar surfaces need to be subdivided/tessellated 
			//     when projected into non-Euclidean spaces like the dual-paraboloid space
			//     (otherwise they lead to nasty artifacts!)
		shaders[CREATE]->Disable(); 

	// Finish rendering our shadow map
	glColorMask( GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE );
	parabSMapFBO->Unbind();


	// Did the user ask to just draw the shadow map?
	if( visualizeShadowmap )
	{
		shaders[DISPLAY]["layer"] = float(visualizeLayer.GetValue());	                // Which layer?
		IGLUDraw::Fullscreen( shaders[DISPLAY], parabSMapFBO[IGLU_DEPTH], "copyTex" );  // Draw it
	}
	else // Nope.  So draw a full scene with shadows!
	{	
		// Setup some values constant over all the geometry in the frame
		shaders[QUERY]["useLambertian"] = useLambertian ? 1.0f : 0.0f;
		shaders[QUERY]["esLightPos"]    = esLightPos;
		shaders[QUERY]["view"]          = eyeView;
		shaders[QUERY]["sMapBias"]      = zBias;
		
		// Draw the geometry, modifying per-geometry shader variables as needed
		shaders[QUERY]->Enable();
			shaders[QUERY]["model"] = cowPosition * ball->GetMatrix();
			objCow->Draw( shaders[QUERY] ); 

			shaders[QUERY]["model"] = IGLUMatrix4x4::Identity();
			boxReader->Draw( shaders[QUERY] );
		shaders[QUERY]->Disable();
	}
}


// Code that initializes our OpenGL state.  This is guaranteed (by IGLUWindow) to be 
//    called after the OpenGL context & all extensions have been initialized
void OpenGLInitialization( void ){
	
	//Load obj files
	boxReader    = new IGLUOBJReader( "../../CommonSampleFiles/Models/testBox.obj" );
	objCow       = new IGLUOBJReader( "../../CommonSampleFiles/Models/cow.obj", IGLU_OBJ_UNITIZE );
	IGLUOBJMaterialReader::FinalizeMaterialsForRendering();

	// Create a virtual trackball
	ball         = new IGLUTrackball( myWin->w(), myWin->h() );
	
	// Create a shadow map.  Make sure to use nearest neighbor on the z-buffer/shadow map.  
	parabSMapFBO = IGLUFramebuffer::CreateArray(GL_R8, smapRes, smapRes, 2, true, false);
	parabSMapFBO[IGLU_DEPTH].SetTextureParameters( IGLU_MAG_NEAREST | IGLU_MIN_NEAREST );
	
	//Load a shader that creates a shadow map
	shaders[CREATE] = new IGLUShaderProgram( "shaders/paraboloidShadowMapTransform.vert.glsl", 
		                                     "shaders/paraboloidShadowMapTransform.geom.glsl",
											 "shaders/paraboloidShadowMapTransform.frag.glsl" );
	shaders[CREATE]["nearDist"]   = lightNear;
	shaders[CREATE]["farDist"]    = lightFar;
	
	// Load a shader that queries the shadow map
	shaders[QUERY] = new IGLUShaderProgram("Shaders/test.vert.glsl", "Shaders/query-shadowmap.frag.glsl");
	shaders[QUERY]["shadowTex"]   = parabSMapFBO[IGLU_DEPTH];
	shaders[QUERY]["nearDist"]    = lightNear;
	shaders[QUERY]["farDist"]     = lightFar;
	shaders[QUERY]["proj"]        = eyeProj;
	shaders[QUERY]["sMapBias"]    = zBias;
	shaders[QUERY]["matlInfoTex"] = IGLUOBJMaterialReader::s_matlCoefBuf;

	//Load a shader that visualizes the shadow map
	shaders[DISPLAY] = new IGLUShaderProgram( "shaders/displayShadowArray.vert.glsl",
									  		  "shaders/displayShadowArray.frag.glsl" );
	shaders[DISPLAY]["layer"]   = float(visualizeLayer.GetValue());	
}

// Track any updates to the trackball matrix when the mouse moves 
void Motion(int x, int y)
{
	ball->UpdateOnMotion(x, y);
}

// When the user clicks/releases in the main window, start/stop tracking with our trackball
void Button(int button, int state, int x, int y  )
{
	if(IGLU_EVENT_DOWN == state)
		ball->SetOnClick( x, y );
	else
		ball->Release();
}

// When the user resizes the shadow map with the slider, we need to actually resize
//    the framebuffer object, so this callback function needs to be called
void UpdateShadowMapResolution( void *uiInt )
{
	int size = ((IGLUInt *)uiInt)->GetValue();
	parabSMapFBO->Resize( size, size );
}

// Move the light via a slider
void UpdateLightPosition( void *uiFloat )
{
	float pos = ((IGLUFloat *)uiFloat)->GetValue();
	if (pos >=0 && pos<1)
		lightPos = vec3(5.360,(1.0f-pos)*5.15+0.2,2.795);
	else if (pos >= 1 && pos<2)
		lightPos = vec3((2.0-pos)*5.360+0.1, 0.2, 2.795);
	else if (pos >= 2 && pos<3)
		lightPos = vec3( 0.1, (pos-2.0)*5.15+0.2, 2.795);
	else if (pos >= 3 && pos<4)
		lightPos = vec3( (pos-3.0)*5.360+0.1, 5.35, 2.795);
}

void UpdateLightProjection( void *uiFloat )
{
	shaders[CREATE]["nearDist"]   = lightNear;
	shaders[CREATE]["farDist"]    = lightFar;
	shaders[QUERY]["nearDist"]    = lightNear;
	shaders[QUERY]["farDist"]     = lightFar;
}


int main(int argc, char** argv)
{
	// Create our main window
	myWin = new IGLUWindow( 768, 768, "Simple IGLU Example:  Create and Use a Dual-Paraboloid Shadow Map" );
	myWin->SetWindowProperties( IGLU_WINDOW_NO_RESIZE |	
								IGLU_WINDOW_DOUBLE |
								IGLU_WINDOW_REDRAW_ON_IDLE |
								IGLU_WINDOW_W_FRAMERATE ); 
	myWin->SetDisplayCallback( display );  
	myWin->SetIdleCallback( IGLUWindow::NullIdle );
	myWin->SetPreprocessOnGLInit( OpenGLInitialization );
	myWin->SetActiveMotionCallback(Motion);
	myWin->SetMouseButtonCallback(Button);
	myWin->CreateWindow( argc, argv );     
	
	// Create our widget window & add our widgets to it
	uiWin = new IGLUWidgetWindow( 300, 450, "UI Widget Window" );
	uiWin->AddWidget( &zBias );
	uiWin->AddWidget( &smapRes );
	uiWin->AddWidgetSpacer();
	uiWin->AddWidget( &lightMovement );
	uiWin->AddWidget( &lightNear );
	uiWin->AddWidget( &lightFar );
	uiWin->AddWidgetSpacer();
	uiWin->AddWidget( &useLambertian );
	uiWin->AddWidgetSpacer();
	uiWin->AddWidget( &visualizeShadowmap );
	uiWin->AddWidget( &visualizeLayer );
	myWin->SetWidgetWindow( uiWin );
	
	// Some of our widget/variables need callbacks to affect OpenGL state
	//    when they change, so setup the callback functions here.
	smapRes.SetCallback( UpdateShadowMapResolution );
	lightMovement.SetCallback( UpdateLightPosition );
	lightNear.SetCallback( UpdateLightProjection );
	lightFar.SetCallback( UpdateLightProjection );

	// Start running our IGLU OpenGL program!
	IGLUWindow::Run();
	return 0;
}
