'OHRRPGCE - FreeBASIC gfxlib-based graphics backend
'(C) Copyright 1997-2020 James Paige, Ralph Versteegen, and the OHRRPGCE Developers
'Dual licensed under the GNU GPL v2+ and MIT Licenses. Read LICENSE.txt for terms and disclaimer of liability.

#include "config.bi"

#ifdef USE_X11
	#include "lib/SDL/SDL_x11clipboard.bi"
	#undef font

#if __FB_VER_MAJOR__ = 1 and __FB_VER_MINOR__ <= 5
	' HACK: FB 1.05 and earlier don't have a way to get the Display ptr, only the Window
	' But the Window ptr happens to be the first member of fb_x11
	type X11DRIVER
		display as Display ptr
		'rest omitted
	end type
	extern fb_x11 alias "fb_x11" as X11DRIVER
#endif

#elseif defined(__FB_WIN32__)
	include_windows_bi()
	#include "lib/SDL/SDL_windowsclipboard.bi"
#elseif defined(__FB_DARWIN__)
	#include "lib/SDL/SDL_cocoaclipboard.bi"
#endif

#include "util.bi"
#include "fbgfx.bi"
#include "surface.bi"
#include "gfx.bi"
#include "allmodex.bi"
#include "common.bi"

'Use the FB namespace for the types and constants from fbgfx
USING FB

#ifndef GFX_SCREEN_EXIT
 'Added in FB 1.08
 const as integer GFX_SCREEN_EXIT = &h80000000l
#endif


'a public rtlib function that they seem to have forgotten to expose to FB programs?
declare function fb_KeyHit alias "fb_KeyHit" () as long

extern "C"

'subs only used internally
declare sub calculate_and_set_screen_res(fullscreen as bool)
declare sub update_mouse_visibility()

dim shared nogfx as bool = NO   'Run as a console program only
dim shared noinput as bool = NO  'Only when nogfx: don't accept key input from stdin either (which freezes if it isn't a tty)
dim shared screen_buffer_offset as XYPair  'Position of the image on the window/screen, in screen pixels
dim shared window_state as WindowState
dim shared want_toggle_fullscreen as bool  'Alt-Enter pressed, change pending
dim shared init_gfx as bool = NO
'defaults are 2x zoom and 640x400 in 8-bit
dim shared zoom as integer = 2
dim shared remember_windowed_zoom as integer = 2  'What zoom was before entering fullscreen
dim shared framesize as XYPair = (320, 200) 'The resolution supplied by the engine
dim shared screensize as XYPair            'The size of the window/fullscreen resolution
dim shared screenpitch as fb_integer       'Bytes per screen row
dim shared depth as integer = 8            '8 or 32
dim shared smooth as integer = 0  '0 or 1
dim shared mouseclipped as bool = NO
dim shared mouse_visibility as CursorVisibility = cursorDefault
dim shared remember_windowtitle as string
dim shared as integer mxmin = -1, mxmax = -1, mymin = -1, mymax = -1
dim shared inputtext as string
dim shared extrakeys(127) as KeyBits

'Internal palette for 32-bit mode
dim shared truepal(255) as RGBcolor


'Don't bother with gfxmutex in this function, as polling thread hasn't started
function gfx_fb_init(byval terminate_signal_handler as sub cdecl (), byval windowicon as zstring ptr, byval info_buffer as zstring ptr, byval info_buffer_size as integer) as integer
	if init_gfx then return 0

	if nogfx then
		*info_buffer = "nogfx"
	else
		#ifdef USE_X11
			'Xlib will kill the program if most errors occur, such as if OpenGL on the machine is broken
			'so the window can't be created. We need to install an error handler to prevent that
			set_X11_error_handlers
		#endif

		dim bpp as fb_integer 'bits, not bytes. see, bits is b, bytes is B
		dim refreshrate as fb_integer
		dim driver as string
		dim as fb_integer w, h
		'Poll the size of the desktop (entire desktop across multiple displays), etc
		screeninfo w, h, bpp, , , refreshrate, driver
		debuginfo "gfx_fb: desktop size=" & w & "*" & h & " screen bitdepth=" & bpp & " refreshrate=" & refreshrate

		calculate_and_set_screen_res window_state.fullscreen
		screenset 1, 0    'Only want one FB video page
		screeninfo w, h, bpp, , , refreshrate, driver
		*info_buffer = MID(bpp & "bpp, " & refreshrate & "Hz, " & driver & " driver", 1, info_buffer_size)
	end if
	init_gfx = YES
	window_state.structsize = WINDOWSTATE_SZ
	window_state.focused = YES
	window_state.minimised = NO
	window_state.maximised = NO
	'This may be set by gfx_setwindowed before gfx_init
	'window_state.fullscreen = NO
	window_state.mouse_over = NO
	return 1
end function

'calculate_and_set_screen_res should be called instead of this
local sub gfx_fb_screenres()
	if nogfx then exit sub
	debuginfo "screenres " & screensize & " depth=" & depth & " fullscreen=" & window_state.fullscreen
	'GFX_NO_SWITCH: disable alt+enter to change to/from fullscreen, so we can handle it ourselves,
	'changing screensize at the same time. Otherwise, fbgfx often fails to switch to fullscreen,
	'I think it only accepts screenmodes with the same width as passed to screenres?
	if window_state.fullscreen = YES then
		screenres screensize.x, screensize.y, depth, 1, GFX_FULLSCREEN + GFX_NO_SWITCH
	else
		screenres screensize.x, screensize.y, depth, 1, GFX_WINDOWED + GFX_NO_SWITCH
	end if
	'Get new screenpitch
	'The int64 version of screeninfo doesn't have optional w,h args, so pass w,h so fbc
	'can pick whichever overload is suitable
	dim dummy as fb_integer
	screeninfo dummy, dummy, , , screenpitch
	'hook_fb_End is a kludge that works by setting the gfxlib2 exit hook,
	'which screenres sets, so have to reset it afterwards. See miscc.c
	hook_fb_End()
	update_mouse_visibility()
end sub

private sub update_mouse_visibility
	dim vis as integer  '0 or 1
	if mouse_visibility = cursorDefault then
		vis = iif(window_state.fullscreen, 0, 1)
	elseif mouse_visibility = cursorVisible then
		vis = 1
	else
		vis = 0
	end if
	setmouse  , , vis
end sub

'Recreate the window if needed, while possibly switching to/from fullscreen
local sub update_screen_mode_no_lock(to_fullscreen as optbool = NONBOOL)
	if to_fullscreen = NONBOOL then to_fullscreen = window_state.fullscreen
	calculate_and_set_screen_res to_fullscreen
	windowtitle remember_windowtitle
	'Palette must be re-set
	if depth = 8 then
		for i as integer = 0 to 255
			with truepal(i)
				palette i, .r, .g, .b
			end with
		next
	end if
end sub

sub gfx_fb_update_screen_mode()
	if init_gfx then
		GFX_ENTER
		update_screen_mode_no_lock
		GFX_EXIT
	end if
end sub

sub gfx_fb_close
	init_gfx = NO
	if nogfx then exit sub
	GFX_ENTER
	'GFX_SCREEN_EXIT prevents fbgfx from resetting and clearing the console
	screen 0, , , GFX_SCREEN_EXIT
	GFX_EXIT
end sub

function gfx_fb_getversion() as integer
	return 1
end function

'Called with gfxmutex held
sub gfx_fb_setpal(byval pal as RGBcolor ptr)
	dim as integer i
	if depth = 8 then
		for i = 0 to 255
			palette i, pal[i].r, pal[i].g, pal[i].b
		next
	end if
	'copy the palette, both for 32bit colour mode and when changing
	'res requires resetting the palette
	memcpy @truepal(0), pal, 256 * sizeof(RGBcolor)
	'FIXME: If running in 32 bitdepth, this does not update the page "live", like the 8-bit version
	'so fades won't working, and there's no way to force an
	'update from here at the moment because the screen buffer is not
	'accessible.
end sub

'Called with gfxmutex held
function gfx_fb_present(byval surfaceIn as Surface ptr, byval pal as RGBPalette ptr) as integer
	dim ret as integer = 0
	if nogfx then return 0

	dim newdepth as integer = iif(surfaceIn->format = .SF_32bit, 32, 8)
	'Once in 32-bit depth, don't go back to 8-bit, because recreating the window on a switch
	'(e.g. entering map editor) is nasty.
	if newdepth > depth orelse surfaceIn->size <> framesize then
		depth = newdepth
		framesize = surfaceIn->size
		update_screen_mode_no_lock
	end if

	screenlock
	dim as integer bytespp = depth \ 8
	dim as ubyte ptr screenpixels = screenptr + screen_buffer_offset.x * bytespp + screen_buffer_offset.y * screenpitch

	with *surfaceIn
		if .format = SF_8bit then
			gfx_fb_setpal(cast(RGBcolor ptr, @pal->col(0)))

			if depth = 8 then
				smoothzoomblit_8_to_8bit(.pPaletteData, screenpixels, .size, screenpitch, zoom, smooth)
			elseif depth = 32 then
				smoothzoomblit_8_to_32bit(.pPaletteData, cast(uint32 ptr, screenpixels), .size, screenpitch \ 4, zoom, smooth, @truepal(0))
			end if
		else  '32 bit
			if depth = 32 then
				smoothzoomblit_32_to_32bit(.pColorData, cast(uint32 ptr, screenpixels), .size, screenpitch \ 4, zoom, smooth)
			else
				ret = 1
			end if
		end if
	end with

	screenunlock
	flip

	return ret
end function

function gfx_fb_screenshot(byval fname as zstring ptr) as integer
	gfx_fb_screenshot = 0
end function

sub gfx_fb_setwindowed(byval iswindow as integer)
	dim wantfullscreen as bool = iif(iswindow, NO, YES)
	if window_state.fullscreen = wantfullscreen then exit sub

	if debugging_io then debuginfo "setwindowed fullscreen=" & wantfullscreen

	if wantfullscreen = NO then zoom = remember_windowed_zoom
	GFX_ENTER
	update_screen_mode_no_lock wantfullscreen
	GFX_EXIT
end sub

'Called with gfxmutex held
sub gfx_fb_windowtitle(byval title as zstring ptr)
	remember_windowtitle = *title
	windowtitle *title
end sub

function gfx_fb_getwindowstate() as WindowState ptr
	window_state.windowsize = screensize
	window_state.zoom = zoom
	return @window_state
end function

sub gfx_fb_get_screen_size(wide as integer ptr, high as integer ptr)
	dim as ssize_t wide_, high_  'for 64 bit builds
	'This returns the size of the display on which our window is, as desired, not the whole desktop size.
	'However, if we've fullscreened it returns the fullscreen resolution.
	GFX_ENTER
	ScreenControl GET_DESKTOP_SIZE, wide_, high_
	GFX_EXIT
	*wide = wide_
	*high = high_
end sub

function gfx_fb_supports_variable_resolution() as bool
	'Yes, but we don't support setting the window to resizable, and probably never will.
	return YES
end function

sub gfx_fb_get_settings(byref settings as GfxSettings)
	settings.upscaler = smooth   '0/1
	settings.nogfx = nogfx
end sub

sub gfx_fb_set_settings(settings as GfxSettings)
	smooth = settings.upscaler
end sub

function gfx_fb_setoption(byval opt as zstring ptr, byval arg as zstring ptr) as integer
'handle command-line options in a generic way, so that they
'can be ignored or supported as the library permits.
'This version supports
'	zoom (1, 2*, ..., 16),
'	smooth (0*, 1)
'Changing mode after window already created isn't well tested!
	dim as integer value = str2int(*arg, -1)
	dim as integer ret = 0
	dim as bool screen_mode_changed = NO
	if *opt = "zoom" or *opt = "z" then
		if value >= 1 and value <= 16 then
			if window_state.fullscreen then
				'We are using a computed zoom, ignore the request
				remember_windowed_zoom = value
			else
				zoom = value
				screen_mode_changed = YES
			end if
		end if
		ret = 1
	elseif *opt = "smooth" or *opt = "s" then
		if value = 1 or value = -1 then  'arg optional
			smooth = 1
		else
			smooth = 0
		end if
		ret = 1
	elseif *opt = "nogfx" then
		debuginfo "gfx_fb: --nogfx"
		nogfx = YES
		ret = 1
	elseif *opt = "noinput" then
		debuginfo "gfx_fb: --noinput"
		noinput = YES
		ret = 1
	end if
	'all these take an optional numeric argument, so gobble the arg if it is
	'a number, whether or not it was valid
	if ret = 1 and parse_int(*arg) then ret = 2

	if screen_mode_changed then
		gfx_fb_update_screen_mode
	end if

	return ret
end function

'Figure out what size the window/fullscreen resolution should be, and then recreate the window if needed
sub calculate_and_set_screen_res(fullscreen as bool)
	if fullscreen = NO then
		screensize = framesize * zoom
		screen_buffer_offset = 0
	else
		'Search for a fullscreen resolution that has near-native aspect ratio,
		'and is large enough but no larger than needed, together with a zoom level.

		'Find the native aspect ratio, by getting the largest available resolution,
		'which should be the last one returned from screenlist. We can't just
		'call screeninfo or screencontrol, because if we're fullscreen they return
		'the fullscreen resolution.
		dim modesize as XYPair
		dim mode as integer = screenlist(depth)
		while mode
			modesize = XY(hiword(mode), loword(mode))
			mode = screenlist
		wend
		dim native_aspect_ratio as double
		if modesize.y then native_aspect_ratio = modesize.x / modesize.y

		'debug "max screen size " & modesize & " ratio " & native_aspect_ratio & " zoom " & zoom

		screensize = 0
		dim best_ratio as double = 999
		dim best_cover as double = 0
		dim best_zoom as integer = 0

		mode = screenlist(depth)
		while mode
			modesize = XY(hiword(mode), loword(mode))
			dim ratio as double = modesize.x / modesize.y

			'Try multiple zoom levels
			for tryzoom as integer = 1 to 4
				dim imagesize as XYPair = framesize * tryzoom

				if modesize >= imagesize then
					'screenlist returns resolutions in increasing size, so don't
					'mark a mode best unless it's an improvement
					dim cover as double = (imagesize.x * imagesize.y) / (modesize.x * modesize.y)
					'debug "zoom " & tryzoom & " ratio " & ratio & " dist " & abs(ratio - native_aspect_ratio) & " cover " & cover
					if abs(ratio - native_aspect_ratio) <= abs(best_ratio - native_aspect_ratio) + 0.025 andalso _
					   cover >= best_cover + 0.01 then
						'debug "---best"
						screensize = modesize
						best_zoom = tryzoom
						best_ratio = ratio
						best_cover = cover
					end if
				end if
			next
			mode = screenlist()
		wend
		if best_zoom = 0 then
			debug "failed to find fullscreen mode"
			calculate_and_set_screen_res(NO)
			exit sub
		end if

		remember_windowed_zoom = zoom
		zoom = best_zoom

		screen_buffer_offset = (screensize - framesize * zoom) \ 2
	end if

	if init_gfx then
		'Check whether the new screen mode actually differs from existing
		dim as fb_integer w, h, bpp
		screeninfo w, h, bpp
		if XY(w, h) = screensize andalso bpp = depth andalso window_state.fullscreen = fullscreen then
			'No, it doesn't
			exit sub
		end if
	end if

	window_state.fullscreen = fullscreen
	gfx_fb_screenres
end sub

function gfx_fb_describe_options() as zstring ptr
	return @"-z -zoom [1...16]   Scale screen to 1,2, ... up to 16x normal size (2x default)" LINE_END _
	        "-s -smooth          Enable smoothing filter for zoom modes (default off)" LINE_END _
		"-nogfx              (Unix only) Don't create a window, commandline only. Combine with --print." LINE_END _
		"-noinput            (Unix only) Combine with --nogfx to not read key input, allows running without a tty."
end function

'------------- IO Functions --------------
sub io_fb_init
end sub

sub process_key_event(e as Event, byval value as KeyBits)
	'NOTE: numpad 5 seems to be broken on Windows, events for that key have scancode = 0 regardless of numlock state!

	'On linux, Pause, PrintScreen, and WindowsKey keypresses send events with scancode 0, and multikey shows nothing.

	select case e.scancode
		case scHome to scPageUp, scLeft to scRight, scEnd to scDelete
			'If numlock is on, then when a numerical/period numpad key is pressed the key will
			'generate ascii 0-9 or ., and can be differentiated from arrow and home, etc., keys.
			'Otherwise they both return e.ascii = 0
			if e.ascii then
				extrakeys(e.scancode - scHome + scNumpad7) = value
			else
				extrakeys(e.scancode) = value
			end if
	end select
end sub

private sub debug_key_event(e as Event, eventname as zstring ptr)
	if debugging_io then
		debuginfo *eventname & " scan=" & e.scancode & " (" & scancodename(e.scancode) & ") ascii=" & e.ascii
	end if
end sub

sub process_events()
	if noinput then exit sub

	static last_enter_state as integer
	dim e as Event
	while ScreenEvent(@e)
		'unhide the mouse when the window loses focus
		if e.type = EVENT_WINDOW_LOST_FOCUS then
			if debugging_io then debuginfo "EVENT_WINDOW_LOST_FOCUS"
			setmouse , , 1
			window_state.focused = NO
		elseif e.type = EVENT_WINDOW_GOT_FOCUS then
			if debugging_io then debuginfo "EVENT_WINDOW_GOT_FOCUS"
			update_mouse_visibility()
			window_state.focused = YES
		elseif e.type = EVENT_KEY_PRESS then
			debug_key_event(e, "EVENT_KEY_PRESS")
			if e.ascii <> 0 then inputtext += chr(e.ascii)
			process_key_event(e, 8)
		elseif e.type = EVENT_KEY_REPEAT then
			debug_key_event(e, "EVENT_KEY_REPEAT")
			if e.ascii <> 0 then inputtext += chr(e.ascii)
		elseif e.type = EVENT_KEY_RELEASE then
			debug_key_event(e, "EVENT_KEY_RELEASE")
			process_key_event(e, 0)
		else
			if debugging_io then debuginfo "EVENT " & e.type & " " & e.z
		end if
	wend

	'Don't eat memory if io_textinput is never called
	inputtext = RIGHT(inputtext, 128)

	'the polling thread ought to ensure that these are caught timeously
	'inkey does not seem to be threadsafe (bug 790)
	'if inkey = chr(255) + "k" then post_terminate_signal
	while fb_keyhit
		dim a as integer = getkey
		'there are two different getkey values that cause fb_GfxInkey to return "\255k"
		if a = &h100 or a = &h6bff then post_terminate_signal
	wend
	if multikey(SC_ALT) andalso multikey(SC_F4) then post_terminate_signal

	'Handle alt-enter; we've disabled fbgfx's builtin handling
	'(The window maximise button should be disabled)
	if multikey(SC_ALT) andalso multikey(SC_ENTER) andalso last_enter_state = 0 then
		'debug "ALT ENTER " & main_thread_in_gfx_backend
		'Can't call gfx_fb_setwindowed here, since we might not be on the
		'main thread, so delay it
		want_toggle_fullscreen = YES
	end if
	last_enter_state = multikey(SC_ENTER)
end sub

function gfx_fb_get_resize(byref ret as XYPair) as bool
	'Kludge: this seems the easiest place to act on alt-enter
	'(We can't do this inside gfx_present, because gfxmutex (GFX_ENTER)
	'is held in there, and obtained in gfx_fb_setwindowed; and
	'io_pollkeyevents, io_waitprocessing aren't always called)
	if want_toggle_fullscreen then
		want_toggle_fullscreen = NO
		'Note the inversion: If fullscreen, go windowed
		gfx_fb_setwindowed window_state.fullscreen
		post_event(eventFullscreened, window_state.fullscreen)
	end if
	return NO
end function

sub io_fb_pollkeyevents()
	'not really needed by this backend
	GFX_ENTER
	process_events()
	GFX_EXIT
end sub

sub io_fb_waitprocessing()
	'not needed by this backend
end sub

#ifdef __FB_WIN32__
'vkey: winapi virtual keycode
'is_toggle: read toggle state of the key
local sub vkey_to_keybits(vkey as integer, is_toggle as bool, byref key as KeyBits)
	dim mask as integer = iif(is_toggle, 1, &h8000)
	key = (key and not 8) or iif(GetKeyState(vkey) and mask, 8, 0)
end sub
#endif

'Called with gfxmutex held
sub io_fb_updatekeys(byval keybd as KeyBits ptr)
	process_events()

	for key as KBScancode = 0 to 127
		select case key
			'Pressing PrntScr causes multikey(0) to return true
			case 1 to scHome - 1, scNumpadMinus, scNumpadPlus, scDelete + 1 to scContext
				if multikey(key) then
					keybd[key] or= 8
				end if

			case scNumpad5
				'Events for this key are broken, luckily on X11 can fall back to this.
				'This doesn't work on Windows, but we handle Numpad5 there below.
				if multikey(76) then  'Not an OHR scancode
					keybd[key] or= 8
				end if

			case scHome to scDelete, scNumpad7 to scNumpadPeriod
				keybd[key] or= extrakeys(key)

			'other keys are undetectable with fbgfx!
		end select
	next

	'fbgfx reports separate shift keys, but combined alt and ctrl keys

	keybd[scShift] or= (keybd[scLeftShift] or keybd[scRightShift]) and 8
	keybd[scLeftAlt] or= keybd[scUnfilteredAlt] and 8
	keybd[scRightAlt] or= keybd[scUnfilteredAlt] and 8
	keybd[scLeftCtrl] or= keybd[scCtrl] and 8
	keybd[scRightCtrl] or= keybd[scCtrl] and 8
	'scMeta unsupported

	'Some other keys are also indistinguishable, and are mirrored
	keybd[scNumpadSlash] or= keybd[scSlash] and 8
	keybd[scNumpadEnter] or= keybd[scEnter] and 8
	keybd[scPrintScreen] or= keybd[scNumpadAsterisk] and 8
	keybd[scPause] or= keybd[scNumlock] and 8

	#ifdef __FB_WIN32__
		'Lots of keys broken in fbgfx, but at least on Windows it's easy to
		'workaround the problem by calling the winapi.
		'ScrollLock works in fbgfx, but it doesn't act as a toggle.
		vkey_to_keybits VK_SCROLL, YES, keybd[scScrollLock]
		'Numpad5 broken on Windows, OK on X11
		'(VK_NUMPAD5 only appears presses when numlock is on, but that's consistent)
		vkey_to_keybits VK_NUMPAD5, NO, keybd[scNumpad5]
		'None of the following work properly on any OS
		vkey_to_keybits VK_NUMLOCK, YES, keybd[scNumlock]
		vkey_to_keybits VK_SNAPSHOT, NO, keybd[scPrintScreen]
		'This doesn't work well, it misses most keypresses. Pause is weird.
		vkey_to_keybits VK_PAUSE, NO, keybd[scPause]
	#endif
end sub

sub io_fb_textinput (byval buf as wstring ptr, byval bufsize as integer)
	dim buflen as integer = bufsize \ 2 - 1
	*buf = LEFT(inputtext, buflen)
	inputtext = MID(inputtext, buflen)
end sub

SUB io_fb_show_virtual_keyboard()
	'Does nothing on platforms that have real keyboards
END SUB

SUB io_fb_hide_virtual_keyboard()
	'Does nothing on platforms that have real keyboards
END SUB

sub io_fb_setmousevisibility(visibility as CursorVisibility)
	GFX_ENTER
	mouse_visibility = visibility
	update_mouse_visibility()
	GFX_EXIT
end sub

'Called with gfxmutex held
sub io_fb_getmouse(byref mx as integer, byref my as integer, byref mwheel as integer, byref mbuttons as integer)
	static lastpos as XYPair, lastwheel as integer = 0, lastbuttons as integer = 0
	dim as integer dmx, dmy, dw, db, remx, remy
	if getmouse(dmx, dmy, dw, db) = 0 then
		'mouse is inside window
		window_state.mouse_over = YES
		if mouseclipped then
			remx = dmx
			remy = dmy
			dmx = bound(dmx, mxmin, mxmax)
			dmy = bound(dmy, mymin, mymax)
			'calling setmouse at the same position rapidly causes the mouse to crawl
			if remx <> dmx or remy <> dmy then setmouse dmx, dmy
		end if
		lastpos = (XY(dmx, dmy) - screen_buffer_offset) \ zoom
		lastwheel = 120 * dw
		lastbuttons = db
	else
		window_state.mouse_over = NO
	end if
	'(At least on X11) when dragging off the window we may see first OFFSCREEN position instead of
	'last onscreen, due to fbgfx freezing mouse input fractionally late.
	'All other gfx backends clamp the return value to the window bounds, so do the same here.
	mx = bound(lastpos.x, 0, framesize.w - 1)
	my = bound(lastpos.y, 0, framesize.h - 1)
	mwheel = lastwheel
	mbuttons = lastbuttons
end sub

'Called with gfxmutex held
sub io_fb_setmouse(byval x as integer, byval y as integer)
	dim mpos as XYPair = XY(x, y) * zoom + screen_buffer_offset
	setmouse(mpos.x, mpos.y)
end sub

'Called with gfxmutex held
sub io_fb_mouserect(byval xmin as integer, byval xmax as integer, byval ymin as integer, byval ymax as integer)
	mxmin = screen_buffer_offset.x + xmin * zoom
	mxmax = screen_buffer_offset.x + xmax * zoom + zoom - 1
	mymin = screen_buffer_offset.y + ymin * zoom
	mymax = screen_buffer_offset.y + ymax * zoom + zoom - 1
	if xmin >= 0 then
		'enable clipping
		mouseclipped = YES
		setmouse  , , , 1
	else
		'disable clipping
		mouseclipped = NO
		setmouse  , , , 0
	end if
end sub

'Called with gfxmutex held
function io_fb_get_joystick_state(byval joynum as integer, byval state as IOJoystickState ptr) as integer
	if window_state.focused = NO then return 3  'Not focused

	if joynum > 15 then return 1  'Out of range (fbgfx only supports 16 joysticks)
	static joystick_counter as integer
	'state.instance_id mapping. Used to tell report whether this is a new joystick or not
	static joystick_ids(15) as integer
	static joystick_infos(15) as JoystickInfo

	'Axes which are not present are set to -1000.
	'Those which are present aren't necessarily consecutive on Windows, since there fbgfx
	'sets the axes like so (accessed with plain winapi joyGetPosEx and joyGetDevCaps):
	' 0: X
	' 1: Y
	' 2: Z
	' 3: R
	' 4: U
	' 5: V
	' 6,7: POV hat (direction converted by fbgfx to possible values {-1,0,1} for each axis)
	'On linux, fbgfx reads events from /dev/input/js# and /dev/js#, which seem to
	'treat POV hats as extra axes, not necessarily starting at 6, so no way to distinguish them.
	dim ax(7) as single
	dim as size_t button_bits
	if getjoystick(joynum, button_bits, ax(0), ax(1), ax(2), ax(3), ax(4), ax(5), ax(6), ax(7)) then
		'getjoystick returns 1 on failure if the joystick can't be opened
		if joystick_ids(joynum) then
			joystick_ids(joynum) = 0
			return 2  'Joystick lost
		else
			return 1  'No joystick
		end if
	end if

	dim byref info as JoystickInfo = joystick_infos(joynum)
	state->info = @info
	memset @info, 0, sizeof(info)
	info.structsize = JOYSTICKINFO_SZ

	for i as integer = 0 to 7
		if ax(i) <> -1000 then
			if debugging_io then debuginfo "ax " & i & " " & ax(i)
#ifdef __FB_WIN32__
			if i >= 6 then
				'POV hat (see comment above)
				if ax(i) then
					dim bitnum as integer = 2*(i-6) + iif(ax(i) > 0, 1, 0)
					state->hats(0) or= 1 shl bitnum
				end if
				info.num_hats = 1
				continue for
			end if
#endif
			state->axes(info.num_axes) = 1000 * ax(i)
			info.num_axes += 1
		end if
	next

	state->buttons_down = button_bits

	if joystick_ids(joynum) = 0 then
		joystick_counter += 1
		joystick_ids(joynum) = joystick_counter
		return -1  'Acquired joystick
	else
		return 0  'Success
	end if
end function

sub io_fb_set_clipboard_text(text as zstring ptr)  'ustring
	if text = NULL then text = @""
	#ifdef USE_X11
		' Not supported. For the selection to work, we would need to handle SelectionRequest
		' X11 events, but FB discards these and doesn't forward them. We would have to do peeking
		' of the X11 event queue.
		exit sub
	#elseif defined(__FB_WIN32__)
		dim hwnd as ssize_t 'as HWND
		GFX_ENTER
		screencontrol GET_WINDOW_HANDLE, hwnd
		GFX_EXIT
		WIN_SetClipboardText(cast(HWND, hwnd), text)
	#elseif defined(__FB_DARWIN__)
		Cocoa_SetClipboardText(text)
	#endif
end sub

function io_fb_get_clipboard_text() as zstring ptr  'ustring
	dim ret as zstring ptr
	#ifdef USE_X11
		dim wndw as Window
		dim displayi as ssize_t
		dim display as Display ptr
		GFX_ENTER
		screencontrol GET_WINDOW_HANDLE, wndw, displayi
		GFX_EXIT
		#if __FB_VER_MAJOR__ = 1 and __FB_VER_MINOR__ <= 5
			'Getting display via GET_WINDOW_HANDLE is only in FB 1.06.0
			display = fb_x11.display
		#else
			display = cptr(Display ptr, displayi)
		#endif
		ret = X11_GetClipboardText(display, wndw, NULL)  'No callback, just waits 40ms
	#elseif defined(__FB_WIN32__)
		dim hwnd as ssize_t 'as HWND
		GFX_ENTER
		screencontrol GET_WINDOW_HANDLE, hwnd
		GFX_EXIT
		ret = WIN_GetClipboardText(cast(HWND, hwnd))
	#elseif defined(__FB_DARWIN__)
		ret = Cocoa_GetClipboardText()
	#endif
	return ret
end function

function gfx_fb_setprocptrs() as integer
	gfx_init = @gfx_fb_init
	gfx_close = @gfx_fb_close
	gfx_getversion = @gfx_fb_getversion
	gfx_setpal = @gfx_fb_setpal
	gfx_screenshot = @gfx_fb_screenshot
	gfx_setwindowed = @gfx_fb_setwindowed
	gfx_windowtitle = @gfx_fb_windowtitle
	gfx_getwindowstate = @gfx_fb_getwindowstate
	gfx_get_screen_size = @gfx_fb_get_screen_size
	gfx_supports_variable_resolution = @gfx_fb_supports_variable_resolution
	gfx_get_resize = @gfx_fb_get_resize
	gfx_get_settings = @gfx_fb_get_settings
	gfx_set_settings = @gfx_fb_set_settings
	gfx_setoption = @gfx_fb_setoption
	gfx_describe_options = @gfx_fb_describe_options
	io_init = @io_fb_init
	io_pollkeyevents = @io_fb_pollkeyevents
	io_waitprocessing = @io_fb_waitprocessing
	io_keybits = @io_amx_keybits
	io_updatekeys = @io_fb_updatekeys
	io_textinput = @io_fb_textinput
	io_get_clipboard_text = @io_fb_get_clipboard_text
	io_set_clipboard_text = @io_fb_set_clipboard_text
	io_show_virtual_keyboard = @io_fb_show_virtual_keyboard
	io_hide_virtual_keyboard = @io_fb_hide_virtual_keyboard
	io_mousebits = @io_amx_mousebits
	io_setmousevisibility = @io_fb_setmousevisibility
	io_getmouse = @io_fb_getmouse
	io_setmouse = @io_fb_setmouse
	io_mouserect = @io_fb_mouserect
	io_get_joystick_state = @io_fb_get_joystick_state

	'new render API
	gfx_present = @gfx_fb_present

	return 1
end function

end extern
