/*
	This file is part of Warzone 2100.
	Copyright (C) 2011-2020  Warzone 2100 Project

	Warzone 2100 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.

	Warzone 2100 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 Warzone 2100; if not, write to the Free Software
	Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
/**
 * @file main_sdl.cpp
 *
 * SDL backend code
 */

// Get platform defines before checking for them!
#include "lib/framework/wzapp.h"

#include "lib/framework/input.h"
#include "lib/framework/utf.h"
#include "lib/ivis_opengl/pieclip.h"
#include "lib/ivis_opengl/piemode.h"
#include "lib/ivis_opengl/screen.h"
#include "lib/exceptionhandler/dumpinfo.h"
#include "lib/gamelib/gtime.h"
#include "src/configuration.h"
#include "src/warzoneconfig.h"
#include "src/game.h"
#include "gfx_api_sdl.h"
#include "gfx_api_gl_sdl.h"
#include "sdl_backend_private.h"

#if defined( _MSC_VER )
	// Silence warning when using MSVC ARM64 compiler
	//	warning C4121: 'SDL_hid_device_info': alignment of a member was sensitive to packing
	#pragma warning( push )
	#pragma warning( disable : 4121 )
#endif

#include <SDL3/SDL.h>
#if defined(HAVE_SDL_VULKAN_H)
#include <SDL3/SDL_vulkan.h>
#endif

#if defined( _MSC_VER )
	#pragma warning( pop )
#endif

#include "wz2100icon.h"
#include "cursors_sdl.h"
#include <algorithm>
#include <map>
#include <locale.h>
#include <atomic>
#include <chrono>

#if defined(WZ_OS_MAC)
#include "cocoa_sdl_helpers.h"
#include "cocoa_wz_menus.h"
#include "lib/framework/cocoa_wrapper.h"
#endif

#if defined(__EMSCRIPTEN__)
#include <emscripten.h>
#endif

#include <nonstd/optional.hpp>
using nonstd::optional;
using nonstd::nullopt;

void mainLoop();
// used in crash reports & version info
const char *BACKEND = "SDL";

std::map<KEY_CODE, SDL_Scancode> KEY_CODE_to_SDLScancode;
std::map<SDL_Scancode, KEY_CODE > SDLScancode_to_KEY_CODE;

int realmain(int argc, char *argv[]);

// the main stub which calls realmain() aka, WZ's main startup routines
#include <SDL3/SDL_main.h>
int main(int argc, char *argv[])
{
	return realmain(argc, argv);
}

// At this time, we only have 1 window.
static SDL_Window *WZwindow = nullptr;
static optional<video_backend> WZbackend = video_backend::vulkan;

// The screen that the game window is on.
int screenIndex = 0;
// The logical resolution of the game in the game's coordinate system (points).
unsigned int screenWidth = 0;
unsigned int screenHeight = 0;
// The logical resolution of the SDL window in the window's coordinate system (points) - i.e. not accounting for the Game Display Scale setting.
unsigned int windowWidth = 0;
unsigned int windowHeight = 0;
// The current display scale factor.
unsigned int current_displayScale = 100;
float current_displayScaleFactor = 1.f;

struct QueuedWindowDimensions
{
	int width;
	int height;
	bool recenter;
};
optional<QueuedWindowDimensions> deferredDimensionReset = nullopt;

static std::vector<optional<screeninfo>> displaylist;	// holds all our possible display lists

std::atomic<Uint32> wzSDLAppEvent((Uint32)-1);
enum wzSDLAppEventCodes
{
	MAINTHREADEXEC
};

/* The possible states for keys */
enum KEY_STATE
{
	KEY_UP,
	KEY_PRESSED,
	KEY_DOWN,
	KEY_RELEASED,
	KEY_PRESSRELEASE,	// When a key goes up and down in a frame
	KEY_DOUBLECLICK,	// Only used by mouse keys
	KEY_DRAG			// Only used by mouse keys
};

struct INPUT_STATE
{
	KEY_STATE state; /// Last key/mouse state
	UDWORD lastdown; /// last key/mouse button down timestamp
	Vector2i pressPos;    ///< Location of last mouse press event.
	Vector2i releasePos;  ///< Location of last mouse release event.
};

// Clipboard routines
bool has_scrap(void);
bool get_scrap(char **dst);

/// constant for the interval between 2 singleclicks for doubleclick event in ms
#define DOUBLE_CLICK_INTERVAL 250

/* The current state of the keyboard */
// NOTE: SDL_NUM_SCANCODES is the max, but KEY_MAXSCAN is our limit
static INPUT_STATE aKeyState[KEY_MAXSCAN];		// the logical key state (impacted by attempts to clear input)
static INPUT_STATE actualKeyState[KEY_MAXSCAN];	// the underlying key state (ignoring any attempts to clear input)

/* The current location of the mouse */
static Uint16 mouseXPos = 0;
static Uint16 mouseYPos = 0;
static Vector2i mouseWheelSpeed;
static bool mouseInWindow = true;
static bool windowHasFocus = true;

/* How far the mouse has to move to start a drag */
#define DRAG_THRESHOLD	5

/* Which button is being used for a drag */
static MOUSE_KEY_CODE dragKey;

/* The start of a possible drag by the mouse */
static int dragX = 0;
static int dragY = 0;

/* The current mouse button state */
static INPUT_STATE aMouseState[MOUSE_END];
static MousePresses mousePresses;

/* The current screen resizing state for this iteration through the game loop, in the game coordinate system */
struct ScreenSizeChange
{
	ScreenSizeChange(unsigned int oldWidth, unsigned int oldHeight, unsigned int newWidth, unsigned int newHeight)
	:	oldWidth(oldWidth)
	,	oldHeight(oldHeight)
	,	newWidth(newWidth)
	,	newHeight(newHeight)
	{ }
	unsigned int oldWidth;
	unsigned int oldHeight;
	unsigned int newWidth;
	unsigned int newHeight;
};
static ScreenSizeChange* currentScreenResizingStatus = nullptr;

/* The size of the input buffer */
#define INPUT_MAXSTR 256

/* The input string buffer */
struct InputKey
{
	UDWORD key;
	utf_32_char unicode;
};

static InputKey	pInputBuffer[INPUT_MAXSTR];
static InputKey	*pStartBuffer, *pEndBuffer;
static utf_32_char *utf8Buf;				// is like the old 'unicode' from SDL 1.x
void* GetTextEventsOwner = nullptr;

static optional<int> wzQuitExitCode;

bool wzReduceDisplayScalingIfNeeded(int currWidth, int currHeight);

#if defined(__EMSCRIPTEN__)

#include <emscripten.h>
#include <emscripten/html5.h>

void wzemscripten_startup_ensure_canvas_displayed();
bool wz_emscripten_enable_soft_fullscreen();
EM_BOOL wz_emscripten_fullscreenchange_callback(int eventType, const EmscriptenFullscreenChangeEvent *fullscreenChangeEvent, void *userData);

#endif

/**************************/
/***     Misc support   ***/
/**************************/

WzString wzGetPlatform()
{
	return WzString::fromUtf8(SDL_GetPlatform());
}

// See if we have TEXT in the clipboard
bool has_scrap(void)
{
	return SDL_HasClipboardText();
}

// Set the clipboard text
bool wzSetClipboardText(const char *src)
{
	if (SDL_SetClipboardText(src))
	{
		debug(LOG_ERROR, "Could not put clipboard text because : %s", SDL_GetError());
		return false;
	}
	return true;
}

// Get text from the clipboard
bool get_scrap(char **dst)
{
	if (has_scrap())
	{
		char *cliptext = SDL_GetClipboardText();
		if (!cliptext)
		{
			debug(LOG_ERROR, "Could not get clipboard text because : %s", SDL_GetError());
			return false;
		}
		*dst = cliptext;
		return true;
	}
	else
	{
		// wasn't text or no text in the clipboard
		return false;
	}
}

void StartTextInput(void* pTextInputRequester, const WzTextInputRect& textInputRect)
{
	if (WZwindow == nullptr)
	{
		debug(LOG_WARNING, "StartTextInput called when window is not available");
		return;
	}

	if (!GetTextEventsOwner)
	{
		SDL_Rect rect;
		rect.x = textInputRect.x;
		rect.y = textInputRect.y;
		rect.w = textInputRect.width;
		rect.h = textInputRect.height;
		SDL_SetTextInputArea(WZwindow, &rect, 0);
		SDL_StartTextInput(WZwindow);	// enable text events
		debug(LOG_INPUT, "SDL text events started");
	}
	else if (pTextInputRequester != GetTextEventsOwner)
	{
		debug(LOG_INPUT, "StartTextInput called by new input requester before old requester called StopTextInput");
	}
	GetTextEventsOwner = pTextInputRequester;

	// on some platforms, it seems like we also need to call SDL_SetTextInputRect every frame to properly set the text input rect (?)
	SDL_Rect rect;
	rect.x = textInputRect.x;
	rect.y = textInputRect.y;
	rect.w = textInputRect.width;
	rect.h = textInputRect.height;
	SDL_SetTextInputArea(WZwindow, &rect, 0);
}

void StopTextInput(void* pTextInputResigner)
{
	if (!GetTextEventsOwner)
	{
		debug(LOG_INPUT, "Ignoring StopTextInput call when text input is already disabled");
		return;
	}
	if (pTextInputResigner != GetTextEventsOwner)
	{
		// Rejecting StopTextInput from regsigner who is not the last requester
		debug(LOG_INPUT, "Ignoring StopTextInput call from resigner that is not the last requester (i.e. caller of StartTextInput)");
		return;
	}
	if (WZwindow == nullptr)
	{
		debug(LOG_WARNING, "StartTextInput called when window is not available");
		return;
	}
	SDL_StopTextInput(WZwindow);	// disable text events
	GetTextEventsOwner = nullptr;
	debug(LOG_INPUT, "SDL text events stopped");
}

bool isInTextInputMode()
{
	if (WZwindow == nullptr)
	{
		debug(LOG_WARNING, "isInTextInputMode called when window is not available");
		return false;
	}
	bool result = (GetTextEventsOwner != nullptr);
	ASSERT((SDL_TextInputActive(WZwindow) != false) == result,
	       "How did GetTextEvents state and SDL_IsTextInputActive get out of sync?");
	return result;
}

bool wzHasTouchInputDevices()
{
	int numDevices = 0;
	SDL_TouchID *devices = SDL_GetTouchDevices(&numDevices);
	if (devices == nullptr)
	{
		return false;
	}
	SDL_free(devices);
	return numDevices > 0;
}

bool wzSeemsLikeNonTouchPlatform()
{
	return !wzHasTouchInputDevices() || (SDL_HasScreenKeyboardSupport() == false);
}

/* Put a character into a text buffer overwriting any text under the cursor */
WzString wzGetSelection()
{
	WzString retval;
	static char *scrap = nullptr;

	if (get_scrap(&scrap))
	{
		retval = WzString::fromUtf8(scrap);
	}
	return retval;
}

std::vector<optional<screeninfo>> wzAvailableResolutions()
{
	return displaylist;
}

optional<screeninfo> wzGetCurrentFullscreenDisplayMode()
{
	screeninfo result = {};
	if (WZwindow == nullptr)
	{
		debug(LOG_WARNING, "wzGetCurrentFullscreenDisplayMode called when window is not available");
		return {};
	}
	SDL_DisplayID displayId = SDL_GetDisplayForWindow(WZwindow);
	if (displayId == 0)
	{
		debug(LOG_WZ, "Failed to get current screen index: %s", SDL_GetError());
		displayId = 1;
	}
	const SDL_DisplayMode *currentExclusiveFullscreenMode = SDL_GetWindowFullscreenMode(WZwindow);
	if (!currentExclusiveFullscreenMode)
	{
		// This actually means the current setting is borderless fullscreen
		return nullopt;
	}
	result.screen = displayId;
	result.width = currentExclusiveFullscreenMode->w;
	result.height = currentExclusiveFullscreenMode->h;
	result.pixel_density = currentExclusiveFullscreenMode->pixel_density;
	result.refresh_rate = currentExclusiveFullscreenMode->refresh_rate;
	return result;
}

std::vector<unsigned int> wzAvailableDisplayScales()
{
	static const unsigned int wzDisplayScales[] = { 100, 125, 150, 200, 250, 300, 400, 500 };
	return std::vector<unsigned int>(wzDisplayScales, wzDisplayScales + (sizeof(wzDisplayScales) / sizeof(wzDisplayScales[0])));
}

std::vector<video_backend> wzAvailableGfxBackends()
{
	std::vector<video_backend> availableBackends;
#if defined(__EMSCRIPTEN__)
// EMSCRIPTEN:
	// - Only supports OpenGL ES (WebGL) backend
	availableBackends.push_back(video_backend::opengles);
#elif defined(WZ_OS_WIN)
// WINDOWS:
# if defined(_M_X64) || defined(_M_IX86)
	// [X86, X64 Builds]
	// - Order is: Vulkan, OpenGL, DirectX, OpenGL ES
#   if defined(WZ_VULKAN_ENABLED) && defined(HAVE_SDL_VULKAN_H)
	availableBackends.push_back(video_backend::vulkan);
#   endif
	availableBackends.push_back(video_backend::opengl);
#   if defined(WZ_BACKEND_DIRECTX)
	availableBackends.push_back(video_backend::directx);
#   endif
	availableBackends.push_back(video_backend::opengles);
# else // !(defined(_M_X64) || defined(_M_IX86) // ARM, ARM64, etc
	// [Other Builds: ARM64, etc]
	// For newer architectures (example: ARM64):
	// - The assumption is that OpenGL is least likely to have native / good drivers (probably using a compatibility layer, if anything)
	// - But many ARM64 devices are shipping with native Vulkan drivers
	// - Order is: Vulkan, DirectX, OpenGL, OpenGL ES
#   if defined(WZ_VULKAN_ENABLED) && defined(HAVE_SDL_VULKAN_H)
	availableBackends.push_back(video_backend::vulkan);
#   endif
#   if defined(WZ_BACKEND_DIRECTX)
	availableBackends.push_back(video_backend::directx);
#   endif
	availableBackends.push_back(video_backend::opengl);
	availableBackends.push_back(video_backend::opengles);
# endif // (defined(_M_X64) || defined(_M_IX86)
#elif defined(WZ_OS_MAC)
// MACOS:
	if (cocoaIsRunningOnMacOSAtLeastVersion(13, 0)) // macOS 13.0+, which has Metal 3+
	{
		// - Order is: Vulkan, OpenGL
#   	if defined(WZ_VULKAN_ENABLED) && defined(HAVE_SDL_VULKAN_H)
		availableBackends.push_back(video_backend::vulkan);
#   	endif
		availableBackends.push_back(video_backend::opengl);
	}
	else
	{
		// - For older macOS, default to: OpenGL, Vulkan
		availableBackends.push_back(video_backend::opengl);
#   	if defined(WZ_VULKAN_ENABLED) && defined(HAVE_SDL_VULKAN_H)
		availableBackends.push_back(video_backend::vulkan);
#   	endif
	}
#elif defined(WZ_OS_UNIX)
// LINUX / UNIX:
	// We'd *like* to default to Vulkan first here,
	// But an SDL2 bug (fixed in SDL3) may cause attempts to show a message box (which can happen when Vulkan fails) on X11 to crash.
	// So for now, the order is still: OpenGL, OpenGL ES, Vulkan
	// FUTURE TODO: Once ported to SDL3, switch this to: Vulkan, OpenGL, OpenGL ES (?)
	availableBackends.push_back(video_backend::opengl);
	availableBackends.push_back(video_backend::opengles);
#   if defined(WZ_VULKAN_ENABLED) && defined(HAVE_SDL_VULKAN_H)
	availableBackends.push_back(video_backend::vulkan);
#   endif
#else
// Anything else:
	// Default to offering Vulkan, OpenGL, OpenGL ES
#   if defined(WZ_VULKAN_ENABLED) && defined(HAVE_SDL_VULKAN_H)
	availableBackends.push_back(video_backend::vulkan);
#   endif
	availableBackends.push_back(video_backend::opengl);
	availableBackends.push_back(video_backend::opengles);
#endif
	return availableBackends;
}

video_backend wzGetDefaultGfxBackendForCurrentSystem()
{
	auto availableBackends = wzAvailableGfxBackends();
	ASSERT_OR_RETURN(video_backend::opengl, !availableBackends.empty(), "Available backends list is empty?");
	return availableBackends.front();
}

static video_backend wzGetNextFallbackGfxBackendForCurrentSystem(const video_backend& current_failed_backend)
{
	video_backend next_backend;

	// get sorted list of backends
	auto sortedBackends = wzAvailableGfxBackends();
#if defined(WZ_OS_WIN)
	// Never offer OpenGL ES as a fallback option on Windows
	sortedBackends.erase(std::remove(sortedBackends.begin(), sortedBackends.end(), video_backend::opengles), sortedBackends.end());
#endif

	if (sortedBackends.empty())
	{
		// nothing to do - return the default
		return wzGetDefaultGfxBackendForCurrentSystem();
	}

	// find the position of the current backend in the sorted list
	auto it = std::find(sortedBackends.begin(), sortedBackends.end(), current_failed_backend);
	if (it != sortedBackends.end())
	{
		auto it_next = it + 1;
		if (it_next != sortedBackends.end())
		{
			next_backend = *it_next;
		}
		else
		{
			// loop back to the first
			next_backend = sortedBackends.front();
		}
	}
	else
	{
		debug(LOG_INFO, "Current failed backend is not in the available backends list: %s", to_display_string(current_failed_backend).c_str());
		// use the first backend
		next_backend = sortedBackends.front();
	}

	return next_backend;
}

bool SDL_WZBackend_GetDrawableSize(SDL_Window* window,
								   int*        w,
								   int*        h)
{
	if (!WZbackend.has_value())
	{
		return false;
	}
	return SDL_GetWindowSizeInPixels(window, w, h);
}

void setDisplayScale(unsigned int displayScale)
{
	ASSERT(displayScale >= 100, "Invalid display scale: %u", displayScale);
	current_displayScale = displayScale;
	current_displayScaleFactor = (float)displayScale / 100.f;
}

unsigned int wzGetCurrentDisplayScale()
{
	return current_displayScale;
}

void wzShowMouse(bool visible)
{
	if (visible)
	{
		SDL_ShowCursor();
	}
	else
	{
		SDL_HideCursor();
	}
}

uint32_t wzGetTicks()
{
	return static_cast<uint32_t>(SDL_GetTicks());
}

void wzDisplayDialog(DialogType type, const char *title, const char *message)
{
	if (!WZbackend.has_value())
	{
		// while this could be thread_local, thread_local may not yet be supported on all platforms properly
		// and wzDisplayDialog should probably be called **only** on the main thread anyway
		static bool processingDialog = false;
		if (!processingDialog)
		{
			// in headless mode, do not display a messagebox (which might block the main thread)
			// but just log the details
			processingDialog = true;
			debug(LOG_INFO, "Suppressed dialog (headless):\n\tTitle: %s\n\tMessage: %s", title, message);
			processingDialog = false;
		}
		return;
	}

	Uint32 sdl_messagebox_flags = 0;
	switch (type)
	{
		case Dialog_Error:
			sdl_messagebox_flags = SDL_MESSAGEBOX_ERROR;
			break;
		case Dialog_Warning:
			sdl_messagebox_flags = SDL_MESSAGEBOX_WARNING;
			break;
		case Dialog_Information:
			sdl_messagebox_flags = SDL_MESSAGEBOX_INFORMATION;
			break;
	}
	SDL_ShowSimpleMessageBox(sdl_messagebox_flags, title, message, WZwindow);
}

// Displays a message box with specified button(s)
// Returns 0 on failure, or the (clicked button index + 1)
size_t wzDisplayDialogAdvanced(DialogType type, const char *title, const char *message, std::vector<std::string> buttonsText)
{
	if (!WZbackend.has_value())
	{
		// while this could be thread_local, thread_local may not yet be supported on all platforms properly
		// and wzDisplayDialog should probably be called **only** on the main thread anyway
		static bool processingDialog = false;
		if (!processingDialog)
		{
			// in headless mode, do not display a messagebox (which might block the main thread)
			// but just log the details
			processingDialog = true;
			debug(LOG_INFO, "Suppressed dialog (headless):\n\tTitle: %s\n\tMessage: %s", title, message);
			processingDialog = false;
		}
		return 0;
	}

	if (buttonsText.empty())
	{
		// always at least show an "OK" button
		buttonsText.push_back("OK");
	}

	std::vector<SDL_MessageBoxButtonData> buttons;
	buttons.reserve(buttonsText.size());
	for (size_t i = 0; i < buttonsText.size(); ++i)
	{
		Uint32 buttonFlags = 0;
		if (i == 0)
		{
			buttonFlags |= SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT;
		}
		if (i == buttonsText.size() - 1)
		{
			buttonFlags |= SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT;
		}
		buttons.push_back(SDL_MessageBoxButtonData{buttonFlags, static_cast<int>(i + 1), buttonsText[i].c_str()});
	}

	Uint32 sdl_messagebox_flags = 0;
	switch (type)
	{
		case Dialog_Error:
			sdl_messagebox_flags = SDL_MESSAGEBOX_ERROR;
			break;
		case Dialog_Warning:
			sdl_messagebox_flags = SDL_MESSAGEBOX_WARNING;
			break;
		case Dialog_Information:
			sdl_messagebox_flags = SDL_MESSAGEBOX_INFORMATION;
			break;
	}

	const SDL_MessageBoxData messageboxdata = {
		sdl_messagebox_flags, /* .flags */
		WZwindow, /* .window */
		title, /* .title */
		message, /* .message */
		static_cast<int>(buttons.size()), /* .numbuttons */
		buttons.data(), /* .buttons */
		nullptr /* .colorScheme */
	};
	int buttonid = 0;
	if (!SDL_ShowMessageBox(&messageboxdata, &buttonid)) {
		// error displaying message box
		return 0;
	}
	return buttonid;
}

WINDOW_MODE wzGetCurrentWindowMode()
{
	if (!WZbackend.has_value())
	{
		// return a dummy value
		return WINDOW_MODE::windowed;
	}
	if (WZwindow == nullptr)
	{
		debug(LOG_WARNING, "wzGetCurrentWindowMode called when window is not available");
		return WINDOW_MODE::windowed;
	}

	Uint32 flags = SDL_GetWindowFlags(WZwindow);
	if (flags & SDL_WINDOW_FULLSCREEN)
	{
		return WINDOW_MODE::fullscreen;
	}
	else
	{
		return WINDOW_MODE::windowed;
	}
}

std::vector<WINDOW_MODE> wzSupportedWindowModes()
{
#if defined(__EMSCRIPTEN__)
	// For now, Emscripten always uses soft-fullscreen windowed mode
	return {WINDOW_MODE::windowed};
#else
	return {WINDOW_MODE::fullscreen, WINDOW_MODE::windowed};
#endif
}

bool wzIsSupportedWindowMode(WINDOW_MODE mode)
{
	auto supportedModes = wzSupportedWindowModes();
	return std::any_of(supportedModes.begin(), supportedModes.end(), [mode](WINDOW_MODE i) -> bool {
		return i == mode;
	});
}

WINDOW_MODE wzGetNextWindowMode(WINDOW_MODE currentMode)
{
	auto supportedModes = wzSupportedWindowModes();
	ASSERT_OR_RETURN(WINDOW_MODE::windowed, !supportedModes.empty(), "No supported fullscreen / windowed modes available?");
	auto it = std::find(supportedModes.begin(), supportedModes.end(), currentMode);
	if (it == supportedModes.end())
	{
		// we appear to be in an unsupported mode - so default to the first
		return *supportedModes.begin();
	}
	++it;
	if (it == supportedModes.end()) { it = supportedModes.begin(); }
	return *it;
}

WINDOW_MODE wzAltEnterToggleFullscreen()
{
	auto mode = wzGetCurrentWindowMode();
	switch (mode)
	{
		case WINDOW_MODE::fullscreen:
			if (wzChangeWindowMode(WINDOW_MODE::windowed))
			{
				mode = WINDOW_MODE::windowed;
			}
			break;
		case WINDOW_MODE::windowed:
			if (wzChangeWindowMode(WINDOW_MODE::fullscreen))
			{
				mode = WINDOW_MODE::fullscreen;
			}
			break;
	}
	return mode;
}

void wzPostChangedSwapInterval()
{
	// currently, no-op
}

bool wzChangeWindowMode(WINDOW_MODE mode, bool silent)
{
	auto previousMode = wzGetCurrentWindowMode();
	if (previousMode == mode)
	{
		// already in this mode
		return true;
	}

	if (!wzIsSupportedWindowMode(mode))
	{
		// not a supported mode on this system
		return false;
	}

	if (!silent)
	{
		debug(LOG_INFO, "Changing window mode: %s -> %s", to_display_string(previousMode).c_str(), to_display_string(mode).c_str());
	}

	switch (mode)
	{
		case WINDOW_MODE::windowed:
		{
			SDL_DisplayID currDisplayId = SDL_GetDisplayForWindow(WZwindow);
			if (currDisplayId == 0)
			{
				currDisplayId = screenIndex;
			}
			// disable fullscreen mode
			if (!SDL_SetWindowFullscreen(WZwindow, false)) { return false; }
			wzSetWindowIsResizable(true);
			// Determine the maximum usable windowed size for this display/screen, and cap the desired window size at that
			int desiredWidth = war_GetWidth(), desiredHeight = war_GetHeight();
			SDL_Rect displayUsableBounds = { 0, 0, 0, 0 };
			if (SDL_GetDisplayUsableBounds(currDisplayId, &displayUsableBounds))
			{
				if (displayUsableBounds.w > 0 && displayUsableBounds.h > 0)
				{
					desiredWidth = std::min(displayUsableBounds.w, desiredWidth);
					desiredHeight = std::min(displayUsableBounds.h, desiredHeight);
				}
			}
			// restore the old windowed size
			SDL_SetWindowSize(WZwindow, desiredWidth, desiredHeight);
			// Position the window (centered) on the screen (for its new size)
			SDL_SetWindowPosition(WZwindow, SDL_WINDOWPOS_CENTERED_DISPLAY(currDisplayId), SDL_WINDOWPOS_CENTERED_DISPLAY(currDisplayId));
			// Workaround for issues properly restoring window dimensions when changing from fullscreen -> windowed (on some platforms)
			deferredDimensionReset = QueuedWindowDimensions {desiredWidth, desiredHeight, true};
			break;
		}
		case WINDOW_MODE::fullscreen:
		{
#if defined(__EMSCRIPTEN__)
			emscripten_exit_soft_fullscreen();
#endif

			if (!SDL_SetWindowFullscreen(WZwindow, true))
			{
#if defined(__EMSCRIPTEN__)
				if (previousMode == WINDOW_MODE::windowed)
				{
					wz_emscripten_enable_soft_fullscreen();
				}
#endif
				return false;
			}
			wzSetWindowIsResizable(false);
			break;
		}
	}

#if defined(WZ_OS_MAC)
	// Wait for window size changes to be processed
	SDL_SyncWindow(WZwindow);
#endif

	return true;
}

bool wzIsFullscreen()
{
	if (WZwindow == nullptr)
	{
		// NOTE: Can't use debug(...) to log here or it will risk overwriting fatal error messages,
		// as `_debug(...)` calls this function
		return false;
	}
	Uint32 flags = SDL_GetWindowFlags(WZwindow);
	if (flags & SDL_WINDOW_FULLSCREEN)
	{
		return true;
	}
	return false;
}

bool wzIsMaximized()
{
	if (WZwindow == nullptr)
	{
		debug(LOG_WARNING, "wzIsMaximized called when window is not available");
		return false;
	}
	Uint32 flags = SDL_GetWindowFlags(WZwindow);
	if (flags & SDL_WINDOW_MAXIMIZED)
	{
		return true;
	}
	return false;
}

bool wzWindowHasFocus()
{
	return windowHasFocus;
}

void wzQuit(int exitCode)
{
	if (!wzQuitExitCode.has_value())
	{
		wzQuitExitCode = exitCode;
	}
	// Create a quit event to halt game loop.
	SDL_Event quitEvent;
	quitEvent.type = SDL_EVENT_QUIT;
	SDL_PushEvent(&quitEvent);
}

int wzGetQuitExitCode()
{
	return wzQuitExitCode.value_or(0);
}

void wzGrabMouse()
{
	if (!WZbackend.has_value())
	{
		return;
	}
	if (WZwindow == nullptr)
	{
		debug(LOG_WARNING, "wzGrabMouse called when window is not available - ignoring");
		return;
	}
	SDL_SetWindowMouseGrab(WZwindow, true);
}

void wzReleaseMouse()
{
	if (!WZbackend.has_value())
	{
		return;
	}
	if (WZwindow == nullptr)
	{
		debug(LOG_WARNING, "wzReleaseMouse called when window is not available - ignoring");
		return;
	}
	SDL_SetWindowMouseGrab(WZwindow, false);
}

void wzDelay(unsigned int delay)
{
	SDL_Delay(delay);
}

/**************************/
/***    Thread support  ***/
/**************************/
WZ_THREAD *wzThreadCreate(int (*threadFunc)(void *), void *data, const char* name)
{
	const char* defaultName = "wzThread";
	if (name == nullptr)
		name = defaultName;
#if defined( _MSC_VER )
#pragma warning( push )
#pragma warning( disable : 4191 ) // warning C4191: 'type cast': unsafe conversion from 'uintptr_t (__cdecl *)(void *,unsigned int,_beginthreadex_proc_type,void *,unsigned int,unsigned int *)' to 'SDL_FunctionPointer'
#endif
	return (WZ_THREAD *)SDL_CreateThread(threadFunc, name, data);
#if defined( _MSC_VER )
#pragma warning( pop )
#endif
}

unsigned long wzThreadID(WZ_THREAD *thread)
{
	SDL_ThreadID threadID = SDL_GetThreadID((SDL_Thread *)thread);
	return threadID;
}

int wzThreadJoin(WZ_THREAD *thread)
{
	int result;
	SDL_WaitThread((SDL_Thread *)thread, &result);
	return result;
}

void wzThreadDetach(WZ_THREAD *thread)
{
	SDL_DetachThread((SDL_Thread *)thread);
}

void wzThreadStart(WZ_THREAD *thread)
{
	(void)thread; // no-op
}

void wzYieldCurrentThread()
{
	SDL_Delay(40);
}

WZ_MUTEX *wzMutexCreate()
{
	return (WZ_MUTEX *)SDL_CreateMutex();
}

void wzMutexDestroy(WZ_MUTEX *mutex)
{
	SDL_DestroyMutex((SDL_Mutex *)mutex);
}

void wzMutexLock(WZ_MUTEX *mutex)
{
	SDL_LockMutex((SDL_Mutex *)mutex);
}

void wzMutexUnlock(WZ_MUTEX *mutex)
{
	SDL_UnlockMutex((SDL_Mutex *)mutex);
}

WZ_SEMAPHORE *wzSemaphoreCreate(int startValue)
{
	return (WZ_SEMAPHORE *)SDL_CreateSemaphore(startValue);
}

void wzSemaphoreDestroy(WZ_SEMAPHORE *semaphore)
{
	SDL_DestroySemaphore((SDL_Semaphore *)semaphore);
}

void wzSemaphoreWait(WZ_SEMAPHORE *semaphore)
{
	SDL_WaitSemaphore((SDL_Semaphore *)semaphore);
}

bool wzSemaphoreWaitTimeout(WZ_SEMAPHORE *semaphore, int32_t timeoutMS)
{
	return SDL_WaitSemaphoreTimeout((SDL_Semaphore *)semaphore, timeoutMS);
}

void wzSemaphorePost(WZ_SEMAPHORE *semaphore)
{
	SDL_SignalSemaphore((SDL_Semaphore *)semaphore);
}

// Asynchronously executes exec->doExecOnMainThread() on the main thread
// `exec` should be a subclass of `WZ_MAINTHREADEXEC`
//
// `exec` must be allocated on the heap since the main event loop takes ownership of it
// and will handle deleting it once it has been processed.
// It is not safe to access `exec` after calling wzAsyncExecOnMainThread.
//
// No guarantees are made about when execFunc() will be called relative to the
// calling of this function - this function may return before, during, or after
// execFunc()'s execution on the main thread.
void wzAsyncExecOnMainThread(WZ_MAINTHREADEXEC *exec)
{
	Uint32 _wzSDLAppEvent = wzSDLAppEvent.load();
	assert(_wzSDLAppEvent != ((Uint32)-1));
	if (_wzSDLAppEvent == ((Uint32)-1)) {
		// The app-defined event has not yet been registered with SDL
		return;
	}
	SDL_Event execEvent;
	SDL_memset(&execEvent, 0, sizeof(execEvent));
	execEvent.type = _wzSDLAppEvent;
	execEvent.user.code = wzSDLAppEventCodes::MAINTHREADEXEC;
	execEvent.user.data1 = exec;
	assert(execEvent.user.data1 != nullptr);
	execEvent.user.data2 = 0;
	SDL_PushEvent(&execEvent);
	// receiver handles deleting `exec` on the main thread after doExecOnMainThread() has been called
}

/*!
** The keycodes we care about
**/
static inline void initKeycodes()
{
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_ESC, SDL_SCANCODE_ESCAPE));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_1, SDL_SCANCODE_1));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_2, SDL_SCANCODE_2));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_3, SDL_SCANCODE_3));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_4, SDL_SCANCODE_4));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_5, SDL_SCANCODE_5));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_6, SDL_SCANCODE_6));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_7, SDL_SCANCODE_7));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_8, SDL_SCANCODE_8));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_9, SDL_SCANCODE_9));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_0, SDL_SCANCODE_0));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_MINUS, SDL_SCANCODE_MINUS));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_EQUALS, SDL_SCANCODE_EQUALS));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_BACKSPACE, SDL_SCANCODE_BACKSPACE));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_TAB, SDL_SCANCODE_TAB));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_Q, SDL_SCANCODE_Q));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_W, SDL_SCANCODE_W));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_E, SDL_SCANCODE_E));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_R, SDL_SCANCODE_R));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_T, SDL_SCANCODE_T));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_Y, SDL_SCANCODE_Y));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_U, SDL_SCANCODE_U));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_I, SDL_SCANCODE_I));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_O, SDL_SCANCODE_O));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_P, SDL_SCANCODE_P));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_LBRACE, SDL_SCANCODE_LEFTBRACKET));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_RBRACE, SDL_SCANCODE_RIGHTBRACKET));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_RETURN, SDL_SCANCODE_RETURN));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_LCTRL, SDL_SCANCODE_LCTRL));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_A, SDL_SCANCODE_A));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_S, SDL_SCANCODE_S));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_D, SDL_SCANCODE_D));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_F, SDL_SCANCODE_F));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_G, SDL_SCANCODE_G));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_H, SDL_SCANCODE_H));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_J, SDL_SCANCODE_J));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_K, SDL_SCANCODE_K));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_L, SDL_SCANCODE_L));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_SEMICOLON, SDL_SCANCODE_SEMICOLON));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_QUOTE, SDL_SCANCODE_APOSTROPHE));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_BACKQUOTE, SDL_SCANCODE_GRAVE));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_LSHIFT, SDL_SCANCODE_LSHIFT));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_LMETA, SDL_SCANCODE_LGUI));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_LSUPER, SDL_SCANCODE_LGUI));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_BACKSLASH, SDL_SCANCODE_BACKSLASH));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_Z, SDL_SCANCODE_Z));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_X, SDL_SCANCODE_X));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_C, SDL_SCANCODE_C));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_V, SDL_SCANCODE_V));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_B, SDL_SCANCODE_B));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_N, SDL_SCANCODE_N));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_M, SDL_SCANCODE_M));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_COMMA, SDL_SCANCODE_COMMA));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_FULLSTOP, SDL_SCANCODE_PERIOD));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_FORWARDSLASH, SDL_SCANCODE_SLASH));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_RSHIFT, SDL_SCANCODE_RSHIFT));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_RMETA, SDL_SCANCODE_RGUI));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_RSUPER, SDL_SCANCODE_RGUI));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_KP_STAR, SDL_SCANCODE_KP_MULTIPLY));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_LALT, SDL_SCANCODE_LALT));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_SPACE, SDL_SCANCODE_SPACE));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_CAPSLOCK, SDL_SCANCODE_CAPSLOCK));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_F1, SDL_SCANCODE_F1));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_F2, SDL_SCANCODE_F2));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_F3, SDL_SCANCODE_F3));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_F4, SDL_SCANCODE_F4));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_F5, SDL_SCANCODE_F5));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_F6, SDL_SCANCODE_F6));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_F7, SDL_SCANCODE_F7));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_F8, SDL_SCANCODE_F8));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_F9, SDL_SCANCODE_F9));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_F10, SDL_SCANCODE_F10));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_NUMLOCK, SDL_SCANCODE_NUMLOCKCLEAR));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_SCROLLLOCK, SDL_SCANCODE_SCROLLLOCK));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_KP_7, SDL_SCANCODE_KP_7));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_KP_8, SDL_SCANCODE_KP_8));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_KP_9, SDL_SCANCODE_KP_9));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_KP_MINUS, SDL_SCANCODE_KP_MINUS));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_KP_4, SDL_SCANCODE_KP_4));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_KP_5, SDL_SCANCODE_KP_5));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_KP_6, SDL_SCANCODE_KP_6));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_KP_PLUS, SDL_SCANCODE_KP_PLUS));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_KP_1, SDL_SCANCODE_KP_1));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_KP_2, SDL_SCANCODE_KP_2));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_KP_3, SDL_SCANCODE_KP_3));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_KP_0, SDL_SCANCODE_KP_0));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_KP_FULLSTOP, SDL_SCANCODE_KP_PERIOD));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_F11, SDL_SCANCODE_F11));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_F12, SDL_SCANCODE_F12));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_RCTRL, SDL_SCANCODE_RCTRL));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_KP_BACKSLASH, SDL_SCANCODE_KP_DIVIDE));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_RALT, SDL_SCANCODE_RALT));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_HOME, SDL_SCANCODE_HOME));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_UPARROW, SDL_SCANCODE_UP));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_PAGEUP, SDL_SCANCODE_PAGEUP));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_LEFTARROW, SDL_SCANCODE_LEFT));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_RIGHTARROW, SDL_SCANCODE_RIGHT));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_END, SDL_SCANCODE_END));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_DOWNARROW, SDL_SCANCODE_DOWN));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_PAGEDOWN, SDL_SCANCODE_PAGEDOWN));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_INSERT, SDL_SCANCODE_INSERT));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_DELETE, SDL_SCANCODE_DELETE));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_KPENTER, SDL_SCANCODE_KP_ENTER));
	KEY_CODE_to_SDLScancode.insert(std::pair<KEY_CODE, SDL_Scancode>(KEY_IGNORE, SDL_Scancode(5190)));

	std::map<KEY_CODE, SDL_Scancode>::iterator it;
	for (it = KEY_CODE_to_SDLScancode.begin(); it != KEY_CODE_to_SDLScancode.end(); it++)
	{
		SDLScancode_to_KEY_CODE.insert(std::pair<SDL_Scancode, KEY_CODE>(it->second, it->first));
	}
}

static inline KEY_CODE sdlScancodeToKeyCode(SDL_Scancode key)
{
	std::map<SDL_Scancode, KEY_CODE >::iterator it;

	it = SDLScancode_to_KEY_CODE.find(key);
	if (it != SDLScancode_to_KEY_CODE.end())
	{
		return it->second;
	}
	return KEY_MAXSCAN;
}

static inline SDL_Scancode keyCodeToSDLScancode(KEY_CODE code)
{
	std::map<KEY_CODE, SDL_Scancode>::iterator it;

	it = KEY_CODE_to_SDLScancode.find(code);
	if (it != KEY_CODE_to_SDLScancode.end())
	{
		return it->second;
	}
	return (SDL_Scancode)code;
}

// Cyclic increment.
static InputKey *inputPointerNext(InputKey *p)
{
	return p + 1 == pInputBuffer + INPUT_MAXSTR ? pInputBuffer : p + 1;
}

/* add count copies of the characater code to the input buffer */
static void inputAddBuffer(UDWORD key, utf_32_char unicode)
{
	if (!GetTextEventsOwner)
	{
		// Text input events aren't enabled
		return;
	}

	/* Calculate what pEndBuffer will be set to next */
	InputKey	*pNext = inputPointerNext(pEndBuffer);

	if (pNext == pStartBuffer)
	{
		return;	// Buffer full.
	}

	// Add key to buffer.
	pEndBuffer->key = key;
	pEndBuffer->unicode = unicode;
	pEndBuffer = pNext;
}

// Returns the human-readable name for the key *in the current keyboard layout*
void keyScanToString(KEY_CODE code, char *ascii, UDWORD maxStringSize)
{
	if (code == KEY_LCTRL)
	{
		// shortcuts with modifier keys work with either key.
		strcpy(ascii, "Ctrl");
		return;
	}
	else if (code == KEY_LSHIFT)
	{
		// shortcuts with modifier keys work with either key.
		strcpy(ascii, "Shift");
		return;
	}
	else if (code == KEY_LALT)
	{
		// shortcuts with modifier keys work with either key.
		strcpy(ascii, "Alt");
		return;
	}
	else if (code == KEY_LMETA)
	{
		// shortcuts with modifier keys work with either key.
#ifdef WZ_OS_MAC
		strcpy(ascii, "Cmd");
#else
		strcpy(ascii, "Meta");
#endif
		return;
	}

	if (code < KEY_MAXSCAN)
	{
		snprintf(ascii, maxStringSize, "%s", SDL_GetKeyName(SDL_GetKeyFromScancode(keyCodeToSDLScancode(code), SDL_KMOD_NONE, false)));
		if (ascii[0] >= 'a' && ascii[0] <= 'z' && ascii[1] != 0)
		{
			// capitalize
			ascii[0] += 'A' - 'a';
			return;
		}
	}
	else
	{
		strcpy(ascii, "???");
	}
}

void mouseKeyCodeToString(const MOUSE_KEY_CODE code, char* ascii, const int maxStringLength)
{
	switch (code)
	{
	case MOUSE_KEY_CODE::MOUSE_LMB:
		strcpy(ascii, "Mouse Left");
		break;
	case MOUSE_KEY_CODE::MOUSE_MMB:
		strcpy(ascii, "Mouse Middle");
		break;
	case MOUSE_KEY_CODE::MOUSE_RMB:
		strcpy(ascii, "Mouse Right");
		break;
	case MOUSE_KEY_CODE::MOUSE_X1:
		strcpy(ascii, "Mouse 4");
		break;
	case MOUSE_KEY_CODE::MOUSE_X2:
		strcpy(ascii, "Mouse 5");
		break;
	case MOUSE_KEY_CODE::MOUSE_WUP:
		strcpy(ascii, "Mouse Wheel Up");
		break;
	case MOUSE_KEY_CODE::MOUSE_WDN:
		strcpy(ascii, "Mouse Wheel Down");
		break;
	default:
		strcpy(ascii, "Mouse ???");
		break;
	}
}

/* Initialise the input module */
void inputInitialise(void)
{
	for (unsigned int i = 0; i < KEY_MAXSCAN; i++)
	{
		aKeyState[i].state = KEY_UP;
		actualKeyState[i].state = KEY_UP;
	}

	for (unsigned int i = 0; i < MOUSE_END; i++)
	{
		aMouseState[i].state = KEY_UP;
	}

	pStartBuffer = pInputBuffer;
	pEndBuffer = pInputBuffer;

	dragX = mouseXPos = screenWidth / 2;
	dragY = mouseYPos = screenHeight / 2;
	dragKey = MOUSE_LMB;

}

/* Clear the input buffer */
void inputClearBuffer(void)
{
	pStartBuffer = pInputBuffer;
	pEndBuffer = pInputBuffer;
}

/* Return the next key press or 0 if no key in the buffer.
 * The key returned will have been remapped to the correct ascii code for the
 * windows key map.
 * All key presses are buffered up (including windows auto repeat).
 */
UDWORD inputGetKey(utf_32_char *unicode)
{
	UDWORD	 retVal;

	if (pStartBuffer == pEndBuffer)
	{
		return 0;	// Buffer empty.
	}

	retVal = pStartBuffer->key;
	if (unicode)
	{
		*unicode = pStartBuffer->unicode;
	}
	if (!retVal)
	{
		retVal = ' ';  // Don't return 0 if we got a virtual key, since that's interpreted as no input.
	}
	pStartBuffer = inputPointerNext(pStartBuffer);

	return retVal;
}

MousePresses const &inputGetClicks()
{
	return mousePresses;
}

/*!
 * This is called once a frame so that the system can tell
 * whether a key was pressed this turn or held down from the last frame.
 */
void inputNewFrame(void)
{
	// handle the keyboard
	for (unsigned int i = 0; i < KEY_MAXSCAN; i++)
	{
		if (aKeyState[i].state == KEY_PRESSED)
		{
			aKeyState[i].state = KEY_DOWN;
			actualKeyState[i].state = KEY_DOWN;
			debug(LOG_NEVER, "This key is DOWN! %x, %d [%s]", i, i, SDL_GetScancodeName(keyCodeToSDLScancode((KEY_CODE)i)));
		}
		else if (aKeyState[i].state == KEY_RELEASED  ||
		         aKeyState[i].state == KEY_PRESSRELEASE)
		{
			aKeyState[i].state = KEY_UP;
			actualKeyState[i].state = KEY_UP;
			debug(LOG_NEVER, "This key is UP! %x, %d [%s]", i, i, SDL_GetScancodeName(keyCodeToSDLScancode((KEY_CODE)i)));
		}
	}

	// handle the mouse
	for (unsigned int i = 0; i < MOUSE_END; i++)
	{
		if (aMouseState[i].state == KEY_PRESSED)
		{
			aMouseState[i].state = KEY_DOWN;
		}
		else if (aMouseState[i].state == KEY_RELEASED
		         || aMouseState[i].state == KEY_DOUBLECLICK
		         || aMouseState[i].state == KEY_PRESSRELEASE)
		{
			aMouseState[i].state = KEY_UP;
		}
	}
	mousePresses.clear();
	mouseWheelSpeed = Vector2i(0, 0);
}

/*!
 * Release all keys (and buttons) when we lose focus
 */
void inputLoseFocus(void)
{
	/* Lost the window focus, have to take this as a global key up */
	for (unsigned int i = 0; i < KEY_MAXSCAN; i++)
	{
		aKeyState[i].state = KEY_UP;
		// Do *NOT* clear actualKeyState here!
	}
	for (unsigned int i = 0; i < MOUSE_END; i++)
	{
		aMouseState[i].state = KEY_UP;
	}
}

static void restoreKeyDownState(KEY_CODE code)
{
	if (actualKeyState[code].state != KEY_UP)
	{
		aKeyState[code] = actualKeyState[code];
	}
}

void inputRestoreMetaKeyState()
{
	restoreKeyDownState(KEY_RALT);
	restoreKeyDownState(KEY_LALT);

	restoreKeyDownState(KEY_RCTRL);
	restoreKeyDownState(KEY_LCTRL);

	restoreKeyDownState(KEY_RSHIFT);
	restoreKeyDownState(KEY_LSHIFT);

	restoreKeyDownState(KEY_RMETA);
	restoreKeyDownState(KEY_LMETA);
}

/* This returns true if the key is currently depressed */
bool keyDown(KEY_CODE code)
{
	ASSERT_OR_RETURN(false, code < KEY_MAXSCAN, "Invalid keycode of %d!", (int)code);
	return (aKeyState[code].state != KEY_UP);
}

/* This returns true if the key went from being up to being down this frame */
bool keyPressed(KEY_CODE code)
{
	ASSERT_OR_RETURN(false, code < KEY_MAXSCAN, "Invalid keycode of %d!", (int)code);
	return ((aKeyState[code].state == KEY_PRESSED) || (aKeyState[code].state == KEY_PRESSRELEASE));
}

/* This returns true if the key went from being down to being up this frame */
bool keyReleased(KEY_CODE code)
{
	ASSERT_OR_RETURN(false, code < KEY_MAXSCAN, "Invalid keycode of %d!", (int)code);
	return ((aKeyState[code].state == KEY_RELEASED) || (aKeyState[code].state == KEY_PRESSRELEASE));
}

/* Return the X coordinate of the mouse */
Uint16 mouseX(void)
{
	return mouseXPos;
}

/* Return the Y coordinate of the mouse */
Uint16 mouseY(void)
{
	return mouseYPos;
}

bool wzMouseInWindow()
{
	return mouseInWindow;
}

Vector2i const& getMouseWheelSpeed()
{
	return mouseWheelSpeed;
}

Vector2i mousePressPos_DEPRECATED(MOUSE_KEY_CODE code)
{
	return aMouseState[code].pressPos;
}

Vector2i mouseReleasePos_DEPRECATED(MOUSE_KEY_CODE code)
{
	return aMouseState[code].releasePos;
}

/* This returns true if the mouse key is currently depressed */
bool mouseDown(MOUSE_KEY_CODE code)
{
	return (aMouseState[code].state != KEY_UP) ||

	       // holding down LMB and RMB counts as holding down MMB
	       (code == MOUSE_MMB && aMouseState[MOUSE_LMB].state != KEY_UP && aMouseState[MOUSE_RMB].state != KEY_UP);
}

/* This returns true if the mouse key was double clicked */
bool mouseDClicked(MOUSE_KEY_CODE code)
{
	return (aMouseState[code].state == KEY_DOUBLECLICK);
}

/* This returns true if the mouse key went from being up to being down this frame */
bool mousePressed(MOUSE_KEY_CODE code)
{
	return ((aMouseState[code].state == KEY_PRESSED) ||
	        (aMouseState[code].state == KEY_DOUBLECLICK) ||
	        (aMouseState[code].state == KEY_PRESSRELEASE));
}

/* This returns true if the mouse key went from being down to being up this frame */
bool mouseReleased(MOUSE_KEY_CODE code)
{
	return ((aMouseState[code].state == KEY_RELEASED) ||
	        (aMouseState[code].state == KEY_DOUBLECLICK) ||
	        (aMouseState[code].state == KEY_PRESSRELEASE));
}

/* Check for a mouse drag, return the drag start coords if dragging */
bool mouseDrag(MOUSE_KEY_CODE code, UDWORD *px, UDWORD *py)
{
	if ((aMouseState[code].state == KEY_DRAG) ||
	    // dragging LMB and RMB counts as dragging MMB
	    (code == MOUSE_MMB && ((aMouseState[MOUSE_LMB].state == KEY_DRAG && aMouseState[MOUSE_RMB].state != KEY_UP) ||
	                           (aMouseState[MOUSE_LMB].state != KEY_UP && aMouseState[MOUSE_RMB].state == KEY_DRAG))))
	{
		*px = dragX;
		*py = dragY;
		return true;
	}

	return false;
}

/*!
 * Handle keyboard events
 */
static void inputHandleKeyEvent(SDL_KeyboardEvent *keyEvent)
{
	switch (keyEvent->type)
	{
	case SDL_EVENT_KEY_DOWN :
	{
		unsigned vk = 0;
		switch (keyEvent->key)
		{
		// our "editing" keys for text
		case SDLK_LEFT:
			vk = INPBUF_LEFT;
			break;
		case SDLK_RIGHT:
			vk = INPBUF_RIGHT;
			break;
		case SDLK_UP:
			vk = INPBUF_UP;
			break;
		case SDLK_DOWN:
			vk = INPBUF_DOWN;
			break;
		case SDLK_HOME:
			vk = INPBUF_HOME;
			break;
		case SDLK_END:
			vk = INPBUF_END;
			break;
		case SDLK_INSERT:
			vk = INPBUF_INS;
			break;
		case SDLK_DELETE:
			vk = INPBUF_DEL;
			break;
		case SDLK_PAGEUP:
			vk = INPBUF_PGUP;
			break;
		case SDLK_PAGEDOWN:
			vk = INPBUF_PGDN;
			break;
		case SDLK_BACKSPACE:
			vk = INPBUF_BKSPACE;
			break;
		case SDLK_TAB:
			vk = INPBUF_TAB;
			break;
		case SDLK_RETURN:
			vk = INPBUF_CR;
			break;
		case SDLK_ESCAPE:
			vk = INPBUF_ESC;
			break;
		default:
			break;
		}

		if (vk)
		{
			// Take care of adding 'editing' keys that were pressed to the input buffer (for text editing control handling)
			inputAddBuffer(vk, 0);
			debug(LOG_INPUT, "Editing key: 0x%x, %d SDLkey=[%s] pressed", vk, vk,
			      SDL_GetKeyName(keyEvent->key));
		}
		else
		{
			// add everything else
			inputAddBuffer(keyEvent->key, 0);
		}

		SDL_Scancode currentKey = keyEvent->scancode;
		debug(LOG_INPUT, "Key Code (pressed): 0x%x, %d, SDLscancode=[%s]", currentKey, currentKey, SDL_GetScancodeName(currentKey));

		KEY_CODE code = sdlScancodeToKeyCode(currentKey);
		if (code >= KEY_MAXSCAN)
		{
			break;
		}
		if (aKeyState[code].state == KEY_UP ||
		    aKeyState[code].state == KEY_RELEASED ||
		    aKeyState[code].state == KEY_PRESSRELEASE)
		{
			// whether double key press or not
			aKeyState[code].state = KEY_PRESSED;
			aKeyState[code].lastdown = 0;
			actualKeyState[code].state = KEY_PRESSED;
			actualKeyState[code].lastdown = 0;
		}
		break;
	}

	case SDL_EVENT_KEY_UP :
	{
		SDL_Scancode currentKey = keyEvent->scancode;
		debug(LOG_INPUT, "Key Code (*Depressed*): 0x%x, %d, SDLscancode=[%s]", currentKey, currentKey, SDL_GetScancodeName(currentKey));
		KEY_CODE code = sdlScancodeToKeyCode(currentKey);
		if (code >= KEY_MAXSCAN)
		{
			break;
		}
		actualKeyState[code].state = KEY_UP;
		if (aKeyState[code].state == KEY_PRESSED)
		{
			aKeyState[code].state = KEY_PRESSRELEASE;
		}
		else if (aKeyState[code].state == KEY_DOWN)
		{
			aKeyState[code].state = KEY_RELEASED;
		}
		break;
	}
	default:
		break;
	}
}

/*!
 * Handle text events (if we were to use SDL2)
*/
void inputhandleText(SDL_TextInputEvent *Tevent)
{
	size_t size = SDL_strlen(Tevent->text);
	if (size)
	{
		if (utf8Buf)
		{
			// clean up memory from last use.
			free(utf8Buf);
			utf8Buf = nullptr;
		}
		size_t newtextsize = 0;
		utf8Buf = UTF8toUTF32(Tevent->text, &newtextsize);
		debug(LOG_INPUT, "Keyboard: text input \"%s\"", Tevent->text);
		for (unsigned i = 0; i < newtextsize / sizeof(utf_32_char); ++i)
		{
			inputAddBuffer(0, utf8Buf[i]);
		}
	}
}

/*!
 * Handle mouse wheel events
 */
static void inputHandleMouseWheelEvent(SDL_MouseWheelEvent *wheel)
{
	mouseWheelSpeed += Vector2i(wheel->integer_x, wheel->integer_y);

	if (wheel->x > 0 || wheel->y > 0)
	{
		aMouseState[MOUSE_WUP].state = KEY_PRESSED;
		aMouseState[MOUSE_WUP].lastdown = 0;
	}
	else if (wheel->x < 0 || wheel->y < 0)
	{
		aMouseState[MOUSE_WDN].state = KEY_PRESSED;
		aMouseState[MOUSE_WDN].lastdown = 0;
	}
}

/*!
 * Handle mouse button events (We can handle up to 5)
 */
static void inputHandleMouseButtonEvent(SDL_MouseButtonEvent *buttonEvent)
{
	mouseXPos = (int)((float)buttonEvent->x / current_displayScaleFactor);
	mouseYPos = (int)((float)buttonEvent->y / current_displayScaleFactor);

	MOUSE_KEY_CODE mouseKeyCode;
	switch (buttonEvent->button)
	{
	case SDL_BUTTON_LEFT: mouseKeyCode = MOUSE_LMB; break;
	case SDL_BUTTON_MIDDLE: mouseKeyCode = MOUSE_MMB; break;
	case SDL_BUTTON_RIGHT: mouseKeyCode = MOUSE_RMB; break;
	case SDL_BUTTON_X1: mouseKeyCode = MOUSE_X1; break;
	case SDL_BUTTON_X2: mouseKeyCode = MOUSE_X2; break;
	default: return;  // Unknown button.
	}

	MousePress mousePress;
	mousePress.key = mouseKeyCode;
	mousePress.pos = Vector2i(mouseXPos, mouseYPos);

	switch (buttonEvent->type)
	{
	case SDL_EVENT_MOUSE_BUTTON_DOWN :
		mousePress.action = MousePress::Press;
		mousePresses.push_back(mousePress);

		aMouseState[mouseKeyCode].pressPos.x = mouseXPos;
		aMouseState[mouseKeyCode].pressPos.y = mouseYPos;
		if (aMouseState[mouseKeyCode].state == KEY_UP
		    || aMouseState[mouseKeyCode].state == KEY_RELEASED
		    || aMouseState[mouseKeyCode].state == KEY_PRESSRELEASE)
		{
			// whether double click or not
			if (realTime - aMouseState[mouseKeyCode].lastdown < DOUBLE_CLICK_INTERVAL)
			{
				aMouseState[mouseKeyCode].state = KEY_DOUBLECLICK;
				aMouseState[mouseKeyCode].lastdown = 0;
			}
			else
			{
				aMouseState[mouseKeyCode].state = KEY_PRESSED;
				aMouseState[mouseKeyCode].lastdown = realTime;
			}

			if (mouseKeyCode < MOUSE_X1) // Assume they are draggin' with either LMB|RMB|MMB
			{
				dragKey = mouseKeyCode;
				dragX = mouseXPos;
				dragY = mouseYPos;
			}
		}
		break;
	case SDL_EVENT_MOUSE_BUTTON_UP :
		mousePress.action = MousePress::Release;
		mousePresses.push_back(mousePress);

		aMouseState[mouseKeyCode].releasePos.x = mouseXPos;
		aMouseState[mouseKeyCode].releasePos.y = mouseYPos;
		if (aMouseState[mouseKeyCode].state == KEY_PRESSED)
		{
			aMouseState[mouseKeyCode].state = KEY_PRESSRELEASE;
		}
		else if (aMouseState[mouseKeyCode].state == KEY_DOWN
		         || aMouseState[mouseKeyCode].state == KEY_DRAG
		         || aMouseState[mouseKeyCode].state == KEY_DOUBLECLICK)
		{
			aMouseState[mouseKeyCode].state = KEY_RELEASED;
		}
		break;
	default:
		break;
	}
}

/*!
 * Handle mousemotion events
 */
static void inputHandleMouseMotionEvent(SDL_MouseMotionEvent *motionEvent)
{
	switch (motionEvent->type)
	{
	case SDL_EVENT_MOUSE_MOTION :
		/* store the current mouse position */
		mouseXPos = (int)((float)motionEvent->x / current_displayScaleFactor);
		mouseYPos = (int)((float)motionEvent->y / current_displayScaleFactor);

		/* now see if a drag has started */
		if ((aMouseState[dragKey].state == KEY_PRESSED ||
		     aMouseState[dragKey].state == KEY_DOWN) &&
		    (ABSDIF(dragX, mouseXPos) > DRAG_THRESHOLD ||
		     ABSDIF(dragY, mouseYPos) > DRAG_THRESHOLD))
		{
			aMouseState[dragKey].state = KEY_DRAG;
		}
		break;
	default:
		break;
	}
}

static int copied_argc = 0;
static char** copied_argv = nullptr;

// This stage, we only setup keycodes, and copy argc & argv for later use initializing stuff.
void wzMain(int &argc, char **argv)
{
	initKeycodes();

	// Create copies of argc and arv (for later use initializing QApplication for the script engine)
	copied_argv = new char*[argc+1];
	for(int i=0; i < argc; i++) {
		size_t len = strlen(argv[i]) + 1;
		copied_argv[i] = new char[len];
		memcpy(copied_argv[i], argv[i], len);
	}
	copied_argv[argc] = NULL;
	copied_argc = argc;
}

#define MIN_WZ_GAMESCREEN_WIDTH 640
#define MIN_WZ_GAMESCREEN_HEIGHT 480

void handleGameScreenSizeChange(unsigned int oldWidth, unsigned int oldHeight, unsigned int newWidth, unsigned int newHeight)
{
	screenWidth = newWidth;
	screenHeight = newHeight;

	pie_SetVideoBufferWidth(screenWidth);
	pie_SetVideoBufferHeight(screenHeight);
	pie_UpdateSurfaceGeometry();

	if (currentScreenResizingStatus == nullptr)
	{
		// The screen size change details are stored in scaled, logical units (points)
		// i.e. the values expect by the game engine.
		currentScreenResizingStatus = new ScreenSizeChange(oldWidth, oldHeight, screenWidth, screenHeight);
	}
	else
	{
		// update the new screen width / height, in case more than one resize message is processed this event loop
		currentScreenResizingStatus->newWidth = screenWidth;
		currentScreenResizingStatus->newHeight = screenHeight;
	}
}

void handleWindowSizeChange(unsigned int oldWidth, unsigned int oldHeight, unsigned int newWidth, unsigned int newHeight)
{
	windowWidth = newWidth;
	windowHeight = newHeight;

	// NOTE: This function receives the window size in the window's logical units, but not accounting for the interface scale factor.
	// Therefore, the provided old/newWidth/Height must be divided by the interface scale factor to calculate the new
	// *game* screen logical width / height.
	unsigned int oldScreenWidth = static_cast<unsigned int>(oldWidth / current_displayScaleFactor);
	unsigned int oldScreenHeight = static_cast<unsigned int>(oldHeight / current_displayScaleFactor);
	unsigned int newScreenWidth = static_cast<unsigned int>(newWidth / current_displayScaleFactor);
	unsigned int newScreenHeight = static_cast<unsigned int>(newHeight / current_displayScaleFactor);

	handleGameScreenSizeChange(oldScreenWidth, oldScreenHeight, newScreenWidth, newScreenHeight);

	gfx_api::context::get().handleWindowSizeChange(oldWidth, oldHeight, newWidth, newHeight);
}


void wzGetMinimumWindowSizeForDisplayScaleFactor(unsigned int *minWindowWidth, unsigned int *minWindowHeight, float displayScaleFactor = current_displayScaleFactor)
{
	if (minWindowWidth != nullptr)
	{
		*minWindowWidth = (int)ceil(MIN_WZ_GAMESCREEN_WIDTH * displayScaleFactor);
	}
	if (minWindowHeight != nullptr)
	{
		*minWindowHeight = (int)ceil(MIN_WZ_GAMESCREEN_HEIGHT * displayScaleFactor);
	}
}

void wzGetMaximumDisplayScaleFactorsForWindowSize(unsigned int width, unsigned int height, float *horizScaleFactor, float *vertScaleFactor)
{
	if (horizScaleFactor != nullptr)
	{
		*horizScaleFactor = (float)width / (float)MIN_WZ_GAMESCREEN_WIDTH;
	}
	if (vertScaleFactor != nullptr)
	{
		*vertScaleFactor = (float)height / (float)MIN_WZ_GAMESCREEN_HEIGHT;
	}
}

float wzGetMaximumDisplayScaleFactorForWindowSize(unsigned int width, unsigned int height)
{
	float maxHorizScaleFactor = 0.f, maxVertScaleFactor = 0.f;
	wzGetMaximumDisplayScaleFactorsForWindowSize(width, height, &maxHorizScaleFactor, &maxVertScaleFactor);
	return std::min(maxHorizScaleFactor, maxVertScaleFactor);
}

// returns: the maximum display scale percentage (sourced from wzAvailableDisplayScales), or 0 if window is below the minimum required size for the minimum supported display scale
unsigned int wzGetMaximumDisplayScaleForWindowSize(unsigned int width, unsigned int height)
{
	float maxDisplayScaleFactor = wzGetMaximumDisplayScaleFactorForWindowSize(width, height);
	unsigned int maxDisplayScalePercentage = static_cast<unsigned int>(floor(maxDisplayScaleFactor * 100.f));

	auto availableDisplayScales = wzAvailableDisplayScales();
	std::sort(availableDisplayScales.begin(), availableDisplayScales.end());

	auto maxDisplayScale = std::lower_bound(availableDisplayScales.begin(), availableDisplayScales.end(), maxDisplayScalePercentage);
	if (maxDisplayScale == availableDisplayScales.end())
	{
		// return the largest available display scale
		return availableDisplayScales.back();
	}
	if (*maxDisplayScale != maxDisplayScalePercentage)
	{
		if (maxDisplayScale == availableDisplayScales.begin())
		{
			// no lower display scale to return
			return 0;
		}
		--maxDisplayScale;
	}
	return *maxDisplayScale;
}

unsigned int wzGetMaximumDisplayScaleForCurrentWindowSize()
{
	return wzGetMaximumDisplayScaleForWindowSize(windowWidth, windowHeight);
}

unsigned int wzGetSuggestedDisplayScaleForCurrentWindowSize(unsigned int desiredMaxScreenDimension)
{
	unsigned int maxDisplayScale = wzGetMaximumDisplayScaleForCurrentWindowSize();

	auto availableDisplayScales = wzAvailableDisplayScales();
	std::sort(availableDisplayScales.begin(), availableDisplayScales.end());

	for (auto it = availableDisplayScales.begin(); it != availableDisplayScales.end() && (*it <= maxDisplayScale); it++)
	{
		auto resultingLogicalScreenWidth = (windowWidth * 100) / *it;
		auto resultingLogicalScreenHeight = (windowHeight * 100) / *it;
		if (resultingLogicalScreenWidth <= desiredMaxScreenDimension && resultingLogicalScreenHeight <= desiredMaxScreenDimension)
		{
			return *it;
		}
	}

	return maxDisplayScale;
}

bool wzWindowSizeIsSmallerThanMinimumRequired(unsigned int width, unsigned int height, float displayScaleFactor = current_displayScaleFactor)
{
	unsigned int minWindowWidth = 0, minWindowHeight = 0;
	wzGetMinimumWindowSizeForDisplayScaleFactor(&minWindowWidth, &minWindowHeight, displayScaleFactor);
	return ((width < minWindowWidth) || (height < minWindowHeight));
}

void processScreenSizeChangeNotificationIfNeeded()
{
	if (currentScreenResizingStatus != nullptr)
	{
		// WZ must process the screen size change
		screen_updateGeometry(); // must come after gfx_api::context::handleWindowSizeChange
		gameScreenSizeDidChange(currentScreenResizingStatus->oldWidth, currentScreenResizingStatus->oldHeight, currentScreenResizingStatus->newWidth, currentScreenResizingStatus->newHeight);
		delete currentScreenResizingStatus;
		currentScreenResizingStatus = nullptr;
	}
}

float wzGetDisplayContentScale()
{
	float defaultContentScale = SDL_GetDisplayContentScale(screenIndex > 0 ? screenIndex : SDL_GetPrimaryDisplay());
	if (defaultContentScale < 1.f)
	{
		return 1.f;
	}
	return defaultContentScale;
}

unsigned int wzGetDefaultBaseDisplayScale(int displayIndex)
{
	float defaultContentScale = SDL_GetDisplayContentScale(displayIndex);
	debug(LOG_WZ, "GetDisplayContentScale(%d)=%f", displayIndex, defaultContentScale);
	if (defaultContentScale <= 0.f)
	{
		return 100;
	}
	else
	{
		// This is an system-configured value that _could_ be applied *IN ADDITION TO* the user-configured display scaling value for WZ
		// But for now, simply apply it as the default display scale
		unsigned int approxActualDisplayScale = std::max<unsigned int>(static_cast<unsigned int>(ceilf(defaultContentScale * 100.f)), 100);
		auto availableDisplayScales = wzAvailableDisplayScales();
		std::sort(availableDisplayScales.begin(), availableDisplayScales.end());
		auto displayScale = std::lower_bound(availableDisplayScales.begin(), availableDisplayScales.end(), approxActualDisplayScale);
		if (displayScale == availableDisplayScales.end())
		{
			// return the largest available display scale
			return availableDisplayScales.back();
		}
		if (*displayScale != approxActualDisplayScale)
		{
			if (displayScale == availableDisplayScales.begin())
			{
				// no lower display scale to return
				return 100;
			}
			--displayScale;
		}
		return *displayScale;
	}
}

bool wzChangeDisplayScale(unsigned int displayScale)
{
	if (WZwindow == nullptr)
	{
		debug(LOG_WARNING, "wzChangeDisplayScale called when window is not available");
		return false;
	}

	float newDisplayScaleFactor = (float)displayScale / 100.f;

	if (wzWindowSizeIsSmallerThanMinimumRequired(windowWidth, windowHeight, newDisplayScaleFactor))
	{
		// The current window width and/or height are below the required minimum window size
		// for this display scale factor.
		return false;
	}

	// Store the new display scale factor
	setDisplayScale(displayScale);

	// Set the new minimum window size
	if (wzGetCurrentWindowMode() == WINDOW_MODE::windowed)
	{
		unsigned int minWindowWidth = 0, minWindowHeight = 0;
		wzGetMinimumWindowSizeForDisplayScaleFactor(&minWindowWidth, &minWindowHeight, newDisplayScaleFactor);
		SDL_SetWindowMinimumSize(WZwindow, minWindowWidth, minWindowHeight);
	}

	// Update the game's logical screen size
	unsigned int oldScreenWidth = screenWidth, oldScreenHeight = screenHeight;
	unsigned int newScreenWidth = windowWidth, newScreenHeight = windowHeight;
	if (newDisplayScaleFactor > 1.0f)
	{
		newScreenWidth = static_cast<unsigned int>(windowWidth / newDisplayScaleFactor);
		newScreenHeight = static_cast<unsigned int>(windowHeight / newDisplayScaleFactor);
	}
	handleGameScreenSizeChange(oldScreenWidth, oldScreenHeight, newScreenWidth, newScreenHeight);
	gameDisplayScaleFactorDidChange(newDisplayScaleFactor);

	// Update the current mouse coordinates
	// (The prior stored mouseXPos / mouseYPos apply to the old coordinate system, and must be translated to the
	// new game coordinate system. Since the mouse hasn't moved - or it would generate events that override this -
	// the current position with respect to the window (which hasn't changed size) can be queried and used to
	// calculate the new game coordinate system mouse position.)
	//
	float windowMouseXPos = 0.0, windowMouseYPos = 0.0;
	SDL_GetMouseState(&windowMouseXPos, &windowMouseYPos);
	debug(LOG_WZ, "Old mouse position: %d, %d", mouseXPos, mouseYPos);
	mouseXPos = (int)((float)windowMouseXPos / current_displayScaleFactor);
	mouseYPos = (int)((float)windowMouseYPos / current_displayScaleFactor);
	debug(LOG_WZ, "New mouse position: %d, %d", mouseXPos, mouseYPos);


	processScreenSizeChangeNotificationIfNeeded();

	return true;
}

bool wzChangeCursorScale(unsigned int cursorScale)
{
	if (WZwindow == nullptr)
	{
		debug(LOG_WARNING, "wzChangeCursorScale called when window is not available");
		return false;
	}

	if (cursorScale == war_getCursorScale())
	{
		return true;
	}

	war_setCursorScale(cursorScale); // must be set before cursors are reinit
	wzSDLReinitCursors();
	return true;
}

bool wzReduceDisplayScalingIfNeeded(int currWidth, int currHeight)
{
	// Check whether the desired window size is smaller than the minimum required for the current Display Scale
	if (wzWindowSizeIsSmallerThanMinimumRequired(currWidth, currHeight))
	{
		// The new window size is smaller than the minimum required size for the current display scale level.

		unsigned int maxDisplayScale = wzGetMaximumDisplayScaleForWindowSize(currWidth, currHeight);
		if (maxDisplayScale < 100)
		{
			// Cannot adjust display scale factor below 1. Desired window size is below the minimum supported.
			debug(LOG_WZ, "Size (%d x %d) is smaller than the minimum supported at a 100%% display scale", currWidth, currHeight);
			return false;
		}

		// Adjust the current display scale level to the nearest supported level.
		debug(LOG_WZ, "The current Display Scale (%d%%) is too high for the desired window size. Reducing the current Display Scale to the maximum possible for the desired window size: %d%%.", current_displayScale, maxDisplayScale);
		wzChangeDisplayScale(maxDisplayScale);

		// Store the new display scale
		war_SetDisplayScale(maxDisplayScale);
	}

	return true;
}

bool wzChangeFullscreenDisplayMode(optional<screeninfo> config)
{
	if (WZwindow == nullptr)
	{
		debug(LOG_WARNING, "wzChangeFullscreenDisplayMode called when window is not available");
		return false;
	}

	if (!config.has_value())
	{
		debug(LOG_INFO, "Changing fullscreen mode to Desktop Full");
		if (!SDL_SetWindowFullscreenMode(WZwindow, nullptr))
		{
			debug(LOG_INFO, "Unable to set desktop fullscreen mode?: %s", SDL_GetError());
		}

		war_SetFullscreenModeScreen(0);
		war_SetFullscreenModeWidth(0);
		war_SetFullscreenModeHeight(0);
		war_SetFullscreenModePixelDensity(1.0);
		war_SetFullscreenModeRefreshRate(0.f);
		return true;
	}

	screeninfo& request = config.value();
	debug(LOG_INFO, "Changing fullscreen mode to [%u] %dx%d", request.screen, request.width, request.height);

	SDL_DisplayMode closest;
	if (request.width == 0 || request.height == 0)
	{
		debug(LOG_INFO, "Getting desktop display mode");
		const SDL_DisplayMode *desktopDisplayMode = SDL_GetDesktopDisplayMode((request.screen > 0) ? request.screen : SDL_GetDisplayForWindow(WZwindow));
		if (desktopDisplayMode == nullptr)
		{
			debug(LOG_INFO, "Unable to get desktop DisplayMode?: %s", SDL_GetError());
			return false;
		}
		request.screen = desktopDisplayMode->displayID;
		request.width = desktopDisplayMode->w;
		request.height = desktopDisplayMode->h;
		request.pixel_density = desktopDisplayMode->pixel_density;
		request.refresh_rate = desktopDisplayMode->refresh_rate;
	}

	if (!SDL_GetClosestFullscreenDisplayMode(request.screen, request.width, request.height, request.refresh_rate, (request.pixel_density > 1.0f), &closest))
	{
		// no match was found
		debug(LOG_INFO, "No closest DisplayMode found ([%u] [%u x %u]); error: %s", request.screen, request.width, request.height, SDL_GetError());
		return false;
	}

	if ((closest.w < MIN_WZ_GAMESCREEN_WIDTH) || (closest.h < MIN_WZ_GAMESCREEN_HEIGHT))
	{
		debug(LOG_INFO, "Closest fullscreen mode minimum logical resolution < %d x %d", MIN_WZ_GAMESCREEN_WIDTH, MIN_WZ_GAMESCREEN_HEIGHT);
		return false;
	}

	if (!SDL_SetWindowFullscreenMode(WZwindow, &closest))
	{
		// SDL_SetWindowFullscreenMode failed
		debug(LOG_INFO, "SDL_SetWindowFullscreenMode ([%u] [%u x %u]) failed: %s", request.screen, request.width, request.height, SDL_GetError());
		return false;
	}

	// Note: Reducing display scaling if needed is handled in the window resize event

	war_SetFullscreenModeScreen(closest.displayID);
	war_SetFullscreenModeWidth(closest.w);
	war_SetFullscreenModeHeight(closest.h);
	war_SetFullscreenModePixelDensity(closest.pixel_density);
	war_SetFullscreenModeRefreshRate(closest.refresh_rate);

	return true;
}

static bool wzSetDefaultFullscreenDisplayMode()
{
#if defined(__EMSCRIPTEN__)
	// Always Desktop Full mode for Emscripten
	return wzChangeFullscreenDisplayMode(nullopt);
#endif

	screeninfo info;
	info.screen = war_GetFullscreenModeScreen();
	info.width = war_GetFullscreenModeWidth();
	info.height = war_GetFullscreenModeHeight();
	info.pixel_density = war_GetFullscreenModePixelDensity();
	info.refresh_rate = war_GetFullscreenModeRefreshRate();

	if (info.width <= 0 || info.height <= 0)
	{
		// Desktop Full mode
		return wzChangeFullscreenDisplayMode(nullopt);
	}

	return wzChangeFullscreenDisplayMode(info);
}

bool wzChangeWindowResolution(int screen, unsigned int width, unsigned int height)
{
	if (WZwindow == nullptr)
	{
		debug(LOG_WARNING, "wzChangeWindowResolution called when window is not available");
		return false;
	}
	debug(LOG_WZ, "Attempt to change resolution to [%d] %dx%d", screen, width, height);

#if defined(WZ_OS_MAC)
	// Workaround for SDL (2.0.5) quirk on macOS:
	//	When the green titlebar button is used to fullscreen the app in a new space:
	//		- SDL does not return SDL_WINDOW_MAXIMIZED nor SDL_WINDOW_FULLSCREEN.
	//		- Attempting to change the window resolution "succeeds" (in that the new window size is "set" and returned
	//		  by the SDL GetWindowSize functions).
	//		- But other things break (ex. mouse coordinate translation) if the resolution is changed while the window
	//        is maximized in this way.
	//		- And the GL drawable size remains unchanged.
	//		- So if it's been fullscreened by the user like this, but doesn't show as SDL_WINDOW_FULLSCREEN,
	//		  prevent window resolution changes.
	if (cocoaIsSDLWindowFullscreened(WZwindow) && !wzIsFullscreen())
	{
		debug(LOG_WZ, "The main window is fullscreened, but SDL doesn't think it is. Changing window resolution is not possible in this state. (SDL Bug).");
		return false;
	}
#endif

	// Get current window size + position + bounds
	int prev_x = 0, prev_y = 0, prev_width = 0, prev_height = 0;
	SDL_GetWindowPosition(WZwindow, &prev_x, &prev_y);
	SDL_GetWindowSize(WZwindow, &prev_width, &prev_height);

	// Get the usable bounds for the current screen
	SDL_Rect bounds;
	if (wzIsFullscreen())
	{
		// When in fullscreen mode, obtain the screen's overall bounds
		if (!SDL_GetDisplayBounds(screen, &bounds)) {
			debug(LOG_ERROR, "Failed to get display bounds for screen: %d", screen);
			return false;
		}
		debug(LOG_WZ, "SDL_GetDisplayBounds for screen [%d]: pos %d x %d : res %d x %d", screen, (int)bounds.x, (int)bounds.y, (int)bounds.w, (int)bounds.h);
	}
	else
	{
		// When in windowed mode, obtain the screen's *usable* display bounds
		if (!SDL_GetDisplayUsableBounds(screen, &bounds)) {
			debug(LOG_ERROR, "Failed to get usable display bounds for screen: %d", screen);
			return false;
		}
		debug(LOG_WZ, "SDL_GetDisplayUsableBounds for screen [%d]: pos %d x %d : WxH %d x %d", screen, (int)bounds.x, (int)bounds.y, (int)bounds.w, (int)bounds.h);

		// Verify that the desired window size does not exceed the usable bounds of the specified display.
		if ((width > bounds.w) || (height > bounds.h))
		{
			debug(LOG_WZ, "Unable to change window size to (%d x %d) because it is larger than the screen's usable bounds", width, height);
			return false;
		}
	}

	// Check whether the desired window size is smaller than the minimum required for the current Display Scale
	unsigned int priorDisplayScale = current_displayScale;
	if (wzWindowSizeIsSmallerThanMinimumRequired(width, height))
	{
		// The new window size is smaller than the minimum required size for the current display scale level.

		unsigned int maxDisplayScale = wzGetMaximumDisplayScaleForWindowSize(width, height);
		if (maxDisplayScale < 100)
		{
			// Cannot adjust display scale factor below 1. Desired window size is below the minimum supported.
			debug(LOG_WZ, "Unable to change window size to (%d x %d) because it is smaller than the minimum supported at a 100%% display scale", width, height);
			return false;
		}

		// Adjust the current display scale level to the nearest supported level.
		debug(LOG_WZ, "The current Display Scale (%d%%) is too high for the desired window size. Reducing the current Display Scale to the maximum possible for the desired window size: %d%%.", current_displayScale, maxDisplayScale);
		wzChangeDisplayScale(maxDisplayScale);

		// Store the new display scale
		war_SetDisplayScale(maxDisplayScale);
	}

	// Position the window (centered) on the screen (for its upcoming new size)
	SDL_SetWindowPosition(WZwindow, SDL_WINDOWPOS_CENTERED_DISPLAY(screen), SDL_WINDOWPOS_CENTERED_DISPLAY(screen));

	// Change the window size
	// NOTE: Changing the window size will trigger an SDL window size changed event which will handle recalculating layout.
	SDL_SetWindowSize(WZwindow, width, height);

	// Check that the new size is the desired size
	int resultingWidth, resultingHeight = 0;
	SDL_GetWindowSize(WZwindow, &resultingWidth, &resultingHeight);
	if (resultingWidth != width || resultingHeight != height) {
		// Attempting to set the resolution failed
		debug(LOG_WZ, "Attempting to change the resolution to %dx%d seems to have failed (result: %dx%d).", width, height, resultingWidth, resultingHeight);

		// Revert to the prior position + resolution + display scale, and return false
		SDL_SetWindowSize(WZwindow, prev_width, prev_height);
		SDL_SetWindowPosition(WZwindow, prev_x, prev_y);
		if (current_displayScale != priorDisplayScale)
		{
			// Reverse the correction applied to the Display Scale to support the desired resolution.
			wzChangeDisplayScale(priorDisplayScale);
			war_SetDisplayScale(priorDisplayScale);
		}
		return false;
	}

	// Store the updated screenIndex
	screenIndex = screen;

	return true;
}

MinimizeOnFocusLossBehavior wzGetCurrentMinimizeOnFocusLossBehavior()
{
	const char* hint = SDL_GetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS);
	if (!hint || !*hint || SDL_strcasecmp(hint, "auto") == 0)
	{
		return MinimizeOnFocusLossBehavior::Auto;
	}
	bool bValue = SDL_GetHintBoolean(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS,
					 false);
	return (bValue) ? MinimizeOnFocusLossBehavior::On_Fullscreen : MinimizeOnFocusLossBehavior::Off;
}

void wzSetMinimizeOnFocusLoss(MinimizeOnFocusLossBehavior behavior)
{
#if !defined(__EMSCRIPTEN__)
	const char* value = "auto";
	switch (behavior)
	{
		case MinimizeOnFocusLossBehavior::Off:
			value = "0";
			break;
		case MinimizeOnFocusLossBehavior::On_Fullscreen:
			value = "1";
			break;
		case MinimizeOnFocusLossBehavior::Auto:
			value = "auto";
			break;
		default:
			return;
	}
	SDL_SetHintWithPriority(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, value, SDL_HINT_OVERRIDE);
#else
	// no-op on Emscripten
#endif
}

// Returns the current window screen, width, and height
void wzGetWindowResolution(int *screen, unsigned int *width, unsigned int *height)
{
	ASSERT_OR_RETURN(, WZwindow != nullptr, "wzGetWindowResolution called when window is not available");

	if (screen != nullptr)
	{
		*screen = screenIndex;
	}

	int currentWidth = 0, currentHeight = 0;
	SDL_GetWindowSize(WZwindow, &currentWidth, &currentHeight);
	assert(currentWidth >= 0);
	assert(currentHeight >= 0);
	if (width != nullptr)
	{
		*width = currentWidth;
	}
	if (height != nullptr)
	{
		*height = currentHeight;
	}
}

static void WZ_SDL_setBackendProperty(SDL_PropertiesID props, const video_backend& backend)
{
	switch (backend)
	{
#if defined(WZ_BACKEND_DIRECTX)
		case video_backend::directx: // because DirectX is supported via OpenGLES (LibANGLE)
#endif
		case video_backend::opengl:
		case video_backend::opengles:
			SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_OPENGL_BOOLEAN, true);
			return;
		case video_backend::vulkan:
			SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_VULKAN_BOOLEAN, true);
			return;
		case video_backend::num_backends:
			debug(LOG_FATAL, "Should never happen");
			break;
	}
}

bool shouldResetGfxBackendPrompt_internal(video_backend currentBackend, video_backend newBackend, std::string failureVerb = "initialize", std::string failedToInitializeObject = "graphics", std::string additionalErrorDetails = "")
{
	// Offer to reset to the specified gfx backend
	std::string resetString = std::string("Reset to ") + to_display_string(newBackend) + "";
	const SDL_MessageBoxButtonData buttons[] = {
	   { SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, 1, resetString.c_str() },
	   { SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, 2, "Not Now" },
	};
	std::string titleString = std::string("Warzone: Failed to ") + failureVerb + " " + failedToInitializeObject;
	std::string messageString = std::string("Failed to ") + failureVerb + " " + failedToInitializeObject + " for backend: " + to_display_string(currentBackend) + ".\n\n";
	if (!additionalErrorDetails.empty())
	{
		messageString += "Error Details: \n\"" + additionalErrorDetails + "\"\n\n";
	}
	messageString += "Do you want to reset the graphics backend to: " + to_display_string(newBackend) + "?";
	const SDL_MessageBoxData messageboxdata = {
		SDL_MESSAGEBOX_ERROR, /* .flags */
		WZwindow, /* .window */
		titleString.c_str(), /* .title */
		messageString.c_str(), /* .message */
		SDL_arraysize(buttons), /* .numbuttons */
		buttons, /* .buttons */
		nullptr /* .colorScheme */
	};
	int buttonid;
	if (!SDL_ShowMessageBox(&messageboxdata, &buttonid)) {
		// error displaying message box
		debug(LOG_FATAL, "Failed to display message box");
		return false;
	}
	if (buttonid == 1)
	{
		return true;
	}
	return false;
}

bool shouldResetGfxBackendPrompt(video_backend currentBackend, video_backend newBackend, std::string failedToInitializeObject = "graphics", std::string additionalErrorDetails = "")
{
	return shouldResetGfxBackendPrompt_internal(currentBackend, newBackend, "initialize", failedToInitializeObject, additionalErrorDetails);
}

void resetGfxBackend(video_backend newBackend, bool displayRestartMessage = true)
{
	war_setGfxBackend(newBackend);
	if (displayRestartMessage)
	{
		std::string title = std::string("Backend reset to: ") + to_display_string(newBackend);
		wzDisplayDialog(Dialog_Information, title.c_str(), "(Note: Do not specify a --gfxbackend option, or it will override this new setting.)\n\nPlease restart Warzone 2100 to use the new graphics setting.");
	}
}

void wzResetGfxSettingsOnFailure()
{
	// Because too high an MSAA value could lead to out-of-memory errors (for example, trying to create the scene renderpass), reset MSAA to off for next run
	war_setAntialiasing(0);

	saveGfxConfig();
}

void wzDisplayFatalGfxBackendFailure(const std::string& additionalErrorDetails)
{
	std::string backendString;
	if (WZbackend.has_value())
	{
		backendString = to_display_string(WZbackend.value());
	}

	// Display message that there was a failure with the gfx backend
	std::string title = std::string("Graphics Error: ") + backendString;
	std::string messageString = std::string("An error occured with graphics backend: ") + backendString + ".\n\n";
	if (!additionalErrorDetails.empty())
	{
		messageString += "Error Details: \n\"" + additionalErrorDetails + "\"\n\n";
	}
	messageString += "Warzone 2100 will now close.";
	wzDisplayDialog(Dialog_Error, title.c_str(), messageString.c_str());
}

bool wzPromptToChangeGfxBackendOnFailure(const std::string& additionalErrorDetails /*= ""*/)
{
	if (!WZbackend.has_value())
	{
		ASSERT(false, "Can't reset gfx backend when there is no gfx backend.");
		return false;
	}
	video_backend defaultBackend = wzGetNextFallbackGfxBackendForCurrentSystem(WZbackend.value());
	if (WZbackend.value() != defaultBackend)
	{
		if (shouldResetGfxBackendPrompt_internal(WZbackend.value(), defaultBackend, "draw", "graphics", additionalErrorDetails))
		{
			resetGfxBackend(defaultBackend);
			saveGfxConfig(); // must force-persist the new value before returning!
		}
		return true; // handled by this function (prompted user)
	}
	return false;
}

bool wzSDLOneTimeInit()
{
	const Uint32 sdl_init_flags = SDL_INIT_EVENTS;
	if (!(SDL_WasInit(sdl_init_flags) == sdl_init_flags))
	{
		if (!SDL_Init(sdl_init_flags))
		{
			debug(LOG_ERROR, "Error: Could not initialise SDL (%s).", SDL_GetError());
			return false;
		}
	}

	if (wzSDLAppEvent == ((Uint32)-1))
	{
		wzSDLAppEvent = SDL_RegisterEvents(1);
		if (wzSDLAppEvent == ((Uint32)-1))
		{
			// Failed to register app-defined event with SDL
			debug(LOG_ERROR, "Error: Failed to register app-defined SDL event (%s).", SDL_GetError());
			return false;
		}
	}

	return true;
}

static bool wzSDLOneTimeInitSubsystem(uint32_t subsystem_flag)
{
	if (SDL_WasInit(subsystem_flag) == subsystem_flag)
	{
		// already initialized
		return true;
	}
	if (!SDL_InitSubSystem(subsystem_flag))
	{
		debug(LOG_WZ, "SDL_InitSubSystem(%" PRIu32 ") failed", subsystem_flag);
		return false;
	}
	return true;
}

bool wzSDLPreWindowCreate_InitOpenGLAttributes(int antialiasing, bool useOpenGLES, bool useOpenGLESLibrary)
{
	// Set OpenGL attributes before creating the SDL Window

	// Set minimum number of bits for the RGB channels of the color buffer
	SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5);
	SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 5);
	SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5);
#if defined(WZ_OS_WIN)
	if (useOpenGLES)
	{
		// Always force minimum 8-bit color channels when using OpenGL ES on Windows
		SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
		SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
		SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
	}
#endif

	// Do *not* request sRGB framebuffer
	if (!SDL_GL_SetAttribute(SDL_GL_FRAMEBUFFER_SRGB_CAPABLE, 0))
	{
		debug(LOG_ERROR, "Failed to set SDL_GL_FRAMEBUFFER_SRGB_CAPABLE: %s", SDL_GetError());
	}

	// Set the double buffer OpenGL attribute.
	SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

	// Request a 24-bit depth buffer.
	SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);

	// Enable stencil buffer, needed for shadows to work.
	SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);

	// Do *not* enable multisampling on the default framebuffer (i.e. setting SDL_GL_MULTISAMPLEBUFFERS)
	// This is handled separately for the scene render buffer / frame buffer

	if (!sdl_OpenGL_Impl::configureOpenGLContextRequest(sdl_OpenGL_Impl::getInitialContextRequest(useOpenGLES), useOpenGLESLibrary))
	{
		// Failed to configure OpenGL context request
		debug(LOG_INFO, "Failed to configure initial OpenGL context request");
		return false;
	}

	return true;
}

void wzSDLPreWindowCreate_InitVulkanLibrary()
{
#if defined(WZ_OS_MAC)
	// Attempt to explicitly load libvulkan.dylib with a full path
	// to support situations where run-time search paths using @executable_path are prohibited
	std::string fullPathToVulkanLibrary = cocoaGetFrameworksPath("libvulkan.dylib");
	if (!SDL_Vulkan_LoadLibrary(fullPathToVulkanLibrary.c_str()))
	{
		debug(LOG_ERROR, "Failed to explicitly load Vulkan library");
	}
#else
	// rely on SDL's built-in loading paths / behavior
#endif
}

class WzInitializeBackendError : public std::runtime_error
{
public:
	WzInitializeBackendError(const std::string& what_arg)
	: std::runtime_error(what_arg)
	{ }
};

class WzCreateWindowError : public WzInitializeBackendError
{
public:
	WzCreateWindowError(const std::string& what_arg)
	: WzInitializeBackendError(what_arg)
	{ }
};

class WzInitializeGraphicsContextError : public WzInitializeBackendError
{
public:
	WzInitializeGraphicsContextError(const std::string& what_arg)
	: WzInitializeBackendError(what_arg)
	{ }
};

// May throw a WzInitializeBackendError
optional<SDL_gfx_api_Impl_Factory::Configuration> wzMainScreenSetup_CreateVideoWindow(const video_backend& backend, int antialiasing, WINDOW_MODE fullscreen, bool highDPI)
{
	const bool useOpenGLES = (backend == video_backend::opengles)
#if defined(WZ_BACKEND_DIRECTX)
		|| (backend == video_backend::directx)
#endif
	;
	const bool useOpenGLESLibrary = false
#if defined(WZ_BACKEND_DIRECTX)
		|| (backend == video_backend::directx)
#endif
	;
	const bool usesSDLBackend_OpenGL = useOpenGLES || (backend == video_backend::opengl);

	// Initialize video subsystem (if not yet initialized)
	if (!wzSDLOneTimeInitSubsystem(SDL_INIT_VIDEO))
	{
		debug(LOG_FATAL, "Error: Could not initialise SDL video subsystem (%s).", SDL_GetError());
		SDL_Quit();
		exit(EXIT_FAILURE);
	}

	// ensure "fullscreen" is in the supported fullscreen modes for the current system
	auto supportedFullscreenModes = wzSupportedWindowModes();
	if (std::find(supportedFullscreenModes.begin(), supportedFullscreenModes.end(), fullscreen) == supportedFullscreenModes.end())
	{
		debug(LOG_ERROR, "Unsupported fullscreen mode specified: %d; using default", static_cast<int>(fullscreen));
		fullscreen = WINDOW_MODE::windowed;
		war_setWindowMode(fullscreen); // persist the change
	}

	if (usesSDLBackend_OpenGL)
	{
		if (!wzSDLPreWindowCreate_InitOpenGLAttributes(antialiasing, useOpenGLES, useOpenGLESLibrary))
		{
			throw WzCreateWindowError("Failed to preinitialize the window OpenGL attributes");
		}
	}
	else if (backend == video_backend::vulkan)
	{
		wzSDLPreWindowCreate_InitVulkanLibrary();
	}

	displaylist.clear();

	int currentNumDisplays = 0;
	SDL_DisplayID *displays = SDL_GetDisplays(&currentNumDisplays);

	// Add the "Desktop Full" (borderless fullscreen) mode to the displaylist
	displaylist.push_back(nullopt);

	// Populate our resolution list (does all displays now)
	struct screeninfo screenlist;
	for (int i = 0; i < currentNumDisplays; ++i)
	{
		int countDisplayModes = 0;
		SDL_DisplayMode **displayModes = SDL_GetFullscreenDisplayModes(displays[i], &countDisplayModes);
		if (!displayModes)
		{
			debug(LOG_ERROR, "Can't get the current display mode, because: %s", SDL_GetError());
			continue;
		}

		for (int modeIdx = 0; modeIdx < countDisplayModes; ++modeIdx)
		{
			SDL_DisplayMode* displaymode = displayModes[modeIdx];
			if (displaymode == nullptr)
			{
				continue;
			}

			debug(LOG_WZ, "Monitor [%d] %dx%d %f @%f %s", displays[i], displaymode->w, displaymode->h, displaymode->refresh_rate, displaymode->pixel_density, SDL_GetPixelFormatName(displaymode->format));

			const SDL_PixelFormatDetails *pixelFormatDetails = SDL_GetPixelFormatDetails(displaymode->format);

			if ((displaymode->w < MIN_WZ_GAMESCREEN_WIDTH) || (displaymode->h < MIN_WZ_GAMESCREEN_HEIGHT))
			{
				debug(LOG_WZ, "Monitor mode logical resolution < %d x %d -- discarding entry", MIN_WZ_GAMESCREEN_WIDTH, MIN_WZ_GAMESCREEN_HEIGHT);
			}
			else if (displaymode->refresh_rate < 59.f)
			{
				debug(LOG_WZ, "Monitor mode refresh rate < 59 -- discarding entry");
				// only store 60Hz & higher modes, some display report 59 on Linux
			}
#if defined(WZ_OS_MAC)
			else if (displaymode->refresh_rate < 59.94f)
			{
				// skip entries with 59.93 refresh rate, as SDL 3.2.x doesn't support switching to them due to a bug
			}
#endif
			else if (pixelFormatDetails->Rbits < 8 || pixelFormatDetails->Gbits < 8 || pixelFormatDetails->Bbits < 8)
			{
				debug(LOG_INFO, "Monitor mode pixel format has R, G, or B bits < 8 -- discarding entry");
			}
			else
			{
				screenlist.width = displaymode->w;
				screenlist.height = displaymode->h;
				screenlist.pixel_density = displaymode->pixel_density;
				screenlist.refresh_rate = displaymode->refresh_rate;
				screenlist.screen = displays[i];		// which display this belongs to
				displaylist.push_back(screenlist);

				debug(LOG_WZ, "Monitor [%d] %dx%d %f @%f %s", displays[i], displaymode->w, displaymode->h, displaymode->refresh_rate, displaymode->pixel_density, SDL_GetPixelFormatName(displaymode->format));
			}
		}

		SDL_free(displayModes);
	}

	// populate with the saved configuration values (if we had any) for windowed mode
	int desiredWindowWidth = war_GetWidth();
	int desiredWindowHeight = war_GetHeight();

	// NOTE: Prior to wzMainScreenSetup being run, the display system is populated with the window width + height
	// (i.e. not taking into account the game display scale). This function later sets the display system
	// to the *game screen* width and height (taking into account the display scale).

	if (desiredWindowWidth == 0 || desiredWindowHeight == 0)
	{
		windowWidth = 1024;
		windowHeight = 768;
	}
	else
	{
		windowWidth = desiredWindowWidth;
		windowHeight = desiredWindowHeight;
	}

	setDisplayScale(war_GetDisplayScale());

	SDL_Rect bounds;
	for (int i = 0; i < currentNumDisplays; i++)
	{
		SDL_GetDisplayBounds(displays[i], &bounds);
		debug(LOG_WZ, "Monitor %d: pos %d x %d : res %d x %d", displays[i], (int)bounds.x, (int)bounds.y, (int)bounds.w, (int)bounds.h);
	}
	if (currentNumDisplays < 1)
	{
		debug(LOG_FATAL, "SDL_GetDisplays returned: %d, with error: %s", currentNumDisplays, SDL_GetError());
		SDL_Quit();
		exit(EXIT_FAILURE);
	}
	SDL_free(displays);

	// Check desired screen index values versus current system
	if (war_GetScreen() > currentNumDisplays)
	{
		debug(LOG_WARNING, "Invalid screen [%d] defined in configuration; there are only %d displays; falling back to display 0", war_GetScreen(), currentNumDisplays);
		war_SetScreen(0);
	}
	if (war_GetFullscreenModeScreen() > currentNumDisplays || war_GetFullscreenModeScreen() < 0)
	{
		debug(LOG_WARNING, "Invalid fullscreen screen [%d] defined in configuration; there are only %d displays; falling back to display 0", war_GetFullscreenModeScreen(), currentNumDisplays);
		war_SetFullscreenModeScreen(0);
	}

	screenIndex = war_GetScreen();

	// Calculate the minimum window size (in *logical* points) given the current display scale
	unsigned int minWindowWidth = 0, minWindowHeight = 0;
	wzGetMinimumWindowSizeForDisplayScaleFactor(&minWindowWidth, &minWindowHeight);

	if (fullscreen != WINDOW_MODE::fullscreen)
	{
		if (war_getAutoAdjustDisplayScale())
		{
			// Since SDL (at least <= 2.0.14) does not provide built-in support for high-DPI displays on *some* platforms
			// (example: Windows), do our best to bump up the game's Display Scale setting to be a better match if the screen
			// on which the game starts has a higher effective Display Scale than the game's starting Display Scale setting.
			unsigned int screenBaseDisplayScale = wzGetDefaultBaseDisplayScale(screenIndex > 0 ? screenIndex : SDL_GetPrimaryDisplay());

			if (screenBaseDisplayScale > wzGetCurrentDisplayScale())
			{
				// When bumping up the display scale, also increase the target window size proportionally
				debug(LOG_WZ, "Increasing game Display Scale to better match calculated default base display scale: %u%% -> %u%%", wzGetCurrentDisplayScale(), screenBaseDisplayScale);
				unsigned int priorDisplayScale = wzGetCurrentDisplayScale();
				setDisplayScale(screenBaseDisplayScale);
				float displayScaleDiff = (float)screenBaseDisplayScale / (float)priorDisplayScale;
				windowWidth = static_cast<unsigned int>(ceil((float)windowWidth * displayScaleDiff));
				windowHeight = static_cast<unsigned int>(ceil((float)windowHeight * displayScaleDiff));
			}
		}

		if ((windowWidth < minWindowWidth) || (windowHeight < minWindowHeight))
		{
			// The desired window width and/or height is lower than the required minimum for the current display scale.
			// Reduce the display scale to the maximum supported (for the desired window size), and recalculate the required minimum window size.
			unsigned int maxDisplayScale = wzGetMaximumDisplayScaleForWindowSize(windowWidth, windowHeight);
			maxDisplayScale = std::max(100u, maxDisplayScale); // if wzGetMaximumDisplayScaleForWindowSize fails, it returns < 100
			setDisplayScale(maxDisplayScale);
			wzGetMinimumWindowSizeForDisplayScaleFactor(&minWindowWidth, &minWindowHeight);
		}

		windowWidth = std::max(windowWidth, minWindowWidth);
		windowHeight = std::max(windowHeight, minWindowHeight);

		if (fullscreen == WINDOW_MODE::windowed)
		{
			// Determine the maximum usable windowed size for this display/screen
			SDL_Rect displayUsableBounds = { 0, 0, 0, 0 };
			if (SDL_GetDisplayUsableBounds(screenIndex, &displayUsableBounds) == 0)
			{
				if (displayUsableBounds.w > 0 && displayUsableBounds.h > 0)
				{
					windowWidth = std::min((unsigned int)displayUsableBounds.w, windowWidth);
					windowHeight = std::min((unsigned int)displayUsableBounds.h, windowHeight);
				}
			}
		}
	}

	SDL_PropertiesID props = SDL_CreateProperties();
	SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, PACKAGE_NAME);
	SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, SDL_WINDOWPOS_CENTERED_DISPLAY(screenIndex));
	SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, SDL_WINDOWPOS_CENTERED_DISPLAY(screenIndex));
	SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, windowWidth);
	SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, windowHeight);

	WZ_SDL_setBackendProperty(props, backend);

	if (fullscreen == WINDOW_MODE::windowed)
	{
		// Allow the window to be manually resized, if not fullscreen
		SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, true);
	}

	if (highDPI)
	{
		// Allow SDL to enable its built-in High-DPI display support
		SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN, true);
	}

	WZwindow = SDL_CreateWindowWithProperties(props);
	if (!WZwindow)
	{
		std::string createWindowErrorStr = SDL_GetError();
		debug(LOG_INFO, "Failed to create a window, because: %s", createWindowErrorStr.c_str());
		throw WzCreateWindowError(createWindowErrorStr);
	}

	// Always set the fullscreen mode (so switching works to the desired mode, even if we don't start in fullscreen mode)
	if (wzSetDefaultFullscreenDisplayMode())
	{
		if (fullscreen == WINDOW_MODE::fullscreen)
		{
			// queue a later transition to fullscreen mode
			wzAsyncExecOnMainThread([]() {
				// transition to fullscreen mode
				wzChangeWindowMode(WINDOW_MODE::fullscreen);
			});
		}
	}
	else
	{
		if (fullscreen == WINDOW_MODE::fullscreen)
		{
			debug(LOG_ERROR, "Failed to initialize at fullscreen mode ([%d] [%u x %u]); reverting to windowed mode", screenIndex, war_GetFullscreenModeWidth(), war_GetFullscreenModeHeight());
			wzChangeWindowMode(WINDOW_MODE::windowed);
			fullscreen = WINDOW_MODE::windowed;
		}
	}

	// Get resulting logical window size
	int resultingWidth, resultingHeight = 0;
	SDL_GetWindowSize(WZwindow, &resultingWidth, &resultingHeight);

	// Check that the actual window size matches the desired window size (but not for classic fullscreen mode)
	if ((wzGetCurrentWindowMode() != WINDOW_MODE::fullscreen) && (resultingWidth < minWindowWidth || resultingHeight < minWindowHeight))
	{
		// The created window size (that the system returned) is less than the minimum required window size (for this display scale)
		debug(LOG_WARNING, "Failed to create window at desired resolution: [%d] %d x %d; instead, received window of resolution: [%d] %d x %d; which is below the required minimum size of %d x %d for the current display scale level", war_GetScreen(), windowWidth, windowHeight, war_GetScreen(), resultingWidth, resultingHeight, minWindowWidth, minWindowHeight);

		// Adjust the display scale (if possible)
		unsigned int maxDisplayScale = wzGetMaximumDisplayScaleForWindowSize(resultingWidth, resultingHeight);
		if (maxDisplayScale >= 100)
		{
			// Reduce the display scale
			debug(LOG_WARNING, "Reducing the display scale level to the maximum supported for this window size: %u", maxDisplayScale);
			setDisplayScale(maxDisplayScale);
			wzGetMinimumWindowSizeForDisplayScaleFactor(&minWindowWidth, &minWindowHeight);
		}
		else
		{
			// Problem: The resulting window size that the system provided is below the minimum required
			//          for the lowest possible display scale - i.e. the window is just too small
			debug(LOG_ERROR, "The window size created by the system is too small!");

			// TODO: Should probably exit with a fatal error here?
			// For now...

			// Attempt to default to base resolution at 100%
			setDisplayScale(100);
			wzGetMinimumWindowSizeForDisplayScaleFactor(&minWindowWidth, &minWindowHeight);

			SDL_SetWindowSize(WZwindow, minWindowWidth, minWindowHeight);
			windowWidth = minWindowWidth;
			windowHeight = minWindowHeight;

			// Center window on screen
			SDL_SetWindowPosition(WZwindow, SDL_WINDOWPOS_CENTERED_DISPLAY(screenIndex), SDL_WINDOWPOS_CENTERED_DISPLAY(screenIndex));

			// Wait for window size changes to be processed
			SDL_SyncWindow(WZwindow);

			// Re-request resulting window size
			SDL_GetWindowSize(WZwindow, &resultingWidth, &resultingHeight);
		}
	}
	ASSERT(resultingWidth > 0 && resultingHeight > 0, "Invalid - SDL returned window width: %d, window height: %d", resultingWidth, resultingHeight);
	windowWidth = (unsigned int)resultingWidth;
	windowHeight = (unsigned int)resultingHeight;

	// Calculate the game screen's logical dimensions
	screenWidth = windowWidth;
	screenHeight = windowHeight;
	if (current_displayScaleFactor > 1.0f)
	{
		screenWidth = static_cast<unsigned int>(windowWidth / current_displayScaleFactor);
		screenHeight = static_cast<unsigned int>(windowHeight / current_displayScaleFactor);
	}
	pie_SetVideoBufferWidth(screenWidth);
	pie_SetVideoBufferHeight(screenHeight);

	// Set the minimum window size
	SDL_SetWindowMinimumSize(WZwindow, minWindowWidth, minWindowHeight);

#if !defined(WZ_OS_MAC) && !defined(__EMSCRIPTEN__) // Do not use this method to set the window icon on macOS or Emscripten

	#if SDL_BYTEORDER == SDL_BIG_ENDIAN
		uint32_t rmask = 0xff000000;
		uint32_t gmask = 0x00ff0000;
		uint32_t bmask = 0x0000ff00;
		uint32_t amask = 0x000000ff;
	#else
		uint32_t rmask = 0x000000ff;
		uint32_t gmask = 0x0000ff00;
		uint32_t bmask = 0x00ff0000;
		uint32_t amask = 0xff000000;
	#endif

#if defined(__GNUC__)
	#pragma GCC diagnostic push
	// ignore warning: cast from type 'const unsigned char*' to type 'void*' casts away qualifiers [-Wcast-qual]
	// FIXME?
	#pragma GCC diagnostic ignored "-Wcast-qual"
#endif
	SDL_Surface *surface_icon = SDL_CreateSurfaceFrom(wz2100icon.width, wz2100icon.height,
								 SDL_GetPixelFormatForMasks(wz2100icon.bytes_per_pixel * 8, rmask, gmask, bmask, amask),
								(void *)wz2100icon.pixel_data, wz2100icon.width * wz2100icon.bytes_per_pixel);
#if defined(__GNUC__)
	#pragma GCC diagnostic pop
#endif

	if (surface_icon)
	{
		SDL_SetWindowIcon(WZwindow, surface_icon);
		SDL_DestroySurface(surface_icon);
	}
	else
	{
		debug(LOG_ERROR, "Could not set window icon because %s", SDL_GetError());
	}
#endif

	SDL_SetWindowTitle(WZwindow, PACKAGE_NAME);

	SDL_gfx_api_Impl_Factory::Configuration sdl_impl_config;
	sdl_impl_config.useOpenGLES = useOpenGLES;
	sdl_impl_config.useOpenGLESLibrary = useOpenGLESLibrary;
	if (backend == video_backend::vulkan)
	{
		sdl_impl_config.allowImplicitLayers = war_getAllowVulkanImplicitLayers();
	}
	return sdl_impl_config;
}

bool wzMainScreenSetup_VerifyWindow()
{
	int bitDepth = war_GetVideoBufferDepth();

	int bpp = SDL_BITSPERPIXEL(SDL_GetWindowPixelFormat(WZwindow));
	debug(LOG_WZ, "Bpp = %d format %s" , bpp, SDL_GetPixelFormatName(SDL_GetWindowPixelFormat(WZwindow)));
	if (!bpp)
	{
		debug(LOG_ERROR, "Video mode %ux%u@%dbpp is not supported!", windowWidth, windowHeight, bitDepth);
		return false;
	}
	switch (bpp)
	{
	case 32:
	case 24:		// all is good...
		break;
	case 16:
		wz_info("Using colour depth of %i instead of a 32/24 bit depth (True color).", bpp);
		wz_info("You will experience graphics glitches!");
		break;
	case 8:
		debug(LOG_FATAL, "You don't want to play Warzone with a bit depth of %i, do you?", bpp);
		SDL_Quit();
		exit(1);
		break;
	default:
		debug(LOG_FATAL, "Unsupported bit depth: %i", bpp);
		exit(1);
		break;
	}

	return true;
}

#if defined(WZ_OS_WIN)
class ScopedWindowsProcessAffinityMaskChanger
{
public:
	ScopedWindowsProcessAffinityMaskChanger(optional<video_backend> backend);
	~ScopedWindowsProcessAffinityMaskChanger();
	void restore();
private:
	DWORD_PTR originalProcessAffinityMask = 0;
	DWORD_PTR systemAffinityMask = 0;
	bool restoreAffinityMask = false;
};

ScopedWindowsProcessAffinityMaskChanger::ScopedWindowsProcessAffinityMaskChanger(optional<video_backend> backend)
{
	// Windows: Workaround for Nvidia "threaded optimization"
	// Set the process affinity mask to 1 before creating the window and initializing OpenGL
	// This may disable Nvidia's "threaded optimization" feature, which can cause issues with WZ in OpenGL mode
	// NOTE: Must restore the affinity mask afterwards! (See restore() and the destructor)

	if (backend.has_value() && (backend.value() == video_backend::opengl)) // only do this for OpenGL mode, for now
	{
		if (::GetProcessAffinityMask(::GetCurrentProcess(), &originalProcessAffinityMask, &systemAffinityMask) != 0)
		{
			if (::SetProcessAffinityMask(::GetCurrentProcess(), 1) != 0)
			{
				restoreAffinityMask = true;
			}
			else
			{
				debug(LOG_INFO, "Failed to set process affinity mask");
			}
		}
		else
		{
			// Failed to get the current process affinity mask
			debug(LOG_INFO, "Failed to get current process affinity mask");
		}
	}
}

void ScopedWindowsProcessAffinityMaskChanger::restore()
{
	if (restoreAffinityMask)
	{
		// restore process affinity mask
		if (::SetProcessAffinityMask(::GetCurrentProcess(), originalProcessAffinityMask) != 0)
		{
			debug(LOG_WZ, "Restored process affinity mask");
		}
		else
		{
			debug(LOG_ERROR, "Failed to restore process affinity mask");
		}
		restoreAffinityMask = false;
	}
}

ScopedWindowsProcessAffinityMaskChanger::~ScopedWindowsProcessAffinityMaskChanger()
{
	restore();
}
#endif // WZ_OS_WIN

// Note: May throw a WzInitializeBackendError
static bool wzAttemptInitializeBackend(optional<video_backend> backend, int32_t antialiasing, WINDOW_MODE fullscreen, gfx_api::context::swap_interval_mode swapMode, optional<float> lodDistanceBias, uint32_t depthMapResolution)
{
	bool highDPI = true;

	if (backend.has_value())
	{
		debug(LOG_INFO, "Attempting to initialize backend: %s", to_display_string(backend.value()).c_str());
	}
	else
	{
		debug(LOG_INFO, "Attempting to initialize backend: (headless)");
	}

#if defined(WZ_OS_WIN)
	// Windows: Workaround for Nvidia "threaded optimization"
	ScopedWindowsProcessAffinityMaskChanger win_scoped_process_affinity_changer(backend);
#endif


#if defined(__EMSCRIPTEN__)
	wzemscripten_startup_ensure_canvas_displayed();
#endif

	SDL_gfx_api_Impl_Factory::Configuration sdl_impl_config;

	if (backend.has_value())
	{
		auto result = wzMainScreenSetup_CreateVideoWindow(backend.value(), antialiasing, fullscreen, highDPI); // may throw, caller of wzAttemptInitializeBackend is expected to handle
		if (!result.has_value())
		{
			return false;
		}
		sdl_impl_config = result.value();
	}

	setlocale(LC_NUMERIC, "C"); // set radix character to the period (".")

	gfx_api::backend_type gfxapi_backend = gfx_api::backend_type::null_backend;
	if (backend.has_value())
	{
		if (backend.value() == video_backend::vulkan)
		{
			gfxapi_backend = gfx_api::backend_type::vulkan_backend;
		}
		else
		{
			gfxapi_backend = gfx_api::backend_type::opengl_backend;
		}
	}

	if (!gfx_api::context::initialize(SDL_gfx_api_Impl_Factory(WZwindow, sdl_impl_config), antialiasing, swapMode, lodDistanceBias, depthMapResolution, gfxapi_backend))
	{
		// Failed to initialize desired backend / renderer settings
		if (backend.has_value())
		{
			// Destroy the window
			if (WZwindow != nullptr)
			{
				SDL_DestroyWindow(WZwindow);
				WZwindow = nullptr;
			}
			throw WzInitializeGraphicsContextError("Failed to initialize gfx_api::context");
		}
		else
		{
			// headless mode failed in gfx_api::context::initialize??
			debug(LOG_FATAL, "gfx_api::context::get().initialize failed for headless-mode backend?");
		}
		SDL_Quit();
		exit(EXIT_FAILURE);
	}

	if (backend.has_value())
	{
		wzMainScreenSetup_VerifyWindow();

#if defined(__EMSCRIPTEN__)
		// Catch the full screen change events
		// - If user-initiated (i.e. by pressing ESC or similar to exit fullscreen), SDL does not currently expose this event
		// - SDL itself sets a fullscreenchange callback on the document, which must remain for SDL functionality - fortunately, emscripten lets us set one on the canvas element itself
		emscripten_set_fullscreenchange_callback("#canvas", nullptr, 0, wz_emscripten_fullscreenchange_callback);

		auto mode = wzGetCurrentWindowMode();
		if (mode == WINDOW_MODE::windowed)
		{
			// Enable "soft fullscreen" - where the canvas automatically fills the window
			wz_emscripten_enable_soft_fullscreen();
		}
#endif
	}

#if defined(WZ_OS_WIN)
	win_scoped_process_affinity_changer.restore();
#endif

	if (backend.has_value())
	{
		/* initialise all cursors */
		if (war_GetColouredCursor())
		{
			sdlInitColoredCursors();
		}
		else
		{
			sdlInitCursors();
		}
	}

#if defined(WZ_OS_MAC)
	if (backend.has_value())
	{
		cocoaSetupWZMenus();
	}
#endif

	return true;
}

void failedToInitializeAnyGraphicsBackendMessage_internal(const std::vector<std::string>& additionalErrorDetails)
{
	const SDL_MessageBoxButtonData buttons[] = {
	   { SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, 1, _("Close") }
	};
	std::string titleString = std::string(_("Warzone 2100: Failed to initialize graphics"));
	std::string messageString = std::string(_("Failed to initialize graphics for all backends.")) + "\n\n";
	if (!additionalErrorDetails.empty())
	{
		messageString += std::string(_("Error Details:")) + "\n";
		for (const auto& errStr : additionalErrorDetails)
		{
			messageString += errStr + "\n";
		}
		messageString += "\n\n";
	}
	messageString += _("Please update and/or reinstall your graphics drivers, and check your system settings.");
	const SDL_MessageBoxData messageboxdata = {
		SDL_MESSAGEBOX_ERROR, /* .flags */
		WZwindow, /* .window */
		titleString.c_str(), /* .title */
		messageString.c_str(), /* .message */
		SDL_arraysize(buttons), /* .numbuttons */
		buttons, /* .buttons */
		nullptr /* .colorScheme */
	};
	int buttonid;
	if (!SDL_ShowMessageBox(&messageboxdata, &buttonid)) {
		// error displaying message box
		debug(LOG_FATAL, "Failed to display message box");
	}
}

bool wzMainScreenSetup(optional<video_backend> backend, int antialiasing, WINDOW_MODE fullscreen, int vsync, int lodDistanceBiasPercentage, uint32_t depthMapResolution, bool highDPI)
{
	// Output linked SDL version
	char buf[512];
	auto linked_sdl_version = SDL_GetVersion();
	ssprintf(buf, "Linked SDL version: %u.%u.%u\n", (unsigned int)SDL_VERSIONNUM_MAJOR(linked_sdl_version), (unsigned int)SDL_VERSIONNUM_MINOR(linked_sdl_version), (unsigned int)SDL_VERSIONNUM_MICRO(linked_sdl_version));
	addDumpInfo(buf);
	debug(LOG_WZ, "%s", buf);

	if (!wzSDLOneTimeInit())
	{
		// wzSDLOneTimeInit already logged an error on failure
		return false;
	}

#if defined(WZ_OS_MAC)
	// on macOS, support maximizing to a fullscreen space (modern behavior)
	if (!SDL_SetHint(SDL_HINT_VIDEO_MAC_FULLSCREEN_SPACES, "1"))
	{
		debug(LOG_WARNING, "Failed to set hint: SDL_HINT_VIDEO_MAC_FULLSCREEN_SPACES");
	}
#endif

#if defined(WZ_OS_WIN)
	// on Windows, opt-in to SDL 2.24.0+'s DPI scaling support
	// SDL_HINT_WINDOWS_DPI_AWARENESS does not appear to be needed if SDL_HINT_WINDOWS_DPI_SCALING is set
	// But set it anyway for completeness
	if (!SDL_SetHint("SDL_WINDOWS_DPI_AWARENESS", "permonitorv2")) // TODO: NO longer needed with SDL3?
	{
		debug(LOG_WARNING, "Failed to set hint: SDL_HINT_WINDOWS_DPI_AWARENESS");
	}
#endif

#if defined(SDL_HINT_MOUSE_DPI_SCALE_CURSORS)
	// set SDL_HINT_MOUSE_DPI_SCALE_CURSORS to 1, to enable auto-scaling of cursors to display content scale by default
	if (!SDL_SetHint(SDL_HINT_MOUSE_DPI_SCALE_CURSORS, "1"))
	{
		debug(LOG_WARNING, "Failed to set hint: SDL_HINT_MOUSE_DPI_SCALE_CURSORS");
	}
#endif

	int minOnFocusLossSettingVal = war_getMinimizeOnFocusLoss();
	if (minOnFocusLossSettingVal < -1 || minOnFocusLossSettingVal > 1)
	{
		minOnFocusLossSettingVal = -1;
	}
	const char* hint = SDL_GetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS);
	if (!hint || !*hint || SDL_strcasecmp(hint, "auto") == 0)
	{
		wzSetMinimizeOnFocusLoss(static_cast<MinimizeOnFocusLossBehavior>(minOnFocusLossSettingVal));
	}

	const auto swapMode = to_swap_mode(vsync);

	optional<float> lodDistanceBias = nullopt;
	if (lodDistanceBiasPercentage != 0)
	{
		lodDistanceBias = static_cast<float>(lodDistanceBiasPercentage) / 100.f;
	}

	auto availableBackends = wzAvailableGfxBackends();
	optional<video_backend> requestedBackend = backend;
	std::vector<std::string> backendInitErrors;

	auto videoInitProgress = wzResumeFailedVideoInit(backend, availableBackends, backendInitErrors);
	if (requestedBackend.has_value() && !backend.has_value())
	{
		// Requested a non-null backend, but resuming from previous init failure yielded no valid backends to try
		videoInitProgress->RecordInitFinished(false);
		failedToInitializeAnyGraphicsBackendMessage_internal(backendInitErrors);
		WZbackend = nullopt;
		return false;
	}

	do {
		bool success = false;
		WZbackend = backend; // various other functions might need this before the call to wzAttemptInitializeBackend returns
		videoInitProgress->RecordAttemptingBackend(backend);
		try {
			success = wzAttemptInitializeBackend(backend, antialiasing, fullscreen, swapMode, lodDistanceBias, depthMapResolution);
		}
		catch (const WzInitializeBackendError& e)
		{
			if (backend.has_value())
			{
				backendInitErrors.push_back(astringf("[%s]: %s", to_display_string(backend.value()).c_str(), e.what()));
				videoInitProgress->RecordFailedBackend(backend, e.what());
			}
		}
		if (success)
		{
			war_SetDisplayScale(wzGetCurrentDisplayScale()); // persist any auto-adjustments to the display scale
			break;
		}
		else
		{
			if (!backend.has_value())
			{
				// if the null backend, and initialization failed, return false
				videoInitProgress->RecordInitFinished(false);
				return false;
			}

			videoInitProgress->RecordFailedBackend(backend, "Failed to initialize");
			availableBackends.erase(std::remove_if(availableBackends.begin(), availableBackends.end(), [&backend](video_backend a) { return a == backend.value(); }), availableBackends.end());
			if (availableBackends.empty())
			{
				// No more backends to try :(
				videoInitProgress->RecordInitFinished(false);
				failedToInitializeAnyGraphicsBackendMessage_internal(backendInitErrors);
				WZbackend = nullopt;
				return false;
			}
			// Try with a new backend (first in the current list)
			backend = availableBackends.front();
		}
	} while (backend.has_value());

	if (requestedBackend.has_value() && backend.has_value() && (requestedBackend.value() != backend.value()))
	{
		// ended up choosing a different backend at runtime - persist the new setting
		resetGfxBackend(backend.value(), false);
		saveGfxConfig(); // must force-persist the new value before returning!
	}

	videoInitProgress->RecordInitFinished(true);
	return true;
}

optional<video_backend> wzGetInitializedGfxBackend()
{
	return WZbackend;
}

// Calculates and returns the scale factor from the SDL window's coordinate system (in points) to the raw
// underlying pixels of the viewport / renderer.
//
// IMPORTANT: This value is *non-inclusive* of any user-configured Game Display Scale.
//
// This exposes what is effectively the SDL window's "High-DPI Scale Factor", if SDL's high-DPI support is enabled and functioning.
//
// In the normal, non-high-DPI-supported case, (in which the context's drawable size in pixels and the window's logical
// size in points are equal) this will return 1.0 for both values.
//
void wzGetWindowToRendererScaleFactor(float *horizScaleFactor, float *vertScaleFactor)
{
	if (!WZbackend.has_value())
	{
		if (horizScaleFactor != nullptr)
		{
			*horizScaleFactor = 1.0f;
		}
		if (vertScaleFactor != nullptr)
		{
			*vertScaleFactor = 1.0f;
		}
		return;
	}
	ASSERT_OR_RETURN(, WZwindow != nullptr, "wzGetWindowToRendererScaleFactor called when window is not available");

	// Obtain the window context's drawable size in pixels
	int drawableWidth, drawableHeight = 0;
	SDL_WZBackend_GetDrawableSize(WZwindow, &drawableWidth, &drawableHeight);

	// Obtain the logical window size (in points)
	int logicalWindowWidth = 0, logicalWindowHeight = 0;
	SDL_GetWindowSize(WZwindow, &logicalWindowWidth, &logicalWindowHeight);

	debug(LOG_WZ, "Window Logical Size (%d, %d) vs Drawable Size in Pixels (%d, %d)", logicalWindowWidth, logicalWindowHeight, drawableWidth, drawableHeight);

	if (horizScaleFactor != nullptr)
	{
		*horizScaleFactor = ((float)drawableWidth / (float)logicalWindowWidth); // Do **NOT** multiply by current_displayScaleFactor
	}
	if (vertScaleFactor != nullptr)
	{
		*vertScaleFactor = ((float)drawableHeight / (float)logicalWindowHeight); // Do **NOT** multiply by current_displayScaleFactor
	}
}

// Calculates and returns the total scale factor from the game's coordinate system (in points)
// to the raw underlying pixels of the viewport / renderer.
//
// IMPORTANT: This value is *inclusive* of both the user-configured "Display Scale" *AND* any underlying
// high-DPI / "Retina" display support provided by SDL.
//
// It is equivalent to: (SDL Window's High-DPI Scale Factor) x (WZ Game Display Scale Factor)
//
// Therefore, if SDL is providing a supported high-DPI window / context, this value will be greater
// than the WZ (user-configured) Display Scale Factor.
//
// It should be used only for internal (non-user-displayed) cases in which the full scaling factor from
// the game system's coordinate system (in points) to the underlying display pixels is required.
// (For example, when rasterizing text for best display.)
//
void wzGetGameToRendererScaleFactor(float *horizScaleFactor, float *vertScaleFactor)
{
	float horizWindowScaleFactor = 0.f, vertWindowScaleFactor = 0.f;
	wzGetWindowToRendererScaleFactor(&horizWindowScaleFactor, &vertWindowScaleFactor);
	assert(horizWindowScaleFactor != 0.f);
	assert(vertWindowScaleFactor != 0.f);

	if (horizScaleFactor != nullptr)
	{
		*horizScaleFactor = horizWindowScaleFactor * current_displayScaleFactor;
	}
	if (vertScaleFactor != nullptr)
	{
		*vertScaleFactor = vertWindowScaleFactor * current_displayScaleFactor;
	}
}
void wzGetGameToRendererScaleFactorInt(unsigned int *horizScalePercentage, unsigned int *vertScalePercentage)
{
	float horizWindowScaleFactor = 0.f, vertWindowScaleFactor = 0.f;
	wzGetWindowToRendererScaleFactor(&horizWindowScaleFactor, &vertWindowScaleFactor);
	assert(horizWindowScaleFactor != 0.f);
	assert(vertWindowScaleFactor != 0.f);

	if (horizScalePercentage != nullptr)
	{
		*horizScalePercentage = static_cast<unsigned int>(ceil(horizWindowScaleFactor * current_displayScale));
	}
	if (vertScalePercentage != nullptr)
	{
		*vertScalePercentage = static_cast<unsigned int>(ceil(vertWindowScaleFactor * current_displayScale));
	}
}

void wzSetWindowIsResizable(bool resizable)
{
	if (WZwindow == nullptr)
	{
		debug(LOG_WARNING, "wzSetWindowIsResizable called when window is not available");
		return;
	}
	SDL_SetWindowResizable(WZwindow, resizable);

	if (resizable)
	{
		// Set the minimum window size
		unsigned int minWindowWidth = 0, minWindowHeight = 0;
		wzGetMinimumWindowSizeForDisplayScaleFactor(&minWindowWidth, &minWindowHeight, current_displayScaleFactor);
		SDL_SetWindowMinimumSize(WZwindow, minWindowWidth, minWindowHeight);
	}
}

bool wzIsWindowResizable()
{
	if (WZwindow == nullptr)
	{
		debug(LOG_WARNING, "wzIsWindowResizable called when window is not available");
		return false;
	}
	Uint32 flags = SDL_GetWindowFlags(WZwindow);
	if (flags & SDL_WINDOW_RESIZABLE)
	{
		return true;
	}
	return false;
}

/*!
 * Activation (focus change ... and) eventhandler.  Mainly for debugging.
 */
static void handleActiveEvent(SDL_Event *event)
{
	if (event->type >= SDL_EVENT_WINDOW_FIRST && event->type <= SDL_EVENT_WINDOW_LAST)
	{
		switch (event->window.type)
		{
		case SDL_EVENT_WINDOW_SHOWN :
			debug(LOG_WZ, "Window %d shown", event->window.windowID);
			break;
		case SDL_EVENT_WINDOW_HIDDEN :
			debug(LOG_WZ, "Window %d hidden", event->window.windowID);
			break;
		case SDL_EVENT_WINDOW_EXPOSED :
			debug(LOG_WZ, "Window %d exposed", event->window.windowID);
			break;
		case SDL_EVENT_WINDOW_MOVED :
			debug(LOG_WZ, "Window %d moved to %d,%d", event->window.windowID, event->window.data1, event->window.data2);
				// FIXME: Handle detecting which screen the window was moved to, and update saved war_SetScreen?
			break;
		case SDL_EVENT_WINDOW_RESIZED :
		case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED :
			debug(LOG_WZ, "Window %d resized to %dx%d", event->window.windowID, event->window.data1, event->window.data2);
			{
				unsigned int oldWindowWidth = windowWidth;
				unsigned int oldWindowHeight = windowHeight;

				Uint32 windowFlags = SDL_GetWindowFlags(WZwindow);
				debug(LOG_WZ, "Window resized to window flags: %u", windowFlags);

				int newWindowWidth = 0, newWindowHeight = 0;
				SDL_GetWindowSize(WZwindow, &newWindowWidth, &newWindowHeight);
				handleWindowSizeChange(oldWindowWidth, oldWindowHeight, newWindowWidth, newWindowHeight);

				// Store the new values (in case the user manually resized the window bounds)
				if (wzGetCurrentWindowMode() == WINDOW_MODE::windowed)
				{
					war_SetWidth(newWindowWidth);
					war_SetHeight(newWindowHeight);
				}

				// Handle deferred size reset
				if (deferredDimensionReset.has_value())
				{
					if (WZwindow != nullptr)
					{
						SDL_SetWindowSize(WZwindow, deferredDimensionReset.value().width, deferredDimensionReset.value().height);
						if (deferredDimensionReset.value().recenter)
						{
							SDL_SetWindowPosition(WZwindow, SDL_WINDOWPOS_CENTERED_DISPLAY(screenIndex), SDL_WINDOWPOS_CENTERED_DISPLAY(screenIndex));
						}
						wzReduceDisplayScalingIfNeeded(deferredDimensionReset.value().width, deferredDimensionReset.value().height);
					}
					deferredDimensionReset.reset();
				}
				else
				{
					wzReduceDisplayScalingIfNeeded(newWindowWidth, newWindowHeight);
				}
			}
			break;
		case SDL_EVENT_WINDOW_MINIMIZED :
			debug(LOG_INFO, "Window %d minimized", event->window.windowID);
			break;
		case SDL_EVENT_WINDOW_MAXIMIZED :
			debug(LOG_WZ, "Window %d maximized", event->window.windowID);
			break;
		case SDL_EVENT_WINDOW_RESTORED :
			debug(LOG_INFO, "Window %d restored", event->window.windowID);
			{
				unsigned int oldWindowWidth = windowWidth;
				unsigned int oldWindowHeight = windowHeight;

				int newWindowWidth = 0, newWindowHeight = 0;
				SDL_GetWindowSize(WZwindow, &newWindowWidth, &newWindowHeight);

				int newDrawableWidth = 0, newDrawableHeight = 0;
				SDL_WZBackend_GetDrawableSize(WZwindow, &newDrawableWidth, &newDrawableHeight);
				auto oldDrawableDimensions = gfx_api::context::get().getDrawableDimensions();

				if (oldWindowWidth != newWindowWidth || oldWindowHeight != newWindowHeight
					|| oldDrawableDimensions.first != newDrawableWidth || oldDrawableDimensions.second != newDrawableHeight)
				{
					debug(LOG_WZ, "Triggering handleWindowSizeChange from window restore event");
					handleWindowSizeChange(oldWindowWidth, oldWindowHeight, newWindowWidth, newWindowHeight);
				}
			}
			break;
		case SDL_EVENT_WINDOW_MOUSE_ENTER :
			mouseInWindow = true;
			debug(LOG_WZ, "Mouse entered window %d", event->window.windowID);
			wzQueueRefreshCursor();
			break;
		case SDL_EVENT_WINDOW_MOUSE_LEAVE :
			mouseInWindow = false;
			debug(LOG_WZ, "Mouse left window %d", event->window.windowID);
			break;
		case SDL_EVENT_WINDOW_FOCUS_GAINED :
			windowHasFocus = true;
			wzQueueRefreshCursor();
			debug(LOG_WZ, "Window %d gained keyboard focus", event->window.windowID);
			break;
		case SDL_EVENT_WINDOW_FOCUS_LOST :
			windowHasFocus = false;
			debug(LOG_WZ, "Window %d lost keyboard focus", event->window.windowID);
			for (unsigned int i = 0; i < KEY_MAXSCAN; i++)
			{
				aKeyState[i].state = KEY_UP;
				actualKeyState[i].state = KEY_UP;
			}
			break;
		case SDL_EVENT_WINDOW_CLOSE_REQUESTED :
			debug(LOG_WZ, "Window %d closed", event->window.windowID);
			WZwindow = nullptr;
			break;
		case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED :
		{
			SDL_Window *window = SDL_GetWindowFromEvent(event);
			if (window)
			{
				float scale = SDL_GetDisplayContentScale(SDL_GetDisplayForWindow(window));
				debug(LOG_WZ, "SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED with display content scale: %f", scale);
				// POSSIBLE TODO: Could try to automatically adjust the display scale setting if this changes...
				// (We don't separate the WZ-configured display scale setting and the system-provided ContentDisplayScale currently, though)
			}
			break;
		}
		default:
			debug(LOG_WZ, "Window %d got unknown event %d", event->window.windowID, event->window.type);
			break;
		}
	}
}

static SDL_Event event;
#if defined(__EMSCRIPTEN__)
std::function<void()> saved_onShutdown;
unsigned lastLoopReturn = 0;
#endif

void wzEventLoopOneFrame(void* arg)
{
#if defined(__EMSCRIPTEN__)
	if (lastLoopReturn > 0)
	{
		wz_emscripten_did_finish_render(lastLoopReturn - wzGetTicks());
	}
#endif

	/* Deal with any windows messages */
	while (SDL_PollEvent(&event))
	{
		switch (event.type)
		{
		case SDL_EVENT_KEY_UP:
		case SDL_EVENT_KEY_DOWN:
			inputHandleKeyEvent(&event.key);
			break;
		case SDL_EVENT_MOUSE_BUTTON_UP:
		case SDL_EVENT_MOUSE_BUTTON_DOWN:
			inputHandleMouseButtonEvent(&event.button);
			break;
		case SDL_EVENT_MOUSE_MOTION:
			inputHandleMouseMotionEvent(&event.motion);
			break;
		case SDL_EVENT_MOUSE_WHEEL:
			inputHandleMouseWheelEvent(&event.wheel);
			break;
		case SDL_EVENT_TEXT_INPUT:	// SDL now handles text input differently
			inputhandleText(&event.text);
			break;
		case SDL_EVENT_QUIT:
#if defined(__EMSCRIPTEN__)
			// Exit "soft fullscreen" - (as long as we aren't in "real" fullscreen mode)
			emscripten_exit_soft_fullscreen();

			// Actually trigger cleanup code
			if (saved_onShutdown)
			{
				saved_onShutdown();
			}
			wzShutdown();

			// Stop Emscripten from calling the main loop
			emscripten_cancel_main_loop();
#else
			ASSERT(arg != nullptr, "No valid bContinue");
			if (arg)
			{
				bool *bContinue = static_cast<bool*>(arg);
				*bContinue = false;
			}
#endif
			return;
		default:
			if (event.type >= SDL_EVENT_WINDOW_FIRST && event.type <= SDL_EVENT_WINDOW_LAST)
			{
				handleActiveEvent(&event);
			}
			break;
		}

		if (wzSDLAppEvent == event.type)
		{
			// Custom WZ App Event
			switch (event.user.code)
			{
				case wzSDLAppEventCodes::MAINTHREADEXEC:
					if (event.user.data1 != nullptr)
					{
						WZ_MAINTHREADEXEC * pExec = static_cast<WZ_MAINTHREADEXEC *>(event.user.data1);
						pExec->doExecOnMainThread();
						delete pExec;
					}
					break;
				default:
					break;
			}
		}
	}

	processScreenSizeChangeNotificationIfNeeded();
	mainLoop();				// WZ does its thing
	inputNewFrame();			// reset input states
#if defined(__EMSCRIPTEN__)
	lastLoopReturn = wzGetTicks();
#endif
}

// Actual mainloop
void wzMainEventLoop(std::function<void()> onShutdown)
{
	event.type = 0;

#if defined(__EMSCRIPTEN__)
	saved_onShutdown = onShutdown;
	// Receives a function to call and some user data to provide it.
	emscripten_set_main_loop_arg(wzEventLoopOneFrame, nullptr, -1, true);
#else
	bool bContinue = true;
	while (bContinue)
	{
		wzEventLoopOneFrame(&bContinue);
	}

	if (onShutdown)
	{
		onShutdown();
	}
#endif
}

void wzPumpEventsWhileLoading()
{
	SDL_PumpEvents();
}

void wzShutdown()
{
	// order is important!
	sdlFreeCursors();
	if (WZwindow != nullptr)
	{
		SDL_DestroyWindow(WZwindow);
		WZwindow = nullptr;
	}
	SDL_Quit();

	// delete copies of argc, argv
	if (copied_argv != nullptr)
	{
		for(int i=0; i < copied_argc; i++) {
			delete [] copied_argv[i];
		}
		delete [] copied_argv;
		copied_argv = nullptr;
		copied_argc = 0;
	}
}

// NOTE: wzBackendAttemptOpenURL should *not* be called directly - instead, call openURLInBrowser() from urlhelpers.h
bool wzBackendAttemptOpenURL(const char *url)
{
	return SDL_OpenURL(url);
}

// Gets the system RAM in MiB
uint64_t wzGetCurrentSystemRAM()
{
	int value = SDL_GetSystemRAM();
	return (value > 0) ? static_cast<uint64_t>(value) : 0;
}

uint32_t wzGetLogicalCPUCount()
{
	auto result = SDL_GetNumLogicalCPUCores();
	if (result <= 0)
	{
		debug(LOG_ERROR, "Failed to get logical CPU count - defaulting to 1");
		result = 1;
	}
	return static_cast<uint32_t>(result);
}

// MARK: - Emscripten-specific functions

#if defined(__EMSCRIPTEN__)

void wzemscripten_startup_ensure_canvas_displayed()
{
	MAIN_THREAD_EM_ASM({
		if (typeof wz_js_display_canvas === "function") {
			wz_js_display_canvas();
		}
		else {
			console.log('Cannot find wz_js_display_canvas function');
		}
	});
}

EM_BOOL wz_emscripten_window_resized_callback(int eventType, const void *reserved, void *userData)
{
	double width, height;
	emscripten_get_element_css_size("#canvas", &width, &height);

	int newWindowWidth = (int)width, newWindowHeight = (int)height;

	wzAsyncExecOnMainThread([newWindowWidth, newWindowHeight]{
		// resize SDL window
		SDL_SetWindowSize(WZwindow, newWindowWidth, newWindowHeight);

		unsigned int oldWindowWidth = windowWidth;
		unsigned int oldWindowHeight = windowHeight;
		handleWindowSizeChange(oldWindowWidth, oldWindowHeight, newWindowWidth, newWindowHeight);
		// Store the new values (in case the user manually resized the window bounds)
		war_SetWidth(newWindowWidth);
		war_SetHeight(newWindowHeight);
	});
	return EMSCRIPTEN_RESULT_SUCCESS;
}

bool wz_emscripten_enable_soft_fullscreen()
{
	// Enable "soft fullscreen" - where the canvas automatically fills the window
	debug(LOG_INFO, "Would enter soft fullscreen");
	EmscriptenFullscreenStrategy strategy;
	strategy.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH;
	strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_HIDEF;
	strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT;
	strategy.canvasResizedCallback = wz_emscripten_window_resized_callback;
	strategy.canvasResizedCallbackUserData = nullptr; // pointer to user data
	strategy.canvasResizedCallbackTargetThread = pthread_self(); // not used
	EMSCRIPTEN_RESULT result = emscripten_enter_soft_fullscreen("#canvas", &strategy);
	return result == EMSCRIPTEN_RESULT_SUCCESS || result == EMSCRIPTEN_RESULT_DEFERRED;
}

EM_BOOL wz_emscripten_fullscreenchange_callback(int eventType, const EmscriptenFullscreenChangeEvent *fullscreenChangeEvent, void *userData)
{
	if (!fullscreenChangeEvent->isFullscreen)
	{
		// browser left fullscreen, so reset soft fullscreen mode
		wzAsyncExecOnMainThread([]{

			war_setWindowMode(WINDOW_MODE::windowed); // persist the change

			wz_emscripten_enable_soft_fullscreen();

			// manually trigger resize callback
			wz_emscripten_window_resized_callback(EMSCRIPTEN_EVENT_FULLSCREENCHANGE, nullptr, nullptr);

		});
	}

	return EM_FALSE; // return false to ensure this event "bubbles up" to the SDL event handler
}

#endif
