#include <math.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>		/* NULL */
#include <stdlib.h>
#include <string.h>

#include "gen_defs.h"
#include "genwrap.h"
#include "dirwrap.h"
#include "xpbeep.h"
#include "threadwrap.h"
#include <xp_dl.h>

#if (defined CIOLIB_IMPORTS)
 #undef CIOLIB_IMPORTS
#endif
#if (defined CIOLIB_EXPORTS)
 #undef CIOLIB_EXPORTS
#endif

#include "ciolib.h"
#include "evdev_codes.h"
#include "utf8_codepages.h"
#include "vidmodes.h"
#define BITMAP_CIOLIB_DRIVER
#include "bitmap_con.h"
#include "scale.h"

#include "SDL.h"

#include "sdlfuncs.h"
#include "sdl_con.h"

#ifdef _MSC_VER
#pragma warning(disable : 4244 4267 4018)
#endif

int bitmap_width,bitmap_height;

/* 256 bytes so I can cheat */
unsigned char		sdl_keybuf[256];		/* Keyboard buffer */
unsigned char		sdl_key=0;				/* Index into keybuf for next key in buffer */
unsigned char		sdl_keynext=0;			/* Index into keybuf for next free position */

int sdl_exitcode=0;

bool internal_scaling = true;	// Protected by the win mutex

SDL_Window	*win=NULL;
SDL_Cursor	*curs=NULL;
SDL_Renderer	*renderer=NULL;
SDL_Texture	*texture=NULL;
pthread_mutex_t win_mutex;
pthread_mutex_t sdl_mode_mutex;
bool sdl_mode;
SDL_Surface	*sdl_icon=NULL;

sem_t sdl_ufunc_ret;
sem_t sdl_ufunc_rec;
pthread_mutex_t sdl_ufunc_mtx;
int sdl_ufunc_retval;

sem_t sdl_flush_sem;
int pending_updates=0;

int fullscreen = 0;

int	sdl_init_good=0;
pthread_mutex_t sdl_keylock;
sem_t sdl_key_pending;
static unsigned int sdl_pending_mousekeys=0;
static bool sdl_sync_initialized;

struct sdl_keyvals {
	int	keysym
		,key
		,shift
		,ctrl
		,alt
		,evdev;
};

static pthread_mutex_t sdl_headlock;
static struct rectlist *update_list = NULL;
static struct rectlist *update_list_tail = NULL;
extern int sdl_video_initialized;

#ifdef __DARWIN__
static int mac_width;
static int mac_height;
#define UPDATE_WINDOW_SIZE sdl.GetWindowSize(win, &mac_width, &mac_height)
#else
#define UPDATE_WINDOW_SIZE
#endif

enum {
	 SDL_USEREVENT_FLUSH
	,SDL_USEREVENT_SETTITLE
	,SDL_USEREVENT_SETNAME
	,SDL_USEREVENT_SETICON
	,SDL_USEREVENT_SETVIDMODE
	,SDL_USEREVENT_SHOWMOUSE
	,SDL_USEREVENT_HIDEMOUSE
	,SDL_USEREVENT_INIT
	,SDL_USEREVENT_QUIT
	,SDL_USEREVENT_GETWINPOS
	,SDL_USEREVENT_MOUSEPOINTER
};

const struct sdl_keyvals sdl_keyval[] =
{
	{SDLK_BACKSPACE, 0x08, 0x08, 0x7f, 0x0e00, EVDEV_KEY_BACKSPACE},
	{SDLK_TAB, 0x09, 0x0f00, 0x9400, 0xa500, EVDEV_KEY_TAB},
	{SDLK_RETURN, 0x0d, 0x0d, 0x0a, 0xa600, EVDEV_KEY_ENTER},
	{SDLK_ESCAPE, 0x1b, 0x1b, 0x1b, 0x0100, EVDEV_KEY_ESC},
	{SDLK_SPACE, 0x20, 0x20, 0x0300, 0x20, EVDEV_KEY_SPACE},
	{SDLK_0, '0', ')', 0, 0x8100, EVDEV_KEY_0},
	{SDLK_1, '1', '!', 0, 0x7800, EVDEV_KEY_1},
	{SDLK_2, '2', '@', 0x0300, 0x7900, EVDEV_KEY_2},
	{SDLK_3, '3', '#', 0, 0x7a00, EVDEV_KEY_3},
	{SDLK_4, '4', '$', 0, 0x7b00, EVDEV_KEY_4},
	{SDLK_5, '5', '%', 0, 0x7c00, EVDEV_KEY_5},
	{SDLK_6, '6', '^', 0x1e, 0x7d00, EVDEV_KEY_6},
	{SDLK_7, '7', '&', 0, 0x7e00, EVDEV_KEY_7},
	{SDLK_8, '8', '*', 0, 0x7f00, EVDEV_KEY_8},
	{SDLK_9, '9', '(', 0, 0x8000, EVDEV_KEY_9},
	{SDLK_a, 'a', 'A', 0x01, 0x1e00, EVDEV_KEY_A},
	{SDLK_b, 'b', 'B', 0x02, 0x3000, EVDEV_KEY_B},
	{SDLK_c, 'c', 'C', 0x03, 0x2e00, EVDEV_KEY_C},
	{SDLK_d, 'd', 'D', 0x04, 0x2000, EVDEV_KEY_D},
	{SDLK_e, 'e', 'E', 0x05, 0x1200, EVDEV_KEY_E},
	{SDLK_f, 'f', 'F', 0x06, 0x2100, EVDEV_KEY_F},
	{SDLK_g, 'g', 'G', 0x07, 0x2200, EVDEV_KEY_G},
	{SDLK_h, 'h', 'H', 0x08, 0x2300, EVDEV_KEY_H},
	{SDLK_i, 'i', 'I', 0x09, 0x1700, EVDEV_KEY_I},
	{SDLK_j, 'j', 'J', 0x0a, 0x2400, EVDEV_KEY_J},
	{SDLK_k, 'k', 'K', 0x0b, 0x2500, EVDEV_KEY_K},
	{SDLK_l, 'l', 'L', 0x0c, 0x2600, EVDEV_KEY_L},
	{SDLK_m, 'm', 'M', 0x0d, 0x3200, EVDEV_KEY_M},
	{SDLK_n, 'n', 'N', 0x0e, 0x3100, EVDEV_KEY_N},
	{SDLK_o, 'o', 'O', 0x0f, 0x1800, EVDEV_KEY_O},
	{SDLK_p, 'p', 'P', 0x10, 0x1900, EVDEV_KEY_P},
	{SDLK_q, 'q', 'Q', 0x11, 0x1000, EVDEV_KEY_Q},
	{SDLK_r, 'r', 'R', 0x12, 0x1300, EVDEV_KEY_R},
	{SDLK_s, 's', 'S', 0x13, 0x1f00, EVDEV_KEY_S},
	{SDLK_t, 't', 'T', 0x14, 0x1400, EVDEV_KEY_T},
	{SDLK_u, 'u', 'U', 0x15, 0x1600, EVDEV_KEY_U},
	{SDLK_v, 'v', 'V', 0x16, 0x2f00, EVDEV_KEY_V},
	{SDLK_w, 'w', 'W', 0x17, 0x1100, EVDEV_KEY_W},
	{SDLK_x, 'x', 'X', 0x18, 0x2d00, EVDEV_KEY_X},
	{SDLK_y, 'y', 'Y', 0x19, 0x1500, EVDEV_KEY_Y},
	{SDLK_z, 'z', 'Z', 0x1a, 0x2c00, EVDEV_KEY_Z},
	{SDLK_PAGEUP, 0x4900, 0x4900, 0x8400, 0x9900, EVDEV_KEY_PAGEUP},
	{SDLK_PAGEDOWN, 0x5100, 0x5100, 0x7600, 0xa100, EVDEV_KEY_PAGEDOWN},
	{SDLK_END, 0x4f00, 0x4f00, 0x7500, 0x9f00, EVDEV_KEY_END},
	{SDLK_HOME, 0x4700, 0x4700, 0x7700, 0x9700, EVDEV_KEY_HOME},
	{SDLK_LEFT, 0x4b00, 0x4b00, 0x7300, 0x9b00, EVDEV_KEY_LEFT},
	{SDLK_UP, 0x4800, 0x4800, 0x8d00, 0x9800, EVDEV_KEY_UP},
	{SDLK_RIGHT, 0x4d00, 0x4d00, 0x7400, 0x9d00, EVDEV_KEY_RIGHT},
	{SDLK_DOWN, 0x5000, 0x5000, 0x9100, 0xa000, EVDEV_KEY_DOWN},
	{SDLK_INSERT, CIO_KEY_IC, CIO_KEY_SHIFT_IC, CIO_KEY_CTRL_IC, CIO_KEY_ALT_IC, EVDEV_KEY_INSERT},
	{SDLK_DELETE, CIO_KEY_DC, CIO_KEY_SHIFT_DC, CIO_KEY_CTRL_DC, CIO_KEY_ALT_DC, EVDEV_KEY_DELETE},
	{SDLK_KP_0, 0x5200, 0x5200, 0x9200, 0, EVDEV_KEY_KP0},
	{SDLK_KP_1, 0x4f00, 0x4f00, 0x7500, 0, EVDEV_KEY_KP1},
	{SDLK_KP_2, 0x5000, 0x5000, 0x9100, 0, EVDEV_KEY_KP2},
	{SDLK_KP_3, 0x5100, 0x5100, 0x7600, 0, EVDEV_KEY_KP3},
	{SDLK_KP_4, 0x4b00, 0x4b00, 0x7300, 0, EVDEV_KEY_KP4},
	{SDLK_KP_5, 0x4c00, 0x4c00, 0x8f00, 0, EVDEV_KEY_KP5},
	{SDLK_KP_6, 0x4d00, 0x4d00, 0x7400, 0, EVDEV_KEY_KP6},
	{SDLK_KP_7, 0x4700, 0x4700, 0x7700, 0, EVDEV_KEY_KP7},
	{SDLK_KP_8, 0x4800, 0x4800, 0x8d00, 0, EVDEV_KEY_KP8},
	{SDLK_KP_9, 0x4900, 0x4900, 0x8400, 0, EVDEV_KEY_KP9},
	{SDLK_KP_MULTIPLY, '*', '*', 0x9600, 0x3700, EVDEV_KEY_KPASTERISK},
	{SDLK_KP_PLUS, '+', '+', 0x9000, 0x4e00, EVDEV_KEY_KPPLUS},
	{SDLK_KP_MINUS, '-', '-', 0x8e00, 0x4a00, EVDEV_KEY_KPMINUS},
	{SDLK_KP_PERIOD, 0x7f, 0x7f, 0x5300, 0x9300, EVDEV_KEY_KPDOT},
	{SDLK_KP_DIVIDE, '/', '/', 0x9500, 0xa400, EVDEV_KEY_KPSLASH},
	{SDLK_KP_ENTER, 0x0d, 0x0d, 0x0a, 0xa600, EVDEV_KEY_KPENTER},
	{SDLK_F1, 0x3b00, 0x5400, 0x5e00, 0x6800, EVDEV_KEY_F1},
	{SDLK_F2, 0x3c00, 0x5500, 0x5f00, 0x6900, EVDEV_KEY_F2},
	{SDLK_F3, 0x3d00, 0x5600, 0x6000, 0x6a00, EVDEV_KEY_F3},
	{SDLK_F4, 0x3e00, 0x5700, 0x6100, 0x6b00, EVDEV_KEY_F4},
	{SDLK_F5, 0x3f00, 0x5800, 0x6200, 0x6c00, EVDEV_KEY_F5},
	{SDLK_F6, 0x4000, 0x5900, 0x6300, 0x6d00, EVDEV_KEY_F6},
	{SDLK_F7, 0x4100, 0x5a00, 0x6400, 0x6e00, EVDEV_KEY_F7},
	{SDLK_F8, 0x4200, 0x5b00, 0x6500, 0x6f00, EVDEV_KEY_F8},
	{SDLK_F9, 0x4300, 0x5c00, 0x6600, 0x7000, EVDEV_KEY_F9},
	{SDLK_F10, 0x4400, 0x5d00, 0x6700, 0x7100, EVDEV_KEY_F10},
	{SDLK_F11, 0x8500, 0x8700, 0x8900, 0x8b00, EVDEV_KEY_F11},
	{SDLK_F12, 0x8600, 0x8800, 0x8a00, 0x8c00, EVDEV_KEY_F12},
	{SDLK_BACKSLASH, '\\', '|', 0x1c, 0x2b00, EVDEV_KEY_BACKSLASH},
	{SDLK_SLASH, '/', '?', 0, 0x3500, EVDEV_KEY_SLASH},
	{SDLK_MINUS, '-', '_', 0x1f, 0x8200, EVDEV_KEY_MINUS},
	{SDLK_EQUALS, '=', '+', 0, 0x8300, EVDEV_KEY_EQUAL},
	{SDLK_LEFTBRACKET, '[', '{', 0x1b, 0x1a00, EVDEV_KEY_LEFTBRACE},
	{SDLK_RIGHTBRACKET, ']', '}', 0x1d, 0x1b00, EVDEV_KEY_RIGHTBRACE},
	{SDLK_SEMICOLON, ';', ':', 0, 0x2700, EVDEV_KEY_SEMICOLON},
	{SDLK_QUOTE, '\'', '"', 0, 0x2800, EVDEV_KEY_APOSTROPHE},
	{SDLK_COMMA, ',', '<', 0, 0x3300, EVDEV_KEY_COMMA},
	{SDLK_PERIOD, '.', '>', 0, 0x3400, EVDEV_KEY_DOT},
	{SDLK_BACKQUOTE, '`', '~', 0x29E0, 0x2900, EVDEV_KEY_GRAVE},  /* Ctrl+` = CIO_KEY_WREN_CONSOLE */
	{0, 0, 0, 0, 0}	/** END **/
};

static uint16_t
sdl_evdev_key(const SDL_Keysym *keysym)
{
	for (int i = 0; sdl_keyval[i].keysym; i++) {
		if (sdl_keyval[i].keysym == keysym->sym)
			return sdl_keyval[i].evdev;
	}
	switch (keysym->scancode) {
		case SDL_SCANCODE_LCTRL:
			return EVDEV_KEY_LEFTCTRL;
		case SDL_SCANCODE_LSHIFT:
			return EVDEV_KEY_LEFTSHIFT;
		case SDL_SCANCODE_LALT:
			return EVDEV_KEY_LEFTALT;
		case SDL_SCANCODE_LGUI:
			return EVDEV_KEY_LEFTMETA;
		case SDL_SCANCODE_RCTRL:
			return EVDEV_KEY_RIGHTCTRL;
		case SDL_SCANCODE_RSHIFT:
			return EVDEV_KEY_RIGHTSHIFT;
		case SDL_SCANCODE_RALT:
			return EVDEV_KEY_RIGHTALT;
		case SDL_SCANCODE_RGUI:
			return EVDEV_KEY_RIGHTMETA;
		case SDL_SCANCODE_CAPSLOCK:
			return EVDEV_KEY_CAPSLOCK;
		case SDL_SCANCODE_NUMLOCKCLEAR:
			return EVDEV_KEY_NUMLOCK;
		case SDL_SCANCODE_SCROLLLOCK:
			return EVDEV_KEY_SCROLLLOCK;
		case SDL_SCANCODE_PRINTSCREEN:
			return EVDEV_KEY_SYSRQ;
		case SDL_SCANCODE_PAUSE:
			return EVDEV_KEY_PAUSE;
		case SDL_SCANCODE_NONUSBACKSLASH:
			return EVDEV_KEY_102ND;
		case SDL_SCANCODE_APPLICATION:
			return EVDEV_KEY_MENU;
		case SDL_SCANCODE_F13:
			return EVDEV_KEY_F13;
		case SDL_SCANCODE_F14:
			return EVDEV_KEY_F14;
		case SDL_SCANCODE_F15:
			return EVDEV_KEY_F15;
		case SDL_SCANCODE_F16:
			return EVDEV_KEY_F16;
		case SDL_SCANCODE_F17:
			return EVDEV_KEY_F17;
		case SDL_SCANCODE_F18:
			return EVDEV_KEY_F18;
		case SDL_SCANCODE_F19:
			return EVDEV_KEY_F19;
		case SDL_SCANCODE_F20:
			return EVDEV_KEY_F20;
		case SDL_SCANCODE_F21:
			return EVDEV_KEY_F21;
		case SDL_SCANCODE_F22:
			return EVDEV_KEY_F22;
		case SDL_SCANCODE_F23:
			return EVDEV_KEY_F23;
		case SDL_SCANCODE_F24:
			return EVDEV_KEY_F24;
		default:
			return 0;
	}
}

void sdl_video_event_thread(void *data);
static void setup_surfaces(struct video_stats *vs);
static int sdl_initsync(void);

static void sdl_user_func(int func, ...)
{
	va_list argptr;
	SDL_Event	ev;
	int rv;

	ev.type=SDL_USEREVENT;
	ev.user.data1=NULL;
	ev.user.data2=NULL;
	ev.user.code=func;
	assert_pthread_mutex_lock(&sdl_ufunc_mtx);
	while (1) {
		va_start(argptr, func);
		switch(func) {
			case SDL_USEREVENT_SETICON:
				ev.user.data1=va_arg(argptr, void *);
				if((ev.user.data2=(unsigned long *)malloc(sizeof(unsigned long)))==NULL) {
					assert_pthread_mutex_unlock(&sdl_ufunc_mtx);
					va_end(argptr);
					return;
				}
				*(unsigned long *)ev.user.data2=va_arg(argptr, unsigned long);
				break;
			case SDL_USEREVENT_SETNAME:
			case SDL_USEREVENT_SETTITLE:
				if((ev.user.data1=strdup(va_arg(argptr, char *)))==NULL) {
					assert_pthread_mutex_unlock(&sdl_ufunc_mtx);
					va_end(argptr);
					return;
				}
				break;
			case SDL_USEREVENT_MOUSEPOINTER:
				ev.user.data1 = (void *)(intptr_t)va_arg(argptr, int);
				break;
			case SDL_USEREVENT_SHOWMOUSE:
			case SDL_USEREVENT_HIDEMOUSE:
			case SDL_USEREVENT_FLUSH:
				break;
			default:
				assert_pthread_mutex_unlock(&sdl_ufunc_mtx);
				va_end(argptr);
				return;
		}
		va_end(argptr);
		while((rv = sdl.PeepEvents(&ev, 1, SDL_ADDEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT))!=1) {
			// coverity[sleep]
			YIELD();
		}
		break;
	}
	assert_pthread_mutex_unlock(&sdl_ufunc_mtx);
}

/* Called from main thread only */
static int sdl_user_func_ret(int func, ...)
{
	int rv;
	va_list argptr;
	SDL_Event	ev;

	ev.type=SDL_USEREVENT;
	ev.user.data1=NULL;
	ev.user.data2=NULL;
	ev.user.code=func;
	va_start(argptr, func);
	assert_pthread_mutex_lock(&sdl_ufunc_mtx);
	/* Drain the swamp */
	while(1) {
		switch(func) {
			case SDL_USEREVENT_SETVIDMODE:
				ev.user.data1 = (void *)(intptr_t)va_arg(argptr, int);
				ev.user.data2 = (void *)(intptr_t)va_arg(argptr, int);
				while(sdl.PeepEvents(&ev, 1, SDL_ADDEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT)!=1) {
					// coverity[sleep]
					YIELD();
				}
				break;
			case SDL_USEREVENT_GETWINPOS:
				ev.user.data1 = va_arg(argptr, void *);
				ev.user.data2 = va_arg(argptr, void *);
				// Fallthrough
			case SDL_USEREVENT_INIT:
			case SDL_USEREVENT_QUIT:
				while(sdl.PeepEvents(&ev, 1, SDL_ADDEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT)!=1)
					// coverity[sleep]
					YIELD();
				break;
			default:
				assert_pthread_mutex_unlock(&sdl_ufunc_mtx);
				va_end(argptr);
				return -1;
		}
		rv = sem_wait(&sdl_ufunc_ret);
		if(rv==0)
			break;
	}
	assert_pthread_mutex_unlock(&sdl_ufunc_mtx);
	va_end(argptr);
	return(sdl_ufunc_retval);
}

void exit_sdl_con(void)
{
	// Avoid calling exit(0) from an atexit() function...
	if (!sdl_sync_initialized)
		sdl_initsync();
	sdl_user_func_ret(SDL_USEREVENT_QUIT);
}

void sdl_copytext(const char *text, size_t buflen)
{
	sdl.SetClipboardText(text);
}

char *sdl_getcliptext(void)
{
	uint8_t *u8;
	u8 = (uint8_t *)sdl.GetClipboardText();
	char *ret = strdup((char *)u8);
	sdl.free(u8);
	return ret;
}

void sdl_drawrect(struct rectlist *data)
{
	if(sdl_init_good) {
		data->next = NULL;
		assert_pthread_mutex_lock(&sdl_headlock);
		if (update_list == NULL)
			update_list = update_list_tail = data;
		else {
			update_list_tail->next = data;
			update_list_tail = data;
		}
		assert_pthread_mutex_unlock(&sdl_headlock);
	}
	else
		bitmap_drv_free_rect(data);
}

void sdl_flush(void)
{
	sdl_user_func(SDL_USEREVENT_FLUSH);
}

static bool
sdl_get_bounds(int *w, int *h)
{
	SDL_Rect r;
	int ABUw, ABUh;
	int pixelw, pixelh;

	if (sdl.GetDisplayUsableBounds(0, &r) != 0)
		return false;
	sdl.GetWindowSize(win, &ABUw, &ABUh);
	sdl.GetWindowSizeInPixels(win, &pixelw, &pixelh);
	if (pixelw == 0 || pixelh == 0 || ABUw == 0 || ABUh == 0) {
		*w = r.w;
		*h = r.h;
		return true;
	}
	*w = ((uint64_t)r.w) * pixelw / ABUw;
	*h = ((uint64_t)r.h) * pixelh / ABUh;
	return true;
}

static int sdl_init_mode(int mode)
{
	int w, h;

	sdl_user_func(SDL_USEREVENT_FLUSH);

	assert_pthread_mutex_lock(&win_mutex);
	assert_rwlock_wrlock(&vstatlock);
	if (!sdl_get_bounds(&w, &h)) {
		w = 0;
		h = 0;
	}
	bitmap_drv_init_mode(mode, &bitmap_width, &bitmap_height, w, h);
	if (ciolib_initial_scaling < 1.0) {
		ciolib_initial_scaling = vstat.scaling;
		if (ciolib_initial_scaling < 1.0)
			ciolib_initial_scaling = 1.0;
	}
	assert_pthread_mutex_lock(&sdl_mode_mutex);
	sdl_mode = true;
	assert_pthread_mutex_unlock(&sdl_mode_mutex);
	assert_rwlock_unlock(&vstatlock);
	assert_pthread_mutex_unlock(&win_mutex);

	sdl_user_func_ret(SDL_USEREVENT_SETVIDMODE, vstat.winwidth, vstat.winheight);

	return(0);
}

/* Called from main thread only (Passes Event) */
int sdl_init(int mode)
{
	load_vmode(&vstat, ciolib_initial_mode);
	if (vstat.scaling < 1.0)
		vstat.scaling = ciolib_initial_scaling;
	if (vstat.scaling < 1.0)
		vstat.scaling = 1.0;
	// TODO: This is gross, why do we need it?
	vstat.winwidth = vstat.scrnwidth * vstat.scaling;
	vstat.winheight = vstat.scrnheight * vstat.scaling;
	internal_scaling = ciolib_initial_scaling_type == CIOLIB_SCALING_INTERNAL;
	bitmap_drv_init(sdl_drawrect, sdl_flush);

	if(mode==CIOLIB_MODE_SDL_FULLSCREEN) {
		assert_pthread_mutex_lock(&win_mutex);
		fullscreen = 1;
		assert_pthread_mutex_unlock(&win_mutex);
	}
	// Needs to be *after* bitmap_drv_init()
#if defined(__DARWIN__)
	sem_post(&startsdl_sem);
#else
	_beginthread(sdl_video_event_thread, 0, NULL);
#endif
	sdl_user_func_ret(SDL_USEREVENT_INIT);
	sdl_init_mode(ciolib_initial_mode);

	if(sdl_init_good) {
		cio_api.mode=fullscreen?CIOLIB_MODE_SDL_FULLSCREEN:CIOLIB_MODE_SDL;
#ifdef _WIN32
		FreeConsole();
#endif
		cio_api.options |= CONIO_OPT_PALETTE_SETTING | CONIO_OPT_SET_TITLE | CONIO_OPT_SET_NAME | CONIO_OPT_SET_ICON | CONIO_OPT_EXTERNAL_SCALING | CONIO_OPT_KEY_EVENTS;
		return(0);
	}

	sdl_user_func_ret(SDL_USEREVENT_QUIT);
	exit(0);
}

static void
update_cvstat(struct video_stats *vs)
{
	if (vs != NULL && vs != &vstat) {
		vstat.scaling = sdl_getscaling();
		assert_rwlock_rdlock(&vstatlock);
		*vs = vstat;
		assert_rwlock_unlock(&vstatlock);
	}
}

static void internal_setwinsize(struct video_stats *vs, bool force)
{
	int w, h;
	bool changed = true;

	w = vs->winwidth;
	h = vs->winheight;
	update_cvstat(vs);
	if (w > 16384)
		w = 16384;
	if (h > 16384)
		h = 16384;
	if (w < vs->scrnwidth)
		w = vs->scrnwidth;
	if (h < vs->scrnheight)
		h = vs->scrnheight;
	assert_pthread_mutex_lock(&win_mutex);
	assert_rwlock_wrlock(&vstatlock);
	if (w == vstat.winwidth && h == vstat.winheight)
		changed = force;
	else {
		vs->winwidth = vstat.winwidth = w;
		vs->winheight = vstat.winheight = h;
	}
	if (changed) {
		assert_rwlock_unlock(&vstatlock);
		assert_pthread_mutex_unlock(&win_mutex);
	}
	else {
		sdl.GetWindowSizeInPixels(win, &w, &h);
		UPDATE_WINDOW_SIZE;
		if (w != vs->winwidth || h != vs->winheight) {
			vs->winwidth = w;
			vs->winheight = h;
			changed = true;
		}
		assert_rwlock_unlock(&vstatlock);
		assert_pthread_mutex_unlock(&win_mutex);
		vstat.scaling = sdl_getscaling();
	}
	if (changed)
		setup_surfaces(vs);
}

void sdl_setwinsize(int w, int h)
{
	sdl_user_func_ret(SDL_USEREVENT_SETVIDMODE, w, h);
}

void sdl_setwinposition(int x, int y)
{
	assert_pthread_mutex_lock(&win_mutex);
	sdl.SetWindowPosition(win, x, y);
	assert_pthread_mutex_unlock(&win_mutex);
}

void sdl_getwinsize_locked(int *w, int *h)
{
	if (w)
		*w = vstat.winwidth;
	if (h)
		*h = vstat.winheight;
}

void sdl_getwinsize(int *w, int *h)
{
	assert_rwlock_rdlock(&vstatlock);
	sdl_getwinsize_locked(w, h);
	assert_rwlock_unlock(&vstatlock);
}

/* Called from main thread only */
int sdl_kbhit(void)
{
	int ret;

	assert_pthread_mutex_lock(&sdl_keylock);
	ret=(sdl_key!=sdl_keynext);
	assert_pthread_mutex_unlock(&sdl_keylock);
	return(ret);
}

/* Called from main thread only */
int sdl_getch(void)
{
	int ch;

	sem_wait(&sdl_key_pending);
	assert_pthread_mutex_lock(&sdl_keylock);

	/* This always frees up space in keybuf for one more char */
	ch=sdl_keybuf[sdl_key++];
	/* If we have missed mouse keys, tack them on to the end of the buffer now */
	if(sdl_pending_mousekeys) {
		if(sdl_pending_mousekeys & 1)	/* Odd number... second char */
			sdl_keybuf[sdl_keynext++]=CIO_KEY_MOUSE >> 8;
		else							/* Even number... first char */
			sdl_keybuf[sdl_keynext++]=CIO_KEY_MOUSE & 0xff;
		sem_post(&sdl_key_pending);
		sdl_pending_mousekeys--;
	}
	assert_pthread_mutex_unlock(&sdl_keylock);
	return(ch);
}

/* Called from main thread only */
void sdl_textmode(int mode)
{
	sdl_init_mode(mode);
}

/* Called from main thread only (Passes Event) */
void sdl_setname(const char *name)
{
	sdl_user_func(SDL_USEREVENT_SETNAME,name);
}

/* Called from main thread only (Passes Event) */
void sdl_seticon(const void *icon, unsigned long size)
{
	sdl_user_func(SDL_USEREVENT_SETICON,icon,size);
}

/* Called from main thread only (Passes Event) */
void sdl_settitle(const char *title)
{
	sdl_user_func(SDL_USEREVENT_SETTITLE,title);
}

int sdl_showmouse(void)
{
	sdl_user_func(SDL_USEREVENT_SHOWMOUSE);
	return(1);
}

int sdl_hidemouse(void)
{
	sdl_user_func(SDL_USEREVENT_HIDEMOUSE);
	return(0);
}

int sdl_get_window_info(int *width, int *height, int *xpos, int *ypos)
{
	int wx, wy;

	if (xpos || ypos) {
		sdl_user_func_ret(SDL_USEREVENT_GETWINPOS, &wx, &wy);
		if(xpos)
			*xpos=wx;
		if(ypos)
			*ypos=wy;
	}

	if (width || height) {
		assert_rwlock_rdlock(&vstatlock);
		if(width)
			*width=vstat.winwidth;
		if(height)
			*height=vstat.winheight;
		assert_rwlock_unlock(&vstatlock);
	}

	return(1);
}

static void
sdl_bughack_minsize(int w, int h, bool new)
{
	static int lw = -1;
	static int lh = -1;

	if ((!new) && w == lw && h == lh)
		return;
	lw = w;
	lh = h;
	sdl.SetWindowMinimumSize(win, w, h);
}

static void setup_surfaces(struct video_stats *vs)
{
	int		flags=0;
	SDL_Event	ev;
	SDL_Texture *newtexture;
	int idealw;
	int idealh;
	int idealmw;
	int idealmh;
	int new_win = false;

	assert_pthread_mutex_lock(&win_mutex);
	if(fullscreen)
		flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
	else
		flags |= SDL_WINDOW_RESIZABLE;
	assert_pthread_mutex_unlock(&win_mutex);
#if (SDL_MINOR_VERSION > 0) || (SDL_PATCHLEVEL >= 1)
        flags |= SDL_WINDOW_ALLOW_HIGHDPI;
#endif
	assert_rwlock_rdlock(&vstatlock);
	bitmap_get_scaled_win_size(1.0, &idealmw, &idealmh, 0, 0);
	assert_rwlock_unlock(&vstatlock);
	assert_pthread_mutex_lock(&win_mutex);
	idealw = vs->winwidth;
	idealh = vs->winheight;
	sdl.SetHint(SDL_HINT_RENDER_SCALE_QUALITY, internal_scaling ? "0" : "2");

	if (win == NULL) {
		// SDL2: This is slow sometimes... not sure why.
		new_win = true;
		if (sdl.CreateWindowAndRenderer(vs->winwidth, vs->winheight, flags, &win, &renderer) == 0) {
			sdl.GetWindowSizeInPixels(win, &idealw, &idealh);
			UPDATE_WINDOW_SIZE;
			vs->winwidth = idealw;
			vs->winheight = idealh;
			sdl.RenderClear(renderer);
			if (internal_scaling) {
				newtexture = sdl.CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, idealw, idealh);
			}
			else {
				newtexture = sdl.CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, vs->scrnwidth, vs->scrnheight);
			}

			if (texture)
				sdl.DestroyTexture(texture);
			texture = newtexture;
		}
		else {
			win = NULL;
			renderer = NULL;
		}
	}
	else {
		sdl_bughack_minsize(idealmw, idealmh, false);
		// Don't change window size when maximized...
		if ((sdl.GetWindowFlags(win) & SDL_WINDOW_MAXIMIZED) == 0) {
			int ABUw, ABUh;
			int pixelw, pixelh;
			uint64_t tmpw, tmph;

			// Workaround for systems where setWindowSize() units aren't pixels (ie: macOS)
			sdl.GetWindowSize(win, &ABUw, &ABUh);
			sdl.GetWindowSizeInPixels(win, &pixelw, &pixelh);
			tmpw = ((uint64_t)idealw) * ABUw / pixelw;
			tmph = ((uint64_t)idealh) * ABUh / pixelh;
			idealw = tmpw;
			idealh = tmph;
			sdl.SetWindowSize(win, idealw, idealh);
		}
		sdl.GetWindowSizeInPixels(win, &idealw, &idealh);
		UPDATE_WINDOW_SIZE;
		vs->winwidth = idealw;
		vs->winheight = idealh;
		if (internal_scaling) {
			newtexture = sdl.CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, idealw, idealh);
		}
		else {
			newtexture = sdl.CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, vs->scrnwidth, vs->scrnheight);
		}
		sdl.RenderClear(renderer);
		if (texture)
			sdl.DestroyTexture(texture);
		texture = newtexture;
	}
	if (vs != &vstat) {
		assert_rwlock_wrlock(&vstatlock);
		vstat.winwidth = vs->winwidth;
		vstat.winheight = vs->winheight;
		assert_rwlock_unlock(&vstatlock);
	}
	sdl_bughack_minsize(idealmw, idealmh, new_win);

	if(win!=NULL) {
		bitmap_drv_request_pixels();
	}
	else if(sdl_init_good) {
		ev.type=SDL_QUIT;
		sdl_exitcode=1;
		sdl.PeepEvents(&ev, 1, SDL_ADDEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT);
	}
	assert_pthread_mutex_unlock(&win_mutex);
	vstat.scaling = sdl_getscaling();
	update_cvstat(vs);
}

/* Called from event thread only */
static void sdl_add_key(unsigned int keyval, struct video_stats *vs)
{
	int w, h;

	if (keyval == 0xe0)
		keyval = CIO_KEY_LITERAL_E0;
	if (keyval == 0x9700) { // Alt-Home, centre screen
		assert_pthread_mutex_lock(&win_mutex);
		sdl.SetWindowPosition(win, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
		assert_pthread_mutex_unlock(&win_mutex);
	}
	if(keyval==0xa600 && vs != NULL) {
		assert_pthread_mutex_lock(&win_mutex);
		fullscreen = !(sdl.GetWindowFlags(win) & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_FULLSCREEN_DESKTOP));
		cio_api.mode=fullscreen?CIOLIB_MODE_SDL_FULLSCREEN:CIOLIB_MODE_SDL;
		update_cvstat(vs);
		sdl.SetWindowFullscreen(win, fullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
		sdl.SetWindowResizable(win, fullscreen ? SDL_FALSE : SDL_TRUE);

		// Get current window size
		sdl.GetWindowSizeInPixels(win, &w, &h);
		UPDATE_WINDOW_SIZE;
		// Limit to max window size if available
		if (!fullscreen)
			sdl_get_bounds(&w, &h);
		assert_pthread_mutex_unlock(&win_mutex);
		// Set size based on current max
		vs->scaling = bitmap_double_mult_inside(w, h);
		bitmap_get_scaled_win_size(vs->scaling, &vs->winwidth, &vs->winheight, w, h);
		setup_surfaces(vs);
		return;
	}
	if(keyval <= 0xffff) {
		assert_pthread_mutex_lock(&sdl_keylock);
		if(sdl_keynext+1==sdl_key) {
			assert_pthread_mutex_unlock(&sdl_keylock);
			beep();
			return;
		}
		if((sdl_keynext+2==sdl_key) && keyval > 0xff) {
			if(keyval==CIO_KEY_MOUSE) {
				sdl_pending_mousekeys+=2;
				assert_pthread_mutex_unlock(&sdl_keylock);
			}
			else {
				assert_pthread_mutex_unlock(&sdl_keylock);
				beep();
			}
			return;
		}
		sdl_keybuf[sdl_keynext++]=keyval & 0xff;
		sem_post(&sdl_key_pending);
		if(keyval>0xff) {
			sdl_keybuf[sdl_keynext++]=keyval >> 8;
			sem_post(&sdl_key_pending);
		}
		assert_pthread_mutex_unlock(&sdl_keylock);
	}
}

static void
sdl_add_key_uc(unsigned int keyval, struct video_stats *vs)
{
	if (keyval < 128)
		keyval = cpchar_from_unicode_cpoint(getcodepage(), keyval, keyval);
	sdl_add_key(keyval, vs);
}

/* Called from event thread only */
static unsigned int sdl_get_char_code(unsigned int keysym, unsigned int mod)
{
	int expect;
	int i;

	/* We don't handle META */
	if (mod & KMOD_GUI)
		return(0x0001ffff);

	/* Glah! */
#ifdef __DARWIN__
	if(keysym==0x7f && !(mod & KMOD_CTRL)) {
		keysym=0x08;
		keysym=SDLK_BACKSPACE;
	}
#endif

	/* Find the SDL keysym */
	for(i=0;sdl_keyval[i].keysym;i++) {
		if(sdl_keyval[i].keysym==keysym) {
			/* KeySym found in table */

			/*
			 * Using the modifiers, look up the expected scan code.
			 */
			if(mod & KMOD_ALT)
				expect = sdl_keyval[i].alt;
			else if(mod & KMOD_CTRL)
				expect=sdl_keyval[i].ctrl;
			else if(mod & KMOD_SHIFT) {
				if((mod & KMOD_CAPS) && keysym != '\t')
					expect=sdl_keyval[i].key;
				else
					expect=sdl_keyval[i].shift;
			}
			else {
				if(mod & KMOD_CAPS && (toupper(sdl_keyval[i].key) == sdl_keyval[i].shift))
					expect=sdl_keyval[i].shift;
				else
					expect=sdl_keyval[i].key;
			}

			return(expect);
		}
	}
	/*
	 * Well, we can't find it in our table, or it's a regular key.
	 */
	if(keysym > 0 && keysym < 128) {
		if (isalpha(keysym)) {
			/*
			 * If CAPS and SHIFT are not in the same state,
			 * upper-case.
			 */
			if(!!(mod & KMOD_CAPS) != !!(mod & KMOD_SHIFT))
				return toupper(keysym);
		}
		return keysym;
	}

	/* Give up.  It's not working out for us. */
	return(0x0001ffff);
}

static void
sdl_add_keys(uint8_t *utf8s, struct video_stats *vs)
{
	char *chars;
	char *p;

	chars = utf8_to_cp(getcodepage(), utf8s, '\x00', strlen((char *)utf8s), NULL);
	if (chars) {
		for (p = chars; *p; p++) {
			sdl_add_key(*((uint8_t *)p), vs);
		}
		free(chars);
	}
}

/* Mouse event/keyboard thread */
static void sdl_mouse_thread(void *data)
{
	SetThreadName("SDL Mouse");
	while(1) {
		if(mouse_wait())
			sdl_add_key(CIO_KEY_MOUSE, NULL);
	}
}

static void sdl_key_thread(void *data)
{
	SetThreadName("SDL Key");
	while (1) {
		if (ciokey_wait())
			sdl_add_key(CIO_KEY_KEY_EVENT, NULL);
	}
}

static int win_to_res_xpos(int winpos, struct video_stats *vs)
{
	int ret;
	int w, h;

	bitmap_get_scaled_win_size(vs->scaling, &w, &h, 0, 0);
#ifdef __DARWIN__
	winpos = winpos * vs->winwidth / mac_width;
#endif
	int xoff = (vs->winwidth - w) / 2;
	if (xoff < 0)
		xoff = 0;
	if (winpos < xoff)
		winpos = 0;
	else
		winpos -= xoff;
	if (winpos >= w)
		winpos = w - 1;
	ret = winpos * vs->scrnwidth / w;
	return ret;
}

static int win_to_res_ypos(int winpos, struct video_stats *vs)
{
	int ret;
	int w, h;

	bitmap_get_scaled_win_size(vs->scaling, &w, &h, 0, 0);
#ifdef __DARWIN__
	winpos = winpos * vs->winheight / mac_height;
#endif
	int yoff = (vs->winheight - h) / 2;
	if (yoff < 0)
		yoff = 0;
	if (winpos < yoff)
		winpos = 0;
	else
		winpos -= yoff;
	if (winpos >= h)
		winpos = h - 1;
	ret = winpos * vs->scrnheight / h;
	return ret;
}

static int win_to_text_xpos(int winpos, struct video_stats *vs)
{
	winpos = win_to_res_xpos(winpos, vs);
	return (winpos * vs->cols / vs->scrnwidth) + 1;
}

static int win_to_text_ypos(int winpos, struct video_stats *vs)
{
	winpos = win_to_res_ypos(winpos, vs);
	return (winpos * vs->rows / vs->scrnheight) + 1;
}

static void
handle_bios_key(uint32_t *bios_key, bool *bios_key_parsing, bool *zero_first)
{
	uint8_t ch;

	if (*bios_key > 0 && *bios_key_parsing) {
		if (*zero_first) {
			// Unicode character
			ch = cpchar_from_unicode_cpoint(getcodepage(), *bios_key, 0);
			if (ch == 0)
				sdl_beep();
			else
				sdl_add_key(ch, NULL);
		}
		else {
			// Codepage character
			ch = *bios_key;
			sdl_add_key(ch, NULL);
		}
	}
	*bios_key = 0;
	*bios_key_parsing = false;
	*zero_first = false;
}

void sdl_video_event_thread(void *data)
{
	SDL_Event	ev;
	int		block_text = 0;
	static SDL_Keycode last_sym = SDLK_UNKNOWN;
	static Uint16 last_mod = 0;
	struct video_stats cvstat = vstat;
	uint32_t bios_key = 0;
	bool bios_key_parsing = false;
	bool zero_first = false;
	int mods;
	uint16_t evdev;
	SDL_Keymod km;

	while(1) {
		if(sdl.WaitEventTimeout(&ev, 1)!=1)
			continue;
		switch (ev.type) {
			case SDL_KEYDOWN:			/* Keypress */
				evdev = sdl_evdev_key(&ev.key.keysym);
				if (evdev != 0)
					ciokey_gotevent(evdev, true);
				if (bios_key_parsing) {
					if (ev.key.keysym.sym >= SDLK_KP_1 && ev.key.keysym.sym <= SDLK_KP_0) {
						if (bios_key == 0 && ev.key.keysym.sym == SDLK_KP_0)
							zero_first = true;
						else {
							bool terminate_bios = false;
							if (zero_first) {
								if (bios_key >= 429496730 ||
								    (bios_key == 429496729 && ((ev.key.keysym.sym > SDLK_KP_5) || (ev.key.keysym.sym == SDLK_KP_0)))) {
									terminate_bios = true;
								}
							}
							else {
								if (bios_key >= 26 ||
								    (bios_key == 25 && ((ev.key.keysym.sym > SDLK_KP_5) || (ev.key.keysym.sym == SDLK_KP_0)))) {
									terminate_bios = true;
								}
							}
							if (terminate_bios) {
								handle_bios_key(&bios_key, &bios_key_parsing, &zero_first);
							}
							else {
								bios_key *= 10;
								if (ev.key.keysym.sym < SDLK_KP_0)
									bios_key += ((ev.key.keysym.sym - SDLK_KP_1) + 1);
								break;
							}
						}
					}
					else {
						handle_bios_key(&bios_key, &bios_key_parsing, &zero_first);
					}
				}

				if (ev.key.keysym.sym == SDLK_LALT || ev.key.keysym.sym == SDLK_RALT) {
					block_text = 1;
					bios_key = 0;
					bios_key_parsing = true;
					zero_first = false;
					break;
				}

				last_mod = ev.key.keysym.mod;
				last_sym = ev.key.keysym.sym;
				if ((ev.key.keysym.mod & (KMOD_CTRL|KMOD_ALT|KMOD_GUI)) && !(ev.key.keysym.mod & KMOD_MODE)) {
					block_text = 1;
					if ((ev.key.keysym.mod & KMOD_ALT) &&
					    (ev.key.keysym.sym == SDLK_LEFT ||
					     ev.key.keysym.sym == SDLK_RIGHT)) {
						assert_pthread_mutex_lock(&win_mutex);
						if (sdl.GetWindowFlags(win) & SDL_WINDOW_MAXIMIZED)
							sdl.RestoreWindow(win);
						int w, h;
						if (!sdl_get_bounds(&w, &h)) {
							w = 0;
							h = 0;
						}
						assert_pthread_mutex_unlock(&win_mutex);
						assert_rwlock_wrlock(&vstatlock);
						bitmap_snap(ev.key.keysym.sym == SDLK_RIGHT, w, h);
						assert_rwlock_unlock(&vstatlock);
						update_cvstat(&cvstat);
						setup_surfaces(&cvstat);
						break;
					}
				}
				if (ev.key.keysym.mod & KMOD_RALT) {	// Possible AltGr, let textinput sort it out...
					block_text = 0;
					break;
				}
				if ((ev.key.keysym.mod & KMOD_SHIFT) && (ev.key.keysym.sym == '\t'))
					block_text = 1;
				if (block_text || ev.key.keysym.sym < 0 || ev.key.keysym.sym > 127) {
					// NUMLOCK makes 
					if (ev.key.keysym.sym == SDLK_KP_DIVIDE
					    || ev.key.keysym.sym == SDLK_KP_MULTIPLY
					    || ev.key.keysym.sym == SDLK_KP_MINUS
					    || ev.key.keysym.sym == SDLK_KP_PLUS)
						break;
					if ((ev.key.keysym.mod & KMOD_NUM) && ((ev.key.keysym.sym >= SDLK_KP_1 && ev.key.keysym.sym <= SDLK_KP_0) || (ev.key.keysym.sym == SDLK_KP_PERIOD)))
						break;
					sdl_add_key_uc(sdl_get_char_code(ev.key.keysym.sym, ev.key.keysym.mod), &cvstat);
				}
				else if (!isprint(ev.key.keysym.sym)) {
					sdl_add_key_uc(ev.key.keysym.sym, &cvstat);
				}
				break;
			case SDL_TEXTINPUT:
				if (!block_text) {
					unsigned int charcode = sdl_get_char_code(last_sym, last_mod & ~(KMOD_ALT));
					// If the key is exactly what we would expect, use sdl_get_char_code()
					if (*(uint8_t *)ev.text.text == charcode)
						sdl_add_key_uc(sdl_get_char_code(last_sym, last_mod), &cvstat);
					else
						sdl_add_keys((uint8_t *)ev.text.text, &cvstat);
				}
				break;
			case SDL_KEYUP:
				evdev = sdl_evdev_key(&ev.key.keysym);
				if (evdev != 0)
					ciokey_gotevent(evdev, false);
				if (bios_key_parsing) {
					// If Mod1 (ie: ALT) is released, *and* the only bytes were KP numbers, do the BIOS thing.
					if (ev.key.keysym.sym == SDLK_LALT || ev.key.keysym.sym == SDLK_RALT) {
						handle_bios_key(&bios_key, &bios_key_parsing, &zero_first);
					}
				}

				last_mod = ev.key.keysym.mod;
				if (!(ev.key.keysym.mod & (KMOD_CTRL|KMOD_ALT|KMOD_GUI)))
					block_text = 0;
				break;
			case SDL_MOUSEMOTION:
				km = sdl.GetModState();
				mods = ((km & KMOD_SHIFT) ? CIOLIB_KMOD_SHIFT : 0) |
					((km & KMOD_CTRL) ? CIOLIB_KMOD_CTRL : 0) |
					((km & KMOD_ALT) ? CIOLIB_KMOD_ALT : 0);
				pthread_once(&ciolib_mouse_initialized, init_mouse);
				ciomouse_gotevent(CIOLIB_MOUSE_MOVE,win_to_text_xpos(ev.motion.x, &cvstat),win_to_text_ypos(ev.motion.y, &cvstat), win_to_res_xpos(ev.motion.x, &cvstat), win_to_res_ypos(ev.motion.y, &cvstat), mods);
				break;
			case SDL_MOUSEBUTTONDOWN:
				km = sdl.GetModState();
				mods = ((km & KMOD_SHIFT) ? CIOLIB_KMOD_SHIFT : 0) |
					((km & KMOD_CTRL) ? CIOLIB_KMOD_CTRL : 0) |
					((km & KMOD_ALT) ? CIOLIB_KMOD_ALT : 0);
				pthread_once(&ciolib_mouse_initialized, init_mouse);
				switch(ev.button.button) {
					case SDL_BUTTON_LEFT:
						ciomouse_gotevent(CIOLIB_BUTTON_PRESS(1),win_to_text_xpos(ev.button.x, &cvstat),win_to_text_ypos(ev.button.y, &cvstat), win_to_res_xpos(ev.button.x, &cvstat), win_to_res_ypos(ev.button.y, &cvstat), mods);
						break;
					case SDL_BUTTON_MIDDLE:
						ciomouse_gotevent(CIOLIB_BUTTON_PRESS(2),win_to_text_xpos(ev.button.x, &cvstat),win_to_text_ypos(ev.button.y, &cvstat), win_to_res_xpos(ev.button.x, &cvstat), win_to_res_ypos(ev.button.y, &cvstat), mods);
						break;
					case SDL_BUTTON_RIGHT:
						ciomouse_gotevent(CIOLIB_BUTTON_PRESS(3),win_to_text_xpos(ev.button.x, &cvstat),win_to_text_ypos(ev.button.y, &cvstat), win_to_res_xpos(ev.button.x, &cvstat), win_to_res_ypos(ev.button.y, &cvstat), mods);
						break;
				}
				break;
			case SDL_MOUSEWHEEL:
				km = sdl.GetModState();
				mods = ((km & KMOD_SHIFT) ? CIOLIB_KMOD_SHIFT : 0) |
					((km & KMOD_CTRL) ? CIOLIB_KMOD_CTRL : 0) |
					((km & KMOD_ALT) ? CIOLIB_KMOD_ALT : 0);
				pthread_once(&ciolib_mouse_initialized, init_mouse);
				if (ev.wheel.y) {
#if (SDL_MINOR_VERSION > 0) || (SDL_PATCHLEVEL > 3)
					if (ev.wheel.direction == SDL_MOUSEWHEEL_FLIPPED)
						ev.wheel.y = 0 - ev.wheel.y;
#endif
					if (ev.wheel.y > 0)
						ciomouse_gotevent(CIOLIB_BUTTON_PRESS(4), -1, -1, -1, -1, mods);
					if (ev.wheel.y < 0)
						ciomouse_gotevent(CIOLIB_BUTTON_PRESS(5), -1, -1, -1, -1, mods);
				}
				break;
			case SDL_MOUSEBUTTONUP:
				km = sdl.GetModState();
				mods = ((km & KMOD_SHIFT) ? CIOLIB_KMOD_SHIFT : 0) |
					((km & KMOD_CTRL) ? CIOLIB_KMOD_CTRL : 0) |
					((km & KMOD_ALT) ? CIOLIB_KMOD_ALT : 0);
				pthread_once(&ciolib_mouse_initialized, init_mouse);
				switch(ev.button.button) {
					case SDL_BUTTON_LEFT:
						ciomouse_gotevent(CIOLIB_BUTTON_RELEASE(1),win_to_text_xpos(ev.button.x, &cvstat),win_to_text_ypos(ev.button.y, &cvstat), win_to_res_xpos(ev.button.x, &cvstat), win_to_res_ypos(ev.button.y, &cvstat), mods);
						break;
					case SDL_BUTTON_MIDDLE:
						ciomouse_gotevent(CIOLIB_BUTTON_RELEASE(2),win_to_text_xpos(ev.button.x, &cvstat),win_to_text_ypos(ev.button.y, &cvstat), win_to_res_xpos(ev.button.x, &cvstat), win_to_res_ypos(ev.button.y, &cvstat), mods);
						break;
					case SDL_BUTTON_RIGHT:
						ciomouse_gotevent(CIOLIB_BUTTON_RELEASE(3),win_to_text_xpos(ev.button.x, &cvstat),win_to_text_ypos(ev.button.y, &cvstat), win_to_res_xpos(ev.button.x, &cvstat), win_to_res_ypos(ev.button.y, &cvstat), mods);
						break;
				}
				break;
			case SDL_QUIT:
				if (sdl_init_good)
					sdl_add_key(CIO_KEY_QUIT, &cvstat);
				else {
					sdl.QuitSubSystem(SDL_INIT_VIDEO);
					sdl_video_initialized = FALSE;
					return;
				}
				break;
			case SDL_WINDOWEVENT:
				switch(ev.window.event) {
					case SDL_WINDOWEVENT_MAXIMIZED:
					case SDL_WINDOWEVENT_RESTORED:
						// Fall-through
					case SDL_WINDOWEVENT_SIZE_CHANGED:
						// SDL2: User resized window
					case SDL_WINDOWEVENT_RESIZED:
						assert_pthread_mutex_lock(&win_mutex);
						fullscreen = !!(sdl.GetWindowFlags(win) & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_FULLSCREEN_DESKTOP));
						sdl.SetWindowResizable(win, fullscreen ? SDL_FALSE : SDL_TRUE);
						cio_api.mode=fullscreen?CIOLIB_MODE_SDL_FULLSCREEN:CIOLIB_MODE_SDL;
						assert_pthread_mutex_unlock(&win_mutex);
						assert_pthread_mutex_lock(&sdl_mode_mutex);
						if (sdl_mode) {
							assert_pthread_mutex_unlock(&sdl_mode_mutex);
							break;
						}
						assert_pthread_mutex_unlock(&sdl_mode_mutex);
						internal_setwinsize(&cvstat, false);
						break;
					case SDL_WINDOWEVENT_EXPOSED:
						bitmap_drv_request_pixels();
						break;
					case SDL_WINDOWEVENT_FOCUS_LOST:
						ciokey_focus_lost();
						break;
				}
				break;
			case SDL_USEREVENT: {
				struct rectlist *list;
				struct rectlist *old_next;
				switch(ev.user.code) {
					SDL_Rect dst;

					case SDL_USEREVENT_QUIT:
						sdl_ufunc_retval=0;
						sdl.QuitSubSystem(SDL_INIT_VIDEO);
						sdl_video_initialized = FALSE;
						sem_post(&sdl_ufunc_ret);
						return;
					case SDL_USEREVENT_FLUSH:
						update_cvstat(&cvstat);
						assert_rwlock_rdlock(&vstatlock);
						// Get correct aspect ratio for dst...
						int sw = cvstat.scrnwidth;
						int sh = cvstat.scrnheight;
						bitmap_get_scaled_win_size(cvstat.scaling, &dst.w, &dst.h, cvstat.winwidth, cvstat.winheight);
						assert_rwlock_unlock(&vstatlock);
						assert_pthread_mutex_lock(&win_mutex);
						if (win != NULL) {
							assert_pthread_mutex_unlock(&win_mutex);
							assert_pthread_mutex_lock(&sdl_headlock);
							assert_pthread_mutex_lock(&sdl_mode_mutex);
							list = update_list;
							update_list = update_list_tail = NULL;
							bool skipit = sdl_mode;
							assert_pthread_mutex_unlock(&sdl_mode_mutex);
							assert_pthread_mutex_unlock(&sdl_headlock);
							for (; list; list = old_next) {
								SDL_Rect src;

								old_next = list->next;
								if (list->next == NULL && !skipit && sw == list->rect.width && sh == list->rect.height) {
									void *pixels;
									int pitch;
									int row;
									int tw, th;

									if (internal_scaling) {
										struct graphics_buffer *gb;
										gb = do_scale(list, dst.w, dst.h);
										if (gb == NULL) {
											bitmap_drv_free_rect(list);
											break;
										}
										src.x = 0;
										src.y = 0;
										src.w = gb->w;
										src.h = gb->h;
										if (sdl.QueryTexture(texture, NULL, NULL, &tw, &th) != 0)
											fprintf(stderr, "Unable to query texture (%s)\n", sdl.GetError());
										sdl.RenderClear(renderer);
										if (sdl.LockTexture(texture, &src, &pixels, &pitch) != 0)
											fprintf(stderr, "Unable to lock texture (%s)\n", sdl.GetError());
										if (pitch != gb->w * sizeof(gb->data[0])) {
											// If this happens, we need to copy a row at a time...
											for (row = 0; row < gb->h && row < th; row++) {
												if (pitch < gb->w * sizeof(gb->data[0]))
													memcpy(pixels, &gb->data[gb->w * row], pitch);
												else
													memcpy(pixels, &gb->data[gb->w * row], gb->w * sizeof(gb->data[0]));
												pixels = (void *)((char*)pixels + pitch);
											}
										}
										else {
											int ch = gb->h;
											if (ch > th)
												ch = th;
											memcpy(pixels, gb->data, gb->w * ch * sizeof(gb->data[0]));
										}
										sdl.UnlockTexture(texture);
										dst.x = (cvstat.winwidth - gb->w) / 2;
										dst.y = (cvstat.winheight - gb->h) / 2;
										release_buffer(gb);
									}
									else {
										src.x = 0;
										src.y = 0;
										src.w = list->rect.width;
										src.h = list->rect.height;
										if (sdl.QueryTexture(texture, NULL, NULL, &tw, &th) != 0)
											fprintf(stderr, "Unable to query texture (%s)\n", sdl.GetError());
										sdl.RenderClear(renderer);
										if (sdl.LockTexture(texture, &src, &pixels, &pitch) != 0)
											fprintf(stderr, "Unable to lock texture (%s)\n", sdl.GetError());
										if (pitch != list->rect.width * sizeof(list->data[0])) {
											// If this happens, we need to copy a row at a time...
											for (row = 0; row < list->rect.height && row < th; row++) {
												if (pitch < list->rect.width * sizeof(list->data[0]))
													memcpy(pixels, &list->data[list->rect.width * row], pitch);
												else
													memcpy(pixels, &list->data[list->rect.width * row], list->rect.width * sizeof(list->data[0]));
												pixels = (void *)((char*)pixels + pitch);
											}
										}
										else {
											int ch = list->rect.height;
											if (ch > th)
												ch = th;
											memcpy(pixels, list->data, list->rect.width * ch * sizeof(list->data[0]));
										}
										sdl.UnlockTexture(texture);
										//aspect_fix(&dst.w, &dst.h, cvstat.aspect_width, cvstat.aspect_height);
										dst.x = (cvstat.winwidth - dst.w) / 2;
										dst.y = (cvstat.winheight - dst.h) / 2;
									}
									if (sdl.RenderCopy(renderer, texture, &src, &dst))
										fprintf(stderr, "RenderCopy() failed! (%s)\n", sdl.GetError());
									sdl.RenderPresent(renderer);
								}
								bitmap_drv_free_rect(list);
							}
						}
						else
							assert_pthread_mutex_unlock(&win_mutex);

						break;
					case SDL_USEREVENT_SETNAME:
						assert_pthread_mutex_lock(&win_mutex);
						sdl.SetWindowTitle(win, (char *)ev.user.data1);
						assert_pthread_mutex_unlock(&win_mutex);
						free(ev.user.data1);
						break;
					case SDL_USEREVENT_SETICON:
						if(sdl_icon != NULL)
							sdl.FreeSurface(sdl_icon);
						sdl_icon=sdl.CreateRGBSurfaceFrom(ev.user.data1
								, *(unsigned long *)ev.user.data2
								, *(unsigned long *)ev.user.data2
								, 32
								, *(unsigned long *)ev.user.data2*4
								, *(DWORD *)"\377\0\0\0"
								, *(DWORD *)"\0\377\0\0"
								, *(DWORD *)"\0\0\377\0"
								, *(DWORD *)"\0\0\0\377"
						);
						assert_pthread_mutex_lock(&win_mutex);
						sdl.SetWindowIcon(win, sdl_icon);
						assert_pthread_mutex_unlock(&win_mutex);
						free(ev.user.data2);
						break;
					case SDL_USEREVENT_SETTITLE:
						assert_pthread_mutex_lock(&win_mutex);
						sdl.SetWindowTitle(win, (char *)ev.user.data1);
						assert_pthread_mutex_unlock(&win_mutex);
						free(ev.user.data1);
						break;
					case SDL_USEREVENT_SETVIDMODE:
						assert_pthread_mutex_lock(&sdl_mode_mutex);
						sdl_mode = false;
						assert_pthread_mutex_unlock(&sdl_mode_mutex);

						cvstat.winwidth = (intptr_t)ev.user.data1;
						cvstat.winheight = (intptr_t)ev.user.data2;
						internal_setwinsize(&cvstat, true);
						sdl_ufunc_retval=0;
						sem_post(&sdl_ufunc_ret);
						break;
					case SDL_USEREVENT_HIDEMOUSE:
						sdl.ShowCursor(SDL_DISABLE);
						break;
					case SDL_USEREVENT_SHOWMOUSE:
						sdl.ShowCursor(SDL_ENABLE);
						break;
					case SDL_USEREVENT_INIT:
						if(!sdl_init_good) {
							if(sdl.WasInit(SDL_INIT_VIDEO)==SDL_INIT_VIDEO) {
								_beginthread(sdl_mouse_thread, 0, NULL);
								_beginthread(sdl_key_thread, 0, NULL);
								sdl_init_good=1;
							}
						}
						sdl_ufunc_retval=0;
						sem_post(&sdl_ufunc_ret);
						break;
					case SDL_USEREVENT_GETWINPOS:
						assert_pthread_mutex_lock(&win_mutex);
						sdl.GetWindowPosition(win, ev.user.data1, ev.user.data2);
						assert_pthread_mutex_unlock(&win_mutex);
						sem_post(&sdl_ufunc_ret);
						break;
					case SDL_USEREVENT_MOUSEPOINTER:
					{
						int cid = INT_MIN;
						SDL_Cursor *oc = curs;
						switch((intptr_t)ev.user.data1) {
							case CIOLIB_MOUSEPTR_ARROW:
								break;	// Default
							case CIOLIB_MOUSEPTR_BAR:
								cid = SDL_SYSTEM_CURSOR_IBEAM;
								break;
						}
						if (cid == INT_MIN) {
							sdl.SetCursor(sdl.GetDefaultCursor());
							curs = NULL;
						}
						else {
							curs = sdl.CreateSystemCursor(cid);
							if (curs == NULL)
								sdl.SetCursor(sdl.GetDefaultCursor());
							else
								sdl.SetCursor(curs);
						}
						if (oc)
							sdl.FreeCursor(oc);
						break;
					}
				}
				break;
			}
			case SDL_SYSWMEVENT:			/* ToDo... This is where Copy/Paste needs doing */

			/* Ignore this stuff */
			case SDL_JOYAXISMOTION:
			case SDL_JOYBALLMOTION:
			case SDL_JOYHATMOTION:
			case SDL_JOYBUTTONDOWN:
			case SDL_JOYBUTTONUP:
			default:
				break;
		}
	}
}

static int
sdl_initsync(void)
{
#if defined(__DARWIN__)
	if (initsdl_ret) {
#else
	if(init_sdl_video()) {
#endif
		return(-1);
	}
	sem_init(&sdl_key_pending, 0, 0);
	sem_init(&sdl_ufunc_ret, 0, 0);
	sem_init(&sdl_ufunc_rec, 0, 0);
	assert_pthread_mutex_init(&sdl_ufunc_mtx, NULL);
	assert_pthread_mutex_init(&sdl_headlock, NULL);
	assert_pthread_mutex_init(&win_mutex, NULL);
	assert_pthread_mutex_init(&sdl_keylock, NULL);
	assert_pthread_mutex_init(&sdl_mode_mutex, NULL);
	sdl_sync_initialized = true;
	return 0;
}

int sdl_initciolib(int mode)
{
	if (sdl_initsync() == -1)
		return -1;
	return(sdl_init(mode));
}

void
sdl_beep(void)
{
	static int16_t wave[2205 * 2];
	static bool    generated = false;

	if (!generated) {
		int i;

		xptone_makewave(440, wave, 2205, WAVE_SHAPE_SINE_SAW_HARM);
		/* -12 dB headroom (0.251× linear) — xptone_makewave no longer
		 * bakes it in and xp_play_sample16s has no opts channel.
		 * Baking it into the static wave on first use is the cheapest
		 * fix; the buffer is generated once per process. */
		for (i = 0; i < 2205 * 2; i++)
			wave[i] = (int16_t)((float)wave[i] * 0.251f);
		generated = true;
	}
	xp_play_sample16s(wave, 2205, TRUE);
}

/* Called from main thread only (Passes Event) */
int sdl_mousepointer(enum ciolib_mouse_ptr type)
{
	sdl_user_func(SDL_USEREVENT_MOUSEPOINTER,type);
	return(0);
}

double
sdl_getscaling(void)
{
	double ret;

	// TODO: I hate having nested locks like this. :(
	assert_rwlock_rdlock(&vstatlock);
	ret = bitmap_double_mult_inside(vstat.winwidth, vstat.winheight);
	assert_rwlock_unlock(&vstatlock);
	return ret;
}

void
sdl_setscaling(double newval)
{
	int w, h;

	assert_rwlock_rdlock(&vstatlock);
	bitmap_get_scaled_win_size(newval, &w, &h, 0, 0);
	assert_rwlock_unlock(&vstatlock);
	sdl_setwinsize(w, h);
}

enum ciolib_scaling
sdl_getscaling_type(void)
{
	enum ciolib_scaling ret;

	assert_rwlock_rdlock(&vstatlock);
	ret = (internal_scaling ? CIOLIB_SCALING_INTERNAL : CIOLIB_SCALING_EXTERNAL);
	assert_rwlock_unlock(&vstatlock);
	return ret;
}

void
sdl_setscaling_type(enum ciolib_scaling newval)
{
	struct video_stats cvstat = vstat;
	int w, h;

	update_cvstat(&cvstat);
	assert_rwlock_rdlock(&vstatlock);
	if ((newval == CIOLIB_SCALING_INTERNAL) != internal_scaling) {
		internal_scaling = (newval == CIOLIB_SCALING_INTERNAL);
		w = vstat.winwidth;
		h = vstat.winheight;
		assert_rwlock_unlock(&vstatlock);
		sdl_user_func_ret(SDL_USEREVENT_SETVIDMODE, w, h);
	}
	else {
		assert_rwlock_unlock(&vstatlock);
	}
}
