/*****************************************************************************
 * $Id: eng_opengl.cc,v 1.109 2006/08/10 03:18:58 devel Exp $
 * @file
 * @author	Jim E. Brooks  http://www.jimbrooks.org
 * @brief	OpenGL-specific functions.
 *//*
 * LEGAL:	COPYRIGHT (C) 2004 JIM E. BROOKS
 *          THIS SOURCE CODE IS RELEASED UNDER THE TERMS
 *          OF THE GNU GENERAL PUBLIC LICENSE (GPL).
 *****************************************************************************/

#define ENG_OPENGL_CC 1
#include "eng.hh"
#include "eng_opengl.hh"
#include "eng_init.hh"
#include "eng_polygon.hh"
#include "gfx_file.hh"
#include "gfx_stream.hh"

////////////////////////////////////////////////////////////////////////////////
////////////////////////////  OpenglState class  ///////////////////////////////
////////////////////////////////////////////////////////////////////////////////

class OpenglState
{
PREVENT_COPYING(OpenglState)
public:
	OpenglState( void )
	{
		mReady = false;
	}

	// To be called when OpenGL is raised.
	void
	Init( void )
	{
		mReady = true;
		Update();
	}

	// Call this for initialization or when viewport is changed.
	void
	Update( void )
	{
		OpenglClearError();
		glGetIntegerv( GL_VIEWPORT, mViewport );
		glGetFloatv( GL_PROJECTION_MATRIX, mProjectionMatrix );
		OpenglCheckError( "OpenglState::Update" );
	}

	bool
	IfReady( void )
	{
		return mReady;
	}

public:
	GLfloat		mProjectionMatrix[OPENGL_MATRIX_ELEMS];
	GLint		mViewport[4];

private:
	bool		mReady;
};

////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////  vars  //////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

static OpenglState sOpenglState;
static GfxsysParms sGfxsysParms;

////////////////////////////////////////////////////////////////////////////////
///////////////////////////////  functions  ////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

/*****************************************************************************
 * OpenGL/GLUT initialization and main loop.
 * Code that is more likely to be changed is kept in the second half.
 *****************************************************************************/
void
Opengl( int* pargc, char** argv, const GfxsysParms* parms )
{
	sGfxsysParms = *parms; // copy passed parm struct to static var

	// glutInit() on Mac OS X will do chdir() to where the executable is.
	// Undo that as loading texture files depends on cwd.
string cwd; Getcwd( cwd );
	glutInit( pargc, argv );
chdir( cwd.c_str() );

	//OpenglClearError();	// causes bus error on Mac OS X 10.4 (Mac Mini G4)
	glutInitDisplayMode( GLUT_RGBA | GLUT_DEPTH | GLUT_DOUBLE );
	glutInitWindowSize( parms->mWindowWidth, parms->mWindowHeight );
	glutCreateWindow( argv[0] );
	Opengl2();
	glutReshapeFunc( OpenglReshape );
	glutDisplayFunc( OpenglDisplayCallback );
	GFX_LINE_WIDTH( 1 );
	sOpenglState.Init();
	gEngineEvent.mGfxsysReady.Call();
	OpenglCheckError( "initialization" );
	glutMainLoop();

    gEng.Exit(0);  // this point may or may not be reached (see EngineAtExit())
}
static void
Opengl2( void )
{
ASSERT( sGfxsysParms.mValid  &&  sGfxsysParms.mTimerMilsec > 0 );
	glutTimerFunc( sGfxsysParms.mTimerMilsec, OpenglTimer, 0 );
	if ( gEng.GetSpeed() != eSpeed_FAST )
	{
		glutMouseFunc(         OpenglMouseButtonHandler        );
		glutPassiveMotionFunc( OpenglMouseMotionPassiveHandler ); // no button pressed
		glutMotionFunc(        OpenglMouseMotionHandler        ); // button(s) pressed
	}

//	const RGBA& rgbaBg = gEng.GetBackgroundRGBA();				// wrong, left as a reminder
//	glClearColor( rgbaBg[0], rgbaBg[1], rgbaBg[2], rgbaBg[3] );	// gets uint8, should be float
	const fp* rgbaBg = gEng.GetBackgroundRGBA().Getfv();
	glClearColor( rgbaBg[0], rgbaBg[1], rgbaBg[2], rgbaBg[3] );	// args are floats 0..1

	glClearAccum( 0.0, 0.0, 0.0, 0.0 );
	glShadeModel( GL_FLAT );
	glEnable( GL_DEPTH_TEST );
	PrepOpenglDrawVertexArray();
}

/*****************************************************************************
 * OpenGL/GLUT paint callback.
 *****************************************************************************/
static void
OpenglDisplayCallback( void )
{
	sOpenglState.Update();
	gEng.Render();
}

/*****************************************************************************
 * Call this pair before and after rendering a frame.
 *****************************************************************************/
void
OpenglDisplayPreFrame( void )
{
	glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
	glMatrixMode( GL_MODELVIEW );
	glLoadIdentity();
}
void
OpenglDisplayPostFrame( void )
{
	glFlush();
	glutSwapBuffers();
	OpenglCheckError( "rendering" );
}

/*****************************************************************************
 * OpenGL/GLUT reshape window callback.
 * OpenGL --> OpenglReshape() --> view->Reshape()
 *****************************************************************************/
void
OpenglReshape( int w, int h )
{
	// Don't broadcast to listeners here -- let the ViewMaster class do it.
	sOpenglState.Update();
	gEng.mView.Reshape( w, h );
}

/*****************************************************************************
 * Set viewport geometry (a subdivision of the main window).
 * geom.x,y specifies lower-left corner.
 *****************************************************************************/
void
OpenglSetViewportGeom( const Geom<int>& geom )
{
	glViewport( geom.x, geom.y, geom.w, geom.h );
	glMatrixMode( GL_PROJECTION );
	glLoadIdentity();
	gluPerspective( PerspectiveAngle( 50.0, 200.0 ),
                    fp(geom.w) / fp(geom.h),
//                  200.0, // < 10 causes occassional shredding on Mesa3D
                    125.0, // < 10 causes occassional shredding on Mesa3D
                    200000.0 );
	glMatrixMode( GL_MODELVIEW );
	glLoadIdentity();
}

/*****************************************************************************
 * OpenGL/GLUT timer callback.
 * Every frame must be rendered at regular time intervals for smooth rendering.
 *****************************************************************************/
static void
OpenglTimer( UNUSED int unused )
{
ASSERT( sGfxsysParms.mValid );

	static int busy = 0;
	if ( ! ( busy || gEng.IfBusy() ) )
	{
	++busy;
		// Compute the delta of time that has elasped between timer interrupts.
		// glutGet(GLUT_ELAPSED_TIME) returns the amount of milliseconds
		// that have elapsed since glutInit() was called.
		static int milsec      = 0;
		static int milsecPrev  = 0;
		static int milsecDelta = 0;
		milsecPrev  = milsec;
		milsec      = glutGet( GLUT_ELAPSED_TIME );
		milsecDelta = milsecDelta ? milsec - milsecPrev : sGfxsysParms.mTimerMilsec;

		// Tick() is the consumer.
		gEng.Tick( milsecDelta, milsec );
	--busy;
	}

	// Reschedule timer (would be single-shot otherwise).
	glutTimerFunc( sGfxsysParms.mTimerMilsec, OpenglTimer, 0 );
}

/*****************************************************************************
 * Callback invoked by GLUT when one mouse button is pressed or released.
 *****************************************************************************/
static void
OpenglMouseButtonHandler( int glutButton, int glutState, int x, int y )
{
BUSY_BEGIN;
	eMouseEvent button = eMouseEvent_NONE;
	switch ( glutButton )
	{
		case GLUT_LEFT_BUTTON:   button = eMouseEvent_BUTTON_1; break;
		case GLUT_RIGHT_BUTTON:  button = eMouseEvent_BUTTON_2; break;
		case GLUT_MIDDLE_BUTTON: button = eMouseEvent_BUTTON_3; break;
		default: break;
	}
	if ( button != eMouseEvent_NONE )
		gEngineEvent.mMouseButton.Call( std::make_pair<eMouseEvent,bool>( button, (glutState==GLUT_DOWN) ) );
BUSY_END;
}

/*****************************************************************************
 * Callback invoked by GLUT when mouse moved while a button(s) is pressed.
 *****************************************************************************/
static void
OpenglMouseMotionHandler( int x, int y )
{
	gEngineEvent.mMouseMotion.Call( std::make_pair<int,int>( x, y ) );
}

/*****************************************************************************
 * Callback invoked by GLUT when mouse moved while no button is pressed.
 *****************************************************************************/
static void
OpenglMouseMotionPassiveHandler( int x, int y )
{
	OpenglMouseMotionHandler( x, y ); // no difference
}

/*****************************************************************************
 * Clear/check OpenGL errors.
 * OpenGL error code is sticky.  glGetError() clears it.
 *****************************************************************************/
static void
OpenglClearError( void )
{
	// Clears error code.
	glGetError();
}
static void
OpenglCheckError( const char* funcName )
{
	// Eventually stop printing errors.
	static int errorCnt = 0;
	int error = glGetError();
	if ( error != GL_NO_ERROR && errorCnt < 20 )
	{
		++errorCnt;
		*gStreamError << "OpenGL error [" << funcName << "]: glGetError(" << error << "): "
                      << gluErrorString( error ) << endl;
	}
}

/*****************************************************************************
 * Project a 3D vertex into a 2D point. 
 *
 * This is a highly-optimized version of Mesa 6.3.2 gluProject().
 * The major improvement is bypassing multiplying the modelview matrix
 * since Palomino identity-maps it.
 * Minor improvements are streamling the function args, bypassing unneeded
 * calculations, and using single-precision.
 * 
 * GLint GLAPIENTRY
 * gluProject(GLdouble objx, GLdouble objy, GLdouble objz, 
 *           const GLdouble modelMatrix[OPENGL_MATRIX_ELEMS], 
 *           const GLdouble projMatrix[OPENGL_MATRIX_ELEMS],
 *           const GLint viewport[4],
 *           GLdouble *winx, GLdouble *winy, GLdouble *winz)
 * {
 *     double in[4];
 *     double out[4];
 * 
 *     in[0]=objx;
 *     in[1]=objy;
 *     in[2]=objz;
 *     in[3]=1.0;
 *     __gluMultMatrixVecd(modelMatrix, in, out);
 *     __gluMultMatrixVecd(projMatrix, out, in);
 *     if (in[3] == 0.0) return(GL_FALSE);
 *     in[0] /= in[3];
 *     in[1] /= in[3];
 *     in[2] /= in[3];
 *     // Map x, y and z to range 0-1
 *     in[0] = in[0] * 0.5 + 0.5;
 *     in[1] = in[1] * 0.5 + 0.5;
 *     in[2] = in[2] * 0.5 + 0.5;
 * 
 *     // Map x,y to viewport
 *     in[0] = in[0] * viewport[2] + viewport[0];
 *     in[1] = in[1] * viewport[3] + viewport[1];
 * 
 *     *winx=in[0];
 *     *winy=in[1];
 *     *winz=in[2];
 *     return(GL_TRUE);
 * }
 * 
 * static void __gluMultMatrixVecd(const GLdouble matrix[OPENGL_MATRIX_ELEMS], const GLdouble in[4],
 *               GLdouble out[4])
 * {
 *     int i;
 * 
 *     for (i=0; i<4; i++) {
 *     out[i] = 
 *         in[0] * matrix[0*4+i] +
 *         in[1] * matrix[1*4+i] +
 *         in[2] * matrix[2*4+i] +
 *         in[3] * matrix[3*4+i];
 *     }
 * }
 *
 *****************************************************************************/
void
OpenglProject( int pt[2]/*OUT*/, const EyeVertex& v )
{
#define m        (sOpenglState.mProjectionMatrix)
#define viewport (sOpenglState.mViewport)

	ASSERT( sOpenglState.IfReady() ); // ensure OpenGL state is valid

	fp pr[4];

	// pr[2] doesn't contribute to the resulting 2D point.

	pr[0] = v.x*m[ 0] + v.y*m[ 4] + v.z*m[ 8] + m[12];
	pr[1] = v.x*m[ 1] + v.y*m[ 5] + v.z*m[ 9] + m[13];
//	pr[2] = v.x*m[ 2] + v.y*m[ 6] + v.z*m[10] + m[14];
	pr[3] = v.x*m[ 3] + v.y*m[ 7] + v.z*m[11] + m[15];

    pr[0] /= pr[3];
    pr[1] /= pr[3];
//  pr[2] /= pr[3];

	// Map x,y,z to range 0..1.
    pr[0] = pr[0] * 0.5 + 0.5;
    pr[1] = pr[1] * 0.5 + 0.5;
//  pr[2] = pr[2] * 0.5 + 0.5;

	// Map x,y to viewport.
    pt[XX] = (int) (pr[0] * viewport[2] + viewport[0]);
    pt[YY] = (int) (pr[1] * viewport[3] + viewport[1]);

#undef m
#undef viewport
}

/*****************************************************************************
 * Render an indexed vertex array.
 * Restrictions intrinsically due to vertex array:
 * - all polygons must be opaque or share the same texture (not mixed)
 * - all polygons must be triangles or quads (not mixed)
 *****************************************************************************/
void
OpenglDrawVertexArray( VertexArray* const va, const EyeVertex* const ev, const DrawParm& drawParm )
{
	ASSERT(    va->GetPrimEnum() == GL_TRIANGLES
            || va->GetPrimEnum() == GL_QUADS );
	if ( drawParm.IfCull() ) glEnable( GL_CULL_FACE );
	const EyeVertex* const ev2 = va->Linearize( ev );
#if DEBUG
	// stride arg compensates for EyeVertex typesig in DEBUG builds.
	glVertexPointer( 3, GL_FLOAT, sizeof(EyeVertex), ev2 );
#else
	glVertexPointer( 3, GL_FLOAT, 0, ev2 );
#endif
	glColorPointer( 4, GL_UNSIGNED_BYTE, 0, va->GetRgbaArr() );
	TexturePtrc texture = va->GetTexture();
	if ( texture == NULL )
	{
		glDrawArrays( va->GetPrimEnum(), 0, va->GetPrimCnt() );
	}
	else
	{
		texture->Begin();
		glEnableClientState( GL_TEXTURE_COORD_ARRAY );
		glTexCoordPointer( 2, GL_FLOAT, 0, va->GetTexCoordArr() );
		glDrawArrays( va->GetPrimEnum(), 0, va->GetPrimCnt() );
		glDisableClientState( GL_TEXTURE_COORD_ARRAY );
		texture->End();
	}
	glDisable( GL_CULL_FACE );
}
static void
PrepOpenglDrawVertexArray( void )
{
	glCullFace( GL_FRONT );
	glFrontFace( GL_CCW );
	glEnableClientState( GL_VERTEX_ARRAY );
	glEnableClientState( GL_COLOR_ARRAY );
}
