/*
 * Wolfenstein: Enemy Territory GPL Source Code
 * Copyright (C) 1999-2010 id Software LLC, a ZeniMax Media company.
 *
 * ET: Legacy
 * Copyright (C) 2012-2024 ET:Legacy team <mail@etlegacy.com>
 *
 * This file is part of ET: Legacy - http://www.etlegacy.com
 *
 * ET: Legacy 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 3 of the License, or
 * (at your option) any later version.
 *
 * ET: Legacy 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 ET: Legacy. If not, see <http://www.gnu.org/licenses/>.
 *
 * In addition, Wolfenstein: Enemy Territory GPL Source Code is also
 * subject to certain additional terms. You should have received a copy
 * of these additional terms immediately following the terms and conditions
 * of the GNU General Public License which accompanied the source code.
 * If not, please request a copy in writing from id Software at the address below.
 *
 * id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA.
 */
/**
 * @file cl_keys.c
 */

#include "client.h"
#include "../qcommon/q_unicode.h"
#include "../sdl/sdl_defs.h"

/*
key up events are sent even if in console mode
*/

field_t historyEditLines[COMMAND_HISTORY];

int nextHistoryLine;        // the last line in the history buffer, not masked
int historyLine;            // the line being displayed from history buffer
                            // will be <= nextHistoryLine

field_t g_consoleField;

qboolean        key_overstrikeMode;
static char     fieldYankBuffer[MAX_EDIT_LINE];
static qboolean fieldLastCommandWasKillWord;

int    anykeydown;
qkey_t keys[MAX_KEYS];

typedef struct
{
	char *name;
	int keynum;
} keyname_t;

qboolean UI_checkKeyExec(int key);
qboolean CL_CGameCheckKeyExec(int key);

// names not in this list can either be lowercase ascii, or '0xnn' hex sequences
keyname_t keynames[] =
{
	{ "TAB",                   K_TAB                   },
	{ "ENTER",                 K_ENTER                 },
	{ "ESCAPE",                K_ESCAPE                },
	{ "SPACE",                 K_SPACE                 },
	{ "BACKSPACE",             K_BACKSPACE             },
	{ "UPARROW",               K_UPARROW               },
	{ "DOWNARROW",             K_DOWNARROW             },
	{ "LEFTARROW",             K_LEFTARROW             },
	{ "RIGHTARROW",            K_RIGHTARROW            },

	{ "RIGHTALT",              K_RALT                  },
	{ "LEFTALT",               K_LALT                  },
	{ "ALT",                   K_LALT                  },

	{ "RIGHTCTRL",             K_RCTRL                 },
	{ "LEFTCTRL",              K_LCTRL                 },
	{ "CTRL",                  K_LCTRL                 },

	{ "RIGHTSHIFT",            K_RSHIFT                },
	{ "LEFTSHIFT",             K_LSHIFT                },
	{ "SHIFT",                 K_LSHIFT                },

	{ "COMMAND",               K_COMMAND               },

	{ "CAPSLOCK",              K_CAPSLOCK              },

	{ "F1",                    K_F1                    },
	{ "F2",                    K_F2                    },
	{ "F3",                    K_F3                    },
	{ "F4",                    K_F4                    },
	{ "F5",                    K_F5                    },
	{ "F6",                    K_F6                    },
	{ "F7",                    K_F7                    },
	{ "F8",                    K_F8                    },
	{ "F9",                    K_F9                    },
	{ "F10",                   K_F10                   },
	{ "F11",                   K_F11                   },
	{ "F12",                   K_F12                   },
	{ "F13",                   K_F13                   },
	{ "F14",                   K_F14                   },
	{ "F15",                   K_F15                   },

	{ "INS",                   K_INS                   },
	{ "DEL",                   K_DEL                   },
	{ "PGDN",                  K_PGDN                  },
	{ "PGUP",                  K_PGUP                  },
	{ "HOME",                  K_HOME                  },
	{ "END",                   K_END                   },

	{ "MOUSE1",                K_MOUSE1                },
	{ "MOUSE2",                K_MOUSE2                },
	{ "MOUSE3",                K_MOUSE3                },
	{ "MOUSE4",                K_MOUSE4                },
	{ "MOUSE5",                K_MOUSE5                },

	{ "MWHEELUP",              K_MWHEELUP              },
	{ "MWHEELDOWN",            K_MWHEELDOWN            },

	{ "JOY1",                  K_JOY1                  },
	{ "JOY2",                  K_JOY2                  },
	{ "JOY3",                  K_JOY3                  },
	{ "JOY4",                  K_JOY4                  },
	{ "JOY5",                  K_JOY5                  },
	{ "JOY6",                  K_JOY6                  },
	{ "JOY7",                  K_JOY7                  },
	{ "JOY8",                  K_JOY8                  },
	{ "JOY9",                  K_JOY9                  },
	{ "JOY10",                 K_JOY10                 },
	{ "JOY11",                 K_JOY11                 },
	{ "JOY12",                 K_JOY12                 },
	{ "JOY13",                 K_JOY13                 },
	{ "JOY14",                 K_JOY14                 },
	{ "JOY15",                 K_JOY15                 },
	{ "JOY16",                 K_JOY16                 },
	{ "JOY17",                 K_JOY17                 },
	{ "JOY18",                 K_JOY18                 },
	{ "JOY19",                 K_JOY19                 },
	{ "JOY20",                 K_JOY20                 },
	{ "JOY21",                 K_JOY21                 },
	{ "JOY22",                 K_JOY22                 },
	{ "JOY23",                 K_JOY23                 },
	{ "JOY24",                 K_JOY24                 },
	{ "JOY25",                 K_JOY25                 },
	{ "JOY26",                 K_JOY26                 },
	{ "JOY27",                 K_JOY27                 },
	{ "JOY28",                 K_JOY28                 },
	{ "JOY29",                 K_JOY29                 },
	{ "JOY30",                 K_JOY30                 },
	{ "JOY31",                 K_JOY31                 },
	{ "JOY32",                 K_JOY32                 },

	{ "AUX1",                  K_AUX1                  },
	{ "AUX2",                  K_AUX2                  },
	{ "AUX3",                  K_AUX3                  },
	{ "AUX4",                  K_AUX4                  },
	{ "AUX5",                  K_AUX5                  },
	{ "AUX6",                  K_AUX6                  },
	{ "AUX7",                  K_AUX7                  },
	{ "AUX8",                  K_AUX8                  },
	{ "AUX9",                  K_AUX9                  },
	{ "AUX10",                 K_AUX10                 },
	{ "AUX11",                 K_AUX11                 },
	{ "AUX12",                 K_AUX12                 },
	{ "AUX13",                 K_AUX13                 },
	{ "AUX14",                 K_AUX14                 },
	{ "AUX15",                 K_AUX15                 },
	{ "AUX16",                 K_AUX16                 },

	{ "KP_HOME",               K_KP_HOME               },
	{ "KP_UPARROW",            K_KP_UPARROW            },
	{ "KP_PGUP",               K_KP_PGUP               },
	{ "KP_LEFTARROW",          K_KP_LEFTARROW          },
	{ "KP_5",                  K_KP_5                  },
	{ "KP_RIGHTARROW",         K_KP_RIGHTARROW         },
	{ "KP_END",                K_KP_END                },
	{ "KP_DOWNARROW",          K_KP_DOWNARROW          },
	{ "KP_PGDN",               K_KP_PGDN               },
	{ "KP_ENTER",              K_KP_ENTER              },
	{ "KP_INS",                K_KP_INS                },
	{ "KP_DEL",                K_KP_DEL                },
	{ "KP_SLASH",              K_KP_SLASH              },
	{ "KP_MINUS",              K_KP_MINUS              },
	{ "KP_PLUS",               K_KP_PLUS               },
	{ "KP_NUMLOCK",            K_KP_NUMLOCK            },
	{ "KP_STAR",               K_KP_STAR               },
	{ "KP_EQUALS",             K_KP_EQUALS             },

	{ "PAUSE",                 K_PAUSE                 },

	{ "SEMICOLON",             ';'                     }, // because a raw semicolon seperates commands

	{ "US_0",                  K_0                     },
	{ "US_1",                  K_1                     },
	{ "US_2",                  K_2                     },
	{ "US_3",                  K_3                     },
	{ "US_4",                  K_4                     },
	{ "US_5",                  K_5                     },
	{ "US_6",                  K_6                     },
	{ "US_7",                  K_7                     },
	{ "US_8",                  K_8                     },
	{ "US_9",                  K_9                     },

	{ "US_MINUS",              K_MINUS                 },
	{ "US_EQUALS",             K_EQUALS                },
	{ "US_LEFTBRACKET",        K_LEFTBRACKET           },
	{ "US_RIGHTBRACKET",       K_RIGHTBRACKET          },
	{ "US_BACKSLASH",          K_BACKSLASH             },
	{ "US_SEMICOLON",          K_SEMICOLON             },
	{ "US_APOSTROPHE",         K_APOSTROPHE            },
	{ "US_GRAVE",              K_GRAVE                 },
	{ "US_COMMA",              K_COMMA                 },
	{ "US_PERIOD",             K_PERIOD                },
	{ "US_SLASH",              K_SLASH                 },

	{ "NONUSBACKSLASH",        K_NONUSBACKSLASH        },
	{ "WINDOWS",               K_SUPER                 },
	{ "COMPOSE",               K_COMPOSE               },
	{ "MODE",                  K_MODE                  },
	{ "HELP",                  K_HELP                  },
	{ "PRINT",                 K_PRINT                 },
	{ "SYSREQ",                K_SYSREQ                },
	{ "SCROLLOCK",             K_SCROLLOCK             },
	{ "BREAK",                 K_BREAK                 },
	{ "MENU",                  K_MENU                  },
	{ "POWER",                 K_POWER                 },
	{ "EURO",                  K_EURO                  },
	{ "UNDO",                  K_UNDO                  },

	{ "PAD0_A",                K_PAD0_A                },
	{ "PAD0_B",                K_PAD0_B                },
	{ "PAD0_X",                K_PAD0_X                },
	{ "PAD0_Y",                K_PAD0_Y                },
	{ "PAD0_BACK",             K_PAD0_BACK             },
	{ "PAD0_GUIDE",            K_PAD0_GUIDE            },
	{ "PAD0_START",            K_PAD0_START            },
	{ "PAD0_LEFTSTICK_CLICK",  K_PAD0_LEFTSTICK_CLICK  },
	{ "PAD0_RIGHTSTICK_CLICK", K_PAD0_RIGHTSTICK_CLICK },
	{ "PAD0_LEFTSHOULDER",     K_PAD0_LEFTSHOULDER     },
	{ "PAD0_RIGHTSHOULDER",    K_PAD0_RIGHTSHOULDER    },
	{ "PAD0_DPAD_UP",          K_PAD0_DPAD_UP          },
	{ "PAD0_DPAD_DOWN",        K_PAD0_DPAD_DOWN        },
	{ "PAD0_DPAD_LEFT",        K_PAD0_DPAD_LEFT        },
	{ "PAD0_DPAD_RIGHT",       K_PAD0_DPAD_RIGHT       },

	{ "PAD0_LEFTSTICK_LEFT",   K_PAD0_LEFTSTICK_LEFT   },
	{ "PAD0_LEFTSTICK_RIGHT",  K_PAD0_LEFTSTICK_RIGHT  },
	{ "PAD0_LEFTSTICK_UP",     K_PAD0_LEFTSTICK_UP     },
	{ "PAD0_LEFTSTICK_DOWN",   K_PAD0_LEFTSTICK_DOWN   },
	{ "PAD0_RIGHTSTICK_LEFT",  K_PAD0_RIGHTSTICK_LEFT  },
	{ "PAD0_RIGHTSTICK_RIGHT", K_PAD0_RIGHTSTICK_RIGHT },
	{ "PAD0_RIGHTSTICK_UP",    K_PAD0_RIGHTSTICK_UP    },
	{ "PAD0_RIGHTSTICK_DOWN",  K_PAD0_RIGHTSTICK_DOWN  },
	{ "PAD0_LEFTTRIGGER",      K_PAD0_LEFTTRIGGER      },
	{ "PAD0_RIGHTTRIGGER",     K_PAD0_RIGHTTRIGGER     },

	{ "PAD0_MISC1",            K_PAD0_MISC1            },
	{ "PAD0_PADDLE1",          K_PAD0_PADDLE1          },
	{ "PAD0_PADDLE2",          K_PAD0_PADDLE2          },
	{ "PAD0_PADDLE3",          K_PAD0_PADDLE3          },
	{ "PAD0_PADDLE4",          K_PAD0_PADDLE4          },
	{ "PAD0_TOUCHPAD",         K_PAD0_TOUCHPAD         },

	{ NULL,                    0                       }
};

/*
=============================================================================
EDIT FIELDS
=============================================================================
*/

/**
 * @brief Handles horizontal scrolling and cursor blinking
 * x, y, and width are in pixels
 * @param[in,out] edit
 * @param[in] x
 * @param[in] y
 * @param[in] width - unused
 * @param[in] size
 * @param[in] showCursor
 * @param[in] noColorEscape
 */
void Field_VariableSizeDraw(field_t *edit, int x, int y, int width, int size, qboolean showCursor, qboolean noColorEscape)
{
	int  len     = strlen(edit->buffer);
	int  drawLen = edit->widthInChars;
	int  prestep;
	char str[MAX_STRING_CHARS];

	// guarantee that cursor will be visible
	if (len <= drawLen)
	{
		prestep = 0;
	}
	else
	{
		if (edit->scroll + drawLen > len)
		{
			edit->scroll = len - drawLen;
			if (edit->scroll < 0)
			{
				edit->scroll = 0;
			}
		}
		prestep = edit->scroll;
	}

	if (prestep + drawLen > len)
	{
		drawLen = len - prestep;
	}

	// extract <drawLen> characters from the field at <prestep>
	if (drawLen >= MAX_STRING_CHARS)
	{
		Com_Error(ERR_DROP, "drawLen >= MAX_STRING_CHARS");
		return;
	}

	Com_Memcpy(str, edit->buffer + prestep, drawLen);
	str[drawLen] = 0;

	// draw it
	SCR_DrawSmallString(x, y, str, colorWhite, qfalse, noColorEscape);

	// draw the cursor
	if (showCursor)
	{
		int cursorChar, i;

		if ((int)(cls.realtime >> 8) & 1)
		{
			return;     // off blink
		}

		if (key_overstrikeMode)
		{
			cursorChar = 11;
		}
		else
		{
			cursorChar = 10;
		}

		if (!noColorEscape)
		{
			Q_CleanStr(str);
		}
		i = drawLen - strlen(str);

		SCR_DrawSmallChar(x + (edit->cursor - prestep - i) * size, y, cursorChar);
	}
}

/**
 * @brief Field_Draw
 * @param[in] edit
 * @param[in] x
 * @param[in] y
 * @param[in] width
 * @param[in] showCursor
 * @param[in] noColorEscape
 */
void Field_Draw(field_t *edit, int x, int y, int width, qboolean showCursor, qboolean noColorEscape)
{
	Field_VariableSizeDraw(edit, x, y, width, smallCharWidth, showCursor, noColorEscape);
}

/**
 * @brief Field_Paste
 * @param[in] edit
 */
void Field_Paste(field_t *edit)
{
	char     *cbd;
	size_t   pasteLen, i;
	uint32_t *chars;

	cbd = IN_GetClipboardData();

	if (!cbd)
	{
		return;
	}

	// send as if typed, so insert / overstrike works properly
	pasteLen = Q_UTF8_Strlen(cbd);
	chars    = Com_Allocate(sizeof(uint32_t) * pasteLen);
	Com_Memset(chars, 0, sizeof(uint32_t) * pasteLen);
	Q_UTF8_ToUTF32(cbd, chars, &pasteLen);

	for (i = 0 ; i < pasteLen ; i++)
	{
		Field_InsertChar(edit, (int) chars[i], key_overstrikeMode);
	}
	Com_Dealloc(chars);

	Z_Free(cbd);
}

/*
=================
Field_NextWord
=================
*/

/**
 * @brief Seeks words in console with CTRL + left/right arrow keys
 *
 * @param[in,out] edit
 * @param[in] direction
 */
static void Field_SeekWord(field_t *edit, int direction)
{
	if (direction > 0)
	{
		while (edit->buffer[edit->cursor] == ' ')
		{
			edit->cursor++;
		}
		while (edit->buffer[edit->cursor] != '\0' && edit->buffer[edit->cursor] != ' ')
		{
			edit->cursor++;
		}
		while (edit->buffer[edit->cursor] == ' ')
		{
			edit->cursor++;
		}
	}
	else
	{
		while (edit->cursor > 0 && edit->buffer[edit->cursor - 1] == ' ')
		{
			edit->cursor--;
		}
		while (edit->cursor > 0 && edit->buffer[edit->cursor - 1] != ' ')
		{
			edit->cursor--;
		}
		if (edit->cursor == 0 && (edit->buffer[0] == '/' || edit->buffer[0] == '\\'))
		{
			edit->cursor++;
		}
	}
}

/**
 * @brief Checks if a UTF-8 character should be treated as a kill-word separator.
 * @param[in] utf8Char
 * @return
 */
static qboolean Field_IsKillWordSeparator(const char *utf8Char)
{
	unsigned char ch;

	if (!utf8Char || !*utf8Char)
	{
		return qfalse;
	}

	ch = (unsigned char)utf8Char[0];

	// Keep shell-like path/token separators explicit for console editing.
	return (ch == '/' || ch == '\\' || ch == '_');
}

/**
 * @brief Checks if a UTF-8 character stops backward kill-word scanning.
 * @param[in] utf8Char
 * @return
 */
static qboolean Field_IsKillWordStop(const char *utf8Char)
{
	unsigned char ch;

	if (!utf8Char || !*utf8Char)
	{
		return qtrue;
	}

	ch = (unsigned char)utf8Char[0];

	// Whitespace and explicit token separators define kill-word boundaries.
	return (isspace(ch) || Field_IsKillWordSeparator(utf8Char));
}

/**
 * @brief Checks if a character index is at a lower->upper camel-case boundary.
 * @param[in] edit
 * @param[in] charIndex
 * @return
 */
static qboolean Field_IsKillWordCamelBoundary(field_t *edit, int charIndex)
{
	const char    *currentChar;
	const char    *previousChar;
	unsigned char current;
	unsigned char previous;

	if (!edit || charIndex <= 0)
	{
		return qfalse;
	}

	currentChar  = Q_UTF8_CharAt(edit->buffer, charIndex);
	previousChar = Q_UTF8_CharAt(edit->buffer, charIndex - 1);
	if (!currentChar || !previousChar || !*currentChar || !*previousChar)
	{
		return qfalse;
	}

	current  = (unsigned char)currentChar[0];
	previous = (unsigned char)previousChar[0];

	// Stop on the first lower->upper case transition inside a token.
	return (islower(previous) && isupper(current));
}

/**
 * @brief Deletes one backward kill-word unit and appends it to yank buffer.
 * @param[in,out] edit
 */
static void Field_KillWordBackward(field_t *edit)
{
	int    start, end;
	int    startOffset, endOffset;
	int    removedLength;
	size_t len;
	char   removedText[MAX_EDIT_LINE];

	if (edit->cursor <= 0)
	{
		return;
	}

	start = edit->cursor;
	end   = edit->cursor;

	// First remove trailing spaces, if any.
	while (start > 0 && isspace((unsigned char)Q_UTF8_CharAt(edit->buffer, start - 1)[0]))
	{
		start--;
	}

	// If the first character is a separator, consume separators then the preceding word segment.
	if (start > 0 && Field_IsKillWordSeparator(Q_UTF8_CharAt(edit->buffer, start - 1)))
	{
		while (start > 0 && Field_IsKillWordSeparator(Q_UTF8_CharAt(edit->buffer, start - 1)))
		{
			start--;
		}

		while (start > 0 && !Field_IsKillWordStop(Q_UTF8_CharAt(edit->buffer, start - 1)))
		{
			start--;
			if (start > 0 && Field_IsKillWordCamelBoundary(edit, start))
			{
				break;
			}
		}
	}
	else
	{
		// Regular kill-word: consume word chars until a boundary is reached.
		while (start > 0 && !Field_IsKillWordStop(Q_UTF8_CharAt(edit->buffer, start - 1)))
		{
			start--;
			if (start > 0 && Field_IsKillWordCamelBoundary(edit, start))
			{
				break;
			}
		}
	}

	startOffset   = Q_UTF8_ByteOffset(edit->buffer, start);
	endOffset     = Q_UTF8_ByteOffset(edit->buffer, end);
	removedLength = endOffset - startOffset;
	if (removedLength <= 0)
	{
		return;
	}

	if (removedLength >= (int)sizeof(removedText))
	{
		removedLength = sizeof(removedText) - 1;
	}

	Com_Memcpy(removedText, edit->buffer + startOffset, removedLength);
	removedText[removedLength] = '\0';

	len = strlen(edit->buffer);
	memmove(edit->buffer + startOffset, edit->buffer + endOffset, len + 1 - endOffset);

	// Consecutive kill-word operations prepend to preserve natural yank order.
	if (fieldLastCommandWasKillWord && fieldYankBuffer[0] != '\0')
	{
		char existingYank[MAX_EDIT_LINE];
		Q_strncpyz(existingYank, fieldYankBuffer, sizeof(existingYank));
		Com_sprintf(fieldYankBuffer, sizeof(fieldYankBuffer), "%s%s", removedText, existingYank);
	}
	else
	{
		Q_strncpyz(fieldYankBuffer, removedText, sizeof(fieldYankBuffer));
	}

	fieldLastCommandWasKillWord = qtrue;
	edit->cursor                = start;

	if (edit->cursor < edit->scroll)
	{
		edit->scroll = edit->cursor;
	}
}

/**
 * @brief Performs the basic line editing functions for the console,
 * in-game talk, and menu fields
 *
 * Key events are used for non-printable characters, others are gotten from char events.
 *
 * @param[in,out] edit
 * @param[in] key
 */
void Field_KeyDownEvent(field_t *edit, int key)
{
	size_t len, stringLen;

	// shift-insert is paste
	if (((key == K_INS) || (key == K_KP_INS)) && (keys[K_RSHIFT].down || keys[K_LSHIFT].down))
	{
		Field_Paste(edit);
		return;
	}

	key       = tolower(key);
	len       = strlen(edit->buffer);
	stringLen = Q_UTF8_Strlen(edit->buffer);

	switch (key)
	{
	case K_DEL:
	case K_KP_DEL:
		if (edit->cursor < stringLen)
		{
			int  offset    = Q_UTF8_ByteOffset(edit->buffer, edit->cursor);
			char *current  = Q_UTF8_CharAt(edit->buffer, edit->cursor);
			int  charWidth = Q_UTF8_Width(current);

			memmove(edit->buffer + offset,
			        edit->buffer + offset + charWidth, len - offset);
		}
		break;
	case K_RIGHTARROW:
	case K_KP_RIGHTARROW:
		if (edit->cursor < stringLen)
		{
			if (keys[K_LCTRL].down || keys[K_RCTRL].down)
			{
				Field_SeekWord(edit, 1);
			}
			else
			{
				edit->cursor++;
			}
		}
		break;
	case K_LEFTARROW:
	case K_KP_LEFTARROW:
		if (edit->cursor > 0)
		{
			if (keys[K_LCTRL].down || keys[K_RCTRL].down)
			{
				Field_SeekWord(edit, -1);
			}
			else
			{
				edit->cursor--;
			}
		}
		break;
	case K_HOME:
	case K_KP_HOME:
		edit->cursor = 0;
		break;
	case 'a':
		if (keys[K_LCTRL].down || keys[K_RCTRL].down)
		{
			edit->cursor = 0;
		}
		break;
	case K_END:
	case K_KP_END:
		edit->cursor = stringLen;
		break;
	case 'e':
		if (keys[K_LCTRL].down || keys[K_RCTRL].down)
		{
			edit->cursor = stringLen;
		}
		break;
	case K_INS:
	case K_KP_INS:
		key_overstrikeMode = !key_overstrikeMode;
		break;
	default:
		break;
	}

	// Change scroll if cursor is no longer visible
	if (edit->cursor < edit->scroll)
	{
		edit->scroll = edit->cursor;
	}
	else if (edit->cursor >= edit->scroll + edit->widthInChars && edit->cursor <= len)
	{
		edit->scroll = edit->cursor - edit->widthInChars + 1;
	}
}

/**
 * @brief Field_CharEvent
 * @param[in,out] edit
 * @param[in] ch
 */
void Field_CharEvent(field_t *edit, int ch)
{
	int len, stringLen;
	//int charWidth;
	//char *value = NULL;

	//charWidth = Q_UTF8_WidthCP(ch);
	//value     = Q_UTF8_Encode(ch);

	if (ch == CTRL('v') && !keys[K_RALT].down)      // ctrl-v is paste
	{
		fieldLastCommandWasKillWord = qfalse;
		Field_Paste(edit);
		return;
	}

	if (ch == CTRL('c') || ch == CTRL('u'))      // ctrl-c/u clears the entire input line
	{
		fieldLastCommandWasKillWord = qfalse;
		Field_Clear(edit);
		return;
	}

	if (ch == CTRL('w'))      // ctrl-w deletes backward word segments and updates yank buffer
	{
		Field_KillWordBackward(edit);
		return;
	}

	if (ch == CTRL('y'))      // ctrl-y yanks previously kill-word deleted text
	{
		int    yankOffset;
		size_t yankByteLen;
		size_t yankCharLen;

		fieldLastCommandWasKillWord = qfalse;

		if (fieldYankBuffer[0] == '\0')
		{
			return;
		}

		len         = strlen(edit->buffer);
		yankByteLen = strlen(fieldYankBuffer);
		yankCharLen = Q_UTF8_Strlen(fieldYankBuffer);

		// Keep headroom consistent with normal field insertion behavior.
		if ((size_t)len + yankByteLen >= (MAX_EDIT_LINE - 1))
		{
			return;
		}

		yankOffset = Q_UTF8_ByteOffset(edit->buffer, edit->cursor);
		memmove(edit->buffer + yankOffset + yankByteLen,
		        edit->buffer + yankOffset,
		        len + 1 - yankOffset);
		Com_Memcpy(edit->buffer + yankOffset, fieldYankBuffer, yankByteLen);

		edit->cursor += yankCharLen;
		if (edit->cursor >= edit->scroll + edit->widthInChars)
		{
			edit->scroll = edit->cursor - edit->widthInChars + 1;
		}
		return;
	}

	fieldLastCommandWasKillWord = qfalse;

	len       = strlen(edit->buffer);
	stringLen = Q_UTF8_Strlen(edit->buffer);

	if (ch == CTRL('h'))          // ctrl-h is backspace
	{
		if (edit->cursor > 0)
		{
			int  offset    = Q_UTF8_ByteOffset(edit->buffer, edit->cursor);
			char *prev     = Q_UTF8_CharAt(edit->buffer, edit->cursor - 1);
			int  charWidth = Q_UTF8_Width(prev);
			memmove(edit->buffer + offset - charWidth, edit->buffer + offset, len + 1 - offset);
			edit->cursor--;
			if (edit->cursor < edit->scroll)
			{
				edit->scroll--;
			}
		}
		return;
	}

	if (ch == CTRL('a'))      // ctrl-a is home
	{
		edit->cursor = 0;
		edit->scroll = 0;
		return;
	}

	if (ch == CTRL('e'))      // ctrl-e is end
	{
		edit->cursor = stringLen;
		edit->scroll = edit->cursor - edit->widthInChars;
		return;
	}

	// ignore any other non printable chars
	if (ch < 32)
	{
		return;
	}

	// - 2 to leave room for the leading slash and trailing \0
	if (len == MAX_EDIT_LINE - 2)
	{
		return; // all full
	}

	Q_UTF8_Insert(edit->buffer, stringLen, edit->cursor, ch, key_overstrikeMode);
	edit->cursor++;


	if (edit->cursor >= edit->widthInChars)
	{
		edit->scroll++;
	}

	if (edit->cursor == stringLen + 1)
	{
		edit->buffer[Q_UTF8_ByteOffset(edit->buffer, edit->cursor)] = 0;
	}
}

/*
=============================================================================
CONSOLE LINE EDITING
==============================================================================
*/

/**
 * @brief Handles history and console scrollback
 * @param[in] key
 */
void Console_Key(int key)
{
	// ctrl-L clears screen
	if (key == 'l' && (keys[K_LCTRL].down || keys[K_RCTRL].down))
	{
		Cbuf_AddText("clear\n");
		return;
	}

	// enter finishes the line
	if (key == K_ENTER || key == K_KP_ENTER || key == K_PAD0_START)
	{
		con.highlightOffset = 0;

#if SLASH_COMMAND
		// if not in the game explicitly prepend a slash if needed
		if (cls.state != CA_ACTIVE && g_consoleField.buffer[0] != '\\'
		    && g_consoleField.buffer[0] != '/')
		{
			char temp[MAX_STRING_CHARS];

			Q_strncpyz(temp, g_consoleField.buffer, sizeof(temp));
			Com_sprintf(g_consoleField.buffer, sizeof(g_consoleField.buffer), "\\%s", temp);
			g_consoleField.cursor++;
		}
#endif

		Com_Printf("]%s\n", g_consoleField.buffer);

#if SLASH_COMMAND
		// leading slash is an explicit command
		if (g_consoleField.buffer[0] == '\\' || g_consoleField.buffer[0] == '/')
		{
			Cbuf_AddText(g_consoleField.buffer + 1);      // valid command
			Cbuf_AddText("\n");
		}
		else
		{
			// other text will be chat messages
			if (!g_consoleField.buffer[0])
			{
				return; // empty lines just scroll the console without adding to history
			}
			else
			{
				char escapedChatMessage[MAX_EDIT_LINE * 2];
				Q_EscapeUnicode(g_consoleField.buffer, escapedChatMessage, MAX_EDIT_LINE * 2);

				Cbuf_AddText("cmd say ");
				Cbuf_AddText(escapedChatMessage);
				Cbuf_AddText("\n");
			}
		}
#else
		Cbuf_AddText(g_consoleField.buffer);      // valid command
		Cbuf_AddText("\n");

		if (!g_consoleField.buffer[0])
		{
			return; // empty lines just scroll the console without adding to history
		}
#endif

		// copy line to history buffer
		historyEditLines[nextHistoryLine % COMMAND_HISTORY] = g_consoleField;
		nextHistoryLine++;
		historyLine = nextHistoryLine;

		Field_Clear(&g_consoleField);

		g_consoleField.widthInChars = Con_ConsoleFieldWidth();

		Con_SaveConsoleHistory();

		if (cls.state == CA_DISCONNECTED)
		{
			SCR_UpdateScreen();     // force an update, because the command
		}                           // may take some time
		return;
	}

	// command completion

	if (key == K_TAB)
	{
		Console_AutoComplete(&g_consoleField, &con.highlightOffset);
		return;
	}

	// clear autocompletion buffer on normal key input
	if ((key >= K_SPACE && key <= K_BACKSPACE) || (key == K_LEFTARROW) || (key == K_RIGHTARROW) ||
	    (key >= K_KP_LEFTARROW && key <= K_KP_RIGHTARROW) ||
	    (key >= K_KP_SLASH && key <= K_KP_PLUS) || (key >= K_KP_STAR && key <= K_KP_EQUALS))
	{
		if (key == K_BACKSPACE && con.highlightOffset)
		{
			Console_RemoveHighlighted(&g_consoleField, &con.highlightOffset);
		}
		else
		{
			con.highlightOffset = 0;
		}
	}

	// command history (ctrl-p ctrl-n for unix style)

	// added some mousewheel functionality to the console
	if ((key == K_MWHEELUP && (keys[K_RSHIFT].down || keys[K_LSHIFT].down)) || (key == K_UPARROW) || (key == K_KP_UPARROW) ||
	    ((tolower(key) == 'p') && (keys[K_LCTRL].down || keys[K_RCTRL].down)))
	{
		if (nextHistoryLine - historyLine < COMMAND_HISTORY
		    && historyLine > 0)
		{
			historyLine--;
		}
		g_consoleField              = historyEditLines[historyLine % COMMAND_HISTORY];
		g_consoleField.widthInChars = Con_ConsoleFieldWidth();
		con.highlightOffset         = 0;
		return;
	}

	// added some mousewheel functionality to the console

	if ((key == K_MWHEELDOWN && (keys[K_RSHIFT].down || keys[K_LSHIFT].down)) || (key == K_DOWNARROW) || (key == K_KP_DOWNARROW) ||
	    ((tolower(key) == 'n') && (keys[K_LCTRL].down || keys[K_RCTRL].down)))
	{
		if (historyLine == nextHistoryLine)
		{
			return;
		}
		historyLine++;
		g_consoleField              = historyEditLines[historyLine % COMMAND_HISTORY];
		g_consoleField.widthInChars = Con_ConsoleFieldWidth();
		con.highlightOffset         = 0;
		return;
	}

	// console scrolling
	if ((key == K_PGUP || key == K_KP_PGUP) && (keys[K_LSHIFT].down || keys[K_RSHIFT].down))
	{
		// Shift+PgUp performs an absolute jump to the oldest visible console line.
		Con_ScrollTop();
		return;
	}

	if ((key == K_PGDN || key == K_KP_PGDN) && (keys[K_LSHIFT].down || keys[K_RSHIFT].down))
	{
		// Shift+PgDn performs an absolute jump back to live console output.
		Con_ScrollBottom();
		return;
	}

	if (key == K_PGUP || key == K_KP_PGUP)
	{
		Con_PageUp();
		return;
	}

	if (key == K_PGDN || key == K_KP_PGDN)
	{
		Con_PageDown();
		return;
	}

	if (key == K_MWHEELUP)           // added some mousewheel functionality to the console
	{
		if (keys[K_LCTRL].down || keys[K_RCTRL].down) // hold <ctrl> to accelerate scrolling
		{
			Con_ScrollUp(con.visibleLines);
		}
		else
		{
			Con_ScrollUp(con.visibleLines / 4);
		}
		return;
	}

	if (key == K_MWHEELDOWN)         // added some mousewheel functionality to the console
	{
		if (keys[K_LCTRL].down || keys[K_RCTRL].down) // hold <ctrl> to accelerate scrolling
		{
			Con_ScrollDown(con.visibleLines);
		}
		else
		{
			Con_ScrollDown(con.visibleLines / 4);
		}
		return;
	}

	// ctrl-home = top of console
	if ((key == K_HOME || key == K_KP_HOME) && (keys[K_LCTRL].down || keys[K_RCTRL].down))
	{
		Con_ScrollTop();
		return;
	}

	// ctrl-end = bottom of console
	if ((key == K_END || key == K_KP_END) && (keys[K_LCTRL].down || keys[K_RCTRL].down))
	{
		Con_ScrollBottom();
		return;
	}

	// pass to the normal editline routine
	Field_KeyDownEvent(&g_consoleField, key);
}

//============================================================================

/**
 * @brief Key_GetOverstrikeMode
 * @return
 */
qboolean Key_GetOverstrikeMode(void)
{
	return key_overstrikeMode;
}

/**
 * @brief Key_SetOverstrikeMode
 * @param[in] state
 */
void Key_SetOverstrikeMode(qboolean state)
{
	key_overstrikeMode = state;
}

/**
 * @brief Key_IsDown
 * @param[in] keynum
 * @return
 */
qboolean Key_IsDown(int keynum)
{
	if (keynum < 0 || keynum >= MAX_KEYS)
	{
		return qfalse;
	}

	return keys[keynum].down;
}

/**
 * @brief Returns a key number to be used to index keys[] by looking at
 * the given string.  Single ascii characters return themselves, while
 * the K_* names are matched up.
 *
 * 0x11 will be interpreted as raw hex, which will allow new controlers
 * to be configured even if they don't have defined names.
 *
 * @param[in] str
 * @return
 */
int Key_StringToKeynum(const char *str)
{
	keyname_t *kn;

	if (!str || !str[0])
	{
		return -1;
	}
	if (!str[1])
	{
		return tolower(str[0]);
	}

	// check for hex code
	if (strlen(str) == 4)
	{
		int n = Com_HexStrToInt(str);

		if (n >= 0)
		{
			return n;
		}
	}

	// scan for a text match
	for (kn = keynames ; kn->name ; kn++)
	{
		if (!Q_stricmp(str, kn->name))
		{
			return kn->keynum;
		}
	}

	return -1;
}

/**
 * @brief Returns a string (either a single ascii char, a K_* name, or a 0x11 hex string) for the
 * given keynum.
 *
 * @param[in] keynum
 * @return
 */
char *Key_KeynumToString(int keynum)
{
	keyname_t   *kn;
	static char tinystr[5];
	int         i, j;

	if (keynum == -1)
	{
		return "<KEY NOT FOUND>";
	}

	if (keynum < 0 || keynum >= MAX_KEYS)
	{
		return "<OUT OF RANGE>";
	}

	// check for printable ascii (don't use quote)
	if (keynum > 32 && keynum < 127 && keynum != '"' && keynum != ';')
	{
		tinystr[0] = keynum;
		tinystr[1] = 0;
		return tinystr;
	}

	// check for a key string
	for (kn = keynames ; kn->name ; kn++)
	{
		if (keynum == kn->keynum)
		{
			return kn->name;
		}
	}

	// make a hex string
	i = keynum >> 4;
	j = keynum & 15;

	tinystr[0] = '0';
	tinystr[1] = 'x';
	tinystr[2] = i > 9 ? i - 10 + 'a' : i + '0';
	tinystr[3] = j > 9 ? j - 10 + 'a' : j + '0';
	tinystr[4] = 0;

	return tinystr;
}

#define BIND_HASH_SIZE 1024
#define generateHashValue(fname) Q_GenerateHashValue(fname, BIND_HASH_SIZE, qtrue, qtrue)

/**
 * @brief Key_SetBinding
 * @param[in] keynum
 * @param[in] binding
 */
void Key_SetBinding(int keynum, const char *binding)
{
	if (keynum == -1)
	{
		return;
	}

	// free old bindings
	if (keys[keynum].binding)
	{
		Z_Free(keys[keynum].binding);
	}

	// allocate memory for new binding
	keys[keynum].binding = CopyString(binding);
	keys[keynum].hash    = generateHashValue(binding);

	// consider this like modifying an archived cvar, so the
	// file write will be triggered at the next oportunity
	cvar_modifiedFlags |= CVAR_ARCHIVE;
}

/**
 * @brief Key_GetBinding
 * @param[in] keynum
 * @return
 */
char *Key_GetBinding(int keynum)
{
	if (keynum < 0 || keynum >= MAX_KEYS)
	{
		return "";
	}

	return keys[keynum].binding;
}

/**
 * @brief Key_GetBindingByString
 * @param[in] binding
 * @param[out] key1
 * @param[out] key2
 *
 * @note Binding MUST be lower case
 */
void Key_GetBindingByString(const char *binding, int *key1, int *key2)
{
	int i;
	int hash = generateHashValue(binding);

	*key1 = -1;
	*key2 = -1;

	for (i = 0; i < MAX_KEYS; i++)
	{
		if (keys[i].hash == hash && !Q_stricmp(binding, keys[i].binding))
		{
			if (*key1 == -1)
			{
				*key1 = i;
			}
			else if (*key2 == -1)
			{
				*key2 = i;
				return;
			}
		}
	}
}

/**
 * @brief Key_GetKey
 * @param[in] binding
 * @return
 */
int Key_GetKey(const char *binding)
{
	if (binding)
	{
		int i;

		for (i = 0 ; i < MAX_KEYS ; i++)
		{
			if (keys[i].binding && Q_stricmp(binding, keys[i].binding) == 0)
			{
				return i;
			}
		}
	}
	return -1;
}

/**
 * @brief Key_Unbind_f
 */
void Key_Unbind_f(void)
{
	int b;

	if (Cmd_Argc() != 2)
	{
		Com_Printf("unbind <key> : remove commands from a key\n");
		return;
	}

	b = Key_StringToKeynum(Cmd_Argv(1));
	if (b == -1)
	{
		Com_Printf("\"%s\" isn't a valid key\n", Cmd_Argv(1));
		return;
	}

	Key_SetBinding(b, "");
}

/**
 * @brief Key_Unbindall_f
 */
void Key_Unbindall_f(void)
{
	int i;

	for (i = 0 ; i < MAX_KEYS ; i++)
	{
		if (keys[i].binding)
		{
			Key_SetBinding(i, "");
		}
	}
}

/**
 * @brief Key_Bind_f
 */
void Key_Bind_f(void)
{
	int  i, c, b;
	char cmd[MAX_CMD];

	c = Cmd_Argc();

	if (c < 2)
	{
		Com_Printf("bind <key> [command] : attach a command to a key\n");
		return;
	}
	b = Key_StringToKeynum(Cmd_Argv(1));
	if (b == -1)
	{
		Com_Printf("\"%s\" isn't a valid key\n", Cmd_Argv(1));
		return;
	}

	if (c == 2)
	{
		// FIXME: keyboard translations
		if (keys[b].binding && keys[b].binding[0])
		{
			Com_Printf("\"%s\" = \"%s\"\n", Key_KeynumToString(b), keys[b].binding);
		}
		else
		{
			Com_Printf("\"%s\" is not bound\n", Key_KeynumToString(b));
		}
		return;
	}

	// copy the rest of the command line
	cmd[0] = 0;     // start out with a null string
	for (i = 2 ; i < c ; i++)
	{
		Q_strcat(cmd, sizeof(cmd), Cmd_Argv(i));
		if (i != (c - 1))
		{
			Q_strcat(cmd, sizeof(cmd), " ");
		}
	}

	Key_SetBinding(b, cmd);
}

/**
 * @brief Writes lines containing "bind key value"
 * @param[in] f
 */
void Key_WriteBindings(fileHandle_t f)
{
	int i;

	FS_Printf(f, "unbindall\n");

	for (i = 0 ; i < MAX_KEYS ; i++)
	{
		if (keys[i].binding && keys[i].binding[0])
		{
			FS_Printf(f, "bind %s \"%s\"\n", Key_KeynumToString(i), keys[i].binding);
		}
	}
}

/**
 * @brief Key_Bindlist_f
 */
void Key_Bindlist_f(void)
{
	int i;
	int freeKeys = 0;

	Com_Printf("key             bind\n");

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

	for (i = 0 ; i < MAX_KEYS ; i++)
	{
		if (keys[i].binding && keys[i].binding[0])
		{
			if (Cmd_Argc() != 2)
			{
				Com_Printf("%-15s \"%s\"\n", Key_KeynumToString(i), keys[i].binding);
			}
		}
		else
		{
			++freeKeys;

			if (Cmd_Argc() == 2)
			{
				Com_Printf("%-15s *free to bind*\n", Key_KeynumToString(i));
			}
		}
	}

	Com_Printf("-----------------------------------\n");
	Com_Printf("%i free keys available.\n", freeKeys);
	Com_Printf("Enter /bindlist -f to see free keys.\n"); // or any other param ... :)
}

/**
 * @brief Key_KeynameCompletion
 */
void Key_KeynameCompletion(void (*callback)(const char *s))
{
	int i;

	for (i = 0; keynames[i].name != NULL; i++)
	{
		callback(keynames[i].name);
	}
}

/**
 * @brief Key_CompleteUnbind
 * @param[in] args
 * @param[in] argNum
 */
static void Key_CompleteUnbind(char *args, int argNum)
{
	if (argNum == 2)
	{
		// Skip "unbind "
		char *p = Com_SkipTokens(args, 1, " ");

		if (p > args)
		{
			Field_CompleteKeyname();
		}
	}
}

/**
 * @brief Key_CompleteBind
 * @param[in] args
 * @param[in] argNum
 */
static void Key_CompleteBind(char *args, int argNum)
{
	char *p;

	if (argNum == 2)
	{
		// Skip "bind "
		p = Com_SkipTokens(args, 1, " ");

		if (p > args)
		{
			Field_CompleteKeyname();
		}
	}
	else if (argNum >= 3)
	{
		// Skip "bind <key> "
		p = Com_SkipTokens(args, 2, " ");

		if (p > args)
		{
			Field_CompleteCommand(p, qtrue, qtrue);
		}
	}
}

/**
 * @brief CL_InitKeyCommands
 */
void CL_InitKeyCommands(void)
{
	// register our functions
	Cmd_AddCommand("bind", Key_Bind_f, "Binds a key.");
	Cmd_SetCommandCompletionFunc("bind", Key_CompleteBind);
	Cmd_AddCommand("unbind", Key_Unbind_f, "Unbinds a key.");
	Cmd_SetCommandCompletionFunc("unbind", Key_CompleteUnbind);
	Cmd_AddCommand("unbindall", Key_Unbindall_f, "Unbinds all keys.");
	Cmd_AddCommand("bindlist", Key_Bindlist_f, "Prints a list of all key bindings.");
}

qboolean consoleButtonWasPressed = qfalse;

qboolean CL_NumPadEvent(int key)
{
	switch (key)
	{
	case K_KP_INS:        // 0
	case K_KP_END:        // 1
	case K_KP_DOWNARROW:  // 2
	case K_KP_PGDN:       // 3
	case K_KP_LEFTARROW:  // 4
	case K_KP_5:          // 5
	case K_KP_RIGHTARROW: // 6
	case K_KP_HOME:       // 7
	case K_KP_UPARROW:    // 8
	case K_KP_PGUP:       // 9
		return qtrue;
	}

	return qfalse;
}

/**
 * @brief Called by the system for both key up and key down events
 * @param[in] key
 * @param[in] down
 * @param[in] time
 */
void CL_KeyEvent(int key, qboolean down, unsigned time)
{
	char     *kb;
	char     cmd[1024];
	qboolean bypassMenu = qfalse;
	qboolean onlybinds  = qfalse;

	if (!key)
	{
		return;
	}

	// special handling for numbers of numeric keypad & NUM pressed
	if (IN_IsNumLockDown() && down)
	{
		onlybinds = CL_NumPadEvent(key);
	}

	// update auto-repeat status and BUTTON_ANY status
	keys[key].down = down;

	if (down)
	{
		keys[key].repeats++;
		if (keys[key].repeats == 1)
		{
			anykeydown++;
		}
	}
	else
	{
		keys[key].repeats = 0;
		anykeydown--;
		if (anykeydown < 0)
		{
			anykeydown = 0;
		}
	}


	// Switch from / to fullscreen on Alt+Enter.
	if (key == K_ENTER)
	{
		if (down)
		{
			if (keys[K_LALT].down || keys[K_RALT].down)
			{
				Key_ClearKeys();
				if (Cvar_VariableIntegerValue("r_fullscreen"))
				{
					Com_Printf("Switching to windowed rendering\n");
					Cvar_Set("r_fullscreen", "0");
				}
				else
				{
					Com_Printf("Switching to fullscreen rendering\n");
					Cvar_Set("r_fullscreen", "1");
				}
				// some desktops might freeze without restarting the video subsystem
				if (Q_stristr(cls.glconfig.renderer_string, "mesa") ||
				    Q_stristr(cls.glconfig.renderer_string, "gallium") ||
				    Q_stristr(cls.glconfig.vendor_string, "nouveau") ||
				    Q_stristr(cls.glconfig.vendor_string, "mesa"))
				{
					Cbuf_ExecuteText(EXEC_APPEND, "vid_restart\n");
				}

				return;
			}
		}
	}

#ifdef __linux__
	// Minimize the game on Alt+Tab press.
	if (key == K_TAB)
	{
		if (down)
		{
			if (keys[K_LALT].down || keys[K_RALT].down)
			{
				Key_ClearStates();
				Cbuf_ExecuteText(EXEC_NOW, "minimize");
				return;
			}
		}
	}
#endif

	// console key is hardcoded, so the user can never unbind it
	if (key == CONSOLE_KEY || ((keys[K_RSHIFT].down || keys[K_LSHIFT].down) && key == K_ESCAPE))
	{
		if (!down)
		{
			return;
		}

		Con_ToggleConsole_f();

		// the console key should never be used as a char
		consoleButtonWasPressed = qtrue;
		return;
	}
	else
	{
		consoleButtonWasPressed = qfalse;
	}

	// most keys during demo playback will bring up the menu, but non-ascii
	// keys can still be used for bound actions
	if (down && (key < 128 || key == K_MOUSE1)
	    && (clc.demo.playing || cls.state == CA_CINEMATIC) && !cls.keyCatchers)
	{

		Cvar_Set("nextdemo", "");
		key = K_ESCAPE;
	}

	// escape is always handled special
	if (key == K_ESCAPE && down)
	{
		if (cls.keyCatchers & KEYCATCH_CONSOLE)
		{
			// First time you press escape it will clear the written command (reset the line)
			// if the commandline is empty then we close the console
			if (strlen(g_consoleField.buffer))
			{
				con.highlightOffset   = 0;
				g_consoleField.cursor = 0;
				Com_Memset(g_consoleField.buffer, 0, sizeof(char) * MAX_EDIT_LINE);
			}
			else
			{
				Con_ToggleConsole_f();
			}

			return;
		}

		// escape always gets out of CGAME stuff
		if (cls.keyCatchers & KEYCATCH_CGAME)
		{
			cls.keyCatchers &= ~KEYCATCH_CGAME;
			VM_Call(cgvm, CG_EVENT_HANDLING, CGAME_EVENT_NONE);

			if (clc.demo.playing)
			{
				CL_Disconnect_f();
				S_StopAllSounds();
				VM_Call(uivm, UI_SET_ACTIVE_MENU, UIMENU_MAIN);
			}

			return;
		}

		if (!(cls.keyCatchers & KEYCATCH_UI))
		{
			if (cls.state == CA_ACTIVE && !clc.demo.playing)
			{
				VM_Call(uivm, UI_SET_ACTIVE_MENU, UIMENU_INGAME);
			}
			else
			{
				CL_Disconnect_f();
				S_StopAllSounds();
				VM_Call(uivm, UI_SET_ACTIVE_MENU, UIMENU_MAIN);
			}
			return;
		}

		VM_Call(uivm, UI_KEY_EVENT, key, down);
		return;
	}

	// key up events only perform actions if the game key binding is
	// a button command (leading + sign).  These will be processed even in
	// console mode and menu mode, to keep the character from continuing
	// an action started before a mode switch.

	if (!down)
	{
		kb = keys[key].binding;
		if (kb && kb[0] == '+')
		{
			// button commands add keynum and time as parms so that multiple
			// sources can be discriminated and subframe corrected
			Com_sprintf(cmd, sizeof(cmd), "-%s %i %i\n", kb + 1, key, time);
			Cbuf_AddText(cmd);
		}

		if ((cls.keyCatchers & KEYCATCH_UI) && uivm)
		{
			if (!onlybinds || VM_Call(uivm, UI_WANTSBINDKEYS))
			{
				VM_Call(uivm, UI_KEY_EVENT, key, down);
			}
		}
		else if ((cls.keyCatchers & KEYCATCH_CGAME) && cgvm)
		{
			if (!onlybinds || VM_Call(cgvm, CG_WANTSBINDKEYS))
			{
				VM_Call(cgvm, CG_KEY_EVENT, key, down);
			}
		}

		return;
	}

	// if we just want to pass it along to game
	if (cl_bypassMouseInput && cl_bypassMouseInput->integer)
	{
		if ((key == K_MOUSE1 || key == K_MOUSE2 || key == K_MOUSE3 || key == K_MOUSE4 || key == K_MOUSE5))
		{
			if (cl_bypassMouseInput->integer == 1)
			{
				bypassMenu = qtrue;
			}
		}
		else if (((cls.keyCatchers & KEYCATCH_UI) && !UI_checkKeyExec(key)) || ((cls.keyCatchers & KEYCATCH_CGAME) && !CL_CGameCheckKeyExec(key)))
		{
			bypassMenu = qtrue;
		}
	}

	// distribute the key down event to the apropriate handler
	if (cls.keyCatchers & KEYCATCH_CONSOLE)
	{
		if (!onlybinds)
		{
			Console_Key(key);
		}
	}
	else if ((cls.keyCatchers & KEYCATCH_UI) && !bypassMenu)
	{
		if (!onlybinds || VM_Call(uivm, UI_WANTSBINDKEYS))
		{
			VM_Call(uivm, UI_KEY_EVENT, key, down);
		}
	}
	else if ((cls.keyCatchers & KEYCATCH_CGAME) && !bypassMenu)
	{
		if (cgvm)
		{
			if (!onlybinds || VM_Call(cgvm, CG_WANTSBINDKEYS))
			{
				VM_Call(cgvm, CG_KEY_EVENT, key, down);
			}
		}
	}
	else if (cls.state == CA_DISCONNECTED)
	{
		if (!onlybinds)
		{
			Console_Key(key);
		}
	}
	else
	{
		// send the bound action
		kb = keys[key].binding;
		if (!kb)
		{
			if (key >= 200)
			{
				Com_DPrintf("%s (key %i) is unbound, use controls menu to set.\n"
				            , Key_KeynumToString(key), key);
			}
		}
		else if (kb[0] == '+')
		{
			// button commands add keynum and time as parms so that multiple
			// sources can be discriminated and subframe corrected
			Com_sprintf(cmd, sizeof(cmd), "%s %i %i\n", kb, key, time);
			Cbuf_AddText(cmd);
		}
		else
		{
			// down-only command
			Cbuf_AddText(kb);
			Cbuf_AddText("\n");
		}
	}
}

/**
 * @brief Normal keyboard characters, already shifted / capslocked / etc
 * @param[in] key
 */
void CL_CharEvent(int key)
{
	// the console key should never be used as a char
	// - added uk equivalent of shift+`
	// the RIGHT way to do this would be to have certain keys disable the equivalent SE_CHAR event

	// this should be fixed in Com_EventLoop but I can't be arsed to leave this as is

	if (key == (unsigned char) '`' || key == (unsigned char) '~' || key == (unsigned char) '\xAC')
	{
		return;
	}

	// distribute the key down event to the apropriate handler
	if (cls.keyCatchers & KEYCATCH_CONSOLE)
	{
		Field_CharEvent(&g_consoleField, key);
	}
	else if (cls.keyCatchers & KEYCATCH_UI)
	{
		VM_Call(uivm, UI_KEY_EVENT, key | K_CHAR_FLAG, qtrue);
	}
	else if (cls.keyCatchers & KEYCATCH_CGAME)
	{
		VM_Call(cgvm, CG_KEY_EVENT, key | K_CHAR_FLAG, qtrue);
	}
	else if (cls.state == CA_DISCONNECTED)
	{
		Field_CharEvent(&g_consoleField, key);
	}
}

/**
 * @brief Key_ClearStates
 */
void Key_ClearStates(void)
{
	int i;

	anykeydown = 0;

	for (i = 0; i < MAX_KEYS; i++)
	{
		if (keys[i].down)
		{
			CL_KeyEvent(i, qfalse, 0);
		}
		keys[i].down    = 0;
		keys[i].repeats = 0;
	}
}

void Key_ClearKeys(void)
{
	Key_ClearStates();
	CL_ClearKeys();
}

/**
 * @brief CL_GetCatcher
 * @return
 */
int Key_GetCatcher(void)
{
	return cls.keyCatchers;
}

/**
 * @brief CL_SetCatcherVM
 * @param[in] catcher
 */
void Key_SetCatcherVM(int catcher)
{
	// Don't allow the ui module to close the console
	// console overrides everything
	if (cls.keyCatchers & KEYCATCH_CONSOLE)
	{
		cls.keyCatchers = catcher | KEYCATCH_CONSOLE;
	}
	else
	{
		cls.keyCatchers = catcher;
	}
}

/**
 * @brief CL_SetCatcherVM
 * @param[in] catcher
 */
void Key_SetCatcher(int catcher)
{
	cls.keyCatchers = catcher;
}
