/*****************************************************************************
 * $Id: eng_opengl.cc,v 1.117 2006/11/19 15:15:38 devel Exp $
 * @file
 * @author	Jim E. Brooks  http://www.jimbrooks.org
 * @brief	OpenGL-specific functions.
 * @remarks	The startup of OpenGL, the engine, and the simulated World
 *          are coordinated.  See eng_init.cc.
 *//*
 * 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"

//------------------------------------------------------------------------------
// OpenglInit() brings up OpenGL.
// OpenglRun() then transfers execution to GLUT's event loop.
//------------------------------------------------------------------------------

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

////////////////////////////////////////////////////////////////////////////////
// This class is a data struct: it holds/gets OpenGL state without affecting it.
//
class OpenglState
{
PREVENT_COPYING(OpenglState)
public:

	OpenglState( void )
	{
		mGeom.Clear();
		mGeomValid = false;
        mFrustumFar = 100000.0;  // interim value used before World constructed
	}

	// Call this when OpenGL is operational.
	void
	Init( void )
	{
		Update();
	}

	// Call this when initializing or changing viewport.
	void
	Update( void )
	{
		OPENGL_CLEAR_ERROR();
		glGetIntegerv( GL_VIEWPORT, mViewport );
		glGetFloatv( GL_PROJECTION_MATRIX, mProjectionMatrix );
		OPENGL_CHECK_ERROR( "OpenglState::Update" );
	}

    // Just store values.
	void SetGeom( const Geom<int>& geom )
	{
		mGeom = geom;
		mGeomValid = true;
	}

	const Geom<int>& GetGeom( void )
	{
		ASSERT(mGeomValid);
		return mGeom;
	}

    // This kludge exists because there's a limbo period between
    // the starting of OpenGL and before the viewport was really created.
    bool IfGeomValid( void ) const
    {
        return mGeomValid;
    }

    fp GetFrustumFar( void )
    {
        return mFrustumFar;
    }

    // Just store values.
    void SetFrustumFar( fp frustumFar )
    {
        mFrustumFar = frustumFar;
    }

private:
	GLfloat		mProjectionMatrix[OPENGL_MATRIX_ELEMS];
	GLint		mViewport[4];  // OpenGL format
	Geom<int>	mGeom;         // higher-level, same as mViewport[4]
	bool		mGeomValid;    // if mGeom is valid
    fp          mFrustumFar;

friend void OpenglProject( int pt[2]/*OUT*/, const EyeVertex& v );
};

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

static OpenglState sOpenglState;
static GfxsysParms sGfxsysParms;

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

/*****************************************************************************
 * Ideally, checking OpenGL version/capabilities should be done by gfx.
 * But these OpenGL functions don't work until glutInit() etc is called
 * here by the Engine which is long after gfx initialization.
 *****************************************************************************/
static void
OpenglCheck( void )
{
ASSERT( GFXSYS_IF_READY() );
#if DEBUG
    *gStreamOut << "OpenGL "
                << glGetString(GL_VERSION)  << ' '
                << glGetString(GL_VENDOR)   << ' '
                << glGetString(GL_RENDERER) << endl;
#endif

    // Check OpenGL version.
	// Vendors may append their names to the version string,
    // eg "2.0.2 NVIDIA 87.62", but atof() will succeed.
	float ver = atof( (const char*)glGetString( GL_VERSION ) );
	DEBUG_CODE( *gStreamDebug << "OpenGL version string as float: " << ver << endl; )
	if ( ver == 0.0 )  // atof() failed
	{
		*gStreamError << "WARNING: Unknown OpenGL version" << endl;
	}
	else if ( ver < GFX_MININUM_OPENGL_VERSION_FLOAT )
    {
		*gStreamError << "ERROR: Requires minimum OpenGL version: " << GFX_MININUM_OPENGL_VERSION_FLOAT << endl
                      << "ERROR: OpenGL version on this system is: " << ver << endl;
        //gEng.Exit(1);  // stumble ahead
    }
}

/*****************************************************************************
 * OpenGL/GLUT initialization and main loop.
 * Code that is more likely to be changed is kept in the second half.
 *****************************************************************************/
void
OpenglInit( 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() );

	//OPENGL_CLEAR_ERROR();	// causes bus error on Mac OS X 10.4 (Mac Mini G4)
	// GLUT_ALPHA must be passed, regardless of GLUT_RGBA, to get alpha channel (see man page).
  //glutInitDisplayMode( GLUT_RGBA | GLUT_DEPTH | GLUT_DOUBLE );  // wrong
	glutInitDisplayMode( GLUT_RGBA | GLUT_ALPHA | GLUT_DEPTH | GLUT_DOUBLE );
	glutInitWindowSize( parms->mWindowWidth, parms->mWindowHeight );
	glutCreateWindow( argv[0] );
	GFXSYS_SET_READY();
	OpenglInit2();
	glutReshapeFunc( OpenglReshape );
	glutDisplayFunc( OpenglDisplayCallback );
	GFX_LINE_WIDTH( 1 );
	sOpenglState.Init();
	OPENGL_CHECK_ERROR( "initialization" );
}
static void
OpenglInit2( 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
	}

	GLfloat bg4f[4]; gEng.GetBackgroundRGBA().Get4f( bg4f );
	glClearColor( bg4f[RR], bg4f[GG], bg4f[BB], bg4f[AA] );  // args are floats 0..1

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

	OpenglCheck();
}
void
OpenglRun( void )
{
ASSERT( GFXSYS_IF_READY() );
	glutMainLoop();
    gEng.Exit(0);  // this point may or may not be reached (see EngineAtExit())
}

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

/*****************************************************************************
 * Call this pair before and after rendering a frame.
 *****************************************************************************/
void
OpenglDisplayPreFrame( void )
{
ASSERT( GFXSYS_IF_READY() );
	glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
    OpenglResetModelViewMatrix();
}
void
OpenglDisplayPostFrame( void )
{
ASSERT( GFXSYS_IF_READY() );
	glFlush();
	glutSwapBuffers();
	OPENGL_CHECK_ERROR( "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 );  // indirectly calls OpenglSetViewportGeom()

    // Compute new OpenGL projection matrix.
	OpenglSetFrustum( sOpenglState.GetFrustumFar() );  // affects sorted pile of Objects

    // Change back to modelview matrix.
	//glMatrixMode( GL_MODELVIEW );
    OpenglResetModelViewMatrix();
}

/*****************************************************************************
 * Set viewport geometry (a subdivision of the main window).
 * geom.x,y specifies lower-left corner.
 *****************************************************************************/
void
OpenglSetViewportGeom( const Geom<int>& geom )
{
    // Change actual OpenGL viewport.
	glViewport( geom.x, geom.y, geom.w, geom.h );

    // Update OpenglState data struct.
	sOpenglState.SetGeom( geom );
}

/*****************************************************************************
 * Reset/select modelview matrix.
 *****************************************************************************/
static void
OpenglResetModelViewMatrix( void )
{
ASSERT( GFXSYS_IF_READY() );

	glMatrixMode( GL_MODELVIEW );
	glLoadIdentity();
	// Using glTranslatef() to compensate for perspective breaks visibility test.
}

/*****************************************************************************
 * Update projection matrix.
 *****************************************************************************/
void
OpenglSetFrustum( const fp frustumFar )
{
ASSERT( GFXSYS_IF_READY() );

    // Remember in case viewport is resized which demands updating projection matrix.
    sOpenglState.SetFrustumFar( frustumFar );

    // OpenglSetFrustum() can be called before OpenGL viewport is really initialized.
    // Just return at this point.
    if ( ! sOpenglState.IfGeomValid() ) return;

    // Get viewport geometry.
	const Geom<int>& geom = sOpenglState.GetGeom();

    // Update OpenGL projection matrix which defines the OpenGL frustum.
    // If frustum near plane is too near (<100.0) then polygon
    // rendering problems occurs on older (1998..2003) video hw.
	glMatrixMode( GL_PROJECTION );
	glLoadIdentity();
    switch ( gEng.GetPerspective() )
    {
        case Engine::ePerspective_NORMAL: default:
        	gluPerspective( PerspectiveAngle( 50.0, 200.0 ),
                            fp(geom.w) / fp(geom.h),
                            125.0,
                            frustumFar );  //200000.0
        break;

        case Engine::ePerspective_NEAR:
        	gluPerspective( 5.0,  // degree
                            fp(geom.w) / fp(geom.h),
                            650.0,
                            frustumFar ); //175000.0 );
        break;

        case Engine::ePerspective_FAR:
        	gluPerspective( 35.0,  // degree
                            fp(geom.w) / fp(geom.h),
                            450.0,
                            frustumFar ); // 300000.0 );
        break;
    }

    // This must be called when projection matrix is changed.
	sOpenglState.Update();

    // Switch back to modelview matrix.
	//glMatrixMode( GL_MODELVIEW );
    OpenglResetModelViewMatrix();

	// Changing frustrum disrupted results of visibility tests.
	gEngineEvent.mVisibilityChange.Broadcast();
}

/*****************************************************************************
 * 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 );
ASSERT( GFXSYS_IF_READY() );

	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.Broadcast( 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.Broadcast( 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
}

/*****************************************************************************
 * 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( GFXSYS_IF_READY() );

	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( GFXSYS_IF_READY() );

	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() );
	Texture2PtrConst 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 )
{
ASSERT( GFXSYS_IF_READY() );
	glCullFace( GL_FRONT );
	glFrontFace( GL_CCW );
	glEnableClientState( GL_VERTEX_ARRAY );
	glEnableClientState( GL_COLOR_ARRAY );
}
