/* Copyright (c) 2012 Wildfire Games
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

/*
 * emulate SDL on Windows.
 */

#include "precompiled.h"
#include "lib/external_libraries/libsdl.h"

#if CONFIG2_WSDL

#include <stdio.h>
#include <math.h>
#include <queue>
#include <algorithm>

#include "lib/sysdep/os/win/win.h"
#include <process.h>	// _beginthreadex
#include <WindowsX.h>	// message crackers

#include "lib/module_init.h"
#include "lib/posix/posix_pthread.h"
#include "lib/sysdep/sysdep.h"
#include "lib/sysdep/os/win/wutil.h"
#include "lib/sysdep/os/win/winit.h"
#include "lib/sysdep/os/win/wmi.h"	// for SDL_GetVideoInfo

#if MSC_VERSION
#pragma comment(lib, "user32.lib")
#pragma comment(lib, "gdi32.lib")
#endif

#include "lib/ogl.h"		// needed to pull in the delay-loaded opengl32.dll


WINIT_REGISTER_LATE_INIT(wsdl_Init);
WINIT_REGISTER_EARLY_SHUTDOWN(wsdl_Shutdown);

// in fullscreen mode, i.e. not windowed.
// video mode will be restored when app is deactivated.
static bool fullscreen;

// the app is shutting down.
// if set, ignore further Windows messages for clean shutdown.
static bool is_quitting;

static HWND g_hWnd = (HWND)INVALID_HANDLE_VALUE;

// usable at any time (required by SDL_SetGamma); this is made
// mostly safe via CS_OWNDC.
static HDC g_hDC = (HDC)INVALID_HANDLE_VALUE;


//----------------------------------------------------------------------------
// gamma

class GammaRamp
{
public:
	GammaRamp()
		: m_hasChanged(false)
	{
	}

	bool Change(HDC hDC, float gamma_r, float gamma_g, float gamma_b)
	{
		// get current ramp (once) so we can later restore it.
		if(!m_hasChanged)
		{
			ENSURE(wutil_IsValidHandle(hDC));
			if(!GetDeviceGammaRamp(hDC, m_original))
				return false;
		}

		Compute(gamma_r, m_changed+0*256);
		Compute(gamma_g, m_changed+1*256);
		Compute(gamma_b, m_changed+2*256);
		if(!Upload(m_changed))
			return false;

		m_hasChanged = true;
		return true;
	}

	void Latch()
	{
		if(m_hasChanged)
		{
			if(!Upload(m_changed))
				m_hasChanged = false;
		}

	}

	void RestoreOriginal()
	{
		if(m_hasChanged)
		{
			if(!Upload(m_original))
				m_hasChanged = false;
		}
	}

private:
	static void Compute(float gamma, u16* ramp)
	{
		// assume identity if invalid
		if(gamma <= 0.0f)
			gamma = 1.0f;

		// identity: special-case to make sure we get exact values
		if(gamma == 1.0f)
		{
			for(u16 i = 0; i < 256; i++)
				ramp[i] = u16(i << 8);
			return;
		}

		for(int i = 0; i < 256; i++)
		{
			const double val = pow(i/255.0, (double)gamma);
			const double clamped = std::max(0.0, std::min(val, 1.0-DBL_EPSILON));
			ramp[i] = u16_from_double(clamped);
		}
		ENSURE(ramp[0] == 0);
		ENSURE(ramp[255] == 0xFFFF);
	}

	bool Upload(u16* ramps)
	{
		WinScopedPreserveLastError s;
		SetLastError(0);
		ENSURE(wutil_IsValidHandle(g_hDC));
		BOOL ok = SetDeviceGammaRamp(g_hDC, ramps);
		// on multi-monitor NVidia systems, the first call after a reboot
		// fails, but subsequent ones succeed.
		// see http://icculus.org/pipermail/quake3-bugzilla/2009-October/001316.html
		if(ok == FALSE)
		{
			ok = SetDeviceGammaRamp(g_hDC, ramps);
			// at least one 32-bit XP system STILL fails here,
			// so don't raise an error dialog.
			if(!ok)
				debug_printf(L"SetDeviceGammaRamp failed twice. Oh well.\n");
		}
		return (ok == TRUE);
	}

	bool m_hasChanged;

	// values are 8.8 fixed point
	u16 m_original[3*256];
	u16 m_changed[3*256];
};

static GammaRamp gammaRamp;


// note: any component gamma = 0 is assumed to be identity.
int SDL_SetGamma(float r, float g, float b)
{
	return gammaRamp.Change(g_hDC, r, g, b)? 0 : -1;
}


//----------------------------------------------------------------------------
// window
//----------------------------------------------------------------------------

static DWORD wnd_ChooseWindowStyle(bool fullscreen, HWND previousWindow = (HWND)INVALID_HANDLE_VALUE)
{
	DWORD windowStyle = fullscreen? WS_POPUP : WS_POPUPWINDOW|WS_CAPTION|WS_MINIMIZEBOX;
	windowStyle |= WS_VISIBLE;
	windowStyle |= WS_CLIPCHILDREN|WS_CLIPSIBLINGS;	// MSDN SetPixelFormat says this is required

	if(!fullscreen)	// windowed
	{
		// support resizing
		windowStyle |= WS_SIZEBOX|WS_MAXIMIZEBOX;

		// remember the previous maximized state
		// (else subsequent attempts to maximize will fail)
		if(wutil_IsValidHandle(previousWindow))
		{
			const DWORD previousWindowState = GetWindowLongW(previousWindow, GWL_STYLE);
			windowStyle |= (previousWindowState & WS_MAXIMIZE);
		}
	}

	return windowStyle;
}


// @param w,h value-return (in: desired, out: actual pixel count)
static void wnd_UpdateWindowDimensions(DWORD windowStyle, int& w, int& h)
{
	// Calculate the size of the outer window, so that the client area has
	// the desired dimensions.
	RECT r;
	r.left = r.top = 0;
	r.right = w; r.bottom = h;
	if(AdjustWindowRectEx(&r, windowStyle, FALSE, 0))
	{
		w = r.right - r.left;
		h = r.bottom - r.top;
	}
}


static LRESULT CALLBACK OnMessage(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

static HWND wnd_CreateWindow(int w, int h)
{
	// (create new window every time (instead of once at startup), because
	// pixel format isn't supposed to be changed more than once)

	// app instance.
	// returned by GetModuleHandle and used in keyboard hook and window creation. 
	const HINSTANCE hInst = GetModuleHandle(0);

	// register window class
	WNDCLASSW wc;
	memset(&wc, 0, sizeof(wc));
	// no CS_VREDRAW and CS_HREDRAW - avoids redrawing when resized.
	wc.style = CS_OWNDC;	// (see g_hDC definition)
	wc.lpfnWndProc = OnMessage;
	wc.lpszClassName = L"WSDL{55752F43-0241-492C-8648-C7243397FCE4}";
	wc.hInstance = hInst;
	ATOM class_atom = RegisterClassW(&wc);
    // ignore failure, which is probably caused by not unregistering the class
    // (does not happen automatically when called from a DLL). just re-use
    // the existing class, which is safe because our class name is unique.

	const DWORD windowStyle = wnd_ChooseWindowStyle(fullscreen);
	wnd_UpdateWindowDimensions(windowStyle, w, h);

	// note: you can override the hard-coded window name via SDL_WM_SetCaption.
	HWND hWnd = CreateWindowExW(WS_EX_APPWINDOW, (LPCWSTR)(uintptr_t)class_atom, L"wsdl", windowStyle, 0, 0, w, h, 0, 0, hInst, 0);
	if(!wutil_IsValidHandle(hWnd))
		DEBUG_WARN_ERR(ERR::FAIL);
	return hWnd;
}


static RECT ClientRect(HWND hWnd)
{
	RECT rect;
	ENSURE(wutil_IsValidHandle(hWnd));
	WARN_IF_FALSE(GetClientRect(hWnd, &rect));
	return rect;
}


//----------------------------------------------------------------------------
// video
//----------------------------------------------------------------------------

static DEVMODE dm;			// current video mode
static HGLRC hGLRC = (HGLRC)INVALID_HANDLE_VALUE;

// set via SDL_GL_SetAttribute:
static int depthBufferBits = 24;
static int stencilBufferBits = 0;
static int vsyncEnabled = 1;

int SDL_GL_SetAttribute(SDL_GLattr attr, int value)
{
	switch(attr)
	{
	case SDL_GL_DEPTH_SIZE:
		depthBufferBits = value;
		break;

	case SDL_GL_STENCIL_SIZE:
		stencilBufferBits = value;
		break;

	case SDL_GL_SWAP_CONTROL:
		vsyncEnabled = value;
		break;

	case SDL_GL_DOUBLEBUFFER:
		// (always enabled)
		break;
	}

	return 0;
}


// check if resolution needs to be changed
static bool video_NeedsChange(int w, int h, int cur_w, int cur_h, bool fullscreen)
{
	// invalid: keep current settings
	if(w <= 0 || h <= 0)
		return false;

	// higher resolution mode needed
	if(w > cur_w || h > cur_h)
		return true;

	// fullscreen requested and not exact same mode set
	if(fullscreen && (w != cur_w || h != cur_h))
		return true;

	return false;
}


static void video_SetPixelFormat(HDC hDC, int bpp)
{
	const DWORD dwFlags = PFD_SUPPORT_OPENGL|PFD_DRAW_TO_WINDOW|PFD_DOUBLEBUFFER;
	BYTE cColourBits = (BYTE)bpp;
	BYTE cAlphaBits = 0;
	if(bpp == 32)
	{
		cColourBits = 24;
		cAlphaBits = 8;
	}
	const BYTE cAccumBits   = 0;
	const BYTE cDepthBits   = (BYTE)depthBufferBits;
	const BYTE cStencilBits = (BYTE)stencilBufferBits;
	const BYTE cAuxBuffers  = 0;

	PIXELFORMATDESCRIPTOR pfd =
	{
		sizeof(PIXELFORMATDESCRIPTOR),
		1,								// version
		dwFlags,
		PFD_TYPE_RGBA,
		cColourBits, 0, 0, 0, 0, 0, 0,	// c*Bits, c*Shift are unused
		cAlphaBits, 0,					// cAlphaShift is unused
		cAccumBits,	0, 0, 0, 0,			// cAccum*Bits are unused
		cDepthBits,
		cStencilBits,
		cAuxBuffers,
		PFD_MAIN_PLANE,
		0, 0, 0, 0						// bReserved, dw*Mask are unused
	};

	// note: the GDI pixel format functions require opengl32.dll to be loaded.
	// a deadlock on the next line is probably due to VLD's LdrLoadDll hook.

	const int pf = ChoosePixelFormat(hDC, &pfd);
	ENSURE(pf >= 1);
	WARN_IF_FALSE(SetPixelFormat(hDC, pf, &pfd));
}


// set video mode width x height : bpp (or leave unchanged if already adequate).
// w = h = bpp = 0 => no change.
SDL_Surface* SDL_SetVideoMode(int w, int h, int bpp, Uint32 flags)
{
	WinScopedPreserveLastError s;	// OpenGL and GDI

	fullscreen = (flags & SDL_FULLSCREEN) != 0;

	// get current mode settings
	memset(&dm, 0, sizeof(dm));
	dm.dmSize = sizeof(dm);
	WARN_IF_FALSE(EnumDisplaySettings(0, ENUM_CURRENT_SETTINGS, &dm));
	const int cur_w = (int)dm.dmPelsWidth, cur_h = (int)dm.dmPelsHeight;

	// independent of resolution; app must always get bpp it wants
	dm.dmBitsPerPel = bpp;
	dm.dmFields = DM_BITSPERPEL;

	if(video_NeedsChange(w,h, cur_w,cur_h, fullscreen))
	{
		dm.dmPelsWidth  = (DWORD)w;
		dm.dmPelsHeight = (DWORD)h;
		dm.dmFields |= DM_PELSWIDTH|DM_PELSHEIGHT;
	}
	// the (possibly changed) mode will be (re)set at next WM_ACTIVATE

	if(!wutil_IsValidHandle(g_hWnd))
	{
		g_hWnd = wnd_CreateWindow(w, h);
		if(!wutil_IsValidHandle(g_hWnd))
			return 0;

		g_hDC = GetDC(g_hWnd);

		video_SetPixelFormat(g_hDC, bpp);

		hGLRC = wglCreateContext(g_hDC);
		if(!hGLRC)
			return 0;

		if(!wglMakeCurrent(g_hDC, hGLRC))
			return 0;
	}
	else	// update the existing window
	{
		const DWORD windowStyle = wnd_ChooseWindowStyle(fullscreen, g_hWnd);
		wnd_UpdateWindowDimensions(windowStyle, w, h);

		UINT swp_flags = SWP_FRAMECHANGED|SWP_NOZORDER|SWP_NOACTIVATE;
		if(!fullscreen)	// windowed: preserve the top-left corner
			swp_flags |= SWP_NOMOVE;

		WARN_IF_FALSE(SetWindowLongW(g_hWnd, GWL_STYLE, windowStyle));
		WARN_IF_FALSE(SetWindowPos(g_hWnd, 0, 0, 0, w, h, swp_flags));

		LONG status;
		if(fullscreen)
		{
			ShowWindow(g_hWnd, SW_RESTORE);
			status = ChangeDisplaySettings(&dm, CDS_FULLSCREEN);
		}
		else
		{
			status = ChangeDisplaySettings(0, 0);
			// don't ShowWindow with SW_MINIMIZE (we just want to update)
		}
		ENSURE(status == DISP_CHANGE_SUCCESSFUL);
	}

	// (required for ogl_HaveExtension, but callers should also invoke
	// ogl_Init in case the real SDL is being used.)
	ogl_Init();
	if(ogl_HaveExtension("WGL_EXT_swap_control") && pwglSwapIntervalEXT)
		pwglSwapIntervalEXT(vsyncEnabled);

	const RECT rect = ClientRect(g_hWnd);	// updated window size

	static SDL_PixelFormat format;
	format.BitsPerPixel = bpp;

	static SDL_Surface screen;
	screen.w = rect.right;
	screen.h = rect.bottom;
	screen.format = &format;
	return &screen;
}


static void video_Shutdown()
{
	if(fullscreen)
	{
		LONG status = ChangeDisplaySettings(0, 0);
		ENSURE(status == DISP_CHANGE_SUCCESSFUL);
	}

	if(wutil_IsValidHandle(hGLRC))
	{
		WARN_IF_FALSE(wglMakeCurrent(0, 0));
		WARN_IF_FALSE(wglDeleteContext(hGLRC));
		hGLRC = (HGLRC)INVALID_HANDLE_VALUE;
	}

	g_hWnd = (HWND)INVALID_HANDLE_VALUE;
	g_hDC = (HDC)INVALID_HANDLE_VALUE;
}


void SDL_GL_SwapBuffers()
{
	WARN_IF_FALSE(SwapBuffers(g_hDC));
}


SDL_VideoInfo* SDL_GetVideoInfo()
{
	static SDL_VideoInfo video_info;

	if(video_info.video_mem == 0)
	{
		WmiInstances instances;
		if(wmi_GetClassInstances(L"Win32_VideoController", instances) == INFO::OK)
		{
			for(WmiInstances::iterator it = instances.begin(); it != instances.end(); ++it)
			{
				if((*it)[L"Availability"].intVal != 8)	// not offline
				{
					video_info.video_mem = std::max<Uint32>(video_info.video_mem, (*it)[L"AdapterRAM"].lVal / KiB);
					break;
				}
			}
		}
	}

	return &video_info;
}


SDL_Surface* SDL_GetVideoSurface()
{
	return 0;
}


//----------------------------------------------------------------------------
// event queue

// note: we only use winit to redirect stdout; this queue won't be used
// before _cinit.
typedef std::queue<SDL_Event> Queue;
static Queue g_queue;

static void QueueEvent(const SDL_Event& ev)
{
	g_queue.push(ev);
}

static bool DequeueEvent(SDL_Event& ev)
{
	if(g_queue.empty())
		return false;
	ev = g_queue.front();
	g_queue.pop();
	return true;
}


//----------------------------------------------------------------------------
// keyboard

// note: keysym.unicode is only returned for SDL_KEYDOWN, and is otherwise 0.
static void QueueKeyEvent(Uint8 type, SDLKey sdlk, WCHAR unicode_char)
{
	SDL_Event ev;
	ev.type = type;
	ev.key.keysym.sym = sdlk;
	ev.key.keysym.unicode = (Uint16)unicode_char;
	QueueEvent(ev);
}


static Uint8 keys[SDLK_LAST];

static SDLKey g_SDLKeyForVK[256]; // g_SDLKeyForVK[vk] == SDLK

static void key_Init()
{
	// Map the VK keysyms
	for(size_t i = 0; i < ARRAY_SIZE(g_SDLKeyForVK); i++)
		g_SDLKeyForVK[i] = SDLK_UNKNOWN;

	g_SDLKeyForVK[VK_BACK] = SDLK_BACKSPACE;
	g_SDLKeyForVK[VK_TAB] = SDLK_TAB;
	g_SDLKeyForVK[VK_CLEAR] = SDLK_CLEAR;
	g_SDLKeyForVK[VK_RETURN] = SDLK_RETURN;
	g_SDLKeyForVK[VK_PAUSE] = SDLK_PAUSE;
	g_SDLKeyForVK[VK_ESCAPE] = SDLK_ESCAPE;
	g_SDLKeyForVK[VK_SPACE] = SDLK_SPACE;
	g_SDLKeyForVK[VK_OEM_7] = SDLK_QUOTE;
	g_SDLKeyForVK[VK_OEM_COMMA] = SDLK_COMMA;
	g_SDLKeyForVK[VK_OEM_MINUS] = SDLK_MINUS;
	g_SDLKeyForVK[VK_OEM_PERIOD] = SDLK_PERIOD;
	g_SDLKeyForVK[VK_OEM_2] = SDLK_SLASH;
	g_SDLKeyForVK[VK_OEM_1] = SDLK_SEMICOLON;
	g_SDLKeyForVK[VK_OEM_PLUS] = SDLK_EQUALS;
	g_SDLKeyForVK[VK_OEM_4] = SDLK_LEFTBRACKET;
	g_SDLKeyForVK[VK_OEM_5] = SDLK_BACKSLASH;
	g_SDLKeyForVK[VK_OEM_6] = SDLK_RIGHTBRACKET;
	g_SDLKeyForVK[VK_OEM_3] = SDLK_BACKQUOTE;
	g_SDLKeyForVK[VK_OEM_8] = SDLK_BACKQUOTE;

	// winuser.h guarantees A..Z and 0..9 match their ASCII values:
	const int VK_0 = '0';
	for(int i = 0; i < 10; i++)
		g_SDLKeyForVK[VK_0+i] = (SDLKey)(SDLK_0+i);
	const int VK_A = 'A';
	for(int i = 0; i < 26; i++)
		g_SDLKeyForVK[VK_A+i] = (SDLKey)(SDLK_a+i);

	g_SDLKeyForVK[VK_DELETE] = SDLK_DELETE;

	for(int i = 0; i < 10; i++)
		g_SDLKeyForVK[VK_NUMPAD0+i] = (SDLKey)(SDLK_KP0+i);

	g_SDLKeyForVK[VK_DECIMAL] = SDLK_KP_PERIOD;
	g_SDLKeyForVK[VK_DIVIDE] = SDLK_KP_DIVIDE;
	g_SDLKeyForVK[VK_MULTIPLY] = SDLK_KP_MULTIPLY;
	g_SDLKeyForVK[VK_SUBTRACT] = SDLK_KP_MINUS;
	g_SDLKeyForVK[VK_ADD] = SDLK_KP_PLUS;

	g_SDLKeyForVK[VK_UP] = SDLK_UP;
	g_SDLKeyForVK[VK_DOWN] = SDLK_DOWN;
	g_SDLKeyForVK[VK_RIGHT] = SDLK_RIGHT;
	g_SDLKeyForVK[VK_LEFT] = SDLK_LEFT;
	g_SDLKeyForVK[VK_INSERT] = SDLK_INSERT;
	g_SDLKeyForVK[VK_HOME] = SDLK_HOME;
	g_SDLKeyForVK[VK_END] = SDLK_END;
	g_SDLKeyForVK[VK_PRIOR] = SDLK_PAGEUP;
	g_SDLKeyForVK[VK_NEXT] = SDLK_PAGEDOWN;

	for(int i = 0; i < 12; i++)
		g_SDLKeyForVK[VK_F1+i] = (SDLKey)(SDLK_F1+i);

	g_SDLKeyForVK[VK_NUMLOCK] = SDLK_NUMLOCK;
	g_SDLKeyForVK[VK_CAPITAL] = SDLK_CAPSLOCK;
	g_SDLKeyForVK[VK_SCROLL] = SDLK_SCROLLOCK;
	g_SDLKeyForVK[VK_RSHIFT] = SDLK_RSHIFT;
	g_SDLKeyForVK[VK_LSHIFT] = SDLK_LSHIFT;
	g_SDLKeyForVK[VK_SHIFT] = SDLK_LSHIFT; // XXX: Not quite
	g_SDLKeyForVK[VK_RCONTROL] = SDLK_RCTRL;
	g_SDLKeyForVK[VK_LCONTROL] = SDLK_LCTRL;
	g_SDLKeyForVK[VK_CONTROL] = SDLK_LCTRL; // XXX: Not quite
	g_SDLKeyForVK[VK_RMENU] = SDLK_RALT;
	g_SDLKeyForVK[VK_LMENU] = SDLK_LALT;
	g_SDLKeyForVK[VK_MENU] = SDLK_LALT; // XXX: Not quite
	g_SDLKeyForVK[VK_RWIN] = SDLK_RSUPER;
	g_SDLKeyForVK[VK_LWIN] = SDLK_LSUPER;

	g_SDLKeyForVK[VK_HELP] = SDLK_HELP;
#ifdef VK_PRINT
	g_SDLKeyForVK[VK_PRINT] = SDLK_PRINT;
#endif
	g_SDLKeyForVK[VK_SNAPSHOT] = SDLK_PRINT;
	g_SDLKeyForVK[VK_CANCEL] = SDLK_BREAK;
	g_SDLKeyForVK[VK_APPS] = SDLK_MENU;
}


static inline SDLKey SDLKeyFromVK(int vk)
{
	if(!(0 <= vk && vk < 256))
	{
		DEBUG_WARN_ERR(ERR::LOGIC);	// invalid vk
		return SDLK_UNKNOWN;
	}
	return g_SDLKeyForVK[vk];
}


static void key_ResetAll()
{
	SDL_Event spoofed_up_event;
	spoofed_up_event.type = SDL_KEYUP;
	spoofed_up_event.key.keysym.unicode = 0;

	for(size_t i = 0; i < ARRAY_SIZE(keys); i++)
	{
		if(keys[i])
		{
			spoofed_up_event.key.keysym.sym = (SDLKey)i;
			QueueEvent(spoofed_up_event);
		}
	}
}


static LRESULT OnKey(HWND UNUSED(hWnd), UINT vk, BOOL fDown, int UNUSED(cRepeat), UINT flags)
{
	// TODO Mappings for left/right modifier keys
	// TODO Modifier statekeeping

	const SDLKey sdlk = SDLKeyFromVK(vk);
	if(sdlk != SDLK_UNKNOWN)
		keys[sdlk] = (Uint8)fDown;

	if(!fDown)
		QueueKeyEvent(SDL_KEYUP, sdlk, 0);
	else
	{
		// note: flags is HIWORD(lParam) from WM_KEYDOWN, which includes
		// scancode. ToUnicode also uses its bit 15 to determine if the
		// key is currently pressed.
		const UINT scancode = flags;
		u8 key_states[256];
		WARN_IF_FALSE(GetKeyboardState(key_states));
		WCHAR wchars[8];
		int output_count = ToUnicode(vk, scancode, key_states, wchars, ARRAY_SIZE(wchars), 0);
		// translation succeeded; queue each produced character
		if(output_count > 0)
		{
			for(int i = 0; i < output_count; i++)
				QueueKeyEvent(SDL_KEYDOWN, sdlk, wchars[i]);
		}
		// dead-char; do nothing
		else if(output_count == -1)
		{
		}
		// translation failed; just generate a regular (non-unicode) event
		else if(output_count == 0)
			QueueKeyEvent(SDL_KEYDOWN, sdlk, 0);
		else
			UNREACHABLE;
	}

	return 0;
}


Uint8* SDL_GetKeyState(int* num_keys)
{
	if(num_keys)
		*num_keys = SDLK_LAST;
	return keys;
}


// always on (we don't care about the extra overhead)
int SDL_EnableUNICODE(int UNUSED(enable))
{
	return 1;
}


//----------------------------------------------------------------------------
// joystick

int SDL_NumJoysticks()
{
	return 0;
}

int SDL_JoystickEventState(int UNUSED(state))
{
	return 0;
}

const char* SDL_JoystickName(int UNUSED(device_index))
{
	return NULL;
}

SDL_Joystick* SDL_JoystickOpen(int UNUSED(device_index))
{
	return NULL;
}

int SDL_JoystickNumAxes(SDL_Joystick* UNUSED(joystick))
{
	return 0;
}

Sint16 SDL_JoystickGetAxis(SDL_Joystick* UNUSED(joystick), int UNUSED(axis))
{
	return 0;
}


//----------------------------------------------------------------------------
// app activation

enum SdlActivationType { LOSE = 0, GAIN = 1 };

static inline void QueueActiveEvent(SdlActivationType type, size_t changed_app_state)
{
	// SDL says this event is not generated when the window is created,
	// but skipping the first event may confuse things.

	SDL_Event ev;
	ev.type = SDL_ACTIVEEVENT;
	ev.active.state = (u8)changed_app_state;
	ev.active.gain  = (u8)((type == GAIN)? 1 : 0);
	QueueEvent(ev);
}


// SDL_APP* bitflags indicating whether we are active.
// note: responsibility for yielding lies with SDL apps -
// they control the main loop.
static Uint8 app_state;

static void active_ChangeState(SdlActivationType type, Uint8 changed_app_state)
{
	Uint8 old_app_state = app_state;

	if(type == GAIN)
		app_state = Uint8(app_state | changed_app_state);
	else
		app_state = Uint8(app_state & ~changed_app_state);

	// generate an event - but only if the given state flags actually changed.
	if((old_app_state & changed_app_state) != (app_state & changed_app_state))
		QueueActiveEvent(type, changed_app_state);
}


static LRESULT OnActivate(HWND hWnd, UINT state, HWND UNUSED(hWndActDeact), BOOL fMinimized)
{
	SdlActivationType type;
	Uint8 changed_app_state;

	// went active and not minimized
	if(state != WA_INACTIVE && !fMinimized)
	{
		type = GAIN;
		changed_app_state = SDL_APPINPUTFOCUS|SDL_APPACTIVE;

		// grab keyboard focus (we previously had DefWindowProc do this).
		SetFocus(hWnd);

		gammaRamp.Latch();
		if(fullscreen)
		{
			ShowWindow(g_hWnd, SW_RESTORE);
			const LONG ret = ChangeDisplaySettings(&dm, CDS_FULLSCREEN);
			ENSURE(ret == DISP_CHANGE_SUCCESSFUL);
		}

		// re-assert mouse grab state
		SDL_WM_GrabInput(SDL_WM_GrabInput(SDL_GRAB_QUERY));
	}
	// deactivated (Alt+Tab out) or minimized
	else
	{
		type = LOSE;
		changed_app_state = SDL_APPINPUTFOCUS;
		if(fMinimized)
			changed_app_state |= SDL_APPACTIVE;

		key_ResetAll();

		gammaRamp.RestoreOriginal();
		if(fullscreen)
		{
			const LONG ret = ChangeDisplaySettings(0, 0);
			ENSURE(ret == DISP_CHANGE_SUCCESSFUL);
			WARN_IF_FALSE(ShowWindow(g_hWnd, SW_MINIMIZE));
		}
	}

	active_ChangeState(type, changed_app_state);
	return 0;
}


Uint8 SDL_GetAppState()
{
	return app_state;
}


static void QueueQuitEvent()
{
	SDL_Event ev;
	ev.type = SDL_QUIT;
	QueueEvent(ev);
}


//----------------------------------------------------------------------------
// mouse

// background: there are several types of coordinates.
// - screen coords are relative to the primary desktop and may therefore be
//   negative on multi-monitor systems (e.g. if secondary monitor is left of
//   primary). they are prefixed with screen_*.
// - "client" coords are simply relative to the parent window's origin and
//   can also be negative (e.g. in the window's NC area).
//   these are prefixed with client_*.
// - "idealized" coords are what the app sees. these range from 0 to
//   windowDimensions-1. they are returned by mouse_GetCoords and have no prefix.

static void QueueMouseEvent(int x, int y)
{
	SDL_Event ev;
	ev.type = SDL_MOUSEMOTION;
	ENSURE(unsigned(x|y) <= USHRT_MAX);
	ev.motion.x = (Uint16)x;
	ev.motion.y = (Uint16)y;
	QueueEvent(ev);
}

static void QueueButtonEvent(int button, int state, int x, int y)
{
	SDL_Event ev;
	ev.type = Uint8((state == SDL_PRESSED)? SDL_MOUSEBUTTONDOWN : SDL_MOUSEBUTTONUP);
	ev.button.button = (u8)button;
	ev.button.state  = (u8)state;
	ENSURE(unsigned(x|y) <= USHRT_MAX);
	ev.button.x = (Uint16)x;
	ev.button.y = (Uint16)y;
	QueueEvent(ev);
}


static int mouse_x, mouse_y;

// generate a mouse move message and update our notion of the mouse position.
// x, y are client pixel coordinates.
// notes:
// - does not actually move the OS cursor;
// - called from mouse_Update and SDL_WarpMouse.
static void mouse_UpdatePosition(int x, int y)
{
	// nothing to do if it hasn't changed since last time
	if(mouse_x == x && mouse_y == y)
		return;

	mouse_x = x;
	mouse_y = y;
	QueueMouseEvent(x, y);
}

static POINT mouse_ScreenFromClient(int client_x, int client_y)
{
	POINT screen_pt;
	screen_pt.x = (LONG)client_x;
	screen_pt.y = (LONG)client_y;
	WARN_IF_FALSE(ClientToScreen(g_hWnd, &screen_pt));
	return screen_pt;
}

// get idealized client coordinates or return false if outside our window.
static bool mouse_GetCoords(int screen_x, int screen_y, int& x, int& y)
{
	ENSURE(wutil_IsValidHandle(g_hWnd));

	POINT screen_pt;
	screen_pt.x = (LONG)screen_x;
	screen_pt.y = (LONG)screen_y;

	POINT client_pt;
	{
		// note: MapWindowPoints has a really stupid interface, returning 0
		// on failure or if no shift was needed (i.e. window is fullscreen).
		// we must use GetLastError to detect error conditions.
		WinScopedPreserveLastError s;
		SetLastError(0);
		client_pt = screen_pt;	// translated below
		const int ret = MapWindowPoints(HWND_DESKTOP, g_hWnd, &client_pt, 1);
		ENSURE(ret != 0 || GetLastError() == 0);
	}

	{
		const RECT client_rect = ClientRect(g_hWnd);
		if(!PtInRect(&client_rect, client_pt))
			return false;
	}

	if(WindowFromPoint(screen_pt) != g_hWnd)
		return false;

	x = client_pt.x;
	y = client_pt.y;
	ENSURE(x >= 0 && y >= 0);
	return true;
}


static void mouse_Update()
{
	// window not created yet or already shut down. no sense reporting
	// mouse position, and bail now to avoid ScreenToClient failing.
	if(!wutil_IsValidHandle(g_hWnd))
		return;

	// don't use DirectInput, because we want to respect the user's mouse
	// sensitivity settings. Windows messages are prone to lag,
	// so query current position directly.
	// note: GetCursorPos fails if the desktop is switched (e.g. after
	// pressing Ctrl+Alt+Del), which can be ignored.
	POINT screen_pt;
	if(!GetCursorPos(&screen_pt))
		return;
	int x, y;
	if(mouse_GetCoords(screen_pt.x, screen_pt.y, x, y))
	{
		active_ChangeState(GAIN, SDL_APPMOUSEFOCUS);
		mouse_UpdatePosition(x, y);
	}
	// moved outside of window
	else
		active_ChangeState(LOSE, SDL_APPMOUSEFOCUS);
}


static unsigned mouse_buttons;

// (we define a new function signature since the windowsx.h message crackers
// don't provide for passing uMsg)
static LRESULT OnMouseButton(HWND UNUSED(hWnd), UINT uMsg, int client_x, int client_y, UINT flags)
{
	int button;
	int state;
	switch(uMsg)
	{
	case WM_LBUTTONDOWN:
		button = SDL_BUTTON_LEFT;
		state = SDL_PRESSED;
		break;
	case WM_LBUTTONUP:
		button = SDL_BUTTON_LEFT;
		state = SDL_RELEASED;
		break;
	case WM_RBUTTONDOWN:
		button = SDL_BUTTON_RIGHT;
		state = SDL_PRESSED;
		break;
	case WM_RBUTTONUP:
		button = SDL_BUTTON_RIGHT;
		state = SDL_RELEASED;
		break;
	case WM_MBUTTONDOWN:
		button = SDL_BUTTON_MIDDLE;
		state = SDL_PRESSED;
		break;
	case WM_MBUTTONUP:
		button = SDL_BUTTON_MIDDLE;
		state = SDL_RELEASED;
		break;
	case WM_XBUTTONDOWN:
		button = SDL_BUTTON_X1 + GET_XBUTTON_WPARAM(flags) - 1;
		state = SDL_PRESSED;
		break;
	case WM_XBUTTONUP:
		button = SDL_BUTTON_X1 + GET_XBUTTON_WPARAM(flags) - 1;
		state = SDL_RELEASED;
		break;
	NODEFAULT;
	}

	// mouse capture
	static int outstanding_press_events;
	if(state == SDL_PRESSED)
	{
		// grab mouse to ensure we get up events
		if(++outstanding_press_events > 0)
			(void)SetCapture(g_hWnd);	// (returns previous window)
	}
	else
	{
		// release after all up events received
		if(--outstanding_press_events <= 0)
		{
			WARN_IF_FALSE(ReleaseCapture());
			outstanding_press_events = 0;
		}
	}

	// update button bitfield
	if(state == SDL_PRESSED)
		mouse_buttons |= SDL_BUTTON(button);
	else
		mouse_buttons &= ~SDL_BUTTON(button);

	const POINT screen_pt = mouse_ScreenFromClient(client_x, client_y);
	int x, y;
	if(mouse_GetCoords(screen_pt.x, screen_pt.y, x, y))
		QueueButtonEvent(button, state, x, y);

	// Per MSDN, return TRUE for XBUTTON events
	if (uMsg == WM_XBUTTONDOWN || uMsg == WM_XBUTTONUP)
		return TRUE;

	return 0;
}


// (note: this message is sent even if the cursor is outside our window)
static LRESULT OnMouseWheel(HWND UNUSED(hWnd), int screen_x, int screen_y, int zDelta, UINT UNUSED(fwKeys))
{
	int x, y;
	if(mouse_GetCoords(screen_x, screen_y, x, y))
	{
		int button = (zDelta < 0)? SDL_BUTTON_WHEELDOWN : SDL_BUTTON_WHEELUP;
		// SDL says this sends a down message followed by up.
		QueueButtonEvent(button, SDL_PRESSED,  x, y);
		QueueButtonEvent(button, SDL_RELEASED, x, y);
	}

	return 0;	// handled
}


Uint8 SDL_GetMouseState(int* x, int* y)
{
	if(x)
		*x = (int)mouse_x;
	if(y)
		*y = (int)mouse_y;
	return (Uint8)mouse_buttons;
}


void SDL_WarpMouse(int x, int y)
{
	// SDL interface provides for int, but the values should be
	// idealized client coords (>= 0)
	ENSURE(x >= 0 && y >= 0);
	mouse_UpdatePosition(x, y);

	const int client_x = x, client_y = y;
	const POINT screen_pt = mouse_ScreenFromClient(client_x, client_y);
	WARN_IF_FALSE(SetCursorPos(screen_pt.x, screen_pt.y));
}


int SDL_ShowCursor(int toggle)
{
	static int cursor_visible = SDL_ENABLE;
	if(toggle != SDL_QUERY)
	{
		// only call Windows ShowCursor if changing the visibility -
		// it maintains a counter.
		if(cursor_visible != toggle)
		{
			(void)ShowCursor(toggle);	// (returns display counter)
			cursor_visible = toggle;
		}
	}
	return cursor_visible;
}


SDL_GrabMode SDL_WM_GrabInput(SDL_GrabMode mode)
{
	static SDL_GrabMode prevMode;
	if(mode == SDL_GRAB_QUERY)
		return prevMode;
	prevMode = mode;

	if(mode == SDL_GRAB_OFF)
		WARN_IF_FALSE(ClipCursor(0));
	else
	{
		const RECT clientRect = ClientRect(g_hWnd);
		POINT upperLeft = { clientRect.left, clientRect.top };
		WARN_IF_FALSE(ClientToScreen(g_hWnd, &upperLeft));
		POINT lowerRight = { clientRect.right, clientRect.bottom };
		WARN_IF_FALSE(ClientToScreen(g_hWnd, &lowerRight));
		const RECT screenRect = { upperLeft.x, upperLeft.y, lowerRight.x, lowerRight.y };
		WARN_IF_FALSE(ClipCursor(&screenRect));
	}

	return mode;
}


//----------------------------------------------------------------------------
// video resizing/expose

static bool ResizeEventEnabled(int clientWidth, int clientHeight)
{
	// when the app receives a resize event, it must call SDL_SetVideoMode.
	// avoid that if the size hasn't actually changed. this also
	// prevents infinite recursion, since SDL_SetVideoMode might
	// trigger resize events. note that this logic is safer than a
	// skipResize flag, because that would remain set if SDL_SetVideoMode
	// doesn't trigger a resize, and we would miss an actual resize.
	static int lastClientWidth, lastClientHeight;
	if(lastClientWidth == clientWidth && lastClientHeight == clientHeight)
		return false;
	lastClientWidth = clientWidth;
	lastClientHeight = clientHeight;

	// if fullscreen, interaction with other topmost windows causes
	// minimization and a spurious resize. however, the app only
	// expects resizing events if !fullscreen.
	if(fullscreen)
		return false;

	// this happens during minimization, which results in an
	// app-active event anyway, and the app might have
	// trouble with size=0.
	if(clientWidth == 0 && clientHeight == 0)
		return false;

	return true;
}

// note: this is called continuously during resizing. since SDL doesn't
// discard any SDL_VIDEORESIZE events, the application must deal with
// the flood (and only call SDL_SetVideoMode once a frame or similar).
// note: SDL uses WM_WINDOWPOSCHANGING, which requires calling
// GetClientRect and suffers from false alarms.
static void OnSize(HWND UNUSED(hWnd), UINT UNUSED(state), int clientWidth, int clientHeight)
{
	if(!ResizeEventEnabled(clientWidth, clientHeight))
		return;

	SDL_Event ev;
	ev.type = SDL_VIDEORESIZE;
	ev.resize.w = clientWidth;
	ev.resize.h = clientHeight;
	QueueEvent(ev);
}


static BOOL OnEraseBkgnd(HWND UNUSED(hWnd), HDC UNUSED(hDC))
{
	SDL_Event ev;
	ev.type = SDL_VIDEOEXPOSE;
	QueueEvent(ev);

	// prevent GDI from erasing the background by claiming we did so.
	// PAINTSTRUCT.fErase will later be FALSE.
	return TRUE;
}


//----------------------------------------------------------------------------

static LRESULT OnPaint(HWND hWnd)
{
	// BeginPaint/EndPaint is unnecessary (see http://opengl.czweb.org/ch04/082-084.html)
	// however, we at least need to validate the window to prevent
	// continuous WM_PAINT messages.
	WARN_IF_FALSE(ValidateRect(hWnd, 0));
	return 0;
}

static LRESULT OnDestroy(HWND hWnd)
{
	ENSURE(hWnd == g_hWnd);
	WARN_IF_FALSE(ReleaseDC(g_hWnd, g_hDC));
	g_hDC = (HDC)INVALID_HANDLE_VALUE;
	g_hWnd = (HWND)INVALID_HANDLE_VALUE;
	QueueQuitEvent();

#ifdef _DEBUG
	// see http://www.adrianmccarthy.com/blog/?p=51
	// with WM_QUIT in the message queue, MessageBox will immediately
	// return IDABORT. to ensure any subsequent CRT error reports are
	// at least somewhat visible, we redirect them to debug output.
	_CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_DEBUG);
#endif

	PostQuitMessage(0);
	return 0;
}


enum DefWindowProcDisposition
{
	kSkipDefWindowProc,
	kRunDefWindowProc
};

static DefWindowProcDisposition OnSysCommand(WPARAM wParam)
{
	switch(wParam)
	{
	case SC_SCREENSAVE:
		// disable screen-saver in fullscreen mode (other applications
		// may interfere with us, and we have set the system-wide gamma)
		if(fullscreen)
			return kSkipDefWindowProc;
		break;

	// Alt+F4 or system menu double-click / exit
	case SC_CLOSE:
		QueueQuitEvent();
		break;
	}

	return kRunDefWindowProc;
}


static LRESULT CALLBACK OnMessage(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	if(is_quitting)
		return DefWindowProcW(hWnd, uMsg, wParam, lParam);

	switch(uMsg)
	{
	case WM_PAINT:
		return OnPaint(hWnd);

	HANDLE_MSG(hWnd, WM_ERASEBKGND, OnEraseBkgnd);

	// prevent selecting menu in fullscreen mode
	case WM_NCHITTEST:
		if(fullscreen)
			return HTCLIENT;
		break;

	HANDLE_MSG(hWnd, WM_ACTIVATE, OnActivate);
	HANDLE_MSG(hWnd, WM_DESTROY, OnDestroy);

	case WM_SYSCOMMAND:
		if(OnSysCommand(wParam) == kSkipDefWindowProc)
			return 0;
		break;

	HANDLE_MSG(hWnd, WM_SYSKEYUP  , OnKey);
	HANDLE_MSG(hWnd, WM_KEYUP     , OnKey);
	HANDLE_MSG(hWnd, WM_SYSKEYDOWN, OnKey);
	HANDLE_MSG(hWnd, WM_KEYDOWN   , OnKey);

	HANDLE_MSG(hWnd, WM_MOUSEWHEEL, OnMouseWheel);

	HANDLE_MSG(hWnd, WM_SIZE, OnSize);

	// (can't use message crackers: they do not provide for passing uMsg)
	case WM_LBUTTONDOWN:
	case WM_LBUTTONUP:
	case WM_RBUTTONDOWN:
	case WM_RBUTTONUP:
	case WM_MBUTTONDOWN:
	case WM_MBUTTONUP:
	case WM_XBUTTONDOWN:
	case WM_XBUTTONUP:
		return OnMouseButton(hWnd, uMsg, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), (UINT)wParam);

	default:
		// (DefWindowProc must be called outside the switch because some
		// messages are only conditionally 'grabbed', e.g. NCHITTEST)
		break;
	}

	return DefWindowProcW(hWnd, uMsg, wParam, lParam);
}


void SDL_PumpEvents()
{
	// rationale: we would like to reduce CPU usage automatically if
	// possible. blocking here until a message arrives would accomplish
	// that, but might potentially freeze the app too long.
	// instead, they should check active state and call SDL_Delay etc.
	// if our window is minimized.

	mouse_Update();	// polled

	MSG msg;
	while(PeekMessageW(&msg, 0, 0, 0, PM_REMOVE))
	{
		(void)DispatchMessageW(&msg);	// (returns window procedure's return value)
	}
}



int SDL_PollEvent(SDL_Event* ev)
{
	SDL_PumpEvents();

	if(DequeueEvent(*ev))
		return 1;

	return 0;
}


int SDL_PushEvent(SDL_Event* ev)
{
	QueueEvent(*ev);
	return 0;
}


//-----------------------------------------------------------------------------
// byte swapping

// implement only if the header hasn't mapped SDL_Swap* to intrinsics

#ifndef SDL_Swap16
u16 SDL_Swap16(const u16 x)
{
	return (u16)(((x & 0xff) << 8) | (x >> 8));
}
#endif

#ifndef SDL_Swap32
u32 SDL_Swap32(const u32 x)
{
	return (x << 24) |
	       (x >> 24) |
	       ((x << 8) & 0x00ff0000) |
	       ((x >> 8) & 0x0000ff00);
}
#endif

#ifndef SDL_Swap64
u64 SDL_Swap64(const u64 x)
{
	const u32 lo = (u32)(x & 0xFFFFFFFF);
	const u32 hi = (u32)(x >> 32);
	u64 ret = SDL_Swap32(lo);
	ret <<= 32;
		// careful: must shift var of type u64, not u32
	ret |= SDL_Swap32(hi);
	return ret;
}
#endif


//-----------------------------------------------------------------------------
// multithread support

// semaphores
// note: implementing these in terms of pthread sem_t doesn't help;
// this wrapper is very close to the Win32 routines.

static HANDLE HANDLE_from_sem(SDL_sem* s)
{
	return (HANDLE)s;
}

static SDL_sem* sem_from_HANDLE(HANDLE h)
{
	return (SDL_sem*)h;
}

SDL_sem* SDL_CreateSemaphore(int cnt)
{
	HANDLE h = CreateSemaphore(0, cnt, std::numeric_limits<LONG>::max(), 0);
	return sem_from_HANDLE(h);
}

void SDL_DestroySemaphore(SDL_sem* sem)
{
	HANDLE h = HANDLE_from_sem(sem);
	WARN_IF_FALSE(CloseHandle(h));
}

int SDL_SemPost(SDL_sem* sem)
{
	HANDLE h = HANDLE_from_sem(sem);
	return ReleaseSemaphore(h, 1, 0);
}

int SDL_SemWait(SDL_sem* sem)
{
	HANDLE h = HANDLE_from_sem(sem);
	return WaitForSingleObject(h, INFINITE);
}


//-----------------------------------------------------------------------------
// misc API

void SDL_WM_SetCaption(const char* title, const char* icon)
{
	WARN_IF_FALSE(SetWindowTextA(g_hWnd, title));

	// real SDL ignores this parameter, so we will follow suit.
	UNUSED2(icon);
}


u32 SDL_GetTicks()
{
	return GetTickCount();
}


void SDL_Delay(Uint32 ms)
{
	Sleep(ms);
}


void* SDL_GL_GetProcAddress(const char* name)
{
	return wglGetProcAddress(name);
}


//-----------------------------------------------------------------------------
// init/shutdown

// defend against calling SDL_Quit twice (GameSetup does this to work
// around ATI driver breakage)
static ModuleInitState initState;

static Status Init()
{
	key_Init();
	return INFO::OK;
}

static void Shutdown()
{
	is_quitting = true;

	if(wutil_IsValidHandle(g_hDC))
		gammaRamp.RestoreOriginal();

	if(wutil_IsValidHandle(g_hWnd))
		WARN_IF_FALSE(DestroyWindow(g_hWnd));

	video_Shutdown();
}

int SDL_Init(Uint32 UNUSED(flags))
{
	return (ModuleInit(&initState, Init) < 0)? -1 : 0;
}

int SDL_InitSubSystem(Uint32 UNUSED(flags))
{
	return 0;
}

void SDL_Quit()
{
	ModuleShutdown(&initState, Shutdown);
}


static void RedirectStdout()
{
	// this process is apparently attached to a console, and users might be
	// surprised to find that we redirected the output to a file, so don't.
	if(wutil_IsValidHandle(GetStdHandle(STD_OUTPUT_HANDLE)))
		return;

	WinScopedPreserveLastError s;	// ChangeExtension

	// this code may be included in multiple executables sharing the same
	// directory, so include the executable's name in the filename. use its
	// full path since the current directory is unreliable.
	const OsPath pathname = sys_ExecutablePathname().ChangeExtension(L"_stdout.txt");

 	// ignore BoundsChecker warnings here. subsystem is set to "Windows"
 	// to prevent the OS from opening a console on startup (ugly).
	// that means stdout isn't associated with a lowio handle; _close is
 	// called with fd = -1. oh well, there's nothing we can do.
	FILE* f = 0;
	// ignore return value - it might indicate 'file already exists' even
	// if f is valid, which is what actually counts.
	(void)_wfreopen_s(&f, OsString(pathname).c_str(), L"wt", stdout);
	if(GetLastError() == ERROR_ALREADY_EXISTS)
		SetLastError(0);
	// executable directory (probably Program Files) is read-only for
	// non-Administrators. we can't pick another directory because
	// ah_log_dir might not be valid until the app's init has run,
	// nor should we pollute the (root) wutil_AppdataPath directory.
	// since stdout usually isn't critical and is seen if launching the
	// app from a console, just skip the redirection in this case.
	if(f == 0)
		return;

#if CONFIG_ENABLE_CHECKS
	// disable buffering, so that no writes are lost even if the program
 	// crashes. only enabled in full debug mode because this is really slow!
 	setvbuf(stdout, 0, _IONBF, 0);
 #endif
}

static Status wsdl_Init()
{
	// note: SDL does this in its WinMain hook. doing so in SDL_Init would be
	// too late (we might miss some output), so we use winit.
	// (this is possible because _cinit has already been called)
	RedirectStdout();

 	return INFO::OK;
}

static Status wsdl_Shutdown()
{
	// was redirected to stdout.txt; closing avoids a BoundsChecker warning.
	fclose(stdout);

	return INFO::OK;
}

#endif	// #if CONFIG2_WSDL
