-- lx-bundle:true

--- Parse, compare, and display shortcuts for QUI and its Lx callers
-- The lowercase value is used for matching while display text is prepared only for drawing
-- @module qui.shortcut

local shortcut = {}

-- Keep the established Lx matching order stable
local matching_modifiers = { "ctrl", "cmd", "alt", "altgr", "shift" }

-- Put Shift first when a shortcut is shown to a person
local display_modifiers = {
	shift = "Shift",
	ctrl = "Ctrl",
	cmd = "Cmd",
	alt = "Alt",
	altgr = "AltGr",
}
local display_modifier_order = { "shift", "ctrl", "cmd", "alt", "altgr" }

local mouse_button_aliases = {
	click = "lclick",
	lclick = "lclick",
	lbutton = "lclick",
	mclick = "mclick",
	mbutton = "mclick",
	rclick = "rclick",
	rbutton = "rclick",
	xclick1 = "xclick1",
	xbutton1 = "xclick1",
	xclick2 = "xclick2",
	xbutton2 = "xclick2",
}

local display_key_names = {
	["backspace"] = "Backspace",
	["delete"] = "Delete",
	["down"] = "Down",
	["end"] = "End",
	["enter"] = "Enter",
	["escape"] = "Escape",
	["home"] = "Home",
	["insert"] = "Insert",
	["left"] = "Left",
	["pageup"] = "Page Up",
	["pagedown"] = "Page Down",
	["right"] = "Right",
	["space"] = "Space",
	["tab"] = "Tab",
	["up"] = "Up",
}

--- Normalize one shortcut into the established Lx matching order
-- Only leading modifier names are reordered so SDL key names remain intact
-- @param stroke any source shortcut
-- @return any normalized shortcut or the original non-string value
function shortcut.normalize(stroke)
	if type(stroke) ~= "string" then
		return stroke
	end

	local rest = stroke
	local seen = {}
	local progressed = true

	-- Consume only leading modifier prefixes such as 'ctrl+' and 'shift+'
	while progressed do
		progressed = false
		for _, modifier in ipairs(matching_modifiers) do
			local prefix = modifier .. "+"
			if rest:sub(1, #prefix) == prefix then
				seen[modifier] = true
				rest = rest:sub(#prefix + 1)
				progressed = true
				break
			end
		end
	end

	local out = ""
	for _, modifier in ipairs(matching_modifiers) do
		if seen[modifier] then
			out = out .. modifier .. "+"
		end
	end

	-- Enter and mouse aliases keep old declarations and live events on one value
	if rest == "return" then
		rest = "enter"
	elseif rest == "keypad return" then
		rest = "keypad enter"
	end
	rest = mouse_button_aliases[rest] or rest

	return out .. rest
end

--- Parse one authored shortcut into its stable lowercase matching value
-- @param source string authored shortcut in any letter case or modifier order
-- @return string normalized lowercase shortcut
function shortcut.parse(source)
	assert(type(source) == "string", "shortcut expects a string")

	local lowered = source:lower()
	assert(lowered ~= "", "shortcut cannot be empty")
	local normalized = shortcut.normalize(lowered)
	local key = normalized:match("([^+]+)$")
	-- Pattern breakdown:
	-- '([^+]+)' captures the final non-empty part after any modifier prefixes
	-- '$' requires that part to reach the end of the shortcut
	assert(key ~= nil and key ~= "", "shortcut requires a key")
	for _, modifier in ipairs(matching_modifiers) do
		assert(normalized ~= modifier, "shortcut requires a key after " .. modifier)
	end

	return normalized
end

--- Build one canonical shortcut from a normalized QUI keyboard event
-- @param event table key plus current modifier booleans
-- @return string|nil normalized shortcut
function shortcut.from_event(event)
	if type(event) ~= "table" or type(event.key) ~= "string" or event.key == "" then
		return nil
	end

	local source = ""
	if event.control and not event.altgr then
		source = source .. "ctrl+"
	end
	if event.system then
		source = source .. "cmd+"
	end
	if event.alt and not event.altgr then
		source = source .. "alt+"
	end
	if event.altgr then
		source = source .. "altgr+"
	end
	if event.shift then
		source = source .. "shift+"
	end

	return shortcut.parse(source .. event.key)
end

--- Turn one canonical shortcut into stable user-facing text
-- Shift deliberately appears first regardless of authored or matching order
-- @param value string shortcut accepted by 'shortcut.parse'
-- @return string display_text
function shortcut.format(value)
	local normalized = shortcut.parse(value)
	local rest = normalized
	local seen = {}

	for _, modifier in ipairs(matching_modifiers) do
		local prefix = modifier .. "+"
		if rest:sub(1, #prefix) == prefix then
			seen[modifier] = true
			rest = rest:sub(#prefix + 1)
		end
	end

	local parts = {}
	for _, modifier in ipairs(display_modifier_order) do
		if seen[modifier] then
			parts[#parts + 1] = display_modifiers[modifier]
		end
	end

	local display_key = display_key_names[rest]
	if display_key == nil then
		-- Pattern breakdown:
		-- '(%S+)' visits each non-space word in an SDL key name
		-- Each word receives one leading capital without changing its remaining letters
		display_key = rest:gsub("(%S+)", function(word)
			return word:sub(1, 1):upper() .. word:sub(2)
		end)
	end
	parts[#parts + 1] = display_key

	return table.concat(parts, "+")
end

return shortcut
