

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

#include "iglu.h"
using namespace iglu;

bool                    lockedMotion = true;

// Where is our object located at?
vec3                    objCenter    = vec3( 3, 3, 0 );

// Where does the camera start out?
vec3                    origLight1   = vec3( 1, 0, 0 );
vec3                    origLight2   = vec3( 0, 1, 0 );

// Viewing parameters for the current pinhole camera
vec3                    viewLight1   = origLight1;
vec3                    viewLight2   = origLight2;
vec3                    viewAt       = vec3( 2.5, 2.5, 0 );

// Given the eye & at, and that this is a 2D image, find an "up" that's in the image plane
vec3                    viewUp       = vec3( 0.0, 0.0, 1.0 ).Cross(viewAt-viewLight1).vNormalize(); 

// Out model reader
IGLUOBJReader::Ptr      objReader    = 0;

// Simple projection matrix for our pinhole camera. 
float mat[] = { 1, 0, 0, 0,   0, 1, 0, 0,  0, 0, 1, -1,   0, 0, 1, 0 };
IGLUMatrix4x4           projPin      = IGLUMatrix4x4( mat );

// Find the area projection matrix (see Hunt & Johnson' "The Area Perspective Transform")
float areaProjMat[] = { 1, 0, 0, 1,   0, 1, 0, 1,   0, 0, 1, 1,   0, 0, 0, -1 };
IGLUMatrix4x4           projXYArea   = IGLUMatrix4x4( areaProjMat );

// We actually don't want to display the xy plane in projection space, but
//     rather the same plane we show in pinhole space.   Given that y is typically
//     "up" and z is "depth" (or "away from the camera") that means we want to
//    show the yz plane.  To keep y as "up", we'll just swap x and z axes.
float swapMat[] = { 0, 0, 1, 0,   0, 1, 0, 0,   1, 0, 0, 0,   0, 0, 0, 1 };
IGLUMatrix4x4           swapAxes     = IGLUMatrix4x4( swapMat );

// Key matriices variable for this program
IGLUMatrix4x4           modelOrtho   = IGLUMatrix4x4::Identity();
IGLUMatrix4x4           modelPinhole = IGLUMatrix4x4::LookAt( viewLight1, viewAt, viewUp );
IGLUMatrix4x4           project      = IGLUMatrix4x4::Ortho( 0, 5, 0, 5 ); 

// Shaders used in this sample program
IGLUShaderProgram::Ptr  ortho2D      = 0;
IGLUShaderProgram::Ptr  areaProj     = 0;

// An intermediate framebuffer object we'll use to store a multisample buffer
IGLUFramebuffer::Ptr    fbo          = 0;
IGLUFramebuffer::Ptr    fboPersp     = 0;

// A vertex array to store our line geometry
IGLUVertexArray::Ptr    lightLineArray1 = 0;
IGLUVertexArray::Ptr    lightLineArray2 = 0;

// A frame timer we'll use to display a counter on screen
IGLUFrameRate::Ptr      frameTime    = 0;

// A simple trackball to rotate our geometry
IGLUMouseTranslator::Ptr     move     = 0;
IGLU2DMouseRotator::Ptr    rotate     = 0;
IGLU2DMouseRotator::Ptr    rotate2    = 0;
IGLU2DMouseRotator::Ptr    cowRotator = 0;


float persScale = 4.0;

// The current size of our window and viewport positioning
//    This is (re-)computed inside the reshape callback
int  winWidth, winHeight;
int  viewWidth, viewHeight;
int  view1Off, view2Off;

// Data for the camera lines we'll draw eminating from the pinhole.
//    Most of this is a garbage array, as we'll call SetPinholeCameraRays()
//    to fill this up with data for the current view (e.g., from viewEye)
float lightLineData1[128];
float lightLineData2[128];



void display ( void )
{
	// Start timing this frame
	frameTime->StartFrame();	

	// For our actual displayed screen, clear to gray.
	glClearColor(0,0,0,1);
	glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

	// Compute our cow transform
	IGLUMatrix4x4 cowTransform = move->GetMatrix() * IGLUMatrix4x4::Translate( objCenter ) * 
		                         cowRotator->GetMatrix() * IGLUMatrix4x4::Scale( 0.2 );

	// Draw our viewport 1 ("pinhole") on screen
	glViewport( view1Off, 2, viewWidth, viewHeight );
		ortho2D->Enable();
		    // Draw the cow, scaled to a reasonable size, translated so it's not
			//    in the center, and then modified by whatever user translation may have occured
			ortho2D["color"]  = vec4(1,0,0,1);
			ortho2D["model"]  = modelOrtho * cowTransform;
			objReader->Draw( ortho2D );

			// Draw the lines eminating from the pinhole camera.
			ortho2D["color"] = vec4(0,0.5,0.5,1);
			ortho2D["model"] = modelOrtho;
			lightLineArray1->DrawArrays( GL_LINES, 0, 64 );
			ortho2D["color"] = vec4(0.5,0.5,0,1);
			lightLineArray2->DrawArrays( GL_LINES, 0, 64 );
		ortho2D->Disable();

	// Draw our viewport 1 ("perpspective") on screen
	glViewport( view2Off, 2, viewWidth, viewHeight );
		areaProj->Enable();
			// Set up data to transform this frame into "light space"
			areaProj["lightPos1"] = viewLight1;
			areaProj["lightPos2"] = viewLight2;
			areaProj["lookAt"]    = viewAt;

			// Set up transform to perform the area transform, then
			//    project into the 2D image we want do display;
			areaProj["displayProj"]  = IGLUMatrix4x4::Ortho( 0, persScale, 0, persScale ); 
			areaProj["areaProj"]     = projXYArea; 

			// Draw the cow.
			areaProj["color"]    = vec4(1,0,0,1);
			areaProj["model"]    = cowTransform;
			objReader->Draw( areaProj );

			// Draw the first set of lines
			areaProj["model"]    = IGLUMatrix4x4::Identity(); 
			areaProj["color"]    = vec4(0,0.5,0.5,1);
			lightLineArray1->DrawArrays( GL_LINES, 0, 64 );

			// Draw the second set of lines
			areaProj["color"]    = vec4(0.5,0.5,0,1);
			lightLineArray2->DrawArrays( GL_LINES, 0, 64 );
		areaProj->Disable();

	// Draw miscellaneous frame information.  This needs a full-screen viewport
	glViewport( 0, 0, winWidth, winHeight );

	// Draw framerate
	char buf[32];
	sprintf( buf, "%.1f fps", frameTime->EndFrame() );
	IGLUDraw::DrawText( IGLU_FONT_FIXED, winWidth-180, winHeight-27, buf );

	// Draw instructions
	IGLUDraw::DrawText( IGLU_FONT_VARIABLE, 85, winHeight-27, "(M) Change Manipulation Mode" );
	if (lockedMotion)
	{
		IGLUDraw::DrawText( IGLU_FONT_VARIABLE, 5, winHeight-57, "(Left) Move object", 0.7);    
		IGLUDraw::DrawText( IGLU_FONT_VARIABLE, 165, winHeight-57, "(Middle) Rotate cow", 0.7 );
		IGLUDraw::DrawText( IGLU_FONT_VARIABLE, 340, winHeight-57, "(Right) Rotate lights", 0.7 );
	}
	else
		IGLUDraw::DrawText( IGLU_FONT_VARIABLE, 5, winHeight-57, "(Left) Move object    (Middle) Rotate yellow light    (Right) Rotate cyan light", 0.7);    
	
	// Label the two viewports
	IGLUDraw::DrawColorText( IGLU_FONT_VARIABLE, view1Off+5, 5, vec4(1.0, 1.0, 0.0, 1.0 ), "Pinhole Camera" );
	IGLUDraw::DrawColorText( IGLU_FONT_VARIABLE, view2Off+5, 5, vec4(1.0, 1.0, 0.0, 1.0 ), "Area Perspective Space" );

	sprintf( buf, "Scale Perspective Space: %dx", int(persScale) );
	IGLUDraw::DrawColorText( IGLU_FONT_VARIABLE, view2Off+5, winHeight-57, vec4(1.0, 1.0, 0.0, 1.0 ), buf, 0.7 );
	IGLUDraw::DrawColorText( IGLU_FONT_VARIABLE, view2Off+200, winHeight-57, vec4(1.0, 1.0, 0.0, 1.0 ), "(Change +/-)", 0.7 );
}

void mouse( int button, int state, int x, int y )
{
	// Handle trackball clicks and releases
	if ( state == GLUT_DOWN && button == GLUT_LEFT_BUTTON && (x < view1Off+viewWidth))
		move->SetOnClick( x, y );
	else if ( state == GLUT_DOWN && button == GLUT_RIGHT_BUTTON && (x < view1Off+viewWidth))
		rotate->SetOnClick( x, y );
	else if ( state == GLUT_DOWN && button == GLUT_MIDDLE_BUTTON && (x < view1Off+viewWidth))
	{	
		if (lockedMotion)
			cowRotator->SetOnClick( x, y );
		else
			rotate2->SetOnClick( x, y );
	}
	else if (state == GLUT_UP )
	{
		move->Release();
		rotate->Release();
		rotate2->Release();
		cowRotator->Release();
	}
}

// This gets called every time the eye moves to reset the lines eminating from it.
void SetPinholeCameraRays( void )
{
	// Create rays leaving the eye (viewEye) that go in the general direction
	//     of the lookAt point (viewAt) just like rays would for a camera.
	vec3 toAt1  = (viewAt-viewLight1).vNormalize();
	vec3 upVec1 = toAt1.Cross(vec3(0,0,-1)).vNormalize();
	vec3 toAt2  = (viewAt-viewLight2).vNormalize();
	vec3 upVec2 = toAt2.Cross(vec3(0,0,-1)).vNormalize();
	
	vec3 L1ToL2   = (viewLight2-viewLight1).vNormalize();
	vec3 L2ToL1   = -L1ToL2; 
	vec3 rotateTo = L1ToL2.Cross( vec3( 0, 0, 1 ) ).vNormalize();

	// Put the grid on the correct side
	if (rotateTo.Dot( toAt1 ) < 0) 
		rotateTo = -rotateTo;

	for (int i=0; i<=31; i++)
	{
		
		float angle = i*4.8*IGLU_PI/180.0;                 
		float cosAngle = cos(angle), sinAngle = sin(angle);    
		float factor1 = 0.01, factor2 = 50.0;

		lightLineData1[4*i+0]=viewLight1.X()+factor1*(cosAngle*L1ToL2.X()+sinAngle*rotateTo.X());                                
		lightLineData1[4*i+1]=viewLight1.Y()+factor1*(cosAngle*L1ToL2.Y()+sinAngle*rotateTo.Y());
		lightLineData1[4*i+2]=viewLight1.X()+factor2*(cosAngle*L1ToL2.X()+sinAngle*rotateTo.X());  
		lightLineData1[4*i+3]=viewLight1.Y()+factor2*(cosAngle*L1ToL2.Y()+sinAngle*rotateTo.Y());

		lightLineData2[4*i+0]=viewLight2.X()+factor1*(cosAngle*L2ToL1.X()+sinAngle*rotateTo.X());                                 
		lightLineData2[4*i+1]=viewLight2.Y()+factor1*(cosAngle*L2ToL1.Y()+sinAngle*rotateTo.Y());
		lightLineData2[4*i+2]=viewLight2.X()+factor2*(cosAngle*L2ToL1.X()+sinAngle*rotateTo.X());  
		lightLineData2[4*i+3]=viewLight2.Y()+factor2*(cosAngle*L2ToL1.Y()+sinAngle*rotateTo.Y());
	}
}

void motion( int x, int y )
{
	// Update the trackball matrix when we detect motion.
	move->UpdateOnMotion( x, y ); 
	rotate->UpdateOnMotion( x, y );
	rotate2->UpdateOnMotion( x, y );
	cowRotator->UpdateOnMotion( x, y );
	
	// If we did, in fact have rotation occur, lets handle it
	if ( rotate->HasChanged() || 
		 (!lockedMotion && rotate2->HasChanged()) )
	{
		// Compute the new eye positions.  (Rotate the view vector around the at point)
		vec3 newLight1     = origLight1-viewAt;
		vec4 rotatedLight1 = rotate->ApplyMatrix( vec4( newLight1.X(), newLight1.Y(), newLight1.Z(), 1.0 ) );
		viewLight1         = vec3( rotatedLight1.X(), rotatedLight1.Y(), rotatedLight1.Z() ) + viewAt;
		viewUp             = vec3( 0.0, 0.0, 1.0 ).Cross(viewAt-viewLight1).vNormalize(); 

		vec3 newLight2     = origLight2-viewAt;
		vec4 rotatedLight2 = lockedMotion ? 
			                 rotate->ApplyMatrix( vec4( newLight2.X(), newLight2.Y(), newLight2.Z(), 1.0 ) ) :
                             rotate2->ApplyMatrix( vec4( newLight2.X(), newLight2.Y(), newLight2.Z(), 1.0 ) ) ;
		viewLight2         = vec3( rotatedLight2.X(), rotatedLight2.Y(), rotatedLight2.Z() ) + viewAt;

		// Recompute the look-at matrix needed for transforming into perspective space
		modelPinhole    = IGLUMatrix4x4::LookAt( viewLight1, viewAt, viewUp );

		// Recompute where the lines eminating from the eye are
		SetPinholeCameraRays();
		lightLineArray1->SetVertexArraySubset( 0, sizeof( lightLineData1 ), lightLineData1 );
		lightLineArray2->SetVertexArraySubset( 0, sizeof( lightLineData2 ), lightLineData2 );
	}

	// Update the cow rotator so it behaves usefully, instead of rotating around an arbitraty
	//    screen point, make it rotate around the center of the cow
	if ( move->HasChanged() )
	{
		// Get the matrix to translate the cow
		IGLUMatrix4x4 cowTranslator = move->GetMatrix() * IGLUMatrix4x4::Translate( objCenter );
		// Translate the origin to the center of the cow, project into NDC to find center of rotation
		vec4 ndcRotation = project * cowTranslator * vec4(0,0,0,1);
		// Find the view-space pixel to rotate around
		vec2 pixRotate = (0.5f * vec2( ndcRotation.X(), ndcRotation.Y() ) + vec2( 0.5, 0.5 ) ) * vec2( viewWidth, viewHeight );
		// Transform into window coordinate & set.
		cowRotator->ResetRotationCenter( 2+pixRotate.X(), winHeight-2-pixRotate.Y() );
	}
}

void keys(unsigned char key, int x, int y)
{
	switch(key)
	{
	case 27:
	case'q':
	case'Q':
		exit(0);
		break;
	case 'M':
	case 'm':
		lockedMotion = !lockedMotion;
		break;
	case '+':
	case '=':
		persScale = iglu::clamp( persScale + 1.0f, 1.0f, 16.0f );
		break;
	case '-':
	case '_':
		persScale = iglu::clamp( persScale - 1.0f, 1.0f, 16.0f );
		break;
	}
}


// What happens when you resize?  This isn't particularly compelling,
//    But resize the viewports and store the sizes so we can position
//    text correctly
void reshape( int w, int h )
{
	glViewport( 0, 0, winWidth = w, winHeight = h );
	move->ResizeInteractorWindow( winWidth, winHeight );
	rotate->ResizeInteractorWindow( winWidth, winHeight );
	rotate2->ResizeInteractorWindow( winWidth, winHeight );
	cowRotator->ResizeInteractorWindow( winWidth, winHeight );

	// Recompute viewport positioning information
	viewHeight = winHeight-58;
	viewWidth  = viewHeight;
	view1Off   = 2;
	view2Off   = 4+viewWidth;

	// Reset where the center of rotation is for the rotator interactors
	rotate->ResetRotationCenter( 2+viewWidth*0.5, 58+viewHeight*0.5 );
	rotate2->ResetRotationCenter( 2+viewWidth*0.5, 58+viewHeight*0.5 );

	// The cow rotator is different -- we want to rotate around the center of 
	//    the cow.  So compute that.
	{
		// Get the matrix to translate the cow
		IGLUMatrix4x4 cowTranslator = move->GetMatrix() * IGLUMatrix4x4::Translate( objCenter );
		// Translate the origin to the center of the cow, project into NDC to find center of rotation
		vec4 ndcRotation = project * cowTranslator * vec4(0,0,0,1);
		// Find the view-space pixel to rotate around
		vec2 pixRotate = (0.5 * vec2( ndcRotation.X(), ndcRotation.Y() ) + vec2( 0.5, 0.5 ) ) * vec2( viewWidth, viewHeight );
		// Transform into window coordinate & set.
		cowRotator->ResetRotationCenter( 2+pixRotate.X(), winHeight-2-pixRotate.Y() );
	}
}

void OpenGLInitialization( void )
{
	// Create our frame timer
	frameTime = new IGLUFrameRate( 100 );

	// Create our interactors to interact with the scene
	move   = new IGLUMouseTranslator( 1030, 570, 1.0, vec3(10.0,0,0), vec3(0.0,5.0,0) );
	rotate = new IGLU2DMouseRotator( 1030, 570, 257, 303 );
	rotate2 = new IGLU2DMouseRotator( 1030, 570, 257, 303 );
	cowRotator = new IGLU2DMouseRotator( 1030, 570, 257, 303 );

	// Load the texture with IGLU
	objReader  = new IGLUOBJReader( "../../CommonSampleFiles/models/cow.obj" );
	
	// Create the framebuffers we'll use to draw our two inset viewports
	fbo      = IGLUFramebuffer::Create( GL_RGBA32F, 1024, 1024 );
	fboPersp = IGLUFramebuffer::Create( GL_RGBA32F, 1024, 1024 );
	
	// Load the shader that draws onto the screen.  It takes in various
	//    matrices, so you can use it to do almost anything you really want.
	ortho2D          = new IGLUShaderProgram( "shaders/drawGeometry.vert.glsl", "shaders/drawGeometry.frag.glsl");
	ortho2D->SetProgramDisables( IGLU_GLSL_DEPTH_TEST ); 
	ortho2D["proj"]  = project;
	ortho2D["model"] = modelOrtho;

	areaProj         = new IGLUShaderProgram( "shaders/drawIntoPerspective.vert.glsl", "shaders/drawGeometry.frag.glsl");
	areaProj->SetProgramDisables( IGLU_GLSL_DEPTH_TEST ); 
	
	// Create the list storing the lines eminating from the light
	lightLineArray1 = new IGLUVertexArray();
	SetPinholeCameraRays();            // Performs computations to locate them.
	lightLineArray1->SetVertexArray( sizeof( lightLineData1 ), lightLineData1, IGLU_DYNAMIC|IGLU_DRAW );
	lightLineArray1->EnableAttribute( IGLU_ATTRIB_VERTEX, 2, GL_FLOAT );

	lightLineArray2 = new IGLUVertexArray();
	lightLineArray2->SetVertexArray( sizeof( lightLineData2 ), lightLineData2, IGLU_DYNAMIC|IGLU_DRAW );
	lightLineArray2->EnableAttribute( IGLU_ATTRIB_VERTEX, 2, GL_FLOAT );
}

int main(int argc, char** argv)
{
	// Create a FLTK window using IGLUWindow()
	IGLUWindow *myWin = new IGLUWindow( 1030, 570, 
		                                "Advanced IGLU Example:  An Area Perspective Transform Demonstration" );

	// Set the FLTK window's properties
	myWin->SetWindowProperties( IGLU_WINDOW_NO_RESIZE |	
								IGLU_WINDOW_DOUBLE |
								IGLU_WINDOW_BLEND |
								IGLU_WINDOW_REDRAW_ON_IDLE );

	// Setup a function that gets called once the OpenGL context has been initialized
	myWin->SetPreprocessOnGLInit( OpenGLInitialization );

	// Setup the user-interface callbacks
	myWin->SetDisplayCallback( display );
	myWin->SetIdleCallback( IGLUWindow::NullIdle );
	myWin->SetKeyboardCallback( keys );
	myWin->SetMouseButtonCallback( mouse );
	myWin->SetReshapeCallback( reshape );
	myWin->SetActiveMotionCallback( motion );

	// Create (i.e., show) the window we've created
	myWin->CreateWindow( argc, argv );

	// Start the FLTK interaction loop
	IGLUWindow::Run();
	return 0;
}
