#include <windows.h>
#include <math.h>
#include <GL/gl.h>
#include <GL/glu.h>

#include "../Oni/Oni.h"

#include "../Daodan_Config.h"
#include "Utility.h"
#include "Win32.h"
#include "GL.h"

static const M3tDisplayMode daodan_reslist[] =
{
	{ 640,  480,  0, 0 },
	{ 720,  480,  0, 0 },
	{ 720,  576,  0, 0 },
	{ 768,  480,  0, 0 },
	{ 800,  480,  0, 0 },
	{ 800,  600,  0, 0 },
	{ 852,  480,  0, 0 },
	{ 856,  480,  0, 0 },
	{ 960,  540,  0, 0 },
	{ 960,  720,  0, 0 },
	{ 1024, 576,  0, 0 },
	{ 1024, 600,  0, 0 },
	{ 1024, 640,  0, 0 },
	{ 1024, 768,  0, 0 },
	{ 1152, 768,  0, 0 },
	{ 1152, 864,  0, 0 },
	{ 1280, 720,  0, 0 },
	{ 1280, 768,  0, 0 },
	{ 1280, 800,  0, 0 },
	{ 1280, 960,  0, 0 },
	{ 1280, 1024, 0, 0 },
	{ 1366, 768,  0, 0 },
	{ 1400, 1050, 0, 0 },
	{ 1440, 900,  0, 0 },
	{ 1600, 900,  0, 0 },
	{ 1600, 1200, 0, 0 },
	{ 1920, 1080, 0, 0 },
	{ 1920, 1200, 0, 0 },
	{ 1920, 1440, 0, 0 },
};

static DWORD window_style, window_exstyle;

// HACK: use additional device entries to store display modes. It would give us
// 67 mode slots total (far more than enough). I absolutely have no idea where
// Rossy got his 104 (it would take up to 0x660 bytes while the whole GL state
// is only 0x63c bytes). Maybe it was just octal (67 + 1).
// This hack would break (crash!) "m3_display_list" script command.
#define DD_MAX_MODES ((offsetof(M3tDrawEngineCaps,__unknown) - \
						offsetof(M3tDrawEngineCaps,DisplayDevices) - \
						offsetof(M3tDisplayDevice,Modes)) / sizeof(M3tDisplayMode))

// Former daodan_resdepths.
#define DD_MIN_DEPTH 16

unsigned short ONICALL DD_GLrEnumerateDisplayModes(M3tDisplayMode* modes)
{
	unsigned int vmodes = 0;
	unsigned int screen_x = GetSystemMetrics(SM_CXSCREEN);
	unsigned int screen_y = GetSystemMetrics(SM_CYSCREEN);
	
	unsigned int i;
	signed int j;
	
	STARTUPMESSAGE("Listing display modes", 0);

	memset(modes, 0, sizeof(M3tDisplayMode) * DD_MAX_MODES);

	if (M3gResolutionSwitch)
	{
		// Enumerate in -switch mode. "67 slots ought to be enough for anybody".

		DEVMODE dm;

		dm.dmSize        = sizeof(dm);
		dm.dmDriverExtra = 0;

		for (i = 0; EnumDisplaySettings(NULL, i, &dm); ++i)
		{
			if (dm.dmBitsPerPel < DD_MIN_DEPTH || dm.dmPelsWidth < 640 || dm.dmPelsHeight < 480)
				continue;
			if (dm.dmPelsWidth < dm.dmPelsHeight)
				continue;

			// Already exists? Search backwards as modes are sorted most of the times
			for (j = vmodes - 1; j >= 0; --j)
				if (modes[j].Width == dm.dmPelsWidth && modes[j].Height == dm.dmPelsHeight &&
					modes[j].Depth == dm.dmBitsPerPel)
					break;

			if (j >= 0)
				continue; // We've found a match.

			modes[vmodes].Width  = dm.dmPelsWidth;
			modes[vmodes].Height = dm.dmPelsHeight;
			modes[vmodes].Depth  = dm.dmBitsPerPel;

			if (++vmodes >= DD_MAX_MODES)
				break;
		}
	}
	else
	{
		// In -noswitch we put predefined window sizes which don't overlap
		// deskbar(s) plus one "native" fullscreen mode.

		unsigned int workarea_width, workarea_height, frame_width, frame_height;
		DWORD style, exstyle;
		RECT rc;

		if (SystemParametersInfo(SPI_GETWORKAREA, 0, &rc, 0))
		{
			workarea_width  = rc.right - rc.left;
			workarea_height = rc.bottom - rc.top;
		}
		else
		{
			workarea_width  = screen_x;
			workarea_height = screen_y;
		}

		style   = (DWORD) GetWindowLongPtr(ONgPlatformData.Window, GWL_STYLE);
		exstyle = (DWORD) GetWindowLongPtr(ONgPlatformData.Window, GWL_EXSTYLE);

		// Calculate additional width and height for window borders. Don't
		// bother with system metrics. Let Windows calculate this.
		rc.left  = rc.top = 0;
		rc.right = rc.bottom = 300;

		if (AdjustWindowRectEx(&rc, style, FALSE, exstyle))
		{
			frame_width  = rc.right - rc.left - 300;
			frame_height = rc.bottom - rc.top - 300;
		}
		else
		{
			frame_width  = 0;
			frame_height = 0;
		}

		for (i = 0; i < sizeof(daodan_reslist) / sizeof(daodan_reslist[0]); ++i)
		{
			// Don't check the mode which has the same rect as screen. We would
			// add it later as a special case.
			if (daodan_reslist[i].Width == screen_x && daodan_reslist[i].Height == screen_y)
				continue;
			
			if (daodan_reslist[i].Width + frame_width <= workarea_width &&
				daodan_reslist[i].Height + frame_height <= workarea_height)
			{
				modes[vmodes] = daodan_reslist[i];
				modes[vmodes].Depth = GLgInitialMode.dmBitsPerPel;

				if (++vmodes >= DD_MAX_MODES)
				{
					--vmodes; // Remove the last mode to make room for "fullscreen" mode.
					break;
				}
			}
		}

		modes[vmodes].Width  = GLgInitialMode.dmPelsWidth;
		modes[vmodes].Height = GLgInitialMode.dmPelsHeight;
		modes[vmodes].Depth  = GLgInitialMode.dmBitsPerPel;
		++vmodes;
	}

	STARTUPMESSAGE("%u modes available:", vmodes);
	for (i = 0; i < vmodes; ++i)
		STARTUPMESSAGE("   %ux%ux%u", modes[i].Width, modes[i].Height, modes[i].Depth);

	return vmodes;
}

// Sets a new display mode (if it is somehow different from a current mode).
// NOTE: signature for this function was changed to simplify code.
UUtBool DD_GLrPlatform_SetDisplayMode(M3tDisplayMode* mode)
{
	if (mode->Height < 480)
		return UUcFalse;

	if (M3gResolutionSwitch)
	{
		DEVMODE new_mode, cur_mode;

		cur_mode.dmSize        = sizeof(cur_mode);
		cur_mode.dmDriverExtra = 0;

		// We don't need this check. Windows does this too (see CDS_RESET).
		if (!EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &cur_mode) ||
			cur_mode.dmPelsWidth != mode->Width || cur_mode.dmPelsHeight !=  mode->Height ||
			cur_mode.dmBitsPerPel != mode->Depth)
		{
			new_mode.dmSize       = sizeof(new_mode);
			new_mode.dmFields     = DM_BITSPERPEL | DM_PELSHEIGHT | DM_PELSWIDTH;
			new_mode.dmPelsWidth  = mode->Width;
			new_mode.dmPelsHeight = mode->Height;
			new_mode.dmBitsPerPel = mode->Depth;

			if (ChangeDisplaySettings(&new_mode, CDS_FULLSCREEN) != DISP_CHANGE_SUCCESSFUL)
				return UUcFalse;
		}
		
		// We didn't change window size in DD_GLrPlatform_Initialize so we need
		// to change it here.
		SetWindowPos(ONgPlatformData.Window, NULL, 0, 0, mode->Width, mode->Height,
			SWP_NOACTIVATE | SWP_NOSENDCHANGING | SWP_NOZORDER);

		return UUcTrue;
	}
	else
	{
		unsigned screen_x, screen_y;
		DWORD style, exstyle, new_style, new_exstyle;
		DWORD flags;
		RECT rc, workarea_rc;
		POINT pt;

		screen_x = GetSystemMetrics(SM_CXSCREEN);
		screen_y = GetSystemMetrics(SM_CYSCREEN);

		GetClientRect(ONgPlatformData.Window, &rc);
		
		// Don't do anything if the mode was not changed.
		if (rc.right == mode->Width && rc.bottom == mode->Height)
			return UUcTrue;

		style   = (DWORD) GetWindowLongPtr(ONgPlatformData.Window, GWL_STYLE);
		exstyle = (DWORD) GetWindowLongPtr(ONgPlatformData.Window, GWL_EXSTYLE);
		flags   = SWP_NOACTIVATE | SWP_NOZORDER;

		// Remember initial window style to correctly restore from fullscreen.
		if (window_style == 0)
		{
			window_style = style;
			window_exstyle = exstyle;
		}

		if (mode->Width == screen_x && mode->Height == screen_y)
		{
			// "Fullscreen" mode.
			new_exstyle = exstyle & ~(WS_EX_CLIENTEDGE | WS_EX_DLGMODALFRAME | WS_EX_STATICEDGE | WS_EX_WINDOWEDGE);
			new_style   = style & ~(WS_CAPTION | WS_BORDER | WS_THICKFRAME | WS_DLGFRAME);
			new_style   = new_style | WS_POPUP;
			rc.left     = 0;
			rc.top      = 0;
			rc.right    = mode->Width;
			rc.bottom   = mode->Height;
		}
		else
		{
			ConfigOption_t* co = DDrConfig_GetOptOfType("windows.border", C_BOOL);
			if (co && co->value.intBoolVal)
			{
				pt.x = rc.left;
				pt.y = rc.top;
				ClientToScreen(ONgPlatformData.Window, &pt);
			}
			else
			{
				pt.x = screen_x / 2 - mode->Width / 2;
				pt.y = screen_y / 2 - mode->Height / 2;
			}

			new_exstyle = window_exstyle;
			new_style   = window_style;
			rc.left     = pt.x;
			rc.top      = pt.y;
			rc.right    = rc.left + mode->Width;
			rc.bottom   = rc.top + mode->Height;

			AdjustWindowRectEx(&rc, new_style, FALSE, new_exstyle);

			// Convert to width and height.
			rc.right  -= rc.left;
			rc.bottom -= rc.top;

			if (SystemParametersInfo(SPI_GETWORKAREA, 0, &workarea_rc, 0))
			{
				// We try to keep window position, but we should prevent window
				// from going off screen.

				if (rc.left + rc.right > workarea_rc.right)
					rc.left = workarea_rc.right - rc.right;
				if (rc.top + rc.bottom > workarea_rc.bottom)
					rc.top = workarea_rc.bottom - rc.bottom;

				//  Titlebar should always be visible.

				if (rc.left < workarea_rc.left)
					rc.left = workarea_rc.left;
				if (rc.top < workarea_rc.top)
					rc.top = workarea_rc.top;
			}
		}

		if (new_style != style)
		{
			SetWindowLongPtr(ONgPlatformData.Window, GWL_STYLE, (LONG_PTR) new_style);
			flags |= SWP_FRAMECHANGED | SWP_DRAWFRAME;
		}

		if (new_exstyle != exstyle)
		{
			SetWindowLongPtr(ONgPlatformData.Window, GWL_EXSTYLE, (LONG_PTR) new_exstyle);
			flags |= SWP_FRAMECHANGED | SWP_DRAWFRAME;
		}

		SetWindowPos(ONgPlatformData.Window, NULL, rc.left, rc.top, rc.right, rc.bottom, flags);
		return UUcTrue;
	}
}

static void ONICALL DD_GLiGamma_Restore(void)
{
	ConfigOption_t* co = DDrConfig_GetOptOfType("graphics.gamma", C_BOOL);
	if (co->value.intBoolVal)
	{
		if (gl_api->wglSetDeviceGammaRamp3DFX)
			gl_api->wglSetDeviceGammaRamp3DFX(gl->hDC, GLgInitialGammaRamp);
		else
			SetDeviceGammaRamp(gl->hDC, GLgInitialGammaRamp);
	}
}

static void ONICALL DD_GLiGamma_Initialize(void)
{
	ConfigOption_t* co = DDrConfig_GetOptOfType("graphics.gamma", C_BOOL);
	if (co->value.intBoolVal)
	{
		if (gl_api->wglSetDeviceGammaRamp3DFX)
		{
			STARTUPMESSAGE("Using 3dfx gamma adjustment", 0);
			GLgGammaRampValid = gl_api->wglGetDeviceGammaRamp3DFX(gl->hDC, GLgInitialGammaRamp);
		}
		else
		{
			STARTUPMESSAGE("Using Windows gamma adjustment", 0);
			GLgGammaRampValid = GetDeviceGammaRamp(gl->hDC, GLgInitialGammaRamp);
		}

		M3rSetGamma(ONrPersist_GetGamma());
	}
	else
	{
		GLgGammaRampValid = FALSE;
	}
}
 
// Disposes OpenGL engine. Called once.
void ONICALL DD_GLrPlatform_Dispose(void)
{
	DEVMODE dm;
	
	DD_GLiGamma_Restore();

	gl_api->wglMakeCurrent(NULL, NULL);
	gl_api->wglDeleteContext(gl->hGLRC);
	ReleaseDC(ONgPlatformData.Window, gl->hDC);

	// Restore initial display mode if it does not match current mode.
	
	dm.dmSize        = sizeof(dm);
	dm.dmDriverExtra = 0;
	
	if (!EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &dm) ||
		dm.dmPelsWidth != GLgInitialMode.dmPelsWidth ||
		dm.dmPelsHeight != GLgInitialMode.dmPelsHeight ||
		dm.dmBitsPerPel != GLgInitialMode.dmBitsPerPel)
	{
		ChangeDisplaySettings(&GLgInitialMode, 0);
	}
	
	// (skipping SetWindowPos as it only adds flickering)
	gl_unload_library();
}

// Initializes (and re-initializes) OpenGL.
UUtBool ONICALL DD_GLrPlatform_Initialize(void)
{
	static const M3tDisplayMode FallbackMode = { 640, 480, 16, 0 };

	if (!DD_GLrPlatform_SetDisplayMode(&gl->DisplayMode))
	{
		gl->DisplayMode = FallbackMode;
		
		if (!DD_GLrPlatform_SetDisplayMode(&gl->DisplayMode))
		{
			goto exit_err;
		}
	}

	// (DD_GLrPlatform_SetDisplayMode updates a window rectangle for us)
	
	if (!gl->hDC && !(gl->hDC = GetDC(ONgPlatformData.Window)))
	{
		goto exit_err;
	}

	ConfigOption_t* co = DDrConfig_GetOptOfType("graphics.gamma", C_BOOL);
	if (!M3gResolutionSwitch && co->value.intBoolVal)
	{
		STARTUPMESSAGE("Ignoring gamma setting due to windowed mode", 0);
		co->value.intBoolVal = false;
	}

	DD_GLiGamma_Initialize();

	// This creates a rendering context too.
	if (!gl_platform_set_pixel_format(gl->hDC))
	{
		if (gl->DisplayMode.Depth != 16)
		{
			gl->DisplayMode.Depth = 16;
			if (!DD_GLrPlatform_SetDisplayMode(&gl->DisplayMode))
				goto exit_err;
			
			if (!gl_platform_set_pixel_format(gl->hDC))
				goto exit_err;
		}
	}

	return UUcTrue;

exit_err:
	AUrMessageBox(1, "Daodan: Failed to initialize OpenGL contexts; Oni will now exit.");
	exit(0);
	return 0;
}
