#include <stdlib.h>
#include <stdarg.h>

#include "../Daodan.h"
#include "Input.h"
#include "../Oni/Oni.h"
#include "../Daodan_Config.h"
#include "../Daodan_Patch.h"
#include "Utility.h"

typedef struct {
	LItActionDescription descr;
	DDtActionEventType eventType;

	DDtCustomActionCallback callback;
	intptr_t ctx;
} DDtCustomAction;

static DDtCustomAction DDgCustomActions[100] = { 0 };

// Extra keys (make sure these don't collide with Oni's LIc_* keys)
enum {
	DDcKey_MouseButton5 = LIcKey_Max,
	DDcKey_ScrollUp,
	DDcKey_ScrollDown,
	DDcKey_ScrollLeft,
	DDcKey_ScrollRight,
};

// Enhanced version of LIgInputNames from Oni with some extra keys
static const LItInputName DDgInputNames[] = {
	// The following key names are mapped in Oni
	{"fkey1", LIcKey_FKey1}, {"fkey2", LIcKey_FKey2}, {"fkey3", LIcKey_FKey3},
	{"fkey4", LIcKey_FKey4}, {"fkey5", LIcKey_FKey5}, {"fkey6", LIcKey_FKey6},
	{"fkey7", LIcKey_FKey7}, {"fkey8", LIcKey_FKey8}, {"fkey9", LIcKey_FKey9},
	{"fkey10", LIcKey_FKey10}, {"fkey11", LIcKey_FKey11},
	{"fkey12", LIcKey_FKey12}, {"fkey13", LIcKey_FKey13},
	{"fkey14", LIcKey_FKey14}, {"fkey15", LIcKey_FKey15},
	{"backspace", LIcKey_Backspace}, {"tab", LIcKey_Tab},
	{"capslock", LIcKey_CapsLock}, {"enter", LIcKey_Enter},
	{"leftshift", LIcKey_LeftShift}, {"rightshift", LIcKey_RightShift},
	{"leftcontrol", LIcKey_LeftControl},
	{"leftwindows", 0x94}, {"leftoption", 0x94}, // Does nothing in Oni
	{"leftalt", LIcKey_LeftAlt}, {"space", ' '}, {"rightalt", LIcKey_RightAlt},
	{"rightoption", 0x97}, {"rightwindows", 0x97}, // Does nothing in Oni
	{"rightcontrol", LIcKey_RightControl}, {"printscreen", LIcKey_PrintScreen},
	{"scrolllock", LIcKey_ScrollLock}, {"pause", LIcKey_Pause},
	{"insert", LIcKey_Insert}, {"home", LIcKey_Home}, {"pageup", LIcKey_PageUp},
	{"delete", LIcKey_Delete}, {"end", LIcKey_End},
	{"pagedown", LIcKey_PageDown}, {"uparrow", LIcKey_UpArrow},
	{"leftarrow", LIcKey_LeftArrow}, {"downarrow", LIcKey_DownArrow},
	{"rightarrow", LIcKey_RightArrow}, {"numlock", LIcKey_NumLock},
	{"divide", LIcKey_Divide}, {"multiply", LIcKey_Multiply},
	{"subtract", LIcKey_Subtract}, {"add", LIcKey_Add},
	{"numpadequals", LIcKey_NumpadEquals}, {"numpadenter", LIcKey_NumpadEnter},
	{"decimal", LIcKey_Decimal}, {"numpad0", LIcKey_Numpad0},
	{"numpad1", LIcKey_Numpad1}, {"numpad2", LIcKey_Numpad2},
	{"numpad3", LIcKey_Numpad3}, {"numpad4", LIcKey_Numpad4},
	{"numpad5", LIcKey_Numpad5}, {"numpad6", LIcKey_Numpad6},
	{"numpad7", LIcKey_Numpad7}, {"numpad8", LIcKey_Numpad8},
	{"numpad9", LIcKey_Numpad9}, {"backslash", '\\'}, {"semicolon", ';'},
	{"period", '.'}, {"apostrophe", '\''}, {"slash", '/'}, {"leftbracket", '['},
	{"rightbracket", ']'}, {"comma", ','},
	{"mousebutton1", LIcKey_MouseButton1},
	{"mousebutton2", LIcKey_MouseButton2},
	{"mousebutton3", LIcKey_MouseButton3},
	{"mousebutton4", LIcKey_MouseButton4},
	{"mousexaxis", LIcKey_MouseXAxis}, {"mouseyaxis", LIcKey_MouseYAxis},
	{"mousezaxis", LIcKey_MouseZAxis},

	// Extra keys for Daodan Input
	{"mousebutton5", DDcKey_MouseButton5},
	{"scrollup", DDcKey_ScrollUp},
	{"scrolldown", DDcKey_ScrollDown},
	{"scrollleft", DDcKey_ScrollLeft},
	{"scrollright", DDcKey_ScrollRight},

	{"", 0}
};

// Enhanced version of LIgPlatform_ScanCodeToChar from Oni
static const uint8_t DDgPlatform_ScanCodeToChar[256] = {
	// The following scan codes are mapped in Oni
	[0x01] = LIcKey_Escape, [0x02] = '1', [0x03] = '2', [0x04] = '3',
	[0x05] = '4', [0x06] = '5', [0x07] = '6', [0x08] = '7', [0x09] = '8',
	[0x0a] = '9', [0x0b] = '0', [0x0c] = '-', [0x0d] = '=',
	[0x0e] = LIcKey_Backspace, [0x0f] = LIcKey_Tab, [0x10] = 'q', [0x11] = 'w',
	[0x12] = 'e', [0x13] = 'r', [0x14] = 't', [0x15] = 'y', [0x16] = 'u',
	[0x17] = 'i', [0x18] = 'o', [0x19] = 'p', [0x1a] = '[', [0x1b] = ']',
	[0x1c] = LIcKey_Enter, [0x1d] = LIcKey_LeftControl, [0x1e] = 'a',
	[0x1f] = 's', [0x20] = 'd', [0x21] = 'f', [0x22] = 'g', [0x23] = 'h',
	[0x24] = 'j', [0x25] = 'k', [0x26] = 'l', [0x27] = ';', [0x28] = '\'',
	[0x29] = '`', [0x2a] = LIcKey_LeftShift, [0x2b] = '\\', [0x2c] = 'z',
	[0x2d] = 'x', [0x2e] = 'c', [0x2f] = 'v', [0x30] = 'b', [0x31] = 'n',
	[0x32] = 'm', [0x33] = ',', [0x34] = '.', [0x35] = '/',
	[0x36] = LIcKey_RightShift, [0x37] = LIcKey_Multiply,
	[0x38] = LIcKey_LeftAlt, [0x39] = ' ', [0x3a] = LIcKey_CapsLock,
	[0x3b] = LIcKey_FKey1, [0x3c] = LIcKey_FKey2, [0x3d] = LIcKey_FKey3,
	[0x3e] = LIcKey_FKey4, [0x3f] = LIcKey_FKey5, [0x40] = LIcKey_FKey6,
	[0x41] = LIcKey_FKey7, [0x42] = LIcKey_FKey8, [0x43] = LIcKey_FKey9,
	[0x44] = LIcKey_FKey10, [0x45] = LIcKey_NumLock, [0x46] = LIcKey_ScrollLock,
	[0x47] = LIcKey_Numpad7, [0x48] = LIcKey_Numpad8, [0x49] = LIcKey_Numpad9,
	[0x4a] = LIcKey_Subtract, [0x4b] = LIcKey_Numpad4, [0x4c] = LIcKey_Numpad5,
	[0x4d] = LIcKey_Numpad6, [0x4e] = LIcKey_Add, [0x4f] = LIcKey_Numpad1,
	[0x50] = LIcKey_Numpad2, [0x51] = LIcKey_Numpad3, [0x52] = LIcKey_Numpad0,
	[0x53] = LIcKey_Decimal, [0x57] = LIcKey_FKey11, [0x58] = LIcKey_FKey12,
	[0x64] = LIcKey_FKey13, [0x65] = LIcKey_FKey14, [0x66] = LIcKey_FKey15,
	[0x8d] = LIcKey_NumpadEquals, [0x9c] = LIcKey_NumpadEnter,
	[0x9d] = LIcKey_RightControl, [0xb3] = LIcKey_NumpadComma,
	[0xb5] = LIcKey_Divide, [0xb8] = LIcKey_RightAlt, [0xc7] = LIcKey_Home,
	[0xc8] = LIcKey_UpArrow, [0xc9] = LIcKey_PageUp, [0xcb] = LIcKey_LeftArrow,
	[0xcd] = LIcKey_RightArrow, [0xcf] = LIcKey_End, [0xd0] = LIcKey_DownArrow,
	[0xd2] = LIcKey_Insert, [0xd3] = LIcKey_Delete, [0xdb] = LIcKey_LeftWindows,
	[0xdc] = LIcKey_RightWindows, [0xdd] = LIcKey_Apps,

	// Missing in Oni
	[0xd1] = LIcKey_PageDown,
};

// Set in Patches.c if the Daodan input patches are applied. This just enables
// the windows message handling for now
bool DDgUseDaodanInput = false;

// The Oni key codes that correspond to the togglable keys
static uint8_t DDgCapsOniKey = 0;
static uint8_t DDgScrollOniKey = 0;
static uint8_t DDgNumLockOniKey = 0;

// Multiplier for mouse values
static float DDgMouseSensitivity = 1.0;

// Accumulators for mouse scrolling. These are needed because some mice have
// continuous scroll wheels (not to mention touchpads.) We should only add an
// action to Oni's input if one of these accumulators exceeds +/-WHEEL_DELTA.
static int DDgWheelDelta_V = 0;
static int DDgWheelDelta_H = 0;

// UUrMachineTime_High for the last update of the accumulators. Used so they can
// be reset after a period of no scroll events.
static int64_t DDgWheelDelta_Updated = 0;

// Temporary action buffer that we build over the duration of a frame with the
// input we're going to send to the engine. This includes the accumulated
// movement of the mouse cursor and all actions (keyboard and mouse buttons)
// that were pressed this frame (but not held down from previous frames - that
// gets added later from DDgInputState.)
static LItActionBuffer DDgActionBuffer = { 0 };

// Temporary buffer containing the current state of the keyboard and mouse
// buttons, that is, if they're being held now
static char DDgInputState[256] = { 0 };

static short ONICALL DDrBinding_Add(int key, const char *name)
{
	// First try to replace an existing binding for the same key
	LItBinding *binding = NULL;
	for (int i = 0; i < 100; i++) {
		if (LIgBindingArray[i].key == key) {
			binding = &LIgBindingArray[i];
			break;
		}
	}

	// If there are no existing bindings for this key, find a free entry
	if (!binding) {
		for (int i = 0; i < 100; i++) {
			if (!LIgBindingArray[i].key) {
				binding = &LIgBindingArray[i];
				break;
			}
		}
	}
	// No free entries, so give up
	if (!binding)
		return 2;

	// Now try to find the action to bind to. First check Oni's built-in list
	// of actions.
	LItActionDescription *descr = NULL;
	for (int i = 0; LIgActionDescriptions[i].name[0]; i++) {
		if (!UUrString_Compare_NoCase(name, LIgActionDescriptions[i].name)) {
			descr = &LIgActionDescriptions[i];
			break;
		}
	}

	// Next, try Daodan's list of custom actions
	if (!descr) {
		for (int i = 0; i < ARRAY_SIZE(DDgCustomActions); i++) {
			if (!DDgCustomActions[i].descr.name[0])
				break;

			if (!UUrString_Compare_NoCase(name, DDgCustomActions[i].descr.name)) {
				descr = &DDgCustomActions[i].descr;
				break;
			}
		}
	}
	if (!descr)
		return 0;

	binding->key = key;
	binding->descr = descr;
	return 0;
}

static void ONICALL DDrGameState_HandleUtilityInput(GameInput *input)
{
	// Mac Oni 1.2.1 checks the cheat binds here, so we should too. Note that
	// unlike Mac Oni, which hardcodes each cheat here, we use our flexible
	// custom action system.
	for (int i = 0; i < ARRAY_SIZE(DDgCustomActions); i++) {
		if (!DDgCustomActions[i].descr.name[0])
			break;

		uint64_t action = 1ull << DDgCustomActions[i].descr.index;
		bool active = false;

		switch (DDgCustomActions[i].eventType) {
		case DDcEventType_KeyPress:
			if (input->ActionsPressed & action)
				active = true;
			break;
		case DDcEventType_KeyDown:
			if (input->ActionsDown & action)
				active = true;
			break;
		}

		if (active)
			DDgCustomActions[i].callback(DDgCustomActions[i].ctx);
	}

	// Now do everything Oni does in this function
	ONrGameState_HandleUtilityInput(input);

	// This is for show_triggervolumes. Mac Oni does this at the end of
	// HandleUtilityInput too.
	if (ONrDebugKey_WentDown(7))
		OBJgTriggerVolume_Visible = !OBJgTriggerVolume_Visible;
}

static int GetLowestFreeDigitalAction(void)
{
	// Get the digital action indexes that Oni is using right now, plus any in
	// use by our custom actions
	uint64_t used = 0;
	for (int i = 0; LIgActionDescriptions[i].name[0]; i++) {
		if (LIgActionDescriptions[i].type != LIcActionType_Digital)
			continue;
		used |= 1ull << LIgActionDescriptions[i].index;
	}
	for (int i = 0; i < ARRAY_SIZE(DDgCustomActions); i++) {
		if (!DDgCustomActions[i].descr.name[0])
			break;

		if (DDgCustomActions[i].descr.type != LIcActionType_Digital)
			continue;
		used |= 1ull << DDgCustomActions[i].descr.index;
	}

	// Get the lowest unused action index and use it. This isn't totally safe
	// since Oni _might_ have "orphaned" actions that are checked in the code
	// but not bindable, but Mac Oni 1.2.1 seems to have allocated its new
	// bindings this way, including filling the gaps between eg. f12 and
	// lookmode, so we're probably fine to do the same thing.
	unsigned long lowest;
	if (BitScanForward(&lowest, ~(unsigned long)used))
		return lowest;
	if (BitScanForward(&lowest, ~(unsigned long)(used >> 32)))
		return lowest + 32;
	return -1;
}

void DDrInput_RegisterCustomAction(const char *name, DDtActionEventType type,
                                   DDtCustomActionCallback callback,
                                   intptr_t ctx)
{
	int index = GetLowestFreeDigitalAction();
	if (index < 0) {
		STARTUPMESSAGE("Registering action %s failed, maximum actions reached",
		               name);
		return;
	}

	DDtCustomAction *action;
	for (int i = 0; i < ARRAY_SIZE(DDgCustomActions); i++) {
		if (!DDgCustomActions[i].descr.name[0]) {
			action = &DDgCustomActions[i];
			break;
		}
	}
	if (!action) {
		STARTUPMESSAGE("Registering action %s failed, maximum actions reached",
		               name);
		return;
	}

	*action = (DDtCustomAction) {
		.descr = {
			.type = 1,
			.index = index,
		},
		.callback = callback,
		.ctx = ctx,
	};
	UUrString_Copy(action->descr.name, name, sizeof(action->descr.name));
}

static uint8_t VKeyToChar(UINT vkey)
{
	int sc = MapVirtualKeyA(vkey, MAPVK_VK_TO_VSC_EX);
	if ((sc & 0xff00) == 0xe000)
		sc |= 0x80;
	sc &= 0xff;
	return DDgPlatform_ScanCodeToChar[sc];
}

static int ONICALL DDrTranslate_InputName(char *name)
{
	// Mutate the source argument to convert to lowercase. It's ugly but Oni
	// does this too. Unlike Oni, we don't use tolower, since passing
	// potentially out-of-range values to tolower is undefined.
	for (char *c = name; *c; c++) {
		if (*c >= 'A' && *c <= 'Z')
			*c = *c - 0x20;
	}

	// Single character names just use that character as the key code. Unlike
	// Oni, we restrict this to printable ASCII.
	if (strlen(name) == 1 && name[0] >= ' ' && name[0] <= '~')
		return name[0];

	// Otherwise, look up the name in DDgInputNames
	for (int i = 0; DDgInputNames[i].name[0]; i++) {
		if (!strcmp(name, DDgInputNames[i].name))
			return DDgInputNames[i].key;
	}
	return 0;
}

static void CenterCursor(void)
{
	// This can be set to false by script. Not sure why you'd turn it off, but
	// let's respect it.
	if (!LIgCenterCursor)
		return;

	RECT rc;
	if (!GetClientRect(LIgPlatform_HWND, &rc))
		return;
	POINT mid = { rc.right / 2, rc.bottom / 2 };
	if (!ClientToScreen(LIgPlatform_HWND, &mid))
		return;
	SetCursorPos(mid.x, mid.y);
}

static void ONICALL DDrPlatform_Mode_Set(int active)
{
	// Oni's input system uses LIgPlatform_HWND instead of
	// ONgPlatformData.Window, but they should both have the same value
	DDmAssert(LIgPlatform_HWND);

	// Clear the input state when switching input modes
	for (int i = 0; i < ARRAY_SIZE(DDgInputState); i++)
		DDgInputState[i] = 0;
	DDgActionBuffer = (LItActionBuffer) { 0 };

	// Center the cursor before switching modes. Raw Input doesn't need the
	// cursor to be centered, but when switching modes, centering the cursor
	// means it will be in a predictable position for using the pause or F1
	// menu, which are centered on the screen. Also, the cursor must be inside
	// the clip region when we call ClipCursor, otherwise it doesn't work.
	CenterCursor();

	// If leaving input mode (switching from gameplay to menus,) unregister the
	// input device. Otherwise, register it.
	RegisterRawInputDevices(&(RAWINPUTDEVICE) {
		.usUsagePage = 0x01, // HID_USAGE_PAGE_GENERIC
		.usUsage = 0x02, // HID_USAGE_GENERIC_MOUSE
		.hwndTarget = LIgPlatform_HWND,
		.dwFlags = active ? 0 : RIDEV_REMOVE,
	}, 1, sizeof(RAWINPUTDEVICE));

	if (active) {
		DDgMouseSensitivity =
			DDrConfig_GetOptOfType("windows.mousesensitivity", C_FLOAT)->value.floatVal;

		// Get the Oni key codes corresponding to the togglable keys
		DDgCapsOniKey = VKeyToChar(VK_CAPITAL);
		DDgScrollOniKey = VKeyToChar(VK_SCROLL);
		DDgNumLockOniKey = VKeyToChar(VK_NUMLOCK);

		// Clip the cursor to the window bounds when entering input mode to
		// prevent other programs being clicked in windowed mode
		RECT rc;
		if (GetClientRect(LIgPlatform_HWND, &rc)) {
			if (MapWindowRect(LIgPlatform_HWND, NULL, &rc))
				ClipCursor(&rc);
		}
	} else {
		ClipCursor(NULL);
	}
}

static void ONICALL DDrPlatform_InputEvent_GetMouse(int active,
                                                    LItInputEvent *event)
{
	POINT pt;
	if (!GetCursorPos(&pt))
		goto error;

	// Unlike Oni's version of this function, we support windowed mode by
	// mapping the cursor coordinates to the window's client area
	if (!ScreenToClient(LIgPlatform_HWND, &pt))
		goto error;

	*event = (LItInputEvent) { .mouse_pos = { pt.x, pt.y } };
	return;

error:
	*event = (LItInputEvent) { 0 };
	return;
}

static UUtBool ONICALL DDrPlatform_TestKey(int ch, int active)
{
	// DDrPlatform_TestKey is always called with active = LIgMode_Internal

	if (active) {
		// The input system is running, which means DDgInputState will be up to
		// date, so just use that
		return DDgInputState[ch];
	} else {
		// Use Oni's map from key codes to DirectInput scan codes to get the
		// scan code we want to test for
		int sc = 0;
		for (int i = 0; i < 256; i++) {
			if (DDgPlatform_ScanCodeToChar[i] == ch) {
				sc = i;
				break;
			}
		}
		if (!sc)
			return UUcFalse;

		// DirectInput scan codes have 0x80 set for extended keys. Replace this
		// with an 0xe0 prefix for MapVirtualKey.
		if (sc & 0x80) {
			sc &= 0x7f;
			sc |= 0xe000;
		}
		int vkey = MapVirtualKeyA(sc, MAPVK_VSC_TO_VK_EX);

		// Now check if the key is down. We must use GetAsyncKeyState here
		// because DDrPlatform_TestKey can be called from anywhere, even before
		// we have a message loop or game loop. For example, it's called from
		// ONiMain to test the state of the shift key on startup.
		return (GetAsyncKeyState(vkey) & 0x8000) ? UUcTrue : UUcFalse;
	}
}

// Update DDgInputState and DDgActionBuffer with a new key state
static void SetKeyState(int key, bool down)
{
	// Keep track of held keys. Held keys are added to every buffer and they're
	// also checked in DDrPlatform_TestKey.
	DDgInputState[key] = down;

	if (down) {
		// Add the key to the next buffer. This is so key presses are never
		// dropped, even if the key is released before Oni checks the buffer.
		LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
			.input = key,
			.analog = 1.0,
		});
	}
}

static void ProcessRawInputPacket(RAWINPUT *ri)
{
	if (ri->header.dwType != RIM_TYPEMOUSE)
		return;

	// We don't handle MOUSE_MOVE_ABSOLUTE at all yet
	if (!(ri->data.mouse.usFlags & MOUSE_MOVE_ABSOLUTE)) {
		LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
			.input = LIcKey_MouseXAxis,
			.analog = (float)ri->data.mouse.lLastX * 0.25 * DDgMouseSensitivity,
		});
		LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
			.input = LIcKey_MouseYAxis,
			.analog = (float)ri->data.mouse.lLastY *
			          (LIgMode_InvertMouse ? -0.25 : 0.25) * DDgMouseSensitivity,
		});
	}

	// Oni supports using the mouse wheel to look up and down or left and right
	// by binding mousezaxis to aim_lr or aim_ud. We don't support this
	// incredibly useful feature, but if you need it, let me know. Instead, we
	// allow scrolling to be bound to digital actions.
	if (ri->data.mouse.usButtonFlags & (RI_MOUSE_WHEEL | RI_MOUSE_HWHEEL)) {
		int64_t now = UUrMachineTime_High();
		int64_t last_updated = now - DDgWheelDelta_Updated;
		DDgWheelDelta_Updated = now;

		// Reset the accumulators if too much time has passed since the last
		// scroll event. The player is assumed to have finished scrolling.
		if (last_updated / UUrMachineTime_High_Frequency() > 0.3) {
			DDgWheelDelta_V = 0;
			DDgWheelDelta_H = 0;
		}

		int neg_key, pos_key;
		int *delta;
		if (ri->data.mouse.usButtonFlags & RI_MOUSE_WHEEL) {
			neg_key = DDcKey_ScrollUp;
			pos_key = DDcKey_ScrollDown;
			delta = &DDgWheelDelta_V;
		} else {
			neg_key = DDcKey_ScrollLeft;
			pos_key = DDcKey_ScrollRight;
			delta = &DDgWheelDelta_H;
		}

		// To support touchpad scrolling and mice with continuous scroll wheels,
		// accumulate the wheel delta and only generate an input event once it
		// crosses the WHEEL_DELTA threshold
		*delta += (short)ri->data.mouse.usButtonData;
		if (*delta >= WHEEL_DELTA) {
			LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
				.input = neg_key,
				.analog = 1.0,
			});

			*delta -= (*delta / WHEEL_DELTA) * WHEEL_DELTA;
		} else if (*delta <= -WHEEL_DELTA) {
			LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
				.input = pos_key,
				.analog = 1.0,
			});

			*delta -= (*delta / -WHEEL_DELTA) * -WHEEL_DELTA;
		}
	}

	// This probably doesn't obey SM_SWAPBUTTON... should it?
	if (ri->data.mouse.usButtonFlags & RI_MOUSE_LEFT_BUTTON_DOWN)
		SetKeyState(LIcKey_MouseButton1, true);
	if (ri->data.mouse.usButtonFlags & RI_MOUSE_LEFT_BUTTON_UP)
		SetKeyState(LIcKey_MouseButton1, false);
	if (ri->data.mouse.usButtonFlags & RI_MOUSE_RIGHT_BUTTON_DOWN)
		SetKeyState(LIcKey_MouseButton2, true);
	if (ri->data.mouse.usButtonFlags & RI_MOUSE_RIGHT_BUTTON_UP)
		SetKeyState(LIcKey_MouseButton2, false);
	if (ri->data.mouse.usButtonFlags & RI_MOUSE_MIDDLE_BUTTON_DOWN)
		SetKeyState(LIcKey_MouseButton3, true);
	if (ri->data.mouse.usButtonFlags & RI_MOUSE_MIDDLE_BUTTON_UP)
		SetKeyState(LIcKey_MouseButton3, false);

	// Oni supports binding this button too. It's the back button on most mice.
	if (ri->data.mouse.usButtonFlags & RI_MOUSE_BUTTON_4_DOWN)
		SetKeyState(LIcKey_MouseButton4, true);
	if (ri->data.mouse.usButtonFlags & RI_MOUSE_BUTTON_4_UP)
		SetKeyState(LIcKey_MouseButton4, false);

	// Daodan supports binding the forward button too
	if (ri->data.mouse.usButtonFlags & RI_MOUSE_BUTTON_5_DOWN)
		SetKeyState(DDcKey_MouseButton5, true);
	if (ri->data.mouse.usButtonFlags & RI_MOUSE_BUTTON_5_UP)
		SetKeyState(DDcKey_MouseButton5, false);
}

static void DrainRawInput(void)
{
	if (!LIgMode_Internal)
		return;

	UINT ri_size = 10240;
	static RAWINPUT *ri_buf = NULL;
	if (!ri_buf)
		ri_buf = calloc(1, ri_size);

	BOOL wow_hack;
	IsWow64Process(GetCurrentProcess(), &wow_hack);

	for (;;) {
		UINT count = GetRawInputBuffer(ri_buf, &ri_size, sizeof ri_buf->header);
		if (count == 0 || count == (UINT)-1)
			return;

		RAWINPUT *ri = ri_buf;
		for (UINT i = 0; i < count; i++) {
			// In WOW64, these structures are aligned like in Win64 and they
			// have to be fixed to use from 32-bit code. Yes, really.
			if (wow_hack) {
				memmove(&ri->data, ((char *)&ri->data) + 8,
					ri->header.dwSize - offsetof(RAWINPUT, data) - 8);
			}

			ProcessRawInputPacket(ri);
			ri = NEXTRAWINPUTBLOCK(ri);
		}
	}
}

static UUtBool ONICALL DDiPlatform_InputEvent_GetEvent(void)
{
	// Center the cursor just in case. Raw Input doesn't need it, but sometimes
	// ClipCursor doesn't work for some reason and in that case we should still
	// prevent the user from accidentally clicking on other windows.
	if (LIgMode_Internal)
		CenterCursor();

	// Do a buffered read of raw input. Apparently this is faster for high-res
	// mice. Note that we still have to handle WM_INPUT in our wndproc in case
	// a WM_INPUT message arrives during the message loop.
	DrainRawInput();

	// Oni only processes a maximum of three messages here (for performance
	// reasons?) We're actually using Windows messages for input so we need to
	// process all of them.
	MSG msg;
	while (PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE)) {
		TranslateMessage(&msg);
		DispatchMessageA(&msg);
	}

	// Oni returns true here if there are still messages to process, so that
	// LIrMode_Set and LIrMode_Set_Internal can call this repeatedly to drain
	// all messages. We always drain all messages so return false.
	return UUcFalse;
}

static bool HandleWmInput(HRAWINPUT hri, WPARAM wparam)
{
	if (!LIgMode_Internal)
		return false;

	static RAWINPUT* ri = NULL;
	static UINT ri_size = 0;
	UINT minsize = 0;

	GetRawInputData(hri, RID_INPUT, NULL, &minsize, sizeof ri->header);
	if (ri_size < minsize) {
		if (ri)
			free(ri);
		ri_size = minsize;
		ri = calloc(1, ri_size);
	}
	if (GetRawInputData(hri, RID_INPUT, ri, &ri_size, sizeof ri->header) == (UINT)-1)
		return false;

	ProcessRawInputPacket(ri);
	return true;
}

static void HandleWmWindowPosChanged(WINDOWPOS *pos)
{
	if (!LIgMode_Internal)
		return;

	CenterCursor();

	RECT rc = { pos->x, pos->y, pos->x + pos->cx, pos->y + pos->cy };
	ClipCursor(&rc);
}

static void HandleWmKeyboard(int vkey, WORD repeat_count, WORD flags)
{
	if (!LIgMode_Internal)
		return;

	bool is_extended = flags & KF_EXTENDED;
	bool is_repeat = flags & KF_REPEAT;
	bool is_up = flags & KF_UP;
	BYTE sc = LOBYTE(flags);

	// Ignore togglable keys since we handle them specially
	if (vkey == VK_CAPITAL || vkey == VK_SCROLL || vkey == VK_NUMLOCK)
		return;

	// Ignore key down messages sent because of key-repeat
	if (!is_up && is_repeat)
		return;

	// Apparently some synthetic keyboard messages can be missing the scancode,
	// so get it from the vkey
	if (!sc)
		sc = MapVirtualKeyA(vkey, MAPVK_VK_TO_VSC);

	// DirectInput scan codes have 0x80 set for extended keys, and we're using
	// a map based on Oni's DirectInput map to convert to key codes
	if (is_extended)
		sc |= 0x80;
	uint8_t ch = DDgPlatform_ScanCodeToChar[sc];
	if (!ch)
		return;

	SetKeyState(ch, !is_up);
}

bool DDrInput_WindowProc(HWND window, UINT msg, WPARAM wparam, LPARAM lparam,
                         LRESULT* res)
{
	// This is called from our own window proc for now, so we only want to use
	// it when Daodan input is enabled
	if (!DDgUseDaodanInput)
		return false;

	switch (msg) {
		case WM_INPUT:
			if (HandleWmInput((HRAWINPUT)lparam, wparam)) {
				*res = 0;
				return true;
			}
			break;
		case WM_WINDOWPOSCHANGED:
			HandleWmWindowPosChanged((WINDOWPOS *)lparam);
			break;
		case WM_KEYDOWN:
		case WM_SYSKEYDOWN:
		case WM_KEYUP:
		case WM_SYSKEYUP:
			HandleWmKeyboard(LOWORD(wparam), LOWORD(lparam), HIWORD(lparam));
			break;
	}

	return false;
}

static void ONICALL DDrActionBuffer_Get(short* count, LItActionBuffer** buffers)
{
	// So, Oni's version of this function was totally different. In unpatched
	// Oni, action buffers were produced at 60Hz by a separate high-priority
	// input thread, LIiInterruptHandleProc, and consumed by
	// LIrActionBuffer_Get, which was called from the game loop and could
	// provide multiple outstanding action buffers to the engine at once.
	//
	// That was a problem for a couple of reasons. Firstly, the resolution of
	// Windows timers is limited by the timer frequency, which can be as low as
	// 15.6ms and in the worst case would cause a delay of 31.2ms between action
	// buffers. That meant that even if Oni was running at a steady 60 fps, the
	// input thread would provide no action buffers on a lot of frames.
	//
	// Secondly, even though Oni drained the list of pending action buffers by
	// calling DDrActionBuffer_Get on every frame, the engine only uses them
	// when the internal game time advances, and that happens on a separate 60Hz
	// timer which was totally unsynchronized with the 60Hz timer on the input
	// thread. That wasn't too much of a problem when the game loop was running
	// at less than 60 fps, but when it ran faster, the only action buffers that
	// got processed were the ones produced when the game timer and the input
	// thread timer happened to tick at the same time, meaning potentially a lot
	// of dropped input.
	//
	// Oni's input system was probably designed that way so that input would
	// still run at 60Hz even on PCs that weren't powerful enough to render at
	// 60 fps. It was a well-meaning design, but due to the aforementioned
	// flaws, we do something much different and simpler here. On the frames
	// that Oni will consume input, we generate a single action buffer inside
	// DDrActionBuffer_Get based on most up-to-date input.

	// Update ONgGameState->TargetGameTime. We use TargetGameTime to determine
	// if Oni is going to consume input on this frame. Unfortunately, in
	// unpatched Oni, the call to ONrGameState_UpdateServerTime happened after
	// LIrActionBuffer_Get. In Daodan, we NOOP out the original call and call it
	// here instead, so it runs before our code.
	ONrGameState_UpdateServerTime(ONgGameState);
	bool time_updated = ONgGameState->GameTime != ONgGameState->TargetGameTime;

	// Only produce input buffers when input is enabled. LIrActionBuffer_Get
	// does the same thing. Also only produce them when Oni will consume them.
	if (!LIgMode_Internal || !time_updated) {
		*count = 0;
		*buffers = NULL;
		return;
	}

	// Add held keys to the action buffer
	for (int i = 0; i < ARRAY_SIZE(DDgInputState); i++) {
		if (DDgInputState[i]) {
			LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
				.input = i,
				.analog = 1.0,
			});
		}
	}

	// Add togglable keys to the action buffer
	if (DDgCapsOniKey && (GetKeyState(VK_CAPITAL) & 0x01)) {
		LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
			.input = DDgCapsOniKey,
			.analog = 1.0,
		});
	}
	if (DDgScrollOniKey && (GetKeyState(VK_SCROLL) & 0x01)) {
		LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
			.input = DDgScrollOniKey,
			.analog = 1.0,
		});
	}
	if (DDgNumLockOniKey && (GetKeyState(VK_NUMLOCK) & 0x01)) {
		LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
			.input = DDgNumLockOniKey,
			.analog = 1.0,
		});
	}

	// Make a copy of our temporary action buffer with all the input we've
	// gathered this frame. This is the copy that Oni's engine will see.
	static LItActionBuffer buf = { 0 };
	buf = DDgActionBuffer;
	DDgActionBuffer	= (LItActionBuffer) { 0 };

	*count = 1;
	*buffers = &buf;
}

void DDrInput_PatchUtilityInput(void)
{
	// Patch the call to ONrGameState_HandleUtilityInput in
	// ONrGameState_ProcessHeartbeat. This is where Oni checks a bunch of
	// miscellaneous inputs, and where Mac Oni 1.2.1 checks the cheat bindings.
	// It's also where Mac Oni toggles the show_triggervolumes flag.
	DDrPatch_MakeCall((void *)0x004fa91c, (void *)DDrGameState_HandleUtilityInput);
}

void DDrInput_PatchCustomActions(void)
{
	DDrInput_PatchUtilityInput();

	// Replace the function which adds bindings with ours, which checks the list
	// of custom bindings as well
	DDrPatch_MakeJump((void *)LIrBinding_Add, (void *)DDrBinding_Add);
}

void DDrInput_PatchDaodanInput(void)
{
	// In LIrInitialize, NOOP the call to UUrInterruptProc_Install and
	// associated error checking
	DDrPatch_NOOP((char *)(OniExe + 0x421f), 106);

	// In LIrPlatform_Initialize, NOOP the Windows version checks so we never
	// use DirectInput
	DDrPatch_NOOP((char *)(OniExe + 0x2e64), 11);

	// Replace Oni's Windows message loop with one that does buffered raw input
	// reads and processes all messages
	DDrPatch_MakeJump((void *)LIiPlatform_InputEvent_GetEvent, (void *)DDiPlatform_InputEvent_GetEvent);

	// Replace the function that gets the latest input frames
	DDrPatch_MakeJump((void *)LIrActionBuffer_Get, (void *)DDrActionBuffer_Get);

	// Replace the function that gets the mouse cursor position
	DDrPatch_MakeJump((void *)LIrPlatform_InputEvent_GetMouse, (void *)DDrPlatform_InputEvent_GetMouse);

	// Replace the function that performs platform-specific actions when the
	// input mode changes
	DDrPatch_MakeJump((void *)LIrPlatform_Mode_Set, (void *)DDrPlatform_Mode_Set);

	// Replaces the function that tests the state of keyboard keys
	DDrPatch_MakeJump((void *)LIrPlatform_TestKey, (void *)DDrPlatform_TestKey);

	// Enable extra key names in key_config.txt
	DDrPatch_MakeJump((void *)LIrTranslate_InputName, (void *)DDrTranslate_InputName);

	// Patch out the call to ONrGameState_UpdateServerTime in ONiRunGame because
	// we want to do it earlier, in DDrActionBuffer_Get
	DDrPatch_NOOP((char *)(OniExe + 0xd4708), 11);

	DDgUseDaodanInput = true;
}
