/*
===========================================================================
Copyright (C) 1999-2005 Id Software, Inc.
Copyright (C) 2006 Robert Beckebans <trebor_7@users.sourceforge.net>

This file is part of XreaL source code.

XreaL 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.

XreaL 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 XreaL source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
===========================================================================
*/

// win_input.c - win32 mouse and joystick code
// 02/21/97 JCB Added extended DirectInput code to support external controllers.
#include "../client/client.h"
#include "win_local.h"

typedef struct
{
	int             oldButtonState;

	qboolean        mouseActive;
	qboolean        mouseInitialized;
	qboolean        mouseStartupDelayed;	// delay mouse init to try DI again when we have a window
} WinMouseVars_t;

static WinMouseVars_t s_wmv;

static int      window_center_x, window_center_y;

//
// Joystick definitions
//
#define	JOY_MAX_AXES		6	// X, Y, Z, R, U, V

typedef struct
{
	qboolean        avail;
	int             id;			// joystick number
	JOYCAPS         jc;

	int             oldbuttonstate;
	int             oldpovstate;

	JOYINFOEX       ji;
} joystickInfo_t;

static joystickInfo_t joy;

cvar_t         *in_mouse;
cvar_t         *in_joystick;
cvar_t         *in_joyBallScale;
cvar_t         *in_debugJoystick;
cvar_t         *joy_threshold;

qboolean        in_appactive;

// forward-referenced functions
void            IN_StartupJoystick(void);
void            IN_JoyMove(void);

/*
============================================================

WIN32 MOUSE CONTROL

============================================================
*/

/*
================
IN_InitWin32Mouse
================
*/
void IN_InitWin32Mouse(void)
{
}

/*
================
IN_ShutdownWin32Mouse
================
*/
void IN_ShutdownWin32Mouse(void)
{
}

/*
================
IN_ActivateWin32Mouse
================
*/
void IN_ActivateWin32Mouse(void)
{
	int             width, height;
	RECT            window_rect;

	width = GetSystemMetrics(SM_CXSCREEN);
	height = GetSystemMetrics(SM_CYSCREEN);

	GetWindowRect(g_wv.hWnd, &window_rect);
	if(window_rect.left < 0)
		window_rect.left = 0;
	if(window_rect.top < 0)
		window_rect.top = 0;
	if(window_rect.right >= width)
		window_rect.right = width - 1;
	if(window_rect.bottom >= height - 1)
		window_rect.bottom = height - 1;
	window_center_x = (window_rect.right + window_rect.left) / 2;
	window_center_y = (window_rect.top + window_rect.bottom) / 2;

	SetCursorPos(window_center_x, window_center_y);

	SetCapture(g_wv.hWnd);
	ClipCursor(&window_rect);
	while(ShowCursor(FALSE) >= 0)
		;
}

/*
================
IN_DeactivateWin32Mouse
================
*/
void IN_DeactivateWin32Mouse(void)
{
	ClipCursor(NULL);
	ReleaseCapture();
	while(ShowCursor(TRUE) < 0)
		;
}

/*
================
IN_Win32Mouse
================
*/
void IN_Win32Mouse(int *mx, int *my)
{
	POINT           current_pos;

	// find mouse movement
	GetCursorPos(&current_pos);

	// force the mouse to the center, so there's room to move
	SetCursorPos(window_center_x, window_center_y);

	*mx = current_pos.x - window_center_x;
	*my = current_pos.y - window_center_y;
}


/*
============================================================

DIRECT INPUT MOUSE CONTROL

============================================================
*/

#undef DEFINE_GUID

#define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \
        const GUID name \
                = { l, w1, w2, { b1, b2,  b3,  b4,  b5,  b6,  b7,  b8 } }

DEFINE_GUID(GUID_SysMouse, 0x6F1D2B60, 0xD5A0, 0x11CF, 0xBF, 0xC7, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00);
DEFINE_GUID(GUID_XAxis, 0xA36D02E0, 0xC9F3, 0x11CF, 0xBF, 0xC7, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00);
DEFINE_GUID(GUID_YAxis, 0xA36D02E1, 0xC9F3, 0x11CF, 0xBF, 0xC7, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00);
DEFINE_GUID(GUID_ZAxis, 0xA36D02E2, 0xC9F3, 0x11CF, 0xBF, 0xC7, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00);


#define DINPUT_BUFFERSIZE           16
#define iDirectInputCreate(a,b,c,d)	pDirectInputCreate(a,b,c,d)

HRESULT(WINAPI * pDirectInputCreate) (HINSTANCE hinst, DWORD dwVersion, LPDIRECTINPUT * lplpDirectInput, LPUNKNOWN punkOuter);

static HINSTANCE hInstDI;

typedef struct MYDATA
{
	LONG            lX;			// X axis goes here
	LONG            lY;			// Y axis goes here
	LONG            lZ;			// Z axis goes here
	BYTE            bButtonA;	// One button goes here
	BYTE            bButtonB;	// Another button goes here
	BYTE            bButtonC;	// Another button goes here
	BYTE            bButtonD;	// Another button goes here
} MYDATA;

static DIOBJECTDATAFORMAT rgodf[] = {
	{&GUID_XAxis, FIELD_OFFSET(MYDATA, lX), DIDFT_AXIS | DIDFT_ANYINSTANCE, 0,},
	{&GUID_YAxis, FIELD_OFFSET(MYDATA, lY), DIDFT_AXIS | DIDFT_ANYINSTANCE, 0,},
	{&GUID_ZAxis, FIELD_OFFSET(MYDATA, lZ), 0x80000000 | DIDFT_AXIS | DIDFT_ANYINSTANCE, 0,},
	{0, FIELD_OFFSET(MYDATA, bButtonA), DIDFT_BUTTON | DIDFT_ANYINSTANCE, 0,},
	{0, FIELD_OFFSET(MYDATA, bButtonB), DIDFT_BUTTON | DIDFT_ANYINSTANCE, 0,},
	{0, FIELD_OFFSET(MYDATA, bButtonC), 0x80000000 | DIDFT_BUTTON | DIDFT_ANYINSTANCE, 0,},
	{0, FIELD_OFFSET(MYDATA, bButtonD), 0x80000000 | DIDFT_BUTTON | DIDFT_ANYINSTANCE, 0,},
};

#define NUM_OBJECTS (sizeof(rgodf) / sizeof(rgodf[0]))

// NOTE TTimo: would be easier using c_dfDIMouse or c_dfDIMouse2 
static DIDATAFORMAT df = {
	sizeof(DIDATAFORMAT),		// this structure
	sizeof(DIOBJECTDATAFORMAT),	// size of object data format
	DIDF_RELAXIS,				// absolute axis coordinates
	sizeof(MYDATA),				// device data size
	NUM_OBJECTS,				// number of objects
	rgodf,						// and here they are
};

static LPDIRECTINPUT g_pdi;
static LPDIRECTINPUTDEVICE g_pMouse;

void            IN_DIMouse(int *mx, int *my);

/*
========================
IN_InitDIMouse
========================
*/
qboolean IN_InitDIMouse(void)
{
	HRESULT         hr;
	int             x, y;
	DIPROPDWORD     dipdw = {
		{
		 sizeof(DIPROPDWORD),	// diph.dwSize
		 sizeof(DIPROPHEADER),	// diph.dwHeaderSize
		 0,						// diph.dwObj
		 DIPH_DEVICE,			// diph.dwHow
		 }
		,
		DINPUT_BUFFERSIZE,		// dwData
	};

	Com_Printf("Initializing DirectInput...\n");

	if(!hInstDI)
	{
		hInstDI = LoadLibrary("dinput.dll");

		if(hInstDI == NULL)
		{
			Com_Printf("Couldn't load dinput.dll\n");
			return qfalse;
		}
	}

	if(!pDirectInputCreate)
	{
		pDirectInputCreate = (HRESULT(WINAPI *) (HINSTANCE, DWORD, LPDIRECTINPUT *, LPUNKNOWN))
			GetProcAddress(hInstDI, "DirectInputCreateA");

		if(!pDirectInputCreate)
		{
			Com_Printf("Couldn't get DI proc addr\n");
			return qfalse;
		}
	}

	// register with DirectInput and get an IDirectInput to play with.
	hr = iDirectInputCreate(g_wv.hInstance, DIRECTINPUT_VERSION, &g_pdi, NULL);

	if(FAILED(hr))
	{
		Com_Printf("iDirectInputCreate failed\n");
		return qfalse;
	}

	// obtain an interface to the system mouse device.
	hr = IDirectInput_CreateDevice(g_pdi, &GUID_SysMouse, &g_pMouse, NULL);

	if(FAILED(hr))
	{
		Com_Printf("Couldn't open DI mouse device\n");
		return qfalse;
	}

	// set the data format to "mouse format".
	hr = IDirectInputDevice_SetDataFormat(g_pMouse, &df);

	if(FAILED(hr))
	{
		Com_Printf("Couldn't set DI mouse format\n");
		return qfalse;
	}

	// set the cooperativity level.
	hr = IDirectInputDevice_SetCooperativeLevel(g_pMouse, g_wv.hWnd, DISCL_EXCLUSIVE | DISCL_FOREGROUND);

	// https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=50
	if(FAILED(hr))
	{
		Com_Printf("Couldn't set DI coop level\n");
		return qfalse;
	}


	// set the buffer size to DINPUT_BUFFERSIZE elements.
	// the buffer size is a DWORD property associated with the device
	hr = IDirectInputDevice_SetProperty(g_pMouse, DIPROP_BUFFERSIZE, &dipdw.diph);

	if(FAILED(hr))
	{
		Com_Printf("Couldn't set DI buffersize\n");
		return qfalse;
	}

	// clear any pending samples
	IN_DIMouse(&x, &y);
	IN_DIMouse(&x, &y);

	Com_Printf("DirectInput initialized.\n");
	return qtrue;
}

/*
==========================
IN_ShutdownDIMouse
==========================
*/
void IN_ShutdownDIMouse(void)
{
	if(g_pMouse)
	{
		IDirectInputDevice_Release(g_pMouse);
		g_pMouse = NULL;
	}

	if(g_pdi)
	{
		IDirectInput_Release(g_pdi);
		g_pdi = NULL;
	}
}

/*
==========================
IN_ActivateDIMouse
==========================
*/
void IN_ActivateDIMouse(void)
{
	HRESULT         hr;

	if(!g_pMouse)
	{
		return;
	}

	// we may fail to reacquire if the window has been recreated
	hr = IDirectInputDevice_Acquire(g_pMouse);
	if(FAILED(hr))
	{
		if(!IN_InitDIMouse())
		{
			Com_Printf("Falling back to Win32 mouse support...\n");
			Cvar_Set("in_mouse", "-1");
		}
	}
}

/*
==========================
IN_DeactivateDIMouse
==========================
*/
void IN_DeactivateDIMouse(void)
{
	if(!g_pMouse)
	{
		return;
	}
	IDirectInputDevice_Unacquire(g_pMouse);
}


/*
===================
IN_DIMouse
===================
*/
void IN_DIMouse(int *mx, int *my)
{
	DIDEVICEOBJECTDATA od;
	DIMOUSESTATE    state;
	DWORD           dwElements;
	HRESULT         hr;
	int             value;

	if(!g_pMouse)
	{
		return;
	}

	// fetch new events
	for(;;)
	{
		dwElements = 1;

		hr = IDirectInputDevice_GetDeviceData(g_pMouse, sizeof(DIDEVICEOBJECTDATA), &od, &dwElements, 0);
		if((hr == DIERR_INPUTLOST) || (hr == DIERR_NOTACQUIRED))
		{
			IDirectInputDevice_Acquire(g_pMouse);
			return;
		}

		/* Unable to read data or no data available */
		if(FAILED(hr))
		{
			break;
		}

		if(dwElements == 0)
		{
			break;
		}

		switch (od.dwOfs)
		{
			case DIMOFS_BUTTON0:
				if(od.dwData & 0x80)
					Sys_QueEvent(od.dwTimeStamp, SE_KEY, K_MOUSE1, qtrue, 0, NULL);
				else
					Sys_QueEvent(od.dwTimeStamp, SE_KEY, K_MOUSE1, qfalse, 0, NULL);
				break;

			case DIMOFS_BUTTON1:
				if(od.dwData & 0x80)
					Sys_QueEvent(od.dwTimeStamp, SE_KEY, K_MOUSE2, qtrue, 0, NULL);
				else
					Sys_QueEvent(od.dwTimeStamp, SE_KEY, K_MOUSE2, qfalse, 0, NULL);
				break;

			case DIMOFS_BUTTON2:
				if(od.dwData & 0x80)
					Sys_QueEvent(od.dwTimeStamp, SE_KEY, K_MOUSE3, qtrue, 0, NULL);
				else
					Sys_QueEvent(od.dwTimeStamp, SE_KEY, K_MOUSE3, qfalse, 0, NULL);
				break;

			case DIMOFS_BUTTON3:
				if(od.dwData & 0x80)
					Sys_QueEvent(od.dwTimeStamp, SE_KEY, K_MOUSE4, qtrue, 0, NULL);
				else
					Sys_QueEvent(od.dwTimeStamp, SE_KEY, K_MOUSE4, qfalse, 0, NULL);
				break;
				// https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=50
			case DIMOFS_Z:
				value = od.dwData;
				if(value == 0)
				{

				}
				else if(value < 0)
				{
					Sys_QueEvent(od.dwTimeStamp, SE_KEY, K_MWHEELDOWN, qtrue, 0, NULL);
					Sys_QueEvent(od.dwTimeStamp, SE_KEY, K_MWHEELDOWN, qfalse, 0, NULL);
				}
				else
				{
					Sys_QueEvent(od.dwTimeStamp, SE_KEY, K_MWHEELUP, qtrue, 0, NULL);
					Sys_QueEvent(od.dwTimeStamp, SE_KEY, K_MWHEELUP, qfalse, 0, NULL);
				}
				break;
		}
	}

	// read the raw delta counter and ignore
	// the individual sample time / values
	hr = IDirectInputDevice_GetDeviceState(g_pMouse, sizeof(DIDEVICEOBJECTDATA), &state);
	if(FAILED(hr))
	{
		*mx = *my = 0;
		return;
	}
	*mx = state.lX;
	*my = state.lY;
}

/*
============================================================

  MOUSE CONTROL

============================================================
*/

/*
===========
IN_ActivateMouse

Called when the window gains focus or changes in some way
===========
*/
void IN_ActivateMouse(void)
{
	if(!s_wmv.mouseInitialized)
	{
		return;
	}
	if(!in_mouse->integer)
	{
		s_wmv.mouseActive = qfalse;
		return;
	}
	if(s_wmv.mouseActive)
	{
		return;
	}

	s_wmv.mouseActive = qtrue;

	if(in_mouse->integer != -1)
	{
		IN_ActivateDIMouse();
	}
	IN_ActivateWin32Mouse();
}


/*
===========
IN_DeactivateMouse

Called when the window loses focus
===========
*/
void IN_DeactivateMouse(void)
{
	if(!s_wmv.mouseInitialized)
	{
		return;
	}
	if(!s_wmv.mouseActive)
	{
		return;
	}
	s_wmv.mouseActive = qfalse;

	IN_DeactivateDIMouse();
	IN_DeactivateWin32Mouse();
}



/*
===========
IN_StartupMouse
===========
*/
void IN_StartupMouse(void)
{
	s_wmv.mouseInitialized = qfalse;
	s_wmv.mouseStartupDelayed = qfalse;

	if(in_mouse->integer == 0)
	{
		Com_Printf("Mouse control not active.\n");
		return;
	}

	// nt4.0 direct input is screwed up
	if((g_wv.osversion.dwPlatformId == VER_PLATFORM_WIN32_NT) && (g_wv.osversion.dwMajorVersion == 4))
	{
		Com_Printf("Disallowing DirectInput on NT 4.0\n");
		Cvar_Set("in_mouse", "-1");
	}

	if(in_mouse->integer == -1)
	{
		Com_Printf("Skipping check for DirectInput\n");
	}
	else
	{
		if(!g_wv.hWnd)
		{
			Com_Printf("No window for DirectInput mouse init, delaying\n");
			s_wmv.mouseStartupDelayed = qtrue;
			return;
		}
		if(IN_InitDIMouse())
		{
			s_wmv.mouseInitialized = qtrue;
			return;
		}
		Com_Printf("Falling back to Win32 mouse support...\n");
	}
	s_wmv.mouseInitialized = qtrue;
	IN_InitWin32Mouse();
}

/*
===========
IN_MouseEvent
===========
*/
void IN_MouseEvent(int mstate)
{
	int             i;

	if(!s_wmv.mouseInitialized)
		return;

// perform button actions
	for(i = 0; i < 3; i++)
	{
		if((mstate & (1 << i)) && !(s_wmv.oldButtonState & (1 << i)))
		{
			Sys_QueEvent(g_wv.sysMsgTime, SE_KEY, K_MOUSE1 + i, qtrue, 0, NULL);
		}

		if(!(mstate & (1 << i)) && (s_wmv.oldButtonState & (1 << i)))
		{
			Sys_QueEvent(g_wv.sysMsgTime, SE_KEY, K_MOUSE1 + i, qfalse, 0, NULL);
		}
	}

	s_wmv.oldButtonState = mstate;
}


/*
===========
IN_MouseMove
===========
*/
void IN_MouseMove(void)
{
	int             mx, my;

	if(g_pMouse)
	{
		IN_DIMouse(&mx, &my);
	}
	else
	{
		IN_Win32Mouse(&mx, &my);
	}

	if(!mx && !my)
	{
		return;
	}

	Sys_QueEvent(0, SE_MOUSE, mx, my, 0, NULL);
}


/*
=========================================================================

=========================================================================
*/

/*
===========
IN_Startup
===========
*/
void IN_Startup(void)
{
	Com_Printf("\n------- Input Initialization -------\n");

	IN_StartupMouse();

	IN_StartupJoystick();

	Com_Printf("------------------------------------\n");

	in_mouse->modified = qfalse;
	in_joystick->modified = qfalse;
}

/*
===========
IN_Shutdown
===========
*/
void IN_Shutdown(void)
{
	IN_DeactivateMouse();

	IN_ShutdownDIMouse();

	Cmd_RemoveCommand("midiinfo");
}

/*
===========
IN_Init
===========
*/
void IN_Init(void)
{
	// mouse variables
	in_mouse = Cvar_Get("in_mouse", "1", CVAR_ARCHIVE | CVAR_LATCH);

	// joystick variables
	in_joystick = Cvar_Get("in_joystick", "0", CVAR_ARCHIVE | CVAR_LATCH);
	in_joyBallScale = Cvar_Get("in_joyBallScale", "0.02", CVAR_ARCHIVE);
	in_debugJoystick = Cvar_Get("in_debugjoystick", "0", CVAR_TEMP);

	joy_threshold = Cvar_Get("joy_threshold", "0.15", CVAR_ARCHIVE);

	IN_Startup();
}

/*
===========
IN_Activate

Called when the main window gains or loses focus.
The window may have been destroyed and recreated
between a deactivate and an activate.
===========
*/
void IN_Activate(qboolean active)
{
	in_appactive = active;

	if(!active)
	{
		IN_DeactivateMouse();
	}
}

/*
==================
IN_Frame

Called every frame, even if not generating commands
==================
*/
void IN_Frame(void)
{
	// post joystick events
	IN_JoyMove();

	if(!s_wmv.mouseInitialized)
	{
		if(s_wmv.mouseStartupDelayed && g_wv.hWnd)
		{
			Com_Printf("Proceeding with delayed mouse init\n");
			IN_StartupMouse();
			s_wmv.mouseStartupDelayed = qfalse;
		}
		return;
	}

	if(cls.keyCatchers & KEYCATCH_CONSOLE)
	{
		// temporarily deactivate if not in the game and running on the desktop
		if(Cvar_VariableValue("r_fullscreen") == 0)
		{
			IN_DeactivateMouse();
			return;
		}
	}

	if(!in_appactive)
	{
		IN_DeactivateMouse();
		return;
	}

	IN_ActivateMouse();

	// post events to the system que
	IN_MouseMove();
}

/*
===================
IN_ClearStates
===================
*/
void IN_ClearStates(void)
{
	s_wmv.oldButtonState = 0;
}

/*
=========================================================================

JOYSTICK

=========================================================================
*/

/* 
=============== 
IN_StartupJoystick 
=============== 
*/
void IN_StartupJoystick(void)
{
	int             numdevs;
	MMRESULT        mmr;

	// assume no joystick
	joy.avail = qfalse;

	if(!in_joystick->integer)
	{
		Com_Printf("Joystick is not active.\n");
		return;
	}

	// verify joystick driver is present
	if((numdevs = joyGetNumDevs()) == 0)
	{
		Com_Printf("joystick not found -- driver not present\n");
		return;
	}

	// cycle through the joystick ids for the first valid one
	mmr = 0;
	for(joy.id = 0; joy.id < numdevs; joy.id++)
	{
		Com_Memset(&joy.ji, 0, sizeof(joy.ji));
		joy.ji.dwSize = sizeof(joy.ji);
		joy.ji.dwFlags = JOY_RETURNCENTERED;

		if((mmr = joyGetPosEx(joy.id, &joy.ji)) == JOYERR_NOERROR)
			break;
	}

	// abort startup if we didn't find a valid joystick
	if(mmr != JOYERR_NOERROR)
	{
		Com_Printf("joystick not found -- no valid joysticks (%x)\n", mmr);
		return;
	}

	// get the capabilities of the selected joystick
	// abort startup if command fails
	Com_Memset(&joy.jc, 0, sizeof(joy.jc));
	if((mmr = joyGetDevCaps(joy.id, &joy.jc, sizeof(joy.jc))) != JOYERR_NOERROR)
	{
		Com_Printf("joystick not found -- invalid joystick capabilities (%x)\n", mmr);
		return;
	}

	Com_Printf("Joystick found.\n");
	Com_Printf("Pname: %s\n", joy.jc.szPname);
	Com_Printf("OemVxD: %s\n", joy.jc.szOEMVxD);
	Com_Printf("RegKey: %s\n", joy.jc.szRegKey);

	Com_Printf("Numbuttons: %i / %i\n", joy.jc.wNumButtons, joy.jc.wMaxButtons);
	Com_Printf("Axis: %i / %i\n", joy.jc.wNumAxes, joy.jc.wMaxAxes);
	Com_Printf("Caps: 0x%x\n", joy.jc.wCaps);
	if(joy.jc.wCaps & JOYCAPS_HASPOV)
	{
		Com_Printf("HASPOV\n");
	}
	else
	{
		Com_Printf("no POV\n");
	}

	// old button and POV states default to no buttons pressed
	joy.oldbuttonstate = 0;
	joy.oldpovstate = 0;

	// mark the joystick as available
	joy.avail = qtrue;
}

/*
===========
JoyToF
===========
*/
float JoyToF(int value)
{
	float           fValue;

	// move centerpoint to zero
	value -= 32768;

	// convert range from -32768..32767 to -1..1 
	fValue = (float)value / 32768.0;

	if(fValue < -1)
	{
		fValue = -1;
	}
	if(fValue > 1)
	{
		fValue = 1;
	}
	return fValue;
}

int JoyToI(int value)
{
	// move centerpoint to zero
	value -= 32768;

	return value;
}

int             joyDirectionKeys[16] = {
	K_LEFTARROW, K_RIGHTARROW,
	K_UPARROW, K_DOWNARROW,
	K_JOY16, K_JOY17,
	K_JOY18, K_JOY19,
	K_JOY20, K_JOY21,
	K_JOY22, K_JOY23,

	K_JOY24, K_JOY25,
	K_JOY26, K_JOY27
};

/*
===========
IN_JoyMove
===========
*/
void IN_JoyMove(void)
{
	float           fAxisValue;
	int             i;
	DWORD           buttonstate, povstate;
	int             x, y;

	// verify joystick is available and that the user wants to use it
	if(!joy.avail)
	{
		return;
	}

	// collect the joystick data, if possible
	Com_Memset(&joy.ji, 0, sizeof(joy.ji));
	joy.ji.dwSize = sizeof(joy.ji);
	joy.ji.dwFlags = JOY_RETURNALL;

	if(joyGetPosEx(joy.id, &joy.ji) != JOYERR_NOERROR)
	{
		// read error occurred
		// turning off the joystick seems too harsh for 1 read error,
		// but what should be done?
		// Com_Printf ("IN_ReadJoystick: no response\n");
		// joy.avail = false;
		return;
	}

	if(in_debugJoystick->integer)
	{
		Com_Printf("%8x %5i %5.2f %5.2f %5.2f %5.2f %6i %6i\n",
				   joy.ji.dwButtons,
				   joy.ji.dwPOV,
				   JoyToF(joy.ji.dwXpos), JoyToF(joy.ji.dwYpos),
				   JoyToF(joy.ji.dwZpos), JoyToF(joy.ji.dwRpos), JoyToI(joy.ji.dwUpos), JoyToI(joy.ji.dwVpos));
	}

	// loop through the joystick buttons
	// key a joystick event or auxillary event for higher number buttons for each state change
	buttonstate = joy.ji.dwButtons;
	for(i = 0; i < joy.jc.wNumButtons; i++)
	{
		if((buttonstate & (1 << i)) && !(joy.oldbuttonstate & (1 << i)))
		{
			Sys_QueEvent(g_wv.sysMsgTime, SE_KEY, K_JOY1 + i, qtrue, 0, NULL);
		}
		if(!(buttonstate & (1 << i)) && (joy.oldbuttonstate & (1 << i)))
		{
			Sys_QueEvent(g_wv.sysMsgTime, SE_KEY, K_JOY1 + i, qfalse, 0, NULL);
		}
	}
	joy.oldbuttonstate = buttonstate;

	povstate = 0;

	// convert main joystick motion into 6 direction button bits
	for(i = 0; i < joy.jc.wNumAxes && i < 4; i++)
	{
		// get the floating point zero-centered, potentially-inverted data for the current axis
		fAxisValue = JoyToF((&joy.ji.dwXpos)[i]);

		if(fAxisValue < -joy_threshold->value)
		{
			povstate |= (1 << (i * 2));
		}
		else if(fAxisValue > joy_threshold->value)
		{
			povstate |= (1 << (i * 2 + 1));
		}
	}

	// convert POV information from a direction into 4 button bits
	if(joy.jc.wCaps & JOYCAPS_HASPOV)
	{
		if(joy.ji.dwPOV != JOY_POVCENTERED)
		{
			if(joy.ji.dwPOV == JOY_POVFORWARD)
				povstate |= 1 << 12;
			if(joy.ji.dwPOV == JOY_POVBACKWARD)
				povstate |= 1 << 13;
			if(joy.ji.dwPOV == JOY_POVRIGHT)
				povstate |= 1 << 14;
			if(joy.ji.dwPOV == JOY_POVLEFT)
				povstate |= 1 << 15;
		}
	}

	// determine which bits have changed and key an auxillary event for each change
	for(i = 0; i < 16; i++)
	{
		if((povstate & (1 << i)) && !(joy.oldpovstate & (1 << i)))
		{
			Sys_QueEvent(g_wv.sysMsgTime, SE_KEY, joyDirectionKeys[i], qtrue, 0, NULL);
		}

		if(!(povstate & (1 << i)) && (joy.oldpovstate & (1 << i)))
		{
			Sys_QueEvent(g_wv.sysMsgTime, SE_KEY, joyDirectionKeys[i], qfalse, 0, NULL);
		}
	}
	joy.oldpovstate = povstate;

	// if there is a trackball like interface, simulate mouse moves
	if(joy.jc.wNumAxes >= 6)
	{
		x = JoyToI(joy.ji.dwUpos) * in_joyBallScale->value;
		y = JoyToI(joy.ji.dwVpos) * in_joyBallScale->value;
		if(x || y)
		{
			Sys_QueEvent(g_wv.sysMsgTime, SE_MOUSE, x, y, 0, NULL);
		}
	}
}
