/**
 * FreeRDP: A Remote Desktop Protocol Implementation
 * SDL Client
 *
 * Copyright 2022 Armin Novak <armin.novak@thincast.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 <algorithm>
#include <cmath>

#include "sdl_context.hpp"
#include "sdl_config.hpp"
#include "sdl_channels.hpp"
#include "sdl_monitor.hpp"
#include "sdl_pointer.hpp"
#include "sdl_touch.hpp"

#include <sdl_common_utils.hpp>
#include <scoped_guard.hpp>

#include "dialogs/sdl_dialogs.hpp"

#if defined(WITH_WEBVIEW)
#include <aad/sdl_webview.hpp>
#endif

static constexpr auto sdl_allow_screensaver = "sdl-allow-screensaver";

SdlContext::SdlContext(rdpContext* context)
    : _context(context), _log(WLog_Get(CLIENT_TAG("SDL"))), _cursor(nullptr, sdl_Pointer_FreeCopy),
      _rdpThreadRunning(false), _primary(nullptr, SDL_DestroySurface), _disp(this), _input(this),
      _clip(this), _dialog(_log)
{
	WINPR_ASSERT(context);
	setMetadata();

	auto instance = _context->instance;
	WINPR_ASSERT(instance);

	instance->PreConnect = preConnect;
	instance->PostConnect = postConnect;
	instance->PostDisconnect = postDisconnect;
	instance->PostFinalDisconnect = postFinalDisconnect;
	instance->AuthenticateEx = sdl_authenticate_ex;
	instance->VerifyCertificateEx = sdl_verify_certificate_ex;
	instance->VerifyChangedCertificateEx = sdl_verify_changed_certificate_ex;
	instance->LogonErrorInfo = sdl_logon_error_info;
	instance->PresentGatewayMessage = sdl_present_gateway_message;
	instance->ChooseSmartcard = sdl_choose_smartcard;
	instance->RetryDialog = sdl_retry_dialog;

#ifdef WITH_WEBVIEW
	instance->GetAccessToken = sdl_webview_get_access_token;
#else
	instance->GetAccessToken = client_cli_get_access_token;
#endif
	/* TODO: Client display set up */

	_args.push_back({ sdl_allow_screensaver, COMMAND_LINE_VALUE_BOOL, nullptr, BoolValueFalse,
	                  nullptr, -1, nullptr, "Allow local screensaver to activate" });

	/* Push a null element used as abort when iterating the array */
	_args.push_back({ nullptr, 0, nullptr, nullptr, nullptr, -1, nullptr, nullptr });
}

void SdlContext::setHasCursor(bool val)
{
	this->_cursor_visible = val;
}

bool SdlContext::hasCursor() const
{
	return _cursor_visible;
}

void SdlContext::setMetadata()
{
	auto wmclass = freerdp_settings_get_string(_context->settings, FreeRDP_WmClass);
	if (!wmclass || (strlen(wmclass) == 0))
		wmclass = SDL_CLIENT_UUID;

	SDL_SetAppMetadataProperty(SDL_PROP_APP_METADATA_IDENTIFIER_STRING, wmclass);
	SDL_SetAppMetadataProperty(SDL_PROP_APP_METADATA_NAME_STRING, SDL_CLIENT_NAME);
	SDL_SetAppMetadataProperty(SDL_PROP_APP_METADATA_VERSION_STRING, SDL_CLIENT_VERSION);
	SDL_SetAppMetadataProperty(SDL_PROP_APP_METADATA_CREATOR_STRING, SDL_CLIENT_VENDOR);
	SDL_SetAppMetadataProperty(SDL_PROP_APP_METADATA_COPYRIGHT_STRING, SDL_CLIENT_COPYRIGHT);
	SDL_SetAppMetadataProperty(SDL_PROP_APP_METADATA_URL_STRING, SDL_CLIENT_URL);
	SDL_SetAppMetadataProperty(SDL_PROP_APP_METADATA_TYPE_STRING, SDL_CLIENT_TYPE);
}

int SdlContext::start()
{
	_thread = std::thread(rdpThreadRun, this);
	return 0;
}

int SdlContext::join()
{
	/* We do not want to use freerdp_abort_connect_context here.
	 * It would change the exit code and we do not want that. */
	HANDLE event = freerdp_abort_event(context());
	if (!SetEvent(event))
		return -1;

	_thread.join();
	return 0;
}

void SdlContext::cleanup()
{
	std::unique_lock lock(_critical);
	_windows.clear();
	_dialog.destroy();
	_primary.reset();
}

bool SdlContext::shallAbort(bool ignoreDialogs)
{
	std::unique_lock lock(_critical);
	if (freerdp_shall_disconnect_context(context()))
	{
		if (ignoreDialogs)
			return true;
		if (_rdpThreadRunning)
			return false;
		return !getDialog().isRunning();
	}
	return false;
}

/* Called before a connection is established.
 * Set all configuration options to support and load channels here. */
BOOL SdlContext::preConnect(freerdp* instance)
{
	WINPR_ASSERT(instance);
	WINPR_ASSERT(instance->context);

	auto sdl = get_context(instance->context);

	auto settings = instance->context->settings;
	WINPR_ASSERT(settings);

	if (!freerdp_settings_set_bool(settings, FreeRDP_CertificateCallbackPreferPEM, TRUE))
		return FALSE;

	/* Optional OS identifier sent to server */
	if (!freerdp_settings_set_uint32(settings, FreeRDP_OsMajorType, OSMAJORTYPE_UNIX))
		return FALSE;
	if (!freerdp_settings_set_uint32(settings, FreeRDP_OsMinorType, OSMINORTYPE_NATIVE_SDL))
		return FALSE;
	/* OrderSupport is initialized at this point.
	 * Only override it if you plan to implement custom order
	 * callbacks or deactivate certain features. */
	/* Register the channel listeners.
	 * They are required to set up / tear down channels if they are loaded. */
	if (PubSub_SubscribeChannelConnected(instance->context->pubSub,
	                                     sdl_OnChannelConnectedEventHandler) < 0)
		return FALSE;
	if (PubSub_SubscribeChannelDisconnected(instance->context->pubSub,
	                                        sdl_OnChannelDisconnectedEventHandler) < 0)
		return FALSE;
	if (PubSub_SubscribeUserNotification(instance->context->pubSub,
	                                     sdl_OnUserNotificationEventHandler) < 0)
		return FALSE;

	if (!freerdp_settings_get_bool(settings, FreeRDP_AuthenticationOnly))
	{
		UINT32 maxWidth = 0;
		UINT32 maxHeight = 0;

		if (!sdl_detect_monitors(sdl, &maxWidth, &maxHeight))
			return FALSE;

		if ((maxWidth != 0) && (maxHeight != 0) &&
		    !freerdp_settings_get_bool(settings, FreeRDP_SmartSizing))
		{
			WLog_Print(sdl->getWLog(), WLOG_INFO, "Update size to %ux%u", maxWidth, maxHeight);
			if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopWidth, maxWidth))
				return FALSE;
			if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, maxHeight))
				return FALSE;
		}

		/**
		 * If /f is specified in combination with /smart-sizing:widthxheight then
		 * we run the session in the /smart-sizing dimensions scaled to full screen
		 */

		const uint32_t sw = freerdp_settings_get_uint32(settings, FreeRDP_SmartSizingWidth);
		const uint32_t sh = freerdp_settings_get_uint32(settings, FreeRDP_SmartSizingHeight);
		const BOOL sm = freerdp_settings_get_bool(settings, FreeRDP_SmartSizing);
		if (sm && (sw > 0) && (sh > 0))
		{
			const BOOL mm = freerdp_settings_get_bool(settings, FreeRDP_UseMultimon);
			if (mm)
				WLog_Print(sdl->getWLog(), WLOG_WARN,
				           "/smart-sizing and /multimon are currently not supported, ignoring "
				           "/smart-sizing!");
			else
			{
				sdl->_windowWidth = freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth);
				sdl->_windowHeigth = freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight);

				if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopWidth, sw))
					return FALSE;
				if (!freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, sh))
					return FALSE;
			}
		}
	}
	else
	{
		/* Check +auth-only has a username and password. */
		if (!freerdp_settings_get_string(settings, FreeRDP_Password))
		{
			WLog_Print(sdl->getWLog(), WLOG_INFO,
			           "auth-only, but no password set. Please provide one.");
			return FALSE;
		}

		if (!freerdp_settings_set_bool(settings, FreeRDP_DeactivateClientDecoding, TRUE))
			return FALSE;

		WLog_Print(sdl->getWLog(), WLOG_INFO, "Authentication only. Don't connect SDL.");
	}

	if (!sdl->getInputChannelContext().initialize())
		return FALSE;

	sdl->_credentialsRead = false;
	/* TODO: Any code your client requires */
	return TRUE;
}

/* Called after a RDP connection was successfully established.
 * Settings might have changed during negotiation of client / server feature
 * support.
 *
 * Set up local framebuffers and paing callbacks.
 * If required, register pointer callbacks to change the local mouse cursor
 * when hovering over the RDP window
 */
BOOL SdlContext::postConnect(freerdp* instance)
{
	WINPR_ASSERT(instance);

	auto context = instance->context;
	WINPR_ASSERT(context);

	auto sdl = get_context(context);

	// Retry was successful, discard dialog
	sdl->getDialog().show(false);

	if (freerdp_settings_get_bool(context->settings, FreeRDP_AuthenticationOnly))
	{
		/* Check +auth-only has a username and password. */
		if (!freerdp_settings_get_string(context->settings, FreeRDP_Password))
		{
			WLog_Print(sdl->getWLog(), WLOG_INFO,
			           "auth-only, but no password set. Please provide one.");
			return FALSE;
		}

		WLog_Print(sdl->getWLog(), WLOG_INFO, "Authentication only. Don't connect to X.");
		return TRUE;
	}

	if (!sdl->waitForWindowsCreated())
		return FALSE;

	sdl->_sdlPixelFormat = SDL_PIXELFORMAT_BGRA32;
	if (!gdi_init(instance, PIXEL_FORMAT_BGRA32))
		return FALSE;

	if (!sdl->createPrimary())
		return FALSE;

	if (!sdl_register_pointer(instance->context->graphics))
		return FALSE;

	WINPR_ASSERT(context->update);

	context->update->BeginPaint = beginPaint;
	context->update->EndPaint = endPaint;
	context->update->PlaySound = playSound;
	context->update->DesktopResize = desktopResize;
	context->update->SetKeyboardIndicators = sdlInput::keyboard_set_indicators;
	context->update->SetKeyboardImeStatus = sdlInput::keyboard_set_ime_status;

	if (!sdl->setResizeable(false))
		return FALSE;
	if (!sdl->setFullscreen(freerdp_settings_get_bool(context->settings, FreeRDP_Fullscreen) ||
	                            freerdp_settings_get_bool(context->settings, FreeRDP_UseMultimon),
	                        true))
		return FALSE;
	sdl->setConnected(true);
	return TRUE;
}

/* This function is called whether a session ends by failure or success.
 * Clean up everything allocated by pre_connect and post_connect.
 */
void SdlContext::postDisconnect(freerdp* instance)
{
	if (!instance)
		return;

	if (!instance->context)
		return;

	auto sdl = get_context(instance->context);
	sdl->setConnected(false);

	gdi_free(instance);
}

void SdlContext::postFinalDisconnect(freerdp* instance)
{
	if (!instance)
		return;

	if (!instance->context)
		return;

	PubSub_UnsubscribeChannelConnected(instance->context->pubSub,
	                                   sdl_OnChannelConnectedEventHandler);
	PubSub_UnsubscribeChannelDisconnected(instance->context->pubSub,
	                                      sdl_OnChannelDisconnectedEventHandler);
	PubSub_UnsubscribeUserNotification(instance->context->pubSub,
	                                   sdl_OnUserNotificationEventHandler);
}

/* Create a SDL surface from the GDI buffer */
bool SdlContext::createPrimary()
{
	auto gdi = context()->gdi;
	WINPR_ASSERT(gdi);

	_primary = SDLSurfacePtr(
	    SDL_CreateSurfaceFrom(static_cast<int>(gdi->width), static_cast<int>(gdi->height),
	                          pixelFormat(), gdi->primary_buffer, static_cast<int>(gdi->stride)),
	    SDL_DestroySurface);
	if (!_primary)
		return false;

	SDL_SetSurfaceBlendMode(_primary.get(), SDL_BLENDMODE_NONE);
	SDL_Rect surfaceRect = { 0, 0, gdi->width, gdi->height };
	SDL_FillSurfaceRect(_primary.get(), &surfaceRect,
	                    SDL_MapSurfaceRGBA(_primary.get(), 0, 0, 0, 0xff));

	return true;
}

bool SdlContext::createWindows()
{
	auto settings = context()->settings;
	const auto& title = windowTitle();

	ScopeGuard guard1([&]() { _windowsCreatedEvent.set(); });

	UINT32 windowCount = freerdp_settings_get_uint32(settings, FreeRDP_MonitorCount);

	Sint32 originX = 0;
	Sint32 originY = 0;
	for (UINT32 x = 0; x < windowCount; x++)
	{
		auto id = monitorId(x);
		if (id < 0)
			return false;

		auto monitor = static_cast<rdpMonitor*>(
		    freerdp_settings_get_pointer_array_writable(settings, FreeRDP_MonitorDefArray, x));

		originX = std::min<Sint32>(monitor->x, originX);
		originY = std::min<Sint32>(monitor->y, originY);
	}

	for (UINT32 x = 0; x < windowCount; x++)
	{
		auto id = monitorId(x);
		if (id < 0)
			return false;

		auto monitor = static_cast<rdpMonitor*>(
		    freerdp_settings_get_pointer_array_writable(settings, FreeRDP_MonitorDefArray, x));

		Uint32 w = WINPR_ASSERTING_INT_CAST(Uint32, monitor->width);
		Uint32 h = WINPR_ASSERTING_INT_CAST(Uint32, monitor->height);
		if (!(freerdp_settings_get_bool(settings, FreeRDP_UseMultimon) ||
		      freerdp_settings_get_bool(settings, FreeRDP_Fullscreen)))
		{
			if (_windowWidth > 0)
				w = _windowWidth;
			else
				w = freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth);

			if (_windowHeigth > 0)
				h = _windowHeigth;
			else
				h = freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight);
		}

		Uint32 flags = SDL_WINDOW_HIGH_PIXEL_DENSITY;

		if (freerdp_settings_get_bool(settings, FreeRDP_Fullscreen) &&
		    !freerdp_settings_get_bool(settings, FreeRDP_UseMultimon))
		{
			flags |= SDL_WINDOW_FULLSCREEN;
		}

		if (freerdp_settings_get_bool(settings, FreeRDP_UseMultimon))
		{
			flags |= SDL_WINDOW_BORDERLESS;
		}

		if (!freerdp_settings_get_bool(settings, FreeRDP_Decorations))
			flags |= SDL_WINDOW_BORDERLESS;

		auto did = WINPR_ASSERTING_INT_CAST(SDL_DisplayID, id);
		auto window = SdlWindow::create(did, title, flags, w, h);

		if (freerdp_settings_get_bool(settings, FreeRDP_UseMultimon))
		{
			window.setOffsetX(originX - monitor->x);
			window.setOffsetY(originY - monitor->y);
		}

		_windows.insert({ window.id(), std::move(window) });
	}

	return true;
}

bool SdlContext::updateWindowList()
{
	std::vector<rdpMonitor> list;
	list.reserve(_windows.size());
	for (const auto& win : _windows)
		list.push_back(win.second.monitor(_windows.size() == 1));

	// /monitors: subset may exclude the SDL primary. The library requires
	// the array to mark one monitor as primary, so promote the first when
	// none of the kept windows cover the original primary.
	if (!list.empty() &&
	    std::none_of(list.cbegin(), list.cend(), [](const rdpMonitor& m) { return m.is_primary; }))
		list.at(0).is_primary = true;

	return freerdp_settings_set_monitor_def_array_sorted(context()->settings, list.data(),
	                                                     list.size());
}

bool SdlContext::updateWindow(SDL_WindowID id)
{
	if (freerdp_settings_get_bool(_context->settings, FreeRDP_Fullscreen) ||
	    freerdp_settings_get_bool(_context->settings, FreeRDP_UseMultimon))
		return true;

	auto& w = _windows.at(id);
	auto m = w.monitor(true);
	auto r = w.rect();
	m.width = r.w;
	m.height = r.h;
	m.attributes.physicalWidth = static_cast<UINT32>(r.w);
	m.attributes.physicalHeight = static_cast<UINT32>(r.h);
	w.setMonitor(m);
	return true;
}

std::string SdlContext::windowTitle() const
{
	const char* prefix = "FreeRDP:";

	const auto windowTitle = freerdp_settings_get_string(context()->settings, FreeRDP_WindowTitle);
	if (windowTitle)
		return windowTitle;

	const auto name = freerdp_settings_get_server_name(context()->settings);
	const auto port = freerdp_settings_get_uint32(context()->settings, FreeRDP_ServerPort);
	const auto addPort = (port != 3389);

	std::stringstream ss;
	ss << prefix << " " << name;

	if (addPort)
		ss << ":" << port;

	return ss.str();
}

bool SdlContext::waitForWindowsCreated()
{
	{
		std::unique_lock<CriticalSection> lock(_critical);
		_windowsCreatedEvent.clear();
		if (!sdl_push_user_event(SDL_EVENT_USER_CREATE_WINDOWS, this))
			return false;
	}

	HANDLE handles[] = { _windowsCreatedEvent.handle(), freerdp_abort_event(context()) };

	const DWORD rc = WaitForMultipleObjects(ARRAYSIZE(handles), handles, FALSE, INFINITE);
	switch (rc)
	{
		case WAIT_OBJECT_0:
			return true;
		default:
			return false;
	}
}

/* This function is called when the library completed composing a new
 * frame. Read out the changed areas and blit them to your output device.
 * The image buffer will have the format specified by gdi_init
 */
BOOL SdlContext::endPaint(rdpContext* context)
{
	auto sdl = get_context(context);
	WINPR_ASSERT(sdl);

	auto gdi = context->gdi;
	WINPR_ASSERT(gdi);
	WINPR_ASSERT(gdi->primary);

	HGDI_DC hdc = gdi->primary->hdc;
	WINPR_ASSERT(hdc);
	if (!hdc->hwnd)
		return TRUE;

	HGDI_WND hwnd = hdc->hwnd;
	WINPR_ASSERT(hwnd->invalid || (hwnd->ninvalid == 0));

	if (hwnd->invalid->null)
		return TRUE;

	WINPR_ASSERT(hwnd->invalid);
	if (gdi->suppressOutput || hwnd->invalid->null)
		return TRUE;

	const INT32 ninvalid = hwnd->ninvalid;
	const GDI_RGN* cinvalid = hwnd->cinvalid;

	if (ninvalid < 1)
		return TRUE;

	std::vector<SDL_Rect> rects;
	for (INT32 x = 0; x < ninvalid; x++)
	{
		auto& rgn = cinvalid[x];
		rects.push_back({ rgn.x, rgn.y, rgn.w, rgn.h });
	}

	sdl->push(std::move(rects));
	return sdl_push_user_event(SDL_EVENT_USER_UPDATE);
}

void SdlContext::sdl_client_cleanup(int exit_code, const std::string& error_msg)
{
	rdpSettings* settings = context()->settings;
	WINPR_ASSERT(settings);

	_rdpThreadRunning = false;
	bool showError = false;
	if (freerdp_settings_get_bool(settings, FreeRDP_AuthenticationOnly))
		WLog_Print(getWLog(), WLOG_INFO, "Authentication only, exit status %s [%" PRId32 "]",
		           sdl::error::exitCodeToTag(exit_code), exit_code);
	else
	{
		switch (exit_code)
		{
			case sdl::error::SUCCESS:
			case sdl::error::DISCONNECT:
			case sdl::error::LOGOFF:
			case sdl::error::DISCONNECT_BY_USER:
			case sdl::error::CONNECT_CANCELLED:
				break;
			default:
			{
				getDialog().showError(error_msg);
			}
			break;
		}
	}

	if (!showError)
		getDialog().show(false);

	_exitCode = exit_code;
	std::ignore = sdl_push_user_event(SDL_EVENT_USER_QUIT);
	SDL_CleanupTLS();
}

int SdlContext::sdl_client_thread_connect(std::string& error_msg)
{
	auto instance = context()->instance;
	WINPR_ASSERT(instance);

	_rdpThreadRunning = true;
	BOOL rc = freerdp_connect(instance);

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

	int exit_code = sdl::error::SUCCESS;
	if (!rc)
	{
		UINT32 error = freerdp_get_last_error(context());
		exit_code = sdl::error::errorToExitCode(error);
	}

	if (freerdp_settings_get_bool(settings, FreeRDP_AuthenticationOnly))
	{
		DWORD code = freerdp_get_last_error(context());
		freerdp_abort_connect_context(context());
		WLog_Print(getWLog(), WLOG_ERROR, "Authentication only, %s [0x%08" PRIx32 "] %s",
		           freerdp_get_last_error_name(code), code, freerdp_get_last_error_string(code));
		return exit_code;
	}

	if (!rc)
	{
		DWORD code = freerdp_error_info(instance);
		if (exit_code == sdl::error::SUCCESS)
		{
			char* msg = nullptr;
			size_t len = 0;
			exit_code = error_info_to_error(&code, &msg, &len);
			if (msg)
				error_msg = msg;
			free(msg);
		}

		auto last = freerdp_get_last_error(context());
		if (error_msg.empty())
		{
			char* msg = nullptr;
			size_t len = 0;
			winpr_asprintf(&msg, &len, "%s [0x%08" PRIx32 "]\n%s",
			               freerdp_get_last_error_name(last), last,
			               freerdp_get_last_error_string(last));
			if (msg)
				error_msg = msg;
			free(msg);
		}

		if (exit_code == sdl::error::SUCCESS)
		{
			if (last == FREERDP_ERROR_AUTHENTICATION_FAILED)
				exit_code = sdl::error::AUTH_FAILURE;
			else if (code == ERRINFO_SUCCESS)
				exit_code = sdl::error::CONN_FAILED;
		}

		getDialog().show(false);
	}

	return exit_code;
}

int SdlContext::sdl_client_thread_run(std::string& error_msg)
{
	auto instance = context()->instance;
	WINPR_ASSERT(instance);

	int exit_code = sdl::error::SUCCESS;
	while (!freerdp_shall_disconnect_context(context()))
	{
		HANDLE handles[MAXIMUM_WAIT_OBJECTS] = {};
		/*
		 * win8 and server 2k12 seem to have some timing issue/race condition
		 * when a initial sync request is send to sync the keyboard indicators
		 * sending the sync event twice fixed this problem
		 */
		if (freerdp_focus_required(instance))
		{
			auto ctx = get_context(context());
			WINPR_ASSERT(ctx);

			auto& input = ctx->getInputChannelContext();
			if (!input.keyboard_focus_in())
				break;
			if (!input.keyboard_focus_in())
				break;
		}

		const DWORD nCount = freerdp_get_event_handles(context(), handles, ARRAYSIZE(handles));

		if (nCount == 0)
		{
			WLog_Print(getWLog(), WLOG_ERROR, "freerdp_get_event_handles failed");
			break;
		}

		const DWORD status = WaitForMultipleObjects(nCount, handles, FALSE, INFINITE);

		if (status == WAIT_FAILED)
		{
			WLog_Print(getWLog(), WLOG_ERROR, "WaitForMultipleObjects WAIT_FAILED");
			break;
		}

		if (!freerdp_check_event_handles(context()))
		{
			if (client_auto_reconnect(instance))
			{
				// Retry was successful, discard dialog
				getDialog().show(false);
				continue;
			}
			else
			{
				/*
				 * Indicate an unsuccessful connection attempt if reconnect
				 * did not succeed and no other error was specified.
				 */
				if (freerdp_error_info(instance) == 0)
					exit_code = sdl::error::CONN_FAILED;
			}

			if (freerdp_get_last_error(context()) == FREERDP_ERROR_SUCCESS)
				WLog_Print(getWLog(), WLOG_ERROR, "WaitForMultipleObjects failed with %" PRIu32 "",
				           status);
			if (freerdp_get_last_error(context()) == FREERDP_ERROR_SUCCESS)
				WLog_Print(getWLog(), WLOG_ERROR, "Failed to check FreeRDP event handles");
			break;
		}
	}

	if (exit_code == sdl::error::SUCCESS)
	{
		DWORD code = 0;
		{
			char* emsg = nullptr;
			size_t elen = 0;
			exit_code = error_info_to_error(&code, &emsg, &elen);
			if (emsg)
				error_msg = emsg;
			free(emsg);
		}

		if ((code == ERRINFO_LOGOFF_BY_USER) &&
		    (freerdp_get_disconnect_ultimatum(context()) == Disconnect_Ultimatum_user_requested))
		{
			const char* msg = "Error info says user did not initiate but disconnect ultimatum says "
			                  "they did; treat this as a user logoff";

			char* emsg = nullptr;
			size_t elen = 0;
			winpr_asprintf(&emsg, &elen, "%s", msg);
			if (emsg)
				error_msg = emsg;
			free(emsg);

			/* This situation might be limited to Windows XP. */
			WLog_Print(getWLog(), WLOG_INFO, "%s", msg);
			exit_code = sdl::error::LOGOFF;
		}
	}

	freerdp_disconnect(instance);

	return exit_code;
}

/* RDP main loop.
 * Connects RDP, loops while running and handles event and dispatch, cleans up
 * after the connection ends. */
DWORD SdlContext::rdpThreadRun(SdlContext* sdl)
{
	WINPR_ASSERT(sdl);

	std::string error_msg;
	int exit_code = sdl->sdl_client_thread_connect(error_msg);
	if (exit_code == sdl::error::SUCCESS)
		exit_code = sdl->sdl_client_thread_run(error_msg);
	sdl->sdl_client_cleanup(exit_code, error_msg);

	return static_cast<DWORD>(exit_code);
}

int SdlContext::error_info_to_error(DWORD* pcode, char** msg, size_t* len) const
{
	const DWORD code = freerdp_error_info(context()->instance);
	const char* name = freerdp_get_error_info_name(code);
	const char* str = freerdp_get_error_info_string(code);
	const int exit_code = sdl::error::errorToExitCode(code);

	winpr_asprintf(msg, len, "Terminate with %s due to ERROR_INFO %s [0x%08" PRIx32 "]: %s",
	               sdl::error::errorToExitCodeTag(code), name, code, str);
	SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "%s", *msg);
	if (pcode)
		*pcode = code;
	return exit_code;
}

void SdlContext::applyMonitorOffset(SDL_WindowID window, float& x, float& y) const
{
	if (!freerdp_settings_get_bool(context()->settings, FreeRDP_UseMultimon))
		return;

	auto w = getWindowForId(window);
	x -= static_cast<float>(w->offsetX());
	y -= static_cast<float>(w->offsetY());
}

static bool alignX(const SDL_Rect& a, const SDL_Rect& b)
{
	if (a.x + a.w == b.x)
		return true;
	if (b.x + b.w == a.x)
		return true;
	return false;
}

static bool alignY(const SDL_Rect& a, const SDL_Rect& b)
{
	if (a.y + a.h == b.y)
		return true;
	if (b.y + b.h == a.y)
		return true;
	return false;
}

std::vector<SDL_DisplayID>
SdlContext::updateDisplayOffsetsForNeighbours(SDL_DisplayID id,
                                              const std::vector<SDL_DisplayID>& ignore)
{
	auto first = _offsets.at(id);
	std::vector<SDL_DisplayID> neighbours;

	for (auto& entry : _offsets)
	{
		if (entry.first == id)
			continue;
		if (std::find(ignore.begin(), ignore.end(), entry.first) != ignore.end())
			continue;

		bool neighbor = false;
		if (alignX(entry.second.first, first.first))
		{
			if (entry.second.first.x < first.first.x)
				entry.second.second.x = first.second.x - entry.second.second.w;
			else
				entry.second.second.x = first.second.x + first.second.w;
			neighbor = true;
		}
		if (alignY(entry.second.first, first.first))
		{
			if (entry.second.first.y < first.first.y)
				entry.second.second.y = first.second.y - entry.second.second.h;
			else
				entry.second.second.y = first.second.y + first.second.h;
			neighbor = true;
		}

		if (neighbor)
			neighbours.push_back(entry.first);
	}
	return neighbours;
}

void SdlContext::updateMonitorDataFromOffsets()
{
	for (auto& entry : _displays)
	{
		auto offsets = _offsets.at(entry.first);
		entry.second.x = offsets.second.x;
		entry.second.y = offsets.second.y;
	}

	for (auto& entry : _windows)
	{
		const auto& monitor = _displays.at(entry.first);
		entry.second.setMonitor(monitor);
	}
}

bool SdlContext::drawToWindow(SdlWindow& window, const std::vector<SDL_Rect>& rects)
{
	if (!isConnected())
		return true;

	auto gdi = context()->gdi;
	WINPR_ASSERT(gdi);

	auto size = window.rect();

	std::unique_lock lock(_critical);
	auto surface = _primary.get();

	if (useLocalScale())
	{
		window.setOffsetX(0);
		window.setOffsetY(0);
		if (gdi->width < size.w)
		{
			window.setOffsetX((size.w - gdi->width) / 2);
		}
		if (gdi->height < size.h)
		{
			window.setOffsetY((size.h - gdi->height) / 2);
		}

		_localScale = { static_cast<float>(size.w) / static_cast<float>(gdi->width),
			            static_cast<float>(size.h) / static_cast<float>(gdi->height) };
		if (!window.drawScaledRects(surface, _localScale, rects))
			return false;
	}
	else
	{
		SDL_Point offset{ 0, 0 };
		if (freerdp_settings_get_bool(context()->settings, FreeRDP_UseMultimon))
			offset = { window.offsetX(), window.offsetY() };
		if (!window.drawRects(surface, offset, rects))
			return false;
	}

	window.updateSurface();
	return true;
}

bool SdlContext::minimizeAllWindows()
{
	for (auto& w : _windows)
		w.second.minimize();
	return true;
}

int SdlContext::exitCode() const
{
	return _exitCode;
}

SDL_PixelFormat SdlContext::pixelFormat() const
{
	return _sdlPixelFormat;
}

bool SdlContext::addDisplayWindow(SDL_DisplayID id)
{
	const auto flags =
	    SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_FULLSCREEN | SDL_WINDOW_BORDERLESS;
	auto title = sdl::utils::windowTitle(context()->settings);
	auto w = SdlWindow::create(id, title, flags);
	_windows.emplace(w.id(), std::move(w));
	return true;
}

bool SdlContext::removeDisplayWindow(SDL_DisplayID id)
{
	for (auto& w : _windows)
	{
		if (w.second.displayIndex() == id)
			_windows.erase(w.first);
	}
	return true;
}

bool SdlContext::detectDisplays()
{
	int count = 0;
	auto display = SDL_GetDisplays(&count);
	if (!display)
		return false;
	for (int x = 0; x < count; x++)
	{
		const auto id = display[x];
		addOrUpdateDisplay(id);
	}
	SDL_free(display);
	return true;
}

rdpMonitor SdlContext::getDisplay(SDL_DisplayID id) const
{
	return _displays.at(id);
}

std::vector<SDL_DisplayID> SdlContext::getDisplayIds() const
{
	std::vector<SDL_DisplayID> keys;
	keys.reserve(_displays.size());
	for (const auto& entry : _displays)
	{
		keys.push_back(entry.first);
	}
	return keys;
}

const SdlWindow* SdlContext::getWindowForId(SDL_WindowID id) const
{
	auto it = _windows.find(id);
	if (it == _windows.end())
		return nullptr;
	return &it->second;
}

SdlWindow* SdlContext::getWindowForId(SDL_WindowID id)
{
	auto it = _windows.find(id);
	if (it == _windows.end())
		return nullptr;
	return &it->second;
}

SdlWindow* SdlContext::getFirstWindow()
{
	if (_windows.empty())
		return nullptr;
	return &_windows.begin()->second;
}

sdlDispContext& SdlContext::getDisplayChannelContext()
{
	return _disp;
}

sdlInput& SdlContext::getInputChannelContext()
{
	return _input;
}

sdlClip& SdlContext::getClipboardChannelContext()
{
	return _clip;
}

SdlConnectionDialogWrapper& SdlContext::getDialog()
{
	return _dialog;
}

wLog* SdlContext::getWLog()
{
	return _log;
}

bool SdlContext::moveMouseTo(const SDL_FPoint& pos)
{
	auto window = SDL_GetMouseFocus();
	if (!window)
		return true;

	const auto id = SDL_GetWindowID(window);
	const auto spos = pixelToScreen(id, pos);
	SDL_WarpMouseInWindow(window, spos.x, spos.y);
	return true;
}

bool SdlContext::handleEvent(const SDL_MouseMotionEvent& ev)
{
	if (!getWindowForId(ev.windowID))
		return true; /* Event for an untracked window (e.g. closed dialog) */
	SDL_Event copy{};
	copy.motion = ev;
	if (!eventToPixelCoordinates(ev.windowID, copy))
		return true;
	removeLocalScaling(copy.motion.x, copy.motion.y);
	removeLocalScaling(copy.motion.xrel, copy.motion.yrel);
	applyMonitorOffset(copy.motion.windowID, copy.motion.x, copy.motion.y);

	return SdlTouch::handleEvent(this, copy.motion);
}

bool SdlContext::handleEvent(const SDL_MouseWheelEvent& ev)
{
	if (!getWindowForId(ev.windowID))
		return true;
	SDL_Event copy{};
	copy.wheel = ev;
	if (!eventToPixelCoordinates(ev.windowID, copy))
		return true;
	removeLocalScaling(copy.wheel.mouse_x, copy.wheel.mouse_y);
	return SdlTouch::handleEvent(this, copy.wheel);
}

bool SdlContext::handleEvent(const SDL_WindowEvent& ev)
{
	if (!getDisplayChannelContext().handleEvent(ev))
		return false;

	auto window = getWindowForId(ev.windowID);
	if (!window)
		return true;

	{
		const auto& r = window->rect();
		const auto& b = window->bounds();
		const auto& scale = window->scale();
		const auto& orientation = window->orientation();
		SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION,
		             "%s: [%u] %dx%d-%dx%d {%dx%d-%dx%d}{scale=%f,orientation=%s}",
		             sdl::utils::toString(ev.type).c_str(), ev.windowID, r.x, r.y, r.w, r.h, b.x,
		             b.y, b.w, b.h, static_cast<double>(scale),
		             sdl::utils::toString(orientation).c_str());
	}

	switch (ev.type)
	{
		case SDL_EVENT_WINDOW_MOUSE_ENTER:
			return restoreCursor();
		case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED:
			if (!resizeToScale(window))
				return false;
			if (isConnected())
			{
				if (!window->fill())
					return false;
				if (!drawToWindow(*window))
					return false;
				if (!restoreCursor())
					return false;
			}
			break;
		case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
			if (!resizeToScale(window))
				return false;
			if (!window->fill())
				return false;
			if (!drawToWindow(*window))
				return false;
			if (!restoreCursor())
				return false;
			break;
		case SDL_EVENT_WINDOW_MOVED:
		{
			auto r = window->rect();
			auto id = window->id();
			SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "%u: %dx%d-%dx%d", id, r.x, r.y, r.w, r.h);
		}
		break;
		case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
		{
			SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Window closed, terminating RDP session...");
			freerdp_abort_connect_context(context());
		}
		break;
		default:
			break;
	}
	return true;
}

bool SdlContext::handleEvent(const SDL_DisplayEvent& ev)
{
	if (!getDisplayChannelContext().handleEvent(ev))
		return false;

	switch (ev.type)
	{
		case SDL_EVENT_DISPLAY_REMOVED: // Can't show details for this one...
			break;
		default:
		{
			SDL_Rect r = {};
			if (!SDL_GetDisplayBounds(ev.displayID, &r))
				return false;
			const auto name = SDL_GetDisplayName(ev.displayID);
			if (!name)
				return false;
			const auto orientation = SDL_GetCurrentDisplayOrientation(ev.displayID);
			const auto scale = SDL_GetDisplayContentScale(ev.displayID);
			const auto mode = SDL_GetCurrentDisplayMode(ev.displayID);
			if (!mode)
				return false;

			SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION,
			             "%s: [%u, %s] %dx%d-%dx%d {orientation=%s, scale=%f}%s",
			             sdl::utils::toString(ev.type).c_str(), ev.displayID, name, r.x, r.y, r.w,
			             r.h, sdl::utils::toString(orientation).c_str(), static_cast<double>(scale),
			             sdl::utils::toString(mode).c_str());
		}
		break;
	}
	return true;
}

bool SdlContext::handleEvent(const SDL_MouseButtonEvent& ev)
{
	if (!getWindowForId(ev.windowID))
		return true;
	SDL_Event copy = {};
	copy.button = ev;
	if (!eventToPixelCoordinates(ev.windowID, copy))
		return true;
	removeLocalScaling(copy.button.x, copy.button.y);
	applyMonitorOffset(copy.button.windowID, copy.button.x, copy.button.y);
	return SdlTouch::handleEvent(this, copy.button);
}

bool SdlContext::handleEvent(const SDL_TouchFingerEvent& ev)
{
	if (!getWindowForId(ev.windowID))
		return true;
	SDL_Event copy{};
	copy.tfinger = ev;
	if (!eventToPixelCoordinates(ev.windowID, copy))
		return true;
	removeLocalScaling(copy.tfinger.dx, copy.tfinger.dy);
	removeLocalScaling(copy.tfinger.x, copy.tfinger.y);
	applyMonitorOffset(copy.tfinger.windowID, copy.tfinger.x, copy.tfinger.y);
	return SdlTouch::handleEvent(this, copy.tfinger);
}

void SdlContext::addOrUpdateDisplay(SDL_DisplayID id)
{
	auto monitor = SdlWindow::query(id, false);
	_displays.emplace(id, monitor);

	/* Update actual display rectangles:
	 *
	 * 1. Get logical display bounds
	 * 2. Use already known pixel width and height
	 * 3. Iterate over each display and update the x and y offsets by adding all monitor
	 * widths/heights from the primary
	 */
	_offsets.clear();
	for (auto& entry : _displays)
	{
		SDL_Rect bounds{};
		std::ignore = SDL_GetDisplayBounds(entry.first, &bounds);

		SDL_Rect pixel{};
		pixel.w = entry.second.width;
		pixel.h = entry.second.height;
		_offsets.emplace(entry.first, std::pair{ bounds, pixel });
	}

	/* 1. Find primary and update all neighbors
	 * 2. For each neighbor update all neighbors
	 * 3. repeat until all displays updated.
	 */
	const auto primary = SDL_GetPrimaryDisplay();
	std::vector<SDL_DisplayID> handled;
	handled.push_back(primary);

	auto neighbors = updateDisplayOffsetsForNeighbours(primary);
	while (!neighbors.empty())
	{
		auto neighbor = neighbors.front();
		neighbors.erase(neighbors.begin());

		if (std::find(handled.begin(), handled.end(), neighbor) != handled.end())
			continue;
		handled.push_back(neighbor);

		auto next = updateDisplayOffsetsForNeighbours(neighbor, handled);
		neighbors.insert(neighbors.end(), next.begin(), next.end());
	}
	updateMonitorDataFromOffsets();
}

void SdlContext::deleteDisplay(SDL_DisplayID id)
{
	_displays.erase(id);
}

bool SdlContext::eventToPixelCoordinates(SDL_WindowID id, SDL_Event& ev)
{
	auto w = getWindowForId(id);
	if (!w)
		return false;

	/* Ignore errors here, sometimes SDL has no renderer */
	auto renderer = w->renderer();
	if (!renderer)
		return true;
	return SDL_ConvertEventToRenderCoordinates(renderer, &ev);
}

SDL_FPoint SdlContext::applyLocalScaling(const SDL_FPoint& val) const
{
	if (!useLocalScale())
		return val;

	auto rval = val;
	rval.x *= _localScale.x;
	rval.y *= _localScale.y;
	return rval;
}

void SdlContext::removeLocalScaling(float& x, float& y) const
{
	if (!useLocalScale())
		return;
	x /= _localScale.x;
	y /= _localScale.y;
}

SDL_FPoint SdlContext::screenToPixel(SDL_WindowID id, const SDL_FPoint& pos)
{
	auto w = getWindowForId(id);
	if (!w)
		return {};

	/* Ignore errors here, sometimes SDL has no renderer */
	auto renderer = w->renderer();
	if (!renderer)
		return pos;

	SDL_FPoint rpos{};
	if (!SDL_RenderCoordinatesFromWindow(renderer, pos.x, pos.y, &rpos.x, &rpos.y))
		return {};
	removeLocalScaling(rpos.x, rpos.y);
	return rpos;
}

SDL_FPoint SdlContext::pixelToScreen(SDL_WindowID id, const SDL_FPoint& pos)
{
	auto w = getWindowForId(id);
	if (!w)
		return {};

	/* Ignore errors here, sometimes SDL has no renderer */
	auto renderer = w->renderer();
	if (!renderer)
		return pos;

	SDL_FPoint rpos{};
	if (!SDL_RenderCoordinatesToWindow(renderer, pos.x, pos.y, &rpos.x, &rpos.y))
		return {};
	return applyLocalScaling(rpos);
}

SDL_FRect SdlContext::pixelToScreen(SDL_WindowID id, const SDL_FRect& pos, bool round)
{
	const auto fpos = pixelToScreen(id, SDL_FPoint{ pos.x, pos.y });
	const auto size = pixelToScreen(id, SDL_FPoint{ pos.w, pos.h });
	SDL_FRect r{ fpos.x, fpos.y, size.x, size.y };
	if (round)
	{
		r.w = std::ceil(r.w);
		r.h = std::ceil(r.h);
		r.x = std::floor(r.x);
		r.y = std::floor(r.y);
	}
	return r;
}

bool SdlContext::handleEvent(const SDL_Event& ev)
{
	if ((ev.type >= SDL_EVENT_DISPLAY_FIRST) && (ev.type <= SDL_EVENT_DISPLAY_LAST))
	{
		const auto& dev = ev.display;
		return handleEvent(dev);
	}
	if ((ev.type >= SDL_EVENT_WINDOW_FIRST) && (ev.type <= SDL_EVENT_WINDOW_LAST))
	{
		const auto& wev = ev.window;
		return handleEvent(wev);
	}
	switch (ev.type)
	{
		case SDL_EVENT_RENDER_TARGETS_RESET:
		case SDL_EVENT_RENDER_DEVICE_RESET:
		case SDL_EVENT_WILL_ENTER_FOREGROUND:
			return redraw();
		default:
			break;
	}

	if (!isConnected())
		return true;

	switch (ev.type)
	{
		case SDL_EVENT_FINGER_DOWN:
		case SDL_EVENT_FINGER_UP:
		case SDL_EVENT_FINGER_MOTION:
		{
			const auto& cev = ev.tfinger;
			return handleEvent(cev);
		}
		case SDL_EVENT_MOUSE_MOTION:

		{
			const auto& cev = ev.motion;
			return handleEvent(cev);
		}
		case SDL_EVENT_MOUSE_BUTTON_DOWN:
		case SDL_EVENT_MOUSE_BUTTON_UP:
		{
			const auto& cev = ev.button;
			return handleEvent(cev);
		}
		case SDL_EVENT_MOUSE_WHEEL:
		{
			const auto& cev = ev.wheel;
			return handleEvent(cev);
		}
		case SDL_EVENT_CLIPBOARD_UPDATE:
		{
			const auto& cev = ev.clipboard;
			return getClipboardChannelContext().handleEvent(cev);
		}
		case SDL_EVENT_KEY_DOWN:
		case SDL_EVENT_KEY_UP:
		{
			const auto& cev = ev.key;
			return getInputChannelContext().handleEvent(cev);
		}
		default:
			return true;
	}
}

COMMAND_LINE_ARGUMENT_A* SdlContext::args()
{
	return _args.data();
}

size_t SdlContext::argsCount() const
{
	if (_args.size() <= 1)
		return 0;
	return _args.size() - 1;
}

int SdlContext::argumentHandler(const COMMAND_LINE_ARGUMENT_A* arg, void* custom)
{
	auto sdl = static_cast<SdlContext*>(custom);
	if (!sdl)
		return -1;

	if (arg->Name)
	{
		if (strcmp(arg->Name, sdl_allow_screensaver) == 0)
		{
			if (arg->Value != nullptr)
			{
				if (!SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1"))
				{
					SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
					             "SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER) failed with %s",
					             SDL_GetError());
					return -2;
				}
			}
		}
	}
	return 0;
}

CriticalSection& SdlContext::lock()
{
	return _critical;
}

std::vector<rdpPointer*>& SdlContext::pointers()
{
	return _valid_pointers;
}

bool SdlContext::contains(const rdpPointer* ptr) const
{
	for (const auto& cur : _valid_pointers)
	{
		if (cur == ptr)
			return true;
	}
	return false;
}

bool SdlContext::credentialsRead() const
{
	return _credentialsRead;
}

void SdlContext::setCredentialsRead()
{
	_credentialsRead = true;
}

bool SdlContext::resizeToScale(SdlWindow* window)
{
	if (freerdp_settings_get_bool(context()->settings, FreeRDP_SmartSizing))
		return true;
	if (!useLocalScale())
		return true;
	if (!window)
		return false;
	return window->resizeToScale();
}

bool SdlContext::useLocalScale() const
{
	const auto ssize = freerdp_settings_get_bool(context()->settings, FreeRDP_SmartSizing);
	if (ssize)
		return true;
	const auto dynResize =
	    freerdp_settings_get_bool(context()->settings, FreeRDP_DynamicResolutionUpdate);
	const auto fs = freerdp_settings_get_bool(context()->settings, FreeRDP_Fullscreen);
	const auto multimon = freerdp_settings_get_bool(context()->settings, FreeRDP_UseMultimon);
	return !dynResize && !fs && !multimon;
}

bool SdlContext::drawToWindows(const std::vector<SDL_Rect>& rects)
{
	for (auto& window : _windows)
	{
		if (!drawToWindow(window.second, rects))
			return FALSE;
	}

	return TRUE;
}

BOOL SdlContext::desktopResize(rdpContext* context)
{
	rdpGdi* gdi = nullptr;
	rdpSettings* settings = nullptr;
	auto sdl = get_context(context);

	WINPR_ASSERT(sdl);
	WINPR_ASSERT(context);

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

	std::unique_lock lock(sdl->_critical);
	gdi = context->gdi;
	if (!gdi_resize(gdi, freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth),
	                freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight)))
		return FALSE;
	return sdl->createPrimary();
}

/* This function is called to output a System BEEP */
BOOL SdlContext::playSound(rdpContext* context, const PLAY_SOUND_UPDATE* play_sound)
{
	/* TODO: Implement */
	WINPR_UNUSED(context);
	WINPR_UNUSED(play_sound);
	return TRUE;
}

/* This function is called whenever a new frame starts.
 * It can be used to reset invalidated areas. */
BOOL SdlContext::beginPaint(rdpContext* context)
{
	auto gdi = context->gdi;
	WINPR_ASSERT(gdi);
	WINPR_ASSERT(gdi->primary);

	HGDI_DC hdc = gdi->primary->hdc;
	WINPR_ASSERT(hdc);
	if (!hdc->hwnd)
		return TRUE;

	HGDI_WND hwnd = hdc->hwnd;
	WINPR_ASSERT(hwnd->invalid);
	hwnd->invalid->null = TRUE;
	hwnd->ninvalid = 0;

	return TRUE;
}

bool SdlContext::redraw(bool suppress) const
{
	if (!_connected)
		return true;

	auto gdi = context()->gdi;
	WINPR_ASSERT(gdi);
	return gdi_send_suppress_output(gdi, suppress);
}

void SdlContext::setConnected(bool val)
{
	_connected = val;
}

bool SdlContext::isConnected() const
{
	return _connected;
}

rdpContext* SdlContext::context() const
{
	WINPR_ASSERT(_context);
	return _context;
}

rdpClientContext* SdlContext::common() const
{
	return reinterpret_cast<rdpClientContext*>(context());
}

bool SdlContext::setCursor(CursorType type)
{
	_cursorType = type;
	return restoreCursor();
}

bool SdlContext::setCursor(const rdpPointer* cursor)
{
	std::unique_lock lock(_critical);
	if (!contains(cursor))
		return true;

	_cursor = { sdl_Pointer_Copy(cursor), sdl_PointerFreeCopyAll };
	return setCursor(CURSOR_IMAGE);
}

rdpPointer* SdlContext::cursor() const
{
	return _cursor.get();
}

bool SdlContext::restoreCursor()
{
	WLog_Print(getWLog(), WLOG_DEBUG, "restore cursor: %d", _cursorType);
	switch (_cursorType)
	{
		case CURSOR_NULL:
			if (!SDL_HideCursor())
			{
				WLog_Print(getWLog(), WLOG_ERROR, "SDL_HideCursor failed");
				return false;
			}

			setHasCursor(false);
			return true;

		case CURSOR_DEFAULT:
		{
			auto def = SDL_GetDefaultCursor();
			if (!SDL_SetCursor(def))
			{
				WLog_Print(getWLog(), WLOG_ERROR, "SDL_SetCursor(default=%p) failed",
				           static_cast<void*>(def));
				return false;
			}
			if (!SDL_ShowCursor())
			{
				WLog_Print(getWLog(), WLOG_ERROR, "SDL_ShowCursor failed");
				return false;
			}
			setHasCursor(true);
			return true;
		}
		case CURSOR_IMAGE:
			setHasCursor(true);
			return sdl_Pointer_Set_Process(this);
		default:
			WLog_Print(getWLog(), WLOG_ERROR, "Unknown cursorType %s",
			           sdl::utils::toString(_cursorType).c_str());
			return false;
	}
}

void SdlContext::setMonitorIds(const std::vector<SDL_DisplayID>& ids)
{
	_monitorIds.clear();
	for (auto id : ids)
	{
		_monitorIds.push_back(id);
	}
}

const std::vector<SDL_DisplayID>& SdlContext::monitorIds() const
{
	return _monitorIds;
}

int64_t SdlContext::monitorId(uint32_t index) const
{
	if (index >= _monitorIds.size())
	{
		return -1;
	}
	return _monitorIds.at(index);
}

void SdlContext::push(std::vector<SDL_Rect>&& rects)
{
	std::unique_lock lock(_queue_mux);
	_queue.emplace(std::move(rects));
}

std::vector<SDL_Rect> SdlContext::pop()
{
	std::unique_lock lock(_queue_mux);
	if (_queue.empty())
	{
		return {};
	}
	auto val = std::move(_queue.front());
	_queue.pop();
	return val;
}

bool SdlContext::setFullscreen(bool enter, bool forceOriginalDisplay)
{
	for (const auto& window : _windows)
	{
		if (!sdl_push_user_event(SDL_EVENT_USER_WINDOW_FULLSCREEN, &window.second, enter,
		                         forceOriginalDisplay))
			return false;
	}
	_fullscreen = enter;
	return true;
}

bool SdlContext::setMinimized()
{
	return sdl_push_user_event(SDL_EVENT_USER_WINDOW_MINIMIZE);
}

bool SdlContext::grabMouse() const
{
	return _grabMouse;
}

bool SdlContext::toggleGrabMouse()
{
	return setGrabMouse(!grabMouse());
}

bool SdlContext::setGrabMouse(bool enter)
{
	_grabMouse = enter;
	return true;
}

bool SdlContext::grabKeyboard() const
{
	return _grabKeyboard;
}

bool SdlContext::toggleGrabKeyboard()
{
	return setGrabKeyboard(!grabKeyboard());
}

bool SdlContext::setGrabKeyboard(bool enter)
{
	_grabKeyboard = enter;
	return true;
}

bool SdlContext::setResizeable(bool enable)
{
	const auto settings = context()->settings;
	const bool dyn = freerdp_settings_get_bool(settings, FreeRDP_DynamicResolutionUpdate);
	const bool smart = freerdp_settings_get_bool(settings, FreeRDP_SmartSizing);
	bool use = (dyn && enable) || smart;

	for (const auto& window : _windows)
	{
		if (!sdl_push_user_event(SDL_EVENT_USER_WINDOW_RESIZEABLE, &window.second, use))
			return false;
	}
	_resizeable = use;

	return true;
}

bool SdlContext::resizeable() const
{
	return _resizeable;
}

bool SdlContext::toggleResizeable()
{
	return setResizeable(!resizeable());
}

bool SdlContext::fullscreen() const
{
	return _fullscreen;
}

bool SdlContext::toggleFullscreen()
{
	return setFullscreen(!fullscreen());
}
