/**
 * =========================================================================
 * File        : wsdl.cpp
 * Project     : 0 A.D.
 * Description : emulate SDL on Windows.
 *
 * @author Jan.Wassenberg@stud.uni-karlsruhe.de
 * =========================================================================
 */

/*
 * Copyright (c) 2003-2004 Jan Wassenberg
 *
 * Redistribution and/or modification are also permitted under the
 * terms of the GNU General Public License as published by the
 * Free Software Foundation (version 2 or later, at your option).
 *
 * 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.
 */

#include "precompiled.h"

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

#include "win_internal.h"
#include <ddraw.h>
#include <process.h>	// _beginthreadex
#include <WindowsX.h>	// message crackers

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


// for easy removal of DirectDraw dependency (used to query total video mem)
#define DDRAW

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

// don't bother with dynamic linking -
// DirectX is present in all Windows versions since Win95.
#ifdef DDRAW
# pragma comment(lib, "ddraw.lib")
#endif

#endif


#pragma data_seg(WIN_CALLBACK_PRE_MAIN(b))
WIN_REGISTER_FUNC(wsdl_init);
#pragma data_seg(WIN_CALLBACK_POST_ATEXIT(d))
WIN_REGISTER_FUNC(wsdl_shutdown);
#pragma data_seg()


// 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_shutdown;

// app instance.
// returned by GetModuleHandle and used in kbd hook and window creation. 
static HINSTANCE hInst = 0;

HWND hWnd = (HWND)INVALID_HANDLE_VALUE;
static HDC hDC = (HDC)INVALID_HANDLE_VALUE;	// needed by gamma code


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

static bool gamma_changed;
static u16 org_ramp[3][256];
static u16 cur_ramp[3][256];


// ramp: 8.8 fixed point
static LibError calc_gamma_ramp(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] = (i << 8);
		return INFO_OK;
	}

	const double inv_gamma = 1.0 / gamma;
	for(int i = 0; i < 256; i++)
	{
		const double frac = i / 256.0;
		// don't add 1/256 - this isn't time-critical and
		// accuracy is more important.
		// need a temp variable to disambiguate pow() argument type.
		ramp[i] = fp_to_u16(pow(frac, inv_gamma));
	}

	return INFO_OK;
}


enum GammaAction
{
	GAMMA_LATCH_NEW_RAMP,
	GAMMA_RESTORE_ORIGINAL
};

static void gamma_swap(GammaAction action)
{
	if(gamma_changed)
	{
		void* ramp = (action == GAMMA_LATCH_NEW_RAMP)? cur_ramp : org_ramp;
		SetDeviceGammaRamp(hDC, ramp);
	}
}


// note: any component gamma = 0 is assumed to be identity.
int SDL_SetGamma(float r, float g, float b)
{
	// if we haven't successfully changed gamma yet,
	// get current ramp so we can later restore it.
	if(!gamma_changed)
		if(!GetDeviceGammaRamp(hDC, org_ramp))
			return -1;

	LibError err1 = calc_gamma_ramp(r, cur_ramp[0]);
	LibError err2 = calc_gamma_ramp(g, cur_ramp[1]);
	LibError err3 = calc_gamma_ramp(b, cur_ramp[2]);
	if(err1 != INFO_OK || err2 != INFO_OK || err3 != INFO_OK)
		return -1;

	if(!SetDeviceGammaRamp(hDC, cur_ramp))
		return -1;

	gamma_changed = true;
	return 0;
}


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

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

static int depth_bits = 24;	// depth buffer size; set via SDL_GL_SetAttribute

// check if resolution needs to be changed
static bool video_need_change(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 inline void video_enter_game_mode()
{
	ShowWindow(hWnd, SW_RESTORE);
	ChangeDisplaySettings(&dm, CDS_FULLSCREEN);
}

static inline void video_leave_game_mode()
{
	ChangeDisplaySettings(0, 0);
	ShowWindow(hWnd, SW_MINIMIZE);
}


int SDL_GL_SetAttribute(SDL_GLattr attr, int value)
{
	if(attr == SDL_GL_DEPTH_SIZE)
		depth_bits = value;

	return 0;
}


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

// set video mode wxh:bpp if necessary.
// w = h = bpp = 0 => no change.
int SDL_SetVideoMode(int w, int h, int bpp, unsigned long flags)
{
	int ret = 0;	// assume failure
	WIN_SAVE_LAST_ERROR;	// OpenGL and GDI

	fullscreen = (flags & SDL_FULLSCREEN) != 0;

	// get current mode settings
	memset(&dm, 0, sizeof(dm));
	dm.dmSize = sizeof(dm);
	EnumDisplaySettings(0, ENUM_CURRENT_SETTINGS, &dm);
	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_need_change(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

	//
	// window init
	// create new window every time (instead of once at startup), because
	// pixel format isn't supposed to be changed more than once
	//

	// register window class
	WNDCLASS wc;
	memset(&wc, 0, sizeof(wc));
	wc.lpfnWndProc = wndproc;
	wc.lpszClassName = "WSDL";
	wc.hInstance = hInst;
	ATOM class_atom = RegisterClass(&wc);
	if(!class_atom)
	{
		debug_warn("SDL_SetVideoMode: RegisterClass failed");
		return 0;
	}

	DWORD windowStyle = fullscreen ? (WS_POPUP|WS_VISIBLE) : WS_VISIBLE | WS_CAPTION|WS_POPUPWINDOW|WS_MINIMIZEBOX;

	// 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;
	}

	// note: you can override the hardcoded window name via SDL_WM_SetCaption.
	hWnd = CreateWindowEx(0, (LPCSTR)class_atom, "wsdl", windowStyle, 0, 0, w, h, 0, 0, hInst, 0);
	if(!hWnd)
		return 0;

	hDC = GetDC(hWnd);


	//
	// pixel format
	//

	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)depth_bits;
	const BYTE cStencilBits = 0;
	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
	};

	// GDI pixel format functions apparently require opengl to be loaded
	// (may not have been done yet, if delay-loaded). call a gl function
	// (no-op) to make sure.
	(void)glGetError();

	int pf = ChoosePixelFormat(hDC, &pfd);
	if(!SetPixelFormat(hDC, pf, &pfd))
		goto fail;

	hGLRC = wglCreateContext(hDC);
	if(!hGLRC)
		goto fail;

	if(!wglMakeCurrent(hDC, hGLRC))
		goto fail;

	ret = 1;

fail:
	WIN_RESTORE_LAST_ERROR;
	return ret;
}


static void video_shutdown()
{
	if(fullscreen)
		if(ChangeDisplaySettings(0, 0) != DISP_CHANGE_SUCCESSFUL)
			debug_warn("ChangeDisplaySettings failed");

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

	if(hDC != INVALID_HANDLE_VALUE)
	{
		// return value is whether the DC was actually freed.
		// this seems to sometimes be 0, so don't warn.
		(void)ReleaseDC(hWnd, hDC);
		hDC = (HDC)INVALID_HANDLE_VALUE;
	}

	if(hWnd != INVALID_HANDLE_VALUE)
	{
		// this also seems to fail spuriously with GetLastError == 0,
		// so don't complain.
		(void)DestroyWindow(hWnd);
		hWnd = (HWND)INVALID_HANDLE_VALUE;
	}
}


void SDL_GL_SwapBuffers()
{
	SwapBuffers(hDC);
}


SDL_VideoInfo* SDL_GetVideoInfo()
{
	static SDL_VideoInfo video_info;

#ifdef DDRAW

	WIN_SAVE_LAST_ERROR;	// DirectDraw

	ONCE({
		IDirectDraw* dd = 0;
		HRESULT hr = DirectDrawCreate(0, &dd, 0);
		if(SUCCEEDED(hr) && dd != 0)
		{
			DDCAPS caps;
			memset(&caps, 0, sizeof(caps));
			caps.dwSize = sizeof(caps);
			hr = dd->GetCaps(&caps, 0);
			if(SUCCEEDED(hr))
				video_info.video_mem = caps.dwVidMemTotal;
			dd->Release();
		}
	});

	WIN_RESTORE_LAST_ERROR;

#endif

	return &video_info;
}


// For very [very] basic memory-usage information.
// Should be replaced by a decent memory profiler.
//
// copied from SDL_GetVideoInfo but cannot be implemented in terms of it:
// SDL_VideoInfo doesn't provide for returning "remaining video memory".
int GetVRAMInfo(int& remaining, int& total)
{
	int ok = 0;
#ifdef DDRAW

	WIN_SAVE_LAST_ERROR;	// DirectDraw

	IDirectDraw* dd = 0;
	HRESULT hr = DirectDrawCreate(0, &dd, 0);
	if(SUCCEEDED(hr) && dd != 0)
	{
		static DDCAPS caps;
		caps.dwSize = sizeof(caps);
		hr = dd->GetCaps(&caps, 0);
		if(SUCCEEDED(hr))
		{
			ok = 1;
			remaining = caps.dwVidMemFree;
			total = caps.dwVidMemTotal;
		}
		dd->Release();
	}

	WIN_RESTORE_LAST_ERROR;

#endif

	return ok;
}


SDL_Surface* SDL_GetVideoSurface()
{
	return 0;
}


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

typedef std::queue<SDL_Event> Queue;
static Queue queue;

static void queue_event(const SDL_Event& ev)
{
	queue.push(ev);
}

static bool dequeue_event(SDL_Event* ev)
{
	if(queue.empty())
		return false;
	*ev = queue.front();
	queue.pop();
	return true;
}



static void queue_quit_event()
{
	SDL_Event ev;
	ev.type = SDL_QUIT;
	queue_event(ev);
}





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

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

static inline void queue_active_event(SdlActivationType type, uint 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  = (type == GAIN)? 1 : 0;
	queue_event(ev);
}


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

static void active_change_state(SdlActivationType type, uint changed_app_state)
{
	uint old_app_state = app_state;

	if(type == GAIN)
		app_state |= changed_app_state;
	else
		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))
		queue_active_event(type, changed_app_state);
}

static void reset_all_keys();

static LRESULT OnActivate(HWND hWnd, UINT state, HWND UNUSED(hWndActDeact), BOOL fMinimized)
{
	SdlActivationType type;
	uint 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);

		gamma_swap(GAMMA_LATCH_NEW_RAMP);
		if(fullscreen)
			video_enter_game_mode();
	}
	// deactivated (Alt+Tab out) or minimized
	else
	{
		type = LOSE;
		changed_app_state = SDL_APPINPUTFOCUS;
		if(fMinimized)
			changed_app_state |= SDL_APPACTIVE;


		reset_all_keys();

		gamma_swap(GAMMA_RESTORE_ORIGINAL);
		if(fullscreen)
			video_leave_game_mode();
	}

	active_change_state(type, changed_app_state);
	return 0;
}


Uint8 SDL_GetAppState()
{
	return app_state;
}

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

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


static Uint8 keys[SDLK_LAST];

// winuser.h promises VK_0 and VK_A etc. match ASCII value.
#define VK_0 '0'
#define VK_A 'A'

static void init_vkmap(SDLKey (&VK_keymap)[256])
{
	int i;

	// Map the VK keysyms
	for ( i=0; i<sizeof(VK_keymap)/sizeof(VK_keymap[0]); ++i )
		VK_keymap[i] = SDLK_UNKNOWN;

	VK_keymap[VK_BACK] = SDLK_BACKSPACE;
	VK_keymap[VK_TAB] = SDLK_TAB;
	VK_keymap[VK_CLEAR] = SDLK_CLEAR;
	VK_keymap[VK_RETURN] = SDLK_RETURN;
	VK_keymap[VK_PAUSE] = SDLK_PAUSE;
	VK_keymap[VK_ESCAPE] = SDLK_ESCAPE;
	VK_keymap[VK_SPACE] = SDLK_SPACE;
	VK_keymap[VK_OEM_7] = SDLK_QUOTE;
	VK_keymap[VK_OEM_COMMA] = SDLK_COMMA;
	VK_keymap[VK_OEM_MINUS] = SDLK_MINUS;
	VK_keymap[VK_OEM_PERIOD] = SDLK_PERIOD;
	VK_keymap[VK_OEM_2] = SDLK_SLASH;
	for(i = 0; i < 10; i++)
		VK_keymap[VK_0+i] = (SDLKey)(SDLK_0+i);
	VK_keymap[VK_OEM_1] = SDLK_SEMICOLON;
	VK_keymap[VK_OEM_PLUS] = SDLK_EQUALS;
	VK_keymap[VK_OEM_4] = SDLK_LEFTBRACKET;
	VK_keymap[VK_OEM_5] = SDLK_BACKSLASH;
	VK_keymap[VK_OEM_6] = SDLK_RIGHTBRACKET;
	VK_keymap[VK_OEM_3] = SDLK_BACKQUOTE;
	VK_keymap[VK_OEM_8] = SDLK_BACKQUOTE;

	for(i = 0; i < 26; i++)
		VK_keymap[VK_A+i] = (SDLKey)(SDLK_a+i);

	VK_keymap[VK_DELETE] = SDLK_DELETE;

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

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

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

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

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

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

inline SDLKey vkmap(int vk)
{
	static SDLKey VK_SDLKMap[256]; // VK_SDLKMap[vk] == SDLK
	ONCE( init_vkmap(VK_SDLKMap); );

	if(!(0 <= vk && vk < 256))
	{
		debug_warn("vkmap: invalid vk");
		return SDLK_UNKNOWN;
	}
	return VK_SDLKMap[vk];
}


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

	for(int i = 0; i < ARRAY_SIZE(keys); i++)
		if(keys[i])
		{
			spoofed_up_event.key.keysym.sym = (SDLKey)i;
			queue_event(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 uint sdlk = vkmap(vk);
	if(sdlk != SDLK_UNKNOWN)
		keys[sdlk] = (Uint8)fDown;

	if(!fDown)
		queue_key_event(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];
		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++)
				queue_key_event(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)
			queue_key_event(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;
}


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

static void queue_mouse_event(uint x, uint y)
{
	SDL_Event ev;
	ev.type = SDL_MOUSEMOTION;
	debug_assert(x <= USHRT_MAX && y <= USHRT_MAX);
	ev.motion.x = x;
	ev.motion.y = y;
	queue_event(ev);
}

static void queue_button_event(uint button, uint state, uint x, uint y)
{
	SDL_Event ev;
	ev.type = (state == SDL_PRESSED)? SDL_MOUSEBUTTONDOWN : SDL_MOUSEBUTTONUP;
	ev.button.button = (u8)button;
	ev.button.state  = (u8)state;
	debug_assert(x <= USHRT_MAX && y <= USHRT_MAX);
	ev.button.x = x;
	ev.button.y = y;
	queue_event(ev);
}


static uint mouse_x, mouse_y;

// generate a mouse move message and update our notion of the mouse position.
// x, y are screen pixel coordinates.
// notes:
// - does not actually move the OS cursor;
// - called from mouse_update and SDL_WarpMouse.
// - coords should never be negative - this would indicate a logic error.
static void mouse_moved(int x, int y)
{
	debug_assert(x >= 0 && y >= 0);
	mouse_x = (uint)x;
	mouse_y = (uint)y;
	queue_mouse_event(mouse_x, mouse_y);
}


static void mouse_update()
{
	// don't use DirectInput, because we want to respect the user's mouse
	// sensitivity settings. Windows messages are laggy, so poll instead.

	// window not created yet or already shut down. no sense reporting
	// mouse position, and bail now to avoid ScreenToClient failing.
	if(hWnd == INVALID_HANDLE_VALUE)
		return;

	POINT pt;
	WARN_IF_FALSE(GetCursorPos(&pt));
	WARN_IF_FALSE(ScreenToClient(hWnd, &pt));

	// nothing to do if it hasn't changed since last time
	if(mouse_x == (uint)pt.x && mouse_y == (uint)pt.y)
		return;

	// moved within window
	RECT client_rect;
	GetClientRect(hWnd, &client_rect);
	if(PtInRect(&client_rect, pt) && WindowFromPoint(pt) == hWnd)
	{
		active_change_state(GAIN, SDL_APPMOUSEFOCUS);
		mouse_moved((int)pt.x, (int)pt.y);
	}
	// moved outside of window
	else
		active_change_state(LOSE, SDL_APPMOUSEFOCUS);
}

static uint 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 x, int y, UINT UNUSED(flags))
{
	uint button;
	uint 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;
	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)
			SetCapture(hWnd);
	}
	else
	{
		// release after all up events received
		if(--outstanding_press_events <= 0)
		{
			ReleaseCapture();
			outstanding_press_events = 0;
		}
	}

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

	queue_button_event(button, state, x, y);
	return 0;
}


static LRESULT OnMouseWheel(HWND UNUSED(hWnd), int xPos, int yPos, int zDelta, UINT UNUSED(fwKeys))
{
	uint button = (zDelta < 0)? SDL_BUTTON_WHEELDOWN : SDL_BUTTON_WHEELUP;
	// SDL says this sends a down message followed by up.
	queue_button_event(button, SDL_PRESSED , xPos, yPos);
	queue_button_event(button, SDL_RELEASED, xPos, yPos);
	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;
}


inline void SDL_WarpMouse(int x, int y)
{
	WARN_IF_FALSE(SetCursorPos(x, y));
	mouse_moved(x, 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)
		{
			ShowCursor(toggle);
			cursor_visible = toggle;
		}
	}
	return cursor_visible;
}




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

static LRESULT CALLBACK wndproc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	if(is_shutdown)
		return DefWindowProc(hWnd, uMsg, wParam, lParam);

	switch(uMsg)
	{
	case WM_PAINT:
		PAINTSTRUCT ps;
		BeginPaint(hWnd, &ps);
		EndPaint(hWnd, &ps);
		return 0;

	case WM_ERASEBKGND:
		// this indicates we allegedly erased the background;
		// PAINTSTRUCT.fErase is then FALSE.
		return 1;

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

	HANDLE_MSG(hWnd, WM_ACTIVATE, OnActivate);

	case WM_DESTROY:
		queue_quit_event();
		break;

	case WM_SYSCOMMAND:
		switch(wParam)
		{
		// prevent moving, sizing, screensaver, and power-off in fullscreen mode
		case SC_MOVE:
		case SC_SIZE:
		case SC_MAXIMIZE:
		case SC_MONITORPOWER:
			if(fullscreen)
				return 1;
			break;

		// Alt+F4 or system menu doubleclick/exit
		case SC_CLOSE:
			queue_quit_event();
			break;
		}
		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);

	// (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:
		return OnMouseButton(hWnd, uMsg, (int)(short)LOWORD(lParam), (int)(short)HIWORD(lParam), (UINT)(wParam));

	default:
		// can't call DefWindowProc here: some messages
		// are only conditionally 'grabbed' (e.g. NCHITTEST)
		break;
	}

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


void SDL_PumpEvents(void)
{
	// 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))
	{
		DispatchMessageW(&msg);
	}
}



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

	if(dequeue_event(ev))
		return 1;

	return 0;
}


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


//----------------------------------------------------------------------------
// keyboard hook (intercepts PrintScreen and system keys, e.g. Alt+Tab)

// note: the LowLevel hooks are global, but a DLL isn't actually required
// as stated in the docs. Windows apparently calls the handler in its original
// context. see http://www.gamedev.net/community/forums/topic.asp?topic_id=255399 .

static HHOOK hKeyboard_LL_Hook = (HHOOK)0;

static LRESULT CALLBACK keyboard_ll_hook(int nCode, WPARAM wParam, LPARAM lParam)
{
	if(nCode == HC_ACTION)
	{
		PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT)lParam;
		DWORD vk = p->vkCode;

		// replace Windows PrintScreen handler
		if(vk == VK_SNAPSHOT)
		{
// disabled - we want the normal Windows printscreen handling to
// remain so as not to confuse artists.
#if 0
			// check whether PrintScreen should be taking screenshots -- if
			// not, allow the standard Windows clipboard to work
			if(app_state & SDL_APPINPUTFOCUS)
			{
				// send to wndproc
				UINT msg = (UINT)wParam;
				PostMessage(hWnd, msg, vk, 0);
					// specify hWnd to be safe.
					// if window not yet created, it's INVALID_HANDLE_VALUE.

				// don't pass it on to other handlers
				return 1;
			}
#endif
		}	// vk == VK_SNAPSHOT
	}

	// pass it on to other hook handlers
	return CallNextHookEx(hKeyboard_LL_Hook, nCode, wParam, lParam);
}


static void enable_kbd_hook(bool enable)
{
	if(enable)
	{
		debug_assert(hKeyboard_LL_Hook == 0);
		hKeyboard_LL_Hook = SetWindowsHookEx(WH_KEYBOARD_LL, keyboard_ll_hook, hInst, 0);
		debug_assert(hKeyboard_LL_Hook != 0);
	}
	else
	{
		debug_assert(hKeyboard_LL_Hook != 0);
		UnhookWindowsHookEx(hKeyboard_LL_Hook);
		hKeyboard_LL_Hook = 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.

union HANDLE_sem
{
	HANDLE h;
	SDL_sem* s;
};

SDL_sem* SDL_CreateSemaphore(int cnt)
{
	HANDLE_sem u;
	u.h = CreateSemaphore(0, cnt, 0x7fffffff, 0);
	return u.s;
}

inline void SDL_DestroySemaphore(SDL_sem* sem)
{
	HANDLE_sem u;
	u.s = sem;
	CloseHandle(u.h);
}

int SDL_SemPost(SDL_sem* sem)
{
	HANDLE_sem u;
	u.s = sem;
	return ReleaseSemaphore(u.h, 1, 0);
}

int SDL_SemWait(SDL_sem* sem)
{
	HANDLE_sem u;
	u.s = sem;
	return WaitForSingleObject(u.h, INFINITE);
}


// threads
// users don't need to allocate SDL_Thread variables, so type = void
// API returns SDL_Thread*, which is the HANDLE value itself.
//
// we go through hoops to avoid type cast warnings;
// a simple union { pthread_t; SDL_Thread* } yields "uninitialized"
// warnings in VC2005, so we coerce values directly.
cassert(sizeof(pthread_t) == sizeof(SDL_Thread*));

SDL_Thread* SDL_CreateThread(int (*func)(void*), void* param)
{
	pthread_t thread = 0;
	if(pthread_create(&thread, 0, (void* (*)(void*))func, param) < 0)
		return 0;
	return *(SDL_Thread**)&thread;
}

int SDL_KillThread(SDL_Thread* thread)
{
	pthread_cancel(*(pthread_t*)&thread);
	return 0;
}


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

void SDL_WM_SetCaption(const char* title, const char* icon)
{
	WARN_IF_FALSE(SetWindowText(hWnd, title));

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


inline u32 SDL_GetTicks()
{
	return GetTickCount();
}


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


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


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

static LibError wsdl_init()
{
	hInst = GetModuleHandle(0);

	// redirect stdout to file (otherwise it's simply ignored on Win32).
	// notes:
	// - use full path for safety (works even if someone does chdir)
	// - SDL does this in its WinMain hook. we need to do this here
	//   (before main is called) instead of in SDL_Init to completely
	//   emulate SDL; bonus: we don't miss any output before SDL_Init.
	char path[MAX_PATH];
	snprintf(path, ARRAY_SIZE(path), "%s\\stdout.txt", win_exe_dir);
	// ignore BoundsChecker warnings here. subsystem is set to "Windows"
	// to avoid the OS opening a console on startup (ugly). that means
	// stdout isn't associated with a lowio handle; _close ends up
	// getting called with fd = -1. oh well, nothing we can do.
	FILE* f = freopen(path, "wt", stdout);
	if(!f)
		debug_warn("stdout freopen failed");

#if CONFIG_PARANOIA
	// 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

	enable_kbd_hook(true);

	return INFO_OK;
}


static LibError wsdl_shutdown()
{
	is_shutdown = true;

	// redirected to stdout.txt in SDL_Init;
	// close to avoid BoundsChecker warning.
	fclose(stdout);

	gamma_swap(GAMMA_RESTORE_ORIGINAL);

	video_shutdown();

	enable_kbd_hook(false);

	return INFO_OK;
}


// these are placebos. since some init needs to happen before main(),
// we take care of it all in the module init/shutdown hooks.

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

void SDL_Quit()
{
}
