/**
 * FreeRDP: A Remote Desktop Protocol Implementation
 * X11 RAIL
 *
 * 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 <freerdp/config.h>

#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xutil.h>

#include <winpr/cast.h>
#include <winpr/assert.h>
#include <winpr/wlog.h>
#include <winpr/print.h>

#include <freerdp/client/rail.h>

#include "xf_window.h"
#include "xf_rail.h"
#include "xf_utils.h"

#include <freerdp/log.h>
#define TAG CLIENT_TAG("x11")

static const char* error_code2str(UINT32 code)
{
#define EVCASE(x) \
	case x:       \
		return #x
	switch (code)
	{
		EVCASE(RAIL_EXEC_S_OK);
		EVCASE(RAIL_EXEC_E_HOOK_NOT_LOADED);
		EVCASE(RAIL_EXEC_E_DECODE_FAILED);
		EVCASE(RAIL_EXEC_E_NOT_IN_ALLOWLIST);
		EVCASE(RAIL_EXEC_E_FILE_NOT_FOUND);
		EVCASE(RAIL_EXEC_E_FAIL);
		EVCASE(RAIL_EXEC_E_SESSION_LOCKED);
		default:
			return "RAIL_EXEC_E_UNKNOWN";
	}
#undef EVCASE
}

static const char* movetype2str(UINT32 code)
{
#define EVCASE(x) \
	case x:       \
		return #x

	switch (code)
	{

		EVCASE(RAIL_WMSZ_LEFT);
		EVCASE(RAIL_WMSZ_RIGHT);
		EVCASE(RAIL_WMSZ_TOP);
		EVCASE(RAIL_WMSZ_TOPLEFT);
		EVCASE(RAIL_WMSZ_TOPRIGHT);
		EVCASE(RAIL_WMSZ_BOTTOM);
		EVCASE(RAIL_WMSZ_BOTTOMLEFT);
		EVCASE(RAIL_WMSZ_BOTTOMRIGHT);
		EVCASE(RAIL_WMSZ_MOVE);
		EVCASE(RAIL_WMSZ_KEYMOVE);
		EVCASE(RAIL_WMSZ_KEYSIZE);
		default:
			return "RAIL_WMSZ_INVALID";
	}
#undef EVCASE
}

struct xf_rail_icon
{
	long* data;
	int length;
};
typedef struct xf_rail_icon xfRailIcon;

struct xf_rail_icon_cache
{
	xfRailIcon* entries;
	UINT32 numCaches;
	UINT32 numCacheEntries;
	xfRailIcon scratch;
};

typedef struct
{
	xfContext* xfc;
	const RECTANGLE_16* rect;
} rail_paint_fn_arg_t;

BOOL xf_rail_enable_remoteapp_mode(xfContext* xfc)
{
	WINPR_ASSERT(xfc);
	if (!xfc->remote_app)
	{
		rdpGdi* gdi = xfc->common.context.gdi;
		WINPR_ASSERT(gdi);

		const BOOL old = gdi->suppressOutput;
		gdi->suppressOutput = TRUE;
		xfc->remote_app = TRUE;
		xfc->drawable = xf_CreateDummyWindow(xfc);
		xf_DestroyDesktopWindow(xfc, xfc->window);
		xfc->window = nullptr;

		gdi->suppressOutput = old;
	}
	return TRUE;
}

BOOL xf_rail_disable_remoteapp_mode(xfContext* xfc)
{
	WINPR_ASSERT(xfc);
	if (xfc->remote_app)
	{
		rdpGdi* gdi = xfc->common.context.gdi;
		WINPR_ASSERT(gdi);

		const BOOL old = gdi->suppressOutput;
		gdi->suppressOutput = TRUE;

		xfc->remote_app = FALSE;
		xf_DestroyDummyWindow(xfc, xfc->drawable);
		xf_destroy_window(xfc);
		xf_create_window(xfc);
		xf_create_image(xfc);

		gdi->suppressOutput = old;
	}
	return TRUE;
}

/* Report the real work area so the server maximizes RemoteApp windows within the panel. */
static UINT xf_rail_send_workarea(xfContext* xfc)
{
	WINPR_ASSERT(xfc);
	WINPR_ASSERT(xfc->rail);

	/* xf_GetWorkArea overwrites xfc->workArea; restore it (RAIL keeps it full). */
	const xfWorkArea saved = xfc->workArea;
	if (!xf_GetWorkArea(xfc))
		return CHANNEL_RC_OK;
	const INT64 right = (INT64)xfc->workArea.x + xfc->workArea.width;
	const INT64 bottom = (INT64)xfc->workArea.y + xfc->workArea.height;
	const RAIL_SYSPARAM_ORDER sysparam = {
		.params = SPI_MASK_SET_WORK_AREA,
		.workArea.left = WINPR_CXX_COMPAT_CAST(UINT16, xfc->workArea.x),
		.workArea.top = WINPR_CXX_COMPAT_CAST(UINT16, xfc->workArea.y),
		.workArea.right = WINPR_CXX_COMPAT_CAST(UINT16, right),
		.workArea.bottom = WINPR_CXX_COMPAT_CAST(UINT16, bottom)
	};
	xfc->workArea = saved;
	return xfc->rail->ClientSystemParam(xfc->rail, &sysparam);
}

BOOL xf_rail_send_activate(xfContext* xfc, Window xwindow, BOOL enabled)
{
	RAIL_ACTIVATE_ORDER activate = WINPR_C_ARRAY_INIT;
	xfAppWindow* appWindow = xf_AppWindowFromX11Window(xfc, xwindow);

	if (!appWindow)
	{
		WLog_Print(xfc->log, WLOG_DEBUG, "xf_rail_send_activate: ignoring unknown window 0x%08lx",
		           xwindow);
		return TRUE;
	}

	if (enabled)
		xf_SetWindowStyle(xfc, appWindow, appWindow->dwStyle, appWindow->dwExStyle);

	WINPR_ASSERT(appWindow->windowId <= UINT32_MAX);
	activate.windowId = (UINT32)appWindow->windowId;
	xf_rail_return_window(appWindow, FALSE);

	activate.enabled = enabled;
	const UINT rc = xfc->rail->ClientActivate(xfc->rail, &activate);
	return rc == CHANNEL_RC_OK;
}

BOOL xf_rail_send_client_system_command(xfContext* xfc, UINT64 windowId, UINT16 command)
{
	WINPR_ASSERT(xfc);
	WINPR_ASSERT(xfc->rail);
	WINPR_ASSERT(xfc->rail->ClientSystemCommand);
	if (windowId > UINT32_MAX)
		return FALSE;

	const RAIL_SYSCOMMAND_ORDER syscommand = { .windowId = (UINT32)windowId, .command = command };
	const UINT rc = xfc->rail->ClientSystemCommand(xfc->rail, &syscommand);
	return rc == CHANNEL_RC_OK;
}

/**
 * The position of the X window can become out of sync with the RDP window
 * if the X window is moved locally by the window manager.  In this event
 * send an update to the RDP server informing it of the new window position
 * and size.
 */
BOOL xf_rail_adjust_position(xfContext* xfc, xfAppWindow* appWindow)
{
	RAIL_WINDOW_MOVE_ORDER windowMove = WINPR_C_ARRAY_INIT;

	WINPR_ASSERT(xfc);
	WINPR_ASSERT(appWindow);
	if (!appWindow->is_mapped || appWindow->local_move.state != LMS_NOT_ACTIVE)
		return FALSE;

	/* If current window position disagrees with RDP window position, send update to RDP server */
	if (appWindow->x != appWindow->windowOffsetX || appWindow->y != appWindow->windowOffsetY ||
	    appWindow->width != (INT64)appWindow->windowWidth ||
	    appWindow->height != (INT64)appWindow->windowHeight)
	{
		WINPR_ASSERT(appWindow->windowId <= UINT32_MAX);
		windowMove.windowId = (UINT32)appWindow->windowId;
		/*
		 * Calculate new size/position for the rail window(new values for
		 * windowOffsetX/windowOffsetY/windowWidth/windowHeight) on the server
		 */
		const INT16 left = WINPR_ASSERTING_INT_CAST(INT16, appWindow->resizeMarginLeft);
		const INT16 right = WINPR_ASSERTING_INT_CAST(INT16, appWindow->resizeMarginRight);
		const INT16 top = WINPR_ASSERTING_INT_CAST(INT16, appWindow->resizeMarginTop);
		const INT16 bottom = WINPR_ASSERTING_INT_CAST(INT16, appWindow->resizeMarginBottom);
		windowMove.left = WINPR_ASSERTING_INT_CAST(INT16, appWindow->x - left);
		windowMove.top = WINPR_ASSERTING_INT_CAST(INT16, appWindow->y - top);
		windowMove.right = WINPR_ASSERTING_INT_CAST(INT16, appWindow->x + appWindow->width + right);
		windowMove.bottom =
		    WINPR_ASSERTING_INT_CAST(INT16, appWindow->y + appWindow->height + bottom);
		const UINT rc = xfc->rail->ClientWindowMove(xfc->rail, &windowMove);
		return rc == CHANNEL_RC_OK;
	}
	return TRUE;
}

BOOL xf_rail_end_local_move(xfContext* xfc, xfAppWindow* appWindow)
{
	int x = 0;
	int y = 0;
	int child_x = 0;
	int child_y = 0;
	unsigned int mask = 0;
	Window root_window = 0;
	Window child_window = 0;

	WINPR_ASSERT(xfc);
	WINPR_ASSERT(appWindow);

	if ((appWindow->local_move.direction == NET_WM_MOVERESIZE_MOVE_KEYBOARD) ||
	    (appWindow->local_move.direction == NET_WM_MOVERESIZE_SIZE_KEYBOARD))
	{
		RAIL_WINDOW_MOVE_ORDER windowMove = WINPR_C_ARRAY_INIT;

		/*
		 * For keyboard moves send and explicit update to RDP server
		 */
		WINPR_ASSERT(appWindow->windowId <= UINT32_MAX);
		windowMove.windowId = (UINT32)appWindow->windowId;
		/*
		 * Calculate new size/position for the rail window(new values for
		 * windowOffsetX/windowOffsetY/windowWidth/windowHeight) on the server
		 *
		 */
		const INT16 left = WINPR_ASSERTING_INT_CAST(INT16, appWindow->resizeMarginLeft);
		const INT16 right = WINPR_ASSERTING_INT_CAST(INT16, appWindow->resizeMarginRight);
		const INT16 top = WINPR_ASSERTING_INT_CAST(INT16, appWindow->resizeMarginTop);
		const INT16 bottom = WINPR_ASSERTING_INT_CAST(INT16, appWindow->resizeMarginBottom);
		const INT16 w = WINPR_ASSERTING_INT_CAST(INT16, appWindow->width + right);
		const INT16 h = WINPR_ASSERTING_INT_CAST(INT16, appWindow->height + bottom);
		windowMove.left = WINPR_ASSERTING_INT_CAST(INT16, appWindow->x - left);
		windowMove.top = WINPR_ASSERTING_INT_CAST(INT16, appWindow->y - top);
		windowMove.right = WINPR_ASSERTING_INT_CAST(INT16, appWindow->x + w); /* In the update to
		           RDP the position is one past the window */
		windowMove.bottom = WINPR_ASSERTING_INT_CAST(INT16, appWindow->y + h);
		const UINT rc = xfc->rail->ClientWindowMove(xfc->rail, &windowMove);
		if (rc != CHANNEL_RC_OK)
			return FALSE;
	}

	/*
	 * Simulate button up at new position to end the local move (per RDP spec)
	 */
	XQueryPointer(xfc->display, appWindow->handle, &root_window, &child_window, &x, &y, &child_x,
	              &child_y, &mask);

	/* only send the mouse coordinates if not a keyboard move or size */
	if ((appWindow->local_move.direction != NET_WM_MOVERESIZE_MOVE_KEYBOARD) &&
	    (appWindow->local_move.direction != NET_WM_MOVERESIZE_SIZE_KEYBOARD))
	{
		if (!freerdp_client_send_button_event(&xfc->common, FALSE, PTR_FLAGS_BUTTON1, x, y))
			return FALSE;
	}

	/*
	 * Proactively update the RAIL window dimensions.  There is a race condition where
	 * we can start to receive GDI orders for the new window dimensions before we
	 * receive the RAIL ORDER for the new window size.  This avoids that race condition.
	 */
	appWindow->windowOffsetX = appWindow->x;
	appWindow->windowOffsetY = appWindow->y;
	appWindow->windowWidth = WINPR_ASSERTING_INT_CAST(uint32_t, appWindow->width);
	appWindow->windowHeight = WINPR_ASSERTING_INT_CAST(uint32_t, appWindow->height);
	appWindow->local_move.state = LMS_TERMINATING;
	return TRUE;
}

BOOL xf_rail_paint_surface(xfContext* xfc, UINT64 windowId, const RECTANGLE_16* rect)
{
	xfAppWindow* appWindow = xf_rail_get_window(xfc, windowId, FALSE);

	WINPR_ASSERT(rect);

	if (!appWindow)
		return FALSE;

	const RECTANGLE_16 windowRect = {
		.left = WINPR_ASSERTING_INT_CAST(UINT16, MAX(appWindow->x, 0)),
		.top = WINPR_ASSERTING_INT_CAST(UINT16, MAX(appWindow->y, 0)),
		.right = WINPR_ASSERTING_INT_CAST(UINT16, MAX(appWindow->x + appWindow->width, 0)),
		.bottom = WINPR_ASSERTING_INT_CAST(UINT16, MAX(appWindow->y + appWindow->height, 0))
	};

	REGION16 windowInvalidRegion = WINPR_C_ARRAY_INIT;
	region16_init(&windowInvalidRegion);
	if (!region16_union_rect(&windowInvalidRegion, &windowInvalidRegion, &windowRect))
		return FALSE;
	if (!region16_intersect_rect(&windowInvalidRegion, &windowInvalidRegion, rect))
		return FALSE;

	if (!region16_is_empty(&windowInvalidRegion))
	{
		const RECTANGLE_16* extents = region16_extents(&windowInvalidRegion);

		const RECTANGLE_16 updateRect = {
			.left = WINPR_ASSERTING_INT_CAST(UINT16, extents->left - appWindow->x),
			.top = WINPR_ASSERTING_INT_CAST(UINT16, extents->top - appWindow->y),
			.right = WINPR_ASSERTING_INT_CAST(UINT16, extents->right - appWindow->x),
			.bottom = WINPR_ASSERTING_INT_CAST(UINT16, extents->bottom - appWindow->y)
		};

		xf_UpdateWindowArea(xfc, appWindow, updateRect.left, updateRect.top,
		                    updateRect.right - updateRect.left, updateRect.bottom - updateRect.top);
	}
	region16_uninit(&windowInvalidRegion);
	xf_rail_return_window(appWindow, FALSE);
	return TRUE;
}

static BOOL rail_paint_fn(const void* pvkey, WINPR_ATTR_UNUSED void* value, void* pvarg)
{
	rail_paint_fn_arg_t* arg = pvarg;
	WINPR_ASSERT(pvkey);
	WINPR_ASSERT(arg);

	const UINT64 key = *(const UINT64*)pvkey;
	return xf_rail_paint_surface(arg->xfc, key, arg->rect);
}

BOOL xf_rail_paint(xfContext* xfc, const RECTANGLE_16* rect)
{
	rail_paint_fn_arg_t arg = { .xfc = xfc, .rect = rect };

	WINPR_ASSERT(xfc);
	WINPR_ASSERT(rect);

	if (!xfc->railWindows)
		return TRUE;

	return HashTable_Foreach(xfc->railWindows, rail_paint_fn, &arg);
}

#define window_state_log_style(log, windowState) \
	window_state_log_style_int((log), (windowState), __FILE__, __func__, __LINE__)
static void window_state_log_style_int(wLog* log, const WINDOW_STATE_ORDER* windowState,
                                       const char* file, const char* fkt, size_t line)
{
	const DWORD log_level = WLOG_DEBUG;

	WINPR_ASSERT(log);
	WINPR_ASSERT(windowState);
	if (WLog_IsLevelActive(log, log_level))
	{
		char buffer1[128] = WINPR_C_ARRAY_INIT;
		char buffer2[128] = WINPR_C_ARRAY_INIT;

		window_styles_to_string(windowState->style, buffer1, sizeof(buffer1));
		window_styles_ex_to_string(windowState->extendedStyle, buffer2, sizeof(buffer2));
		WLog_PrintTextMessage(log, log_level, line, file, fkt, "windowStyle={%s, %s}", buffer1,
		                      buffer2);
	}
}

/* RemoteApp Core Protocol Extension */

static BOOL xf_rail_window_common(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo,
                                  const WINDOW_STATE_ORDER* windowState)
{
	BOOL rc = FALSE;
	xfContext* xfc = (xfContext*)context;

	WINPR_ASSERT(xfc);
	WINPR_ASSERT(orderInfo);
	WINPR_ASSERT(windowState);

	UINT32 fieldFlags = orderInfo->fieldFlags;
	BOOL position_or_size_updated = FALSE;
	xfAppWindow* appWindow = xf_rail_get_window(xfc, orderInfo->windowId, FALSE);

	if (fieldFlags & WINDOW_ORDER_STATE_NEW)
	{
		if (!appWindow)
			appWindow = xf_rail_add_window(xfc, orderInfo->windowId, windowState->windowOffsetX,
			                               windowState->windowOffsetY, windowState->windowWidth,
			                               windowState->windowHeight, 0xFFFFFFFF);

		if (!appWindow)
			goto fail;

		appWindow->dwStyle = windowState->style;
		appWindow->dwExStyle = windowState->extendedStyle;
		window_state_log_style(xfc->log, windowState);

		/* Ensure window always gets a window title */
		if (fieldFlags & WINDOW_ORDER_FIELD_TITLE)
		{
			char* title = rail_string_to_utf8_string(&windowState->titleInfo);
			if (!title)
			{
				WLog_ERR(TAG, "failed to duplicate window title string");
				/* error handled below */
			}

			appWindow->title = title;
		}
		else
		{
			if (!(appWindow->title = _strdup("RdpRailWindow")))
				WLog_ERR(TAG, "failed to duplicate default window title string");
		}

		if (!appWindow->title)
			goto fail;

		xf_AppWindowInit(xfc, appWindow);
	}

	if (!appWindow)
		return FALSE;

	/* Keep track of any position/size update so that we can force a refresh of the window */
	if ((fieldFlags & WINDOW_ORDER_FIELD_WND_OFFSET) ||
	    (fieldFlags & WINDOW_ORDER_FIELD_WND_SIZE) ||
	    (fieldFlags & WINDOW_ORDER_FIELD_CLIENT_AREA_OFFSET) ||
	    (fieldFlags & WINDOW_ORDER_FIELD_CLIENT_AREA_SIZE) ||
	    (fieldFlags & WINDOW_ORDER_FIELD_WND_CLIENT_DELTA) ||
	    (fieldFlags & WINDOW_ORDER_FIELD_VIS_OFFSET) ||
	    (fieldFlags & WINDOW_ORDER_FIELD_VISIBILITY))
	{
		position_or_size_updated = TRUE;
	}

	/* Update Parameters */

	if (fieldFlags & WINDOW_ORDER_FIELD_WND_OFFSET)
	{
		appWindow->windowOffsetX = windowState->windowOffsetX;
		appWindow->windowOffsetY = windowState->windowOffsetY;
	}

	if (fieldFlags & WINDOW_ORDER_FIELD_WND_SIZE)
	{
		appWindow->windowWidth = windowState->windowWidth;
		appWindow->windowHeight = windowState->windowHeight;
	}

	if (fieldFlags & WINDOW_ORDER_FIELD_RESIZE_MARGIN_X)
	{
		appWindow->resizeMarginLeft = windowState->resizeMarginLeft;
		appWindow->resizeMarginRight = windowState->resizeMarginRight;
	}

	if (fieldFlags & WINDOW_ORDER_FIELD_RESIZE_MARGIN_Y)
	{
		appWindow->resizeMarginTop = windowState->resizeMarginTop;
		appWindow->resizeMarginBottom = windowState->resizeMarginBottom;
	}

	if (fieldFlags & WINDOW_ORDER_FIELD_OWNER)
	{
		appWindow->ownerWindowId = windowState->ownerWindowId;
	}

	if (fieldFlags & WINDOW_ORDER_FIELD_STYLE)
	{
		appWindow->dwStyle = windowState->style;
		appWindow->dwExStyle = windowState->extendedStyle;
		window_state_log_style(xfc->log, windowState);
	}

	if (fieldFlags & WINDOW_ORDER_FIELD_SHOW)
	{
		appWindow->showState = windowState->showState;
	}

	if (fieldFlags & WINDOW_ORDER_FIELD_TITLE)
	{
		char* title = rail_string_to_utf8_string(&windowState->titleInfo);
		if (!title)
		{
			WLog_ERR(TAG, "failed to duplicate window title string");
			goto fail;
		}

		free(appWindow->title);
		appWindow->title = title;
	}

	if (fieldFlags & WINDOW_ORDER_FIELD_CLIENT_AREA_OFFSET)
	{
		appWindow->clientOffsetX = windowState->clientOffsetX;
		appWindow->clientOffsetY = windowState->clientOffsetY;
	}

	if (fieldFlags & WINDOW_ORDER_FIELD_CLIENT_AREA_SIZE)
	{
		appWindow->clientAreaWidth = windowState->clientAreaWidth;
		appWindow->clientAreaHeight = windowState->clientAreaHeight;
	}

	if (fieldFlags & WINDOW_ORDER_FIELD_WND_CLIENT_DELTA)
	{
		appWindow->windowClientDeltaX = windowState->windowClientDeltaX;
		appWindow->windowClientDeltaY = windowState->windowClientDeltaY;
	}

	if (fieldFlags & WINDOW_ORDER_FIELD_WND_RECTS)
	{
		if (appWindow->windowRects)
		{
			free(appWindow->windowRects);
			appWindow->windowRects = nullptr;
		}

		appWindow->numWindowRects = windowState->numWindowRects;

		if (appWindow->numWindowRects)
		{
			appWindow->windowRects =
			    (RECTANGLE_16*)calloc(appWindow->numWindowRects, sizeof(RECTANGLE_16));

			if (!appWindow->windowRects)
				goto fail;

			CopyMemory(appWindow->windowRects, windowState->windowRects,
			           appWindow->numWindowRects * sizeof(RECTANGLE_16));
		}
	}

	if (fieldFlags & WINDOW_ORDER_FIELD_VIS_OFFSET)
	{
		appWindow->visibleOffsetX = windowState->visibleOffsetX;
		appWindow->visibleOffsetY = windowState->visibleOffsetY;
	}

	if (fieldFlags & WINDOW_ORDER_FIELD_VISIBILITY)
	{
		if (appWindow->visibilityRects)
		{
			free(appWindow->visibilityRects);
			appWindow->visibilityRects = nullptr;
		}

		appWindow->numVisibilityRects = windowState->numVisibilityRects;

		if (appWindow->numVisibilityRects)
		{
			appWindow->visibilityRects =
			    (RECTANGLE_16*)calloc(appWindow->numVisibilityRects, sizeof(RECTANGLE_16));

			if (!appWindow->visibilityRects)
				goto fail;

			CopyMemory(appWindow->visibilityRects, windowState->visibilityRects,
			           appWindow->numVisibilityRects * sizeof(RECTANGLE_16));
		}
	}

	/* Update Window */

	if (fieldFlags & WINDOW_ORDER_FIELD_STYLE)
	{
	}

	if (fieldFlags & WINDOW_ORDER_FIELD_SHOW)
	{
		xf_ShowWindow(xfc, appWindow, WINPR_ASSERTING_INT_CAST(UINT8, appWindow->showState));
	}

	if (fieldFlags & WINDOW_ORDER_FIELD_TITLE)
	{
		if (appWindow->title)
			xf_SetWindowText(xfc, appWindow, appWindow->title);
	}

	if (position_or_size_updated)
	{
		const INT32 visibilityRectsOffsetX =
		    (appWindow->visibleOffsetX -
		     (appWindow->clientOffsetX - appWindow->windowClientDeltaX));
		const INT32 visibilityRectsOffsetY =
		    (appWindow->visibleOffsetY -
		     (appWindow->clientOffsetY - appWindow->windowClientDeltaY));

		/*
		 * The rail server like to set the window to a small size when it is minimized even though
		 * it is hidden in some cases this can cause the window not to restore back to its original
		 * size. Therefore we don't update our local window when that rail window state is minimized
		 */
		if (appWindow->rail_state != WINDOW_SHOW_MINIMIZED)
		{
			const BOOL maximized = (appWindow->dwStyle & WS_MAXIMIZE) != 0;
			/* Leave maximized windows to the WM, which honors the work area. */
			if (maximized || (appWindow->x == (INT64)appWindow->windowOffsetX &&
			                  appWindow->y == (INT64)appWindow->windowOffsetY &&
			                  appWindow->width == (INT64)appWindow->windowWidth &&
			                  appWindow->height == (INT64)appWindow->windowHeight))
			{
				xf_UpdateWindowArea(xfc, appWindow, 0, 0,
				                    WINPR_ASSERTING_INT_CAST(int, appWindow->windowWidth),
				                    WINPR_ASSERTING_INT_CAST(int, appWindow->windowHeight));
			}
			else
			{
				xf_MoveWindow(xfc, appWindow, appWindow->windowOffsetX, appWindow->windowOffsetY,
				              WINPR_ASSERTING_INT_CAST(int, appWindow->windowWidth),
				              WINPR_ASSERTING_INT_CAST(int, appWindow->windowHeight));
			}

			/* Show a maximized window fully; its frame-inset visibility rects would clip it. */
			if (maximized)
				xf_ClearWindowVisibilityRects(xfc, appWindow);
			else
				xf_SetWindowVisibilityRects(
				    xfc, appWindow, WINPR_ASSERTING_INT_CAST(uint32_t, visibilityRectsOffsetX),
				    WINPR_ASSERTING_INT_CAST(uint32_t, visibilityRectsOffsetY),
				    appWindow->visibilityRects,
				    WINPR_ASSERTING_INT_CAST(int, appWindow->numVisibilityRects));
		}

		if (appWindow->rail_state == WINDOW_SHOW_MAXIMIZED)
		{
			xf_SendClientEvent(xfc, appWindow->handle, xfc->NET_WM_STATE, 4, NET_WM_STATE_ADD,
			                   xfc->NET_WM_STATE_MAXIMIZED_VERT, xfc->NET_WM_STATE_MAXIMIZED_HORZ,
			                   0, 0);
		}
	}

	if (fieldFlags & (WINDOW_ORDER_STATE_NEW | WINDOW_ORDER_FIELD_STYLE))
		xf_SetWindowStyle(xfc, appWindow, appWindow->dwStyle, appWindow->dwExStyle);

	/* We should only be using the visibility rects for shaping the window */
	/*if (fieldFlags & WINDOW_ORDER_FIELD_WND_RECTS)
	{
	    xf_SetWindowRects(xfc, appWindow, appWindow->windowRects, appWindow->numWindowRects);
	}*/
	rc = TRUE;
fail:
	xf_rail_return_window(appWindow, FALSE);
	return rc;
}

static BOOL xf_rail_window_delete(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo)
{
	xfContext* xfc = (xfContext*)context;
	WINPR_ASSERT(xfc);
	return xf_rail_del_window(xfc, orderInfo->windowId);
}

static xfRailIconCache* RailIconCache_New(rdpSettings* settings)
{
	xfRailIconCache* cache = calloc(1, sizeof(xfRailIconCache));

	if (!cache)
		return nullptr;

	cache->numCaches = freerdp_settings_get_uint32(settings, FreeRDP_RemoteAppNumIconCaches);
	cache->numCacheEntries =
	    freerdp_settings_get_uint32(settings, FreeRDP_RemoteAppNumIconCacheEntries);
	cache->entries = calloc(1ull * cache->numCaches * cache->numCacheEntries, sizeof(xfRailIcon));

	if (!cache->entries)
	{
		WLog_ERR(TAG, "failed to allocate icon cache %" PRIu32 " x %" PRIu32 " entries",
		         cache->numCaches, cache->numCacheEntries);
		free(cache);
		return nullptr;
	}

	return cache;
}

static void RailIconCache_Free(xfRailIconCache* cache)
{
	if (!cache)
		return;

	for (UINT32 i = 0; i < cache->numCaches * cache->numCacheEntries; i++)
	{
		xfRailIcon* cur = &cache->entries[i];
		free(cur->data);
	}

	free(cache->scratch.data);
	free(cache->entries);
	free(cache);
}

static xfRailIcon* RailIconCache_Lookup(xfRailIconCache* cache, UINT8 cacheId, UINT16 cacheEntry)
{
	WINPR_ASSERT(cache);
	/*
	 * MS-RDPERP 2.2.1.2.3 Icon Info (TS_ICON_INFO)
	 *
	 * CacheId (1 byte):
	 *     If the value is 0xFFFF, the icon SHOULD NOT be cached.
	 *
	 * Yes, the spec says "0xFFFF" in the 2018-03-16 revision,
	 * but the actual protocol field is 1-byte wide.
	 */
	if (cacheId == 0xFF)
		return &cache->scratch;

	if (cacheId >= cache->numCaches)
		return nullptr;

	if (cacheEntry >= cache->numCacheEntries)
		return nullptr;

	return &cache->entries[cache->numCacheEntries * cacheId + cacheEntry];
}

/*
 * _NET_WM_ICON format is defined as "array of CARDINAL" values which for
 * Xlib must be represented with an array of C's "long" values. Note that
 * "long" != "INT32" on 64-bit systems. Therefore we can't simply cast
 * the bitmap data as (unsigned char*), we have to copy all the pixels.
 *
 * The first two values are width and height followed by actual color data
 * in ARGB format (e.g., 0xFFFF0000L is opaque red), pixels are in normal,
 * left-to-right top-down order.
 */
static BOOL convert_rail_icon(const ICON_INFO* iconInfo, xfRailIcon* railIcon)
{
	WINPR_ASSERT(iconInfo);
	WINPR_ASSERT(railIcon);

	BYTE* nextPixel = nullptr;
	long* pixels = nullptr;
	BYTE* argbPixels = calloc(1ull * iconInfo->width * iconInfo->height, 4);

	if (!argbPixels)
		goto error;

	if (!freerdp_image_copy_from_icon_data(
	        argbPixels, PIXEL_FORMAT_ARGB32, 0, 0, 0,
	        WINPR_ASSERTING_INT_CAST(UINT16, iconInfo->width),
	        WINPR_ASSERTING_INT_CAST(UINT16, iconInfo->height), iconInfo->bitsColor,
	        WINPR_ASSERTING_INT_CAST(UINT16, iconInfo->cbBitsColor), iconInfo->bitsMask,
	        WINPR_ASSERTING_INT_CAST(UINT16, iconInfo->cbBitsMask), iconInfo->colorTable,
	        WINPR_ASSERTING_INT_CAST(UINT16, iconInfo->cbColorTable), iconInfo->bpp))
		goto error;

	{
		const UINT32 nelements = 2 + iconInfo->width * iconInfo->height;
		pixels = realloc(railIcon->data, nelements * sizeof(long));

		if (!pixels)
			goto error;

		railIcon->data = pixels;

		railIcon->length = WINPR_ASSERTING_INT_CAST(int, nelements);
		pixels[0] = iconInfo->width;
		pixels[1] = iconInfo->height;
		nextPixel = argbPixels;

		for (UINT32 i = 2; i < nelements; i++)
		{
			pixels[i] = FreeRDPReadColor(nextPixel, PIXEL_FORMAT_BGRA32);
			nextPixel += 4;
		}
	}

	free(argbPixels);
	return TRUE;
error:
	free(argbPixels);
	return FALSE;
}

static void xf_rail_set_window_icon(xfContext* xfc, xfAppWindow* railWindow, xfRailIcon* icon,
                                    BOOL replace)
{
	WINPR_ASSERT(xfc);

	LogDynAndXChangeProperty(xfc->log, xfc->display, railWindow->handle, xfc->NET_WM_ICON,
	                         XA_CARDINAL, 32, replace ? PropModeReplace : PropModeAppend,
	                         (unsigned char*)icon->data, icon->length);
	LogDynAndXFlush(xfc->log, xfc->display);
}

static BOOL xf_rail_window_icon(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo,
                                const WINDOW_ICON_ORDER* windowIcon)
{
	BOOL rc = FALSE;
	xfContext* xfc = (xfContext*)context;
	BOOL replaceIcon = 0;
	xfAppWindow* railWindow = xf_rail_get_window(xfc, orderInfo->windowId, FALSE);

	if (!railWindow)
		return TRUE;

	WINPR_ASSERT(windowIcon);
	WINPR_ASSERT(windowIcon->iconInfo);
	xfRailIcon* icon = RailIconCache_Lookup(
	    xfc->railIconCache, WINPR_ASSERTING_INT_CAST(UINT8, windowIcon->iconInfo->cacheId),
	    WINPR_ASSERTING_INT_CAST(UINT16, windowIcon->iconInfo->cacheEntry));

	if (!icon)
	{
		WLog_Print(xfc->log, WLOG_WARN, "failed to get icon from cache %02X:%04X",
		           windowIcon->iconInfo->cacheId, windowIcon->iconInfo->cacheEntry);
	}
	else if (!convert_rail_icon(windowIcon->iconInfo, icon))
	{
		WLog_Print(xfc->log, WLOG_WARN, "failed to convert icon for window %08X",
		           orderInfo->windowId);
	}
	else
	{
		replaceIcon = !!(orderInfo->fieldFlags & WINDOW_ORDER_STATE_NEW);
		xf_rail_set_window_icon(xfc, railWindow, icon, replaceIcon);
		rc = TRUE;
	}
	xf_rail_return_window(railWindow, FALSE);
	return rc;
}

static BOOL xf_rail_window_cached_icon(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo,
                                       const WINDOW_CACHED_ICON_ORDER* windowCachedIcon)
{
	BOOL rc = FALSE;
	xfContext* xfc = (xfContext*)context;
	WINPR_ASSERT(orderInfo);

	BOOL replaceIcon = 0;
	xfAppWindow* railWindow = xf_rail_get_window(xfc, orderInfo->windowId, FALSE);

	if (!railWindow)
		return TRUE;

	WINPR_ASSERT(windowCachedIcon);

	xfRailIcon* icon = RailIconCache_Lookup(
	    xfc->railIconCache, WINPR_ASSERTING_INT_CAST(UINT8, windowCachedIcon->cachedIcon.cacheId),
	    WINPR_ASSERTING_INT_CAST(UINT16, windowCachedIcon->cachedIcon.cacheEntry));

	if (!icon)
	{
		WLog_Print(xfc->log, WLOG_WARN, "failed to get icon from cache %02X:%04X",
		           windowCachedIcon->cachedIcon.cacheId, windowCachedIcon->cachedIcon.cacheEntry);
	}
	else
	{
		replaceIcon = !!(orderInfo->fieldFlags & WINDOW_ORDER_STATE_NEW);
		xf_rail_set_window_icon(xfc, railWindow, icon, replaceIcon);
		rc = TRUE;
	}
	xf_rail_return_window(railWindow, FALSE);
	return rc;
}

static BOOL
xf_rail_notify_icon_common(WINPR_ATTR_UNUSED rdpContext* context,
                           const WINDOW_ORDER_INFO* orderInfo,
                           WINPR_ATTR_UNUSED const NOTIFY_ICON_STATE_ORDER* notifyIconState)
{
	WLog_ERR("TODO", "TODO: implement");
	if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_NOTIFY_VERSION)
	{
	}

	if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_NOTIFY_TIP)
	{
	}

	if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_NOTIFY_INFO_TIP)
	{
	}

	if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_NOTIFY_STATE)
	{
	}

	if (orderInfo->fieldFlags & WINDOW_ORDER_ICON)
	{
	}

	if (orderInfo->fieldFlags & WINDOW_ORDER_CACHED_ICON)
	{
	}

	return TRUE;
}

static BOOL xf_rail_notify_icon_create(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo,
                                       const NOTIFY_ICON_STATE_ORDER* notifyIconState)
{
	return xf_rail_notify_icon_common(context, orderInfo, notifyIconState);
}

static BOOL xf_rail_notify_icon_update(rdpContext* context, const WINDOW_ORDER_INFO* orderInfo,
                                       const NOTIFY_ICON_STATE_ORDER* notifyIconState)
{
	return xf_rail_notify_icon_common(context, orderInfo, notifyIconState);
}

static BOOL xf_rail_notify_icon_delete(WINPR_ATTR_UNUSED rdpContext* context,
                                       WINPR_ATTR_UNUSED const WINDOW_ORDER_INFO* orderInfo)
{
	WLog_ERR("TODO", "TODO: implement");
	return TRUE;
}

static BOOL
xf_rail_monitored_desktop(WINPR_ATTR_UNUSED rdpContext* context,
                          WINPR_ATTR_UNUSED const WINDOW_ORDER_INFO* orderInfo,
                          WINPR_ATTR_UNUSED const MONITORED_DESKTOP_ORDER* monitoredDesktop)
{
	const UINT32 mask = WINDOW_ORDER_TYPE_DESKTOP | WINDOW_ORDER_FIELD_DESKTOP_HOOKED |
	                    WINDOW_ORDER_FIELD_DESKTOP_ARC_BEGAN |
	                    WINDOW_ORDER_FIELD_DESKTOP_ARC_COMPLETED |
	                    WINDOW_ORDER_FIELD_DESKTOP_ACTIVE_WND | WINDOW_ORDER_FIELD_DESKTOP_ZORDER;
	xfContext* xfc = (xfContext*)context;

	if (!context || !orderInfo || !monitoredDesktop)
		return FALSE;

	if ((orderInfo->fieldFlags & WINDOW_ORDER_TYPE_DESKTOP) == 0)
	{
		WLog_Print(xfc->log, WLOG_WARN, "WINDOW_ORDER_TYPE_DESKTOP flag missing!");
		return FALSE;
	}

	if ((orderInfo->fieldFlags & WINDOW_ORDER_FIELD_DESKTOP_ARC_BEGAN) &&
	    (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_DESKTOP_HOOKED))
	{
		// discard all windows/notify icons
		WLog_Print(xfc->log, WLOG_WARN,
		           "TODO: implement WINDOW_ORDER_FIELD_DESKTOP_ARC_BEGAN && "
		           "WINDOW_ORDER_FIELD_DESKTOP_HOOKED");
	}
	else if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_DESKTOP_HOOKED)
	{
		WLog_Print(xfc->log, WLOG_WARN, "TODO: implement WINDOW_ORDER_FIELD_DESKTOP_HOOKED");
	}
	if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_DESKTOP_ARC_COMPLETED)
	{
		WLog_DBG(TAG, "WINDOW_ORDER_FIELD_DESKTOP_ARC_COMPLETED -> switch to RAILS mode");
		if (!xf_rail_enable_remoteapp_mode(xfc))
			return FALSE;

		const char* app =
		    freerdp_settings_get_string(context->settings, FreeRDP_RemoteApplicationProgram);
		if ((app != nullptr) && (strnlen(app, 1) > 0))
		{
			if (client_rail_server_start_cmd(xfc->rail) != CHANNEL_RC_OK)
				return FALSE;
			if (xf_rail_send_workarea(xfc) != CHANNEL_RC_OK)
				return FALSE;
		}
	}
	if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_DESKTOP_ACTIVE_WND)
	{
		WLog_Print(xfc->log, WLOG_WARN, "TODO: implement WINDOW_ORDER_FIELD_DESKTOP_ACTIVE_WND");
	}
	if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_DESKTOP_ZORDER)
	{
		WLog_Print(xfc->log, WLOG_WARN, "TODO: implement WINDOW_ORDER_FIELD_DESKTOP_ZORDER");
	}
	if (orderInfo->fieldFlags & ~mask)
	{
		WLog_Print(xfc->log, WLOG_WARN, "unknown flags 0x%08" PRIx32 "!", orderInfo->fieldFlags);
	}
	return TRUE;
}

static BOOL xf_rail_non_monitored_desktop(rdpContext* context,
                                          WINPR_ATTR_UNUSED const WINDOW_ORDER_INFO* orderInfo)
{
	xfContext* xfc = (xfContext*)context;
	const UINT32 mask = WINDOW_ORDER_TYPE_DESKTOP | WINDOW_ORDER_FIELD_DESKTOP_NONE;

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

	if ((orderInfo->fieldFlags & WINDOW_ORDER_TYPE_DESKTOP) == 0)
	{
		WLog_Print(xfc->log, WLOG_WARN, "TODO: implement WINDOW_ORDER_TYPE_DESKTOP");
		return FALSE;
	}
	if (orderInfo->fieldFlags & WINDOW_ORDER_FIELD_DESKTOP_NONE)
	{
		WLog_Print(xfc->log, WLOG_WARN, "TODO: implement WINDOW_ORDER_FIELD_DESKTOP_NONE");
	}
	if (orderInfo->fieldFlags & ~mask)
	{
		WLog_Print(xfc->log, WLOG_WARN, "unknown flags 0x%08" PRIx32 "!", orderInfo->fieldFlags);
	}

	return xf_rail_disable_remoteapp_mode(xfc);
}

static void xf_rail_register_update_callbacks(rdpUpdate* update)
{
	WINPR_ASSERT(update);

	rdpWindowUpdate* window = update->window;
	WINPR_ASSERT(window);

	window->WindowCreate = xf_rail_window_common;
	window->WindowUpdate = xf_rail_window_common;
	window->WindowDelete = xf_rail_window_delete;
	window->WindowIcon = xf_rail_window_icon;
	window->WindowCachedIcon = xf_rail_window_cached_icon;
	window->NotifyIconCreate = xf_rail_notify_icon_create;
	window->NotifyIconUpdate = xf_rail_notify_icon_update;
	window->NotifyIconDelete = xf_rail_notify_icon_delete;
	window->MonitoredDesktop = xf_rail_monitored_desktop;
	window->NonMonitoredDesktop = xf_rail_non_monitored_desktop;
}

/* RemoteApp Virtual Channel Extension */

/**
 * Function description
 *
 * @return 0 on success, otherwise a Win32 error code
 */
static UINT xf_rail_server_execute_result(RailClientContext* context,
                                          const RAIL_EXEC_RESULT_ORDER* execResult)
{
	WINPR_ASSERT(context);
	WINPR_ASSERT(execResult);

	xfContext* xfc = (xfContext*)context->custom;
	WINPR_ASSERT(xfc);

	if (execResult->execResult != RAIL_EXEC_S_OK)
	{
		WLog_ERR(TAG, "RAIL exec error: execResult=%s [0x%08" PRIx32 "] NtError=0x%X\n",
		         error_code2str(execResult->execResult), execResult->execResult,
		         execResult->rawResult);
		freerdp_abort_connect_context(&xfc->common.context);
	}

	return CHANNEL_RC_OK;
}

/**
 * Function description
 *
 * @return 0 on success, otherwise a Win32 error code
 */
static UINT xf_rail_server_system_param(WINPR_ATTR_UNUSED RailClientContext* context,
                                        WINPR_ATTR_UNUSED const RAIL_SYSPARAM_ORDER* sysparam)
{
	// TODO: Actually apply param
	WLog_ERR("TODO", "TODO: implement");
	return CHANNEL_RC_OK;
}

/**
 * Function description
 *
 * @return 0 on success, otherwise a Win32 error code
 */
static UINT xf_rail_server_local_move_size(RailClientContext* context,
                                           const RAIL_LOCALMOVESIZE_ORDER* localMoveSize)
{
	int x = 0;
	int y = 0;
	int direction = 0;
	Window child_window = 0;
	WINPR_ASSERT(context);
	WINPR_ASSERT(localMoveSize);

	xfContext* xfc = (xfContext*)context->custom;
	xfAppWindow* appWindow = xf_rail_get_window(xfc, localMoveSize->windowId, FALSE);

	if (!appWindow)
		return ERROR_INTERNAL_ERROR;

	WLog_Print(xfc->log, WLOG_TRACE, "%s [0x%08" PRIx32 "]",
	           movetype2str(localMoveSize->moveSizeType), localMoveSize->moveSizeType);
	switch (localMoveSize->moveSizeType)
	{
		case RAIL_WMSZ_LEFT:
			direction = NET_WM_MOVERESIZE_SIZE_LEFT;
			x = localMoveSize->posX;
			y = localMoveSize->posY;
			break;

		case RAIL_WMSZ_RIGHT:
			direction = NET_WM_MOVERESIZE_SIZE_RIGHT;
			x = localMoveSize->posX;
			y = localMoveSize->posY;
			break;

		case RAIL_WMSZ_TOP:
			direction = NET_WM_MOVERESIZE_SIZE_TOP;
			x = localMoveSize->posX;
			y = localMoveSize->posY;
			break;

		case RAIL_WMSZ_TOPLEFT:
			direction = NET_WM_MOVERESIZE_SIZE_TOPLEFT;
			x = localMoveSize->posX;
			y = localMoveSize->posY;
			break;

		case RAIL_WMSZ_TOPRIGHT:
			direction = NET_WM_MOVERESIZE_SIZE_TOPRIGHT;
			x = localMoveSize->posX;
			y = localMoveSize->posY;
			break;

		case RAIL_WMSZ_BOTTOM:
			direction = NET_WM_MOVERESIZE_SIZE_BOTTOM;
			x = localMoveSize->posX;
			y = localMoveSize->posY;
			break;

		case RAIL_WMSZ_BOTTOMLEFT:
			direction = NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT;
			x = localMoveSize->posX;
			y = localMoveSize->posY;
			break;

		case RAIL_WMSZ_BOTTOMRIGHT:
			direction = NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT;
			x = localMoveSize->posX;
			y = localMoveSize->posY;
			break;

		case RAIL_WMSZ_MOVE:
			direction = NET_WM_MOVERESIZE_MOVE;
			XTranslateCoordinates(xfc->display, appWindow->handle, RootWindowOfScreen(xfc->screen),
			                      localMoveSize->posX, localMoveSize->posY, &x, &y, &child_window);
			break;

		case RAIL_WMSZ_KEYMOVE:
			direction = NET_WM_MOVERESIZE_MOVE_KEYBOARD;
			x = localMoveSize->posX;
			y = localMoveSize->posY;
			/* FIXME: local keyboard moves not working */
			break;

		case RAIL_WMSZ_KEYSIZE:
			direction = NET_WM_MOVERESIZE_SIZE_KEYBOARD;
			x = localMoveSize->posX;
			y = localMoveSize->posY;
			/* FIXME: local keyboard moves not working */
			break;
		default:
			break;
	}

	if (localMoveSize->isMoveSizeStart)
		xf_StartLocalMoveSize(xfc, appWindow, direction, x, y);
	else
		xf_EndLocalMoveSize(xfc, appWindow);

	xf_rail_return_window(appWindow, FALSE);
	return CHANNEL_RC_OK;
}

/**
 * Function description
 *
 * @return 0 on success, otherwise a Win32 error code
 */
static UINT xf_rail_server_min_max_info(RailClientContext* context,
                                        const RAIL_MINMAXINFO_ORDER* minMaxInfo)
{
	WINPR_ASSERT(context);
	WINPR_ASSERT(minMaxInfo);

	xfContext* xfc = (xfContext*)context->custom;
	xfAppWindow* appWindow = xf_rail_get_window(xfc, minMaxInfo->windowId, FALSE);

	if (appWindow)
	{
		xf_SetWindowMinMaxInfo(xfc, appWindow, minMaxInfo->maxWidth, minMaxInfo->maxHeight,
		                       minMaxInfo->maxPosX, minMaxInfo->maxPosY, minMaxInfo->minTrackWidth,
		                       minMaxInfo->minTrackHeight, minMaxInfo->maxTrackWidth,
		                       minMaxInfo->maxTrackHeight);
	}
	xf_rail_return_window(appWindow, FALSE);

	return CHANNEL_RC_OK;
}

/**
 * Function description
 *
 * @return 0 on success, otherwise a Win32 error code
 */
static UINT
xf_rail_server_language_bar_info(WINPR_ATTR_UNUSED RailClientContext* context,
                                 WINPR_ATTR_UNUSED const RAIL_LANGBAR_INFO_ORDER* langBarInfo)
{
	WLog_ERR("TODO", "TODO: implement");
	return CHANNEL_RC_OK;
}

/**
 * Function description
 *
 * @return 0 on success, otherwise a Win32 error code
 */
static UINT
xf_rail_server_get_appid_response(WINPR_ATTR_UNUSED RailClientContext* context,
                                  WINPR_ATTR_UNUSED const RAIL_GET_APPID_RESP_ORDER* getAppIdResp)
{
	WLog_ERR("TODO", "TODO: implement");
	return CHANNEL_RC_OK;
}

static BOOL rail_window_key_equals(const void* key1, const void* key2)
{
	const UINT64* k1 = (const UINT64*)key1;
	const UINT64* k2 = (const UINT64*)key2;

	if (!k1 || !k2)
		return FALSE;

	return *k1 == *k2;
}

static UINT32 rail_window_key_hash(const void* key)
{
	const UINT64* k1 = (const UINT64*)key;
	return (UINT32)*k1;
}

static void rail_window_free(void* value)
{
	xfAppWindow* appWindow = (xfAppWindow*)value;

	if (!appWindow)
		return;

	xf_DestroyWindow(appWindow->xfc, appWindow);
}

int xf_rail_init(xfContext* xfc, RailClientContext* rail)
{
	rdpContext* context = (rdpContext*)xfc;

	if (!xfc || !rail)
		return 0;

	xfc->rail = rail;
	xf_rail_register_update_callbacks(context->update);
	rail->custom = (void*)xfc;
	rail->ServerExecuteResult = xf_rail_server_execute_result;
	rail->ServerSystemParam = xf_rail_server_system_param;
	rail->ServerLocalMoveSize = xf_rail_server_local_move_size;
	rail->ServerMinMaxInfo = xf_rail_server_min_max_info;
	rail->ServerLanguageBarInfo = xf_rail_server_language_bar_info;
	rail->ServerGetAppIdResponse = xf_rail_server_get_appid_response;
	xfc->railWindows = HashTable_New(TRUE);

	if (!xfc->railWindows)
		return 0;

	if (!HashTable_SetHashFunction(xfc->railWindows, rail_window_key_hash))
		goto fail;
	{
		wObject* obj = HashTable_KeyObject(xfc->railWindows);
		obj->fnObjectEquals = rail_window_key_equals;
	}
	{
		wObject* obj = HashTable_ValueObject(xfc->railWindows);
		obj->fnObjectFree = rail_window_free;
	}
	xfc->railIconCache = RailIconCache_New(xfc->common.context.settings);

	if (!xfc->railIconCache)
	{
	}

	return 1;
fail:
	HashTable_Free(xfc->railWindows);
	return 0;
}

int xf_rail_uninit(xfContext* xfc, RailClientContext* rail)
{
	WINPR_UNUSED(rail);

	if (xfc->rail)
	{
		xfc->rail->custom = nullptr;
		xfc->rail = nullptr;
	}

	if (xfc->railWindows)
	{
		HashTable_Free(xfc->railWindows);
		xfc->railWindows = nullptr;
	}

	if (xfc->railIconCache)
	{
		RailIconCache_Free(xfc->railIconCache);
		xfc->railIconCache = nullptr;
	}

	return 1;
}

xfAppWindow* xf_rail_add_window(xfContext* xfc, UINT64 id, INT32 x, INT32 y, UINT32 width,
                                UINT32 height, UINT32 surfaceId)
{
	if (!xfc)
		return nullptr;

	xfAppWindow* appWindow = (xfAppWindow*)calloc(1, sizeof(xfAppWindow));

	if (!appWindow)
		return nullptr;

	appWindow->xfc = xfc;
	appWindow->windowId = id;
	appWindow->surfaceId = surfaceId;
	appWindow->x = x;
	appWindow->y = y;
	appWindow->width = WINPR_ASSERTING_INT_CAST(int, width);
	appWindow->height = WINPR_ASSERTING_INT_CAST(int, height);

	xf_AppWindowsLock(xfc);
	if (!xf_AppWindowCreate(xfc, appWindow))
		goto fail;

	if (!HashTable_Insert(xfc->railWindows, &appWindow->windowId, (void*)appWindow))
		goto fail;
	return appWindow;
fail:
	rail_window_free(appWindow);
	xf_AppWindowsUnlock(xfc);
	return nullptr;
}

BOOL xf_rail_del_window(xfContext* xfc, UINT64 id)
{
	if (!xfc)
		return FALSE;

	if (!xfc->railWindows)
		return FALSE;

	xf_lock_x11(xfc);
	const BOOL res = HashTable_Remove(xfc->railWindows, &id);
	xf_unlock_x11(xfc);
	return res;
}

void xf_rail_return_windowFrom(xfAppWindow* window, BOOL alreadyLocked, const char* file,
                               const char* fkt, size_t line)
{
	if (!window)
		return;

	if (alreadyLocked)
		return;

	xfAppWindowsUnlockFrom(window->xfc, file, fkt, line);
}
