#include "mungal4.h"
#pragma hdrstop

#include "l4padbindings.h"

#include <XInput.h>
#include <stdio.h>

//########################################################################
// Name tables: .NET Keys names (what vRIO uses) to virtual keys, pad
// names to XInput masks. Matching is case-insensitive.
//########################################################################

namespace
{
	struct NameValue
	{
		const char	*name;
		int			value;
	};

	const NameValue kKeyNames[] =
	{
		// letters and digits are generated in LookupKey
		{ "F1", VK_F1 }, { "F2", VK_F2 }, { "F3", VK_F3 }, { "F4", VK_F4 },
		{ "F5", VK_F5 }, { "F6", VK_F6 }, { "F7", VK_F7 }, { "F8", VK_F8 },
		{ "F9", VK_F9 }, { "F10", VK_F10 }, { "F11", VK_F11 }, { "F12", VK_F12 },
		{ "NumPad0", VK_NUMPAD0 }, { "NumPad1", VK_NUMPAD1 },
		{ "NumPad2", VK_NUMPAD2 }, { "NumPad3", VK_NUMPAD3 },
		{ "NumPad4", VK_NUMPAD4 }, { "NumPad5", VK_NUMPAD5 },
		{ "NumPad6", VK_NUMPAD6 }, { "NumPad7", VK_NUMPAD7 },
		{ "NumPad8", VK_NUMPAD8 }, { "NumPad9", VK_NUMPAD9 },
		{ "Divide", VK_DIVIDE }, { "Multiply", VK_MULTIPLY },
		{ "Subtract", VK_SUBTRACT }, { "Add", VK_ADD },
		{ "Decimal", VK_DECIMAL },
		{ "Up", VK_UP }, { "Down", VK_DOWN },
		{ "Left", VK_LEFT }, { "Right", VK_RIGHT },
		{ "Space", VK_SPACE }, { "Return", VK_RETURN }, { "Enter", VK_RETURN },
		{ "Escape", VK_ESCAPE }, { "Tab", VK_TAB }, { "Back", VK_BACK },
		{ "Home", VK_HOME }, { "End", VK_END },
		{ "PageUp", VK_PRIOR }, { "PageDown", VK_NEXT },
		{ "Prior", VK_PRIOR }, { "Next", VK_NEXT },
		{ "Insert", VK_INSERT }, { "Delete", VK_DELETE },
		{ "ShiftKey", VK_SHIFT }, { "Shift", VK_SHIFT },
		{ "ControlKey", VK_CONTROL }, { "Ctrl", VK_CONTROL }, { "Control", VK_CONTROL },
		{ "Menu", VK_MENU }, { "Alt", VK_MENU },
		{ "OemMinus", VK_OEM_MINUS }, { "Oemplus", VK_OEM_PLUS },
		{ "Oemcomma", VK_OEM_COMMA }, { "OemPeriod", VK_OEM_PERIOD },
		{ "OemOpenBrackets", VK_OEM_4 }, { "OemCloseBrackets", VK_OEM_6 },
		{ "OemSemicolon", VK_OEM_1 }, { "OemQuotes", VK_OEM_7 },
		{ "OemQuestion", VK_OEM_2 }, { "OemPipe", VK_OEM_5 },
		{ "Oemtilde", VK_OEM_3 },
	};

	const NameValue kPadButtonNames[] =
	{
		{ "A", XINPUT_GAMEPAD_A }, { "B", XINPUT_GAMEPAD_B },
		{ "X", XINPUT_GAMEPAD_X }, { "Y", XINPUT_GAMEPAD_Y },
		{ "DPadUp", XINPUT_GAMEPAD_DPAD_UP },
		{ "DPadDown", XINPUT_GAMEPAD_DPAD_DOWN },
		{ "DPadLeft", XINPUT_GAMEPAD_DPAD_LEFT },
		{ "DPadRight", XINPUT_GAMEPAD_DPAD_RIGHT },
		{ "Start", XINPUT_GAMEPAD_START }, { "Back", XINPUT_GAMEPAD_BACK },
		{ "LeftShoulder", XINPUT_GAMEPAD_LEFT_SHOULDER },
		{ "RightShoulder", XINPUT_GAMEPAD_RIGHT_SHOULDER },
		{ "LeftThumb", XINPUT_GAMEPAD_LEFT_THUMB },
		{ "RightThumb", XINPUT_GAMEPAD_RIGHT_THUMB },
	};

	const NameValue kPadAxisNames[] =
	{
		{ "LeftStickX", BindPadLeftStickX }, { "LeftStickY", BindPadLeftStickY },
		{ "RightStickX", BindPadRightStickX }, { "RightStickY", BindPadRightStickY },
		{ "LeftTrigger", BindPadLeftTrigger }, { "RightTrigger", BindPadRightTrigger },
	};

	const NameValue kRioAxisNames[] =
	{
		{ "Throttle", BindAxisThrottle },
		{ "LeftPedal", BindAxisLeftPedal }, { "RightPedal", BindAxisRightPedal },
		{ "JoystickY", BindAxisJoystickY }, { "JoystickX", BindAxisJoystickX },
	};

	Logical NameEquals(const char *a, const char *b)
	{
		return _stricmp(a, b) == 0;
	}

	int LookupTable(const NameValue *table, int count, const char *name)
	{
		for (int i = 0; i < count; ++i)
		{
			if (NameEquals(table[i].name, name))
			{
				return table[i].value;
			}
		}
		return -1;
	}

	int LookupKey(const char *name)
	{
		// single letter A-Z (VK == uppercase ASCII)
		if (name[1] == '\0' &&
			((name[0] >= 'A' && name[0] <= 'Z') || (name[0] >= 'a' && name[0] <= 'z')))
		{
			return toupper(name[0]);
		}
		// digit row: D0-D9 (VK == '0'-'9')
		if ((name[0] == 'D' || name[0] == 'd') &&
			name[1] >= '0' && name[1] <= '9' && name[2] == '\0')
		{
			return name[1];
		}
		return LookupTable(kKeyNames, sizeof(kKeyNames) / sizeof(kKeyNames[0]), name);
	}

	Logical ParseAddress(const char *text, int *address)
	{
		int value = -1;
		if (text[0] == '0' && (text[1] == 'x' || text[1] == 'X'))
		{
			if (sscanf(text + 2, "%x", &value) != 1)
			{
				return False;
			}
		}
		else if (sscanf(text, "%d", &value) != 1)
		{
			return False;
		}
		if ((value >= 0x00 && value <= 0x47) || (value >= 0x50 && value <= 0x6F))
		{
			*address = value;
			return True;
		}
		return False;
	}

	Logical ParseNumber(const char *text, Scalar *value)
	{
		float parsed;
		if (sscanf(text, "%f", &parsed) != 1)
		{
			return False;
		}
		*value = (Scalar) parsed;
		return True;
	}

	//---------------------------------------------------------------
	// Line tokenizer: whitespace separated, '#' starts a comment
	//---------------------------------------------------------------
	int Tokenize(char *line, char *tokens[], int max_tokens)
	{
		char *hash = strchr(line, '#');
		if (hash != NULL)
		{
			*hash = '\0';
		}
		int count = 0;
		char *cursor = line;
		while (count < max_tokens)
		{
			while (*cursor == ' ' || *cursor == '\t' || *cursor == '\r' || *cursor == '\n')
			{
				++cursor;
			}
			if (*cursor == '\0')
			{
				break;
			}
			tokens[count++] = cursor;
			while (*cursor != '\0' && *cursor != ' ' && *cursor != '\t' &&
				*cursor != '\r' && *cursor != '\n')
			{
				++cursor;
			}
			if (*cursor != '\0')
			{
				*cursor++ = '\0';
			}
		}
		return count;
	}

	//---------------------------------------------------------------
	// One line of the profile grammar
	//---------------------------------------------------------------
	Logical ParseLine(char *tokens[], int token_count, PadBindingProfile *profile)
	{
		if (token_count < 4)
		{
			return False;
		}

		if (NameEquals(tokens[0], "key") && NameEquals(tokens[2], "button"))
		{
			int key = LookupKey(tokens[1]);
			int address;
			if (key < 0 || !ParseAddress(tokens[3], &address) ||
				profile->keyButtonCount >= PadBindingProfile::maxKeyButtons)
			{
				return False;
			}
			Logical toggle = (token_count > 4 && NameEquals(tokens[4], "toggle"));
			PadKeyButtonBinding *binding = &profile->keyButtons[profile->keyButtonCount++];
			memset(binding, 0, sizeof(*binding));
			binding->virtualKey = key;
			binding->address = address;
			binding->toggle = toggle;
			return True;
		}

		if (NameEquals(tokens[0], "key") && NameEquals(tokens[2], "axis"))
		{
			if (token_count < 6)
			{
				return False;
			}
			int key = LookupKey(tokens[1]);
			int axis = LookupTable(kRioAxisNames,
				sizeof(kRioAxisNames) / sizeof(kRioAxisNames[0]), tokens[3]);
			int mode = NameEquals(tokens[4], "deflect") ? BindKeyDeflect
				: NameEquals(tokens[4], "rate") ? BindKeyRate : -1;
			Scalar value;
			if (key < 0 || axis < 0 || mode < 0 || !ParseNumber(tokens[5], &value) ||
				profile->keyAxisCount >= PadBindingProfile::maxKeyAxes)
			{
				return False;
			}
			PadKeyAxisBinding *binding = &profile->keyAxes[profile->keyAxisCount++];
			binding->virtualKey = key;
			binding->axis = axis;
			binding->mode = mode;
			binding->value = value;
			return True;
		}

		if (NameEquals(tokens[0], "pad") && NameEquals(tokens[2], "button"))
		{
			int mask = LookupTable(kPadButtonNames,
				sizeof(kPadButtonNames) / sizeof(kPadButtonNames[0]), tokens[1]);
			int address;
			if (mask < 0 || !ParseAddress(tokens[3], &address) ||
				profile->padButtonCount >= PadBindingProfile::maxPadButtons)
			{
				return False;
			}
			Logical toggle = (token_count > 4 && NameEquals(tokens[4], "toggle"));
			PadPadButtonBinding *binding = &profile->padButtons[profile->padButtonCount++];
			memset(binding, 0, sizeof(*binding));
			binding->padMask = (unsigned short) mask;
			binding->address = address;
			binding->toggle = toggle;
			return True;
		}

		if (NameEquals(tokens[0], "padaxis") && NameEquals(tokens[2], "axis"))
		{
			int source = LookupTable(kPadAxisNames,
				sizeof(kPadAxisNames) / sizeof(kPadAxisNames[0]), tokens[1]);
			int axis = LookupTable(kRioAxisNames,
				sizeof(kRioAxisNames) / sizeof(kRioAxisNames[0]), tokens[3]);
			if (source < 0 || axis < 0 ||
				profile->padAxisCount >= PadBindingProfile::maxPadAxes)
			{
				return False;
			}
			PadPadAxisBinding *binding = &profile->padAxes[profile->padAxisCount++];
			memset(binding, 0, sizeof(*binding));
			binding->source = source;
			binding->axis = axis;
			for (int i = 4; i < token_count; ++i)
			{
				if (NameEquals(tokens[i], "invert"))
				{
					binding->invert = True;
				}
				else if (NameEquals(tokens[i], "deadzone") && i + 1 < token_count)
				{
					if (!ParseNumber(tokens[++i], &binding->deadzone))
					{
						return False;
					}
				}
				else if (NameEquals(tokens[i], "rate") && i + 1 < token_count)
				{
					if (!ParseNumber(tokens[++i], &binding->rate))
					{
						return False;
					}
				}
				else
				{
					return False;
				}
			}
			return True;
		}

		return False;
	}

	//---------------------------------------------------------------
	// The default profile: vRIO's board-complete layout, adapted for
	// the desktop game - the driving keys (WASD/QE/PgUp/PgDn) stay
	// axes, so six bank buttons lose their keyboard keys (they are
	// still clickable on the on-screen cockpit, and rebindable here).
	//---------------------------------------------------------------
	const char kDefaultProfile[] =
"# RP412 input bindings - keyboard and Xbox (XInput) controller.\n"
"#\n"
"# One binding per line, '#' starts a comment, keywords are case-\n"
"# insensitive. Loaded at game start; delete this file to restore\n"
"# the defaults.\n"
"#\n"
"#   key <name>      button <addr> [toggle]\n"
"#   key <name>      axis <axis> deflect <n>\n"
"#   key <name>      axis <axis> rate <n-per-second>\n"
"#   pad <button>    button <addr> [toggle]\n"
"#   padaxis <src>   axis <axis> [invert] [deadzone <d>] [rate <n-per-second>]\n"
"#\n"
"# <addr>    RIO input address: lamp buttons 0x00-0x47, internal keypad\n"
"#           0x50-0x5F, external keypad 0x60-0x6F (hex or decimal).\n"
"# <axis>    Throttle | LeftPedal | RightPedal | JoystickY | JoystickX\n"
"# <name>    Keys name: A-Z, D0-D9 (digit row), F1-F12, NumPad0-NumPad9,\n"
"#           Up, Down, Left, Right, Space, Enter, PageUp, PageDown,\n"
"#           OemMinus, Oemplus, Oemcomma, OemPeriod, ...\n"
"# <button>  A B X Y DPadUp DPadDown DPadLeft DPadRight Start Back\n"
"#           LeftShoulder RightShoulder LeftThumb RightThumb\n"
"# <src>     LeftStickX LeftStickY RightStickX RightStickY\n"
"#           LeftTrigger RightTrigger\n"
"#\n"
"# 'deflect' holds the axis there while the key is down and springs\n"
"# back on release; 'rate' walks the axis by <n> per second and the\n"
"# position sticks (the throttle). Every lamp button is also clickable\n"
"# on the on-screen cockpit, so unbound addresses are never stranded.\n"
"\n"
"# ---- Flight: number pad + modifiers -------------------------------\n"
"# The whole letter board stays free for the MFD banks; flight lives\n"
"# on the numpad with throttle and reverse on the modifier keys.\n"
"key NumPad8 axis JoystickY deflect -1    # stick forward\n"
"key NumPad2 axis JoystickY deflect 1     # stick back\n"
"key NumPad4 axis JoystickX deflect 1     # stick left\n"
"key NumPad6 axis JoystickX deflect -1    # stick right\n"
"key NumPad7 axis LeftPedal deflect 1\n"
"key NumPad9 axis RightPedal deflect 1\n"
"key NumPad0 button 0x40                  # Main trigger (Space works too)\n"
"key Shift   axis Throttle rate 0.75      # throttle up\n"
"key Ctrl    axis Throttle rate -0.75     # throttle down\n"
"key Alt     button 0x3F                  # Throttle button (reverse thrust)\n"
"\n"
"# ---- Driving: Xbox controller (signs match the pod convention) ----\n"
"padaxis LeftStickX   axis JoystickX invert deadzone 0.24\n"
"padaxis LeftStickY   axis JoystickY invert deadzone 0.24\n"
"padaxis RightStickY  axis Throttle deadzone 0.24 rate 0.75\n"
"padaxis LeftTrigger  axis LeftPedal deadzone 0.12\n"
"padaxis RightTrigger axis RightPedal deadzone 0.12\n"
"\n"
"# ---- Joystick column: hat on the arrows, Main on Space ------------\n"
"key Space   button 0x40    # Main trigger\n"
"key Up      button 0x42    # Hat Up\n"
"key Down    button 0x41    # Hat Back\n"
"key Left    button 0x44    # Hat Left\n"
"key Right   button 0x43    # Hat Right\n"
"\n"
"# ---- Xbox controller: buttons -------------------------------------\n"
"pad A             button 0x40    # Main\n"
"pad B             button 0x46    # Middle\n"
"pad X             button 0x45    # Pinky\n"
"pad Y             button 0x47    # Upper\n"
"pad DPadUp        button 0x42    # Hat Up\n"
"pad DPadDown      button 0x41    # Hat Back\n"
"pad DPadLeft      button 0x44    # Hat Left\n"
"pad DPadRight     button 0x43    # Hat Right\n"
"pad LeftShoulder  button 0x3D    # Panic\n"
"pad RightShoulder button 0x3F    # Throttle button (reverse)\n"
"\n"
"# Start and Back are left free - map them to whatever you want, e.g.\n"
"# the config buttons on the Upper Right MFD:\n"
"#pad Start        button 0x37\n"
"#pad Back         button 0x36\n"
"\n"
"# ---- Upper MFD bank: the number row is the top MFD row and the\n"
"# ---- QWERTY row is the row under it, left to right across the\n"
"# ---- Left / Middle / Right MFDs.\n"
"key D1      button 0x2F\n"
"key D2      button 0x2E\n"
"key D3      button 0x2D\n"
"key D4      button 0x2C\n"
"key D5      button 0x27\n"
"key D6      button 0x26\n"
"key D7      button 0x25\n"
"key D8      button 0x24\n"
"key D9      button 0x37\n"
"key D0      button 0x36\n"
"key OemMinus button 0x35\n"
"key Oemplus  button 0x34\n"
"key Q       button 0x2B\n"
"key W       button 0x2A\n"
"key E       button 0x29\n"
"key R       button 0x28\n"
"key T       button 0x23\n"
"key Y       button 0x22\n"
"key U       button 0x21\n"
"key I       button 0x20\n"
"key O       button 0x33\n"
"key P       button 0x32\n"
"key OemOpenBrackets  button 0x31\n"
"key OemCloseBrackets button 0x30\n"
"\n"
"# ---- Lower MFD bank: home row and the row below it, two 4-key\n"
"# ---- blocks split by an unbound gap key (G / B), mirroring the\n"
"# ---- keypad gap between the Lower Left and Lower Right MFDs.\n"
"key A       button 0x0F\n"
"key S       button 0x0E\n"
"key D       button 0x0D\n"
"key F       button 0x0C\n"
"key H       button 0x07\n"
"key J       button 0x06\n"
"key K       button 0x05\n"
"key L       button 0x04\n"
"key Z       button 0x0B\n"
"key X       button 0x0A\n"
"key C       button 0x09\n"
"key V       button 0x08\n"
"key N       button 0x03\n"
"key M       button 0x02\n"
"key Oemcomma  button 0x01\n"
"key OemPeriod button 0x00\n"
"\n"
"# ---- Secondary / Screen columns on the function keys, top to\n"
"# ---- bottom (0x16/0x17 and 0x1E/0x1F intentionally unmapped).\n"
"key F1      button 0x10\n"
"key F2      button 0x11\n"
"key F3      button 0x12\n"
"key F4      button 0x13\n"
"key F5      button 0x14\n"
"key F6      button 0x15\n"
"key F7      button 0x18\n"
"key F8      button 0x19\n"
"key F9      button 0x1A\n"
"key F10     button 0x1B\n"
"key F11     button 0x1C\n"
"key F12     button 0x1D\n"
"\n"
"# The pilot keypad (0x50-0x5F) and external keypad (0x60-0x6F) are\n"
"# unbound - the game never reads them - but remain bindable here\n"
"# (they arrive as arcade RIO key events).\n";

	const char kBindingsFileName[] = "bindings.txt";
}

//########################################################################
// Loader
//########################################################################

void
	PadBindings_Load(PadBindingProfile *profile)
{
	memset(profile, 0, sizeof(*profile));

	//
	// First run: write the default profile so the player has a
	// self-documenting file to edit
	//
	FILE *file = fopen(kBindingsFileName, "rb");
	if (file == NULL)
	{
		FILE *out = fopen(kBindingsFileName, "wb");
		if (out != NULL)
		{
			fwrite(kDefaultProfile, 1, strlen(kDefaultProfile), out);
			fclose(out);
			DEBUG_STREAM << "PadBindings: wrote default " << kBindingsFileName
				<< "\n" << std::flush;
		}
		file = fopen(kBindingsFileName, "rb");
	}

	//
	// Parse: file if we have one, the built-in default if not
	//
	char *text = NULL;
	long size = 0;
	if (file != NULL)
	{
		fseek(file, 0, SEEK_END);
		size = ftell(file);
		fseek(file, 0, SEEK_SET);
		text = new char[size + 1];
		fread(text, 1, size, file);
		text[size] = '\0';
		fclose(file);
	}
	const char *source = (text != NULL) ? text : kDefaultProfile;

	char line[256];
	int line_number = 0;
	int error_count = 0;
	const char *cursor = source;
	while (*cursor != '\0')
	{
		int length = 0;
		while (cursor[length] != '\0' && cursor[length] != '\n' &&
			length < (int) sizeof(line) - 1)
		{
			line[length] = cursor[length];
			++length;
		}
		line[length] = '\0';
		cursor += length;
		if (*cursor == '\n')
		{
			++cursor;
		}
		++line_number;

		char *tokens[12];
		int token_count = Tokenize(line, tokens, 12);
		if (token_count == 0)
		{
			continue;
		}
		if (!ParseLine(tokens, token_count, profile))
		{
			++error_count;
			DEBUG_STREAM << "PadBindings: " << kBindingsFileName << " line "
				<< line_number << " rejected\n" << std::flush;
		}
	}

	if (text != NULL)
	{
		delete[] text;
	}

	DEBUG_STREAM << "PadBindings: " << profile->keyButtonCount << " key buttons, "
		<< profile->keyAxisCount << " key axes, "
		<< profile->padButtonCount << " pad buttons, "
		<< profile->padAxisCount << " pad axes"
		<< (error_count ? " (with rejected lines)" : "")
		<< "\n" << std::flush;
}
