/**
 * FreeRDP: A Remote Desktop Protocol Implementation
 * Input PDUs
 *
 * Copyright 2011 Marc-Andre Moreau <marcandre.moreau@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <time.h>
#include <freerdp/config.h>

#include <winpr/crt.h>
#include <winpr/assert.h>

#include <freerdp/input.h>
#include <freerdp/log.h>

#include "message.h"

#include "input.h"

#define TAG FREERDP_TAG("core")

/* Input Events */
#define INPUT_EVENT_SYNC 0x0000
#define INPUT_EVENT_SCANCODE 0x0004
#define INPUT_EVENT_UNICODE 0x0005
#define INPUT_EVENT_MOUSE 0x8001
#define INPUT_EVENT_MOUSEX 0x8002
#define INPUT_EVENT_MOUSEREL 0x8004

static const char* SyncEventFlag2Str(uint32_t flag)
{
	if (flag == KBD_SYNC_SCROLL_LOCK)
		return "SYNC_SCROLL_LOCK";
	if (flag == KBD_SYNC_NUM_LOCK)
		return "SYNC_NUM_LOCK";
	if (flag == KBD_SYNC_CAPS_LOCK)
		return "SYNC_CAPS_LOCK";
	if (flag == KBD_SYNC_KANA_LOCK)
		return "SYNC_KANA_LOCK";
	return "SYNC_UNKNOWN";
}

static const char* SyncEventFlags2Str(const char* prefix, uint32_t flags, char* buffer, size_t len)
{
	const uint32_t tflags[] = { KBD_SYNC_SCROLL_LOCK, KBD_SYNC_NUM_LOCK, KBD_SYNC_CAPS_LOCK,
		                        KBD_SYNC_KANA_LOCK };

	if (len <= 2)
		return nullptr;

	if (!winpr_str_append("{", buffer, len, ""))
		return nullptr;

	/* Strip initial symbol so we do not get duplicate separators */
	for (size_t x = 0; x < ARRAYSIZE(tflags); x++)
	{
		const uint32_t flag = tflags[x];
		if (flags & flag)
		{
			char ibuffer[64] = WINPR_C_ARRAY_INIT;
			(void)_snprintf(ibuffer, sizeof(ibuffer), "%s%s", prefix, SyncEventFlag2Str(flag));
			if (!winpr_str_append(ibuffer, &buffer[1], len - 2, "|"))
				return nullptr;
		}
	}
	if (!winpr_str_append("}", &buffer[1], len - 2, ""))
		return nullptr;

	return buffer;
}

const char* freerdp_input_keyboard_flags_string(uint32_t flags, char* buffer, size_t len)
{
	return SyncEventFlags2Str("KBD_", flags, buffer, len);
}

static void rdp_write_client_input_pdu_header(wStream* s, UINT16 number)
{
	WINPR_ASSERT(s);
	WINPR_ASSERT(Stream_GetRemainingCapacity(s) >= 4);
	Stream_Write_UINT16(s, number); /* numberEvents (2 bytes) */
	Stream_Write_UINT16(s, 0);      /* pad2Octets (2 bytes) */
}

static void rdp_write_input_event_header(wStream* s, UINT32 time, UINT16 type)
{
	WINPR_ASSERT(s);
	WINPR_ASSERT(Stream_GetRemainingCapacity(s) >= 6);
	Stream_Write_UINT32(s, time); /* eventTime (4 bytes) */
	Stream_Write_UINT16(s, type); /* messageType (2 bytes) */
}

static wStream* rdp_client_input_pdu_init(rdpRdp* rdp, UINT16 type, UINT16* sec_flags)
{
	wStream* s = rdp_data_pdu_init(rdp, sec_flags);

	if (!s)
		return nullptr;

	rdp_write_client_input_pdu_header(s, 1);
	rdp_write_input_event_header(s, 0, type);
	return s;
}

static BOOL rdp_send_client_input_pdu(rdpRdp* rdp, wStream* s, UINT16 sec_flags)
{
	WINPR_ASSERT(rdp);
	WINPR_ASSERT(rdp->mcs);
	return rdp_send_data_pdu(rdp, s, DATA_PDU_TYPE_INPUT, rdp->mcs->userId, sec_flags);
}

static void input_write_synchronize_event(wStream* s, UINT32 flags)
{
	WINPR_ASSERT(s);
	WINPR_ASSERT(Stream_GetRemainingCapacity(s) >= 6);
	Stream_Write_UINT16(s, 0);     /* pad2Octets (2 bytes) */
	Stream_Write_UINT32(s, flags); /* toggleFlags (4 bytes) */
}

static BOOL input_ensure_client_running(rdpInput* input)
{
	WINPR_ASSERT(input);
	if (freerdp_shall_disconnect_context(input->context))
	{
		WLog_WARN(TAG, "[APPLICATION BUG] input functions called after the session terminated");
		return FALSE;
	}
	return TRUE;
}

static BOOL input_send_synchronize_event(rdpInput* input, UINT32 flags)
{
	UINT16 sec_flags = 0;

	if (!input || !input->context)
		return FALSE;

	rdpRdp* rdp = input->context->rdp;

	if (!input_ensure_client_running(input))
		return FALSE;

	wStream* s = rdp_client_input_pdu_init(rdp, INPUT_EVENT_SYNC, &sec_flags);

	if (!s)
		return FALSE;

	input_write_synchronize_event(s, flags);
	return rdp_send_client_input_pdu(rdp, s, sec_flags);
}

static void input_write_keyboard_event(wStream* s, UINT16 flags, UINT16 code)
{
	WINPR_ASSERT(s);
	WINPR_ASSERT(code <= UINT8_MAX);

	Stream_Write_UINT16(s, flags); /* keyboardFlags (2 bytes) */
	Stream_Write_UINT16(s, code);  /* keyCode (2 bytes) */
	Stream_Write_UINT16(s, 0);     /* pad2Octets (2 bytes) */
}

static BOOL input_send_keyboard_event(rdpInput* input, UINT16 flags, UINT8 code)
{
	UINT16 sec_flags = 0;
	wStream* s = nullptr;
	rdpRdp* rdp = nullptr;

	if (!input || !input->context)
		return FALSE;

	rdp = input->context->rdp;

	if (!input_ensure_client_running(input))
		return FALSE;

	s = rdp_client_input_pdu_init(rdp, INPUT_EVENT_SCANCODE, &sec_flags);

	if (!s)
		return FALSE;

	input_write_keyboard_event(s, flags, code);
	return rdp_send_client_input_pdu(rdp, s, sec_flags);
}

static void input_write_unicode_keyboard_event(wStream* s, UINT16 flags, UINT16 code)
{
	Stream_Write_UINT16(s, flags); /* keyboardFlags (2 bytes) */
	Stream_Write_UINT16(s, code);  /* unicodeCode (2 bytes) */
	Stream_Write_UINT16(s, 0);     /* pad2Octets (2 bytes) */
}

static BOOL input_send_unicode_keyboard_event(rdpInput* input, UINT16 flags, UINT16 code)
{
	UINT16 sec_flags = 0;
	wStream* s = nullptr;
	rdpRdp* rdp = nullptr;

	if (!input || !input->context)
		return FALSE;

	if (!input_ensure_client_running(input))
		return FALSE;

	if (!freerdp_settings_get_bool(input->context->settings, FreeRDP_UnicodeInput))
	{
		WLog_WARN(TAG, "Unicode input not supported by server.");
		return FALSE;
	}

	rdp = input->context->rdp;
	s = rdp_client_input_pdu_init(rdp, INPUT_EVENT_UNICODE, &sec_flags);

	if (!s)
		return FALSE;

	input_write_unicode_keyboard_event(s, flags, code);
	return rdp_send_client_input_pdu(rdp, s, sec_flags);
}

static void input_write_mouse_event(wStream* s, UINT16 flags, UINT16 x, UINT16 y)
{
	Stream_Write_UINT16(s, flags); /* pointerFlags (2 bytes) */
	Stream_Write_UINT16(s, x);     /* xPos (2 bytes) */
	Stream_Write_UINT16(s, y);     /* yPos (2 bytes) */
}

static BOOL input_send_mouse_event(rdpInput* input, UINT16 flags, UINT16 x, UINT16 y)
{
	UINT16 sec_flags = 0;

	if (!input || !input->context || !input->context->settings)
		return FALSE;

	rdpRdp* rdp = input->context->rdp;

	if (!input_ensure_client_running(input))
		return FALSE;

	if (!freerdp_settings_get_bool(input->context->settings, FreeRDP_HasHorizontalWheel))
	{
		if (flags & PTR_FLAGS_HWHEEL)
		{
			WLog_WARN(TAG,
			          "skip mouse event %" PRIu16 "x%" PRIu16 " flags=0x%04" PRIX16
			          ", no horizontal mouse wheel supported",
			          x, y, flags);
			return TRUE;
		}
	}

	wStream* s = rdp_client_input_pdu_init(rdp, INPUT_EVENT_MOUSE, &sec_flags);

	if (!s)
		return FALSE;

	input_write_mouse_event(s, flags, x, y);
	return rdp_send_client_input_pdu(rdp, s, sec_flags);
}

static BOOL input_send_relmouse_event(rdpInput* input, UINT16 flags, INT16 xDelta, INT16 yDelta)
{
	UINT16 sec_flags = 0;
	wStream* s = nullptr;
	rdpRdp* rdp = nullptr;

	if (!input || !input->context || !input->context->settings)
		return FALSE;

	rdp = input->context->rdp;

	if (!input_ensure_client_running(input))
		return FALSE;

	if (!freerdp_settings_get_bool(input->context->settings, FreeRDP_HasRelativeMouseEvent))
	{
		WLog_ERR(TAG, "Sending relative mouse event, but no support for that");
		return FALSE;
	}

	s = rdp_client_input_pdu_init(rdp, INPUT_EVENT_MOUSEREL, &sec_flags);

	if (!s)
		return FALSE;

	Stream_Write_UINT16(s, flags); /* pointerFlags (2 bytes) */
	Stream_Write_INT16(s, xDelta); /* xDelta (2 bytes) */
	Stream_Write_INT16(s, yDelta); /* yDelta (2 bytes) */

	return rdp_send_client_input_pdu(rdp, s, sec_flags);
}

static void input_write_extended_mouse_event(wStream* s, UINT16 flags, UINT16 x, UINT16 y)
{
	Stream_Write_UINT16(s, flags); /* pointerFlags (2 bytes) */
	Stream_Write_UINT16(s, x);     /* xPos (2 bytes) */
	Stream_Write_UINT16(s, y);     /* yPos (2 bytes) */
}

static BOOL input_send_extended_mouse_event(rdpInput* input, UINT16 flags, UINT16 x, UINT16 y)
{
	UINT16 sec_flags = 0;

	WINPR_ASSERT(input);
	WINPR_ASSERT(input->context);
	WINPR_ASSERT(input->context->settings);

	rdpRdp* rdp = input->context->rdp;
	WINPR_ASSERT(rdp);

	if (!input_ensure_client_running(input))
		return FALSE;

	if (!freerdp_settings_get_bool(input->context->settings, FreeRDP_HasExtendedMouseEvent))
	{
		WLog_WARN(TAG,
		          "skip extended mouse event %" PRIu16 "x%" PRIu16 " flags=0x%04" PRIX16
		          ", no extended mouse events supported",
		          x, y, flags);
		return TRUE;
	}

	wStream* s = rdp_client_input_pdu_init(rdp, INPUT_EVENT_MOUSEX, &sec_flags);

	if (!s)
		return FALSE;

	input_write_extended_mouse_event(s, flags, x, y);
	return rdp_send_client_input_pdu(rdp, s, sec_flags);
}

static BOOL input_send_focus_in_event(rdpInput* input, UINT16 toggleStates)
{
	/* send a tab up like mstsc.exe */
	if (!input_send_keyboard_event(input, KBD_FLAGS_RELEASE, 0x0f))
		return FALSE;

	/* send the toggle key states */
	if (!input_send_synchronize_event(input, (toggleStates & 0x1F)))
		return FALSE;

	/* send another tab up like mstsc.exe */
	return input_send_keyboard_event(input, KBD_FLAGS_RELEASE, 0x0f);
}

static BOOL input_send_keyboard_pause_event(rdpInput* input)
{
	/* In ancient days, pause-down without control sent E1 1D 45 E1 9D C5,
	 * and pause-up sent nothing.  However, reverse engineering mstsc shows
	 * it sending the following sequence:
	 */

	/* Control down (0x1D) */
	if (!input_send_keyboard_event(input, KBD_FLAGS_EXTENDED1,
	                               RDP_SCANCODE_CODE(RDP_SCANCODE_LCONTROL)))
		return FALSE;

	/* Numlock down (0x45) */
	if (!input_send_keyboard_event(input, 0, RDP_SCANCODE_CODE(RDP_SCANCODE_NUMLOCK)))
		return FALSE;

	/* Control up (0x1D) */
	if (!input_send_keyboard_event(input, KBD_FLAGS_RELEASE | KBD_FLAGS_EXTENDED1,
	                               RDP_SCANCODE_CODE(RDP_SCANCODE_LCONTROL)))
		return FALSE;

	/* Numlock up (0x45) */
	return input_send_keyboard_event(input, KBD_FLAGS_RELEASE,
	                                 RDP_SCANCODE_CODE(RDP_SCANCODE_NUMLOCK));
}

static BOOL input_send_fastpath_synchronize_event(rdpInput* input, UINT32 flags)
{
	UINT16 sec_flags = 0;
	wStream* s = nullptr;
	rdpRdp* rdp = nullptr;

	WINPR_ASSERT(input);
	WINPR_ASSERT(input->context);

	rdp = input->context->rdp;
	WINPR_ASSERT(rdp);

	if (!input_ensure_client_running(input))
		return FALSE;

	/* The FastPath Synchronization eventFlags has identical values as SlowPath */
	s = fastpath_input_pdu_init(rdp->fastpath, (BYTE)flags, FASTPATH_INPUT_EVENT_SYNC, &sec_flags);

	if (!s)
		return FALSE;

	return fastpath_send_input_pdu(rdp->fastpath, s, sec_flags);
}

static BOOL input_send_fastpath_keyboard_event(rdpInput* input, UINT16 flags, UINT8 code)
{
	UINT16 sec_flags = 0;
	wStream* s = nullptr;
	BYTE eventFlags = 0;
	rdpRdp* rdp = nullptr;

	WINPR_ASSERT(input);
	WINPR_ASSERT(input->context);

	rdp = input->context->rdp;
	WINPR_ASSERT(rdp);

	if (!input_ensure_client_running(input))
		return FALSE;

	eventFlags |= (flags & KBD_FLAGS_RELEASE) ? FASTPATH_INPUT_KBDFLAGS_RELEASE : 0;
	eventFlags |= (flags & KBD_FLAGS_EXTENDED) ? FASTPATH_INPUT_KBDFLAGS_EXTENDED : 0;
	eventFlags |= (flags & KBD_FLAGS_EXTENDED1) ? FASTPATH_INPUT_KBDFLAGS_PREFIX_E1 : 0;
	s = fastpath_input_pdu_init(rdp->fastpath, eventFlags, FASTPATH_INPUT_EVENT_SCANCODE,
	                            &sec_flags);

	if (!s)
		return FALSE;

	WINPR_ASSERT(code <= UINT8_MAX);
	Stream_Write_UINT8(s, code); /* keyCode (1 byte) */
	return fastpath_send_input_pdu(rdp->fastpath, s, sec_flags);
}

static BOOL input_send_fastpath_unicode_keyboard_event(rdpInput* input, UINT16 flags, UINT16 code)
{
	UINT16 sec_flags = 0;
	wStream* s = nullptr;
	BYTE eventFlags = 0;
	rdpRdp* rdp = nullptr;

	WINPR_ASSERT(input);
	WINPR_ASSERT(input->context);
	WINPR_ASSERT(input->context->settings);

	rdp = input->context->rdp;
	WINPR_ASSERT(rdp);

	if (!input_ensure_client_running(input))
		return FALSE;

	if (!freerdp_settings_get_bool(input->context->settings, FreeRDP_UnicodeInput))
	{
		WLog_WARN(TAG, "Unicode input not supported by server.");
		return FALSE;
	}

	eventFlags |= (flags & KBD_FLAGS_RELEASE) ? FASTPATH_INPUT_KBDFLAGS_RELEASE : 0;
	s = fastpath_input_pdu_init(rdp->fastpath, eventFlags, FASTPATH_INPUT_EVENT_UNICODE,
	                            &sec_flags);

	if (!s)
		return FALSE;

	Stream_Write_UINT16(s, code); /* unicodeCode (2 bytes) */
	return fastpath_send_input_pdu(rdp->fastpath, s, sec_flags);
}

static BOOL input_send_fastpath_mouse_event(rdpInput* input, UINT16 flags, UINT16 x, UINT16 y)
{
	UINT16 sec_flags = 0;
	wStream* s = nullptr;
	rdpRdp* rdp = nullptr;

	WINPR_ASSERT(input);
	WINPR_ASSERT(input->context);
	WINPR_ASSERT(input->context->settings);

	rdp = input->context->rdp;
	WINPR_ASSERT(rdp);

	if (!input_ensure_client_running(input))
		return FALSE;

	if (!freerdp_settings_get_bool(input->context->settings, FreeRDP_HasHorizontalWheel))
	{
		if (flags & PTR_FLAGS_HWHEEL)
		{
			WLog_WARN(TAG,
			          "skip mouse event %" PRIu16 "x%" PRIu16 " flags=0x%04" PRIX16
			          ", no horizontal mouse wheel supported",
			          x, y, flags);
			return TRUE;
		}
	}

	s = fastpath_input_pdu_init(rdp->fastpath, 0, FASTPATH_INPUT_EVENT_MOUSE, &sec_flags);

	if (!s)
		return FALSE;

	input_write_mouse_event(s, flags, x, y);
	return fastpath_send_input_pdu(rdp->fastpath, s, sec_flags);
}

static BOOL input_send_fastpath_extended_mouse_event(rdpInput* input, UINT16 flags, UINT16 x,
                                                     UINT16 y)
{
	UINT16 sec_flags = 0;
	wStream* s = nullptr;
	rdpRdp* rdp = nullptr;

	WINPR_ASSERT(input);
	WINPR_ASSERT(input->context);

	rdp = input->context->rdp;
	WINPR_ASSERT(rdp);

	if (!input_ensure_client_running(input))
		return FALSE;

	if (!freerdp_settings_get_bool(input->context->settings, FreeRDP_HasExtendedMouseEvent))
	{
		WLog_WARN(TAG,
		          "skip extended mouse event %" PRIu16 "x%" PRIu16 " flags=0x%04" PRIX16
		          ", no extended mouse events supported",
		          x, y, flags);
		return TRUE;
	}

	s = fastpath_input_pdu_init(rdp->fastpath, 0, FASTPATH_INPUT_EVENT_MOUSEX, &sec_flags);

	if (!s)
		return FALSE;

	input_write_extended_mouse_event(s, flags, x, y);
	return fastpath_send_input_pdu(rdp->fastpath, s, sec_flags);
}

static BOOL input_send_fastpath_relmouse_event(rdpInput* input, UINT16 flags, INT16 xDelta,
                                               INT16 yDelta)
{
	UINT16 sec_flags = 0;
	wStream* s = nullptr;
	rdpRdp* rdp = nullptr;

	WINPR_ASSERT(input);
	WINPR_ASSERT(input->context);
	WINPR_ASSERT(input->context->settings);

	rdp = input->context->rdp;
	WINPR_ASSERT(rdp);

	if (!input_ensure_client_running(input))
		return FALSE;

	if (!freerdp_settings_get_bool(input->context->settings, FreeRDP_HasRelativeMouseEvent))
	{
		WLog_ERR(TAG, "Sending relative fastpath mouse event, but no support for that announced");
		return FALSE;
	}

	s = fastpath_input_pdu_init(rdp->fastpath, 0, TS_FP_RELPOINTER_EVENT, &sec_flags);

	if (!s)
		return FALSE;

	Stream_Write_UINT16(s, flags); /* pointerFlags (2 bytes) */
	Stream_Write_INT16(s, xDelta); /* xDelta (2 bytes) */
	Stream_Write_INT16(s, yDelta); /* yDelta (2 bytes) */
	return fastpath_send_input_pdu(rdp->fastpath, s, sec_flags);
}

static BOOL input_send_fastpath_qoe_event(rdpInput* input, UINT32 timestampMS)
{
	WINPR_ASSERT(input);
	WINPR_ASSERT(input->context);
	WINPR_ASSERT(input->context->settings);

	rdpRdp* rdp = input->context->rdp;
	WINPR_ASSERT(rdp);

	if (!input_ensure_client_running(input))
		return FALSE;

	if (!freerdp_settings_get_bool(input->context->settings, FreeRDP_HasQoeEvent))
	{
		WLog_ERR(TAG, "Sending qoe event, but no support for that announced");
		return FALSE;
	}

	UINT16 sec_flags = 0;
	wStream* s = fastpath_input_pdu_init(rdp->fastpath, 0, TS_FP_QOETIMESTAMP_EVENT, &sec_flags);

	if (!s)
		return FALSE;

	if (!Stream_EnsureRemainingCapacity(s, 4))
	{
		Stream_Free(s, TRUE);
		return FALSE;
	}

	Stream_Write_UINT32(s, timestampMS);
	return fastpath_send_input_pdu(rdp->fastpath, s, sec_flags);
}

static BOOL input_send_fastpath_focus_in_event(rdpInput* input, UINT16 toggleStates)
{
	UINT16 sec_flags = 0;
	wStream* s = nullptr;
	BYTE eventFlags = 0;
	rdpRdp* rdp = nullptr;

	WINPR_ASSERT(input);
	WINPR_ASSERT(input->context);

	rdp = input->context->rdp;
	WINPR_ASSERT(rdp);

	if (!input_ensure_client_running(input))
		return FALSE;

	s = fastpath_input_pdu_init_header(rdp->fastpath, &sec_flags);

	if (!s)
		return FALSE;

	/* send a tab up like mstsc.exe */
	eventFlags = FASTPATH_INPUT_KBDFLAGS_RELEASE | FASTPATH_INPUT_EVENT_SCANCODE << 5;
	Stream_Write_UINT8(s, eventFlags); /* Key Release event (1 byte) */
	Stream_Write_UINT8(s, 0x0f);       /* keyCode (1 byte) */
	/* send the toggle key states */
	eventFlags = (toggleStates & 0x1F) | FASTPATH_INPUT_EVENT_SYNC << 5;
	Stream_Write_UINT8(s, eventFlags); /* toggle state (1 byte) */
	/* send another tab up like mstsc.exe */
	eventFlags = FASTPATH_INPUT_KBDFLAGS_RELEASE | FASTPATH_INPUT_EVENT_SCANCODE << 5;
	Stream_Write_UINT8(s, eventFlags); /* Key Release event (1 byte) */
	Stream_Write_UINT8(s, 0x0f);       /* keyCode (1 byte) */
	return fastpath_send_multiple_input_pdu(rdp->fastpath, s, 3, sec_flags);
}

static BOOL input_send_fastpath_keyboard_pause_event(rdpInput* input)
{
	/* In ancient days, pause-down without control sent E1 1D 45 E1 9D C5,
	 * and pause-up sent nothing.  However, reverse engineering mstsc shows
	 * it sending the following sequence:
	 */
	UINT16 sec_flags = 0;
	wStream* s = nullptr;
	const BYTE keyDownEvent = FASTPATH_INPUT_EVENT_SCANCODE << 5;
	const BYTE keyUpEvent = (FASTPATH_INPUT_EVENT_SCANCODE << 5) | FASTPATH_INPUT_KBDFLAGS_RELEASE;
	rdpRdp* rdp = nullptr;

	WINPR_ASSERT(input);
	WINPR_ASSERT(input->context);

	rdp = input->context->rdp;
	WINPR_ASSERT(rdp);

	if (!input_ensure_client_running(input))
		return FALSE;

	s = fastpath_input_pdu_init_header(rdp->fastpath, &sec_flags);

	if (!s)
		return FALSE;

	/* Control down (0x1D) */
	Stream_Write_UINT8(s, keyDownEvent | FASTPATH_INPUT_KBDFLAGS_PREFIX_E1);
	Stream_Write_UINT8(s, RDP_SCANCODE_CODE(RDP_SCANCODE_LCONTROL));
	/* Numlock down (0x45) */
	Stream_Write_UINT8(s, keyDownEvent);
	Stream_Write_UINT8(s, RDP_SCANCODE_CODE(RDP_SCANCODE_NUMLOCK));
	/* Control up (0x1D) */
	Stream_Write_UINT8(s, keyUpEvent | FASTPATH_INPUT_KBDFLAGS_PREFIX_E1);
	Stream_Write_UINT8(s, RDP_SCANCODE_CODE(RDP_SCANCODE_LCONTROL));
	/* Numlock down (0x45) */
	Stream_Write_UINT8(s, keyUpEvent);
	Stream_Write_UINT8(s, RDP_SCANCODE_CODE(RDP_SCANCODE_NUMLOCK));
	return fastpath_send_multiple_input_pdu(rdp->fastpath, s, 4, sec_flags);
}

static BOOL input_recv_sync_event(rdpInput* input, wStream* s)
{
	UINT32 toggleFlags = 0;

	WINPR_ASSERT(input);
	WINPR_ASSERT(s);

	if (!Stream_CheckAndLogRequiredLength(TAG, s, 6))
		return FALSE;

	Stream_Seek(s, 2);                  /* pad2Octets (2 bytes) */
	Stream_Read_UINT32(s, toggleFlags); /* toggleFlags (4 bytes) */
	return IFCALLRESULT(TRUE, input->SynchronizeEvent, input, toggleFlags);
}

static BOOL input_recv_keyboard_event(rdpInput* input, wStream* s)
{
	UINT16 keyboardFlags = 0;
	UINT16 keyCode = 0;

	WINPR_ASSERT(input);
	WINPR_ASSERT(s);

	if (!Stream_CheckAndLogRequiredLength(TAG, s, 6))
		return FALSE;

	Stream_Read_UINT16(s, keyboardFlags); /* keyboardFlags (2 bytes) */
	Stream_Read_UINT16(s, keyCode);       /* keyCode (2 bytes) */
	Stream_Seek(s, 2);                    /* pad2Octets (2 bytes) */

	if (keyboardFlags & KBD_FLAGS_RELEASE)
		keyboardFlags &= ~KBD_FLAGS_DOWN;

	if (keyCode & 0xFF00)
		WLog_WARN(TAG,
		          "Problematic [MS-RDPBCGR] 2.2.8.1.1.3.1.1.1 Keyboard Event (TS_KEYBOARD_EVENT) "
		          "keyCode=0x%04" PRIx16
		          ", high byte values should be sent in keyboardFlags field, ignoring.",
		          keyCode);
	return IFCALLRESULT(TRUE, input->KeyboardEvent, input, keyboardFlags, keyCode & 0xFF);
}

static BOOL input_recv_unicode_keyboard_event(rdpInput* input, wStream* s)
{
	UINT16 keyboardFlags = 0;
	UINT16 unicodeCode = 0;

	WINPR_ASSERT(input);
	WINPR_ASSERT(s);

	if (!Stream_CheckAndLogRequiredLength(TAG, s, 6))
		return FALSE;

	Stream_Read_UINT16(s, keyboardFlags); /* keyboardFlags (2 bytes) */
	Stream_Read_UINT16(s, unicodeCode);   /* unicodeCode (2 bytes) */
	Stream_Seek(s, 2);                    /* pad2Octets (2 bytes) */

	/* "fix" keyboardFlags - see comment in input_recv_keyboard_event() */

	if (keyboardFlags & KBD_FLAGS_RELEASE)
		keyboardFlags &= ~KBD_FLAGS_DOWN;

	return IFCALLRESULT(TRUE, input->UnicodeKeyboardEvent, input, keyboardFlags, unicodeCode);
}

static BOOL input_recv_mouse_event(rdpInput* input, wStream* s)
{
	UINT16 pointerFlags = 0;
	UINT16 xPos = 0;
	UINT16 yPos = 0;

	WINPR_ASSERT(input);
	WINPR_ASSERT(s);

	if (!Stream_CheckAndLogRequiredLength(TAG, s, 6))
		return FALSE;

	Stream_Read_UINT16(s, pointerFlags); /* pointerFlags (2 bytes) */
	Stream_Read_UINT16(s, xPos);         /* xPos (2 bytes) */
	Stream_Read_UINT16(s, yPos);         /* yPos (2 bytes) */
	return IFCALLRESULT(TRUE, input->MouseEvent, input, pointerFlags, xPos, yPos);
}

static BOOL input_recv_relmouse_event(rdpInput* input, wStream* s)
{
	UINT16 pointerFlags = 0;
	INT16 xDelta = 0;
	INT16 yDelta = 0;

	WINPR_ASSERT(input);
	WINPR_ASSERT(s);

	if (!Stream_CheckAndLogRequiredLength(TAG, s, 6))
		return FALSE;

	Stream_Read_UINT16(s, pointerFlags); /* pointerFlags (2 bytes) */
	Stream_Read_INT16(s, xDelta);        /* xPos (2 bytes) */
	Stream_Read_INT16(s, yDelta);        /* yPos (2 bytes) */

	if (!freerdp_settings_get_bool(input->context->settings, FreeRDP_HasRelativeMouseEvent))
	{
		WLog_ERR(TAG,
		         "Received relative mouse event(flags=0x%04" PRIx16 ", xPos=%" PRId16
		         ", yPos=%" PRId16 "), but we did not announce support for that",
		         pointerFlags, xDelta, yDelta);
		return FALSE;
	}

	return IFCALLRESULT(TRUE, input->RelMouseEvent, input, pointerFlags, xDelta, yDelta);
}

static BOOL input_recv_extended_mouse_event(rdpInput* input, wStream* s)
{
	UINT16 pointerFlags = 0;
	UINT16 xPos = 0;
	UINT16 yPos = 0;

	WINPR_ASSERT(input);
	WINPR_ASSERT(s);

	if (!Stream_CheckAndLogRequiredLength(TAG, s, 6))
		return FALSE;

	Stream_Read_UINT16(s, pointerFlags); /* pointerFlags (2 bytes) */
	Stream_Read_UINT16(s, xPos);         /* xPos (2 bytes) */
	Stream_Read_UINT16(s, yPos);         /* yPos (2 bytes) */

	if (!freerdp_settings_get_bool(input->context->settings, FreeRDP_HasExtendedMouseEvent))
	{
		WLog_ERR(TAG,
		         "Received extended mouse event(flags=0x%04" PRIx16 ", xPos=%" PRIu16
		         ", yPos=%" PRIu16 "), but we did not announce support for that",
		         pointerFlags, xPos, yPos);
		return FALSE;
	}

	return IFCALLRESULT(TRUE, input->ExtendedMouseEvent, input, pointerFlags, xPos, yPos);
}

static BOOL input_recv_event(rdpInput* input, wStream* s)
{
	UINT16 messageType = 0;

	WINPR_ASSERT(input);
	WINPR_ASSERT(s);

	if (!Stream_CheckAndLogRequiredLength(TAG, s, 6))
		return FALSE;

	Stream_Seek(s, 4);                  /* eventTime (4 bytes), ignored by the server */
	Stream_Read_UINT16(s, messageType); /* messageType (2 bytes) */

	switch (messageType)
	{
		case INPUT_EVENT_SYNC:
			if (!input_recv_sync_event(input, s))
				return FALSE;

			break;

		case INPUT_EVENT_SCANCODE:
			if (!input_recv_keyboard_event(input, s))
				return FALSE;

			break;

		case INPUT_EVENT_UNICODE:
			if (!input_recv_unicode_keyboard_event(input, s))
				return FALSE;

			break;

		case INPUT_EVENT_MOUSE:
			if (!input_recv_mouse_event(input, s))
				return FALSE;

			break;

		case INPUT_EVENT_MOUSEX:
			if (!input_recv_extended_mouse_event(input, s))
				return FALSE;

			break;

		case INPUT_EVENT_MOUSEREL:
			if (!input_recv_relmouse_event(input, s))
				return FALSE;

			break;

		default:
			WLog_ERR(TAG, "Unknown messageType %" PRIu16 "", messageType);
			/* Each input event uses 6 bytes. */
			Stream_Seek(s, 6);
			break;
	}

	return TRUE;
}

BOOL input_recv(rdpInput* input, wStream* s)
{
	UINT16 numberEvents = 0;

	WINPR_ASSERT(input);
	WINPR_ASSERT(s);

	if (!Stream_CheckAndLogRequiredLength(TAG, s, 4))
		return FALSE;

	Stream_Read_UINT16(s, numberEvents); /* numberEvents (2 bytes) */
	Stream_Seek(s, 2);                   /* pad2Octets (2 bytes) */

	/* Each input event uses 6 exactly bytes. */
	if (!Stream_CheckAndLogRequiredLengthOfSize(TAG, s, numberEvents, 6ull))
		return FALSE;

	for (UINT16 i = 0; i < numberEvents; i++)
	{
		if (!input_recv_event(input, s))
			return FALSE;
	}

	return TRUE;
}

BOOL input_register_client_callbacks(rdpInput* input)
{
	rdpSettings* settings = nullptr;

	if (!input->context)
		return FALSE;

	settings = input->context->settings;

	if (!settings)
		return FALSE;

	if (freerdp_settings_get_bool(settings, FreeRDP_FastPathInput))
	{
		input->SynchronizeEvent = input_send_fastpath_synchronize_event;
		input->KeyboardEvent = input_send_fastpath_keyboard_event;
		input->KeyboardPauseEvent = input_send_fastpath_keyboard_pause_event;
		input->UnicodeKeyboardEvent = input_send_fastpath_unicode_keyboard_event;
		input->MouseEvent = input_send_fastpath_mouse_event;
		input->RelMouseEvent = input_send_fastpath_relmouse_event;
		input->ExtendedMouseEvent = input_send_fastpath_extended_mouse_event;
		input->FocusInEvent = input_send_fastpath_focus_in_event;
		input->QoEEvent = input_send_fastpath_qoe_event;
	}
	else
	{
		input->SynchronizeEvent = input_send_synchronize_event;
		input->KeyboardEvent = input_send_keyboard_event;
		input->KeyboardPauseEvent = input_send_keyboard_pause_event;
		input->UnicodeKeyboardEvent = input_send_unicode_keyboard_event;
		input->MouseEvent = input_send_mouse_event;
		input->RelMouseEvent = input_send_relmouse_event;
		input->ExtendedMouseEvent = input_send_extended_mouse_event;
		input->FocusInEvent = input_send_focus_in_event;
	}

	return TRUE;
}

/* Save last input timestamp and/or mouse position in prevent-session-lock mode */
static BOOL input_update_last_event(rdpInput* input, BOOL mouse, UINT16 x, UINT16 y)
{
	rdp_input_internal* in = input_cast(input);

	WINPR_ASSERT(input);
	WINPR_ASSERT(input->context);

	if (freerdp_settings_get_uint32(input->context->settings, FreeRDP_FakeMouseMotionInterval) > 0)
	{
		const time_t now = time(nullptr);
		in->lastInputTimestamp = WINPR_ASSERTING_INT_CAST(UINT64, now);

		if (mouse)
		{
			in->lastX = x;
			in->lastY = y;
		}
	}
	return TRUE;
}

WINPR_ATTR_NODISCARD
static const char* boolstr(bool value)
{
	if (value)
		return "true";
	return "false";
}

WINPR_ATTR_NODISCARD
WINPR_ATTR_FORMAT_ARG(2, 3)
static BOOL input_skip(const rdpInput* input, WINPR_FORMAT_ARG const char* fmt, ...)
{
	WINPR_ASSERT(input);

	const rdpContext* context = input->context;
	WINPR_ASSERT(context);

	const rdp_input_internal* in = (const rdp_input_internal*)input;
	WINPR_ASSERT(in);

	const BOOL suspended = freerdp_settings_get_bool(context->settings, FreeRDP_SuspendInput);
	const BOOL connected = freerdp_is_active_state(context);
	const DWORD loglevel = WLOG_DEBUG;

	if (WLog_IsLevelActive(in->log, loglevel))
	{
		char buffer[128] = WINPR_C_ARRAY_INIT;

		va_list args = WINPR_C_ARRAY_INIT;
		va_start(args, fmt);
		(void)vsnprintf(buffer, sizeof(buffer), fmt, args);
		va_end(args);

		WLog_Print(in->log, loglevel, "[connected=%s, suspended=%s] %s", boolstr(connected),
		           boolstr(suspended), buffer);
	}

	return suspended || !connected;
}

BOOL freerdp_input_send_synchronize_event(rdpInput* input, UINT32 flags)
{
	if (!input || !input->context)
		return FALSE;

	char buffer[128] = WINPR_C_ARRAY_INIT;
	if (input_skip(input, "Keyboard {Sync} [%s]",
	               freerdp_input_keyboard_flags_string(flags, buffer, sizeof(buffer))))
		return TRUE;

	return IFCALLRESULT(TRUE, input->SynchronizeEvent, input, flags);
}

BOOL freerdp_input_send_keyboard_event(rdpInput* input, UINT16 flags, UINT8 code)
{
	if (!input || !input->context)
		return FALSE;

	char buffer[128] = WINPR_C_ARRAY_INIT;
	if (input_skip(input, "Keyboard {Event} [%s]",
	               freerdp_input_keypress_flags_string(flags, buffer, sizeof(buffer))))
		return TRUE;

	input_update_last_event(input, FALSE, 0, 0);

	return IFCALLRESULT(TRUE, input->KeyboardEvent, input, flags, code);
}

BOOL freerdp_input_send_keyboard_event_ex(rdpInput* input, BOOL down, BOOL repeat,
                                          UINT32 rdp_scancode)
{
	UINT16 flags = (RDP_SCANCODE_EXTENDED(rdp_scancode) ? KBD_FLAGS_EXTENDED : 0);
	if (down && repeat)
		flags |= KBD_FLAGS_DOWN;
	else if (!down)
		flags |= KBD_FLAGS_RELEASE;

	return freerdp_input_send_keyboard_event(input, flags, RDP_SCANCODE_CODE(rdp_scancode));
}

BOOL freerdp_input_send_unicode_keyboard_event(rdpInput* input, UINT16 flags, UINT16 code)
{
	if (!input || !input->context)
		return FALSE;

	char buffer[128] = WINPR_C_ARRAY_INIT;
	if (input_skip(input, "Keyboard {Unicode} [%s]",
	               freerdp_input_keypress_flags_string(flags, buffer, ARRAYSIZE(buffer))))
		return TRUE;

	input_update_last_event(input, FALSE, 0, 0);

	return IFCALLRESULT(TRUE, input->UnicodeKeyboardEvent, input, flags, code);
}

BOOL freerdp_input_send_mouse_event(rdpInput* input, UINT16 flags, UINT16 x, UINT16 y)
{
	if (!input || !input->context)
		return FALSE;

	char buffer[128] = WINPR_C_ARRAY_INIT;
	if (input_skip(input, "Mouse {Event} [%s]",
	               freerdp_input_mouse_flags_string(flags, false, buffer, ARRAYSIZE(buffer))))
		return TRUE;

	input_update_last_event(
	    input, flags & (PTR_FLAGS_MOVE | PTR_FLAGS_BUTTON1 | PTR_FLAGS_BUTTON2 | PTR_FLAGS_BUTTON3),
	    x, y);

	return IFCALLRESULT(TRUE, input->MouseEvent, input, flags, x, y);
}

BOOL freerdp_input_send_rel_mouse_event(rdpInput* input, UINT16 flags, INT16 xDelta, INT16 yDelta)
{
	if (!input || !input->context)
		return FALSE;

	char buffer[128] = WINPR_C_ARRAY_INIT;
	if (input_skip(input, "Mouse {RelativeEvent} [%s]",
	               freerdp_input_mouse_flags_string(flags, true, buffer, ARRAYSIZE(buffer))))
		return TRUE;

	return IFCALLRESULT(TRUE, input->RelMouseEvent, input, flags, xDelta, yDelta);
}

BOOL freerdp_input_send_qoe_timestamp(rdpInput* input, UINT32 timestampMS)
{
	if (!input || !input->context)
		return FALSE;

	return IFCALLRESULT(TRUE, input->QoEEvent, input, timestampMS);
}

BOOL freerdp_input_send_extended_mouse_event(rdpInput* input, UINT16 flags, UINT16 x, UINT16 y)
{
	if (!input || !input->context)
		return FALSE;

	char buffer[128] = WINPR_C_ARRAY_INIT;
	if (input_skip(input, "Mouse {ExtendedEvent} %s",
	               freerdp_input_mouse_flags_string(flags, true, buffer, ARRAYSIZE(buffer))))
		return TRUE;

	input_update_last_event(input, TRUE, x, y);

	return IFCALLRESULT(TRUE, input->ExtendedMouseEvent, input, flags, x, y);
}

BOOL freerdp_input_send_focus_in_event(rdpInput* input, UINT16 toggleStates)
{
	if (!input || !input->context)
		return FALSE;

	char buffer[128] = WINPR_C_ARRAY_INIT;
	if (input_skip(input, "Keyboard {FocusIn} [%s]",
	               freerdp_input_keyboard_flags_string(toggleStates, buffer, sizeof(buffer))))
		return TRUE;

	return IFCALLRESULT(TRUE, input->FocusInEvent, input, toggleStates);
}

BOOL freerdp_input_send_keyboard_pause_event(rdpInput* input)
{
	if (!input || !input->context)
		return FALSE;

	if (input_skip(input, "Keyboard {Pause}"))
		return TRUE;

	return IFCALLRESULT(TRUE, input->KeyboardPauseEvent, input);
}

int input_process_events(rdpInput* input)
{
	if (!input)
		return FALSE;

	return input_message_queue_process_pending_messages(input);
}

static void input_free_queued_message(void* obj)
{
	wMessage* msg = (wMessage*)obj;
	input_message_queue_free_message(msg);
}

rdpInput* input_new(rdpRdp* rdp)
{
	const wObject cb = { nullptr, nullptr, nullptr, input_free_queued_message, nullptr };
	rdp_input_internal* input = (rdp_input_internal*)calloc(1, sizeof(rdp_input_internal));

	WINPR_UNUSED(rdp);

	if (!input)
		return nullptr;

	input->common.context = rdp->context;
	input->queue = MessageQueue_New(&cb);
	input->log = WLog_Get(TAG);

	if (!input->queue)
	{
		free(input);
		return nullptr;
	}

	return &input->common;
}

void input_free(rdpInput* input)
{
	if (input != nullptr)
	{
		rdp_input_internal* in = input_cast(input);

		MessageQueue_Free(in->queue);
		free(in);
	}
}

static const char* flag2str(uint32_t flag, bool extended)
{
#define EVCASE(x) \
	case x:       \
		return #x
	if (extended)
	{
		switch (flag)
		{
			/* Extended Pointer Flags */
			EVCASE(PTR_XFLAGS_DOWN);
			EVCASE(PTR_XFLAGS_BUTTON1);
			EVCASE(PTR_XFLAGS_BUTTON2);
			default:
				return "PTR_XFLAGS_UNKNOWN";
		}
	}

	switch (flag)
	{
		/* Pointer Flags */
		EVCASE(PTR_FLAGS_HWHEEL);
		EVCASE(PTR_FLAGS_WHEEL);
		EVCASE(PTR_FLAGS_WHEEL_NEGATIVE);
		EVCASE(PTR_FLAGS_MOVE);
		EVCASE(PTR_FLAGS_DOWN);
		EVCASE(PTR_FLAGS_BUTTON1);
		EVCASE(PTR_FLAGS_BUTTON2);
		EVCASE(PTR_FLAGS_BUTTON3);
		default:
			return "PTR_FLAGS_UNKNOWN";
	}
#undef EVCASE
}

const char* freerdp_input_mouse_flags_string(uint32_t flags, bool extended, char* buffer,
                                             size_t len)
{
	if (len <= 2)
		return nullptr;

	if (!winpr_str_append("{", buffer, len, ""))
		return nullptr;

	/* Strip initial symbol so we do not get duplicate separators */
	for (size_t x = 0; x < 32; x++)
	{
		const uint32_t flag = 1u << x;
		if (flags & flag)
		{
			char ibuffer[64] = WINPR_C_ARRAY_INIT;
			(void)_snprintf(ibuffer, sizeof(ibuffer), "%s", flag2str(flag, extended));
			if (!winpr_str_append(ibuffer, &buffer[1], len - 2, "|"))
				return nullptr;
		}
	}
	if (!winpr_str_append("}", &buffer[1], len - 2, ""))
		return nullptr;

	return buffer;
}

static const char* kbdflag2str(uint32_t flag)
{
#define EVCASE(x) \
	case x:       \
		return #x

	switch (flag)
	{
		/* Pointer Flags */
		EVCASE(KBD_FLAGS_EXTENDED);
		EVCASE(KBD_FLAGS_EXTENDED1);
		EVCASE(KBD_FLAGS_DOWN);
		EVCASE(KBD_FLAGS_RELEASE);
		default:
			return "KBD_FLAGS_UNKNOWN";
	}
#undef EVCASE
}

const char* freerdp_input_keypress_flags_string(uint32_t flags, char* buffer, size_t len)
{
	if (len <= 2)
		return nullptr;

	if (!winpr_str_append("{", buffer, len, ""))
		return nullptr;

	/* Strip initial symbol so we do not get duplicate separators */
	for (size_t x = 0; x < 32; x++)
	{
		const uint32_t flag = 1u << x;
		if (flags & flag)
		{
			char ibuffer[64] = WINPR_C_ARRAY_INIT;
			(void)_snprintf(ibuffer, sizeof(ibuffer), "%s", kbdflag2str(flag));
			if (!winpr_str_append(ibuffer, &buffer[1], len - 2, "|"))
				return nullptr;
		}
	}
	if (!winpr_str_append("}", &buffer[1], len - 2, ""))
		return nullptr;

	return buffer;
}
