/*
===========================================================================
Copyright (C) 1999-2005 Id Software, Inc.

This file is part of Quake III Arena source code.

Quake III Arena source code 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 of the License,
or (at your option) any later version.

Quake III Arena source code 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 Quake III Arena source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
===========================================================================
*/
/*
** WIN_GLIMP.C
**
** This file contains ALL Win32 specific stuff having to do with the
** OpenGL refresh. When a port is being made the following functions
** must be implemented by the port:
**
** Sys_GL_EndFrame
** Sys_GL_Init
** Sys_GL_Shutdown
**
** Note that the GLW_xxx functions are Windows specific GL-subsystem
** related functions that are relevant ONLY to win_glimp.c
*/

#if _MSC_VER
#pragma warning (disable: 4996) // deprecated ZOMGOVERRUN! nannywhine
#endif

#include "../renderer/tr_local.h"
#include "resource.h"
#include "glw_win.h"
#include "wglext.h"
#include "win_local.h"


#define TRY_PFD_SUCCESS		0
#define TRY_PFD_FAIL_SOFT	1
#define TRY_PFD_FAIL_HARD	2

glwstate_t glw_state;


/*
** ChoosePFD
**
** Helper function that replaces ChoosePixelFormat.
*/
#define MAX_PFDS 256

static int GLW_ChoosePFD( HDC hDC, PIXELFORMATDESCRIPTOR *pPFD )
{
	PIXELFORMATDESCRIPTOR pfds[MAX_PFDS+1];
	int maxPFD = 0;
	int i;
	int bestMatch = 0;

	ri.Printf( PRINT_DEVELOPER, "...GLW_ChoosePFD( %d, %d, %d )\n", ( int ) pPFD->cColorBits, ( int ) pPFD->cDepthBits, ( int ) pPFD->cStencilBits );

	maxPFD = DescribePixelFormat( hDC, 1, sizeof( PIXELFORMATDESCRIPTOR ), &pfds[0] );
	if ( maxPFD > MAX_PFDS )
	{
		ri.Printf( PRINT_WARNING, "...numPFDs > MAX_PFDS (%d > %d)\n", maxPFD, MAX_PFDS );
		maxPFD = MAX_PFDS;
	}

	ri.Printf( PRINT_DEVELOPER, "...%d PFDs found\n", maxPFD - 1 );

	// grab information
	for ( i = 1; i <= maxPFD; i++ )
	{
		DescribePixelFormat( hDC, i, sizeof( PIXELFORMATDESCRIPTOR ), &pfds[i] );
	}

	// look for a best match
	for ( i = 1; i <= maxPFD; i++ )
	{
		//
		// make sure this has hardware acceleration
		//
		if ( ( pfds[i].dwFlags & PFD_GENERIC_FORMAT ) != 0 ) 
		{
			continue;
		}

		// verify pixel type
		if ( pfds[i].iPixelType != PFD_TYPE_RGBA )
		{
			if ( r_verbose->integer )
			{
				ri.Printf( PRINT_DEVELOPER, "...PFD %d rejected, not RGBA\n", i );
			}
			continue;
		}

		// verify proper flags
		if ( ( ( pfds[i].dwFlags & pPFD->dwFlags ) & pPFD->dwFlags ) != pPFD->dwFlags ) 
		{
			if ( r_verbose->integer )
			{
				ri.Printf( PRINT_DEVELOPER, "...PFD %d rejected, improper flags (%x instead of %x)\n", i, pfds[i].dwFlags, pPFD->dwFlags );
			}
			continue;
		}

		// verify enough bits
		if ( pfds[i].cDepthBits < 15 )
		{
			continue;
		}
		if ( ( pfds[i].cStencilBits < 4 ) && ( pPFD->cStencilBits > 0 ) )
		{
			continue;
		}

		//
		// selection criteria (in order of priority):
		// 
		//  colorBits
		//  depthBits
		//  stencilBits
		//
		if ( bestMatch )
		{
			// check color
			if ( pfds[bestMatch].cColorBits != pPFD->cColorBits )
			{
				// prefer perfect match
				if ( pfds[i].cColorBits == pPFD->cColorBits )
				{
					bestMatch = i;
					continue;
				}
				// otherwise if this PFD has more bits than our best, use it
				else if ( pfds[i].cColorBits > pfds[bestMatch].cColorBits )
				{
					bestMatch = i;
					continue;
				}
			}

			// check depth
			if ( pfds[bestMatch].cDepthBits != pPFD->cDepthBits )
			{
				// prefer perfect match
				if ( pfds[i].cDepthBits == pPFD->cDepthBits )
				{
					bestMatch = i;
					continue;
				}
				// otherwise if this PFD has more bits than our best, use it
				else if ( pfds[i].cDepthBits > pfds[bestMatch].cDepthBits )
				{
					bestMatch = i;
					continue;
				}
			}

			// check stencil
			if ( pfds[bestMatch].cStencilBits != pPFD->cStencilBits )
			{
				// prefer perfect match
				if ( pfds[i].cStencilBits == pPFD->cStencilBits )
				{
					bestMatch = i;
					continue;
				}
				// otherwise if this PFD has more bits than our best, use it
				else if ( ( pfds[i].cStencilBits > pfds[bestMatch].cStencilBits ) && 
					 ( pPFD->cStencilBits > 0 ) )
				{
					bestMatch = i;
					continue;
				}
			}
		}
		else
		{
			bestMatch = i;
		}
	}

	if ( !bestMatch )
		return 0;

	if ( pfds[bestMatch].dwFlags & (PFD_GENERIC_FORMAT | PFD_GENERIC_ACCELERATED) ) {
		ri.Printf( PRINT_ALL, "...no OpenGL ICD found\n" );
		return 0;
	}

	*pPFD = pfds[bestMatch];

	return bestMatch;
}

/*
** void GLW_CreatePFD
**
** Helper function zeros out then fills in a PFD
*/
static void GLW_CreatePFD( PIXELFORMATDESCRIPTOR *pPFD )
{
	ZeroMemory( pPFD, sizeof( *pPFD ) );
	pPFD->nSize = sizeof( *pPFD );
	pPFD->dwFlags		= PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
	pPFD->iPixelType	= PFD_TYPE_RGBA;
	pPFD->iLayerType	= PFD_MAIN_PLANE;
	pPFD->cColorBits	= 32;
	pPFD->cDepthBits	= 24;
	pPFD->cStencilBits	= 8;
}

static int GLW_MakeContext( PIXELFORMATDESCRIPTOR *pPFD )
{
	int pixelformat;

	//
	// don't putz around with pixelformat if it's already set (e.g. this is a soft
	// reset of the graphics system)
	//
	if ( !glw_state.pixelFormatSet )
	{
		//
		// choose, set, and describe our desired pixel format.  If we're
		// using a minidriver then we need to bypass the GDI functions,
		// otherwise use the GDI functions.
		//
		if (glw_state.nPendingPF)
			pixelformat = glw_state.nPendingPF;
		else
		if ( ( pixelformat = GLW_ChoosePFD( glw_state.hDC, pPFD ) ) == 0 )
		{
			ri.Printf( PRINT_ALL, "...GLW_ChoosePFD failed\n");
			return TRY_PFD_FAIL_SOFT;
		}
		ri.Printf( PRINT_DEVELOPER, "...PIXELFORMAT %d selected\n", pixelformat );

		DescribePixelFormat( glw_state.hDC, pixelformat, sizeof( *pPFD ), pPFD );

		if ( SetPixelFormat( glw_state.hDC, pixelformat, pPFD ) == FALSE )
		{
			ri.Printf( PRINT_ALL, "...SetPixelFormat failed\n", glw_state.hDC );
			return TRY_PFD_FAIL_SOFT;
		}

		glw_state.pixelFormatSet = qtrue;
	}

	//
	// startup the OpenGL subsystem by creating a context and making it current
	//
	if ( !glw_state.hGLRC )
	{
		if ( ( glw_state.hGLRC = qwglCreateContext( glw_state.hDC ) ) == 0 )
		{
			ri.Printf( PRINT_ALL, "...GL context creation failure\n" );
			return TRY_PFD_FAIL_HARD;
		}
		ri.Printf( PRINT_DEVELOPER, "...GL context created\n" );

		if ( !qwglMakeCurrent( glw_state.hDC, glw_state.hGLRC ) )
		{
			qwglDeleteContext( glw_state.hGLRC );
			glw_state.hGLRC = NULL;
			ri.Printf( PRINT_ALL, "...GL context creation currency failure\n" );
			return TRY_PFD_FAIL_HARD;
		}
		ri.Printf( PRINT_DEVELOPER, "...GL context creation made current\n" );
	}

	return TRY_PFD_SUCCESS;
}


/*
** - get a DC if one doesn't exist
** - create an HGLRC if one doesn't exist
*/
static qbool GLW_InitDriver()
{
	int		tpfd;
	static PIXELFORMATDESCRIPTOR pfd;		// save between frames since 'tr' gets cleared

	ri.Printf( PRINT_DEVELOPER, "Initializing OpenGL driver\n" );

	//
	// get a DC for our window if we don't already have one allocated
	//
	if ( glw_state.hDC == NULL )
	{
		if ( ( glw_state.hDC = GetDC( g_wv.hWnd ) ) == NULL )
		{
			ri.Printf( PRINT_ALL, "...Get DC failed\n" );
			return qfalse;
		}
		ri.Printf( PRINT_DEVELOPER, "...Get DC succeeded\n" );
	}

	//
	// attempt to set the PIXELFORMAT
	//
	if ( !glw_state.pixelFormatSet )
	{
		GLW_CreatePFD( &pfd );
		if ( ( tpfd = GLW_MakeContext( &pfd ) ) != TRY_PFD_SUCCESS )
		{
			if ( tpfd == TRY_PFD_FAIL_HARD )
			{
				ri.Printf( PRINT_WARNING, "...failed hard\n" );
				return qfalse;
			}

			ReleaseDC( g_wv.hWnd, glw_state.hDC );
			glw_state.hDC = NULL;
			ri.Printf( PRINT_ALL, "...failed to find an appropriate PIXELFORMAT\n" );
			return qfalse;
		}
	}

	// store PFD specifics
	//
	glConfig.colorBits = ( int ) pfd.cColorBits;
	glConfig.depthBits = ( int ) pfd.cDepthBits;
	glConfig.stencilBits = ( int ) pfd.cStencilBits;

	return qtrue;
}


// responsible for creating the Win32 window and initializing the OpenGL driver.

static qbool GLW_CreateWindow()
{
	static qbool s_classRegistered = qfalse;

	if ( !s_classRegistered )
	{
		WNDCLASS wc;
		memset( &wc, 0, sizeof( wc ) );

		wc.style         = CS_OWNDC;
		wc.lpfnWndProc   = MainWndProc;
		wc.cbClsExtra    = 0;
		wc.cbWndExtra    = 0;
		wc.hInstance     = g_wv.hInstance;
		wc.hIcon         = LoadIcon( g_wv.hInstance, MAKEINTRESOURCE(IDI_ICON1));
		wc.hCursor       = LoadCursor (NULL,IDC_ARROW);
		wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
		wc.lpszMenuName  = 0;
		wc.lpszClassName = CLIENT_WINDOW_TITLE;

		if ( !RegisterClass( &wc ) )
			ri.Error( ERR_FATAL, "GLW_CreateWindow: could not register window class" );

		s_classRegistered = qtrue;
		ri.Printf( PRINT_DEVELOPER, "...registered window class\n" );
	}

	//
	// create the HWND if one does not already exist
	//
	if ( !g_wv.hWnd )
	{
		g_wv.inputInitialized = qfalse;

		RECT r;
		r.left = 0;
		r.top = 0;
		r.right  = glInfo.winWidth;
		r.bottom = glInfo.winHeight;

		int style = WS_VISIBLE | WS_CLIPCHILDREN;
		int exstyle = 0;

		if ( glInfo.winFullscreen )
		{
			style |= WS_POPUP;
			exstyle |= WS_EX_TOPMOST;
		}
		else
		{
			style |= WS_OVERLAPPED | WS_BORDER | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX;
			AdjustWindowRect( &r, style, FALSE );
		}

		const int w = r.right - r.left;
		const int h = r.bottom - r.top;

		const RECT& monRect = g_wv.monitorRects[g_wv.monitor];

		int dx = 0;
		int dy = 0;

		if ( !glInfo.winFullscreen )
		{
			dx = ri.Cvar_Get( "vid_xpos", "0", 0 )->integer;
			dy = ri.Cvar_Get( "vid_ypos", "0", 0 )->integer;
			dx = Com_ClampInt( 0, max( 0, monRect.right - monRect.left - w ), dx );
			dy = Com_ClampInt( 0, max( 0, monRect.bottom - monRect.top - h ), dy );
		}

		const int x = monRect.left + dx;
		const int y = monRect.top + dy;

		g_wv.hWnd = CreateWindowEx( exstyle, CLIENT_WINDOW_TITLE, " " CLIENT_WINDOW_TITLE, style,
				x, y, w, h, NULL, NULL, g_wv.hInstance, NULL );

		if ( !g_wv.hWnd )
			ri.Error( ERR_FATAL, "GLW_CreateWindow() - Couldn't create window" );

		ShowWindow( g_wv.hWnd, SW_SHOW );
		UpdateWindow( g_wv.hWnd );
		ri.Printf( PRINT_DEVELOPER, "...created window@%d,%d (%dx%d)\n", x, y, w, h );
	}
	else
	{
		ri.Printf( PRINT_DEVELOPER, "...window already present, CreateWindowEx skipped\n" );
	}

	if ( !GLW_InitDriver() )
	{
		ShowWindow( g_wv.hWnd, SW_HIDE );
		DestroyWindow( g_wv.hWnd );
		g_wv.hWnd = NULL;
		return qfalse;
	}

	SetForegroundWindow( g_wv.hWnd );
	SetFocus( g_wv.hWnd );

	return qtrue;
}


static const char* GLW_GetCurrentDisplayDeviceName()
{
	static char deviceName[CCHDEVICENAME + 1];

	const HMONITOR hMonitor = g_wv.hMonitors[g_wv.monitor];
	if ( hMonitor == NULL )
		return NULL;

	MONITORINFOEXA info;
	ZeroMemory( &info, sizeof(info) );
	info.cbSize = sizeof(info);
	if ( GetMonitorInfoA(hMonitor, &info) == 0 )
		return NULL;

	Q_strncpyz( deviceName, info.szDevice, sizeof(deviceName) );
	
	return deviceName;
}


static void GLW_UpdateMonitorRect( const char* deviceName )
{
	if ( deviceName == NULL )
		return;

	DEVMODEA dm;
	ZeroMemory( &dm, sizeof(dm) );
	dm.dmSize = sizeof(dm);
	if ( EnumDisplaySettingsExA(deviceName, ENUM_CURRENT_SETTINGS, &dm, 0) == 0 )
		return;

	if ( dm.dmPelsWidth == 0 || dm.dmPelsHeight == 0 )
		return;

	// Normally, we should check dm.dmFields for the following flags:
	// DM_POSITION DM_PELSWIDTH DM_PELSHEIGHT
	// EnumDisplaySettingsExA doesn't always set up the flags properly.

	RECT& rect = g_wv.monitorRects[g_wv.monitor];
	rect.left = dm.dmPosition.x;
	rect.top = dm.dmPosition.y;
	rect.right = dm.dmPosition.x + dm.dmPelsWidth;
	rect.bottom = dm.dmPosition.y + dm.dmPelsHeight;
}


static qbool GLW_SetDisplaySettings( DEVMODE& dm )
{
	const char* deviceName = GLW_GetCurrentDisplayDeviceName();
	const int ec = ChangeDisplaySettingsExA( deviceName, &dm, NULL, CDS_FULLSCREEN, NULL );
	if ( ec == DISP_CHANGE_SUCCESSFUL )
	{
		glw_state.cdsDevMode = dm;
		glw_state.cdsDevModeValid = qtrue;
		GLW_UpdateMonitorRect( deviceName );
		return qtrue;
	}

	glw_state.cdsDevModeValid = qfalse;

	ri.Printf( PRINT_ALL, "...CDS: %ix%i (C%i) failed: ", (int)dm.dmPelsWidth, (int)dm.dmPelsHeight, (int)dm.dmBitsPerPel );

#define CDS_ERROR(x) case x: ri.Printf( PRINT_ALL, #x##"\n" ); break;
	switch (ec) {
		default:
			ri.Printf( PRINT_ALL, "unknown error %d\n", ec );
			break;
		CDS_ERROR( DISP_CHANGE_RESTART );
		CDS_ERROR( DISP_CHANGE_BADPARAM );
		CDS_ERROR( DISP_CHANGE_BADFLAGS );
		CDS_ERROR( DISP_CHANGE_FAILED );
		CDS_ERROR( DISP_CHANGE_BADMODE );
		CDS_ERROR( DISP_CHANGE_NOTUPDATED );
	}
#undef CDS_ERROR

	return qfalse;
}


static void GLW_ResetDisplaySettings( qbool invalidate )
{
	const char* deviceName = GLW_GetCurrentDisplayDeviceName();
	ChangeDisplaySettingsEx( deviceName, NULL, NULL, 0, NULL );
	GLW_UpdateMonitorRect( deviceName );
	if ( invalidate )
		glw_state.cdsDevModeValid = qfalse;
}


void WIN_SetGameDisplaySettings()
{
	if ( glw_state.cdsDevModeValid )
		GLW_SetDisplaySettings( glw_state.cdsDevMode );
}


void WIN_SetDesktopDisplaySettings()
{
	// We don't invalidate glw_state.cdsDevModeValid so we can
	// return to the previous mode later.
	GLW_ResetDisplaySettings( qfalse );
}


static qbool GLW_SetMode()
{
	WIN_UpdateMonitorIndexFromCvar();

	const RECT& monRect = g_wv.monitorRects[g_wv.monitor];
	const int desktopWidth = (int)(monRect.right - monRect.left);
	const int desktopHeight = (int)(monRect.bottom - monRect.top);
	R_ConfigureVideoMode( desktopWidth, desktopHeight );

	DEVMODE dm;
	ZeroMemory( &dm, sizeof( dm ) );
	dm.dmSize = sizeof( dm );

	if (glInfo.vidFullscreen != glw_state.cdsDevModeValid) {
		if (glInfo.vidFullscreen) {
			dm.dmPelsWidth  = glConfig.vidWidth;
			dm.dmPelsHeight = glConfig.vidHeight;
			dm.dmFields     = DM_PELSWIDTH | DM_PELSHEIGHT;

			if ( r_displayRefresh->integer ) {
				dm.dmDisplayFrequency = r_displayRefresh->integer;
				dm.dmFields |= DM_DISPLAYFREQUENCY;
			}

			dm.dmBitsPerPel = 32;
			dm.dmFields |= DM_BITSPERPEL;

			dm.dmPosition.x = monRect.left;
			dm.dmPosition.y = monRect.top;
			dm.dmFields |= DM_POSITION;

			glInfo.vidFullscreen = GLW_SetDisplaySettings( dm );
		}
		else
		{
			GLW_ResetDisplaySettings( qtrue );
		}
	}

	if (!GLW_CreateWindow())
		return qfalse;

	if (EnumDisplaySettingsA( GLW_GetCurrentDisplayDeviceName(), ENUM_CURRENT_SETTINGS, &dm ))
		glInfo.displayFrequency = dm.dmDisplayFrequency;

	return qtrue;
}


static qbool GLW_LoadOpenGL()
{
	// only real GL implementations are acceptable
	// load the driver and bind our function pointers to it
	if ( WIN_LoadGL( "opengl32" ) ) {
		// create the window and set up the context
		if ( GLW_SetMode() ) {
			return qtrue;
		}
	}

	WIN_UnloadGL();

	return qfalse;
}


void Sys_GL_EndFrame()
{
	if ( r_swapInterval->modified ) {
		r_swapInterval->modified = qfalse;

		if ( qwglSwapIntervalEXT ) {
			qwglSwapIntervalEXT( r_swapInterval->integer );
		}
	}

	SwapBuffers( glw_state.hDC );
}


void Sys_GL_Init()
{
	ri.Printf( PRINT_DEVELOPER, "Initializing OpenGL subsystem\n" );

	// load appropriate DLL and initialize subsystem
	if (!GLW_LoadOpenGL())
		ri.Error( ERR_FATAL, "Sys_GL_Init - could not load OpenGL subsystem\n" );
}


void Sys_GL_Shutdown()
{
	const char* success[] = { "failed", "success" };
	int retVal;

	Sys_ShutdownInput();

	// FIXME: Brian, we need better fallbacks from partially initialized failures
	if ( !qwglMakeCurrent ) {
		return;
	}

	ri.Printf( PRINT_DEVELOPER, "Shutting down OpenGL subsystem\n" );

	// set current context to NULL
	if ( qwglMakeCurrent )
	{
		retVal = qwglMakeCurrent( NULL, NULL ) != 0;
		ri.Printf( PRINT_DEVELOPER, "...wglMakeCurrent( NULL, NULL ): %s\n", success[retVal] );
	}

	// delete HGLRC
	if ( glw_state.hGLRC )
	{
		retVal = qwglDeleteContext( glw_state.hGLRC ) != 0;
		ri.Printf( PRINT_DEVELOPER, "...deleting GL context: %s\n", success[retVal] );
		glw_state.hGLRC = NULL;
	}

	// release DC
	if ( glw_state.hDC )
	{
		retVal = ReleaseDC( g_wv.hWnd, glw_state.hDC ) != 0;
		ri.Printf( PRINT_DEVELOPER, "...releasing DC: %s\n", success[retVal] );
		glw_state.hDC   = NULL;
	}

	// destroy window
	if ( g_wv.hWnd )
	{
		ri.Printf( PRINT_DEVELOPER, "...destroying window\n" );
		ShowWindow( g_wv.hWnd, SW_HIDE );
		DestroyWindow( g_wv.hWnd );
		g_wv.hWnd = NULL;
		glw_state.pixelFormatSet = qfalse;
	}

	// reset display settings
	if ( glw_state.cdsDevModeValid )
	{
		ri.Printf( PRINT_DEVELOPER, "...resetting display\n" );
		GLW_ResetDisplaySettings( qtrue );
	}

	// shutdown OpenGL subsystem
	WIN_UnloadGL();
}


qbool Sys_IsMinimized()
{
	return ( g_wv.hWnd != NULL ) && !!IsIconic( g_wv.hWnd );
}


void WIN_UpdateResolution( int width, int height )
{
	glInfo.winWidth = width;
	glInfo.winHeight = height;
	if ( r_fullscreen->integer == 0 )
	{
		glConfig.vidWidth = width;
		glConfig.vidHeight = height;
	}
}

