
--native windows - Xlib backend.
--Written by Cosmin Apreutesei. Public domain.

--A desktop made by Unix people is Satan's gift to us.

local ffi   = require'ffi'
local bit   = require'bit'
local glue  = require'glue'
local box2d = require'box2d'
local xlib  = require'xlib'
require'xlib_keysym_h'
local time  = require'time' --for timers
local heap  = require'heap' --for timers
local cast  = ffi.cast
local xid   = xlib.xid
local C     = xlib.C
local glx --runtime dependency

local nw = {name = 'xlib'}

--xlib debugging -------------------------------------------------------------

local dbg = false --turn on to debug on xlib
if dbg then
	dbg = require'xlib_debug'
	function dbg_init()
		dbg = dbg.connect(xlib)
		--dbg.trace() --trace Xlib calls
	end
	local t0 = time.clock()
	function dbg_event(e)
		local dt = time.clock() - t0; t0 = t0 + dt
		if dt > 0.5 then
			print(('-'):rep(100))
		end
		print('EVENT', dbg.event_tostring(e))
	end
end

--app object -----------------------------------------------------------------

local app = {}
nw.app = app

function app:init(frontend)

	self.frontend = frontend
	xlib = xlib.connect()
	if dbg then dbg_init() end
	xlib.synchronize(true) --shave off one source of unpredictability
	xlib.set_xsettings_change_notify() --setup to receive XSETTINGS changes

	return self
end

--version checks -------------------------------------------------------------

function app:ver(what)
	if what == 'x' then
		local maj, min = xlib.version()
		return maj..'.'..min
	end
end

--message loop ---------------------------------------------------------------

local winmap = {} --{XWindow (always a number) -> window_backend}

local function win(id) --window_id -> window_backend
	return winmap[xid(id)]
end

local evname = { --{event_code -> event_name}
	[C.PropertyNotify] = 'PropertyNotify',
	[C.ClientMessage] = 'ClientMessage',
	[C.FocusIn] = 'FocusIn',
	[C.FocusOut] = 'FocusOut',
	[C.ConfigureNotify] = 'ConfigureNotify',
	[C.KeyPress] = 'KeyPress',
	[C.KeyRelease] = 'KeyRelease',
	[C.ButtonPress] = 'ButtonPress',
	[C.ButtonRelease] = 'ButtonRelease',
	[C.MotionNotify] = 'MotionNotify',
	[C.EnterNotify] = 'EnterNotify',
	[C.LeaveNotify] = 'LeaveNotify',
	[C.Expose] = 'Expose',
}

local evfield = { --{event_code -> XEvent_field}
	[C.PropertyNotify] = 'xproperty',
	[C.ClientMessage] = 'xclient',
	[C.FocusIn] = 'xfocus',
	[C.FocusOut] = 'xfocus',
	[C.ConfigureNotify] = 'xconfigure',
	[C.KeyPress] = 'xkey',
	[C.KeyRelease] = 'xkey',
	[C.ButtonPress] = 'xbutton',
	[C.ButtonRelease] = 'xbutton',
	[C.MotionNotify] = 'xmotion',
	[C.EnterNotify] = 'xcrossing',
	[C.LeaveNotify] = 'xcrossing',
	[C.Expose] = 'xexpose',
}

--event types for which we only keep the last one from any series
--of consecutive events of the same type (this is called event compression
--and it's important for preventing lag when resizing frameless windows).
local evrepeat = {
	[C.Expose] = true,
	[C.ConfigureNotify] = true,
}

local function dispatch(e)
	if not e then return end
	if dbg then dbg_event(e) end
	local etype = tonumber(e.type)
	local field = evfield[etype]
	if not field then return end
	local e = e[field]
	local win = win(e.window)
	return e, etype, win
end

local function poll(timeout)
	local e, etype, win = dispatch(xlib.poll(timeout))
	if win then
		if evrepeat[etype] then
			while true do --compress events
				local e1, etype1, win1 = dispatch(xlib.peek())
				if etype1 == etype and win1 == win then
					e, etype, win = e1, etype1, win1
					xlib.poll(true) --remove it
				else
					break
				end
			end
		end
		local f = win[evname[etype]]
		if f then f(win, e) end
	else
		local f = app[evname[etype]]
		if f then f(app, e) end
	end
	return true
end

function app:run()
	while not self._stop do
		local timeout = self:_pull_timers()
		if self._stop then --stop() called from timer
			while poll() do end --empty the queue
			break
		end
		poll(timeout)
	end
	self._stop = false
end

function app:stop()
	self._stop = true
end

--timers ---------------------------------------------------------------------

local function cmp(t1, t2)
	return t1.time < t2.time
end
local timers = heap.valueheap{cmp = cmp}

local select_accuracy = 0.01 --assume 10ms accuracy of select()

--pull and execute all expired timers and return the timeout until the next one.
function app:_pull_timers()
	while timers:length() > 0 do
		local t = timers:peek()
		local now = time.clock()
		if now + select_accuracy / 2 > t.time then
			if t.func() == false then --func wants timer to stop
				timers:pop()
			else --func wants timer to keep going
				t.time = now + t.interval
				timers:replace(1, t)
				--break to avoid recurrent timers to starve the event loop.
				return 0
			end
		else
			return t.time - now --wait till next scheduled timer
		end
	end
	return true --block indefinitely
end

function app:runevery(seconds, func)
	timers:push({time = time.clock() + seconds, interval = seconds, func = func})
end

--xsettings ------------------------------------------------------------------

local xsettings

function app:PropertyNotify(e)
	local prop = tonumber(e.atom)
	if prop == xlib.atom'_XSETTINGS_SETTINGS' then
		xsettings = nil
	end
end

function app:_xsettings(key)
	if xsettings == nil then
		xsettings = xlib.get_xsettings() or false
	end
	if not xsettings then return end
	return xsettings[key] and xsettings[key].value
end

--windows --------------------------------------------------------------------

local window = {}
app.window = window

--constrain the client size (cw, ch) based on current size constraints.
local function clamp_opt(x, min, max)
	if min then x = math.max(x, min) end
	if max then x = math.min(x, max) end
	return x
end
function window:__constrain(cw, ch)
	cw = clamp_opt(cw, self._min_cw, self._max_cw)
	ch = clamp_opt(ch, self._min_ch, self._max_ch)
	return cw, ch
end

--check if a given screen has a bgra8 visual (for creating windows with alpha).
local function find_bgra8_visual(screen)
	for i=0,screen.ndepths-1 do
		local d = screen.depths[i]
		if d.depth == 32 then
			for i=0,d.nvisuals-1 do
				local v = d.visuals[i]
				if v.bits_per_rgb == 8 and v.blue_mask == 0xff then --BGRA8
					return v
				end
			end
		end
	end
end

local last_active_window

function window:new(app, frontend, t)
	self = setmetatable({app = app, frontend = frontend}, {__index = self})

	local attrs = {}

	--say that we don't want the server to keep a pixmap for the window.
	attrs.background_pixmap = 0

	--declare what events we want to receive.
	attrs.event_mask = bit.bor(
		C.KeyPressMask,
		C.KeyReleaseMask,
		C.ButtonPressMask,
		C.ButtonReleaseMask,
		C.EnterWindowMask,
		C.LeaveWindowMask,
		C.PointerMotionMask,
		--C.KeymapStateMask, --not used
		C.ExposureMask,
		--C.VisibilityChangeMask, --not used
		C.StructureNotifyMask,
		--C.ResizeRedirectMask, --not working
		C.SubstructureNotifyMask,
		--C.SubstructureRedirectMask, --not used
		C.FocusChangeMask,
		C.PropertyChangeMask,
		--C.ColormapChangeMask, --not used
		C.OwnerGrabButtonMask,
	0)

	if t.transparent then
		--find a 32bit BGRA8 visual so we can create a window with alpha.
		attrs.visual = find_bgra8_visual(xlib.screen)
		if attrs.visual then
			attrs.depth = 32
			--creating a 32bit-depth window requires creating a colormap!
			attrs.colormap = xlib.create_colormap(xlib.screen.root, attrs.visual)
			--setting a colormap requires setting border_pixel!
			attrs.border_pixel = 0
		end
	end

	--store window's depth for put_image()
	self._depth = attrs.depth or xlib.screen.root_depth
	self._visual = attrs.visual or xlib.screen.root_visual

	--store and apply constraints to client size
	self._min_cw = t.min_cw
	self._min_ch = t.min_ch
	self._max_cw = t.max_cw
	self._max_ch = t.max_ch

	local cx, cy, cw, ch = app.frontend:frame_to_client(
		t.frame, t.menu, t.x or 0, t.y or 0, t.w, t.h)
	cx = t.x and cx
	cy = t.y and cy
	cw, ch = self:__constrain(cw, ch)

	attrs.x = cx
	attrs.y = cy
	attrs.width = cw
	attrs.height = ch

	self.win = xlib.create_window(attrs)
	xlib.flush() --lame: XSynchronize() didn't do it's job here

	--NOTE: WMs ignore the initial position unless we set WM_NORMAL_HINTS too
	--(values don't matter, but we're using the same coordinates just in case).
	local hints = {x = cx, y = cy}

	if not t.resizeable then
		--this is how X knows that a window is non-resizeable (there's no flag).
		hints.min_width  = cw
		hints.min_height = ch
		hints.max_width  = cw
		hints.max_height = ch
	else
		--tell X about any (already-applied) constraints.
		hints.min_width  = t.min_cw
		hints.min_height = t.min_ch
		--NOTE: we can't set a constraint on one axis alone, hence the 2^24.
		if t.max_cw or t.max_ch then
			hints.max_width  = t.max_cw or 2^24
			hints.max_height = t.max_ch or 2^24
		end
	end
	xlib.set_wm_size_hints(self.win, hints)

	--declare the X protocols that the window supports.
	xlib.set_atom_map_prop(self.win, 'WM_PROTOCOLS', {
		WM_DELETE_WINDOW = true, --don't close the connection when a window is closed
		_NET_WM_PING = true, --respond to ping events
	})

	--set info for _NET_WM_PING to allow the user to kill a non-responsive process.
	xlib.set_net_wm_ping_info(self.win)

	if t.title then
		self:set_title(t.title)
	end

	if t.parent then
		xlib.set_transient_for(self.win, t.parent.backend.win)
	end

	--set motif hints before mapping the window.
	--NOTE: MWM_DECOR_RESIZEH alone brings all frame elements with it,
	--so we can't have frameless windows that are also resizeable.
	local hints = ffi.new'PropMotifWmHints'
	hints.flags = bit.bor(
		C.MWM_HINTS_FUNCTIONS,
		C.MWM_HINTS_DECORATIONS)
	hints.functions = bit.bor(
		t.resizeable and C.MWM_FUNC_RESIZE or 0,
		C.MWM_FUNC_MOVE,
		t.minimizable and C.MWM_FUNC_MINIMIZE or 0,
		t.maximizable and C.MWM_FUNC_MAXIMIZE or 0,
		t.closeable and C.MWM_FUNC_CLOSE or 0)
	hints.decorations = t.frame == 'none' and 0 or bit.bor(
		C.MWM_DECOR_BORDER,
		C.MWM_DECOR_TITLE,
		C.MWM_DECOR_MENU,
		t.resizeable  and C.MWM_DECOR_RESIZEH or 0,
		t.minimizable and C.MWM_DECOR_MINIMIZE or 0,
		t.maximizable and C.MWM_DECOR_MAXIMIZE or 0)
	xlib.set_motif_wm_hints(self.win, hints)

	--flag to mask off window's reported state while the window is unmapped.
	self._hidden = true
	--if given, the initial _outer_ position that must be set again on show!
	self._init_pos = (t.x or t.y) and {x = t.x, y = t.y}

	--state flags to be reported while the window is hidden.
	self._minimized = t.minimized or false
	self._maximized = t.maximized or false
	self._fullscreen = t.fullscreen or false
	self._topmost = t.topmost or false

	self:_init_opengl(t.opengl)

	winmap[self.win] = self

	--if this is the first window, register it as the last active window
	--just in case the user calls app:activate() before this window activates.
	if not last_active_window then
		last_active_window = self
	end

	return self
end

function window:PropertyNotify(e)
	local prop = tonumber(e.atom)
	if prop == xlib.atom'WM_STATE' then
		self:PropertyNotify_WM_STATE()
	elseif prop == xlib.atom'_NET_WM_STATE' then
		self:PropertyNotify__NET_WM_STATE()
	end
end

--closing --------------------------------------------------------------------

function window:ClientMessage(e)
	local v = e.data.l[0]
	if e.message_type == xlib.atom'WM_PROTOCOLS' then
		if v == xlib.atom'WM_DELETE_WINDOW' then
			self.frontend:close()
		elseif v == xlib.atom'_NET_WM_PING' then
			xlib.pong(e)
		end
	end
end

function window:forceclose()

	--force-close child windows first, consistent with Windows.
	for i,win in ipairs(self.frontend:children()) do
		win:close(true)
	end

	--trigger closed event after children are closed but before destroying the window.
	self.frontend:_backend_closed()

	xlib.destroy_window(self.win)
	winmap[self.win] = nil --discard further messages
	self.win = nil --prevent usage

	--register another random window as the last active window so that
	--app:activate() works even before the next window gets activated.
	--in any case we want to release the reference to self.
	if last_active_window == self then
		local _, backend = next(winmap)
		last_active_window = backend
	end
end

--activation -----------------------------------------------------------------

--how much to wait for another window to become active after a window
--is deactivated, before triggering an 'app deactivated' event.
local focus_out_timeout = 0.1
local last_focus_out
local focus_timer_started
local app_active = false

--NOTE: FocusIn is also sent after ending a resizing operation.
function window:FocusIn(e)
	if self._hiding or self._hidden then return end --ignore while hiding

	last_active_window = self
	last_focus_out = nil --disable the app deactivate timer
	app_active = true --window activation implies app activation.

	self.app.frontend:_backend_changed()
	self.frontend:_backend_changed()
end

--NOTE: FocusOut is also sent before starting a resizing operation.
--NOTE: when hiding, FocusOut is sent before PropertyNotify/WM_STATE.
--NOTE: when minimizing, by the time FocusOut is sent, _NET_WM_STATE_HIDDEN
--is also set, thus was_minimized event will happen here.
function window:FocusOut(e)
	if self._hiding or self._hidden then return end --ignore while hiding

	--start a delayed check for when the app is deactivated.
	--if a timer is already started, just advance the delay.
	last_focus_out = time.clock()
	if not focus_timer_started then
		self.app.frontend:runafter(focus_out_timeout, function()
			if last_focus_out and time.clock() - last_focus_out > focus_out_timeout then
				last_focus_out = nil

				app_active = false
				self.app.frontend:_backend_changed()
			end
			focus_timer_started = false
		end)
		focus_timer_started = true
	end

	self.frontend:_backend_changed()
end

function app:activate(mode)
	if mode ~= 'force' then return end
	--unlike OSX, in X you don't activate an app, you can only activate a window.
	--activating this app means activating the last window that was active.
	local win = last_active_window
	if win and not win.frontend:dead() then
		xlib.change_net_active_window(last_active_window.win)
	end
end

function app:active_window()
	--return the active window only if the app is active, consistent with OSX.
	local win = app_active and win(xlib.get_input_focus())
	return win and win.frontend or nil
end

function app:active()
	return app_active
end

function window:activate()
	--for consistency with OSX, if the app is inactive, this function
	--doesn't activate the window, instead it marks the window that must
	--be activated on the next call to app:activate().
	if not app_active then
		last_active_window = self
	else
		xlib.change_net_active_window(self.win)
	end
end

function window:active()
	return self.app:active_window() == self.frontend
end

--state/app visibility -------------------------------------------------------

function app:visible() return true end
function app:hide() end
function app:unhide() end

--state/visibility -----------------------------------------------------------

function window:visible()
	return not self._hidden
end

function window:show()
	if not self._hidden then return end --hiding or visible: ignore

	if self._normal_rect then
		--set the saved normal rect before mapping the window.
		--this is because the normal rect is lost when hiding a maximized window.
		self:set_frame_rect(unpack(self._normal_rect))
	else
		--store _normal_rect
		self._normal_rect = {self:get_frame_rect()}
	end

	--the initial _frame_ position (if any) must be set again now.
	if self._init_pos then
		xlib.config(self.win, self._init_pos)
		self._init_pos = nil
	end

	--set the _NET_WM_STATE property before mapping the window.
	--later on we have to use change_net_wm_state() to change these values.
	xlib.set_net_wm_state(self.win, {
		_NET_WM_STATE_MAXIMIZED_HORZ = self._maximized or nil,
		_NET_WM_STATE_MAXIMIZED_VERT = self._maximized or nil,
		_NET_WM_STATE_HIDDEN = self._minimized or nil,
		_NET_WM_STATE_ABOVE = self._topmost or nil,
		_NET_WM_STATE_FULLSCREEN = self._fullscreen or nil,
	})

	xlib.map(self.win) --async operation
end

function window:hide()
	if self._hidden then return end

	--window state to report while hidden.
	self._minimized = self:minimized()
	self._maximized = self:maximized()
	self._fullscreen = self:fullscreen()
	self._hiding = true --signal intent to hide in subsequent events

	local x, y = self:get_client_pos()
	xlib.withdraw(self.win) --async operation
	--move it back to position because Unity moves it at the frame coordinates!
	xlib.config(self.win, {x = x, y = y})
end

function window:PropertyNotify_WM_STATE()
	if self._hiding and not xlib.get_wm_state_visible(self.win) then
		self._hiding = false
		self._hidden = true --switch to reporting stored state
	elseif self._hidden and xlib.get_wm_state_visible(self.win) then
		self._hidden = false --switch to reporting real state
	end
	self.frontend:_backend_changed() --visibility and minimization changes
end

--state/minimizing -----------------------------------------------------------

function window:minimized()
	if self._hidden then
		return self._minimized
	end
	return xlib.get_net_wm_state_hidden(self.win)
end

function window:minimize()
	if self:fullscreen() then return end --TODO: remove this after fixing OSX
	if self._hidden then
		self._minimized = true
		self:show()
	else
		xlib.iconify(self.win)
	end
end

function window:_unminimize()
	xlib.map(self.win)
	self:activate() --because map alone doesn't activate a minimized window
end

--state/maximizing -----------------------------------------------------------

function window:maximized()
	if self._hidden then
		return self._maximized
	end
	return xlib.get_net_wm_state_maximized(self.win)
end

function window:maximize()
	if self:fullscreen() then return end --TODO: remove this after fixing OSX
	if self._hidden then
		self._maximized = true
		self._minimized = false
		self:show()
	else
		xlib.change_net_wm_state_maximized(self.win, true)
		self:_unminimize()
	end
end

function window:_unmaximize()
	if self._hidden then
		self._minimized = false
		self._maximized = false
		self:show()
	else
		xlib.change_net_wm_state_maximized(self.win, false)
	end
end

function window:PropertyNotify__NET_WM_STATE()
	if self._hiding or self._hidden then return end --ignore events while hidden
	self.frontend:_backend_changed() --maximization and fullscreen changes
end

--state/restoring ------------------------------------------------------------

function window:restore()
	if self._hidden then
		if self._minimized then
			self._minimized = false
		elseif self._fullscreen then
			self._fullscreen = false
		elseif self._maximized then
			self._maximized = false
		end
		self:show()
	elseif self:minimized() then
		self:_unminimize()
	elseif self:fullscreen() then
		self:fullscreen(false)
	else
		self:_unmaximize()
	end
end

function window:shownormal()
	if self:fullscreen() then return end --TODO: remove this after fixing OSX
	if self._hidden then
		self._minimized = false
		self._maximized = false
		self._fullscreen = false
		self:show()
	else
		self:fullscreen(false)
		self:_unmaximize()
		self:_unminimize()
	end
end

--state/fullscreen -----------------------------------------------------------

function window:fullscreen()
	if self._hidden then
		return self._fullscreen
	end
	return xlib.get_net_wm_state_fullscreen(self.win)
end

function window:enter_fullscreen()
	if self._hidden then
		self._fullscreen = true
		self._minimized = false
		self:show()
	else
		self:_unminimize()
		xlib.change_net_wm_state_fullscreen(self.win, true)
	end
end

function window:exit_fullscreen()
	xlib.change_net_wm_state_fullscreen(self.win, false)
end

--state/enabled --------------------------------------------------------------

function window:get_enabled()
	return not self._disabled
end

function window:set_enabled(enabled)
	self._disabled = not enabled
end

--positioning/frame extents --------------------------------------------------

local function unmapped_frame_extents(win)
	local w1, h1, w2, h2
	if xlib.frame_extents_supported() then
		xlib.request_frame_extents(win)
		w1, h1, w2, h2 = xlib.get_frame_extents(win)
		--some WMs set the extents later or not at all so we have to poll.
		if not w1 then
			local timeout = time.clock() + 0.2
			local period = 0.01
			while true do
				w1, h1, w2, h2 = xlib.get_frame_extents(win)
				if w1 or time.clock() > timeout then break end
				time.sleep(period)
				period = period * 2
			end
		end
	end
	if not w1 then --bail out with wrong values.
		w1, h1, w2, h2 = 0, 0, 0, 0
	end
	return w1, h1, w2, h2
end

local frame_extents = glue.memoize(function(frame, has_menu, resizeable)
	--create a dummy window
	local win = xlib.create_window{width = 200, height = 200}
	--get its frame extents
	local w1, h1, w2, h2 = unmapped_frame_extents(win)
	--destroy the window
	xlib.destroy_window(win)
	--compute/return the frame rectangle
	return {w1, h1, w2, h2}
end)

function app:frame_extents(frame, has_menu, resizeable)
	return unpack(frame_extents(frame, has_menu, resizeable))
end

--positioning/rectangles -----------------------------------------------------

function window:get_client_size()
	local _, _, w, h = xlib.get_geometry(self.win)
	return w, h
end

function window:get_client_pos()
	local x, y = xlib.translate_coords(self.win, xlib.screen.root, 0, 0)
	return x, y
end

local function frame_rect(x, y, w, h, w1, h1, w2, h2)
	return x - w1, y - h1, w + w1 + w2, h + h1 + h2
end

local function unframe_rect(x, y, w, h, w1, h1, w2, h2)
	return frame_rect(x, y, w, h, -w1, -h1, -w2, -h2)
end

function window:_frame_extents()
	if self.frontend:frame() == 'none' then
		return 0, 0, 0, 0
	end
	return unmapped_frame_extents(self.win)
end

function window:get_normal_frame_rect()
	if self._normal_rect then
		return unpack(self._normal_rect)
	else
		return self:get_frame_rect()
	end
end

function window:get_frame_rect()
	local x, y = self:get_client_pos()
	local w, h = self:get_client_size()
	return frame_rect(x, y, w, h, self:_frame_extents())
end

function window:set_frame_rect(x, y, w, h)
	local _, _, cw, ch = unframe_rect(x, y, w, h, self:_frame_extents())
	xlib.config(self.win, {x = x, y = y, width = cw, height = ch, border_width = 0})
end

--positioning/constraints ----------------------------------------------------

function window:_apply_constraints()

	--get the current client size and the new (constrained) client size
	local cw0, ch0 = self:get_client_size()
	local cw, ch = self:__constrain(cw0, ch0)

	--update constraints
	local hints = {}
	if not self.frontend:resizeable() then
		if cw ~= cw0 or ch ~= ch0 then
			hints.min_width  = cw
			hints.min_height = ch
			hints.max_width  = cw
			hints.max_height = ch
		end
	else
		hints.min_width  = self._min_cw or 0
		hints.min_height = self._min_ch or 0
		hints.max_width  = self._max_cw or 2^24
		hints.max_height = self._max_ch or 2^24
	end
	xlib.set_wm_size_hints(self.win, hints)

	--resize the window if dimensions changed
	if cw ~= cw0 or ch ~= ch0 then
		xlib.config(self.win, {width = cw, height = ch, border_width = 0})
	end
end

function window:get_minsize()
	return self._min_cw, self._min_ch
end

function window:get_maxsize()
	return self._max_cw, self._max_ch
end

function window:set_minsize(min_cw, min_ch)
	self._min_cw, self._min_ch = min_cw, min_ch
	self:_apply_constraints()
end

function window:set_maxsize(max_cw, max_ch)
	self._max_cw, self._max_ch = max_cw, max_ch
	self:_apply_constraints()
end

--positioning/resizing -------------------------------------------------------

function window:ConfigureNotify(e)
	if self._hiding or self._hidden then return end --reparenting, ignore

	--track normal rect because there's no way to get it from X.
	--NOTE: we have to track this through resizes instead of just saving it
	--before maximizing because by the time PropertyNotify for _NET_WM_STATE
	--is triggered the frame is already maximized.
	if not (self:maximized() or self:fullscreen()) then
		self._normal_rect = {self:get_frame_rect()}
	end

	--NOTE: don't bother trying to implement sticky children.
	--It's impossible to do that right with the async model (think about it).

	self.frontend:_backend_changed()
end

--positioning/magnets --------------------------------------------------------

--not useful since we can't intervene in window resizing.
function window:magnets() end

--titlebar -------------------------------------------------------------------

function window:get_title()
	return xlib.get_wm_name(self.win)
end

function window:set_title(title)
	xlib.set_wm_name(self.win, title)
	xlib.set_wm_icon_name(self.win, title)
end

--z-order --------------------------------------------------------------------

function window:get_topmost()
	return self._topmost
end

function window:set_topmost(topmost)
	xlib.change_net_wm_state(self.win, topmost, '_NET_WM_STATE_ABOVE')
end

function window:raise(relto)
	--TODO: implement relto
	xlib.raise(self.win)
end

function window:lower(relto)
	--TODO: implement relto
	xlib.lower(self.win)
end

--displays -------------------------------------------------------------------

function app:displays()
	local t = {}
	local screens, n = xlib.xinerama_screens() --the order is undefined
	if screens and n > 1 then --multi-monitor setup
		for i = 0, n-1 do
			local scr = screens[i]
			local x = scr.x_org
			local y = scr.y_org
			local w = scr.width
			local h = scr.height

			--TODO: find a way to get the workarea of each monitor in multi-monitor setups.
			--NOTE: _NET_WORKAREA spans all monitors so it's no good here!
			local cx, cy, cw, ch = x, y, w, h

			t[#t+1] = self.frontend:_display{
				x = x, y = y, w = w, h = h,
				cx = cx, cy = cy, cw = cw, ch = ch,
				scalingfactor = 1}
		end
	else
		--Xinerama not present or single monitor setup
		local x = 0
		local y = 0
		local w = xlib.screen.width
		local h = xlib.screen.height

		local cx, cy, cw, ch

		--get the workarea of the first virtual desktop
		local wa = xlib.get_net_workarea(nil, 1)
		if wa then
			cx, cy, cw, ch = unpack(wa)
		else
			--_NET_WORKAREA not set, fallback to using screen area
			cx, cy, cw, ch = x, y, w, h
		end

		t[1] = self.frontend:_display{
			x = x, y = y, w = w, h = h,
			cx = cx, cy = cy, cw = cw, ch = ch,
			scalingfactor = 1}
	end
	return t
end

function app:display_count()
	local _, n = xlib.xinerama_screens()
	return n or 1
end

function app:_display_overlapping(x, y, w, h)

	--get all displays and the overlapping area between them and the window
	local t = {}
	for i,d in ipairs(self:displays()) do
		local _, _, w1, h1 = box2d.clip(d.x, d.y, d.w, d.h, x, y, w, h)
		t[#t+1] = {area = w1 * h1, display = d}
	end

	--sort displays by overlapping area in descending order.
	table.sort(t, function(t1, t2) return t1.area > t2.area end)

	--return nil if there's no overlapping area i.e. the window is off-screen
	if t[1].area == 0 then return end

	--return the display with the most overlapping
	return t[1].display
end

function app:main_display()
	for i,d in ipairs(self:displays()) do
		if d.x == 0 and d.y == 0 then --main display is at (0, 0) by definition
			return d
		end
	end
end

function app:active_display()
	local display
	local win = xlib.get_input_focus()
	if win then
		--get the client rect of the active window in screen space.
		local cx, cy = xlib.translate_coords(win, xlib.screen.root, 0, 0)
		local _, _, cw, ch = xlib.get_geometry(win)
		--get the display overlapping the active window, if any.
		display = self:_display_overlapping(cx, cy, cw, ch)
	end
	return display or self:main_display()
end

function window:display()
	local x, y, w, h = self:get_frame_rect()
	return self.app:_display_overlapping(x, y, w, h)
end

--TODO:
--self.app.frontend:_backend_displays_changed()

--cursors --------------------------------------------------------------------

function window:update_cursor()
	local name, visible = self.frontend:cursor()
	name = visible and name or '_blank'
	self._cursors = self._cursors or {}
	local cursor = self._cursors[name]
	if not cursor then
		if visible then
			cursor = xlib.try_load_cursor(name)
				or self._cursors.arrow
				or xlib.try_load_cursor'arrow'
		else
			cursor = xlib.blank_cursor()
		end
	end
	if not cursor then return end
	xlib.set_cursor(self.win, cursor)
	self._cursors[name] = cursor
end

--keyboard -------------------------------------------------------------------

local keynames = {
	[C.XK_semicolon]    = ';',  --on US keyboards
	[C.XK_equal]        = '=',
	[C.XK_comma]        = ',',
	[C.XK_minus]        = '-',
	[C.XK_period]       = '.',
	[C.XK_slash]        = '/',
	[C.XK_quoteleft]    = '`',
	[C.XK_bracketleft]  = '[',
	[C.XK_backslash]    = '\\',
	[C.XK_bracketright] = ']',
	[C.XK_apostrophe]   = '\'',

	[C.XK_BackSpace] = 'backspace',
	[C.XK_Tab]       = 'tab',
	[C.XK_space]     = 'space',
	[C.XK_Escape]    = 'esc',
	[C.XK_Return]    = 'enter!',

	[C.XK_F1]  = 'F1',
	[C.XK_F2]  = 'F2',
	[C.XK_F3]  = 'F3',
	[C.XK_F4]  = 'F4',
	[C.XK_F5]  = 'F5',
	[C.XK_F6]  = 'F6',
	[C.XK_F7]  = 'F7',
	[C.XK_F8]  = 'F8',
	[C.XK_F9]  = 'F9',
	[C.XK_F10] = 'F10',
	[C.XK_F11] = 'F11',
	[C.XK_F12] = 'F12',

	[C.XK_Caps_Lock]   = 'capslock',

	[C.XK_Print]       = 'printscreen', --taken (take screenshot); shift+printscreen works
	[C.XK_Scroll_Lock] = 'scrolllock',
	[C.XK_Pause]       = 'break',

	[C.XK_Left]        = 'left!',
	[C.XK_Up]          = 'up!',
	[C.XK_Right]       = 'right!',
	[C.XK_Down]        = 'down!',

	[C.XK_Prior]       = 'pageup!',
	[C.XK_Next]        = 'pagedown!',
	[C.XK_Home]        = 'home!',
	[C.XK_End]         = 'end!',
	[C.XK_Insert]      = 'insert!',
	[C.XK_Delete]      = 'delete!',

	[C.XK_Num_Lock]    = 'numlock',
	[C.XK_KP_Divide]   = 'num/',
	[C.XK_KP_Multiply] = 'num*',
	[C.XK_KP_Subtract] = 'num-',
	[C.XK_KP_Add]      = 'num+',
	[C.XK_KP_Enter]    = 'numenter',
	[C.XK_KP_Delete]   = 'numdelete',
	[C.XK_KP_End]      = 'numend',
	[C.XK_KP_Down]     = 'numdown',
	[C.XK_KP_Next]     = 'numpagedown',
	[C.XK_KP_Left]     = 'numleft',
	[C.XK_KP_Begin]    = 'numclear',
	[C.XK_KP_Right]    = 'numright',
	[C.XK_KP_Home]     = 'numhome',
	[C.XK_KP_Up]       = 'numup',
	[C.XK_KP_Prior]    = 'numpageup',
	[C.XK_KP_Insert]   = 'numinsert',

	[C.XK_Control_L]   = 'lctrl',
	[C.XK_Control_R]   = 'rctrl',
	[C.XK_Shift_L]     = 'lshift',
	[C.XK_Shift_R]     = 'rshift',
	[C.XK_Alt_L]       = 'lalt',
	[C.XK_Alt_R]       = 'ralt',

	[C.XK_Super_L]     = 'lwin',
	[C.XK_Super_R]     = 'rwin',
	[C.XK_Menu]        = 'menu',

	[C.XK_0] = '0',
	[C.XK_1] = '1',
	[C.XK_2] = '2',
	[C.XK_3] = '3',
	[C.XK_4] = '4',
	[C.XK_5] = '5',
	[C.XK_6] = '6',
	[C.XK_7] = '7',
	[C.XK_8] = '8',
	[C.XK_9] = '9',

	[C.XK_a] = 'A',
	[C.XK_b] = 'B',
	[C.XK_c] = 'C',
	[C.XK_d] = 'D',
	[C.XK_e] = 'E',
	[C.XK_f] = 'F',
	[C.XK_g] = 'G',
	[C.XK_h] = 'H',
	[C.XK_i] = 'I',
	[C.XK_j] = 'J',
	[C.XK_k] = 'K',
	[C.XK_l] = 'L',
	[C.XK_m] = 'M',
	[C.XK_n] = 'N',
	[C.XK_o] = 'O',
	[C.XK_p] = 'P',
	[C.XK_q] = 'Q',
	[C.XK_r] = 'R',
	[C.XK_s] = 'S',
	[C.XK_t] = 'T',
	[C.XK_u] = 'U',
	[C.XK_v] = 'V',
	[C.XK_w] = 'W',
	[C.XK_x] = 'X',
	[C.XK_y] = 'Y',
	[C.XK_z] = 'Z',
}

local keysyms = {}
for vk, name in pairs(keynames) do
	keysyms[name:lower()] = vk
end

local function keyname(keycode)
	local sym = xid(xlib.keysym(keycode, 0, 0))
	return keynames[sym]
end

function window:KeyPress(e)
	if self._disabled then return end

	if self._keypressed then
		self._keypressed = false
		return
	end

	local key = keyname(e.keycode)
	if not key then return end

	self.frontend:_backend_keydown(key)
	self.frontend:_backend_keypress(key)
end

function window:KeyRelease(e)
	if self._disabled then return end

	local key = keyname(e.keycode)
	if not key then return end

	--peek next message to distinguish between key release and key repeat
 	local e1 = xlib.peek()
 	if e1 then
 		if e1.type == C.KeyPress then
			local e1 = e1.xkey
			if e1.time == e.time and e1.keycode == e.keycode then
				self.frontend:_backend_keypress(key)
				self._keypressed = true --key press barrier
 			end
 		end
 	end

	if not self._keypressed then
		self.frontend:_backend_keyup(key)
	end
end

--self.frontend:_backend_keychar(char)

--NOTE: scolllock state is not present in XKeyboardState.led_mask by default.
local toggle_keys = {capslock = 1, numlock = 2, scrolllock = 4}

function app:key(name) --name is in lowercase!
	if name:find'^%^' then --'^key' means get the toggle state for that key
		name = name:sub(2)
		local mask = toggle_keys[name]
		if not mask then return false end
		return bit.band(xlib.get_keyboard_control().led_mask, mask) ~= 0
	else
		local sym = keysyms[name]
		if not sym then return false end
		local code = xlib.keycode(sym)
		local keymap = xlib.query_keymap(code)
		return xlib.getbit(code, keymap)
	end
end

--mouse ----------------------------------------------------------------------

local btns = {'left', 'middle', 'right'}

function window:_setmouse(e)
	local m = self.frontend._mouse
	m.x = e.x
	m.y = e.y
	m.left = bit.band(e.state, C.Button1Mask) ~= 0
	m.right = bit.band(e.state, C.Button2Mask) ~= 0
	m.middle = bit.band(e.state, C.Button3Mask) ~= 0
	m.x1 = false --TODO: get the state of these buttons
	m.x2 = false
	return m
end

function app:get_mouse_pos()
	local x, y = xlib.query_pointer(xlib.screen.root)
	return x, y
end

function app:set_mouse_pos(x, y)
	--TODO
end

function window:ButtonPress(e)
	if self._disabled then return end
	if e.button == C.Button4 then --wheel up
		self:_setmouse(e)
		self.frontend:_backend_mousewheel(3, e.x, e.y)
	elseif e.button == C.Button5 then --wheel down
		self:_setmouse(e)
		self.frontend:_backend_mousewheel(-3, e.x, e.y)
	else
		local btn = btns[e.button]
		if not btn then return end
		self:_setmouse(e)
		self.frontend:_backend_mousedown(btn, e.x, e.y)
	end
end

function window:ButtonRelease(e)
	if self._disabled then return end
	local btn = btns[e.button]
	if not btn then return end
	self:_setmouse(e)
	self.frontend:_backend_mouseup(btn, e.x, e.y)
end

function window:MotionNotify(e)
	if self._disabled then return end
	self:_setmouse(e)
	self.frontend:_backend_mousemove(e.x, e.y)
end

function window:EnterNotify(e)
	if self._disabled then return end
	local m = self:_setmouse(e)
	m.inside = true
	self.frontend:_backend_mouseenter(e.x, e.y)
end

function window:LeaveNotify(e)
	if self._disabled then return end
	local m = self:_setmouse(e)
	m.inside = false
	self.frontend:_backend_mouseleave()
end

function app:double_click_time()
	return (self:_xsettings'Net/DoubleClickTime' or 400) / 1000 --seconds
end

function app:caret_blink_time()
	return (self:_xsettings'Net/CursorBlinkTime' or 500) / 1000 --seconds
end

function app:double_click_target_area()
	return 4, 4 --like in windows
end

--bitmaps --------------------------------------------------------------------

--[[
Things you need to know:
- in X11 bitmaps are called pixmaps and 1-bit bitmaps are called bitmaps.
- pixmaps are server-side bitmaps while images are client-side bitmaps.
- you can't create a Drawable, that's just an abstraction: instead,
  any Pixmap or Window can be used where a Drawable is expected.
- pixmaps have depth, but no channel layout. windows have color info.
- the default screen visual has 24 depth, but a screen can have many visuals.
  if it has a 32 depth visual, then we can make windows with alpha.
- a window with alpha needs CWColormap which needs CWBorderPixel.
]]

--static, auto-growing buffer allocation pattern (from glue).
local function buffer(ctype)
	local vla = ffi.typeof(ctype)
	local buf, len = nil, -1
	return function(minlen)
		if minlen == false then
			buf, len = nil, -1
		elseif minlen > len then
			len = glue.nextpow2(minlen)
			buf = vla(len)
		end
		return buf, len
	end
end

local function make_bitmap(w, h, win, win_depth, win_visual, ssbuf)

	local stride = w * 4
	local size = stride * h

	local bitmap = {
		w        = w,
		h        = h,
		stride   = stride,
		size     = size,
		format   = 'bgra8',
	}

	local paint, free

	--NOTE: can't create pix if the window is unmapped.
	local st = xlib.get_wm_state(win)
	if st and st == C.WithdrawnState then return end

	if xlib.shm() then

		local image, shminfo = xlib.shm_create_image(win_visual, win_depth, w, h)
		local gc = xlib.create_gc(win)
		bitmap.data = image.data

		function paint()
			xlib.shm_put_image(gc, image, w, h, win)
		end

		function free()
			xlib.free_gc(gc)
			xlib.shm_free_image(image, shminfo)
			bitmap.data = nil
		end

	else

		local data = ssbuf(size)
		bitmap.data = data
		local gc = xlib.create_gc(win)

		function paint()
			xlib.put_image(gc, data, size, w, h, win_depth, win)
		end

		function free()
			xlib.free_gc(gc)
			bitmap.data = nil
		end

	end

	return bitmap, free, paint
end

--a dynamic bitmap is an API that creates a new bitmap everytime its size
--changes. user supplies the :size() function, :get() gets the bitmap,
--and :freeing(bitmap) is triggered before the bitmap is freed.
local function dynbitmap(api, win, depth, visual)

	api = api or {}

	local w, h, bitmap, free, paint
	local ssbuf = buffer'char[?]'

	function api:get()
		local w1, h1 = api:size()
		if not bitmap or w1 ~= w or h1 ~= h then
			if bitmap then
				free()
			end
			bitmap, free, paint = make_bitmap(w1, h1, win, depth, visual, ssbuf)
			w, h = w1, h1
		end
		return bitmap
	end

	function api:free()
		if not free then return end
		self:freeing(bitmap)
		free()
	end

	function api:paint()
		if not paint then return end
		paint()
	end

	return api
end

--rendering ------------------------------------------------------------------

function window:Expose(e)
	if e.count ~= 0 then return end --partial expose, skip it.
	--can't paint the bitmap while the window is unmapped.
	if self._hidden or (self.minimized and self:minimized()) then return end
	--e.x, e.y, e.width, e.height
	self:_repaint()
end

function window:bitmap()
	if not self._dynbitmap then
		self._dynbitmap = dynbitmap({
			size = function()
				local size = self.frontend.size or self.frontend.client_size
				return size(self.frontend)
			end,
			freeing = function(_, bitmap)
				self.frontend:_backend_free_bitmap(bitmap)
			end,
		}, self.win, self._depth, self._visual)
	end
	return self._dynbitmap:get()
end

function window:invalidate(x, y, w, h)
	if x and y then
		xlib.clear_area(self.win, x, y, w, h)
	else
		xlib.clear_area(self.win, 0, 0, 2^24, 2^24)
	end
end

function window:_free_bitmap()
	 if not self._dynbitmap then return end
	 self._dynbitmap:free()
	 self._dynbitmap = nil
end

function window:_repaint()
	--let the user request the bitmap and draw on it.
	if self.frontend:_backend_needs_repaint() then
		self.frontend:_backend_repaint()
	end
	--if it did, paint the bitmap onto the window.
	if self._dynbitmap then
		--TODO: paint subregion
		self._dynbitmap:paint()
	end
end

function window:_repaint_opengl()
	if self.frontend:_backend_needs_repaint() then
		self.frontend:_backend_repaint()
	end
	if not self._swap then return end
	glx.swap_buffers(self.win)
	self._swap = false
end

--rendering/opengl -----------------------------------------------------------

function window:_init_opengl(t)
	if not t then return end
	if not glx then
		require'gl11'
		glx = require'glx'
		glx = glx.connect(xlib)
	end
	for fbconfig in glx.choose_rgb_fbconfigs() do
		self._glx = glx.create_context(fbconfig)
		break
	end
	self._repaint = self._repaint_opengl
	assert(self._glx)
end

function window:gl()
	glx.make_current(self.win, self._glx)
	self._swap = true
	return glx.C
end

--views ----------------------------------------------------------------------

local view = {}
window.view = view

function view:new(window, frontend, t)
	local self = setmetatable({
		window = window,
		app = window.app,
		frontend = frontend,
	}, {__index = self})

	local attrs = {
		depth = window._depth,
		x = t.x,
		y = t.y,
		width = t.w,
		height = t.h,
		parent = window.win,
		--say that we don't want the server to keep a pixmap for the window.
		background_pixmap = 0,
	}

	--declare what events we want to receive.
	attrs.event_mask = bit.bor(
		C.ButtonPressMask,
		C.ButtonReleaseMask,
		C.EnterWindowMask,
		C.LeaveWindowMask,
		C.PointerMotionMask,
		C.ExposureMask,
		C.StructureNotifyMask,
		C.SubstructureNotifyMask,
		C.PropertyChangeMask,
		C.OwnerGrabButtonMask,
	0)

	self.win = xlib.create_window(attrs)
	xlib.flush()

	self._depth = window._depth

	self:_init_opengl(t.opengl)

	winmap[self.win] = self

	return self
end

function view:get_rect()
	local x, y, w, h = xlib.get_geometry(self.win)
	return x, y, w, h
end

function view:set_rect(x, y, w, h)
	xlib.config(self.win, {x = x, y = y, width = w, height = h, border_width = 0})
end

function view:show()
	xlib.map(self.win)
end

function view:hide()
	xlib.withdraw(self.win)
end

function view:free()
	xlib.destroy_window(self.win)
end

function view:ConfigureNotify(e)
	self.frontend:_backend_changed()
end

view._setmouse = window._setmouse
view.Expose = window.Expose
view.ButtonPress = window.ButtonPress
view.ButtonRelease = window.ButtonRelease
view.MotionNotify = window.MotionNotify
view.EnterNotify = window.EnterNotify
view.LeaveNotify = window.LeaveNotify

view._repaint = window._repaint
view.bitmap = window.bitmap
view._init_opengl = window._init_opengl
view.gl = window.gl
view._repaint_opengl = window._repaint_opengl
view.invalidate = window.invalidate

--menus ----------------------------------------------------------------------

local menu = {}

function app:menu()
end

function menu:add(index, args)
end

function menu:set(index, args)
end

function menu:get(index)
end

function menu:item_count()
end

function menu:remove(index)
end

function menu:get_checked(index)
end

function menu:set_checked(index, checked)
end

function menu:get_enabled(index)
end

function menu:set_enabled(index, enabled)
end

function window:menubar()
end

function window:popup(menu, x, y)
end

--notification icons ---------------------------------------------------------

local notifyicon = {}
app.notifyicon = notifyicon

function notifyicon:new(app, frontend, opt)
	self = setmetatable({app = app, frontend = frontend}, {__index = notifyicon})
	return self
end

function notifyicon:free()
end
--self.backend:_notify_window()

function notifyicon:invalidate()
	self.frontend:_backend_repaint()
end

function notifyicon:get_tooltip()
end

function notifyicon:set_tooltip(tooltip)
end

function notifyicon:get_menu()
end

function notifyicon:set_menu(menu)
end

function notifyicon:rect()
end

--window icon ----------------------------------------------------------------

function window:icon_bitmap(which)
end

function window:invalidate_icon(which)
	self.frontend:_backend_repaint_icon(which)
end

--file chooser ---------------------------------------------------------------

function app:opendialog(opt)
end

function app:savedialog(opt)
end

--clipboard ------------------------------------------------------------------

function app:clipboard_empty(format)
end

function app:clipboard_formats()
end

function app:get_clipboard(format)
end

function app:set_clipboard(t)
end

--drag & drop ----------------------------------------------------------------

--??

function window:start_drag()
end

--buttons --------------------------------------------------------------------


return nw
