/** SGX

The   sss    gggg x   x   
     s      g      x x    Engine
      sss   g  gg   x
         s  g   g  x x          Copyright 1995-2008.
     ssss    gggg x   x         Steven Goodwin. All Rights Reserved.
    
This version has been released under the GNU Public License v2.0.

Please see the license files for details.

File: src/drivers/common/opengl/oglgraphics.cpp

*/

#include "sgx/core/core.hpp"
#include "sgx/graphics/graphics.hpp"

#include "drivers/common/opengl/opengl.hpp"


// TODO: pImpl for gfx engine?? TextureManager is separate
sgxTexEnvState			COpenGLEngine::m_StateTexEnv;
sgxBindTexState		COpenGLEngine::m_StateBindTex;
sgxBlendFuncState		COpenGLEngine::m_StateBlendFunc;

CSGXGraphicsEngine *COpenGLEngine::create(const sgxString &appname, tUINT32 width, tUINT32 height)
{
	m_pSingleton = new COpenGLEngine(appname, width, height);
	if (m_pSingleton) {
		m_pSingleton->initialize();
	}
	return m_pSingleton;
}

void COpenGLEngine::initialize()
{
	CDriverOpenGLDrawSurfaceManager::get();
	CSGXGraphicsEngine::initialize();
}

void COpenGLEngine::showSystemCursor(const tBOOL show) {
	glutSetCursor(GLUT_CURSOR_RIGHT_ARROW);
}

void COpenGLEngine::hideSystemCursor() {
	glutSetCursor(GLUT_CURSOR_NONE);
}

COpenGLEngine *COpenGLEngine::getOpenGL()
{
	return static_cast<COpenGLEngine *>(m_pSingleton);
}

COpenGLEngine::COpenGLEngine(const sgxString &appname, tUINT32 width, tUINT32 height)
	: CSGXGraphicsEngine()
{
	int argc=1;
	char *argv[] = {"sgxEngine"};

	m_ScreenWidth = width;
    m_ScreenHeight = height;

	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_DOUBLE | GLUT_DEPTH | GLUT_RGB);
	glutInitWindowSize(width, height);
	glutInitWindowPosition(0, 0);
	glutCreateWindow(appname.c_str());

	glEnable(GL_DEPTH_TEST);
	// GL_DEPTH_WRITEMASK
	//glDepthFunc(GL_GEQUAL);
	glDisable(GL_ALPHA_TEST);
	glDepthFunc(GL_LEQUAL);
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
	glHint(GL_POINT_SMOOTH_HINT,GL_NICEST);
	glHint(GL_LINE_SMOOTH_HINT,GL_NICEST);
	glHint(GL_POLYGON_SMOOTH_HINT,GL_NICEST);

	//TODO: glClipPlane
	//	GL_DEPTH_RANGE
	//glEnable(GL_FRONT_FACE);
	
	// TODO: add 'quality' mode (configuration file?)
	glDisable(GL_BLEND);
	glShadeModel(GL_SMOOTH);	// GL_FLAT/ GL_SMOOTH

	glEnable(GL_TEXTURE_2D);
	glEnable(GL_COLOR_MATERIAL);
	glEnable(GL_LIGHTING);
	// TODO: put cull faces back in when faces are always wound ccw/cw
	glEnable(GL_CULL_FACE);
	glEnable(GL_CULL_FACE_MODE);
	glCullFace(GL_FRONT);	// our engine winds face CW, OpenGL believes these to be back faces
	//glSetInteger(GL_CULL_FACE_MODE, GL_BACK);

	//GLfloat white[3] = {1,1,1};
	//glLightfv(GL_LIGHT0, GL_AMBIENT, white);

	m_bPostIdleRender = FALSE;

	//				glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);	

	// TODO
//	glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE_ARB);
//	glTexEnvi(GL_TEXTURE_ENV, GL_RGB_SCALE_ARB, 2);
	prepareCallbacks();

#if SGX_DEBUG
	sgxTrace("OpenGL Report");
	sgxTrace((char *)glGetString(GL_VENDOR));
	sgxTrace((char *)glGetString(GL_RENDERER));
	sgxTrace((char *)glGetString(GL_VERSION));
	sgxTrace((char *)glGetString(GL_EXTENSIONS));

	sgxTrace("GLU Report");
	sgxTrace((char *)gluGetString(GLU_VERSION));
	sgxTrace((char *)gluGetString(GLU_EXTENSIONS));
#endif
}

void COpenGLEngine::doBeginScene()
{
	clearFrameBuffer();
	clearZBuffer();

	flushClearBuffer();
}

void COpenGLEngine::preDrawScene()
{
	CSGXGraphicsEngine::preDrawScene();

	set3DMode();

	if (m_Fogging.IsDirty()) {
		sgxFogging &fog = m_Fogging.getState();
		tREAL32 cols[] = { fog.fRed, fog.fGreen, fog.fBlue, fog.fAlpha, };

		if (fog.bActive) {
			glEnable(GL_FOG);

			if (fog.iMode == SGX_FOG_LOGARITHMIC) {
				glFogi(GL_FOG_MODE, GL_EXP2);
			} else {
				glFogi(GL_FOG_MODE, GL_LINEAR);
			}
			
			glFogfv(GL_FOG_COLOR, cols);
			glHint(GL_FOG_HINT, GL_DONT_CARE);
			glFogf(GL_FOG_START, fog.fStart);
			glFogf(GL_FOG_END, fog.fEnd);
			glFogf(GL_FOG_DENSITY, fog.fDensity);
		} else {
			glDisable(GL_FOG);
		}

		m_Fogging.ForceClean();
	}

	applyLighting();
}

void 
COpenGLEngine::applyLighting()
{
	sgxVector<CSGXLight *>::iterator it = m_Lights.begin();
	GLenum light_idx = GL_LIGHT0;

	for(;it!=m_Lights.end();++it, ++light_idx) {

		if ((*it)->type.Apply()) {
			int type = (*it)->type.getState();
			switch(type) {
			case CSGXLight::eTypeOmni:
				{ const sgxColorRGB &c = (*it)->color.getState();
				const GLfloat glcol[3] = { c.r, c.g, c.b };
				glLightfv(light_idx, GL_AMBIENT, glcol);
				}
				break;
				// TODO
			case CSGXLight::eTypeDirectional:
				//glLightfv(light_idx, GL_AMBIENT, white)
				break;
				// TODO
			case CSGXLight::eTypeSpotlight:
				//glLightfv(light_idx, GL_AMBIENT, white)
				break;
			}
		}

		if ((*it)->pos.Apply()) {
			const sgxVector3 &v = (*it)->pos.getState();
			const GLfloat glvector[3] = { v.x, v.y, v.z };
			glLightfv(light_idx, GL_POSITION, glvector);
		}

		if ((*it)->color.Apply()) {
			const sgxColorRGB &c = (*it)->color.getState();
			const GLfloat glcol[3] = { c.r, c.g, c.b };

			//glLightfv(light_idx, GL_DIFFUSE, glcol);

			if ((*it)->type.getState() == CSGXLight::eTypeOmni) {
				glLightfv(light_idx, GL_AMBIENT, glcol);
			}
		}

		if ((*it)->active.Apply()) {
			if ((*it)->active.getState()) {
				glEnable(light_idx);
			} else {
				glDisable(light_idx);
			}
		}

	}
}

void 
COpenGLEngine::setClipRect(const tPOINT x1, const tPOINT y1, const tPOINT x2, const tPOINT y2) {
	CSGXGraphicsEngine::setClipRect(x1, y1, x2, y2);
	glEnable (GL_SCISSOR_TEST);
	glScissor(x1,y1, x2-x1,y2-y1);
}

void COpenGLEngine::flushClearBuffer()
{
	tUINT32 buffers = 0;

	if (m_ClearBufferColor.Apply()) {
		buffers |= GL_COLOR_BUFFER_BIT;
		sgxColorRGB &col = m_ClearBufferColor.getState();
		glClearColor(col.r, col.g, col.b, 1.0);
	}

	if (m_ClearZBuffer.Apply()) {
		buffers |= GL_DEPTH_BUFFER_BIT;
		glClearDepth(1.0f);
	}

	CSGXGraphicsEngine::flushClearBuffer();
	glClear(buffers);
}

void COpenGLEngine::doDrawScene()
{
	CSGXGraphicsEngine::doDrawScene();
	drawAlphaFaces();	// TODO: REMOVE FROM HERE SINCE IT'S WRONG
}


void
COpenGLEngine::drawAlphaFaces()
{
	sgxColorRGBA &col = m_PrimitiveColor.getState();

	//glPushAttrib(GL_ALL_ATTRIB_BITS|GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_ENABLE_BIT | GL_POLYGON_BIT);
	glPushAttrib(GL_DEPTH_BUFFER_BIT | GL_ENABLE_BIT);
	
	glEnable(GL_ALPHA_TEST);
	glAlphaFunc(GL_GREATER, 0.1f);

	glDepthMask(FALSE);			// no depth writes
	
	glDisable(GL_CULL_FACE);	// no back fack culling for alphas

	CSGXGraphicsEngine::drawAlphaFaces();

	glPopAttrib();
	
	m_PrimitiveColor.setState(col);
}


void COpenGLEngine::blit()
{
	CSGXGraphicsEngine::blit();

	glutSwapBuffers();
}


void COpenGLEngine::idleScene()
{
	tREAL32 telaps = CGameClock::get()->getLastTelaps();

	// update the world
	if (!(*m_pIdleCallback)(telaps, m_iIdleParameter)) {
		stopEngine();
	}

	if (m_bPostIdleRender) {
		renderScene();
		blit();
		m_bPostIdleRender = FALSE;
	}

}

void
COpenGLEngine::refresh() 
{
	m_bPostIdleRender = TRUE;
}


void COpenGLEngine::runEngine(cb_pOpenGLIdleFunction *pFN, tMEM_SIZE param)
{
	m_pIdleCallback = pFN;
	m_iIdleParameter = param;
	
	glutMainLoop();
}

void COpenGLEngine::stopEngine()
{
	glutIdleFunc(NULL);
	release();
	// TODO: What's the proper opengl-friendly method for this?
	exit(0);
}

void COpenGLEngine::postUpdateScene(const tREAL32 telaps)
{
	CSGXGraphicsEngine::postUpdateScene(telaps);

	// Cause a refresh
	static tREAL32 upd = 0;
	const tREAL32 refresh = CGameClock::get()->getClampedMax();

	upd += telaps;
	if (upd > refresh) {
		m_bPostIdleRender = TRUE;
		upd = 0;
	}
}

tUINT32 
COpenGLEngine::getMaxLights() const
{
	return (GL_LIGHT7-GL_LIGHT0)+1;
}

GLenum COpenGLEngine::getOGLMode()
{
	if (m_PrimitiveMode.getState() == eWireframe) {
		return GL_LINE_LOOP;
	} else {
		return GL_POLYGON;
	}
}

void 
COpenGLEngine::set2DMode()
{
	CSGXGraphicsEngine::set2DMode();

	m_MatrixModeState.Apply(GL_PROJECTION);
	glLoadIdentity();									// reset the matrix

	// The height & y are reversed to most text books because I like
	// the origin (0,0) at the top-left of the screen.
	sgxViewport &viewport = m_Viewport[getCurrentScene()].getState();
	glViewport(viewport.origin_x, viewport.origin_y, viewport.width, viewport.height);
	gluOrtho2D(viewport.origin_x, viewport.width, viewport.height, viewport.origin_y);

	m_MatrixModeState.Apply(GL_MODELVIEW);
	glLoadIdentity();									// reset that matrix

	updateViewTransform();
	updateProjectionTransform();
	updateViewProjectionTransform();
}

void 
COpenGLEngine::set3DMode()
{
	CSGXGraphicsEngine::set3DMode();

	sgxViewport &viewport = m_Viewport[m_CurrentScene].getState();
	sgxVector3 up = m_CurrentCamera.up();

	m_CurrentCamera = m_ViewCamera[m_CurrentScene].getState();
	//
	//
	//
	//m_MatrixModeState.ForceDirty();
	m_MatrixModeState.Apply(GL_PROJECTION);
	glLoadIdentity();
	glViewport(viewport.origin_x, viewport.origin_y, viewport.width, viewport.height);
	gluPerspective(m_CurrentCamera.m_fFov, viewport.width/(tREAL32)viewport.height, m_CurrentCamera.m_fNearPlane, m_CurrentCamera.m_fFarPlane);

	//
	//
	//
	m_MatrixModeState.Apply(GL_MODELVIEW);
	glLoadIdentity();
	gluLookAt(m_CurrentCamera.m_vFrom.x, m_CurrentCamera.m_vFrom.y, m_CurrentCamera.m_vFrom.z,  
			  m_CurrentCamera.m_vAt.x, m_CurrentCamera.m_vAt.y, m_CurrentCamera.m_vAt.z,  
			  up.x, up.y, up.z);

	updateViewTransform();
	updateProjectionTransform();
	updateViewProjectionTransform();

	m_pFrustum->recalculateFrustumPlanes(m_ProjectViewTransform);

	// Clean states because switching to 3D mode refreshes them all
	m_Viewport[m_CurrentScene].ForceClean();
	m_ViewCamera[m_CurrentScene].ForceClean();
}

void
COpenGLEngine::updateViewTransform()
{
    glGetFloatv(GL_MODELVIEW_MATRIX, m_GLViewMatrix);
	oglMatrix::convertToSgx(m_ViewTransform, m_GLViewMatrix);
	m_bProjectViewDirty = TRUE;
}

void
COpenGLEngine::updateProjectionTransform()
{
    glGetFloatv(GL_PROJECTION_MATRIX, m_GLProjectionMatrix);
	oglMatrix::convertToSgx(m_ProjectionTransform, m_GLProjectionMatrix);
	m_bProjectViewDirty = TRUE;
}

void 
COpenGLEngine::setRenderBuffer(tUINT32 id)
{
	switch(id) {
	case	0:
		glDrawBuffer(GL_BACK);
		break;

	case	1:
		glDrawBuffer(GL_AUX0);
		break;
	};

}

