-- lx-bundle:true

local common = {}

-- Returns true when a byte belongs to a UTF-8 continuation sequence
function common.is_utf8_cont(char)
	-- Accept both a numeric byte and a one-byte string so hot paths can avoid tiny substring allocations
	local byte = type(char) == "number" and char or char:byte()
	return byte >= 0x80 and byte < 0xc0
end

-- Iterates UTF-8 characters in a string
function common.utf8_chars(text)
	return text:gmatch("[\0-\x7f\xc2-\xf4][\x80-\xbf]*")
end

-- Returns next UTF-8 character and its byte range from index 'i'
function common.next_utf8_char(text, i)
	-- Walk bytes directly here because byte-sized substring allocations are extremely expensive in hot loops
	local len = #text
	local j = i + 1
	while j <= len do
		local byte = text:byte(j)
		if not byte or byte < 0x80 or byte >= 0xc0 then
			break
		end
		j = j + 1
	end
	j = j - 1
	local c = text:sub(i, j)
	return c, i, j
end

-- Returns text with trailing whitespace removed
function common.rtrim(text)
	return (tostring(text or ""):gsub("%s+$", ""))
end

-- Returns text with leading whitespace removed
function common.ltrim(text)
	return (tostring(text or ""):gsub("^%s+", ""))
end

-- Returns text with leading and trailing whitespace removed
function common.trim(text)
	local s = tostring(text or "")
	return (s:match("^%s*(.-)%s*$"))
end

-- Returns true when text is empty or only whitespace
function common.is_empty_line(text)
	return tostring(text or ""):match("^%s*$") ~= nil
end

-- Returns true when text starts with leading indentation
function common.has_leading_indent(text)
	return tostring(text or ""):match("^[ \t]") ~= nil
end

-- Returns rendered text width using fixed tab stops
function common.calculate_text_width(text, font, tab_size)
	local width = 0
	local i = 1
	while i <= #text do
		local c, _, end_i = common.next_utf8_char(text, i)
		if c == "\t" then
			width = width + (tab_size - (width % tab_size))
		else
			width = width + font:get_width(c)
		end
		i = end_i + 1
	end
	return width
end

-- Clamps a number to the inclusive '[lo, hi]' range
function common.clamp(n, lo, hi)
	return math.max(math.min(n, hi), lo)
end

-- XL: Returns a new table containing keys from 'a', overridden by keys from 'b'
-- XL: Lite XL plugins commonly use this to merge defaults with user config
function common.merge(a, b)
	a = type(a) == "table" and a or {}
	local t = {}
	for k, v in pairs(a) do
		t[k] = v
	end
	if type(b) == "table" then
		for k, v in pairs(b) do
			t[k] = v
		end
	end
	return t
end

-- Rounds a number to the nearest integer
function common.round(n)
	return n >= 0 and math.floor(n + 0.5) or math.ceil(n - 0.5)
end

-- Linearly interpolates numbers or nested table values
function common.lerp(a, b, t)
	if type(a) ~= "table" then
		return a + (b - a) * t
	end
	local res = {}
	for k, v in pairs(b) do
		res[k] = common.lerp(a[k], v, t)
	end
	return res
end

-- Parses CSS-like color strings into an RGBA table
function common.color(str)
	-- Support #rrggbbaa in addition to #rrggbb and rgba()
	local r, g, b, a = str:match("^#(%x%x)(%x%x)(%x%x)(%x%x)$")
	if r then
		return { tonumber(r, 16), tonumber(g, 16), tonumber(b, 16), tonumber(a, 16) }
	end

	r, g, b = str:match("^#(%x%x)(%x%x)(%x%x)$")
	if r then
		return { tonumber(r, 16), tonumber(g, 16), tonumber(b, 16), 0xff }
	end

	if str:match("rgba?%s*%([%d%s%.,]+%)") then
		local f = str:gmatch("[%d.]+")
		r = (f() or 0)
		g = (f() or 0)
		b = (f() or 0)
		a = f() or 1
		-- We return a ready-to-use color table so callers can use the flat form
		--   color = common.color('rgba(255, 255, 255, 0.5)')
		-- The C core still accepts legacy nested form when enabled
		--   color = { common.color('rgba(255, 255, 255, 0.5)') }
		return { r, g, b, a * 0xff }
	end

	error(string.format("bad color string '%s'", str))
end

-- Resolve urgency token to a style color; unknown values map to normal text color
function common.urgency_to_color(urgency)
	local style = package.loaded["core.style"]
	if type(style) ~= "table" then
		return nil
	end
	local key = type(urgency) == "string" and urgency:lower() or "normal"
	if key == "" then
		key = "normal"
	end
	local map = {
		normal = style.input or style.text,
		info = style.notice or style.text,
		notice = style.notice or style.text,
		success = style.success or style.text,
		warning = style.warning or style.warn or style.error or style.text,
		warn = style.warning or style.warn or style.error or style.text,
		error = style.error or style.warning or style.text,
		remark = style.remark or style.text,
		changed = style.changed or style.text,
		added = style.added or style.text,
		removed = style.removed or style.text,
	}
	return map[key] or map.normal
end

-- Orders fuzzy-match results by descending score
local function compare_score(a, b)
	return a.score > b.score
end

-- Returns items from a list sorted by fuzzy-match score against the needle
local function fuzzy_match_items(items, needle)
	local res = {}
	for _, item in ipairs(items) do
		local score = system.fuzzy_match(tostring(item), needle)
		if score then
			table.insert(res, { text = item, score = score })
		end
	end
	table.sort(res, compare_score)
	for i, item in ipairs(res) do
		res[i] = item.text
	end
	return res
end

-- Fuzzy-matches a string or list against a search needle
function common.fuzzy_match(haystack, needle)
	if type(haystack) == "table" then
		return fuzzy_match_items(haystack, needle)
	end
	return system.fuzzy_match(haystack, needle)
end

-- Returns path completion candidates that start with the provided text
function common.path_suggest(text)
	local path, name = text:match("^(.-)([^/\\]*)$")
	local files = system.list_dir(path == "" and "." or path) or {}
	local res = {}
	for _, file in ipairs(files) do
		file = path .. file
		local info = system.get_file_info(file)
		if info then
			if info.type == "dir" then
				file = file .. PATHSEP
			end
			if file:lower():find(text:lower(), nil, true) == 1 then
				table.insert(res, file)
			end
		end
	end
	return res
end

-- Matches text against a pattern string or list of patterns
function common.match_pattern(text, pattern, ...)
	if type(pattern) == "string" then
		return text:find(pattern, ...)
	end
	for _, p in ipairs(pattern) do
		local s, e = common.match_pattern(text, p, ...)
		if s then
			return s, e
		end
	end
	return false
end

-- Returns true when 'text' begins with 'prefix'
function common.begins_with(text, prefix)
	return type(text) == "string" and text:sub(1, #prefix) == prefix
end

-- Applies a transform function to each line while preserving original line endings
function common.map_lines_keep_eol(text, fn)
	if text == "" then
		return ""
	end
	local out = {}
	local pos = 1
	while pos <= #text do
		local nl = text:find("\n", pos, true)
		local line, eol
		if nl then
			line = text:sub(pos, nl - 1)
			eol = "\n"
			pos = nl + 1
		else
			line = text:sub(pos)
			eol = ""
			pos = #text + 1
		end
		out[#out + 1] = fn(line) .. eol
	end
	return table.concat(out)
end

-- Draws aligned text inside a rectangle and returns trailing coordinates
function common.draw_text(font, color, text, align, x, y, w, h)
	local tw, th = font:get_width(text), font:get_height(text)
	if align == "center" then
		x = x + (w - tw) / 2
	elseif align == "right" then
		x = x + (w - tw)
	end
	y = common.round(y + (h - th) / 2)
	return renderer.draw_text(font, text, x, y, color), y + th
end

-- Draws volatile UI text inside a rectangle and returns trailing coordinates
function common.draw_transient_text(font, color, text, align, x, y, w, h)
	local tw = renderer.measure_transient_text(font, text)
	local th = font:get_height()
	if align == "center" then
		x = x + (w - tw) / 2
	elseif align == "right" then
		x = x + (w - tw)
	end
	y = common.round(y + (h - th) / 2)
	return renderer.draw_transient_text(font, text, x, y, color), y + th
end

-- Times a function call and prints a simple performance summary
function common.bench(name, fn, ...)
	local start = system.get_time()
	local res = fn(...)
	local t = system.get_time() - start
	local ms = t * 1000
	local per = (t / (1 / 60)) * 100
	print(string.format("*** %-16s : %8.3fms %6.2f%%", name, ms, per))
	return res
end

return common
