/**
 * FreeRDP: A Remote Desktop Protocol Implementation
 *
 * Copyright 2014 Marc-Andre Moreau <marcandre.moreau@gmail.com>
 * Copyright 2017 Armin Novak <armin.novak@thincast.com>
 * Copyright 2017 Thincast Technologies GmbH
 *
 * 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 <freerdp/config.h>

#include <winpr/crt.h>
#include <winpr/assert.h>
#include <winpr/cast.h>
#include <winpr/file.h>
#include <winpr/path.h>
#include <winpr/synch.h>
#include <winpr/thread.h>
#include <winpr/sysinfo.h>
#include <winpr/interlocked.h>

#include <freerdp/log.h>
#include <freerdp/utils/gfx.h>
#include <freerdp/channels/drdynvc.h>

#include "shadow.h"

#define TAG CLIENT_TAG("shadow")

typedef struct
{
	BOOL gfxOpened;
	BOOL gfxSurfaceCreated;
} SHADOW_GFX_STATUS;

WINPR_ATTR_NODISCARD
static BOOL shadow_avc420_enabled(const rdpShadowClient* client);
WINPR_ATTR_NODISCARD
static BOOL shadow_avc444_enabled(const rdpShadowClient* client);

/* See https://github.com/FreeRDP/FreeRDP/issues/10413
 *
 * Microsoft ditched support for RFX and multiple rectangles in BitmapUpdate for
 * windows 11 24H2.
 *
 * So send all updates only with a single rectangle.
 */
#define BitmapUpdateProxy(client, bitmap) \
	BitmapUpdateProxyEx((client), (bitmap), __FILE__, __LINE__, __func__)
WINPR_ATTR_NODISCARD
static BOOL BitmapUpdateProxyEx(rdpShadowClient* client, const BITMAP_UPDATE* bitmap,
                                const char* file, size_t line, const char* fkt)
{
	WINPR_ASSERT(client);
	WINPR_ASSERT(bitmap);

	rdpShadowServer* server = client->server;
	WINPR_ASSERT(server);

	rdpContext* context = (rdpContext*)client;

	rdpUpdate* update = context->update;
	WINPR_ASSERT(update);

	if (server->SupportMultiRectBitmapUpdates)
	{
		const BOOL rc = IFCALLRESULT(FALSE, update->BitmapUpdate, context, bitmap);
		if (!rc)
		{
			const DWORD log_level = WLOG_ERROR;
			wLog* log = WLog_Get(TAG);
			if (WLog_IsLevelActive(log, log_level))
			{
				WLog_PrintTextMessage(log, log_level, line, file, fkt,
				                      "BitmapUpdate[count %" PRIu32 "] failed", bitmap->number);
			}
			return FALSE;
		}
	}
	else
	{
		for (UINT32 x = 0; x < bitmap->number; x++)
		{
			BITMAP_UPDATE cur = WINPR_C_ARRAY_INIT;
			BITMAP_DATA* bmp = &bitmap->rectangles[x];
			cur.rectangles = bmp;
			cur.number = 1;
			cur.skipCompression = bitmap->skipCompression;
			const BOOL rc = IFCALLRESULT(FALSE, update->BitmapUpdate, context, &cur);
			if (!rc)
			{
				const DWORD log_level = WLOG_ERROR;
				wLog* log = WLog_Get(TAG);
				if (WLog_IsLevelActive(log, log_level))
				{
					WLog_PrintTextMessage(log, log_level, line, file, fkt,
					                      "BitmapUpdate[count 1, at %" PRIu32 "] failed", x);
				}
				return FALSE;
			}
		}
	}

	return TRUE;
}

WINPR_ATTR_NODISCARD
static inline BOOL shadow_client_rdpgfx_new_surface(rdpShadowClient* client)
{
	UINT error = CHANNEL_RC_OK;
	RDPGFX_CREATE_SURFACE_PDU createSurface;
	RDPGFX_MAP_SURFACE_TO_OUTPUT_PDU surfaceToOutput;
	RdpgfxServerContext* context = nullptr;
	rdpSettings* settings = nullptr;

	WINPR_ASSERT(client);
	context = client->rdpgfx;
	WINPR_ASSERT(context);
	settings = ((rdpContext*)client)->settings;
	WINPR_ASSERT(settings);

	WINPR_ASSERT(freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth) <= UINT16_MAX);
	WINPR_ASSERT(freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight) <= UINT16_MAX);
	createSurface.width = (UINT16)freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth);
	createSurface.height = (UINT16)freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight);
	createSurface.pixelFormat = GFX_PIXEL_FORMAT_XRGB_8888;
	createSurface.surfaceId = client->surfaceId;
	surfaceToOutput.outputOriginX = 0;
	surfaceToOutput.outputOriginY = 0;
	surfaceToOutput.surfaceId = client->surfaceId;
	surfaceToOutput.reserved = 0;
	IFCALLRET(context->CreateSurface, error, context, &createSurface);

	if (error)
	{
		WLog_ERR(TAG, "CreateSurface failed with error %" PRIu32 "", error);
		return FALSE;
	}

	IFCALLRET(context->MapSurfaceToOutput, error, context, &surfaceToOutput);

	if (error)
	{
		WLog_ERR(TAG, "MapSurfaceToOutput failed with error %" PRIu32 "", error);
		return FALSE;
	}

	return TRUE;
}

WINPR_ATTR_NODISCARD
static inline BOOL shadow_client_rdpgfx_release_surface(rdpShadowClient* client)
{
	UINT error = CHANNEL_RC_OK;
	RDPGFX_DELETE_SURFACE_PDU pdu;
	RdpgfxServerContext* context = nullptr;

	WINPR_ASSERT(client);

	context = client->rdpgfx;
	WINPR_ASSERT(context);

	pdu.surfaceId = client->surfaceId++;
	IFCALLRET(context->DeleteSurface, error, context, &pdu);

	if (error)
	{
		WLog_ERR(TAG, "DeleteSurface failed with error %" PRIu32 "", error);
		return FALSE;
	}

	return TRUE;
}

WINPR_ATTR_NODISCARD
static inline BOOL shadow_client_rdpgfx_reset_graphic(rdpShadowClient* client)
{
	UINT error = CHANNEL_RC_OK;
	RDPGFX_RESET_GRAPHICS_PDU pdu = WINPR_C_ARRAY_INIT;
	RdpgfxServerContext* context = nullptr;
	rdpSettings* settings = nullptr;

	WINPR_ASSERT(client);
	WINPR_ASSERT(client->rdpgfx);

	context = client->rdpgfx;
	WINPR_ASSERT(context);

	settings = client->context.settings;
	WINPR_ASSERT(settings);

	pdu.width = freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth);
	pdu.height = freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight);
	pdu.monitorCount = client->subsystem->numMonitors;
	pdu.monitorDefArray = client->subsystem->monitors;
	IFCALLRET(context->ResetGraphics, error, context, &pdu);

	if (error)
	{
		WLog_ERR(TAG, "ResetGraphics failed with error %" PRIu32 "", error);
		return FALSE;
	}

	client->first_frame = TRUE;
	return TRUE;
}

static inline void shadow_client_free_queued_message(void* obj)
{
	wMessage* message = (wMessage*)obj;

	WINPR_ASSERT(message);
	if (message->Free)
	{
		message->Free(message);
		message->Free = nullptr;
	}
}

static void shadow_client_context_free(freerdp_peer* peer, rdpContext* context)
{
	rdpShadowClient* client = (rdpShadowClient*)context;
	rdpShadowServer* server = nullptr;

	WINPR_UNUSED(peer);
	if (!client)
		return;

	server = client->server;
	if (server && server->clients)
		ArrayList_Remove(server->clients, (void*)client);

	shadow_encoder_free(client->encoder);

	/* Clear queued messages and free resource */
	MessageQueue_Free(client->MsgQueue);
	WTSCloseServer(client->vcm);
	region16_uninit(&(client->invalidRegion));
	DeleteCriticalSection(&(client->lock));

	client->MsgQueue = nullptr;
	client->encoder = nullptr;
	client->vcm = nullptr;
}

WINPR_ATTR_NODISCARD
static BOOL shadow_client_context_new(freerdp_peer* peer, rdpContext* context)
{
	BOOL NSCodec = 0;
	const char bind_address[] = "bind-address,";
	rdpShadowClient* client = (rdpShadowClient*)context;
	rdpSettings* settings = nullptr;
	const rdpSettings* srvSettings = nullptr;
	rdpShadowServer* server = nullptr;
	const wObject cb = { nullptr, nullptr, nullptr, shadow_client_free_queued_message, nullptr };

	WINPR_ASSERT(client);
	WINPR_ASSERT(peer);
	WINPR_ASSERT(peer->context);

	server = (rdpShadowServer*)peer->ContextExtra;
	WINPR_ASSERT(server);

	srvSettings = server->settings;
	WINPR_ASSERT(srvSettings);

	client->surfaceId = 1;
	client->server = server;
	client->subsystem = server->subsystem;
	WINPR_ASSERT(client->subsystem);

	settings = peer->context->settings;
	WINPR_ASSERT(settings);

	if (!freerdp_settings_set_uint32(settings, FreeRDP_ColorDepth,
	                                 freerdp_settings_get_uint32(srvSettings, FreeRDP_ColorDepth)))
		return FALSE;
	NSCodec = freerdp_settings_get_bool(srvSettings, FreeRDP_NSCodec);
	if (!freerdp_settings_set_bool(settings, FreeRDP_NSCodec, NSCodec))
		return FALSE;
	if (!freerdp_settings_set_bool(settings, FreeRDP_RemoteFxCodec,
	                               freerdp_settings_get_bool(srvSettings, FreeRDP_RemoteFxCodec)))
		return FALSE;
	if (!freerdp_settings_set_uint32(
	        settings, FreeRDP_RemoteFxRlgrMode,
	        freerdp_settings_get_uint32(srvSettings, FreeRDP_RemoteFxRlgrMode)))
		return FALSE;
	if (!freerdp_settings_set_bool(settings, FreeRDP_BitmapCacheV3Enabled, TRUE))
		return FALSE;
	if (!freerdp_settings_set_bool(settings, FreeRDP_FrameMarkerCommandEnabled, TRUE))
		return FALSE;
	if (!freerdp_settings_set_bool(settings, FreeRDP_SurfaceFrameMarkerEnabled, TRUE))
		return FALSE;
	if (!freerdp_settings_set_bool(
	        settings, FreeRDP_SupportGraphicsPipeline,
	        freerdp_settings_get_bool(srvSettings, FreeRDP_SupportGraphicsPipeline)))
		return FALSE;
	if (!freerdp_settings_set_bool(settings, FreeRDP_GfxH264,
	                               freerdp_settings_get_bool(srvSettings, FreeRDP_GfxH264)))
		return FALSE;
	if (!freerdp_settings_set_bool(settings, FreeRDP_DrawAllowSkipAlpha, TRUE))
		return FALSE;
	if (!freerdp_settings_set_bool(settings, FreeRDP_DrawAllowColorSubsampling, TRUE))
		return FALSE;
	if (!freerdp_settings_set_bool(settings, FreeRDP_DrawAllowDynamicColorFidelity, TRUE))
		return FALSE;
	if (!freerdp_settings_set_uint32(settings, FreeRDP_CompressionLevel, PACKET_COMPR_TYPE_RDP8))
		return FALSE;

	if (server->ipcSocket && (strncmp(bind_address, server->ipcSocket,
	                                  strnlen(bind_address, sizeof(bind_address))) != 0))
	{
		if (!freerdp_settings_set_bool(settings, FreeRDP_LyncRdpMode, TRUE))
			return FALSE;
		if (!freerdp_settings_set_bool(settings, FreeRDP_CompressionEnabled, FALSE))
			return FALSE;
	}

	client->inLobby = TRUE;
	client->mayView = server->mayView;
	client->mayInteract = server->mayInteract;

	if (!InitializeCriticalSectionAndSpinCount(&(client->lock), 4000))
		goto fail;

	region16_init(&(client->invalidRegion));
	client->vcm = WTSOpenServerA((LPSTR)peer->context);

	if (!client->vcm || client->vcm == INVALID_HANDLE_VALUE)
		goto fail;

	if (!(client->MsgQueue = MessageQueue_New(&cb)))
		goto fail;

	if (!(client->encoder = shadow_encoder_new(client)))
		goto fail;

	if (!ArrayList_Append(server->clients, (void*)client))
		goto fail;

	return TRUE;

fail:
	shadow_client_context_free(peer, context);
	return FALSE;
}

static inline void shadow_client_mark_invalid(rdpShadowClient* client, UINT32 numRects,
                                              const RECTANGLE_16* rects)
{
	WINPR_ASSERT(client);
	WINPR_ASSERT(rects || (numRects == 0));

	rdpSettings* settings = client->context.settings;
	WINPR_ASSERT(settings);

	EnterCriticalSection(&(client->lock));

	/* Mark client invalid region. No rectangle means full screen */
	if (numRects > 0)
	{
		for (UINT32 index = 0; index < numRects; index++)
		{
			if (!region16_union_rect(&(client->invalidRegion), &(client->invalidRegion),
			                         &rects[index]))
				goto fail;
		}
	}
	else
	{
		RECTANGLE_16 screenRegion = WINPR_C_ARRAY_INIT;
		WINPR_ASSERT(freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth) <= UINT16_MAX);
		WINPR_ASSERT(freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight) <= UINT16_MAX);
		screenRegion.right = (UINT16)freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth);
		screenRegion.bottom = (UINT16)freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight);
		if (!region16_union_rect(&(client->invalidRegion), &(client->invalidRegion), &screenRegion))
			goto fail;
	}
fail:
	LeaveCriticalSection(&(client->lock));
}

/**
 * Function description
 * Recalculate client desktop size and update to rdpSettings
 *
 * @return TRUE if width/height changed.
 */
WINPR_ATTR_NODISCARD
static inline BOOL shadow_client_recalc_desktop_size(rdpShadowClient* client)
{
	INT32 width = 0;
	INT32 height = 0;
	rdpShadowServer* server = nullptr;
	rdpSettings* settings = nullptr;
	RECTANGLE_16 viewport = WINPR_C_ARRAY_INIT;

	WINPR_ASSERT(client);
	server = client->server;
	settings = client->context.settings;

	WINPR_ASSERT(server);
	WINPR_ASSERT(server->surface);
	WINPR_ASSERT(settings);

	WINPR_ASSERT(server->surface->width <= UINT16_MAX);
	WINPR_ASSERT(server->surface->height <= UINT16_MAX);
	viewport.right = (UINT16)server->surface->width;
	viewport.bottom = (UINT16)server->surface->height;

	if (server->shareSubRect)
	{
		if (!rectangles_intersection(&viewport, &(server->subRect), &viewport))
			return FALSE;
	}

	width = viewport.right - viewport.left;
	height = viewport.bottom - viewport.top;

	WINPR_ASSERT(width >= 0);
	WINPR_ASSERT(width <= UINT16_MAX);
	WINPR_ASSERT(height >= 0);
	WINPR_ASSERT(height <= UINT16_MAX);
	return (freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth) != (UINT32)width ||
	        freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight) != (UINT32)height);
}

WINPR_ATTR_NODISCARD
static BOOL shadow_client_capabilities(freerdp_peer* peer)
{
	rdpShadowSubsystem* subsystem = nullptr;
	rdpShadowClient* client = nullptr;
	BOOL ret = TRUE;

	WINPR_ASSERT(peer);

	client = (rdpShadowClient*)peer->context;
	WINPR_ASSERT(client);
	WINPR_ASSERT(client->server);

	subsystem = client->server->subsystem;
	WINPR_ASSERT(subsystem);

	IFCALLRET(subsystem->ClientCapabilities, ret, subsystem, client);

	if (!ret)
		WLog_WARN(TAG, "subsystem->ClientCapabilities failed");

	return ret;
}

static void shadow_reset_desktop_resize(rdpShadowClient* client)
{
	WINPR_ASSERT(client);
	client->resizeRequested = FALSE;
}

WINPR_ATTR_NODISCARD
static BOOL shadow_send_desktop_resize(rdpShadowClient* client)
{
	BOOL rc = 0;
	rdpUpdate* update = nullptr;
	rdpSettings* settings = nullptr;
	const freerdp_peer* peer = nullptr;

	WINPR_ASSERT(client);

	settings = client->context.settings;
	peer = client->context.peer;
	WINPR_ASSERT(peer);
	WINPR_ASSERT(client->server);
	WINPR_ASSERT(client->server->surface);

	const UINT32 resizeWidth = client->server->surface->width;
	const UINT32 resizeHeight = client->server->surface->height;

	if (client->resizeRequested)
	{
		if ((resizeWidth == client->resizeWidth) && (resizeHeight == client->resizeHeight))
		{
			const UINT32 w = freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth);
			const UINT32 h = freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight);
			WLog_WARN(TAG,
			          "detected previous resize request for resolution %" PRIu32 "x%" PRIu32
			          ", still have %" PRIu32 "x%" PRIu32 ", disconnecting peer",
			          resizeWidth, resizeHeight, w, h);
			return FALSE;
		}
	}

	update = client->context.update;
	WINPR_ASSERT(update);
	WINPR_ASSERT(update->DesktopResize);

	// Update peer resolution, required so that during disconnect/reconnect the correct resolution
	// is sent to the client.
	if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopWidth, resizeWidth))
		return FALSE;
	if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, resizeHeight))
		return FALSE;
	rc = update->DesktopResize(update->context);
	WLog_INFO(TAG, "Client %s resize requested (%" PRIu32 "x%" PRIu32 "@%" PRIu32 ")",
	          peer->hostname, resizeWidth, resizeHeight,
	          freerdp_settings_get_uint32(settings, FreeRDP_ColorDepth));
	client->resizeRequested = TRUE;
	client->resizeWidth = resizeWidth;
	client->resizeHeight = resizeHeight;

	return rc;
}

WINPR_ATTR_NODISCARD
static BOOL shadow_client_post_connect(freerdp_peer* peer)
{
	int authStatus = 0;
	rdpSettings* settings = nullptr;
	rdpShadowClient* client = nullptr;
	rdpShadowServer* server = nullptr;
	rdpShadowSubsystem* subsystem = nullptr;

	WINPR_ASSERT(peer);

	client = (rdpShadowClient*)peer->context;
	WINPR_ASSERT(client);

	settings = peer->context->settings;
	WINPR_ASSERT(settings);

	server = client->server;
	WINPR_ASSERT(server);

	subsystem = server->subsystem;
	WINPR_ASSERT(subsystem);

	if (freerdp_settings_get_uint32(settings, FreeRDP_ColorDepth) == 24)
	{
		if (!freerdp_settings_set_uint32(settings, FreeRDP_ColorDepth, 16)) /* disable 24bpp */
			return FALSE;
	}

	const UINT32 MultifragMaxRequestSize =
	    freerdp_settings_get_uint32(settings, FreeRDP_MultifragMaxRequestSize);
	if (MultifragMaxRequestSize < 0x3F0000)
	{
		BOOL rc = freerdp_settings_set_bool(
		    settings, FreeRDP_NSCodec,
		    FALSE); /* NSCodec compressor does not support fragmentation yet */
		if (!rc)
			return FALSE;
	}

	WLog_INFO(TAG, "Client from %s is activated (%" PRIu32 "x%" PRIu32 "@%" PRIu32 ")",
	          peer->hostname, freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth),
	          freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight),
	          freerdp_settings_get_uint32(settings, FreeRDP_ColorDepth));

	if (shadow_client_channels_post_connect(client) != CHANNEL_RC_OK)
		return FALSE;

	shadow_client_mark_invalid(client, 0, nullptr);
	authStatus = -1;

	const char* Username = freerdp_settings_get_string(settings, FreeRDP_Username);
	const char* Domain = freerdp_settings_get_string(settings, FreeRDP_Domain);
	const char* Password = freerdp_settings_get_string(settings, FreeRDP_Password);

	if (Username && Password)
	{
		if (!freerdp_settings_set_bool(settings, FreeRDP_AutoLogonEnabled, TRUE))
			return FALSE;
	}

	if (server->authentication && !freerdp_settings_get_bool(settings, FreeRDP_NlaSecurity))
	{
		if (subsystem->Authenticate)
		{
			authStatus = subsystem->Authenticate(subsystem, client, Username, Domain, Password);
		}

		if (authStatus < 0)
		{
			WLog_ERR(TAG, "client authentication failure: %d", authStatus);
			return FALSE;
		}
	}

	if (subsystem->ClientConnect)
	{
		return subsystem->ClientConnect(subsystem, client);
	}

	return TRUE;
}

/* Convert rects in sub rect coordinate to client/surface coordinate */
static inline void shadow_client_convert_rects(rdpShadowClient* client, RECTANGLE_16* dst,
                                               const RECTANGLE_16* src, UINT32 numRects)
{
	WINPR_ASSERT(client);
	WINPR_ASSERT(client->server);
	WINPR_ASSERT(dst);
	WINPR_ASSERT(src || (numRects == 0));

	if (client->server->shareSubRect)
	{
		UINT16 offsetX = client->server->subRect.left;
		UINT16 offsetY = client->server->subRect.top;

		for (UINT32 i = 0; i < numRects; i++)
		{
			const RECTANGLE_16* s = &src[i];
			RECTANGLE_16* d = &dst[i];

			d->left = s->left + offsetX;
			d->right = s->right + offsetX;
			d->top = s->top + offsetY;
			d->bottom = s->bottom + offsetY;
		}
	}
	else
	{
		if (src != dst)
		{
			CopyMemory(dst, src, numRects * sizeof(RECTANGLE_16));
		}
	}
}

WINPR_ATTR_NODISCARD
static BOOL shadow_client_refresh_request(rdpShadowClient* client)
{
	wMessage message = WINPR_C_ARRAY_INIT;
	wMessagePipe* MsgPipe = nullptr;

	WINPR_ASSERT(client);
	WINPR_ASSERT(client->subsystem);

	MsgPipe = client->subsystem->MsgPipe;
	WINPR_ASSERT(MsgPipe);

	message.id = SHADOW_MSG_IN_REFRESH_REQUEST_ID;
	message.wParam = nullptr;
	message.lParam = nullptr;
	message.context = (void*)client;
	message.Free = nullptr;
	return MessageQueue_Dispatch(MsgPipe->In, &message);
}

WINPR_ATTR_NODISCARD
static BOOL shadow_client_refresh_rect(rdpContext* context, BYTE count, const RECTANGLE_16* areas)
{
	rdpShadowClient* client = (rdpShadowClient*)context;
	RECTANGLE_16* rects = nullptr;

	/* It is invalid if we have area count but no actual area */
	if (count && !areas)
		return FALSE;

	if (count)
	{
		rects = (RECTANGLE_16*)calloc(count, sizeof(RECTANGLE_16));

		if (!rects)
		{
			return FALSE;
		}

		shadow_client_convert_rects(client, rects, areas, count);
		shadow_client_mark_invalid(client, count, rects);
		free(rects);
	}
	else
	{
		shadow_client_mark_invalid(client, 0, nullptr);
	}

	return shadow_client_refresh_request(client);
}

WINPR_ATTR_NODISCARD
static BOOL shadow_client_suppress_output(rdpContext* context, BYTE allow, const RECTANGLE_16* area)
{
	rdpShadowClient* client = (rdpShadowClient*)context;
	RECTANGLE_16 region;

	WINPR_ASSERT(client);

	client->suppressOutput = !(allow);

	if (allow)
	{
		if (area)
		{
			shadow_client_convert_rects(client, &region, area, 1);
			shadow_client_mark_invalid(client, 1, &region);
		}
		else
		{
			shadow_client_mark_invalid(client, 0, nullptr);
		}
	}

	return shadow_client_refresh_request(client);
}

WINPR_ATTR_NODISCARD
static BOOL shadow_client_activate(freerdp_peer* peer)
{
	WINPR_ASSERT(peer);

	rdpShadowClient* client = (rdpShadowClient*)peer->context;
	WINPR_ASSERT(client);

	/* Resize client if necessary */
	if (shadow_client_recalc_desktop_size(client))
		return shadow_send_desktop_resize(client);

	shadow_reset_desktop_resize(client);
	client->activated = TRUE;
	client->inLobby = !(client->mayView);

	if (shadow_encoder_reset(client->encoder) < 0)
	{
		WLog_ERR(TAG, "Failed to reset encoder");
		return FALSE;
	}

	/* Update full screen in next update */
	return shadow_client_refresh_rect(&client->context, 0, nullptr);
}

WINPR_ATTR_NODISCARD
static BOOL shadow_client_logon(freerdp_peer* peer, const SEC_WINNT_AUTH_IDENTITY* identity,
                                BOOL automatic)
{
	BOOL rc = FALSE;
	char* user = nullptr;
	char* domain = nullptr;
	char* password = nullptr;
	rdpSettings* settings = nullptr;

	WINPR_UNUSED(automatic);

	WINPR_ASSERT(peer);
	WINPR_ASSERT(identity);

	WINPR_ASSERT(peer->context);

	settings = peer->context->settings;
	WINPR_ASSERT(settings);

	if (identity->Flags & SEC_WINNT_AUTH_IDENTITY_UNICODE)
	{
		if (identity->User)
			user = ConvertWCharNToUtf8Alloc(identity->User, identity->UserLength, nullptr);

		if (identity->Domain)
			domain = ConvertWCharNToUtf8Alloc(identity->Domain, identity->DomainLength, nullptr);

		if (identity->Password)
			password =
			    ConvertWCharNToUtf8Alloc(identity->Password, identity->PasswordLength, nullptr);
	}
	else
	{
		if (identity->User)
			user = _strdup((char*)identity->User);

		if (identity->Domain)
			domain = _strdup((char*)identity->Domain);

		if (identity->Password)
			password = _strdup((char*)identity->Password);
	}

	if ((identity->User && !user) || (identity->Domain && !domain) ||
	    (identity->Password && !password))
		goto fail;

	if (user)
	{
		if (!freerdp_settings_set_string(settings, FreeRDP_Username, user))
			goto fail;
	}

	if (domain)
	{
		if (!freerdp_settings_set_string(settings, FreeRDP_Domain, domain))
			goto fail;
	}
	if (password)
	{
		if (!freerdp_settings_set_string(settings, FreeRDP_Password, password))
			goto fail;
	}
	rc = TRUE;
fail:
	free(user);
	free(domain);
	free(password);
	return rc;
}

static inline void shadow_client_common_frame_acknowledge(rdpShadowClient* client, UINT32 frameId)
{
	/*
	 * Record the last client acknowledged frame id to
	 * calculate how much frames are in progress.
	 * Some rdp clients (win7 mstsc) skips frame ACK if it is
	 * inactive, we should not expect ACK for each frame.
	 * So it is OK to calculate in-flight frame count according to
	 * a latest acknowledged frame id.
	 */
	WINPR_ASSERT(client);
	WINPR_ASSERT(client->encoder);
	client->encoder->lastAckframeId = frameId;
}

WINPR_ATTR_NODISCARD
static BOOL shadow_client_surface_frame_acknowledge(rdpContext* context, UINT32 frameId)
{
	rdpShadowClient* client = (rdpShadowClient*)context;
	shadow_client_common_frame_acknowledge(client, frameId);
	/*
	 * Reset queueDepth for legacy none RDPGFX acknowledge
	 */
	WINPR_ASSERT(client);
	WINPR_ASSERT(client->encoder);
	client->encoder->queueDepth = QUEUE_DEPTH_UNAVAILABLE;
	return TRUE;
}

WINPR_ATTR_NODISCARD
static UINT
shadow_client_rdpgfx_frame_acknowledge(RdpgfxServerContext* context,
                                       const RDPGFX_FRAME_ACKNOWLEDGE_PDU* frameAcknowledge)
{
	rdpShadowClient* client = nullptr;

	WINPR_ASSERT(context);
	WINPR_ASSERT(frameAcknowledge);

	client = (rdpShadowClient*)context->custom;
	shadow_client_common_frame_acknowledge(client, frameAcknowledge->frameId);

	WINPR_ASSERT(client);
	WINPR_ASSERT(client->encoder);
	client->encoder->queueDepth = frameAcknowledge->queueDepth;
	return CHANNEL_RC_OK;
}

WINPR_ATTR_NODISCARD
static BOOL shadow_are_caps_filtered(const rdpSettings* settings, UINT32 caps)
{
	const UINT32 capList[] = {
#if defined(WITH_GFX_AV1)
		RDPGFX_CAPVERSION_FRDP_1,
#endif
		RDPGFX_CAPVERSION_8,
		RDPGFX_CAPVERSION_81,
		RDPGFX_CAPVERSION_10,
		RDPGFX_CAPVERSION_101,
		RDPGFX_CAPVERSION_102,
		RDPGFX_CAPVERSION_103,
		RDPGFX_CAPVERSION_104,
		RDPGFX_CAPVERSION_105,
		RDPGFX_CAPVERSION_106,
		RDPGFX_CAPVERSION_106_ERR,
		RDPGFX_CAPVERSION_107
#if defined(WITH_GFX_AZURE)
		,
		RDPGFX_CAPVERSION_111,
		RDPGFX_CAPVERSION_112,
		RDPGFX_CAPVERSION_113
#endif
	};

	WINPR_ASSERT(settings);
	const UINT32 filter = freerdp_settings_get_uint32(settings, FreeRDP_GfxCapsFilter);

	for (UINT32 x = 0; x < ARRAYSIZE(capList); x++)
	{
		if (caps == capList[x])
			return (filter & (1u << x)) != 0;
	}

	return TRUE;
}

WINPR_ATTR_NODISCARD
static UINT shadow_client_send_caps_confirm(RdpgfxServerContext* context, rdpShadowClient* client,
                                            const RDPGFX_CAPS_CONFIRM_PDU* pdu)
{
	WINPR_ASSERT(context);
	WINPR_ASSERT(client);
	WINPR_ASSERT(pdu);

	WINPR_ASSERT(context->CapsConfirm);
	UINT rc = context->CapsConfirm(context, pdu);
	client->areGfxCapsReady = (rc == CHANNEL_RC_OK);
	client->confirmedCaps = *pdu->capsSet;

	rdpSettings* clientSettings = client->context.settings;
	WINPR_ASSERT(clientSettings);

#if defined(WITH_GFX_AV1)
	if (pdu->capsSet->version == RDPGFX_CAPVERSION_FRDP_1)
	{
		UINT32 flags = FREERDP_CODEC_AV1_I420;
		if ((pdu->capsSet->flags & RDPGFX_CAPS_FLAG_AV1_I444_DISABLED) == 0)
			flags = FREERDP_CODEC_AV1_I444;

		if (shadow_encoder_prepare(client->encoder, flags) < 0)
			return ERROR_INVALID_PARAMETER;
	}
	else
#endif
	    if (shadow_avc444_enabled(client) || shadow_avc420_enabled(client))
	{
#ifdef WITH_GFX_H264
		const BOOL h264 = (shadow_encoder_prepare(client->encoder, FREERDP_CODEC_AVC420 |
		                                                               FREERDP_CODEC_AVC444) >= 0);
		if (h264)
		{
			if (!freerdp_settings_set_bool(
			        clientSettings, FreeRDP_GfxH264,
			        (pdu->capsSet->flags & RDPGFX_CAPS_FLAG_AVC420_ENABLED) != 0))
				return rc;
		}
		else
		{
			if (!freerdp_settings_set_bool(clientSettings, FreeRDP_GfxH264, FALSE))
				return rc;
		}
#else
		if (!freerdp_settings_set_bool(clientSettings, FreeRDP_GfxAVC444v2, FALSE))
			return rc;
		if (!freerdp_settings_set_bool(clientSettings, FreeRDP_GfxAVC444, FALSE))
			return rc;
		if (!freerdp_settings_set_bool(clientSettings, FreeRDP_GfxH264, FALSE))
			return rc;
#endif
	}

	return rc;
}

WINPR_ATTR_NODISCARD
static BOOL shadow_client_caps_test_version(RdpgfxServerContext* context, rdpShadowClient* client,
                                            BOOL h264, const RDPGFX_CAPSET* capsSets,
                                            UINT32 capsSetCount, UINT32 capsVersion, UINT* rc)
{
	WINPR_ASSERT(context);
	WINPR_ASSERT(client);
	WINPR_ASSERT(capsSets || (capsSetCount == 0));
	WINPR_ASSERT(rc);

	WINPR_ASSERT(context->rdpcontext);
	const rdpSettings* srvSettings = context->rdpcontext->settings;
	WINPR_ASSERT(srvSettings);

	rdpSettings* clientSettings = client->context.settings;
	WINPR_ASSERT(clientSettings);

	if (shadow_are_caps_filtered(srvSettings, capsVersion))
		return FALSE;

	for (UINT32 index = 0; index < capsSetCount; index++)
	{
		const RDPGFX_CAPSET* currentCaps = &capsSets[index];

		WLog_INFO(TAG, "testing  %s vs %s[0x%08" PRIx32 "]",
		          rdpgfx_caps_version_str(currentCaps->version),
		          rdpgfx_caps_version_str(capsVersion), capsVersion);
		if (currentCaps->version == capsVersion)
		{
			RDPGFX_CAPSET caps = *currentCaps;
			RDPGFX_CAPS_CONFIRM_PDU pdu = WINPR_C_ARRAY_INIT;
			pdu.capsSet = &caps;

			const UINT32 flags = pdu.capsSet->flags;

			if (!freerdp_settings_set_bool(clientSettings, FreeRDP_GfxSmallCache,
			                               (flags & RDPGFX_CAPS_FLAG_SMALL_CACHE) != 0))
				return FALSE;

			BOOL avc420 = TRUE;
			BOOL avc444 = !(flags & RDPGFX_CAPS_FLAG_AVC_DISABLED);
			BOOL avc444v2 = avc444;
			if (!freerdp_settings_get_bool(srvSettings, FreeRDP_GfxAVC444v2) || !h264)
				avc444v2 = FALSE;
			if (!freerdp_settings_set_bool(clientSettings, FreeRDP_GfxAVC444v2, avc444v2))
				return FALSE;
			if (!freerdp_settings_get_bool(srvSettings, FreeRDP_GfxAVC444) || !h264)
				avc444 = FALSE;
			if (!freerdp_settings_set_bool(clientSettings, FreeRDP_GfxAVC444, avc444))
				return FALSE;
			if (!freerdp_settings_get_bool(srvSettings, FreeRDP_GfxH264) || !h264)
				avc420 = FALSE;
			if (!freerdp_settings_set_bool(clientSettings, FreeRDP_GfxH264, avc420))
				return FALSE;

			const BOOL progressive = freerdp_settings_get_bool(srvSettings, FreeRDP_GfxProgressive);
			if (!freerdp_settings_set_bool(clientSettings, FreeRDP_GfxProgressive, progressive))
				return FALSE;
			const BOOL progressivev2 =
			    freerdp_settings_get_bool(srvSettings, FreeRDP_GfxProgressiveV2);
			if (!freerdp_settings_set_bool(clientSettings, FreeRDP_GfxProgressiveV2, progressivev2))
				return FALSE;

			const BOOL rfx = freerdp_settings_get_bool(srvSettings, FreeRDP_RemoteFxCodec);
			if (!freerdp_settings_set_bool(clientSettings, FreeRDP_RemoteFxCodec, rfx))
				return FALSE;

			const BOOL planar = freerdp_settings_get_bool(srvSettings, FreeRDP_GfxPlanar);
			if (!freerdp_settings_set_bool(clientSettings, FreeRDP_GfxPlanar, planar))
				return FALSE;
#if defined(WITH_GFX_AV1)
			const BOOL av1 = freerdp_settings_get_bool(srvSettings, FreeRDP_GfxCodecAV1);
			if (!freerdp_settings_set_bool(clientSettings, FreeRDP_GfxCodecAV1, av1))
				return FALSE;
			const UINT32 av1Profile =
			    freerdp_settings_get_uint32(srvSettings, FreeRDP_GfxCodecAV1Profile);
			if (!freerdp_settings_set_uint32(clientSettings, FreeRDP_GfxCodecAV1Profile,
			                                 av1Profile))
				return FALSE;
#endif

			if (!h264 || (!avc444v2 && !avc444 && !avc420))
				pdu.capsSet->flags |= RDPGFX_CAPS_FLAG_AVC_DISABLED;

#if defined(WITH_GFX_AV1)
			if (currentCaps->version == RDPGFX_CAPVERSION_FRDP_1)
			{
				if (!av1)
					return FALSE;

				pdu.capsSet->flags &=
				    ~(RDPGFX_CAPS_FLAG_AV1_I444_SUPPORTED | RDPGFX_CAPS_FLAG_AV1_I444_DISABLED);

				if ((currentCaps->flags & RDPGFX_CAPS_FLAG_AV1_I444_SUPPORTED) != 0)
				{
					if (av1Profile == 0)
						return FALSE;
				}

				if ((av1Profile == 0) ||
				    ((currentCaps->flags & RDPGFX_CAPS_FLAG_AV1_I444_SUPPORTED) == 0))
					pdu.capsSet->flags |= RDPGFX_CAPS_FLAG_AV1_I444_DISABLED;
				if ((currentCaps->flags & RDPGFX_CAPS_FLAG_AV1_I444_DISABLED) != 0)
					pdu.capsSet->flags |= RDPGFX_CAPS_FLAG_AV1_I444_DISABLED;
			}
#endif
			*rc = shadow_client_send_caps_confirm(context, client, &pdu);
			return TRUE;
		}
	}

	return FALSE;
}

/**
 * Function description
 *
 * @return 0 on success, otherwise a Win32 error code
 */
WINPR_ATTR_NODISCARD
static UINT shadow_client_rdpgfx_caps_advertise(RdpgfxServerContext* context,
                                                const RDPGFX_CAPS_ADVERTISE_PDU* capsAdvertise)
{
	UINT rc = ERROR_INTERNAL_ERROR;

	UINT32 flags = 0;

	WINPR_ASSERT(context);
	WINPR_ASSERT(capsAdvertise);

	rdpShadowClient* client = (rdpShadowClient*)context->custom;
	WINPR_ASSERT(client);
	WINPR_ASSERT(context->rdpcontext);

	const rdpSettings* srvSettings = context->rdpcontext->settings;
	WINPR_ASSERT(srvSettings);

	rdpSettings* clientSettings = client->context.settings;
	WINPR_ASSERT(clientSettings);

	/* Request full screen update for new gfx channel */
	if (!shadow_client_refresh_rect(&client->context, 0, nullptr))
		return rc;

	const UINT32 capsVersions[] = {
#if defined(WITH_GFX_AV1)
		RDPGFX_CAPVERSION_FRDP_1,
#endif
#if defined(WITH_GFX_AZURE)
		RDPGFX_CAPVERSION_113,    RDPGFX_CAPVERSION_112, RDPGFX_CAPVERSION_111,
#endif
		RDPGFX_CAPVERSION_107,    RDPGFX_CAPVERSION_106, RDPGFX_CAPVERSION_106_ERR,
		RDPGFX_CAPVERSION_105,    RDPGFX_CAPVERSION_104, RDPGFX_CAPVERSION_103,
		RDPGFX_CAPVERSION_102,    RDPGFX_CAPVERSION_101, RDPGFX_CAPVERSION_10,
	};

	const BOOL h264 =
	    shadow_encoder_prepare(client->encoder, FREERDP_CODEC_AVC420 | FREERDP_CODEC_AVC444) >= 0;

#if defined(WITH_GFX_AV1)
	if (freerdp_settings_get_bool(clientSettings, FreeRDP_GfxCodecAV1))
	{
		UINT32 codec = 0;
		const UINT32 profile =
		    freerdp_settings_get_uint32(clientSettings, FreeRDP_GfxCodecAV1Profile);
		switch (profile)
		{
			case 0:
				codec = FREERDP_CODEC_AV1_I420;
				break;
			case 1:
				codec = FREERDP_CODEC_AV1_I444;
				break;
			default:
				return ERROR_BAD_PROFILE;
		}
		const BOOL av1 = shadow_encoder_prepare(client->encoder, codec) >= 0;
		if (!av1)
		{
			if (!freerdp_settings_set_bool(clientSettings, FreeRDP_GfxCodecAV1, FALSE))
				return ERROR_INTERNAL_ERROR;
		}
	}
#endif

	for (size_t x = 0; x < ARRAYSIZE(capsVersions); x++)
	{
		const UINT32 cur = capsVersions[x];
		if (shadow_client_caps_test_version(context, client, h264, capsAdvertise->capsSets,
		                                    capsAdvertise->capsSetCount, cur, &rc))
			return rc;
	}

	if (!shadow_are_caps_filtered(srvSettings, RDPGFX_CAPVERSION_81))
	{
		for (UINT32 index = 0; index < capsAdvertise->capsSetCount; index++)
		{
			const RDPGFX_CAPSET* currentCaps = &capsAdvertise->capsSets[index];

			if (currentCaps->version == RDPGFX_CAPVERSION_81)
			{
				RDPGFX_CAPSET caps = *currentCaps;
				RDPGFX_CAPS_CONFIRM_PDU pdu;
				pdu.capsSet = &caps;

				flags = pdu.capsSet->flags;

				if (!freerdp_settings_set_bool(clientSettings, FreeRDP_GfxAVC444v2, FALSE))
					return rc;
				if (!freerdp_settings_set_bool(clientSettings, FreeRDP_GfxAVC444, FALSE))
					return rc;

				if (!freerdp_settings_set_bool(clientSettings, FreeRDP_GfxThinClient,
				                               (flags & RDPGFX_CAPS_FLAG_THINCLIENT) != 0))
					return rc;
				if (!freerdp_settings_set_bool(clientSettings, FreeRDP_GfxSmallCache,
				                               (flags & RDPGFX_CAPS_FLAG_SMALL_CACHE) != 0))
					return rc;

#ifndef WITH_GFX_H264
				if (!freerdp_settings_set_bool(clientSettings, FreeRDP_GfxH264, FALSE))
					return rc;
				if (h264)
					pdu.capsSet->flags &= ~RDPGFX_CAPS_FLAG_AVC420_ENABLED;
#else

#endif

				return shadow_client_send_caps_confirm(context, client, &pdu);
			}
		}
	}

	if (!shadow_are_caps_filtered(srvSettings, RDPGFX_CAPVERSION_8))
	{
		for (UINT32 index = 0; index < capsAdvertise->capsSetCount; index++)
		{
			const RDPGFX_CAPSET* currentCaps = &capsAdvertise->capsSets[index];

			if (currentCaps->version == RDPGFX_CAPVERSION_8)
			{
				RDPGFX_CAPSET caps = *currentCaps;
				RDPGFX_CAPS_CONFIRM_PDU pdu;
				pdu.capsSet = &caps;
				flags = pdu.capsSet->flags;

				if (!freerdp_settings_set_bool(clientSettings, FreeRDP_GfxAVC444v2, FALSE))
					return rc;
				if (!freerdp_settings_set_bool(clientSettings, FreeRDP_GfxAVC444, FALSE))
					return rc;
				if (!freerdp_settings_set_bool(clientSettings, FreeRDP_GfxH264, FALSE))
					return rc;

				if (!freerdp_settings_set_bool(clientSettings, FreeRDP_GfxThinClient,
				                               (flags & RDPGFX_CAPS_FLAG_THINCLIENT) != 0))
					return rc;
				if (!freerdp_settings_set_bool(clientSettings, FreeRDP_GfxSmallCache,
				                               (flags & RDPGFX_CAPS_FLAG_SMALL_CACHE) != 0))
					return rc;

				return shadow_client_send_caps_confirm(context, client, &pdu);
			}
		}
	}

	return CHANNEL_RC_UNSUPPORTED_VERSION;
}

WINPR_ATTR_NODISCARD
static inline UINT32 rdpgfx_estimate_h264_avc420(RDPGFX_AVC420_BITMAP_STREAM* havc420)
{
	/* H264 metadata + H264 stream. See rdpgfx_write_h264_avc420 */
	WINPR_ASSERT(havc420);
	return sizeof(UINT32) /* numRegionRects */
	       + 10ULL        /* regionRects + quantQualityVals */
	             * havc420->meta.numRegionRects +
	       havc420->length;
}

#if defined(WITH_GFX_AV1)
WINPR_ATTR_NODISCARD
static BOOL shadow_client_send_av1(rdpShadowClient* client, const BYTE* pSrcData, UINT32 nSrcStep,
                                   UINT32 SrcFormat, UINT16 nWidth, UINT16 nHeight,
                                   RDPGFX_SURFACE_COMMAND* cmd,
                                   const RDPGFX_START_FRAME_PDU* cmdstart,
                                   const RDPGFX_END_FRAME_PDU* cmdend)
{
	WINPR_ASSERT(client);

	rdpShadowEncoder* encoder = client->encoder;
	WINPR_ASSERT(encoder);

	UINT error = CHANNEL_RC_OK;
	INT32 rc = 0;
	RDPGFX_AVC420_BITMAP_STREAM avc420 = WINPR_C_ARRAY_INIT;
	RECTANGLE_16 regionRect = WINPR_C_ARRAY_INIT;

	UINT32 flags = FREERDP_CODEC_AV1_I444;
	if ((client->confirmedCaps.flags & RDPGFX_CAPS_FLAG_AV1_I444_DISABLED) != 0)
		flags = FREERDP_CODEC_AV1_I420;

	if (shadow_encoder_prepare(encoder, flags) < 0)
	{
		WLog_ERR(TAG, "Failed to prepare encoder FREERDP_CODEC_AV1");
		return FALSE;
	}

	WINPR_ASSERT(cmd->left <= UINT16_MAX);
	WINPR_ASSERT(cmd->top <= UINT16_MAX);
	WINPR_ASSERT(cmd->right <= UINT16_MAX);
	WINPR_ASSERT(cmd->bottom <= UINT16_MAX);
	regionRect.left = (UINT16)cmd->left;
	regionRect.top = (UINT16)cmd->top;
	regionRect.right = (UINT16)cmd->right;
	regionRect.bottom = (UINT16)cmd->bottom;
	rc = freerdp_av1_compress(encoder->av1, pSrcData, SrcFormat, nSrcStep, nWidth, nHeight,
	                          &regionRect, &avc420.data, &avc420.length, &avc420.meta);
	if (rc < 0)
	{
		WLog_ERR(TAG, "freerdp_av1_compress failed");
		return FALSE;
	}

	/* rc > 0 means new data */
	if (rc > 0)
	{
		cmd->codecId = RDPGFX_CODECID_AV1;
		cmd->extra = (void*)&avc420;

		IFCALLRET(client->rdpgfx->SurfaceFrameCommand, error, client->rdpgfx, cmd, cmdstart,
		          cmdend);
		cmd->extra = nullptr;
	}
	free_h264_metablock(&avc420.meta);

	if (error)
	{
		WLog_ERR(TAG, "SurfaceFrameCommand failed with error %" PRIu32 "", error);
		return FALSE;
	}
	return TRUE;
}
#endif

WINPR_ATTR_NODISCARD
static BOOL shadow_client_send_avc444(rdpShadowClient* client, const BYTE* pSrcData,
                                      UINT32 nSrcStep, UINT32 SrcFormat, UINT16 nWidth,
                                      UINT16 nHeight, RDPGFX_SURFACE_COMMAND* cmd,
                                      const RDPGFX_START_FRAME_PDU* cmdstart,
                                      const RDPGFX_END_FRAME_PDU* cmdend)
{
	WINPR_ASSERT(client);

	rdpShadowEncoder* encoder = client->encoder;
	WINPR_ASSERT(encoder);

	UINT error = CHANNEL_RC_OK;
	INT32 rc = 0;
	RDPGFX_AVC444_BITMAP_STREAM avc444 = WINPR_C_ARRAY_INIT;
	RECTANGLE_16 regionRect = WINPR_C_ARRAY_INIT;
	const BOOL GfxAVC444v2 = (cmd->codecId == RDPGFX_CODECID_AVC444v2);
	BYTE version = GfxAVC444v2 ? 2 : 1;

	if (shadow_encoder_prepare(encoder, FREERDP_CODEC_AVC444) < 0)
	{
		WLog_ERR(TAG, "Failed to prepare encoder FREERDP_CODEC_AVC444");
		return FALSE;
	}

	WINPR_ASSERT(cmd->left <= UINT16_MAX);
	WINPR_ASSERT(cmd->top <= UINT16_MAX);
	WINPR_ASSERT(cmd->right <= UINT16_MAX);
	WINPR_ASSERT(cmd->bottom <= UINT16_MAX);
	regionRect.left = (UINT16)cmd->left;
	regionRect.top = (UINT16)cmd->top;
	regionRect.right = (UINT16)cmd->right;
	regionRect.bottom = (UINT16)cmd->bottom;
	rc = avc444_compress(encoder->h264, pSrcData, SrcFormat, nSrcStep, nWidth, nHeight, version,
	                     &regionRect, &avc444.LC, &avc444.bitstream[0].data,
	                     &avc444.bitstream[0].length, &avc444.bitstream[1].data,
	                     &avc444.bitstream[1].length, &avc444.bitstream[0].meta,
	                     &avc444.bitstream[1].meta);
	if (rc < 0)
	{
		WLog_ERR(TAG, "avc420_compress failed for avc444");
		return FALSE;
	}

	/* rc > 0 means new data */
	if (rc > 0)
	{
		avc444.cbAvc420EncodedBitstream1 = rdpgfx_estimate_h264_avc420(&avc444.bitstream[0]);
		cmd->extra = (void*)&avc444;
		IFCALLRET(client->rdpgfx->SurfaceFrameCommand, error, client->rdpgfx, cmd, cmdstart,
		          cmdend);
		cmd->extra = nullptr;
	}

	free_h264_metablock(&avc444.bitstream[0].meta);
	free_h264_metablock(&avc444.bitstream[1].meta);
	if (error)
	{
		WLog_ERR(TAG, "SurfaceFrameCommand failed with error %" PRIu32 "", error);
		return FALSE;
	}
	return TRUE;
}

WINPR_ATTR_NODISCARD
static BOOL shadow_client_send_avc420(rdpShadowClient* client, const BYTE* pSrcData,
                                      UINT32 nSrcStep, UINT32 SrcFormat, UINT16 nWidth,
                                      UINT16 nHeight, RDPGFX_SURFACE_COMMAND* cmd,
                                      const RDPGFX_START_FRAME_PDU* cmdstart,
                                      const RDPGFX_END_FRAME_PDU* cmdend)
{
	WINPR_ASSERT(client);

	rdpShadowEncoder* encoder = client->encoder;
	WINPR_ASSERT(encoder);

	UINT error = CHANNEL_RC_OK;
	INT32 rc = 0;
	RDPGFX_AVC420_BITMAP_STREAM avc420 = WINPR_C_ARRAY_INIT;
	RECTANGLE_16 regionRect;

	if (shadow_encoder_prepare(encoder, FREERDP_CODEC_AVC420) < 0)
	{
		WLog_ERR(TAG, "Failed to prepare encoder FREERDP_CODEC_AVC420");
		return FALSE;
	}

	WINPR_ASSERT(cmd->left <= UINT16_MAX);
	WINPR_ASSERT(cmd->top <= UINT16_MAX);
	WINPR_ASSERT(cmd->right <= UINT16_MAX);
	WINPR_ASSERT(cmd->bottom <= UINT16_MAX);
	regionRect.left = (UINT16)cmd->left;
	regionRect.top = (UINT16)cmd->top;
	regionRect.right = (UINT16)cmd->right;
	regionRect.bottom = (UINT16)cmd->bottom;
	rc = avc420_compress(encoder->h264, pSrcData, SrcFormat, nSrcStep, nWidth, nHeight, &regionRect,
	                     &avc420.data, &avc420.length, &avc420.meta);
	if (rc < 0)
	{
		WLog_ERR(TAG, "avc420_compress failed");
		return FALSE;
	}

	/* rc > 0 means new data */
	if (rc > 0)
	{
		cmd->codecId = RDPGFX_CODECID_AVC420;
		cmd->extra = (void*)&avc420;
		IFCALLRET(client->rdpgfx->SurfaceFrameCommand, error, client->rdpgfx, cmd, cmdstart,
		          cmdend);
		cmd->extra = nullptr;
	}
	free_h264_metablock(&avc420.meta);

	if (error)
	{
		WLog_ERR(TAG, "SurfaceFrameCommand failed with error %" PRIu32 "", error);
		return FALSE;
	}
	return TRUE;
}

WINPR_ATTR_NODISCARD
static BOOL shadow_client_send_rfx(rdpShadowClient* client, const BYTE* pSrcData, UINT32 nSrcStep,
                                   UINT32 SrcFormat, UINT16 nWidth, UINT16 nHeight,
                                   RDPGFX_SURFACE_COMMAND* cmd,
                                   const RDPGFX_START_FRAME_PDU* cmdstart,
                                   const RDPGFX_END_FRAME_PDU* cmdend)
{
	WINPR_ASSERT(client);

	rdpShadowEncoder* encoder = client->encoder;
	WINPR_ASSERT(encoder);

	UINT error = CHANNEL_RC_OK;
	BOOL rc = 0;
	RFX_RECT rect = WINPR_C_ARRAY_INIT;

	if (shadow_encoder_prepare(encoder, FREERDP_CODEC_REMOTEFX) < 0)
	{
		WLog_ERR(TAG, "Failed to prepare encoder FREERDP_CODEC_REMOTEFX");
		return FALSE;
	}

	const UINT32 rfxFormat = rfx_context_get_pixel_format(encoder->rfx);
	if (rfxFormat != SrcFormat)
	{
		WLog_ERR(TAG, "RFX context pixel format mismatch %s, expected %s",
		         FreeRDPGetColorFormatName(rfxFormat), FreeRDPGetColorFormatName(SrcFormat));
		return FALSE;
	}

	wStream* s = Stream_New(nullptr, 1024);
	WINPR_ASSERT(s);

	WINPR_ASSERT(cmd->left <= UINT16_MAX);
	WINPR_ASSERT(cmd->top <= UINT16_MAX);
	WINPR_ASSERT(cmd->right <= UINT16_MAX);
	WINPR_ASSERT(cmd->bottom <= UINT16_MAX);
	rect.x = (UINT16)cmd->left;
	rect.y = (UINT16)cmd->top;
	rect.width = WINPR_ASSERTING_INT_CAST(UINT16, cmd->right - cmd->left);
	rect.height = WINPR_ASSERTING_INT_CAST(UINT16, cmd->bottom - cmd->top);

	rc = rfx_compose_message(encoder->rfx, s, &rect, 1, pSrcData, nWidth, nHeight, nSrcStep);

	if (!rc)
	{
		WLog_ERR(TAG, "rfx_compose_message failed");
		Stream_Free(s, TRUE);
		return FALSE;
	}

	/* rc > 0 means new data */
	if (rc > 0)
	{
		const size_t pos = Stream_GetPosition(s);
		WINPR_ASSERT(pos <= UINT32_MAX);

		cmd->codecId = RDPGFX_CODECID_CAVIDEO;
		cmd->data = Stream_Buffer(s);
		cmd->length = (UINT32)pos;

		IFCALLRET(client->rdpgfx->SurfaceFrameCommand, error, client->rdpgfx, cmd, cmdstart,
		          cmdend);
		cmd->data = nullptr;
	}

	Stream_Free(s, TRUE);
	if (error)
	{
		WLog_ERR(TAG, "SurfaceFrameCommand failed with error %" PRIu32 "", error);
		return FALSE;
	}
	return TRUE;
}

WINPR_ATTR_NODISCARD
static BOOL shadow_client_send_progressive(rdpShadowClient* client, const BYTE* pSrcData,
                                           UINT32 nSrcStep, UINT32 SrcFormat, UINT16 nWidth,
                                           UINT16 nHeight, RDPGFX_SURFACE_COMMAND* cmd,
                                           const RDPGFX_START_FRAME_PDU* cmdstart,
                                           const RDPGFX_END_FRAME_PDU* cmdend)
{
	WINPR_ASSERT(client);

	rdpShadowEncoder* encoder = client->encoder;
	WINPR_ASSERT(encoder);

	UINT error = CHANNEL_RC_OK;
	INT32 rc = 0;
	REGION16 region;
	RECTANGLE_16 regionRect;

	if (shadow_encoder_prepare(encoder, FREERDP_CODEC_PROGRESSIVE) < 0)
	{
		WLog_ERR(TAG, "Failed to prepare encoder FREERDP_CODEC_PROGRESSIVE");
		return FALSE;
	}

	WINPR_ASSERT(cmd->left <= UINT16_MAX);
	WINPR_ASSERT(cmd->top <= UINT16_MAX);
	WINPR_ASSERT(cmd->right <= UINT16_MAX);
	WINPR_ASSERT(cmd->bottom <= UINT16_MAX);
	regionRect.left = (UINT16)cmd->left;
	regionRect.top = (UINT16)cmd->top;
	regionRect.right = (UINT16)cmd->right;
	regionRect.bottom = (UINT16)cmd->bottom;
	region16_init(&region);
	if (!region16_union_rect(&region, &region, &regionRect))
	{
		region16_uninit(&region);
		return FALSE;
	}
	rc = progressive_compress(encoder->progressive, pSrcData, nSrcStep * nHeight, SrcFormat, nWidth,
	                          nHeight, nSrcStep, &region, &cmd->data, &cmd->length);
	region16_uninit(&region);
	if (rc < 0)
	{
		WLog_ERR(TAG, "progressive_compress failed");
		return FALSE;
	}

	/* rc > 0 means new data */
	if (rc > 0)
	{
		cmd->codecId = RDPGFX_CODECID_CAPROGRESSIVE;

		IFCALLRET(client->rdpgfx->SurfaceFrameCommand, error, client->rdpgfx, cmd, cmdstart,
		          cmdend);
	}
	cmd->data = nullptr;

	if (error)
	{
		WLog_ERR(TAG, "SurfaceFrameCommand failed with error %" PRIu32 "", error);
		return FALSE;
	}
	return TRUE;
}

WINPR_ATTR_NODISCARD
static BOOL shadow_client_send_planar(rdpShadowClient* client, const BYTE* pSrcData,
                                      UINT32 nSrcStep, UINT32 SrcFormat,
                                      RDPGFX_SURFACE_COMMAND* cmd,
                                      const RDPGFX_START_FRAME_PDU* cmdstart,
                                      const RDPGFX_END_FRAME_PDU* cmdend)
{
	WINPR_ASSERT(client);

	rdpShadowEncoder* encoder = client->encoder;
	WINPR_ASSERT(encoder);

	UINT error = CHANNEL_RC_OK;
	const UINT32 w = cmd->right - cmd->left;
	const UINT32 h = cmd->bottom - cmd->top;
	const BYTE* src =
	    &pSrcData[cmd->top * nSrcStep + cmd->left * FreeRDPGetBytesPerPixel(SrcFormat)];
	if (shadow_encoder_prepare(encoder, FREERDP_CODEC_PLANAR) < 0)
	{
		WLog_ERR(TAG, "Failed to prepare encoder FREERDP_CODEC_PLANAR");
		return FALSE;
	}

	const BOOL rc = freerdp_bitmap_planar_context_reset(encoder->planar, w, h);
	if (!rc)
		return FALSE;

	freerdp_planar_topdown_image(encoder->planar, TRUE);

	cmd->data = freerdp_bitmap_compress_planar(encoder->planar, src, SrcFormat, w, h, nSrcStep,
	                                           nullptr, &cmd->length);
	WINPR_ASSERT(cmd->data || (cmd->length == 0));

	cmd->codecId = RDPGFX_CODECID_PLANAR;

	IFCALLRET(client->rdpgfx->SurfaceFrameCommand, error, client->rdpgfx, cmd, cmdstart, cmdend);
	free(cmd->data);
	cmd->data = nullptr;

	if (error)
	{
		WLog_ERR(TAG, "SurfaceFrameCommand failed with error %" PRIu32 "", error);
		return FALSE;
	}
	return TRUE;
}

WINPR_ATTR_NODISCARD
static BOOL shadow_client_send_uncompressed(rdpShadowClient* client, const BYTE* pSrcData,
                                            UINT32 nSrcStep, UINT32 SrcFormat,
                                            RDPGFX_SURFACE_COMMAND* cmd,
                                            const RDPGFX_START_FRAME_PDU* cmdstart,
                                            const RDPGFX_END_FRAME_PDU* cmdend)
{
	WINPR_ASSERT(client);

	UINT error = CHANNEL_RC_OK;
	const UINT32 w = cmd->right - cmd->left;
	const UINT32 h = cmd->bottom - cmd->top;
	const UINT32 length = w * 4 * h;

	BYTE* data = malloc(length);
	if (!data)
		return FALSE;

	BOOL rc = freerdp_image_copy_no_overlap(data, cmd->format, 0, 0, 0, w, h, pSrcData, SrcFormat,
	                                        nSrcStep, cmd->left, cmd->top, nullptr, 0);
	if (!rc)
	{
		free(data);
		return FALSE;
	}

	cmd->data = data;
	cmd->length = length;
	cmd->codecId = RDPGFX_CODECID_UNCOMPRESSED;

	IFCALLRET(client->rdpgfx->SurfaceFrameCommand, error, client->rdpgfx, cmd, cmdstart, cmdend);
	free(data);
	cmd->data = nullptr;
	if (error)
	{
		WLog_ERR(TAG, "SurfaceFrameCommand failed with error %" PRIu32 "", error);
		return FALSE;
	}
	return TRUE;
}

WINPR_ATTR_NODISCARD
static BOOL shadow_avc444_enabled(const rdpShadowClient* client)
{
	WINPR_ASSERT(client);
	switch (client->confirmedCaps.version)
	{
		case RDPGFX_CAPVERSION_10:
		case RDPGFX_CAPVERSION_101:
		case RDPGFX_CAPVERSION_102:
		case RDPGFX_CAPVERSION_103:
		case RDPGFX_CAPVERSION_104:
		case RDPGFX_CAPVERSION_105:
		case RDPGFX_CAPVERSION_106:
		case RDPGFX_CAPVERSION_107:
#if defined(WITH_GFX_AZURE)
		case RDPGFX_CAPVERSION_111:
		case RDPGFX_CAPVERSION_112:
		case RDPGFX_CAPVERSION_113:
#endif
			return (client->confirmedCaps.flags & RDPGFX_CAPS_FLAG_AVC_DISABLED) == 0;

		default:
			return FALSE;
	}
}

WINPR_ATTR_NODISCARD
static BOOL shadow_avc420_enabled(const rdpShadowClient* client)
{
	if (shadow_avc444_enabled(client))
		return TRUE;
	if (client->confirmedCaps.version != RDPGFX_CAPVERSION_81)
		return FALSE;
	return (client->confirmedCaps.flags & RDPGFX_CAPS_FLAG_AVC420_ENABLED) != 0;
}

/**
 * Function description
 *
 * @return TRUE on success
 */
WINPR_ATTR_NODISCARD
static BOOL shadow_client_send_surface_gfx(rdpShadowClient* client, const BYTE* pSrcData,
                                           UINT32 nSrcStep, UINT32 SrcFormat, UINT16 nXSrc,
                                           UINT16 nYSrc, UINT16 nWidth, UINT16 nHeight)
{
	const rdpContext* context = (const rdpContext*)client;
	RDPGFX_SURFACE_COMMAND cmd = WINPR_C_ARRAY_INIT;
	RDPGFX_START_FRAME_PDU cmdstart = WINPR_C_ARRAY_INIT;
	RDPGFX_END_FRAME_PDU cmdend = WINPR_C_ARRAY_INIT;
	SYSTEMTIME sTime = WINPR_C_ARRAY_INIT;

	if (!context || !pSrcData)
		return FALSE;

	const rdpSettings* settings = context->settings;
	rdpShadowEncoder* encoder = client->encoder;

	if (!settings || !encoder)
		return FALSE;

	if (client->first_frame)
	{
		if (encoder->rfx)
		{
			if (!rfx_context_reset(encoder->rfx, nWidth, nHeight))
				return FALSE;
		}
		client->first_frame = FALSE;
	}

	cmdstart.frameId = shadow_encoder_create_frame_id(encoder);
	GetSystemTime(&sTime);
	cmdstart.timestamp = (UINT32)(sTime.wHour << 22U | sTime.wMinute << 16U | sTime.wSecond << 10U |
	                              sTime.wMilliseconds);
	cmdend.frameId = cmdstart.frameId;
	cmd.surfaceId = client->surfaceId;
	cmd.format = PIXEL_FORMAT_BGRX32;
	cmd.left = nXSrc;
	cmd.top = nYSrc;
	cmd.right = cmd.left + nWidth;
	cmd.bottom = cmd.top + nHeight;
	cmd.width = nWidth;
	cmd.height = nHeight;

#if defined(WITH_GFX_AV1)
	if (freerdp_settings_get_bool(settings, FreeRDP_GfxCodecAV1))
	{
		if (client->confirmedCaps.version == RDPGFX_CAPVERSION_FRDP_1)
		{
			return shadow_client_send_av1(client, pSrcData, nSrcStep, SrcFormat, nWidth, nHeight,
			                              &cmd, &cmdstart, &cmdend);
		}
	}
#endif

	const UINT32 id = freerdp_settings_get_uint32(settings, FreeRDP_RemoteFxCodecId);
#ifdef WITH_GFX_H264
	const BOOL GfxH264 = freerdp_settings_get_bool(settings, FreeRDP_GfxH264);
	const BOOL GfxAVC444 = freerdp_settings_get_bool(settings, FreeRDP_GfxAVC444);
	const BOOL GfxAVC444v2 = freerdp_settings_get_bool(settings, FreeRDP_GfxAVC444v2);
	if (GfxAVC444 || GfxAVC444v2)
	{
		if (shadow_avc444_enabled(client))
		{
			cmd.codecId = GfxAVC444v2 ? RDPGFX_CODECID_AVC444v2 : RDPGFX_CODECID_AVC444;
			return shadow_client_send_avc444(client, pSrcData, nSrcStep, SrcFormat, nWidth, nHeight,
			                                 &cmd, &cmdstart, &cmdend);
		}
	}

	if (GfxH264 && shadow_avc420_enabled(client))
	{
		return shadow_client_send_avc420(client, pSrcData, nSrcStep, SrcFormat, nWidth, nHeight,
		                                 &cmd, &cmdstart, &cmdend);
	}

#endif
	    if (freerdp_settings_get_bool(settings, FreeRDP_RemoteFxCodec) && (id != 0))
	{
		return shadow_client_send_rfx(client, pSrcData, nSrcStep, SrcFormat, nWidth, nHeight, &cmd,
		                              &cmdstart, &cmdend);
	}

	if (freerdp_settings_get_bool(settings, FreeRDP_GfxProgressive))
	{
		return shadow_client_send_progressive(client, pSrcData, nSrcStep, SrcFormat, nWidth,
		                                      nHeight, &cmd, &cmdstart, &cmdend);
	}

	if (freerdp_settings_get_bool(settings, FreeRDP_GfxPlanar))
	{
		return shadow_client_send_planar(client, pSrcData, nSrcStep, SrcFormat, &cmd, &cmdstart,
		                                 &cmdend);
	}

	return shadow_client_send_uncompressed(client, pSrcData, nSrcStep, SrcFormat, &cmd, &cmdstart,
	                                       &cmdend);
}

WINPR_ATTR_NODISCARD
static BOOL stream_surface_bits_supported(const rdpSettings* settings)
{
	const UINT32 supported =
	    freerdp_settings_get_uint32(settings, FreeRDP_SurfaceCommandsSupported);
	return ((supported & SURFCMDS_STREAM_SURFACE_BITS) != 0);
}

WINPR_ATTR_NODISCARD
static BOOL set_surface_bits_supported(const rdpSettings* settings)
{
	const UINT32 supported =
	    freerdp_settings_get_uint32(settings, FreeRDP_SurfaceCommandsSupported);
	return ((supported & SURFCMDS_SET_SURFACE_BITS) != 0);
}

WINPR_ATTR_NODISCARD
static BOOL is_surface_command_supported(const rdpSettings* settings)
{
	if (stream_surface_bits_supported(settings))
	{
		const UINT32 rfxID = freerdp_settings_get_uint32(settings, FreeRDP_RemoteFxCodecId);
		const BOOL supported = freerdp_settings_get_bool(settings, FreeRDP_RemoteFxCodec);
		if (supported && (rfxID != 0))
			return TRUE;
	}
	if (set_surface_bits_supported(settings))
	{
		const UINT32 nsID = freerdp_settings_get_uint32(settings, FreeRDP_NSCodecId);
		const BOOL supported = freerdp_settings_get_bool(settings, FreeRDP_NSCodec);
		if (supported && (nsID != 0))
			return TRUE;
	}
	return FALSE;
}

/**
 * Function description
 *
 * @return TRUE on success
 */
WINPR_ATTR_NODISCARD
static BOOL shadow_client_send_surface_bits(rdpShadowClient* client, BYTE* pSrcData,
                                            UINT32 nSrcStep, UINT16 nXSrc, UINT16 nYSrc,
                                            UINT16 nWidth, UINT16 nHeight)
{
	BOOL ret = TRUE;
	BOOL first = 0;
	BOOL last = 0;
	wStream* s = nullptr;
	size_t numMessages = 0;
	UINT32 frameId = 0;
	rdpUpdate* update = nullptr;
	rdpContext* context = (rdpContext*)client;
	rdpSettings* settings = nullptr;
	rdpShadowEncoder* encoder = nullptr;
	SURFACE_BITS_COMMAND cmd = WINPR_C_ARRAY_INIT;

	if (!context || !pSrcData)
		return FALSE;

	update = context->update;
	settings = context->settings;
	encoder = client->encoder;

	if (!update || !settings || !encoder)
		return FALSE;

	if (encoder->frameAck)
		frameId = shadow_encoder_create_frame_id(encoder);

	// TODO: Check FreeRDP_RemoteFxCodecMode if we should send RFX IMAGE or VIDEO data
	const UINT32 nsID = freerdp_settings_get_uint32(settings, FreeRDP_NSCodecId);
	const UINT32 rfxID = freerdp_settings_get_uint32(settings, FreeRDP_RemoteFxCodecId);
	if (stream_surface_bits_supported(settings) &&
	    freerdp_settings_get_bool(settings, FreeRDP_RemoteFxCodec) && (rfxID != 0))
	{
		RFX_RECT rect = WINPR_C_ARRAY_INIT;

		if (shadow_encoder_prepare(encoder, FREERDP_CODEC_REMOTEFX) < 0)
		{
			WLog_ERR(TAG, "Failed to prepare encoder FREERDP_CODEC_REMOTEFX");
			return FALSE;
		}

		s = encoder->bs;
		rect.x = nXSrc;
		rect.y = nYSrc;
		rect.width = nWidth;
		rect.height = nHeight;

		const UINT32 MultifragMaxRequestSize =
		    freerdp_settings_get_uint32(settings, FreeRDP_MultifragMaxRequestSize);
		RFX_MESSAGE_LIST* messages =
		    rfx_encode_messages(encoder->rfx, &rect, 1, pSrcData,
		                        freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth),
		                        freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight),
		                        nSrcStep, &numMessages, MultifragMaxRequestSize);
		if (!messages)
		{
			WLog_ERR(TAG, "rfx_encode_messages failed");
			return FALSE;
		}

		cmd.cmdType = CMDTYPE_STREAM_SURFACE_BITS;
		WINPR_ASSERT(rfxID <= UINT16_MAX);
		cmd.bmp.codecID = (UINT16)rfxID;
		cmd.destLeft = 0;
		cmd.destTop = 0;
		cmd.destRight = freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth);
		cmd.destBottom = freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight);
		cmd.bmp.bpp = 32;
		cmd.bmp.flags = 0;
		WINPR_ASSERT(freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth) <= UINT16_MAX);
		WINPR_ASSERT(freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight) <= UINT16_MAX);
		cmd.bmp.width = (UINT16)freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth);
		cmd.bmp.height = (UINT16)freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight);
		cmd.skipCompression = TRUE;

		for (size_t i = 0; i < numMessages; i++)
		{
			Stream_ResetPosition(s);

			const RFX_MESSAGE* msg = rfx_message_list_get(messages, i);
			if (!rfx_write_message(encoder->rfx, s, msg))
			{
				WLog_ERR(TAG, "rfx_write_message failed");
				ret = FALSE;
				break;
			}

			cmd.bmp.bitmapDataLength = WINPR_ASSERTING_INT_CAST(UINT32, Stream_GetPosition(s));
			cmd.bmp.bitmapData = Stream_Buffer(s);
			first = (i == 0);
			last = ((i + 1) == numMessages);

			if (!encoder->frameAck)
				IFCALLRET(update->SurfaceBits, ret, update->context, &cmd);
			else
				IFCALLRET(update->SurfaceFrameBits, ret, update->context, &cmd, first, last,
				          frameId);

			if (!ret)
			{
				WLog_ERR(TAG, "Send surface bits(RemoteFxCodec) failed");
				break;
			}
		}

		rfx_message_list_free(messages);
	}
	else if (set_surface_bits_supported(settings) &&
	         freerdp_settings_get_bool(settings, FreeRDP_NSCodec) && (nsID != 0))
	{
		if (shadow_encoder_prepare(encoder, FREERDP_CODEC_NSCODEC) < 0)
		{
			WLog_ERR(TAG, "Failed to prepare encoder FREERDP_CODEC_NSCODEC");
			return FALSE;
		}

		s = encoder->bs;
		Stream_ResetPosition(s);
		pSrcData = &pSrcData[(nYSrc * nSrcStep) + (nXSrc * 4)];
		if (!nsc_compose_message(encoder->nsc, s, pSrcData, nWidth, nHeight, nSrcStep))
			return FALSE;

		cmd.cmdType = CMDTYPE_SET_SURFACE_BITS;
		cmd.bmp.bpp = 32;
		WINPR_ASSERT(nsID <= UINT16_MAX);
		cmd.bmp.codecID = (UINT16)nsID;
		cmd.destLeft = nXSrc;
		cmd.destTop = nYSrc;
		cmd.destRight = cmd.destLeft + nWidth;
		cmd.destBottom = cmd.destTop + nHeight;
		cmd.bmp.width = nWidth;
		cmd.bmp.height = nHeight;

		cmd.bmp.bitmapDataLength = WINPR_ASSERTING_INT_CAST(UINT32, Stream_GetPosition(s));
		cmd.bmp.bitmapData = Stream_Buffer(s);
		first = TRUE;
		last = TRUE;

		if (!encoder->frameAck)
			IFCALLRET(update->SurfaceBits, ret, update->context, &cmd);
		else
			IFCALLRET(update->SurfaceFrameBits, ret, update->context, &cmd, first, last, frameId);

		if (!ret)
		{
			WLog_ERR(TAG, "Send surface bits(NSCodec) failed");
		}
	}

	return ret;
}

/**
 * Function description
 *
 * @return TRUE on success
 */
WINPR_ATTR_NODISCARD
static BOOL shadow_client_send_bitmap_update(rdpShadowClient* client, BYTE* pSrcData,
                                             UINT32 nSrcStep, UINT16 nXSrc, UINT16 nYSrc,
                                             UINT16 nWidth, UINT16 nHeight)
{
	BOOL ret = TRUE;
	BYTE* data = nullptr;
	BYTE* buffer = nullptr;
	UINT32 k = 0;
	UINT32 yIdx = 0;
	UINT32 xIdx = 0;
	UINT32 rows = 0;
	UINT32 cols = 0;
	UINT32 DstSize = 0;
	UINT32 SrcFormat = 0;
	BITMAP_DATA* bitmap = nullptr;
	rdpContext* context = (rdpContext*)client;
	UINT32 totalBitmapSize = 0;
	UINT32 updateSizeEstimate = 0;
	BITMAP_DATA* bitmapData = nullptr;
	BITMAP_UPDATE bitmapUpdate = WINPR_C_ARRAY_INIT;

	if (!context || !pSrcData)
		return FALSE;

	rdpUpdate* update = context->update;
	rdpSettings* settings = context->settings;
	rdpShadowEncoder* encoder = client->encoder;

	if (!update || !settings || !encoder)
		return FALSE;

	const UINT32 maxUpdateSize =
	    freerdp_settings_get_uint32(settings, FreeRDP_MultifragMaxRequestSize);
	if (freerdp_settings_get_uint32(settings, FreeRDP_ColorDepth) < 32)
	{
		if (shadow_encoder_prepare(encoder, FREERDP_CODEC_INTERLEAVED) < 0)
		{
			WLog_ERR(TAG, "Failed to prepare encoder FREERDP_CODEC_INTERLEAVED");
			return FALSE;
		}
	}
	else
	{
		if (shadow_encoder_prepare(encoder, FREERDP_CODEC_PLANAR) < 0)
		{
			WLog_ERR(TAG, "Failed to prepare encoder FREERDP_CODEC_PLANAR");
			return FALSE;
		}
	}

	SrcFormat = PIXEL_FORMAT_BGRX32;

	if ((nXSrc % 4) != 0)
	{
		nWidth += (nXSrc % 4);
		nXSrc -= (nXSrc % 4);
	}

	if ((nYSrc % 4) != 0)
	{
		nHeight += (nYSrc % 4);
		nYSrc -= (nYSrc % 4);
	}

	rows = (nHeight / 64) + ((nHeight % 64) ? 1 : 0);
	cols = (nWidth / 64) + ((nWidth % 64) ? 1 : 0);
	k = 0;
	totalBitmapSize = 0;
	bitmapUpdate.number = rows * cols;

	if (!(bitmapData = (BITMAP_DATA*)calloc(bitmapUpdate.number, sizeof(BITMAP_DATA))))
		return FALSE;

	bitmapUpdate.rectangles = bitmapData;

	if ((nWidth % 4) != 0)
	{
		nWidth += (4 - (nWidth % 4));
	}

	if ((nHeight % 4) != 0)
	{
		nHeight += (4 - (nHeight % 4));
	}

	for (yIdx = 0; yIdx < rows; yIdx++)
	{
		for (xIdx = 0; xIdx < cols; xIdx++)
		{
			bitmap = &bitmapData[k];
			bitmap->width = 64;
			bitmap->height = 64;
			bitmap->destLeft = nXSrc + (xIdx * 64);
			bitmap->destTop = nYSrc + (yIdx * 64);

			if (((INT64)bitmap->destLeft + bitmap->width) > (nXSrc + nWidth))
				bitmap->width = (UINT32)(nXSrc + nWidth) - bitmap->destLeft;

			if (((INT64)bitmap->destTop + bitmap->height) > (nYSrc + nHeight))
				bitmap->height = (UINT32)(nYSrc + nHeight) - bitmap->destTop;

			bitmap->destRight = bitmap->destLeft + bitmap->width - 1;
			bitmap->destBottom = bitmap->destTop + bitmap->height - 1;
			bitmap->compressed = TRUE;

			if ((bitmap->width < 4) || (bitmap->height < 4))
				continue;

			if (freerdp_settings_get_uint32(settings, FreeRDP_ColorDepth) < 32)
			{
				UINT32 bitsPerPixel = freerdp_settings_get_uint32(settings, FreeRDP_ColorDepth);
				UINT32 bytesPerPixel = (bitsPerPixel + 7) / 8;
				DstSize = 64 * 64 * 4;
				buffer = encoder->grid[k];

				ret = interleaved_compress(
				    encoder->interleaved, buffer, &DstSize, bitmap->width, bitmap->height, pSrcData,
				    SrcFormat, nSrcStep, bitmap->destLeft, bitmap->destTop, nullptr, bitsPerPixel);
				if (!ret)
					goto out;
				bitmap->bitmapDataStream = buffer;
				bitmap->bitmapLength = DstSize;
				bitmap->bitsPerPixel = bitsPerPixel;
				bitmap->cbScanWidth = bitmap->width * bytesPerPixel;
				bitmap->cbUncompressedSize = bitmap->width * bitmap->height * bytesPerPixel;
			}
			else
			{
				UINT32 dstSize = 0;
				buffer = encoder->grid[k];
				data = &pSrcData[(bitmap->destTop * nSrcStep) + (bitmap->destLeft * 4)];

				buffer =
				    freerdp_bitmap_compress_planar(encoder->planar, data, SrcFormat, bitmap->width,
				                                   bitmap->height, nSrcStep, buffer, &dstSize);
				bitmap->bitmapDataStream = buffer;
				bitmap->bitmapLength = dstSize;
				bitmap->bitsPerPixel = 32;
				bitmap->cbScanWidth = bitmap->width * 4;
				bitmap->cbUncompressedSize = bitmap->width * bitmap->height * 4;
			}

			bitmap->cbCompFirstRowSize = 0;
			bitmap->cbCompMainBodySize = bitmap->bitmapLength;
			totalBitmapSize += bitmap->bitmapLength;
			k++;
		}
	}

	bitmapUpdate.number = k;
	updateSizeEstimate = totalBitmapSize + (k * bitmapUpdate.number) + 16;

	if (updateSizeEstimate > maxUpdateSize)
	{
		UINT32 i = 0;
		UINT32 j = 0;
		UINT32 updateSize = 0;
		UINT32 newUpdateSize = 0;
		BITMAP_DATA* fragBitmapData = nullptr;

		if (k > 0)
			fragBitmapData = (BITMAP_DATA*)calloc(k, sizeof(BITMAP_DATA));

		if (!fragBitmapData)
		{
			WLog_ERR(TAG, "Failed to allocate memory for fragBitmapData");
			ret = FALSE;
			goto out;
		}

		bitmapUpdate.rectangles = fragBitmapData;
		i = j = 0;
		updateSize = 1024;

		while (i < k)
		{
			newUpdateSize = updateSize + (bitmapData[i].bitmapLength + 16);

			if (newUpdateSize < maxUpdateSize)
			{
				CopyMemory(&fragBitmapData[j++], &bitmapData[i++], sizeof(BITMAP_DATA));
				updateSize = newUpdateSize;
			}

			if ((newUpdateSize >= maxUpdateSize) || (i + 1) >= k)
			{
				bitmapUpdate.number = j;
				ret = BitmapUpdateProxy(client, &bitmapUpdate);

				if (!ret)
					break;

				updateSize = 1024;
				j = 0;
			}
		}

		free(fragBitmapData);
	}
	else
	{
		ret = BitmapUpdateProxy(client, &bitmapUpdate);
	}

out:
	free(bitmapData);
	return ret;
}

/**
 * Function description
 *
 * @return TRUE on success (or nothing need to be updated)
 */
WINPR_ATTR_NODISCARD
static BOOL shadow_client_send_surface_update(rdpShadowClient* client, SHADOW_GFX_STATUS* pStatus)
{
	BOOL ret = TRUE;
	INT64 nXSrc = 0;
	INT64 nYSrc = 0;
	INT64 nWidth = 0;
	INT64 nHeight = 0;
	rdpContext* context = (rdpContext*)client;
	rdpSettings* settings = nullptr;
	rdpShadowServer* server = nullptr;
	rdpShadowSurface* surface = nullptr;
	REGION16 invalidRegion;
	RECTANGLE_16 surfaceRect;
	const RECTANGLE_16* extents = nullptr;
	BYTE* pSrcData = nullptr;
	UINT32 nSrcStep = 0;
	UINT32 SrcFormat = 0;
	UINT32 numRects = 0;
	const RECTANGLE_16* rects = nullptr;

	if (!context || !pStatus)
		return FALSE;

	settings = context->settings;
	server = client->server;

	if (!settings || !server)
		return FALSE;

	surface = client->inLobby ? server->lobby : server->surface;

	if (!surface)
		return FALSE;

	{
		EnterCriticalSection(&(client->lock));
		region16_init(&invalidRegion);

		const BOOL res = region16_copy(&invalidRegion, &(client->invalidRegion));
		region16_clear(&(client->invalidRegion));
		LeaveCriticalSection(&(client->lock));
		if (!res)
			goto out;
	}

	EnterCriticalSection(&surface->lock);
	rects = region16_rects(&(surface->invalidRegion), &numRects);

	for (UINT32 index = 0; index < numRects; index++)
	{
		if (!region16_union_rect(&invalidRegion, &invalidRegion, &rects[index]))
			goto out;
	}

	surfaceRect.left = 0;
	surfaceRect.top = 0;
	WINPR_ASSERT(surface->width <= UINT16_MAX);
	WINPR_ASSERT(surface->height <= UINT16_MAX);
	surfaceRect.right = (UINT16)surface->width;
	surfaceRect.bottom = (UINT16)surface->height;
	if (!region16_intersect_rect(&invalidRegion, &invalidRegion, &surfaceRect))
		goto out;

	if (server->shareSubRect)
	{
		if (!region16_intersect_rect(&invalidRegion, &invalidRegion, &(server->subRect)))
			goto out;
	}

	if (region16_is_empty(&invalidRegion))
	{
		/* No image region need to be updated. Success */
		goto out;
	}

	extents = region16_extents(&invalidRegion);
	nXSrc = extents->left;
	nYSrc = extents->top;
	nWidth = extents->right - extents->left;
	nHeight = extents->bottom - extents->top;
	pSrcData = surface->data;
	nSrcStep = surface->scanline;
	SrcFormat = surface->format;

	/* Move to new pSrcData / nXSrc / nYSrc according to sub rect */
	if (server->shareSubRect)
	{
		INT32 subX = 0;
		INT32 subY = 0;
		subX = server->subRect.left;
		subY = server->subRect.top;
		nXSrc -= subX;
		nYSrc -= subY;
		WINPR_ASSERT(nXSrc >= 0);
		WINPR_ASSERT(nXSrc <= UINT16_MAX);
		WINPR_ASSERT(nYSrc >= 0);
		WINPR_ASSERT(nYSrc <= UINT16_MAX);
		pSrcData = &pSrcData[((UINT16)subY * nSrcStep) + ((UINT16)subX * 4U)];
	}

	// WLog_INFO(TAG, "shadow_client_send_surface_update: x: %" PRId64 " y: %" PRId64 " width: %"
	// PRId64 " height: %" PRId64 " right: %" PRId64 " bottom: %" PRId64, 	nXSrc, nYSrc, nWidth,
	// nHeight, nXSrc + nWidth, nYSrc + nHeight);

	if (freerdp_settings_get_bool(settings, FreeRDP_SupportGraphicsPipeline))
	{
		if (pStatus->gfxOpened && client->areGfxCapsReady)
		{
			/* GFX/h264 always full screen encoded */
			nWidth = freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth);
			nHeight = freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight);

			/* Create primary surface if have not */
			if (!pStatus->gfxSurfaceCreated)
			{
				/* Only init surface when we have h264 supported */
				if (!(ret = shadow_client_rdpgfx_reset_graphic(client)))
					goto out;

				if (!(ret = shadow_client_rdpgfx_new_surface(client)))
					goto out;

				pStatus->gfxSurfaceCreated = TRUE;
			}

			WINPR_ASSERT(nWidth >= 0);
			WINPR_ASSERT(nWidth <= UINT16_MAX);
			WINPR_ASSERT(nHeight >= 0);
			WINPR_ASSERT(nHeight <= UINT16_MAX);
			ret = shadow_client_send_surface_gfx(client, pSrcData, nSrcStep, SrcFormat, 0, 0,
			                                     (UINT16)nWidth, (UINT16)nHeight);
		}
		else
		{
			ret = TRUE;
		}
	}
	else if (is_surface_command_supported(settings))
	{
		WINPR_ASSERT(nXSrc >= 0);
		WINPR_ASSERT(nXSrc <= UINT16_MAX);
		WINPR_ASSERT(nYSrc >= 0);
		WINPR_ASSERT(nYSrc <= UINT16_MAX);
		WINPR_ASSERT(nWidth >= 0);
		WINPR_ASSERT(nWidth <= UINT16_MAX);
		WINPR_ASSERT(nHeight >= 0);
		WINPR_ASSERT(nHeight <= UINT16_MAX);
		ret = shadow_client_send_surface_bits(client, pSrcData, nSrcStep, (UINT16)nXSrc,
		                                      (UINT16)nYSrc, (UINT16)nWidth, (UINT16)nHeight);
	}
	else
	{
		WINPR_ASSERT(nXSrc >= 0);
		WINPR_ASSERT(nXSrc <= UINT16_MAX);
		WINPR_ASSERT(nYSrc >= 0);
		WINPR_ASSERT(nYSrc <= UINT16_MAX);
		WINPR_ASSERT(nWidth >= 0);
		WINPR_ASSERT(nWidth <= UINT16_MAX);
		WINPR_ASSERT(nHeight >= 0);
		WINPR_ASSERT(nHeight <= UINT16_MAX);
		ret = shadow_client_send_bitmap_update(client, pSrcData, nSrcStep, (UINT16)nXSrc,
		                                       (UINT16)nYSrc, (UINT16)nWidth, (UINT16)nHeight);
	}

out:
	LeaveCriticalSection(&surface->lock);
	region16_uninit(&invalidRegion);
	return ret;
}

/**
 * Function description
 * Notify client for resize. The new desktop width/height
 * should have already been updated in rdpSettings.
 *
 * @return TRUE on success
 */
WINPR_ATTR_NODISCARD
static BOOL shadow_client_send_resize(rdpShadowClient* client, SHADOW_GFX_STATUS* pStatus)
{
	rdpContext* context = (rdpContext*)client;
	rdpSettings* settings = nullptr;
	freerdp_peer* peer = nullptr;

	if (!context || !pStatus)
		return FALSE;

	peer = context->peer;
	settings = context->settings;

	if (!peer || !settings)
		return FALSE;

	/**
	 * Unset client activated flag to avoid sending update message during
	 * resize. DesktopResize will reactive the client and
	 * shadow_client_activate would be invoked later.
	 */
	client->activated = FALSE;

	/* Close Gfx surfaces */
	if (pStatus->gfxSurfaceCreated)
	{
		if (!shadow_client_rdpgfx_release_surface(client))
			return FALSE;

		pStatus->gfxSurfaceCreated = FALSE;
	}

	/* Send Resize */
	if (!shadow_send_desktop_resize(client))
		return FALSE;
	shadow_reset_desktop_resize(client);

	/* Clear my invalidRegion. shadow_client_activate refreshes fullscreen */
	EnterCriticalSection(&(client->lock));
	region16_clear(&(client->invalidRegion));
	LeaveCriticalSection(&(client->lock));
	return TRUE;
}

/**
 * Function description
 * Mark invalid region for client
 *
 * @return TRUE on success
 */
WINPR_ATTR_NODISCARD
static BOOL shadow_client_surface_update(rdpShadowClient* client, REGION16* region)
{
	UINT32 numRects = 0;
	const RECTANGLE_16* rects = nullptr;
	rects = region16_rects(region, &numRects);
	shadow_client_mark_invalid(client, numRects, rects);
	return TRUE;
}

/**
 * Function description
 * Only union invalid region from server surface
 *
 * @return TRUE on success
 */
WINPR_ATTR_NODISCARD
static inline BOOL shadow_client_no_surface_update(rdpShadowClient* client,
                                                   SHADOW_GFX_STATUS* pStatus)
{
	rdpShadowServer* server = nullptr;
	rdpShadowSurface* surface = nullptr;
	WINPR_UNUSED(pStatus);
	WINPR_ASSERT(client);
	server = client->server;
	WINPR_ASSERT(server);
	surface = client->inLobby ? server->lobby : server->surface;
	EnterCriticalSection(&surface->lock);
	const BOOL rc = shadow_client_surface_update(client, &(surface->invalidRegion));
	LeaveCriticalSection(&surface->lock);
	return rc;
}

WINPR_ATTR_NODISCARD
static int shadow_client_subsystem_process_message(rdpShadowClient* client, wMessage* message)
{
	rdpContext* context = (rdpContext*)client;

	WINPR_ASSERT(message);
	WINPR_ASSERT(context);

	rdpUpdate* update = context->update;
	WINPR_ASSERT(update);

	/* FIXME: the pointer updates appear to be broken when used with bulk compression and mstsc */

	switch (message->id)
	{
		case SHADOW_MSG_OUT_POINTER_POSITION_UPDATE_ID:
		{
			POINTER_POSITION_UPDATE pointerPosition;
			const SHADOW_MSG_OUT_POINTER_POSITION_UPDATE* msg =
			    (const SHADOW_MSG_OUT_POINTER_POSITION_UPDATE*)message->wParam;
			pointerPosition.xPos = msg->xPos;
			pointerPosition.yPos = msg->yPos;

			WINPR_ASSERT(client->server);
			if (client->server->shareSubRect)
			{
				pointerPosition.xPos -= client->server->subRect.left;
				pointerPosition.yPos -= client->server->subRect.top;
			}

			if (client->activated)
			{
				if ((msg->xPos != client->pointerX) || (msg->yPos != client->pointerY))
				{
					WINPR_ASSERT(update->pointer);
					if (!IFCALLRESULT(TRUE, update->pointer->PointerPosition, context,
					                  &pointerPosition))
						return -1;
					client->pointerX = msg->xPos;
					client->pointerY = msg->yPos;
				}
			}

			break;
		}

		case SHADOW_MSG_OUT_POINTER_ALPHA_UPDATE_ID:
		{
			POINTER_NEW_UPDATE pointerNew = WINPR_C_ARRAY_INIT;
			POINTER_COLOR_UPDATE* pointerColor = WINPR_C_ARRAY_INIT;
			POINTER_CACHED_UPDATE pointerCached = WINPR_C_ARRAY_INIT;
			const SHADOW_MSG_OUT_POINTER_ALPHA_UPDATE* msg =
			    (const SHADOW_MSG_OUT_POINTER_ALPHA_UPDATE*)message->wParam;

			WINPR_ASSERT(msg);
			pointerNew.xorBpp = 24;
			pointerColor = &(pointerNew.colorPtrAttr);
			pointerColor->cacheIndex = 0;
			pointerColor->hotSpotX = WINPR_ASSERTING_INT_CAST(UINT16, msg->xHot);
			pointerColor->hotSpotY = WINPR_ASSERTING_INT_CAST(UINT16, msg->yHot);
			pointerColor->width = WINPR_ASSERTING_INT_CAST(UINT16, msg->width);
			pointerColor->height = WINPR_ASSERTING_INT_CAST(UINT16, msg->height);
			pointerColor->lengthAndMask = WINPR_ASSERTING_INT_CAST(UINT16, msg->lengthAndMask);
			pointerColor->lengthXorMask = WINPR_ASSERTING_INT_CAST(UINT16, msg->lengthXorMask);
			pointerColor->xorMaskData = msg->xorMaskData;
			pointerColor->andMaskData = msg->andMaskData;
			pointerCached.cacheIndex = pointerColor->cacheIndex;

			if (client->activated)
			{
				if (client->server->ShowMouseCursor)
				{
					if (!IFCALLRESULT(TRUE, update->pointer->PointerNew, context, &pointerNew))
						return -1;
					if (!IFCALLRESULT(TRUE, update->pointer->PointerCached, context,
					                  &pointerCached))
						return -1;
				}
				else
				{
					POINTER_SYSTEM_UPDATE pointer_system = WINPR_C_ARRAY_INIT;
					pointer_system.type = SYSPTR_DEFAULT;
					if (!IFCALLRESULT(TRUE, update->pointer->PointerSystem, context,
					                  &pointer_system))
						return -1;
				}
			}

			break;
		}

		case SHADOW_MSG_OUT_AUDIO_OUT_SAMPLES_ID:
		{
			const SHADOW_MSG_OUT_AUDIO_OUT_SAMPLES* msg =
			    (const SHADOW_MSG_OUT_AUDIO_OUT_SAMPLES*)message->wParam;

			WINPR_ASSERT(msg);

			if (client->activated && client->rdpsnd && client->rdpsnd->Activated)
			{
				client->rdpsnd->src_format = msg->audio_format;

				const UINT error =
				    IFCALLRESULT(CHANNEL_RC_OK, client->rdpsnd->SendSamples, client->rdpsnd,
				                 msg->buf, msg->nFrames, msg->wTimestamp);
				if (CHANNEL_RC_OK != error)
					return -1;
			}

			break;
		}

		case SHADOW_MSG_OUT_AUDIO_OUT_VOLUME_ID:
		{
			const SHADOW_MSG_OUT_AUDIO_OUT_VOLUME* msg =
			    (const SHADOW_MSG_OUT_AUDIO_OUT_VOLUME*)message->wParam;

			if (client->activated && client->rdpsnd && client->rdpsnd->Activated)
			{
				const UINT error = IFCALLRESULT(CHANNEL_RC_OK, client->rdpsnd->SetVolume,
				                                client->rdpsnd, msg->left, msg->right);
				if (CHANNEL_RC_OK != error)
					return -1;
			}

			break;
		}

		default:
			WLog_ERR(TAG, "Unknown message id: %" PRIu32 "", message->id);
			break;
	}

	shadow_client_free_queued_message(message);
	return 1;
}

WINPR_ATTR_NODISCARD
static DWORD WINAPI shadow_client_thread(LPVOID arg)
{
	rdpShadowClient* client = (rdpShadowClient*)arg;
	BOOL rc = FALSE;
	DWORD status = 0;
	wMessage message = WINPR_C_ARRAY_INIT;
	wMessage pointerPositionMsg = WINPR_C_ARRAY_INIT;
	wMessage pointerAlphaMsg = WINPR_C_ARRAY_INIT;
	wMessage audioVolumeMsg = WINPR_C_ARRAY_INIT;
	HANDLE ChannelEvent = nullptr;
	void* UpdateSubscriber = nullptr;
	HANDLE UpdateEvent = nullptr;
	freerdp_peer* peer = nullptr;
	rdpContext* context = nullptr;
	rdpSettings* settings = nullptr;
	rdpShadowServer* server = nullptr;
	rdpShadowSubsystem* subsystem = nullptr;
	wMessageQueue* MsgQueue = nullptr;
	/* This should only be visited in client thread */
	SHADOW_GFX_STATUS gfxstatus = WINPR_C_ARRAY_INIT;
	rdpUpdate* update = nullptr;

	WINPR_ASSERT(client);

	MsgQueue = client->MsgQueue;
	WINPR_ASSERT(MsgQueue);

	server = client->server;
	WINPR_ASSERT(server);
	subsystem = server->subsystem;
	context = (rdpContext*)client;
	peer = context->peer;
	WINPR_ASSERT(peer);
	WINPR_ASSERT(peer->context);

	settings = peer->context->settings;
	WINPR_ASSERT(settings);

	peer->Capabilities = shadow_client_capabilities;
	peer->PostConnect = shadow_client_post_connect;
	peer->Activate = shadow_client_activate;
	peer->Logon = shadow_client_logon;
	shadow_input_register_callbacks(peer->context->input);

	rc = peer->Initialize(peer);
	if (!rc)
		goto out;

	update = peer->context->update;
	WINPR_ASSERT(update);

	update->RefreshRect = shadow_client_refresh_rect;
	update->SuppressOutput = shadow_client_suppress_output;
	update->SurfaceFrameAcknowledge = shadow_client_surface_frame_acknowledge;

	if ((!client->vcm) || (!subsystem->updateEvent))
		goto out;

	UpdateSubscriber = shadow_multiclient_get_subscriber(subsystem->updateEvent);

	if (!UpdateSubscriber)
		goto out;

	UpdateEvent = shadow_multiclient_getevent(UpdateSubscriber);
	WINPR_ASSERT(UpdateEvent);

	ChannelEvent = WTSVirtualChannelManagerGetEventHandle(client->vcm);
	WINPR_ASSERT(ChannelEvent);

	rc = freerdp_settings_set_bool(settings, FreeRDP_UnicodeInput, TRUE);
	WINPR_ASSERT(rc);
	rc = freerdp_settings_set_bool(settings, FreeRDP_HasHorizontalWheel, TRUE);
	WINPR_ASSERT(rc);
	rc = freerdp_settings_set_bool(settings, FreeRDP_HasExtendedMouseEvent, TRUE);
	WINPR_ASSERT(rc);
	rc = freerdp_settings_set_bool(settings, FreeRDP_SupportMonitorLayoutPdu, TRUE);
	WINPR_ASSERT(rc);
	while (1)
	{
		HANDLE events[MAXIMUM_WAIT_OBJECTS] = WINPR_C_ARRAY_INIT;
		DWORD nCount = 0;
		events[nCount++] = UpdateEvent;
		{
			DWORD tmp = peer->GetEventHandles(peer, &events[nCount], 64 - nCount);

			if (tmp == 0)
			{
				WLog_ERR(TAG, "Failed to get FreeRDP transport event handles");
				goto fail;
			}

			nCount += tmp;
		}
		events[nCount++] = ChannelEvent;
		events[nCount++] = MessageQueue_Event(MsgQueue);

#if defined(CHANNEL_RDPGFX_SERVER)
		HANDLE gfxevent = rdpgfx_server_get_event_handle(client->rdpgfx);

		if (gfxevent)
			events[nCount++] = gfxevent;
#endif

		status = WaitForMultipleObjects(nCount, events, FALSE, INFINITE);

		if (status == WAIT_FAILED)
			goto fail;

		if (WaitForSingleObject(UpdateEvent, 0) == WAIT_OBJECT_0)
		{
			/* The UpdateEvent means to start sending current frame. It is
			 * triggered from subsystem implementation and it should ensure
			 * that the screen and primary surface meta data (width, height,
			 * scanline, invalid region, etc) is not changed until it is reset
			 * (at shadow_multiclient_consume). As best practice, subsystem
			 * implementation should invoke shadow_subsystem_frame_update which
			 * triggers the event and then wait for completion */
			if (client->activated && !client->suppressOutput)
			{
				/* Send screen update or resize to this client */

				/* Check resize */
				if (shadow_client_recalc_desktop_size(client))
				{
					/* Screen size changed, do resize */
					if (!shadow_client_send_resize(client, &gfxstatus))
					{
						WLog_ERR(TAG, "Failed to send resize message");
						break;
					}
				}
				else
				{
					/* Send frame */
					if (!shadow_client_send_surface_update(client, &gfxstatus))
					{
						WLog_ERR(TAG, "Failed to send surface update");
						break;
					}
				}
			}
			else
			{
				/* Our client don't receive graphic updates. Just save the invalid region */
				if (!shadow_client_no_surface_update(client, &gfxstatus))
				{
					WLog_ERR(TAG, "Failed to handle surface update");
					break;
				}
			}

			/*
			 * The return value of shadow_multiclient_consume is whether or not
			 * the subscriber really consumes the event. It's not cared currently.
			 */
			(void)shadow_multiclient_consume(UpdateSubscriber);
		}

		WINPR_ASSERT(peer->CheckFileDescriptor);
		if (!peer->CheckFileDescriptor(peer))
		{
			WLog_ERR(TAG, "Failed to check FreeRDP file descriptor");
			goto fail;
		}

		if (client->activated &&
		    WTSVirtualChannelManagerIsChannelJoined(client->vcm, DRDYNVC_SVC_CHANNEL_NAME))
		{
			switch (WTSVirtualChannelManagerGetDrdynvcState(client->vcm))
			{
				/* Dynamic channel status may have been changed after processing */
				case DRDYNVC_STATE_NONE:

					/* Call this routine to Initialize drdynvc channel */
					if (!WTSVirtualChannelManagerCheckFileDescriptor(client->vcm))
					{
						WLog_ERR(TAG, "Failed to initialize drdynvc channel");
						goto fail;
					}

					break;

				case DRDYNVC_STATE_READY:
#if defined(CHANNEL_AUDIN_SERVER)
					if (client->audin && !IFCALLRESULT(TRUE, client->audin->IsOpen, client->audin))
					{
						if (!IFCALLRESULT(FALSE, client->audin->Open, client->audin))
						{
							WLog_ERR(TAG, "Failed to initialize audin channel");
							goto fail;
						}
					}
#endif

					/* Init RDPGFX dynamic channel */
					if (freerdp_settings_get_bool(settings, FreeRDP_SupportGraphicsPipeline) &&
					    client->rdpgfx && !gfxstatus.gfxOpened)
					{
						client->rdpgfx->FrameAcknowledge = shadow_client_rdpgfx_frame_acknowledge;
						client->rdpgfx->CapsAdvertise = shadow_client_rdpgfx_caps_advertise;

						if (!client->rdpgfx->Open(client->rdpgfx))
						{
							WLog_WARN(TAG, "Failed to open GraphicsPipeline");
							if (!freerdp_settings_set_bool(settings,
							                               FreeRDP_SupportGraphicsPipeline, FALSE))
								goto fail;
						}
						else
						{
							gfxstatus.gfxOpened = TRUE;
							WLog_INFO(TAG, "Gfx Pipeline Opened");
						}
					}

					break;

				default:
					break;
			}
		}

		if (WaitForSingleObject(ChannelEvent, 0) == WAIT_OBJECT_0)
		{
			if (!WTSVirtualChannelManagerCheckFileDescriptor(client->vcm))
			{
				WLog_ERR(TAG, "WTSVirtualChannelManagerCheckFileDescriptor failure");
				goto fail;
			}
		}

#if defined(CHANNEL_RDPGFX_SERVER)
		if (gfxevent)
		{
			if (WaitForSingleObject(gfxevent, 0) == WAIT_OBJECT_0)
			{
				const UINT error = rdpgfx_server_handle_messages(client->rdpgfx);
				if (error != CHANNEL_RC_OK)
					goto fail;
			}
		}
#endif

		if (WaitForSingleObject(MessageQueue_Event(MsgQueue), 0) == WAIT_OBJECT_0)
		{
			/* Drain messages. Pointer update could be accumulated. */
			pointerPositionMsg.id = 0;
			pointerPositionMsg.Free = nullptr;
			pointerAlphaMsg.id = 0;
			pointerAlphaMsg.Free = nullptr;
			audioVolumeMsg.id = 0;
			audioVolumeMsg.Free = nullptr;

			while (MessageQueue_Peek(MsgQueue, &message, TRUE))
			{
				if (message.id == WMQ_QUIT)
				{
					break;
				}

				switch (message.id)
				{
					case SHADOW_MSG_OUT_POINTER_POSITION_UPDATE_ID:
						/* Abandon previous message */
						shadow_client_free_queued_message(&pointerPositionMsg);
						pointerPositionMsg = message;
						break;

					case SHADOW_MSG_OUT_POINTER_ALPHA_UPDATE_ID:
						/* Abandon previous message */
						shadow_client_free_queued_message(&pointerAlphaMsg);
						pointerAlphaMsg = message;
						break;

					case SHADOW_MSG_OUT_AUDIO_OUT_VOLUME_ID:
						/* Abandon previous message */
						shadow_client_free_queued_message(&audioVolumeMsg);
						audioVolumeMsg = message;
						break;

					default:
						if (!shadow_client_subsystem_process_message(client, &message))
							goto fail;
						break;
				}
			}

			if (message.id == WMQ_QUIT)
			{
				/* Release stored message */
				shadow_client_free_queued_message(&pointerPositionMsg);
				shadow_client_free_queued_message(&pointerAlphaMsg);
				shadow_client_free_queued_message(&audioVolumeMsg);
				goto fail;
			}
			else
			{
				/* Process accumulated messages if needed */
				if (pointerPositionMsg.id)
				{
					if (!shadow_client_subsystem_process_message(client, &pointerPositionMsg))
						goto fail;
				}

				if (pointerAlphaMsg.id)
				{
					if (!shadow_client_subsystem_process_message(client, &pointerAlphaMsg))
						goto fail;
				}

				if (audioVolumeMsg.id)
				{
					if (!shadow_client_subsystem_process_message(client, &audioVolumeMsg))
						goto fail;
				}
			}
		}
	}

fail:

	/* Free channels early because we establish channels in post connect */
#if defined(CHANNEL_AUDIN_SERVER)
	if (client->audin && !IFCALLRESULT(TRUE, client->audin->IsOpen, client->audin))
	{
		if (!IFCALLRESULT(FALSE, client->audin->Close, client->audin))
		{
			WLog_WARN(TAG, "AUDIN shutdown failure!");
		}
	}
#endif

	if (gfxstatus.gfxOpened)
	{
		if (gfxstatus.gfxSurfaceCreated)
		{
			if (!shadow_client_rdpgfx_release_surface(client))
				WLog_WARN(TAG, "GFX release surface failure!");
		}

		WINPR_ASSERT(client->rdpgfx);
		WINPR_ASSERT(client->rdpgfx->Close);
		rc = client->rdpgfx->Close(client->rdpgfx);
		WINPR_ASSERT(rc);
	}

	shadow_client_channels_free(client);

	if (UpdateSubscriber)
	{
		shadow_multiclient_release_subscriber(UpdateSubscriber);
		UpdateSubscriber = nullptr;
	}

	if (peer->connected && subsystem->ClientDisconnect)
	{
		subsystem->ClientDisconnect(subsystem, client);
	}

out:
	WINPR_ASSERT(peer->Disconnect);
	peer->Disconnect(peer);
	freerdp_peer_context_free(peer);
	freerdp_peer_free(peer);
	ExitThread(0);
	return 0;
}

BOOL shadow_client_accepted(freerdp_listener* listener, freerdp_peer* peer)
{
	rdpShadowClient* client = nullptr;
	rdpShadowServer* server = nullptr;

	if (!listener || !peer)
		return FALSE;

	server = (rdpShadowServer*)listener->info;
	WINPR_ASSERT(server);

	peer->ContextExtra = (void*)server;
	peer->ContextSize = sizeof(rdpShadowClient);
	peer->ContextNew = shadow_client_context_new;
	peer->ContextFree = shadow_client_context_free;

	if (!freerdp_peer_context_new_ex(peer, server->settings))
		return FALSE;

	client = (rdpShadowClient*)peer->context;
	WINPR_ASSERT(client);

	if (!(client->thread = CreateThread(nullptr, 0, shadow_client_thread, client, 0, nullptr)))
	{
		freerdp_peer_context_free(peer);
		return FALSE;
	}
	else
	{
		/* Close the thread handle to make it detached. */
		(void)CloseHandle(client->thread);
		client->thread = nullptr;
	}

	return TRUE;
}

static void shadow_msg_out_addref(wMessage* message)
{
	SHADOW_MSG_OUT* msg = nullptr;

	WINPR_ASSERT(message);
	msg = (SHADOW_MSG_OUT*)message->wParam;
	WINPR_ASSERT(msg);

	InterlockedIncrement(&(msg->refCount));
}

static void shadow_msg_out_release(wMessage* message)
{
	SHADOW_MSG_OUT* msg = nullptr;

	WINPR_ASSERT(message);
	msg = (SHADOW_MSG_OUT*)message->wParam;
	WINPR_ASSERT(msg);

	if (InterlockedDecrement(&(msg->refCount)) <= 0)
	{
		IFCALL(msg->Free, message->id, msg);
	}
}

WINPR_ATTR_NODISCARD
static BOOL shadow_client_dispatch_msg(rdpShadowClient* client, wMessage* message)
{
	if (!client || !message)
		return FALSE;

	/* Add reference when it is posted */
	shadow_msg_out_addref(message);

	WINPR_ASSERT(client->MsgQueue);
	if (MessageQueue_Dispatch(client->MsgQueue, message))
		return TRUE;
	else
	{
		/* Release the reference since post failed */
		shadow_msg_out_release(message);
		return FALSE;
	}
}

BOOL shadow_client_post_msg(rdpShadowClient* client, void* context, UINT32 type,
                            SHADOW_MSG_OUT* msg, void* lParam)
{
	wMessage message = WINPR_C_ARRAY_INIT;
	message.context = context;
	message.id = type;
	message.wParam = (void*)msg;
	message.lParam = lParam;
	message.Free = shadow_msg_out_release;
	return shadow_client_dispatch_msg(client, &message);
}

int shadow_client_boardcast_msg(rdpShadowServer* server, void* context, UINT32 type,
                                SHADOW_MSG_OUT* msg, void* lParam)
{
	wMessage message = WINPR_C_ARRAY_INIT;
	int count = 0;

	WINPR_ASSERT(server);
	WINPR_ASSERT(msg);

	message.context = context;
	message.id = type;
	message.wParam = (void*)msg;
	message.lParam = lParam;
	message.Free = shadow_msg_out_release;
	/* First add reference as we reference it in this function.
	 * Therefore it would not be free'ed during post. */
	shadow_msg_out_addref(&message);

	WINPR_ASSERT(server->clients);
	ArrayList_Lock(server->clients);

	for (size_t index = 0; index < ArrayList_Count(server->clients); index++)
	{
		rdpShadowClient* client = (rdpShadowClient*)ArrayList_GetItem(server->clients, index);

		if (shadow_client_dispatch_msg(client, &message))
		{
			count++;
		}
	}

	ArrayList_Unlock(server->clients);
	/* Release the reference for this function */
	shadow_msg_out_release(&message);
	return count;
}

int shadow_client_boardcast_quit(rdpShadowServer* server, int nExitCode)
{
	wMessageQueue* queue = nullptr;
	int count = 0;

	WINPR_ASSERT(server);
	WINPR_ASSERT(server->clients);

	ArrayList_Lock(server->clients);

	for (size_t index = 0; index < ArrayList_Count(server->clients); index++)
	{
		queue = ((rdpShadowClient*)ArrayList_GetItem(server->clients, index))->MsgQueue;

		if (MessageQueue_PostQuit(queue, nExitCode))
		{
			count++;
		}
	}

	ArrayList_Unlock(server->clients);
	return count;
}
