-- A helper module for using mini-windows with some common useful features.
-- ========================================================================
-- 	* Drag main area to reposition.
-- 	* Drag edges and corners to resize.
-- 	* Some wrapping for a window right-click menu.
-- 		* Lock position and size menu option.

local rossPluginsDir = GetPluginInfo(GetPluginID(), 20);
local RGBToInt = dofile(rossPluginsDir .. "RGBToInt.lua")
local max, min = math.max, math.min

local M = {}

local winData = {} -- Basically holds "self" data for each window.

local edgeWidth = 6
local windowMinSize = edgeWidth * 2.5
local hotspotIdSeparator = "&& "

local hoveredHandleColor = 16777215 -- white
local borderColor = 12632256 -- light grey

local function winIDFromHotspotID(hotspotID)
	return string.match(hotspotID, "^(.*)&&%s(.*)$")
end

local function makeHotspotID(winID, hotspotName)
	return winID .. hotspotIdSeparator .. hotspotName
end

-- Input Box Stuff:
--------------------------------------------------
local INPUT_BOX_SETTINGS = {
	box_width = 400, box_height = 200,
	prompt_width = 350, prompt_height = 50,
	reply_width = 350, reply_height = 50,
}
local INPUT_BOX_MSG_PREFIX = "\n	 ";  local INPUT_BOX_TITLE_PREFIX = "  "

local function inputBox(msg, title, defaultText)
	local result = utils.inputbox(
		INPUT_BOX_MSG_PREFIX .. msg, INPUT_BOX_TITLE_PREFIX .. title,
		defaultText or "", "Arial", 15, INPUT_BOX_SETTINGS
	)
	if result == "" then  result = nil  end -- Just so we can check `if result then`...
	return result
end

-- Handle Data & Utility:
--------------------------------------------------
local handleHotspotSpecs = {
	lt = { lt = {0, 0}, top = {1, 0}, rt = {1, 0}, bot = {-1, 1} }, -- { multiplier for edgeWidth, multiplier for width/height }
	rt = { lt = {-1, 1}, top = {1, 0}, rt = {0, 1}, bot = {-1, 1} },
	top = { lt = {1, 0}, top = {0, 0}, rt = {-1, 1}, bot = {1, 0} },
	bot = { lt = {1, 0}, top = {-1, 1}, rt = {-1, 1}, bot = {0, 1} },
	ltTop = { lt = {0, 0}, top = {0, 0}, rt = {1, 0}, bot = {1, 0} },
	rtTop = { lt = {-1, 1}, top = {0, 0}, rt = {0, 1}, bot = {1, 0} },
	ltBot = { lt = {0, 0}, top = {-1, 1}, rt = {1, 0}, bot = {0, 1} },
	rtBot = { lt = {-1, 1}, top = {-1, 1}, rt = {0, 1}, bot = {0, 1} },
}

local handleCursors = { -- Ref: https://www.gammon.com.au/scripts/doc.php?function=WindowAddHotspot
	lt = 8, rt = 8, top = 9, bot = 9,
	ltTop = 6, rtBot = 6, rtTop = 7, ltBot = 7
}

local function getHandleRect(name, width, height, ew)
	local v = handleHotspotSpecs[name]
	local lt, top = v.lt[1]*ew + v.lt[2]*width, v.top[1]*ew + v.top[2]*height
	local rt, bot = v.rt[1]*ew + v.rt[2]*width, v.bot[1]*ew + v.bot[2]*height
	return lt, top, rt, bot
end

local handleAxis = {
	lt = {x=-1, y=0}, rt = {x=1, y=0},
	top = {x=0, y=-1}, bot = {x=0, y=1},
	ltTop = {x=-1, y=-1}, rtTop = {x=1, y=-1},
	ltBot = {x=-1, y=1}, rtBot = {x=1, y=1},
}

local SIDE_NORMAL_DIR = {lt = -1, rt = 1, top = -1, bot = 1}

-- Some function upvalues:
local setZ

-- Menu:
--------------------------------------------------
local baseMenu = {
	"Lock Window Position and Size",
	">Window Draw Order", "Move Up", "Move Down", "Set...", "<",
	"-",
}
local menuPrefix = "!"
local menuActiveI_Lock = 1
local menuActiveI_ZUp, menuActiveI_ZDown, menuActiveI_ZSet = 2, 3, 4
local menuFullI_ZSet = 5

local function makeBaseMenu()
	local m = {}
	for i,v in ipairs(baseMenu) do  m[i] = v  end
	return m
end

local function updateMenuString(winID)
	local data = winData[winID]
	local ms = table.concat(data.menu, "|")
	ms = menuPrefix .. ms
	data.menuString = ms
end

local INACTIVE_MENU_ITEM_CHARS = {
	["-"] = true, ["^"] = true, [">"] = true, ["<"] = true,
}

local function updateMenuActiveItems(winID)
	local data = winData[winID]
	local active = {}
	for i,v in ipairs(data.menu) do
		if not INACTIVE_MENU_ITEM_CHARS[string.sub(v, 1, 1)] then
			table.insert(active, v)
		end
	end
	data.menuActiveItems = active
end

local function menuActiveIdxToFullIdx(winID, i)
	local data = winData[winID]
	local item = data.menuActiveItems[i]
	local activeI = 0
	for fi,v in ipairs(data.menu) do
		if not INACTIVE_MENU_ITEM_CHARS[string.sub(v, 1, 1)] then
			activeI = activeI + 1
			if activeI == i and v == item then
				return fi
			end
		end
	end
	Note("ERROR! - window - menuActiveIdxToFullIdx - Menu index not found for active index: " .. i)
end

-- Separate the prefix char (if any) and the actual item text.
local function sanitizeMenuResult(r)
	return string.match(r, "([%-%^<>%+]*)(.*)")
end

local function menuResultHandler(winID, i)
	local data = winData[winID]
	local r = data.menuActiveItems[i]
	-- Note("window.menuResultHandler - " .. i .. ", " .. tostring(r))
	local prefix, item = sanitizeMenuResult(r)
	if r == "" then
		return
	elseif i == menuActiveI_Lock then
		data.locked = not data.locked
		-- Toggle the check on the menu item.
		local newPrefix = prefix == "+" and "" or "+"
		data.menu[1] = newPrefix .. item
		updateMenuString(winID)
		updateMenuActiveItems(winID)
	elseif i == menuActiveI_ZUp then -- Draw-Order Up.
		setZ(winID, 1, true, true)
	elseif i == menuActiveI_ZDown then -- Draw-Order Down.
		setZ(winID, -1, true, true)
	elseif i == menuActiveI_ZSet then
		local z = WindowInfo(winID, 22)
		z = inputBox("Enter your desired Z-Order:", "window.setZ", tostring(z))
		if tonumber(z) then
			z = math.floor(tonumber(z))
			setZ(winID, z, nil, true)
		else
			ColourNote("red", "", "Set Z-Order Failed: Input must be a number.")
		end
	else
		-- Call owner's menu result handler (if any).
		-- NOTE: i = index for active items only.
		i = menuActiveIdxToFullIdx(winID, i) -- Convert index.
		if i then
			i = i - #baseMenu -- Send index after base menu. (the index among user-set items)
			if data.callbacks.menuItemClicked then
				data.callbacks.menuItemClicked(winID, i, prefix, item)
			end
		end
	end
end

-- Private Window Manipulation Functions:
--------------------------------------------------
-- is local, upvalue set above.
function setZ(winID, z, relative, printMessage)
	-- Set Z-Order.
	if relative then  z = WindowInfo(winID, 22) + z  end
	WindowSetZOrder(winID, z)

	-- Update menu item.
	local menu = winData[winID].menu
	local s = string.format("Set...(cur: %i)", z)
	menu[menuFullI_ZSet] = s
	updateMenuString(winID)
	updateMenuActiveItems(winID)
	if printMessage then
		ColourNote("#00FF00", "", "Window Z-Order set to: " .. tostring(z))
	end

	Redraw()
end

-- Drawing:
--------------------------------------------------
local function draw(winID)
	local data = winData[winID]
	local w, h = data.w, data.h

	-- Call owner's draw callback if any.
	if data.callbacks.draw then
		data.callbacks.draw(winID, w, h)
	else
		WindowRectOp(winID, 2, 0, 0, w, h, data.bgColor) -- Draw background
		WindowRectOp(winID, 1, 0, 0, w, h, borderColor) -- Draw border
	end

	-- Draw hovered handle if any. (on top of any window contents)
	if data.hoveredHandle then
		local lt, top, rt, bot = getHandleRect(data.hoveredHandle, w, h, edgeWidth)
		WindowRectOp(winID, 2, lt, top, rt, bot, hoveredHandleColor)
	end

	Redraw() -- Schedule window for redraw.
end

-- Snapping:
--------------------------------------------------
local snapList = { x = {}, y = {} }
local WIN_RECT_INFO_CODES = {x = 10, y = 11, width = 3, height = 4, z = 22}
local snapDist = 10
local snapModifierCode = 0x02 -- Control.

-- Make a list of positions for each axis where window edges are.
local function updateSnapList()
	snapList = { x = {}, y = {} } -- Recreate old snap list entirely.
	local winList = WindowList()
	for i,winID in ipairs(winList) do
		local visible = WindowInfo(winID, 5) and not WindowInfo(winID, 6) -- window show flag and not hidden.
		if visible then
			local rect = {}
			for k,code in pairs(WIN_RECT_INFO_CODES) do
				if k ~= "z" then
					rect[k] = WindowInfo(winID, code)
				end
			end
			local rt, bot = rect.x + rect.width, rect.y + rect.height
			snapList.x[rect.x] = true;  snapList.x[rt] = true
			snapList.y[rect.y] = true;  snapList.y[bot] = true
		end
	end
end

-- Returns the original pos if no snap is in range.
local function snap(pos, axis)
	local list = snapList[axis]
	local minDist, minIndex = math.huge, nil
	for snapPos,_ in pairs(list) do
		local dist = math.abs(pos - snapPos)
		if dist < minDist then
			minDist = dist
			minIndex = snapPos
		end
	end
	if minDist < snapDist then
		return minIndex, minDist
	else
		return pos, minDist
	end
end

-- Main Hotspot Callbacks:
--------------------------------------------------
function mainHover(flags, hotspotID)
	local winID, hotspotName = winIDFromHotspotID(hotspotID)
	local data = winData[winID]
	data.hovered = true
	if data.callbacks.mainHover then
		data.callbacks.mainHover(flags, hotspotID, hotspotName, winID)
	end
end

function mainUnhover(flags, hotspotID)
	local winID, hotspotName = winIDFromHotspotID(hotspotID)
	local data = winData[winID]
	data.hovered = nil
	if data.callbacks.mainUnhover then
		data.callbacks.mainUnhover(flags, hotspotID, hotspotName, winID)
	end
end

function mainPress(flags, hotspotID)
	local winID, hotspotName = winIDFromHotspotID(hotspotID)
	local data = winData[winID]
	if data.isDraggingWindow then
		-- For some reason the Mushclient stops drags when another click is pressed.
		data.isDraggingWindow = false
	end
	if data.callbacks.mainPress then
		data.callbacks.mainPress(flags, hotspotID, hotspotName, winID)
	end
end

function mainCancelPress(flags, hotspotID)
	local winID, hotspotName = winIDFromHotspotID(hotspotID)
	local data = winData[winID]
	if data.callbacks.mainCancelPress then
		data.callbacks.mainCancelPress(flags, hotspotID, hotspotName, winID)
	end
end

function mainRelease(flags, hotspotID)
	local winID, hotspotName = winIDFromHotspotID(hotspotID)
	local data = winData[winID]
	if data.callbacks.mainRelease then
		local consume = data.callbacks.mainRelease(flags, hotspotID, hotspotName, winID)
	end
	if not consume and bit.band(flags, miniwin.hotspot_got_rh_mouse) > 0 then
		-- Open right-click menu.
		local x, y = WindowInfo(winID, 14), WindowInfo(winID, 15)
		local i = WindowMenu(winID, x, y, data.menuString)
		if i ~= "" then  menuResultHandler(winID, tonumber(i))  end
	end
end

local _dragSnapDistance = { -- Reuse the same table.
	lt = 0, rt = 0, top = 0, bot = 0,
}

local dragStartOX, dragStartOY -- Initial from mouse to moving thing.

function mainDrag(flags, hotspotID)
	local winID, hotspotName = winIDFromHotspotID(hotspotID)
	local data = winData[winID]
	if not data.locked and bit.band(flags, miniwin.hotspot_got_lh_mouse) > 0 then
		if not data.isDraggingWindow then -- Start drag.
			data.isDraggingWindow = true
			dragStartOX, dragStartOY = WindowInfo(winID, 14), WindowInfo(winID, 15)
			updateSnapList()
		else -- Drag move.
			local mouseX, mouseY = WindowInfo(winID, 17), WindowInfo(winID, 18)

			-- Make sure right and bottom are up-to-date.
			data.rt, data.bot = data.lt + data.w, data.top + data.h

			local dx = (mouseX - dragStartOX) - data.lt
			local dy = (mouseY - dragStartOY) - data.top

			-- Move all edge positions by dx, dy.
			data.lt, data.rt = data.lt + dx, data.rt + dx
			data.top, data.bot = data.top + dy, data.bot + dy

			-- Figure snapping.
			if not (bit.test(flags, snapModifierCode)) then
				local snapD = _dragSnapDistance
				-- Get snap for each new edge pos.
				local _
				_, snapD.lt = snap(data.lt, "x") -- `val` can be nil, `dist` is always a number.
				_, snapD.rt = snap(data.rt, "x") -- Not actually using the value.
				_, snapD.top = snap(data.top, "y")
				_, snapD.bot = snap(data.bot, "y")

				-- For each axis, get the closer snap, or nil if neither are in range.
				snapX = "lt"
				if snapD.rt < snapD.lt then  snapX = "rt"  end
				if snapD[snapX] > snapDist then  snapX = nil  end

				snapY = "top"
				if snapD.bot < snapD.top then  snapY = "bot"  end
				if snapD[snapY] > snapDist then  snapY = nil  end

				-- Add in the extra snap distance.
				if snapX then  data.lt = data.lt + snapD[snapX] * SIDE_NORMAL_DIR[snapX]  end
				if snapY then  data.top = data.top + snapD[snapY] * SIDE_NORMAL_DIR[snapY]  end
			end
			WindowPosition(winID, data.lt, data.top, 0, winData[winID].flags)
		end
	end
	if data.callbacks.mainDrag then
		data.callbacks.mainDrag(flags, hotspotID, hotspotName, winID)
	end
end

function mainDragEnd(flags, hotspotID)
	local winID, hotspotName = winIDFromHotspotID(hotspotID)
	local data = winData[winID]
	if data.isDraggingWindow and bit.band(flags, miniwin.hotspot_got_lh_mouse) > 0 then
		data.isDraggingWindow = nil
	end
	if data.callbacks.mainDragEnd then
		data.callbacks.mainDragEnd(flags, hotspotID, hotspotName, winID)
	end
end

-- Resize Handle Hotspot Callbacks:
--------------------------------------------------
function handleHover(flags, hotspotID)
	local winID, hotspotName = winIDFromHotspotID(hotspotID)
	local data = winData[winID]
	if not data.locked then
		data.hoveredHandle = hotspotName
		draw(winID)
	end
	if data.callbacks.handleHover then
		data.callbacks.handleHover(flags, hotspotID, hotspotName, winID)
	end
end

function handleUnhover(flags, hotspotID)
	local winID, hotspotName = winIDFromHotspotID(hotspotID)
	local data = winData[winID]
	if data.hoveredHandle then
		data.hoveredHandle = nil
		draw(winID)
	end
	if data.callbacks.handleUnhover then
		data.callbacks.handleUnhover(flags, hotspotID, hotspotName, winID)
	end
end

function handlePress(flags, hotspotID)
	local winID, hotspotName = winIDFromHotspotID(hotspotID)
	local data = winData[winID]
	if data.callbacks.handlePress then
		data.callbacks.handlePress(flags, hotspotID, hotspotName, winID)
	end
end

function handleCancelPress(flags, hotspotID)
	handleUnhover(flags, hotspotID)
	local winID, hotspotName = winIDFromHotspotID(hotspotID)
	local data = winData[winID]
	if data.callbacks.handleCancelPress then
		data.callbacks.handleCancelPress(flags, hotspotID, hotspotName, winID)
	end
end

function handleRelease(flags, hotspotID)
	local winID, hotspotName = winIDFromHotspotID(hotspotID)
	local data = winData[winID]
	if data.callbacks.handleRelease then
		data.callbacks.handleRelease(flags, hotspotID, hotspotName, winID)
	end
end

function handleDrag(flags, hotspotID)
	local winID, hotspotName = winIDFromHotspotID(hotspotID)
	local data = winData[winID]
	if not data.locked and bit.band(flags, miniwin.hotspot_got_lh_mouse) > 0 then
		-- Get mouse pos.
		local mouseX, mouseY = WindowInfo(winID, 17), WindowInfo(winID, 18)
		-- Make sure right and bottom are up-to-date.
		data.rt, data.bot = data.lt + data.w, data.top + data.h
		local snapEnabled = not (bit.test(flags, snapModifierCode))

		if not data.isDraggingHandle then -- Start drag
			data.isDraggingHandle = true

			local xDir, yDir = handleAxis[hotspotName].x, handleAxis[hotspotName].y

			-- Save initial mouse offset relative to moving edge(s).
			dragStartOX, dragStartOY = 0, 0
			if xDir == 1 then  dragStartOX = data.rt - mouseX
			elseif xDir == -1 then  dragStartOX = data.lt - mouseX
			end
			if yDir == 1 then  dragStartOY = data.bot - mouseY
			elseif yDir == -1 then  dragStartOY = data.top - mouseY
			end

			updateSnapList()
		else -- Drag update.
			local targetX, targetY = mouseX + dragStartOX, mouseY + dragStartOY

			if snapEnabled then
				targetX, targetY = snap(targetX, "x"), snap(targetY, "y")
			end
			-- Calculate new positions of appropriate edges.
			local xDir, yDir = handleAxis[hotspotName].x, handleAxis[hotspotName].y
			if xDir == 1 then  data.rt = max(targetX, data.lt + windowMinSize)
			elseif xDir == -1 then  data.lt = min(targetX, data.rt - windowMinSize)
			end
			if yDir == 1 then  data.bot = max(targetY, data.top + windowMinSize)
			elseif yDir == -1 then  data.top = min(targetY, data.bot - windowMinSize)
			end
			-- Update width & height.
			local oldW, oldH = data.w, data.h
			data.w, data.h = data.rt - data.lt, data.bot - data.top
			if data.callbacks.sizeUpdated then
				data.callbacks.sizeUpdated(winID, data.w, data.h, oldW, oldH)
			end

			WindowResize(winID, data.w, data.h, data.bgColor)
			WindowPosition(winID, data.lt, data.top, 0, data.flags)
			draw(winID)
		end
	end
end

function handleDragEnd(flags, hotspotID)
	-- Only update hotspots on drag end.
	local winID, hotspotName = winIDFromHotspotID(hotspotID)
	local data = winData[winID]
	if not data.locked and data.isDraggingHandle then
		data.isDraggingHandle = false
		local width, height = data.w, data.h
		local ew = edgeWidth

		-- Resize handle hotspots.
		for name,v in pairs(handleHotspotSpecs) do
			local lt, top, rt, bot = getHandleRect(name, width, height, ew)
			local hotID = makeHotspotID(winID, name)
			WindowMoveHotspot(winID, hotID, lt, top, rt, bot)
		end
		do -- Resize main hotspot.
			local hotID = makeHotspotID(winID, "main")
			WindowMoveHotspot(winID, hotID, ew, ew, width-ew, height-ew)
		end
	end
end

-- Public functions:
--------------------------------------------------
function M.new(id, lt, top, width, height, z, align, flags, bgColor, visible, locked, menuCb, drawCb)
	-- Handle default args.
	align = align or 5
	flags = flags or 2
	bgColor = bgColor or RGBToInt()

	-- Set win data.
	local data = {
		flags = flags, bgColor = bgColor, locked = locked,
		callbacks = {menuItemClicked = menuCb, draw = drawCb}, hotspotIDs = {}
	}
	winData[id] = data
	data.lt, data.top, data.w, data.h = lt, top, width, height
	data.rt, data.bot = data.lt + data.w, data.top + data.h

	-- Create menu.
	data.menu = makeBaseMenu()
	if locked then  data.menu[1] = "+" .. data.menu[1]  end
	updateMenuActiveItems(id)
	updateMenuString(id)

	-- Create window.
	WindowCreate(id, lt, top, width, height, align, flags, bgColor)
	if visible then  WindowShow(id, true)  end
	WindowRectOp(id, 1, 0, 0, width, height, borderColor) -- Draw 1 pixel border.

	-- Set draw order.
	setZ(id, z)

	-- Add main hotspot.
	local mainHotspotID = makeHotspotID(id, "main")
	data.hotspotIDs.main = mainHotspotID
	WindowAddHotspot(
		id, mainHotspotID,
		edgeWidth, edgeWidth, width-edgeWidth, height-edgeWidth,
		"mainHover", "mainUnhover", "mainPress", "mainCancelPress", "mainRelease",
		"", miniwin.cursor_arrow, 0
	)
	WindowDragHandler(id, mainHotspotID, "mainDrag", "mainDragEnd", 0)

	-- Add edge and corner hotspots.
	for name,v in pairs(handleHotspotSpecs) do
		local hotspotID = makeHotspotID(id, name)
		data.hotspotIDs[name] = hotspotID
		local lt, top, rt, bot = getHandleRect(name, width, height, edgeWidth)
		WindowAddHotspot(
			id, hotspotID, lt, top, rt, bot,
			"handleHover", "handleUnhover",
			"handlePress", "handleCancelPress", "handleRelease",
			"", handleCursors[name], 0 -- Tooltip, cursor, flags
		)
		WindowDragHandler(id, hotspotID, "handleDrag", "handleDragEnd", 0) -- winID, hotspotID, onMove, onRelease, flags
	end
end

function M.draw(winID) -- Allow plugins to trigger redraw on their windows.
	draw(winID)
end

function M.addMenuItem(winID, str, func, arg1, arg2, arg3)
	local data = winData[winID]
	table.insert(data.menu, str)
	data.menuResponses[#data.menu] = {func, arg1, arg2, arg3}
end

function M.addMenuItems(winID, startI, ...)
	-- Can take a variable number of item arguments or table of items.
	local items = {...}
	if type(items[1]) == "table" then  items = items[1]  end
	local startI = (startI or 1) + #baseMenu
	local menu = winData[winID].menu
	for i,v in ipairs(items) do
		i = (i-1) + startI
		table.insert(menu, i, v)
	end
	updateMenuString(winID)
	updateMenuActiveItems(winID)
end

function M.setMenuItem(winID, i, item)
	local menu = winData[winID].menu
	i = i + #baseMenu
	menu[i] = item
	updateMenuString(winID)
	updateMenuActiveItems(winID)
end

function M.checkMenuItem(winID, i, setChecked)
	local menu = winData[winID].menu
	i = i + #baseMenu
	local prefix, item = sanitizeMenuResult(menu[i])
	local curChecked = false
	local pf1, pf2
	local prefixLength = string.len(prefix)
	if prefixLength > 1 then
		-- Items can be disabled AND checked, with a prefx of either "^+" or "+^".
		pf1 = string.sub(prefix, 1, 1)
		pf2 = string.sub(prefix, 2, 2)
		-- None of the other prefixes work together though, ">+" is NOT shown checked.
		if pf1 ~= "^" then
			pf2 = ""
			if pf1 ~= "+" then -- Not ^ and not +, must be another prefix which won't work with checked.
				return -- Can't be checked.
			end
		end
		curChecked = (pf1 == "+" or pf2 == "+") and true or false
	elseif prefixLength == 1 then -- Only one character prefix.
		curChecked = (prefix == "+") and true or false
		if not curChecked and prefix ~= "^" then
			return -- Can't be checked.
		end
	end
	-- Otherwise, prefixLength == 0, not checked and can be checked.

	if not setChecked then  curChecked = not curChecked
	elseif setChecked == 0 then  curChecked = false
	else  curChecked = true  end

	menu[i] = (curChecked and "+" or "") .. item

	updateMenuActiveItems(winID)
	updateMenuString(winID)

	return true -- Success.
end

function M.getLocked(winID)
	return winData[winID].locked
end

function M.getRect(winID)
	local x, y, w, h, z
	x = WindowInfo(winID, WIN_RECT_INFO_CODES.x)
	y = WindowInfo(winID, WIN_RECT_INFO_CODES.y)
	w = WindowInfo(winID, WIN_RECT_INFO_CODES.width)
	h = WindowInfo(winID, WIN_RECT_INFO_CODES.height)
	z = WindowInfo(winID, WIN_RECT_INFO_CODES.z)
	return x, y, w, h, z
end

function M.getSize(winID)
	local w = WindowInfo(winID, WIN_RECT_INFO_CODES.width)
	local h = WindowInfo(winID, WIN_RECT_INFO_CODES.height)
	return w, h
end

function M.getHotspotID(winID, name)
	return winData[winID].hotspotIDs[name]
end

-- Available callbacks: draw, sizeUpdated, menuItemClicked, mainHover,
-- mainUnhover, mainPress, mainCancelPress, mainRelease, mainDrag, mainDragEnd,
-- handleHover, handleUnhover, handlePress, handleCancelPress, handleRelease
function M.setCallback(winID, name, func)
	local data = winData[winID]
	data.callbacks[name] = func
end

return M