/*
 * Remmina - The GTK+ Remote Desktop Client
 * Copyright (C) 2012-2012 Jean-Louis Dupond
 * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
 * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA  02110-1301, USA.
 *
 *  In addition, as a special exception, the copyright holders give
 *  permission to link the code of portions of this program with the
 *  OpenSSL library under certain conditions as described in each
 *  individual source file, and distribute linked combinations
 *  including the two.
 *  You must obey the GNU General Public License in all respects
 *  for all of the code used other than OpenSSL. *  If you modify
 *  file(s) with this exception, you may extend this exception to your
 *  version of the file(s), but you are not obligated to do so. *  If you
 *  do not wish to do so, delete this exception statement from your
 *  version. *  If you delete this exception statement from all source
 *  files in the program, then also delete it here.
 *
 */

#include "rdp_plugin.h"
#include "rdp_cliprdr.h"
#include "rdp_event.h"

#include <freerdp/freerdp.h>
#include <freerdp/channels/channels.h>
#include <freerdp/client/cliprdr.h>
#include <sys/time.h>

#define CLIPBOARD_TRANSFER_WAIT_TIME 6

#define CB_FORMAT_HTML 0xD010
#define CB_FORMAT_PNG 0xD011
#define CB_FORMAT_JPEG 0xD012
#define CB_FORMAT_GIF 0xD013
#define CB_FORMAT_TEXTURILIST 0xD014

UINT32 remmina_rdp_cliprdr_get_format_from_gdkatom(GdkAtom atom)
{
	TRACE_CALL(__func__);
	UINT32 rc;
	gchar *name = gdk_atom_name(atom);
	rc = 0;
	if (g_strcmp0("UTF8_STRING", name) == 0 || g_strcmp0("text/plain;charset=utf-8", name) == 0)
		rc = CF_UNICODETEXT;
	if (g_strcmp0("TEXT", name) == 0 || g_strcmp0("text/plain", name) == 0)
		rc = CF_TEXT;
	if (g_strcmp0("text/html", name) == 0)
		rc = CB_FORMAT_HTML;
	if (g_strcmp0("image/png", name) == 0)
		rc = CB_FORMAT_PNG;
	if (g_strcmp0("image/jpeg", name) == 0)
		rc = CB_FORMAT_JPEG;
	if (g_strcmp0("image/bmp", name) == 0)
		rc = CF_DIB;
	if (g_strcmp0("text/uri-list", name) == 0)
		rc = CB_FORMAT_TEXTURILIST;
	g_free(name);
	return rc;
}

/* Never used? */
void remmina_rdp_cliprdr_get_target_types(UINT32 **formats, UINT16 *size, GdkAtom *types, int count)
{
	TRACE_CALL(__func__);
	int i;
	*size = 1;
	*formats = (UINT32 *)malloc(sizeof(UINT32) * (count + 1));

	*formats[0] = 0;
	for (i = 0; i < count; i++) {
		UINT32 format = remmina_rdp_cliprdr_get_format_from_gdkatom(types[i]);
		if (format != 0) {
			(*formats)[*size] = format;
			(*size)++;
		}
	}

	*formats = realloc(*formats, sizeof(UINT32) * (*size));
}

static UINT8 *lf2crlf(UINT8 *data, int *size)
{
	TRACE_CALL(__func__);
	UINT8 c;
	UINT8 *outbuf;
	UINT8 *out;
	UINT8 *in_end;
	UINT8 *in;
	int out_size;

	out_size = (*size) * 2 + 1;
	outbuf = (UINT8 *)malloc(out_size);
	out = outbuf;
	in = data;
	in_end = data + (*size);

	while (in < in_end) {
		c = *in++;
		if (c == '\n') {
			*out++ = '\r';
			*out++ = '\n';
		} else {
			*out++ = c;
		}
	}

	*out++ = 0;
	*size = out - outbuf;

	return outbuf;
}

static void crlf2lf(UINT8 *data, size_t *size)
{
	TRACE_CALL(__func__);
	UINT8 c;
	UINT8 *out;
	UINT8 *in;
	UINT8 *in_end;

	out = data;
	in = data;
	in_end = data + (*size);

	while (in < in_end) {
		c = *in++;
		if (c != '\r')
			*out++ = c;
	}

	*size = out - data;
}

/* Never used? */
int remmina_rdp_cliprdr_server_file_contents_request(CliprdrClientContext *context, CLIPRDR_FILE_CONTENTS_REQUEST *fileContentsRequest)
{
	TRACE_CALL(__func__);
	return -1;
}

/* Never used? */
int remmina_rdp_cliprdr_server_file_contents_response(CliprdrClientContext *context, CLIPRDR_FILE_CONTENTS_RESPONSE *fileContentsResponse)
{
	TRACE_CALL(__func__);
	return 1;
}

void remmina_rdp_cliprdr_send_client_format_list(RemminaProtocolWidget *gp)
{
	TRACE_CALL(__func__);
	RemminaPluginRdpUiObject *ui;
	rfContext *rfi = GET_PLUGIN_DATA(gp);
	rfClipboard *clipboard;
	CLIPRDR_FORMAT_LIST *pFormatList;
	RemminaPluginRdpEvent rdp_event = { 0 };

	if (!rfi || !rfi->connected || rfi->is_reconnecting)
		return;

	clipboard = &(rfi->clipboard);

	ui = g_new0(RemminaPluginRdpUiObject, 1);
	ui->type = REMMINA_RDP_UI_CLIPBOARD;
	ui->clipboard.clipboard = clipboard;
	ui->clipboard.type = REMMINA_RDP_UI_CLIPBOARD_FORMATLIST;
	pFormatList = remmina_rdp_event_queue_ui_sync_retptr(gp, ui);

	rdp_event.type = REMMINA_RDP_EVENT_TYPE_CLIPBOARD_SEND_CLIENT_FORMAT_LIST;
	rdp_event.clipboard_formatlist.pFormatList = pFormatList;
	remmina_rdp_event_event_push(gp, &rdp_event);
}

static void remmina_rdp_cliprdr_send_client_capabilities(rfClipboard *clipboard)
{
	TRACE_CALL(__func__);
	CLIPRDR_CAPABILITIES capabilities;
	CLIPRDR_GENERAL_CAPABILITY_SET generalCapabilitySet;

	capabilities.cCapabilitiesSets = 1;
	capabilities.capabilitySets = (CLIPRDR_CAPABILITY_SET *)&(generalCapabilitySet);

	generalCapabilitySet.capabilitySetType = CB_CAPSTYPE_GENERAL;
	generalCapabilitySet.capabilitySetLength = 12;

	generalCapabilitySet.version = CB_CAPS_VERSION_2;
	generalCapabilitySet.generalFlags = CB_USE_LONG_FORMAT_NAMES;

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

static UINT remmina_rdp_cliprdr_monitor_ready(CliprdrClientContext *context, const CLIPRDR_MONITOR_READY *monitorReady)
{
	TRACE_CALL(__func__);
	rfClipboard *clipboard = (rfClipboard *)context->custom;
	RemminaProtocolWidget *gp;

	remmina_rdp_cliprdr_send_client_capabilities(clipboard);
	gp = clipboard->rfi->protocol_widget;
	remmina_rdp_cliprdr_send_client_format_list(gp);

	return CHANNEL_RC_OK;
}

static UINT remmina_rdp_cliprdr_server_capabilities(CliprdrClientContext *context, const CLIPRDR_CAPABILITIES *capabilities)
{
	TRACE_CALL(__func__);
	return CHANNEL_RC_OK;
}


void remmina_rdp_cliprdr_cached_clipboard_free(rfClipboard *clipboard)
{
	TRACE_CALL(__func__);
	guint fmt;

	pthread_mutex_lock(&clipboard->srv_data_mutex);
	if (clipboard->srv_data != NULL) {
		fmt = clipboard->format;
		if (fmt == CB_FORMAT_PNG || fmt == CF_DIB || fmt == CF_DIBV5 || fmt == CB_FORMAT_JPEG) {
			g_object_unref(clipboard->srv_data);
		} else {
			free(clipboard->srv_data);
		}
		clipboard->srv_data = NULL;
	}
	pthread_mutex_unlock(&clipboard->srv_data_mutex);
}


static UINT remmina_rdp_cliprdr_server_format_list(CliprdrClientContext *context, const CLIPRDR_FORMAT_LIST *formatList)
{
	TRACE_CALL(__func__);

	/* Called when a user do a "Copy" on the server: we collect all formats
	 * the server send us and then setup the local clipboard with the appropriate
	 * targets to request server data */

	RemminaPluginRdpUiObject *ui;
	RemminaProtocolWidget *gp;
	rfClipboard *clipboard;
	CLIPRDR_FORMAT *format;
	CLIPRDR_FORMAT_LIST_RESPONSE formatListResponse;
	UINT rc;

	int has_dib_level = 0;

	int i;

	clipboard = (rfClipboard *)context->custom;
	gp = clipboard->rfi->protocol_widget;

	REMMINA_PLUGIN_DEBUG("gp=%p: Received a new ServerFormatList from server clipboard. Remmina version = %s",
						 gp, VERSION);

	GtkTargetList *list = gtk_target_list_new(NULL, 0);

	if (clipboard->srv_clip_data_wait == SCDW_BUSY_WAIT) {
		REMMINA_PLUGIN_DEBUG("gp=%p: we already have a FormatDataRequest in progress to the server, aborting", gp);
		remmina_rdp_clipboard_abort_client_format_data_request(clipboard->rfi);
	}

	remmina_rdp_cliprdr_cached_clipboard_free(clipboard);
	clipboard->server_html_format_id = 0;

	REMMINA_PLUGIN_DEBUG("gp=%p: format list from the server:", gp);
	for (i = 0; i < formatList->numFormats; i++) {
		format = &formatList->formats[i];
		const char *serverFormatName = format->formatName;
		gchar *gtkFormatName = NULL;
		if (format->formatId == CF_UNICODETEXT) {
			serverFormatName = "CF_UNICODETEXT";
			gtkFormatName = "text/plain;charset=utf-8";
			GdkAtom atom = gdk_atom_intern(gtkFormatName, TRUE);
			gtk_target_list_add(list, atom, 0, CF_UNICODETEXT);
			/* Add also the older UTF8_STRING format for older applications */
			atom = gdk_atom_intern("UTF8_STRING", TRUE);
			gtk_target_list_add(list, atom, 0, CF_UNICODETEXT);
		} else if (format->formatId == CF_TEXT) {
			serverFormatName = "CF_TEXT";
			gtkFormatName = "text/plain";
			GdkAtom atom = gdk_atom_intern(gtkFormatName, TRUE);
			gtk_target_list_add(list, atom, 0, CF_TEXT);
			/* Add also the older TEXT format for older applications */
			atom = gdk_atom_intern("TEXT", TRUE);
			gtk_target_list_add(list, atom, 0, CF_TEXT);
		} else if (format->formatId == CF_DIB) {
			serverFormatName = "CF_DIB";
			if (has_dib_level < 1)
				has_dib_level = 1;
		} else if (format->formatId == CF_DIBV5) {
			serverFormatName = "CF_DIBV5";
			if (has_dib_level < 5)
				has_dib_level = 5;
		} else if (format->formatId == CB_FORMAT_JPEG) {
			serverFormatName = "CB_FORMAT_JPEG";
			gtkFormatName = "image/jpeg";
			GdkAtom atom = gdk_atom_intern(gtkFormatName, TRUE);
			gtk_target_list_add(list, atom, 0, CB_FORMAT_JPEG);
		} else if (format->formatId == CB_FORMAT_PNG) {
			serverFormatName = "CB_FORMAT_PNG";
			gtkFormatName = "image/png";
			GdkAtom atom = gdk_atom_intern(gtkFormatName, TRUE);
			gtk_target_list_add(list, atom, 0, CB_FORMAT_PNG);
		} else if (format->formatId == CB_FORMAT_HTML) {
			serverFormatName = "CB_FORMAT_HTML";
			gtkFormatName = "text/html";
			GdkAtom atom = gdk_atom_intern(gtkFormatName, TRUE);
			gtk_target_list_add(list, atom, 0, CB_FORMAT_HTML);
		} else if (format->formatId == CB_FORMAT_TEXTURILIST) {
			serverFormatName = "CB_FORMAT_TEXTURILIST";
			gtkFormatName = "text/uri-list";
			GdkAtom atom = gdk_atom_intern(gtkFormatName, TRUE);
			gtk_target_list_add(list, atom, 0, CB_FORMAT_TEXTURILIST);
		} else if (format->formatId == CF_LOCALE) {
			serverFormatName = "CF_LOCALE";
		} else if (format->formatId == CF_METAFILEPICT) {
			serverFormatName = "CF_METAFILEPICT";
		} else if (serverFormatName != NULL && strcmp(serverFormatName, "HTML Format") == 0) {
			gtkFormatName = "text/html";
			GdkAtom atom = gdk_atom_intern(gtkFormatName, TRUE);
			gtk_target_list_add(list, atom, 0, format->formatId);
			clipboard->server_html_format_id = format->formatId;
		}
		REMMINA_PLUGIN_DEBUG("the server has clipboard format %d: %s -> GTK %s", format->formatId, serverFormatName, gtkFormatName);
	}

	/* Keep only one DIB format, if present */
	if (has_dib_level) {
		GdkAtom atom = gdk_atom_intern("image/bmp", TRUE);
		if (has_dib_level == 5)
			gtk_target_list_add(list, atom, 0, CF_DIBV5);
		else
			gtk_target_list_add(list, atom, 0, CF_DIB);
	}


	REMMINA_PLUGIN_DEBUG("gp=%p: sending ClientFormatListResponse to server", gp);
#if FREERDP_VERSION_MAJOR >= 3
	formatListResponse.common.msgType = CB_FORMAT_LIST_RESPONSE;
	formatListResponse.common.msgFlags = CB_RESPONSE_OK; // Can be CB_RESPONSE_FAIL in case of error
	formatListResponse.common.dataLen = 0;
#else
	formatListResponse.msgType = CB_FORMAT_LIST_RESPONSE;
	formatListResponse.msgFlags = CB_RESPONSE_OK; // Can be CB_RESPONSE_FAIL in case of error
	formatListResponse.dataLen = 0;
#endif
	rc = clipboard->context->ClientFormatListResponse(clipboard->context, &formatListResponse);
	/* Schedule GTK event to tell GTK to change the local clipboard calling gtk_clipboard_set_with_owner
	 * via REMMINA_RDP_UI_CLIPBOARD_SET_DATA
	 * GTK will immediately fire an "owner-change" event, that we should ignore.
	 * And if we are putting text on the clipboard, mutter or other clipboard
	 * managers may try immediately request a clipboard transfer. */

	/* Ensure we have at least one target into list, or Gtk will not change owner
	  when setting clipboard owner later.
	  We put it directly in the clipboard cache (clipboard->srv_data),
	  so remmina will never ask it to the server via ClientFormatDataRequest */
	gint n_targets;
	GtkTargetEntry *target_table = gtk_target_table_new_from_list(list, &n_targets);
	if (target_table)
		gtk_target_table_free(target_table, n_targets);
	if (n_targets == 0) {
		REMMINA_PLUGIN_DEBUG("gp=%p adding a dummy text target (empty text) for local clipboard, because we have no interesting targets from the server. Putting it in the local clipboard cache.");
		GdkAtom atom = gdk_atom_intern("text/plain;charset=utf-8", TRUE);
		gtk_target_list_add(list, atom, 0, CF_UNICODETEXT);
		pthread_mutex_lock(&clipboard->srv_data_mutex);
		clipboard->srv_data = malloc(1);
		((char *)(clipboard->srv_data))[0] = 0;
		pthread_mutex_unlock(&clipboard->srv_data_mutex);
		clipboard->format = CF_UNICODETEXT;
	}


	ui = g_new0(RemminaPluginRdpUiObject, 1);
	ui->type = REMMINA_RDP_UI_CLIPBOARD;
	ui->clipboard.clipboard = clipboard;
	ui->clipboard.type = REMMINA_RDP_UI_CLIPBOARD_SET_DATA;
	ui->clipboard.targetlist = list;
	remmina_rdp_event_queue_ui_async(gp, ui);

	REMMINA_PLUGIN_DEBUG("gp=%p: processing of ServerFormatList ended, returning rc=%u to libfreerdp", gp, rc);
	return rc;
}

static UINT remmina_rdp_cliprdr_server_format_list_response(CliprdrClientContext *context, const CLIPRDR_FORMAT_LIST_RESPONSE *formatListResponse)
{
	TRACE_CALL(__func__);
	return CHANNEL_RC_OK;
}

static UINT remmina_rdp_cliprdr_server_format_data_request(CliprdrClientContext *context, const CLIPRDR_FORMAT_DATA_REQUEST *formatDataRequest)
{
	TRACE_CALL(__func__);

	RemminaPluginRdpUiObject *ui;
	RemminaProtocolWidget *gp;
	rfClipboard *clipboard;

	clipboard = (rfClipboard *)context->custom;
	gp = clipboard->rfi->protocol_widget;

	ui = g_new0(RemminaPluginRdpUiObject, 1);
	ui->type = REMMINA_RDP_UI_CLIPBOARD;
	ui->clipboard.clipboard = clipboard;
	ui->clipboard.type = REMMINA_RDP_UI_CLIPBOARD_GET_DATA;
	ui->clipboard.format = formatDataRequest->requestedFormatId;
	remmina_rdp_event_queue_ui_sync_retint(gp, ui);

	return CHANNEL_RC_OK;
}

int timeval_diff(struct timeval *start, struct timeval *end)
{
	/* Returns the time elapsed from start to end, in ms */
	int64_t ms_end = (end->tv_sec * 1000) + (end->tv_usec / 1000);
	int64_t ms_start = (start->tv_sec * 1000) + (start->tv_usec / 1000);

	return (int)(ms_end - ms_start);
}


static UINT remmina_rdp_cliprdr_server_format_data_response(CliprdrClientContext *context, const CLIPRDR_FORMAT_DATA_RESPONSE *formatDataResponse)
{
	TRACE_CALL(__func__);

	const UINT8 *data;
	size_t size;
	rfContext *rfi;
	RemminaProtocolWidget *gp;
	rfClipboard *clipboard;
	gpointer output = NULL;
	struct timeval now;
	int mstrans;

	clipboard = (rfClipboard *)context->custom;
	gp = clipboard->rfi->protocol_widget;
	rfi = GET_PLUGIN_DATA(gp);

	data = formatDataResponse->requestedFormatData;
#if FREERDP_VERSION_MAJOR >= 3
	size = formatDataResponse->common.dataLen;
#else
	size = formatDataResponse->dataLen;
#endif

	REMMINA_PLUGIN_DEBUG("gp=%p server FormatDataResponse received: clipboard data arrived form server.", gp);
	gettimeofday(&now, NULL);
	remmina_rdp_cliprdr_cached_clipboard_free(clipboard);

	/* Calculate stats */
	mstrans = timeval_diff(&(clipboard->clientformatdatarequest_tv), &now);
	REMMINA_PLUGIN_DEBUG("gp=%p %zu bytes transferred from server in %d ms. Speed is %d bytes/sec",
		gp, (size_t)size, mstrans, mstrans != 0 ? (int)((int64_t)size * 1000 / mstrans) : 0);

	if (size > 0) {
		switch (rfi->clipboard.format) {
		case CF_UNICODETEXT:
		{
			output =
			g_utf16_to_utf8((const WCHAR *)data, size / sizeof(WCHAR), NULL, NULL, NULL);
			if (output) {
				size = strlen(output) + 1;
				crlf2lf(output, &size);
			}
			break;
		}

		case CF_TEXT:
		case CB_FORMAT_HTML:
		{
			output = (gpointer)calloc(1, size + 1);
			if (output) {
				memcpy(output, data, size);
				crlf2lf(output, &size);
			}
			break;
		}

		case CF_DIBV5:
		case CF_DIB:
		{
			wStream *s;
			UINT32 offset;
			GError *perr;
			BITMAPINFOHEADER *pbi;
			BITMAPV5HEADER *pbi5;

			pbi = (BITMAPINFOHEADER *)data;

			// offset calculation inspired by http://downloads.poolelan.com/MSDN/MSDNLibrary6/Disk1/Samples/VC/OS/WindowsXP/GetImage/BitmapUtil.cpp
			offset = 14 + pbi->biSize;
			if (pbi->biClrUsed != 0)
				offset += sizeof(RGBQUAD) * pbi->biClrUsed;
			else if (pbi->biBitCount <= 8)
				offset += sizeof(RGBQUAD) * (1 << pbi->biBitCount);
			if (pbi->biSize == sizeof(BITMAPINFOHEADER)) {
				if (pbi->biCompression == 3)         // BI_BITFIELDS is 3
					offset += 12;
			} else if (pbi->biSize >= sizeof(BITMAPV5HEADER)) {
				pbi5 = (BITMAPV5HEADER *)pbi;
				if (pbi5->bV5ProfileData <= offset)
					offset += pbi5->bV5ProfileSize;
			}
			s = Stream_New(NULL, 14 + size);
			Stream_Write_UINT8(s, 'B');
			Stream_Write_UINT8(s, 'M');
			Stream_Write_UINT32(s, 14 + size);
			Stream_Write_UINT32(s, 0);
			Stream_Write_UINT32(s, offset);
			Stream_Write(s, data, size);

			data = Stream_Buffer(s);
			size = Stream_Length(s);

			GdkPixbufLoader *loader = gdk_pixbuf_loader_new();
			perr = NULL;
			if (!gdk_pixbuf_loader_write(loader, data, size, &perr)) {
				Stream_Free(s, TRUE);
				g_warning("[RDP] rdp_cliprdr: gdk_pixbuf_loader_write() returned error %s\n", perr->message);
			} else {
				if (!gdk_pixbuf_loader_close(loader, &perr)) {
					g_warning("[RDP] rdp_cliprdr: gdk_pixbuf_loader_close() returned error %s\n", perr->message);
					perr = NULL;
				}
				Stream_Free(s, TRUE);
				output = g_object_ref(gdk_pixbuf_loader_get_pixbuf(loader));
			}
			g_object_unref(loader);
			break;
		}

		case CB_FORMAT_PNG:
		case CB_FORMAT_JPEG:
		{
			GdkPixbufLoader *loader = gdk_pixbuf_loader_new();
			gdk_pixbuf_loader_write(loader, data, size, NULL);
			output = g_object_ref(gdk_pixbuf_loader_get_pixbuf(loader));
			gdk_pixbuf_loader_close(loader, NULL);
			g_object_unref(loader);
			break;
		}
		default:
		{
			if (rfi->clipboard.format == clipboard->server_html_format_id) {
				/* Converting from Microsoft HTML Clipboard Format to pure text/html
				 * https://docs.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format */
				int p = 0, lstart = 0;
				size_t osize;
				char c;
				/* Find the 1st starting with '<' */
				while(p < size && (c = data[p]) != 0) {
					if (c == '<' && p == lstart) break;
					if (c == '\n') lstart = p + 1;
					p++;
				}
				if (p < size) {
					osize = size - lstart;
					output = (gpointer)calloc(1, osize + 1);
					if (output) {
						memcpy(output, data + lstart, osize);
						((char *)output)[osize] = 0;
					}
				}
			}
		}
		}
	}

	pthread_mutex_lock(&clipboard->srv_data_mutex);
	clipboard->srv_data = output;
	pthread_mutex_unlock(&clipboard->srv_data_mutex);

	if (output != NULL)
		REMMINA_PLUGIN_DEBUG("gp=%p: clipboard local cache data has been loaded", gp);
	else
		REMMINA_PLUGIN_DEBUG("gp=%p: data from server is not valid (size=%zu format=%d), cannot load into cache", gp, size, rfi->clipboard.format);


	REMMINA_PLUGIN_DEBUG("gp=%p: signalling main GTK thread that we have some clipboard data.", gp);

	pthread_mutex_lock(&clipboard->transfer_clip_mutex);
	pthread_cond_signal(&clipboard->transfer_clip_cond);
	if (clipboard->srv_clip_data_wait == SCDW_BUSY_WAIT) {
		REMMINA_PLUGIN_DEBUG("gp=%p: clipboard transfer from server completed.", gp);
	} else {
		// Clipboard data arrived from server when we are not busy waiting on main loop
		// Unfortunately, we must discard it
		REMMINA_PLUGIN_DEBUG("gp=%p: clipboard transfer from server completed, but no local application is requesting it. Data is on local cache now, try to paste later.", gp);
	}
	clipboard->srv_clip_data_wait = SCDW_NONE;
	pthread_mutex_unlock(&clipboard->transfer_clip_mutex);

	return CHANNEL_RC_OK;
}


void remmina_rdp_cliprdr_request_data(GtkClipboard *gtkClipboard, GtkSelectionData *selection_data, guint info, RemminaProtocolWidget *gp)
{
	TRACE_CALL(__func__);

	/* Called by GTK when someone presses "Paste" on the client side.
	 * We ask to the server the data we need */

	CLIPRDR_FORMAT_DATA_REQUEST *pFormatDataRequest;
	rfClipboard *clipboard;
	rfContext *rfi = GET_PLUGIN_DATA(gp);
	RemminaPluginRdpEvent rdp_event = { 0 };
	struct timespec to;
	struct timeval tv;
	int rc;
	time_t tlimit, tlimit1s, tnow, tstart;

	REMMINA_PLUGIN_DEBUG("gp=%p: A local application has requested remote clipboard data for remote format id %d", gp, info);

	clipboard = &(rfi->clipboard);
	if (clipboard->srv_clip_data_wait != SCDW_NONE) {
		g_message("[RDP] Cannot paste now, I’m already transferring clipboard data from server. Try again later\n");
		return;
	}

	if (clipboard->format != info || clipboard->srv_data == NULL) {
		/* We do not have a local cached clipboard, so we have to start a remote request */
		remmina_rdp_cliprdr_cached_clipboard_free(clipboard);

		clipboard->format = info;

		pthread_mutex_lock(&clipboard->transfer_clip_mutex);

		pFormatDataRequest = (CLIPRDR_FORMAT_DATA_REQUEST *)malloc(sizeof(CLIPRDR_FORMAT_DATA_REQUEST));
		ZeroMemory(pFormatDataRequest, sizeof(CLIPRDR_FORMAT_DATA_REQUEST));
		pFormatDataRequest->requestedFormatId = clipboard->format;

		clipboard->srv_clip_data_wait = SCDW_BUSY_WAIT;	// Annotate that we are waiting for ServerFormatDataResponse

		REMMINA_PLUGIN_DEBUG("gp=%p Requesting clipboard data with format %d from the server via ServerFormatDataRequest", gp, clipboard->format);
		rdp_event.type = REMMINA_RDP_EVENT_TYPE_CLIPBOARD_SEND_CLIENT_FORMAT_DATA_REQUEST;
		rdp_event.clipboard_formatdatarequest.pFormatDataRequest = pFormatDataRequest;
		remmina_rdp_event_event_push(gp, &rdp_event);

		/* Busy wait clipboard data for CLIPBOARD_TRANSFER_WAIT_TIME seconds.
		*      In the meanwhile allow GTK event loop to proceed */

		tstart = time(NULL);
		tlimit = tstart + CLIPBOARD_TRANSFER_WAIT_TIME;
		rc = 100000;
		tlimit1s = tstart + 1;
		while ((tnow = time(NULL)) < tlimit && rc != 0 && clipboard->srv_clip_data_wait == SCDW_BUSY_WAIT) {

			if (tnow >= tlimit1s) {
				REMMINA_PLUGIN_DEBUG("gp=%p, clipboard data is still not here after %u seconds", gp, (unsigned)(tnow - tstart));
				tlimit1s = time(NULL) + 1;
			}

			gettimeofday(&tv, NULL);
			to.tv_sec = tv.tv_sec;
			to.tv_nsec = tv.tv_usec * 1000 + 5000000;      // wait for data for 5ms
			if (to.tv_nsec >= 1000000000) {
				to.tv_nsec -= 1000000000;
				to.tv_sec++;
			}
			rc = pthread_cond_timedwait(&clipboard->transfer_clip_cond, &clipboard->transfer_clip_mutex, &to);
			if (rc == 0)
				break;

			gtk_main_iteration_do(FALSE);
		}

		if (rc != 0) {
			/* Timeout, just log it and hope that data will arrive later */
			if (clipboard->srv_clip_data_wait == SCDW_ABORTING) {
				g_warning("[RDP] gp=%p Clipboard data wait aborted.",gp);
			} else {
				if (rc == ETIMEDOUT)
					g_warning("[RDP] gp=%p Clipboard data from the server is not available in %d seconds. No data will be available to user.",
						gp, CLIPBOARD_TRANSFER_WAIT_TIME);
				else
					g_warning("[RDP] gp=%p internal error: pthread_cond_timedwait() returned %d\n", gp, rc);
			}
		}
		pthread_mutex_unlock(&clipboard->transfer_clip_mutex);

	}

	pthread_mutex_lock(&clipboard->srv_data_mutex);
	if (clipboard->srv_data != NULL) {
		REMMINA_PLUGIN_DEBUG("gp=%p pasting data to local application", gp);
		/* We have data in cache, just paste it */
		if (info == CB_FORMAT_PNG || info == CF_DIB || info == CF_DIBV5 || info == CB_FORMAT_JPEG) {
			gtk_selection_data_set_pixbuf(selection_data, clipboard->srv_data);
		} else if (info == CB_FORMAT_HTML || info == clipboard->server_html_format_id) {
			REMMINA_PLUGIN_DEBUG("gp=%p returning %zu bytes of HTML in clipboard to requesting application", gp, strlen(clipboard->srv_data));
			GdkAtom atom = gdk_atom_intern("text/html", TRUE);
			gtk_selection_data_set(selection_data, atom, 8, clipboard->srv_data, strlen(clipboard->srv_data));
		} else {
			REMMINA_PLUGIN_DEBUG("gp=%p returning %zu bytes of text in clipboard to requesting application", gp, strlen(clipboard->srv_data));
			gtk_selection_data_set_text(selection_data, clipboard->srv_data, -1);
		}
		clipboard->srv_clip_data_wait = SCDW_NONE;
	} else {
		REMMINA_PLUGIN_DEBUG("gp=%p cannot paste data to local application because ->srv_data is NULL", gp);
	}
	pthread_mutex_unlock(&clipboard->srv_data_mutex);

}

void remmina_rdp_cliprdr_empty_clipboard(GtkClipboard *gtkClipboard, rfClipboard *clipboard)
{
	TRACE_CALL(__func__);
	/* No need to do anything here */
}

CLIPRDR_FORMAT_LIST *remmina_rdp_cliprdr_get_client_format_list(RemminaProtocolWidget *gp)
{
	TRACE_CALL(__func__);

	GtkClipboard *gtkClipboard;
	rfContext *rfi = GET_PLUGIN_DATA(gp);
	GdkAtom *targets;
	gboolean result = 0;
	gint loccount, srvcount;
	gint formatId, i;
	CLIPRDR_FORMAT *formats;
	struct retp_t {
		CLIPRDR_FORMAT_LIST	pFormatList;
		CLIPRDR_FORMAT		formats[];
	} *retp;

	formats = NULL;
	retp = NULL;
	loccount = 0;

	gtkClipboard = gtk_widget_get_clipboard(rfi->drawing_area, GDK_SELECTION_CLIPBOARD);
	if (gtkClipboard)
		result = gtk_clipboard_wait_for_targets(gtkClipboard, &targets, &loccount);
	REMMINA_PLUGIN_DEBUG("gp=%p sending to server the following local clipboard content formats", gp);
	if (result && loccount > 0) {
		formats = (CLIPRDR_FORMAT *)malloc(loccount * sizeof(CLIPRDR_FORMAT));
		srvcount = 0;
		for (i = 0; i < loccount; i++) {
			formatId = remmina_rdp_cliprdr_get_format_from_gdkatom(targets[i]);
			if (formatId != 0) {
				gchar *name = gdk_atom_name(targets[i]);
				REMMINA_PLUGIN_DEBUG("     local clipboard format %s will be sent to remote as %d", name, formatId);
				g_free(name);
				formats[srvcount].formatId = formatId;
				formats[srvcount].formatName = NULL;
				srvcount++;
			}
		}
		if (srvcount > 0) {
			retp = (struct retp_t *)malloc(sizeof(struct retp_t) + sizeof(CLIPRDR_FORMAT) * srvcount);
			retp->pFormatList.formats = retp->formats;
			retp->pFormatList.numFormats = srvcount;
			memcpy(retp->formats, formats, sizeof(CLIPRDR_FORMAT) * srvcount);
		} else {
			retp = (struct retp_t *)malloc(sizeof(struct retp_t));
			retp->pFormatList.formats = NULL;
			retp->pFormatList.numFormats = 0;
		}
		free(formats);
	} else {
		retp = (struct retp_t *)malloc(sizeof(struct retp_t) + sizeof(CLIPRDR_FORMAT));
		retp->pFormatList.formats = NULL;
		retp->pFormatList.numFormats = 0;
	}

	if (result)
		g_free(targets);

#if FREERDP_VERSION_MAJOR >= 3
	retp->pFormatList.common.msgType = CB_FORMAT_LIST;
	retp->pFormatList.common.msgFlags = 0;
#else
	retp->pFormatList.msgType = CB_FORMAT_LIST;
	retp->pFormatList.msgFlags = 0;
#endif

	return (CLIPRDR_FORMAT_LIST *)retp;
}

static void remmina_rdp_cliprdr_mt_get_format_list(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui)
{
	TRACE_CALL(__func__);
	ui->retptr = (void *)remmina_rdp_cliprdr_get_client_format_list(gp);
}

void remmina_rdp_cliprdr_get_clipboard_data(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui)
{
	TRACE_CALL(__func__);
	GtkClipboard *gtkClipboard;
	UINT8 *inbuf = NULL;
	UINT8 *outbuf = NULL;
#if FREERDP_VERSION_MAJOR >= 3
	WCHAR *outbuf_wchar = NULL;
#endif
	GdkPixbuf *image = NULL;
	size_t size = 0;
	rfContext *rfi = GET_PLUGIN_DATA(gp);
	RemminaPluginRdpEvent rdp_event = { 0 };

	gtkClipboard = gtk_widget_get_clipboard(rfi->drawing_area, GDK_SELECTION_CLIPBOARD);
	if (gtkClipboard) {
		switch (ui->clipboard.format) {
		case CF_TEXT:
		case CF_UNICODETEXT:
		case CB_FORMAT_HTML:
		{
			inbuf = (UINT8 *)gtk_clipboard_wait_for_text(gtkClipboard);
			break;
		}

		case CB_FORMAT_PNG:
		case CB_FORMAT_JPEG:
		case CF_DIB:
		case CF_DIBV5:
		{
			image = gtk_clipboard_wait_for_image(gtkClipboard);
			break;
		}
		}
	}

	/* No data received, send nothing */
	if (inbuf != NULL || image != NULL) {
		switch (ui->clipboard.format) {
			case CF_TEXT:
			case CB_FORMAT_HTML:
			{
				size = strlen((char *)inbuf);
				outbuf = lf2crlf(inbuf, (int *) &size);
				break;
			}
			case CF_UNICODETEXT:
			{
				size = strlen((const char *)inbuf);
				inbuf = lf2crlf(inbuf, (int *) &size);
#if FREERDP_VERSION_MAJOR >= 3
				size_t len = 0;
				outbuf_wchar = ConvertUtf8NToWCharAlloc((const char *)inbuf, (size_t)size, &len);
				size = (len + 1) * sizeof(WCHAR);
#else
				const int rc = (ConvertToUnicode(CP_UTF8, 0, (CHAR *)inbuf, -1, (WCHAR **)&outbuf, 0)) * sizeof(WCHAR);
				size = 0;
				if (rc >= 0) {
					size = (size_t)rc;
				}
#endif
				g_free(inbuf);
				break;
			}
			case CB_FORMAT_PNG:
			{
				gchar *data;
				gsize buffersize;
				gdk_pixbuf_save_to_buffer(image, &data, &buffersize, "png", NULL, NULL);
				outbuf = (UINT8 *)malloc(buffersize);
				memcpy(outbuf, data, buffersize);
				size = buffersize;
				g_object_unref(image);
				break;
			}
			case CB_FORMAT_JPEG:
			{
				gchar *data;
				gsize buffersize;
				gdk_pixbuf_save_to_buffer(image, &data, &buffersize, "jpeg", NULL, NULL);
				outbuf = (UINT8 *)malloc(buffersize);
				memcpy(outbuf, data, buffersize);
				size = buffersize;
				g_object_unref(image);
				break;
			}
			case CF_DIB:
			case CF_DIBV5:
			{
				gchar *data;
				gsize buffersize;
				gdk_pixbuf_save_to_buffer(image, &data, &buffersize, "bmp", NULL, NULL);
				size = buffersize - 14;
				outbuf = (UINT8 *)malloc(size);
				memcpy(outbuf, data + 14, size);
				g_object_unref(image);
				break;
			}
		}
	}

	rdp_event.type = REMMINA_RDP_EVENT_TYPE_CLIPBOARD_SEND_CLIENT_FORMAT_DATA_RESPONSE;
	rdp_event.clipboard_formatdataresponse.size = (int)MIN(size, INT32_MAX);

#if FREERDP_VERSION_MAJOR >= 3
	// For unicode, use the wchar buffer
	if (outbuf == NULL && outbuf_wchar != NULL) {
		rdp_event.clipboard_formatdataresponse.data = (unsigned char *)outbuf_wchar;
	}
	else {
		rdp_event.clipboard_formatdataresponse.data = (unsigned char *)outbuf;
	}
#else
	rdp_event.clipboard_formatdataresponse.data = (unsigned char *)outbuf;
#endif

	remmina_rdp_event_event_push(gp, &rdp_event);
}

void remmina_rdp_cliprdr_set_clipboard_data(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui)
{
	TRACE_CALL(__func__);
	GtkClipboard *gtkClipboard;
	gint n_targets;
	rfContext *rfi = GET_PLUGIN_DATA(gp);

	gtkClipboard = gtk_widget_get_clipboard(rfi->drawing_area, GDK_SELECTION_CLIPBOARD);
	if (gtkClipboard) {
		GtkTargetEntry *targets = gtk_target_table_new_from_list(ui->clipboard.targetlist, &n_targets);
		if (!targets) {
			/* If no targets exists, this is an internal error, because
			 * remmina_rdp_cliprdr_server_format_list() must have produced
			 * at least one target before calling REMMINA_RDP_UI_CLIPBOARD_SET_DATA */
			g_warning("[RDP] internal error: no targets to insert into the local clipboard");
		}

		REMMINA_PLUGIN_DEBUG("setting clipboard with owner to me: %p", gp);
		gtk_clipboard_set_with_owner(gtkClipboard, targets, n_targets,
							(GtkClipboardGetFunc)remmina_rdp_cliprdr_request_data,
							(GtkClipboardClearFunc)remmina_rdp_cliprdr_empty_clipboard, G_OBJECT(gp));
		gtk_target_table_free(targets, n_targets);
	}
}

void remmina_rdp_cliprdr_detach_owner(RemminaProtocolWidget *gp)
{
	/* When closing a rdp connection, we should check if gp is a clipboard owner.
	 * If it’s an owner, detach it from the clipboard */
	TRACE_CALL(__func__);
	rfContext *rfi = GET_PLUGIN_DATA(gp);
	GtkClipboard *gtkClipboard;

	if (!rfi || !rfi->drawing_area) return;

	gtkClipboard = gtk_widget_get_clipboard(rfi->drawing_area, GDK_SELECTION_CLIPBOARD);
	if (gtkClipboard && gtk_clipboard_get_owner(gtkClipboard) == (GObject *)gp)
		gtk_clipboard_clear(gtkClipboard);

}

void remmina_rdp_event_process_clipboard(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui)
{
	TRACE_CALL(__func__);

	switch (ui->clipboard.type) {
	case REMMINA_RDP_UI_CLIPBOARD_FORMATLIST:
		remmina_rdp_cliprdr_mt_get_format_list(gp, ui);
		break;

	case REMMINA_RDP_UI_CLIPBOARD_GET_DATA:
		remmina_rdp_cliprdr_get_clipboard_data(gp, ui);
		break;

	case REMMINA_RDP_UI_CLIPBOARD_SET_DATA:
		remmina_rdp_cliprdr_set_clipboard_data(gp, ui);
		break;

	}
}

void remmina_rdp_clipboard_init(rfContext *rfi)
{
	TRACE_CALL(__func__);
	// Future: initialize rfi->clipboard
}
void remmina_rdp_clipboard_free(rfContext *rfi)
{
	TRACE_CALL(__func__);

	remmina_rdp_cliprdr_cached_clipboard_free(&(rfi->clipboard));

}

void remmina_rdp_clipboard_abort_client_format_data_request(rfContext *rfi)
{
	TRACE_CALL(__func__);
	if (rfi && rfi->clipboard.srv_clip_data_wait == SCDW_BUSY_WAIT) {
		REMMINA_PLUGIN_DEBUG("requesting clipboard data transfer from server to be ignored and busywait loop to exit");
		/* Allow clipboard transfer from server to terminate */
		rfi->clipboard.srv_clip_data_wait = SCDW_ABORTING;
		usleep(100000);
	}
}

void remmina_rdp_cliprdr_init(rfContext *rfi, CliprdrClientContext *cliprdr)
{
	TRACE_CALL(__func__);

	rfClipboard *clipboard;
	clipboard = &(rfi->clipboard);

	rfi->clipboard.rfi = rfi;
	cliprdr->custom = (void *)clipboard;

	clipboard->context = cliprdr;
	pthread_mutex_init(&clipboard->transfer_clip_mutex, NULL);
	pthread_cond_init(&clipboard->transfer_clip_cond, NULL);
	clipboard->srv_clip_data_wait = SCDW_NONE;

	pthread_mutex_init(&clipboard->srv_data_mutex, NULL);

	cliprdr->MonitorReady = remmina_rdp_cliprdr_monitor_ready;
	cliprdr->ServerCapabilities = remmina_rdp_cliprdr_server_capabilities;
	cliprdr->ServerFormatList = remmina_rdp_cliprdr_server_format_list;
	cliprdr->ServerFormatListResponse = remmina_rdp_cliprdr_server_format_list_response;
	cliprdr->ServerFormatDataRequest = remmina_rdp_cliprdr_server_format_data_request;
	cliprdr->ServerFormatDataResponse = remmina_rdp_cliprdr_server_format_data_response;
}
