-- lx-bundle:true
---@diagnostic disable: duplicate-set-field,redundant-parameter

-- This file is part of Lx, created by Liqube <www.liqube.com>
-- Copyright Liqube Audio. All rights reserved.
-- Lx Interim License. See LICENSE for details.

--- Create one cache that does not keep unused font handles alive
-- @return table cache
local fontcache = {}

local function new_weak_cache()
	return setmetatable({}, { __mode = "v" })
end

local base_cache = new_weak_cache()
local group_cache = new_weak_cache()
local renderer_font_load = renderer.font.load -- Groups need ownership-safe loads outside base cache

-- Detect dead or transferred font handles so cache does not return unusable userdata
local function is_font_alive(font)
	if not font or type(font) ~= "userdata" then
		return false
	end
	local ok, h = pcall(font.get_height, font)
	return ok and type(h) == "number" and h > 0
end

--- Normalize one optional per-font embolden value
-- @param opts table|nil font load options
-- @return number embolden clamped renderer weight
local function get_embolden(opts)
	local embolden = tonumber(type(opts) == "table" and opts.embolden) or 0
	-- Leave unsafe numbers intact so the native API can reject them
	if embolden ~= embolden or embolden == math.huge or embolden == -math.huge then
		return embolden
	end

	return math.max(0, math.min(2, embolden))
end

--- Normalize key shape so repeated load requests hit the same cached font handle
-- @param filename string font path
-- @param size number font size
-- @param opts table|nil font load options
-- @return string key stable cache key
local function make_base_key(filename, size, opts)
	local n = tonumber(size) or 0

	return table.concat({
		tostring(filename),
		string.format("%.4f", n),
		string.format("%.4f", get_embolden(opts)),
	}, "\31")
end

--- Build stable key for grouped fallback fonts
-- @param primary_filename string primary font path
-- @param size number font size
-- @param fallback_paths table fallback font paths
-- @param opts table|nil font load options
-- @return string key stable cache key
local function make_group_key(primary_filename, size, fallback_paths, opts)
	local n = tonumber(size) or 0
	local parts = {
		tostring(primary_filename),
		string.format("%.4f", n),
		string.format("%.4f", get_embolden(opts)),
	}
	for _, path in ipairs(fallback_paths or {}) do
		parts[#parts + 1] = tostring(path)
	end
	return table.concat(parts, "\31")
end

-- Allow explicit cache reset for diagnostics or future runtime controls
function fontcache.clear()
	base_cache = new_weak_cache()
	group_cache = new_weak_cache()
end

-- Clear only grouped fallback cache entries
function fontcache.clear_groups()
	group_cache = new_weak_cache()
end

-- Clear only base renderer.font.load cache entries
function fontcache.clear_base()
	base_cache = new_weak_cache()
end

--- Load one font by path, size, and optional local weight using shared cache
-- @param filename string font path
-- @param size number font size
-- @param opts table|nil per-font rendering options
-- @return userdata font live renderer font
function fontcache.load(filename, size, opts)
	local key = make_base_key(filename, size, opts)
	local hit = base_cache[key]
	if hit and is_font_alive(hit) then
		return hit
	end
	base_cache[key] = nil
	local font = renderer.font.load(filename, size, opts)
	base_cache[key] = font
	return font
end

--- Load primary and fallback font group through the centralized cache
-- @param primary_filename string primary font path
-- @param size number font size
-- @param fallback_paths table fallback font paths
-- @param opts table|nil cache and per-font rendering options
-- @return userdata font live grouped renderer font
function fontcache.load_group_with_fallbacks(primary_filename, size, fallback_paths, opts)
	opts = type(opts) == "table" and opts or {}
	local use_cache = opts.use_cache ~= false
	local paths = {}
	for _, path in ipairs(fallback_paths or {}) do
		paths[#paths + 1] = tostring(path)
	end

	local key = nil
	if use_cache then
		key = make_group_key(primary_filename, size, paths, opts)
		local hit = group_cache[key]
		if hit and is_font_alive(hit) then
			return hit
		end
		group_cache[key] = nil
	end

	if #paths == 0 then
		local primary = fontcache.load(primary_filename, size, opts)

		if key then
			group_cache[key] = primary
		end
		return primary
	end

	-- A lazy group takes ownership, so its primary must never come from the shared base cache
	local primary = renderer_font_load(primary_filename, size, opts)
	local group_font = renderer.font.group_lazy(primary, paths, size)

	if key then
		group_cache[key] = group_font
	end
	return group_font
end

--- Install one-time wrapper around renderer.font.load to dedupe Lua-side font loads
-- @return boolean installed true when the wrapper is active
function fontcache.install()
	if renderer.font._lx_cached_load_installed then
		return true
	end

	local upstream = renderer_font_load

	renderer.font.load = function(filename, size, opts)
		local key = make_base_key(filename, size, opts)
		local hit = base_cache[key]
		-- group_lazy transfers ownership out of primary handles and leaves stale userdata in cache
		if hit and is_font_alive(hit) then
			return hit
		end
		base_cache[key] = nil
		local font = upstream(filename, size, opts)
		base_cache[key] = font
		return font
	end

	renderer.font._lx_cached_load_installed = true
	return true
end

return fontcache
