// VisualBoyAdvance - Nintendo Gameboy/GameboyAdvance (TM) emulator.
// Copyright (C) 1999-2003 Forgotten
// Copyright (C) 2004 Forgotten and the VBA development team
// Copyright (C) 2005-2006 VBA development team
// Copyright (C) 2007-2008 VBA-M development team
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2, or(at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software Foundation,
// Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.

#ifndef NO_OGL

//OpenGL library
#pragma comment( lib, "OpenGL32" )

// MFC
#include "stdafx.h"

//GUI
#include "MainWnd.h"
#include "FullscreenSettings.h"

// Internals
#include "../System.h"
#include "../agb/GBA.h"
#include "../Globals.h"
#include "../Util.h"
#include "../dmg/gbGlobals.h"
#include "..\memgzio.h"

//Math
#include <cmath>
#include <sys/stat.h>

// OpenGL
#include <gl/GL.h> // main include file
#include <GL/glu.h>
#include "glFont.h"
#include <gl/glext.h>
typedef BOOL (APIENTRY *PFNWGLSWAPINTERVALFARPROC)( int );
extern int Init_2xSaI(u32);
extern void winlog(const char *,...);
extern int systemSpeed;

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

#ifdef MMX
extern "C" bool cpu_mmx;
extern bool detectMMX();
#endif


class OpenGLDisplay : public IDisplay {
private:
	HDC hDC;
	HGLRC hRC;
	GLuint texture;
	int width,height;
	float size;
	u8 *filterData;
	RECT destRect;
	bool failed;
	GLFONT font;
	int pitch;
	u8 *data;
	DWORD currentAdapter;

	void initializeMatrices( int w, int h );
	bool initializeTexture( int w, int h );
	void updateFiltering( int value );
	void setVSync( int interval = 1 );
	void calculateDestRect( int w, int h );
	void initializeFont();

public:
	OpenGLDisplay();
	virtual ~OpenGLDisplay();
	virtual DISPLAY_TYPE getType() { return OPENGL; };

	virtual void EnableOpenGL();
	virtual void DisableOpenGL();
	virtual bool initialize();
	virtual void cleanup();
	virtual void clear();
	virtual void render();
	virtual bool changeRenderSize( int w, int h );
	virtual void resize( int w, int h );
	virtual void setOption( const char *, int );
	virtual bool selectFullScreenMode( VIDEO_MODE &mode );
};

#include "gzglfont.h"
//Load GL font
void OpenGLDisplay::initializeFont()
{
    int ret;
    z_stream strm;
	char *buf = (char *)malloc(GZGLFONT_SIZE);

    /* allocate inflate state */
    strm.zalloc = Z_NULL;
    strm.zfree = Z_NULL;
    strm.opaque = Z_NULL;
    strm.avail_in = 0;
    strm.next_in = Z_NULL;
    ret = inflateInit2(&strm, 16+MAX_WBITS);
    if (ret != Z_OK)
        return;

    strm.avail_in = sizeof(gzglfont);
    strm.next_in = gzglfont;
    strm.avail_out = GZGLFONT_SIZE;
	strm.next_out = (Bytef *)buf;
    ret = inflate(&strm, Z_NO_FLUSH);
	if (ret==Z_STREAM_END)
	{
		glGenTextures( 1, &texture );
		glFontCreate(&font, (char *)buf, texture);
		texture=0;
	}
	free(buf);
    (void)inflateEnd(&strm);
}

//OpenGL class constructor
OpenGLDisplay::OpenGLDisplay()
{
	hDC = NULL;
	hRC = NULL;
	texture = 0;
	width = 0;
	height = 0;
	size = 0.0f;
	failed = false;
	filterData = NULL;
	currentAdapter = 0;
}

//OpenGL class destroyer
OpenGLDisplay::~OpenGLDisplay()
{
	cleanup();
}

//Set OpenGL PFD and contexts
void OpenGLDisplay::EnableOpenGL()
{
	PIXELFORMATDESCRIPTOR pfd;
	// get the device context (DC)
	hDC = GetDC( theApp.m_pMainWnd->GetSafeHwnd() );
	// set the pixel format for the DC
	ZeroMemory( &pfd, sizeof( pfd ) );
	pfd.nSize = sizeof( pfd );
	pfd.nVersion = 1;
	pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
	pfd.iPixelType = PFD_TYPE_RGBA;
	pfd.cColorBits = 24;
	pfd.cDepthBits = 16;
	pfd.iLayerType = PFD_MAIN_PLANE;
    SetPixelFormat (GetDC (theApp.m_pMainWnd->GetSafeHwnd()), ChoosePixelFormat ( GetDC (theApp.m_pMainWnd->GetSafeHwnd()), &pfd), &pfd);
    wglMakeCurrent (GetDC (theApp.m_pMainWnd->GetSafeHwnd()), wglCreateContext(GetDC (theApp.m_pMainWnd->GetSafeHwnd()) ) );
}
//Remove contexts
void OpenGLDisplay::DisableOpenGL()
{
	wglMakeCurrent( NULL, NULL );
	wglDeleteContext( hRC );
	ReleaseDC( theApp.m_pMainWnd->GetSafeHwnd(), hDC );
}
//Remove resources used
void OpenGLDisplay::cleanup()
{
	if(texture != 0) {
		glDeleteTextures(1, &texture);
		texture = 0;
	}

	DisableOpenGL();
	if(filterData) {
		free(filterData);
		filterData = NULL;
	}
	width = 0;
	height = 0;
	size = 0.0f;

	DISPLAY_DEVICE dev;
	ZeroMemory( &dev, sizeof(dev) );
	dev.cb = sizeof(dev);
	EnumDisplayDevices( NULL, currentAdapter, &dev, 0 );
	// restore default video mode
	ChangeDisplaySettingsEx( dev.DeviceName, NULL, NULL, 0, NULL );
}

//init renderer
bool OpenGLDisplay::initialize()
{
	switch( theApp.cartridgeType )
	{
	case IMAGE_GBA:
		theApp.sizeX = 240;
		theApp.sizeY = 160;
		break;
	case IMAGE_GB:
		if ( gbBorderOn )
		{
			theApp.sizeX = 256;
			theApp.sizeY = 224;
		}
		else
		{
			theApp.sizeX = 160;
			theApp.sizeY = 144;
		}
		break;
	}


	switch(theApp.videoOption)
	{
	case VIDEO_1X:
		theApp.surfaceSizeX = theApp.sizeX;
		theApp.surfaceSizeY = theApp.sizeY;
		break;
	case VIDEO_2X:
		theApp.surfaceSizeX = theApp.sizeX * 2;
		theApp.surfaceSizeY = theApp.sizeY * 2;
		break;
	case VIDEO_3X:
		theApp.surfaceSizeX = theApp.sizeX * 3;
		theApp.surfaceSizeY = theApp.sizeY * 3;
		break;
	case VIDEO_4X:
		theApp.surfaceSizeX = theApp.sizeX * 4;
		theApp.surfaceSizeY = theApp.sizeY * 4;
		break;
	case VIDEO_320x240:
	case VIDEO_640x480:
	case VIDEO_800x600:
	case VIDEO_1024x768:
	case VIDEO_1280x1024:
	case VIDEO_OTHER:
		{
			if( theApp.fullScreenStretch ) {
				theApp.surfaceSizeX = theApp.fsWidth;
				theApp.surfaceSizeY = theApp.fsHeight;
			} else {
				float scaleX = (float)theApp.fsWidth / (float)theApp.sizeX;
				float scaleY = (float)theApp.fsHeight / (float)theApp.sizeY;
				float min = ( scaleX < scaleY ) ? scaleX : scaleY;
				if( theApp.fsMaxScale )
					min = ( min > (float)theApp.fsMaxScale ) ? (float)theApp.fsMaxScale : min;
				theApp.surfaceSizeX = (int)((float)theApp.sizeX * min);
				theApp.surfaceSizeY = (int)((float)theApp.sizeY * min);
			}
		}
		break;
	}

	theApp.rect.left = 0;
	theApp.rect.top = 0;
	theApp.rect.right = theApp.sizeX;
	theApp.rect.bottom = theApp.sizeY;

	theApp.dest.left = 0;
	theApp.dest.top = 0;
	theApp.dest.right = theApp.surfaceSizeX;
	theApp.dest.bottom = theApp.surfaceSizeY;

	DWORD style = WS_POPUP | WS_VISIBLE;
	DWORD styleEx = 0;

	if( theApp.videoOption <= VIDEO_4X )
		style |= WS_OVERLAPPEDWINDOW;
	else
		styleEx = 0;

	if( theApp.videoOption <= VIDEO_4X )
		AdjustWindowRectEx( &theApp.dest, style, TRUE, styleEx );
	else
		AdjustWindowRectEx( &theApp.dest, style, FALSE, styleEx );

	int winSizeX = theApp.dest.right - theApp.dest.left;
	int winSizeY = theApp.dest.bottom - theApp.dest.top;
	int x = 0, y = 0;

	if( theApp.videoOption <= VIDEO_4X ) {
		x = theApp.windowPositionX;
		y = theApp.windowPositionY;
	} else {
		winSizeX = theApp.fsWidth;
		winSizeY = theApp.fsHeight;
	}

	

	theApp.updateMenuBar();

	theApp.adjustDestRect();
	theApp.mode320Available = FALSE;
	theApp.mode640Available = FALSE;
	theApp.mode800Available = FALSE;
	theApp.mode1024Available = FALSE;
	theApp.mode1280Available = FALSE;


	currentAdapter = theApp.fsAdapter;
	DISPLAY_DEVICE dev;
	ZeroMemory( &dev, sizeof(dev) );
	dev.cb = sizeof(dev);
	EnumDisplayDevices( NULL, currentAdapter, &dev, 0 );
	if( theApp.videoOption >= VIDEO_320x240 ) {
		// enter full screen mode
		DEVMODE mode;
		ZeroMemory( &mode, sizeof(mode) );
		mode.dmSize = sizeof(mode);
		mode.dmBitsPerPel = theApp.fsColorDepth;
		mode.dmPelsWidth = theApp.fsWidth;
		mode.dmPelsHeight = theApp.fsHeight;
		mode.dmDisplayFrequency = theApp.fsFrequency;
		mode.dmFields = DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY;
		LONG ret = ChangeDisplaySettingsEx( dev.DeviceName, &mode, NULL, CDS_FULLSCREEN, NULL );
		if( ret != DISP_CHANGE_SUCCESSFUL ) {
			systemMessage( 0, "Can not change display mode!" );
			failed = true;
		}
	} else {
		// restore default mode
		ChangeDisplaySettingsEx( dev.DeviceName, NULL, NULL, 0, NULL );
	}

	EnableOpenGL();
	initializeFont();
	glPushAttrib( GL_ENABLE_BIT );
	glDisable( GL_DEPTH_TEST );
	glDisable( GL_CULL_FACE );
	glEnable( GL_TEXTURE_2D );
	glEnable(GL_BLEND);
	glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

	initializeMatrices( theApp.surfaceSizeX, theApp.surfaceSizeY );

	setVSync( theApp.vsync );

#ifdef MMX
	if(!theApp.disableMMX)
		cpu_mmx = theApp.detectMMX();
	else
		cpu_mmx = 0;
#endif

	systemRedShift = 3;
	systemGreenShift = 11;
	systemBlueShift = 19;
	systemColorDepth = 32;
	theApp.fsColorDepth = 32;

	Init_2xSaI(32);

	utilUpdateSystemColorMaps();
	theApp.updateFilter();
	theApp.updateIFB();
	pitch = theApp.filterWidth * (systemColorDepth>>3) + 4;
	data = pix + ( theApp.sizeX + 1 ) * 4;

	if(failed)
		return false;

	return true;
}

//clear colour buffer
void OpenGLDisplay::clear()
{
	glClearColor(0.0,0.0,0.0,1.0);
	glClear( GL_COLOR_BUFFER_BIT );
}

//main render func
void OpenGLDisplay::render()
{
	clear();

	pitch = theApp.filterWidth * (systemColorDepth>>3) + 4;
	data = pix + ( theApp.sizeX + 1 ) * 4;

	// apply pixel filter
	if(theApp.filterFunction) {
		data = filterData;
		theApp.filterFunction(
			pix + pitch,
			pitch,
			(u8*)theApp.delta,
			(u8*)filterData,
			width * 4 ,
			theApp.filterWidth,
			theApp.filterHeight);
	}

	// Texturemap complete texture to surface
	// so we have free scaling and antialiasing

	if( theApp.filterFunction ) {
		glPixelStorei( GL_UNPACK_ROW_LENGTH, width);
	} else {
		glPixelStorei( GL_UNPACK_ROW_LENGTH, theApp.sizeX + 1 );
	}
    glTexSubImage2D(GL_TEXTURE_2D,0,0,0,width,height,GL_RGBA,GL_UNSIGNED_BYTE,data );


	glBegin( GL_QUADS );

	glTexCoord2f( 0.0f, 0.0f );
	glVertex3i( 0, 0, 0 );

	glTexCoord2f( (float)(width) / size, 0.0f );
	glVertex3i( theApp.surfaceSizeX, 0, 0 );

	glTexCoord2f( (float)(width) / size, (float)(height) / size );
	glVertex3i( theApp.surfaceSizeX, theApp.surfaceSizeY, 0 );

	glTexCoord2f( 0.0f, (float)(height) / size );
	glVertex3i( 0, theApp.surfaceSizeY, 0 );
	glEnd();


	if( theApp.showSpeed ) { // && ( theApp.videoOption > VIDEO_4X ) ) {
		char buffer[30];
		if( theApp.showSpeed == 1 ) {
			sprintf( buffer, "%3d%%", systemSpeed );
		} else {
			sprintf( buffer, "%3d%%(%d, %d fps)", systemSpeed, systemFrameSkip, theApp.showRenderedFrames );
		}
		glFontBegin(&font);
		glPushMatrix();
		float fontscale = (float)theApp.surfaceSizeX / 100.0f;
		glScalef(fontscale, fontscale, fontscale);
		glColor4f(1.0f, 0.25f, 0.25f, 1.0f);
		glFontTextOut(buffer, (theApp.surfaceSizeX-(strlen(buffer)*11))/(fontscale*2), (theApp.surfaceSizeY-20)/fontscale, 0);
		glPopMatrix();
		glFontEnd();
		glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
		glBindTexture( GL_TEXTURE_2D, texture );
	}
	if( theApp.screenMessage ) {
		if( ( ( GetTickCount() - theApp.screenMessageTime ) < 3000 ) && !theApp.disableStatusMessage ) {
			glFontBegin(&font);
			glPushMatrix();

			float fontscale = (float)theApp.surfaceSizeX / 100.0f;
			glScalef(fontscale, fontscale, fontscale);
			glColor4f(1.0f, 0.25f, 0.25f, 1.0f);
			glFontTextOut((char *)((const char *)theApp.screenMessageBuffer), (theApp.surfaceSizeX-(theApp.screenMessageBuffer.GetLength()*11))/(fontscale*2), (theApp.surfaceSizeY-40)/fontscale, 0);
			glPopMatrix();
			glFontEnd();
			glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
			glBindTexture( GL_TEXTURE_2D, texture );
		} else {
			theApp.screenMessage = false;
		}
	}

	glFlush();
	SwapBuffers( hDC );
	// since OpenGL draws on the back buffer,
	// we have to swap it to the front buffer to see the content

}

//resize screen
void OpenGLDisplay::resize( int w, int h )
{
	initializeMatrices( w, h );
	
}

//update filtering methods
void OpenGLDisplay::updateFiltering( int value )
{
	switch( value )
	{
	case 0:
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		break;
	case 1:
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		break;
	}

	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP );
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP );
}

//init projection matrixes and viewports
void OpenGLDisplay::initializeMatrices( int w, int h )
{
	if( theApp.fullScreenStretch ) {
		glViewport( 0, 0, w, h );
	} else {
		calculateDestRect( w, h );
		glViewport(
			destRect.left,
			destRect.top,
			destRect.right - destRect.left,
			destRect.bottom - destRect.top );
	}

	glMatrixMode( GL_PROJECTION );
	glLoadIdentity();
	glOrtho(
		/* left   */ 1.0f,
		/* right  */ (GLdouble)(w - 1),
		/* bottom */ (GLdouble)(h - 1),
		/* top    */ 1.0f,
		0.0f,
		1.0f );

	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	
}

//init font texture
bool OpenGLDisplay::initializeTexture( int w, int h )
{
	// size = 2^n
	// w = 24  > size = 256 = 2^8
	// w = 255 > size = 256 = 2^8
	// w = 256 > size = 512 = 2^9
	// w = 300 > size = 512 = 2^9
	// OpenGL textures have to be square and a power of 2
	// We could use methods that allow tex's to not be powers of two
	// but that requires extra OGL extensions

	float n1 = log10( (float)w ) / log10( 2.0f );
	float n2 = log10( (float)h ) / log10( 2.0f );
	float n = ( n1 > n2 ) ? n1 : n2;

	if( ((float)((int)n)) != n ) {
		// round up
		n = ((float)((int)n)) + 1.0f;
	}

	size = pow( 2.0f, n );

	glGenTextures( 1, &texture );
	glBindTexture( GL_TEXTURE_2D, texture );
	updateFiltering( theApp.glFilter );

	glTexImage2D(
		GL_TEXTURE_2D,
		0,
		GL_RGBA,
		(GLsizei)size,
		(GLsizei)size,
		0,
		GL_RGBA,
		GL_UNSIGNED_BYTE,
		NULL );

	width = w;
	height = h;

	//return ( glGetError() == GL_NO_ERROR) ? true : false;
	// Workaround: We usually get GL_INVALID_VALUE, but somehow it works nevertheless
	// In consequence, we must not treat it as an error or else the app behaves as if an error occured.
	// This in the end results in theApp->input not being created = no input when switching from D3D to OGL
	return true;
}

//turn vsync on or off
void OpenGLDisplay::setVSync( int interval )
{
	const char *extensions = (const char *)glGetString( GL_EXTENSIONS );

	if( strstr( extensions, "WGL_EXT_swap_control" ) == 0 ) {
		winlog( "Error: WGL_EXT_swap_control extension not supported on your computer.\n" );
		return;
	} else {
		PFNWGLSWAPINTERVALFARPROC wglSwapIntervalEXT = NULL;
		wglSwapIntervalEXT = (PFNWGLSWAPINTERVALFARPROC)wglGetProcAddress( "wglSwapIntervalEXT" );
		if( wglSwapIntervalEXT ) {
			wglSwapIntervalEXT( interval );
		}
	}
}

//change render size for fonts and filter data
bool OpenGLDisplay::changeRenderSize( int w, int h )
{
	if( (width != w) || (height != h) ) {
		if( texture != 0 ) {
			glDeleteTextures( 1, &texture );
			texture = 0;
		}

		if( !initializeTexture( w, h ) ) {
			failed = true;
			return false;
		}
		if (filterData)
			free(filterData);
		filterData = (u8 *)malloc(4*w*h);
	}
	
	return true;
}

//calculate RECTs
void OpenGLDisplay::calculateDestRect( int w, int h )
{
	float scaleX = (float)w / (float)width;
	float scaleY = (float)h / (float)height;
	float min = (scaleX < scaleY) ? scaleX : scaleY;
	if( theApp.fsMaxScale && (min > theApp.fsMaxScale) ) {
		min = (float)theApp.fsMaxScale;
	}
	destRect.left = 0;
	destRect.top = 0;
	destRect.right = (LONG)(width * min);
	destRect.bottom = (LONG)(height * min);
	if( destRect.right != w ) {
		LONG diff = (w - destRect.right) / 2;
		destRect.left += diff;
		destRect.right += diff;
	}
	if( destRect.bottom != h ) {
		LONG diff = (h - destRect.bottom) / 2;
		destRect.top += diff;
		destRect.bottom += diff;
	}
	
}

//config options
void OpenGLDisplay::setOption( const char *option, int value )
{
	if( !_tcscmp( option, _T("vsync") ) ) {
		setVSync( value );
	}

	if( !_tcscmp( option, _T("glFilter") ) ) {
		updateFiltering( value );
	}

	if( !_tcscmp( option, _T("maxScale") ) ) {
		initializeMatrices( theApp.dest.right, theApp.dest.bottom );
	}

	if( !_tcscmp( option, _T("fullScreenStretch") ) ) {
		initializeMatrices( theApp.dest.right, theApp.dest.bottom );
	}
}

//set fullscreen mode
bool OpenGLDisplay::selectFullScreenMode( VIDEO_MODE &mode )
{
	FullscreenSettings dlg;
	dlg.setAPI( this->getType() );
	INT_PTR ret = dlg.DoModal();
	if( ret == IDOK ) {
		mode.adapter = dlg.m_device;
		switch( dlg.m_colorDepth )
		{
		case 30:
			// TODO: support
			return false;
			break;
		case 24:
			mode.bitDepth = 32;
			break;
		case 16:
		case 15:
			mode.bitDepth = 16;
			break;
		}
		mode.width = dlg.m_width;
		mode.height = dlg.m_height;
		mode.frequency = dlg.m_refreshRate;
		return true;
	} else {
		return false;
	}
}


IDisplay *newOpenGLDisplay()
{
	return new OpenGLDisplay();
}

#endif // #ifndef NO_OGL
