/**
 * FreeRDP: A Remote Desktop Protocol Implementation
 * Wayland Clipboard Redirection
 *
 * Copyright 2018 Armin Novak <armin.novak@thincast.com>
 * Copyright 2018 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 <stdlib.h>

#include <winpr/crt.h>
#include <winpr/image.h>
#include <winpr/stream.h>
#include <winpr/clipboard.h>

#include <freerdp/log.h>
#include <freerdp/client/cliprdr.h>
#include <freerdp/channels/channels.h>
#include <freerdp/channels/cliprdr.h>

#include <freerdp/client/client_cliprdr_file.h>

#include "wlf_cliprdr.h"

#define TAG CLIENT_TAG("wayland.cliprdr")

#define mime_text_plain "text/plain"
// NOLINTNEXTLINE(bugprone-suspicious-missing-comma)
static const char mime_text_utf8[] = mime_text_plain ";charset=utf-8";

// NOLINTNEXTLINE(bugprone-suspicious-missing-comma)
static const char* mime_text[] = { mime_text_plain, mime_text_utf8, "UTF8_STRING",
	                               "COMPOUND_TEXT", "TEXT",         "STRING" };

static const char mime_png[] = "image/png";
static const char mime_webp[] = "image/webp";
static const char mime_jpg[] = "image/jpeg";
static const char mime_tiff[] = "image/tiff";
static const char mime_uri_list[] = "text/uri-list";
static const char mime_html[] = "text/html";

#define BMP_MIME_LIST "image/bmp", "image/x-bmp", "image/x-MS-bmp", "image/x-win-bitmap"
static const char* mime_bitmap[] = { BMP_MIME_LIST };
static const char* mime_image[] = { mime_png, mime_webp, mime_jpg, mime_tiff, BMP_MIME_LIST };

static const char mime_gnome_copied_files[] = "x-special/gnome-copied-files";
static const char mime_mate_copied_files[] = "x-special/mate-copied-files";

static const char type_FileGroupDescriptorW[] = "FileGroupDescriptorW";
static const char type_HtmlFormat[] = "HTML Format";

typedef struct
{
	FILE* responseFile;
	UINT32 responseFormat;
	char* responseMime;
} wlf_request;

typedef struct
{
	const FILE* responseFile;
	UINT32 responseFormat;
	const char* responseMime;
} wlf_const_request;

struct wlf_clipboard
{
	wlfContext* wfc;
	rdpChannels* channels;
	CliprdrClientContext* context;
	wLog* log;

	UwacSeat* seat;
	wClipboard* system;

	size_t numClientFormats;
	CLIPRDR_FORMAT* clientFormats;

	size_t numServerFormats;
	CLIPRDR_FORMAT* serverFormats;

	BOOL sync;

	CRITICAL_SECTION lock;
	CliprdrFileContext* file;

	wQueue* request_queue;
};

static void wlf_request_free(void* rq)
{
	wlf_request* request = rq;
	if (request)
	{
		free(request->responseMime);
		if (request->responseFile)
			(void)fclose(request->responseFile);
	}
	free(request);
}

static wlf_request* wlf_request_new(void)
{
	return calloc(1, sizeof(wlf_request));
}

static void* wlf_request_clone(const void* oth)
{
	const wlf_request* other = (const wlf_request*)oth;
	wlf_request* copy = wlf_request_new();
	if (!copy)
		return nullptr;
	*copy = *other;
	if (other->responseMime)
	{
		copy->responseMime = _strdup(other->responseMime);
		if (!copy->responseMime)
			goto fail;
	}
	return copy;
fail:
	wlf_request_free(copy);
	return nullptr;
}

static BOOL wlf_mime_is_file(const char* mime)
{
	if (strncmp(mime_uri_list, mime, sizeof(mime_uri_list)) == 0)
		return TRUE;
	if (strncmp(mime_gnome_copied_files, mime, sizeof(mime_gnome_copied_files)) == 0)
		return TRUE;
	if (strncmp(mime_mate_copied_files, mime, sizeof(mime_mate_copied_files)) == 0)
		return TRUE;
	return FALSE;
}

static BOOL wlf_mime_is_text(const char* mime)
{
	for (size_t x = 0; x < ARRAYSIZE(mime_text); x++)
	{
		if (strcmp(mime, mime_text[x]) == 0)
			return TRUE;
	}

	return FALSE;
}

static BOOL wlf_mime_is_image(const char* mime)
{
	for (size_t x = 0; x < ARRAYSIZE(mime_image); x++)
	{
		if (strcmp(mime, mime_image[x]) == 0)
			return TRUE;
	}

	return FALSE;
}

static BOOL wlf_mime_is_html(const char* mime)
{
	return strcmp(mime, mime_html) == 0;
}

static void wlf_cliprdr_free_server_formats(wfClipboard* clipboard)
{
	if (clipboard && clipboard->serverFormats)
	{
		for (size_t j = 0; j < clipboard->numServerFormats; j++)
		{
			CLIPRDR_FORMAT* format = &clipboard->serverFormats[j];
			free(format->formatName);
		}

		free(clipboard->serverFormats);
		clipboard->serverFormats = nullptr;
		clipboard->numServerFormats = 0;
	}

	if (clipboard)
		UwacClipboardOfferDestroy(clipboard->seat);
}

static void wlf_cliprdr_free_client_formats(wfClipboard* clipboard)
{
	if (clipboard && clipboard->numClientFormats)
	{
		for (size_t j = 0; j < clipboard->numClientFormats; j++)
		{
			CLIPRDR_FORMAT* format = &clipboard->clientFormats[j];
			free(format->formatName);
		}

		free(clipboard->clientFormats);
		clipboard->clientFormats = nullptr;
		clipboard->numClientFormats = 0;
	}

	if (clipboard)
		UwacClipboardOfferDestroy(clipboard->seat);
}

/**
 * Function description
 *
 * @return 0 on success, otherwise a Win32 error code
 */
static UINT wlf_cliprdr_send_client_format_list(wfClipboard* clipboard)
{
	WINPR_ASSERT(clipboard);

	const CLIPRDR_FORMAT_LIST formatList = { .common.msgFlags = 0,
		                                     .numFormats = (UINT32)clipboard->numClientFormats,
		                                     .formats = clipboard->clientFormats,
		                                     .common.msgType = CB_FORMAT_LIST };

	if (!cliprdr_file_context_clear(clipboard->file))
		return ERROR_INTERNAL_ERROR;

	WLog_VRB(TAG, "-------------- client format list [%" PRIu32 "] ------------------",
	         formatList.numFormats);
	for (UINT32 x = 0; x < formatList.numFormats; x++)
	{
		const CLIPRDR_FORMAT* format = &formatList.formats[x];
		WLog_VRB(TAG, "client announces %" PRIu32 " [%s][%s]", format->formatId,
		         ClipboardGetFormatIdString(format->formatId), format->formatName);
	}
	WINPR_ASSERT(clipboard->context);
	WINPR_ASSERT(clipboard->context->ClientFormatList);
	return clipboard->context->ClientFormatList(clipboard->context, &formatList);
}

static void wfl_cliprdr_add_client_format_id(wfClipboard* clipboard, UINT32 formatId)
{
	CLIPRDR_FORMAT* format = nullptr;
	const char* name = ClipboardGetFormatName(clipboard->system, formatId);

	for (size_t x = 0; x < clipboard->numClientFormats; x++)
	{
		format = &clipboard->clientFormats[x];

		if (format->formatId == formatId)
			return;
	}

	format = realloc(clipboard->clientFormats,
	                 (clipboard->numClientFormats + 1) * sizeof(CLIPRDR_FORMAT));

	if (!format)
		return;

	clipboard->clientFormats = format;
	format = &clipboard->clientFormats[clipboard->numClientFormats++];
	format->formatId = formatId;
	format->formatName = nullptr;

	if (name && (formatId >= CF_MAX))
		format->formatName = _strdup(name);
}

static BOOL wlf_cliprdr_add_client_format(wfClipboard* clipboard, const char* mime)
{
	WINPR_ASSERT(mime);
	ClipboardLock(clipboard->system);
	if (wlf_mime_is_html(mime))
	{
		UINT32 formatId = ClipboardGetFormatId(clipboard->system, type_HtmlFormat);
		wfl_cliprdr_add_client_format_id(clipboard, formatId);
	}
	else if (wlf_mime_is_text(mime))
	{
		wfl_cliprdr_add_client_format_id(clipboard, CF_TEXT);
		wfl_cliprdr_add_client_format_id(clipboard, CF_OEMTEXT);
		wfl_cliprdr_add_client_format_id(clipboard, CF_UNICODETEXT);
	}
	else if (wlf_mime_is_image(mime))
	{
		for (size_t x = 0; x < ARRAYSIZE(mime_image); x++)
		{
			const char* mime_bmp = mime_image[x];
			UINT32 formatId = ClipboardGetFormatId(clipboard->system, mime_bmp);
			if (formatId != 0)
				wfl_cliprdr_add_client_format_id(clipboard, formatId);
		}
		wfl_cliprdr_add_client_format_id(clipboard, CF_DIB);
		wfl_cliprdr_add_client_format_id(clipboard, CF_TIFF);
	}
	else if (wlf_mime_is_file(mime))
	{
		const UINT32 fileFormatId =
		    ClipboardGetFormatId(clipboard->system, type_FileGroupDescriptorW);
		wfl_cliprdr_add_client_format_id(clipboard, fileFormatId);
	}

	ClipboardUnlock(clipboard->system);
	return (wlf_cliprdr_send_client_format_list(clipboard) == CHANNEL_RC_OK);
}

/**
 * Function description
 *
 * @return 0 on success, otherwise a Win32 error code
 */
static UINT wlf_cliprdr_send_data_request(wfClipboard* clipboard, const wlf_const_request* rq)
{
	WINPR_ASSERT(rq);

	CLIPRDR_FORMAT_DATA_REQUEST request = { .requestedFormatId = rq->responseFormat };

	if (!Queue_Enqueue(clipboard->request_queue, rq))
		return ERROR_INTERNAL_ERROR;

	WINPR_ASSERT(clipboard);
	WINPR_ASSERT(clipboard->context);
	WINPR_ASSERT(clipboard->context->ClientFormatDataRequest);
	return clipboard->context->ClientFormatDataRequest(clipboard->context, &request);
}

/**
 * Function description
 *
 * @return 0 on success, otherwise a Win32 error code
 */
static UINT wlf_cliprdr_send_data_response(wfClipboard* clipboard, const BYTE* data, size_t size)
{
	CLIPRDR_FORMAT_DATA_RESPONSE response = WINPR_C_ARRAY_INIT;

	if (size > UINT32_MAX)
		return ERROR_INVALID_PARAMETER;

	response.common.msgFlags = (data) ? CB_RESPONSE_OK : CB_RESPONSE_FAIL;
	response.common.dataLen = (UINT32)size;
	response.requestedFormatData = data;

	WINPR_ASSERT(clipboard);
	WINPR_ASSERT(clipboard->context);
	WINPR_ASSERT(clipboard->context->ClientFormatDataResponse);
	return clipboard->context->ClientFormatDataResponse(clipboard->context, &response);
}

BOOL wlf_cliprdr_handle_event(wfClipboard* clipboard, const UwacClipboardEvent* event)
{
	if (!clipboard || !event)
		return FALSE;

	if (!clipboard->context)
		return TRUE;

	switch (event->type)
	{
		case UWAC_EVENT_CLIPBOARD_AVAILABLE:
			clipboard->seat = event->seat;
			return TRUE;

		case UWAC_EVENT_CLIPBOARD_OFFER:
			WLog_Print(clipboard->log, WLOG_DEBUG, "client announces mime %s", event->mime);
			return wlf_cliprdr_add_client_format(clipboard, event->mime);

		case UWAC_EVENT_CLIPBOARD_SELECT:
			WLog_Print(clipboard->log, WLOG_DEBUG, "client announces new data");
			wlf_cliprdr_free_client_formats(clipboard);
			return TRUE;

		default:
			return FALSE;
	}
}

/**
 * Function description
 *
 * @return 0 on success, otherwise a Win32 error code
 */
static UINT wlf_cliprdr_send_client_capabilities(wfClipboard* clipboard)
{
	WINPR_ASSERT(clipboard);

	CLIPRDR_GENERAL_CAPABILITY_SET generalCapabilitySet = {
		.capabilitySetType = CB_CAPSTYPE_GENERAL,
		.capabilitySetLength = 12,
		.version = CB_CAPS_VERSION_2,
		.generalFlags =
		    CB_USE_LONG_FORMAT_NAMES | cliprdr_file_context_current_flags(clipboard->file)
	};
	CLIPRDR_CAPABILITIES capabilities = { .cCapabilitiesSets = 1,
		                                  .capabilitySets =
		                                      (CLIPRDR_CAPABILITY_SET*)&(generalCapabilitySet) };

	WINPR_ASSERT(clipboard);
	WINPR_ASSERT(clipboard->context);
	WINPR_ASSERT(clipboard->context->ClientCapabilities);
	return clipboard->context->ClientCapabilities(clipboard->context, &capabilities);
}

/**
 * Function description
 *
 * @return 0 on success, otherwise a Win32 error code
 */
static UINT wlf_cliprdr_send_client_format_list_response(wfClipboard* clipboard, BOOL status)
{
	const CLIPRDR_FORMAT_LIST_RESPONSE formatListResponse = {
		.common.msgType = CB_FORMAT_LIST_RESPONSE,
		.common.msgFlags = status ? CB_RESPONSE_OK : CB_RESPONSE_FAIL,
		.common.dataLen = 0
	};
	WINPR_ASSERT(clipboard);
	WINPR_ASSERT(clipboard->context);
	WINPR_ASSERT(clipboard->context->ClientFormatListResponse);
	return clipboard->context->ClientFormatListResponse(clipboard->context, &formatListResponse);
}

/**
 * Function description
 *
 * @return 0 on success, otherwise a Win32 error code
 */
static UINT wlf_cliprdr_monitor_ready(CliprdrClientContext* context,
                                      const CLIPRDR_MONITOR_READY* monitorReady)
{
	UINT ret = 0;

	WINPR_UNUSED(monitorReady);
	WINPR_ASSERT(context);
	WINPR_ASSERT(monitorReady);

	wfClipboard* clipboard = cliprdr_file_context_get_context(context->custom);
	WINPR_ASSERT(clipboard);

	if ((ret = wlf_cliprdr_send_client_capabilities(clipboard)) != CHANNEL_RC_OK)
		return ret;

	if ((ret = wlf_cliprdr_send_client_format_list(clipboard)) != CHANNEL_RC_OK)
		return ret;

	clipboard->sync = TRUE;
	return CHANNEL_RC_OK;
}

/**
 * Function description
 *
 * @return 0 on success, otherwise a Win32 error code
 */
static UINT wlf_cliprdr_server_capabilities(CliprdrClientContext* context,
                                            const CLIPRDR_CAPABILITIES* capabilities)
{
	WINPR_ASSERT(context);
	WINPR_ASSERT(capabilities);

	const BYTE* capsPtr = (const BYTE*)capabilities->capabilitySets;
	WINPR_ASSERT(capsPtr);

	wfClipboard* clipboard = cliprdr_file_context_get_context(context->custom);
	WINPR_ASSERT(clipboard);

	if (!cliprdr_file_context_remote_set_flags(clipboard->file, 0))
		return ERROR_INTERNAL_ERROR;

	for (UINT32 i = 0; i < capabilities->cCapabilitiesSets; i++)
	{
		const CLIPRDR_CAPABILITY_SET* caps = (const CLIPRDR_CAPABILITY_SET*)capsPtr;

		if (caps->capabilitySetType == CB_CAPSTYPE_GENERAL)
		{
			const CLIPRDR_GENERAL_CAPABILITY_SET* generalCaps =
			    (const CLIPRDR_GENERAL_CAPABILITY_SET*)caps;

			if (!cliprdr_file_context_remote_set_flags(clipboard->file, generalCaps->generalFlags))
				return ERROR_INTERNAL_ERROR;
		}

		capsPtr += caps->capabilitySetLength;
	}

	return CHANNEL_RC_OK;
}

static UINT32 wlf_get_server_format_id(const wfClipboard* clipboard, const char* name)
{
	WINPR_ASSERT(clipboard);
	WINPR_ASSERT(name);

	for (UINT32 x = 0; x < clipboard->numServerFormats; x++)
	{
		const CLIPRDR_FORMAT* format = &clipboard->serverFormats[x];
		if (!format->formatName)
			continue;
		if (strcmp(name, format->formatName) == 0)
			return format->formatId;
	}
	return 0;
}

static const char* wlf_get_server_format_name(const wfClipboard* clipboard, UINT32 formatId)
{
	WINPR_ASSERT(clipboard);

	for (UINT32 x = 0; x < clipboard->numServerFormats; x++)
	{
		const CLIPRDR_FORMAT* format = &clipboard->serverFormats[x];
		if (format->formatId == formatId)
			return format->formatName;
	}
	return nullptr;
}

static void wlf_cliprdr_transfer_data(UwacSeat* seat, void* context, const char* mime, int fd)
{
	wfClipboard* clipboard = (wfClipboard*)context;
	WINPR_UNUSED(seat);

	EnterCriticalSection(&clipboard->lock);

	wlf_const_request request = WINPR_C_ARRAY_INIT;
	if (wlf_mime_is_html(mime))
	{
		request.responseMime = mime_html;
		request.responseFormat = wlf_get_server_format_id(clipboard, type_HtmlFormat);
	}
	else if (wlf_mime_is_file(mime))
	{
		request.responseMime = mime;
		request.responseFormat = wlf_get_server_format_id(clipboard, type_FileGroupDescriptorW);
	}
	else if (wlf_mime_is_text(mime))
	{
		request.responseMime = mime_text_plain;
		request.responseFormat = CF_UNICODETEXT;
	}
	else if (wlf_mime_is_image(mime))
	{
		request.responseMime = mime;
		if (strcmp(mime, mime_tiff) == 0)
			request.responseFormat = CF_TIFF;
		else
			request.responseFormat = CF_DIB;
	}

	if (request.responseMime != nullptr)
	{
		request.responseFile = fdopen(fd, "w");

		if (request.responseFile)
			wlf_cliprdr_send_data_request(clipboard, &request);
		else
			WLog_Print(clipboard->log, WLOG_ERROR,
			           "failed to open clipboard file descriptor for MIME %s",
			           request.responseMime);
	}

	LeaveCriticalSection(&clipboard->lock);
}

static void wlf_cliprdr_cancel_data(UwacSeat* seat, void* context)
{
	wfClipboard* clipboard = (wfClipboard*)context;

	WINPR_UNUSED(seat);
	WINPR_ASSERT(clipboard);
	cliprdr_file_context_clear(clipboard->file);
}

/**
 * Called when the clipboard changes server side.
 *
 * Clear the local clipboard offer and replace it with a new one
 * that announces the formats we get listed here.
 *
 * @return 0 on success, otherwise a Win32 error code
 */
static UINT wlf_cliprdr_server_format_list(CliprdrClientContext* context,
                                           const CLIPRDR_FORMAT_LIST* formatList)
{
	BOOL html = FALSE;
	BOOL text = FALSE;
	BOOL image = FALSE;
	BOOL file = FALSE;

	if (!context || !context->custom)
		return ERROR_INVALID_PARAMETER;

	wfClipboard* clipboard = cliprdr_file_context_get_context(context->custom);
	WINPR_ASSERT(clipboard);

	wlf_cliprdr_free_server_formats(clipboard);
	if (!cliprdr_file_context_clear(clipboard->file))
		return ERROR_INTERNAL_ERROR;

	if (!(clipboard->serverFormats =
	          (CLIPRDR_FORMAT*)calloc(formatList->numFormats, sizeof(CLIPRDR_FORMAT))))
	{
		WLog_Print(clipboard->log, WLOG_ERROR,
		           "failed to allocate %" PRIuz " CLIPRDR_FORMAT structs",
		           clipboard->numServerFormats);
		return CHANNEL_RC_NO_MEMORY;
	}

	clipboard->numServerFormats = formatList->numFormats;

	if (!clipboard->seat)
	{
		WLog_Print(clipboard->log, WLOG_ERROR,
		           "clipboard->seat=nullptr, check your client implementation");
		return ERROR_INTERNAL_ERROR;
	}

	for (UINT32 i = 0; i < formatList->numFormats; i++)
	{
		const CLIPRDR_FORMAT* format = &formatList->formats[i];
		CLIPRDR_FORMAT* srvFormat = &clipboard->serverFormats[i];
		srvFormat->formatId = format->formatId;

		if (format->formatName)
		{
			srvFormat->formatName = _strdup(format->formatName);

			if (!srvFormat->formatName)
			{
				wlf_cliprdr_free_server_formats(clipboard);
				return CHANNEL_RC_NO_MEMORY;
			}
		}

		if (format->formatName)
		{
			if (strcmp(format->formatName, type_HtmlFormat) == 0)
			{
				text = TRUE;
				html = TRUE;
			}
			else if (strcmp(format->formatName, type_FileGroupDescriptorW) == 0)
			{
				file = TRUE;
				text = TRUE;
			}
		}
		else
		{
			switch (format->formatId)
			{
				case CF_TEXT:
				case CF_OEMTEXT:
				case CF_UNICODETEXT:
					text = TRUE;
					break;

				case CF_DIB:
					image = TRUE;
					break;

				default:
					break;
			}
		}
	}

	if (html)
	{
		UwacClipboardOfferCreate(clipboard->seat, mime_html);
	}

	if (file && cliprdr_file_context_has_local_support(clipboard->file))
	{
		UwacClipboardOfferCreate(clipboard->seat, mime_uri_list);
		UwacClipboardOfferCreate(clipboard->seat, mime_gnome_copied_files);
		UwacClipboardOfferCreate(clipboard->seat, mime_mate_copied_files);
	}

	if (text)
	{
		for (size_t x = 0; x < ARRAYSIZE(mime_text); x++)
			UwacClipboardOfferCreate(clipboard->seat, mime_text[x]);
	}

	if (image)
	{
		for (size_t x = 0; x < ARRAYSIZE(mime_image); x++)
			UwacClipboardOfferCreate(clipboard->seat, mime_image[x]);
	}

	UwacClipboardOfferAnnounce(clipboard->seat, clipboard, wlf_cliprdr_transfer_data,
	                           wlf_cliprdr_cancel_data);
	return wlf_cliprdr_send_client_format_list_response(clipboard, TRUE);
}

/**
 * Function description
 *
 * @return 0 on success, otherwise a Win32 error code
 */
static UINT
wlf_cliprdr_server_format_list_response(WINPR_ATTR_UNUSED CliprdrClientContext* context,
                                        const CLIPRDR_FORMAT_LIST_RESPONSE* formatListResponse)
{
	WINPR_ASSERT(context);
	WINPR_ASSERT(formatListResponse);

	if (formatListResponse->common.msgFlags & CB_RESPONSE_FAIL)
		WLog_WARN(TAG, "format list update failed");
	return CHANNEL_RC_OK;
}

/**
 * Function description
 *
 * @return 0 on success, otherwise a Win32 error code
 */
static UINT
wlf_cliprdr_server_format_data_request(CliprdrClientContext* context,
                                       const CLIPRDR_FORMAT_DATA_REQUEST* formatDataRequest)
{
	UINT rc = CHANNEL_RC_OK;
	char* data = nullptr;
	size_t size = 0;
	const char* mime = nullptr;
	UINT32 formatId = 0;
	UINT32 localFormatId = 0;
	wfClipboard* clipboard = nullptr;

	UINT32 dsize = 0;
	BYTE* ddata = nullptr;

	WINPR_ASSERT(context);
	WINPR_ASSERT(formatDataRequest);

	localFormatId = formatId = formatDataRequest->requestedFormatId;
	clipboard = cliprdr_file_context_get_context(context->custom);
	WINPR_ASSERT(clipboard);

	ClipboardLock(clipboard->system);
	const UINT32 fileFormatId = ClipboardGetFormatId(clipboard->system, type_FileGroupDescriptorW);
	const UINT32 htmlFormatId = ClipboardGetFormatId(clipboard->system, type_HtmlFormat);

	switch (formatId)
	{
		case CF_TEXT:
		case CF_OEMTEXT:
		case CF_UNICODETEXT:
			localFormatId = ClipboardGetFormatId(clipboard->system, mime_text_plain);
			mime = mime_text_utf8;
			break;

		case CF_DIB:
		case CF_DIBV5:
			mime = mime_bitmap[0];
			break;

		case CF_TIFF:
			mime = mime_tiff;
			break;

		default:
			if (formatId == fileFormatId)
			{
				localFormatId = ClipboardGetFormatId(clipboard->system, mime_uri_list);
				mime = mime_uri_list;
			}
			else if (formatId == htmlFormatId)
			{
				localFormatId = ClipboardGetFormatId(clipboard->system, mime_html);
				mime = mime_html;
			}
			else
				goto fail;
			break;
	}

	data = UwacClipboardDataGet(clipboard->seat, mime, &size);

	if (!data || (size > UINT32_MAX))
		goto fail;

	if (fileFormatId == formatId)
	{
		if (!cliprdr_file_context_update_client_data(clipboard->file, data, size))
			goto fail;
	}

	{
		const BOOL res = ClipboardSetData(clipboard->system, localFormatId, data, (UINT32)size);
		free(data);

		UINT32 len = 0;
		data = nullptr;
		if (res)
			data = ClipboardGetData(clipboard->system, formatId, &len);

		if (!res || !data)
			goto fail;

		if (fileFormatId == formatId)
		{
			const UINT32 flags = cliprdr_file_context_remote_get_flags(clipboard->file);
			const UINT32 error = cliprdr_serialize_file_list_ex(
			    flags, (const FILEDESCRIPTORW*)data, len / sizeof(FILEDESCRIPTORW), &ddata, &dsize);
			if (error)
				goto fail;
		}
	}
fail:
	ClipboardUnlock(clipboard->system);
	rc = wlf_cliprdr_send_data_response(clipboard, ddata, dsize);
	free(data);
	return rc;
}

/**
 * Function description
 *
 * @return 0 on success, otherwise a Win32 error code
 */
static UINT
wlf_cliprdr_server_format_data_response(CliprdrClientContext* context,
                                        const CLIPRDR_FORMAT_DATA_RESPONSE* formatDataResponse)
{
	UINT rc = ERROR_INTERNAL_ERROR;

	WINPR_ASSERT(context);
	WINPR_ASSERT(formatDataResponse);

	const UINT32 size = formatDataResponse->common.dataLen;
	const BYTE* data = formatDataResponse->requestedFormatData;

	wfClipboard* clipboard = cliprdr_file_context_get_context(context->custom);
	WINPR_ASSERT(clipboard);

	wlf_request* request = Queue_Dequeue(clipboard->request_queue);
	if (!request)
		goto fail;

	rc = CHANNEL_RC_OK;
	if (formatDataResponse->common.msgFlags & CB_RESPONSE_FAIL)
	{
		WLog_WARN(TAG, "clipboard data request for format %" PRIu32 " [%s], mime %s failed",
		          request->responseFormat, ClipboardGetFormatIdString(request->responseFormat),
		          request->responseMime);
		goto fail;
	}
	rc = ERROR_INTERNAL_ERROR;

	ClipboardLock(clipboard->system);
	EnterCriticalSection(&clipboard->lock);
	{
		BYTE* cdata = nullptr;
		UINT32 srcFormatId = 0;
		UINT32 dstFormatId = 0;
		switch (request->responseFormat)
		{
			case CF_TEXT:
			case CF_OEMTEXT:
			case CF_UNICODETEXT:
				srcFormatId = request->responseFormat;
				dstFormatId = ClipboardGetFormatId(clipboard->system, request->responseMime);
				break;

			case CF_DIB:
			case CF_DIBV5:
				srcFormatId = request->responseFormat;
				dstFormatId = ClipboardGetFormatId(clipboard->system, request->responseMime);
				break;

			default:
			{
				const char* name = wlf_get_server_format_name(clipboard, request->responseFormat);
				if (name)
				{
					if (strcmp(type_FileGroupDescriptorW, name) == 0)
					{
						srcFormatId =
						    ClipboardGetFormatId(clipboard->system, type_FileGroupDescriptorW);
						dstFormatId =
						    ClipboardGetFormatId(clipboard->system, request->responseMime);

						if (!cliprdr_file_context_update_server_data(clipboard->file,
						                                             clipboard->system, data, size))
							goto unlock;
					}
					else if (strcmp(type_HtmlFormat, name) == 0)
					{
						srcFormatId = ClipboardGetFormatId(clipboard->system, type_HtmlFormat);
						dstFormatId =
						    ClipboardGetFormatId(clipboard->system, request->responseMime);
					}
				}
			}
			break;
		}
		{
			UINT32 len = 0;

			{
				const BOOL sres = ClipboardSetData(clipboard->system, srcFormatId, data, size);
				if (sres)
					cdata = ClipboardGetData(clipboard->system, dstFormatId, &len);

				if (!sres || !cdata)
					goto unlock;
			}

			if (request->responseFile)
			{
				const size_t res = fwrite(cdata, 1, len, request->responseFile);
				if (res == len)
					rc = CHANNEL_RC_OK;
			}
			else
				rc = CHANNEL_RC_OK;
		}

	unlock:
		free(cdata);
	}
	ClipboardUnlock(clipboard->system);
	LeaveCriticalSection(&clipboard->lock);
fail:
	wlf_request_free(request);
	return rc;
}

wfClipboard* wlf_clipboard_new(wlfContext* wfc)
{
	rdpChannels* channels = nullptr;
	wfClipboard* clipboard = nullptr;

	WINPR_ASSERT(wfc);

	clipboard = (wfClipboard*)calloc(1, sizeof(wfClipboard));

	if (!clipboard)
		goto fail;

	InitializeCriticalSection(&clipboard->lock);
	clipboard->wfc = wfc;
	channels = wfc->common.context.channels;
	clipboard->log = WLog_Get(TAG);
	clipboard->channels = channels;
	clipboard->system = ClipboardCreate();
	if (!clipboard->system)
		goto fail;

	clipboard->file = cliprdr_file_context_new(clipboard);
	if (!clipboard->file)
		goto fail;

	if (!cliprdr_file_context_set_locally_available(clipboard->file, TRUE))
		goto fail;

	clipboard->request_queue = Queue_New(TRUE, -1, -1);
	if (!clipboard->request_queue)
		goto fail;

	{
		wObject* obj = Queue_Object(clipboard->request_queue);
		WINPR_ASSERT(obj);
		obj->fnObjectFree = wlf_request_free;
		obj->fnObjectNew = wlf_request_clone;
	}

	return clipboard;

fail:
	wlf_clipboard_free(clipboard);
	return nullptr;
}

void wlf_clipboard_free(wfClipboard* clipboard)
{
	if (!clipboard)
		return;

	cliprdr_file_context_free(clipboard->file);

	wlf_cliprdr_free_server_formats(clipboard);
	wlf_cliprdr_free_client_formats(clipboard);
	ClipboardDestroy(clipboard->system);

	EnterCriticalSection(&clipboard->lock);

	Queue_Free(clipboard->request_queue);
	LeaveCriticalSection(&clipboard->lock);
	DeleteCriticalSection(&clipboard->lock);
	free(clipboard);
}

BOOL wlf_cliprdr_init(wfClipboard* clipboard, CliprdrClientContext* cliprdr)
{
	WINPR_ASSERT(clipboard);
	WINPR_ASSERT(cliprdr);

	clipboard->context = cliprdr;
	cliprdr->MonitorReady = wlf_cliprdr_monitor_ready;
	cliprdr->ServerCapabilities = wlf_cliprdr_server_capabilities;
	cliprdr->ServerFormatList = wlf_cliprdr_server_format_list;
	cliprdr->ServerFormatListResponse = wlf_cliprdr_server_format_list_response;
	cliprdr->ServerFormatDataRequest = wlf_cliprdr_server_format_data_request;
	cliprdr->ServerFormatDataResponse = wlf_cliprdr_server_format_data_response;

	return cliprdr_file_context_init(clipboard->file, cliprdr);
}

BOOL wlf_cliprdr_uninit(wfClipboard* clipboard, CliprdrClientContext* cliprdr)
{
	WINPR_ASSERT(clipboard);
	if (!cliprdr_file_context_uninit(clipboard->file, cliprdr))
		return FALSE;

	if (cliprdr)
		cliprdr->custom = nullptr;

	return TRUE;
}
