-- lx-bundle:true

-- 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.

--- UI font loading and fallback selection
-- Keeps fallback font selection separate from renderer glyph mode switches

local config = require("core.config")
local fontcache = require("core.style.fontcache")
local fonts = {}

local active_font_signature = nil

-- Keep bundled UI face names in one place for style handles and previews
local UI_FONT_PATHS = {
	regular = FONTS_DIR .. "sans.ttf",
	bold = FONTS_DIR .. "sansb.ttf",
	italic = FONTS_DIR .. "sansi.ttf",
	bold_italic = FONTS_DIR .. "sansbi.ttf",
	heavy = FONTS_DIR .. "sansh.ttf",
	heavy_italic = FONTS_DIR .. "sanshi.ttf",
	light = FONTS_DIR .. "sansl.ttf",
	light_italic = FONTS_DIR .. "sansli.ttf",
}

-- Default app-local fallback fonts
-- User config may override fonts.fallback_fonts

-- The setting config.enabled_fallback_font_tags = {} is a filter system
-- that decides which fallback fonts are actually being tried:
--   any_tags: font entry is enabled if at least one of these tags is in config.enabled_fallback_font_tags
--   all_tags: font entry is enabled only if all listed tags are in config.enabled_fallback_font_tags
--   not_tags: font entry is disabled if any listed tag is in config.enabled_fallback_font_tags
-- Any of all of these three may be used at the same time.

fonts.fallback_fonts = fonts.fallback_fonts or {
	-- Generic symbols
	{ path = FONTS_DIR .. "symbols.ttf", any_tags = { "symbols" } },

	-- Color emoji coverage
	-- Note that this should come before monochrome emoji so that they can be optional
	-- and allow the monochrome emoji to override missing color emojis
	{ path = FONTS_DIR .. "coloremoji.ttf", any_tags = { "coloremoji" } },

	-- Monochrome emoji coverage
	{ path = FONTS_DIR .. "emoji.ttf", any_tags = { "emoji" } },

	-- Music symbols coverage
	{ path = FONTS_DIR .. "music.ttf", any_tags = { "music" } },

	-- Fallbacks for missing symbols and emoji
	-- Since these are for Windows only we use 'all_tags' (AND) instead of 'any_tags' (OR),
	-- i.e. we'll require both tags to use these fallback fonts
	{ path = OS_FONTS_DIR .. "seguisym.ttf", all_tags = { "symbols", "windows" } },
	{ path = OS_FONTS_DIR .. "seguiemj.ttf", all_tags = { "coloremoji", "windows" } },

	-- Additional set of Unicode symbols
	{ path = FONTS_DIR .. "symbols2.ttf", any_tags = { "symbols" } },

	-- Japanese
	{ path = FONTS_DIR .. "fontpack/japanese.ttf", any_tags = { "cjk", "japanese" } },

	-- Korean
	{ path = FONTS_DIR .. "fontpack/korean.ttf", any_tags = { "cjk", "korean" } },

	-- Chinese
	{ path = FONTS_DIR .. "fontpack/chinesesi.ttf", any_tags = { "chinese", "cjk" } }, -- simplified
	{ path = FONTS_DIR .. "fontpack/chinesetr.ttf", any_tags = { "chinesetr" } }, -- traditional

	-- Arabic-script (Persian/Urdu)
	{ path = FONTS_DIR .. "fontpack/arabic.ttf", any_tags = { "arabic" } },

	-- Thai
	{ path = FONTS_DIR .. "fontpack/thai.ttf", any_tags = { "thai" } },

	-- Indic
	{ path = FONTS_DIR .. "fontpack/devanagari.ttf", any_tags = { "indic", "devanagari" } },
	{ path = FONTS_DIR .. "fontpack/kannada.ttf", any_tags = { "indic", "kannada" } },
	{ path = FONTS_DIR .. "fontpack/tamil.ttf", any_tags = { "indic", "tamil" } },
	{ path = FONTS_DIR .. "fontpack/telugu.ttf", any_tags = { "indic", "telugu" } },
	{ path = FONTS_DIR .. "fontpack/bengali.ttf", any_tags = { "indic", "bengali" } },

	-- Arabic-script fallback on Windows only
	{ path = OS_FONTS_DIR .. "segoeui.ttf", all_tags = { "arabic", "windows" } },

	-- Indic fallback on Windows only
	{ path = OS_FONTS_DIR .. "nirmala.ttf", all_tags = { "indic", "windows" } },

	-- General fallback on Windows only
	{ path = OS_FONTS_DIR .. "arial.ttf", all_tags = { "windows" } },
}

--- Append one fallback path only once
-- @param paths table collected path array
-- @param seen table set of normalized paths
-- @param path string|nil candidate font path
-- @return nil
local function append_unique_path(paths, seen, path)
	local normalized_path = tostring(path or "")

	if normalized_path == "" or seen[normalized_path] then
		return
	end

	seen[normalized_path] = true
	paths[#paths + 1] = normalized_path
end

--- Build the enabled fallback tag set from config
-- @return table enabled tag lookup table
local function build_enabled_tag_set()
	local enabled_tag_set = {}

	for _, tag in ipairs(config.enabled_fallback_font_tags or {}) do
		local normalized_tag = tostring(tag or "")

		if normalized_tag ~= "" then
			enabled_tag_set[normalized_tag] = true
		end
	end

	-- Automatically add tags for platform-specific fallback
	if WINDOWS then
		enabled_tag_set.windows = true
	elseif LINUX then
		enabled_tag_set.linux = true
	elseif MACOS then
		enabled_tag_set.macos = true
	end

	return enabled_tag_set
end

--- Decide whether one fallback entry is active
-- Entries without tag matcher fields stay always-on
-- Explicit tag fields combine as: all true, any true, not true
-- @param entry table fallback font entry
-- @param enabled_tag_set table enabled tag lookup table
-- @return boolean entry active state
local function is_enabled_fallback_entry(entry, enabled_tag_set)
	-- Walk all tag groups through one evaluator so the rules stay in one place
	local tag_groups = {
		{ mode = "not", tags = entry.not_tags },
		{ mode = "all", tags = entry.all_tags },
		{ mode = "any", tags = entry.any_tags },
	}
	local have_any_tags = false
	local have_all_tags = false
	local have_not_tags = false
	local any_tag_matched = false
	local group_index = 1
	local tag_index = 1

	while group_index <= #tag_groups do
		local group = tag_groups[group_index]
		local tags = group.tags

		if type(tags) ~= "table" or tag_index > #tags then
			group_index = group_index + 1
			tag_index = 1
		else
			local normalized_tag = tostring(tags[tag_index] or "")
			tag_index = tag_index + 1

			if normalized_tag ~= "" then
				if group.mode == "not" then
					have_not_tags = true

					if enabled_tag_set[normalized_tag] then
						return false
					end
				elseif group.mode == "all" then
					have_all_tags = true

					if not enabled_tag_set[normalized_tag] then
						return false
					end
				else
					have_any_tags = true

					if enabled_tag_set[normalized_tag] then
						any_tag_matched = true
						group_index = #tag_groups + 1
					end
				end
			end
		end
	end

	if not have_any_tags and not have_all_tags and not have_not_tags then
		return true
	end

	if have_any_tags and not any_tag_matched then
		return false
	end

	return true
end

--- Build the active fallback path list for current config
-- @return table active fallback font paths
local function build_active_fallback_paths(face_name)
	if config.use_fallback_fonts ~= true then
		return {}
	end

	local paths = {}
	local seen = {}
	local enabled_tag_set = build_enabled_tag_set()

	for _, entry in ipairs(fonts.fallback_fonts or {}) do
		if type(entry) == "table" and is_enabled_fallback_entry(entry, enabled_tag_set) then
			local fallback_path = entry.path

			if face_name == "bold_italic" then
				fallback_path = entry.bold_italic_path or entry.italic_path or entry.bold_path or fallback_path
			elseif face_name == "bold" then
				fallback_path = entry.bold_path or fallback_path
			elseif face_name == "italic" then
				fallback_path = entry.italic_path or fallback_path
			elseif face_name == "heavy_italic" then
				fallback_path = entry.heavy_italic_path or entry.italic_path or entry.heavy_path or entry.bold_italic_path
					or entry.bold_path or fallback_path
			elseif face_name == "heavy" then
				fallback_path = entry.heavy_path or entry.bold_path or fallback_path
			elseif face_name == "light_italic" then
				fallback_path = entry.light_italic_path or entry.italic_path or entry.light_path or fallback_path
			elseif face_name == "light" then
				fallback_path = entry.light_path or fallback_path
			end

			append_unique_path(paths, seen, fallback_path)
		end
	end

	return paths
end

--- Build a stable signature for active font selection state
-- @param active_fallback_paths table active fallback font paths
-- @return string signature text
local function build_font_signature(active_fallback_paths)
	local items = {}

	items[#items + 1] = tostring(config.use_fallback_fonts == true)

	for _, path in ipairs(active_fallback_paths) do
		items[#items + 1] = tostring(path)
	end

	return table.concat(items, "\n")
end

--- Build one UI font with the current fallback path list
-- @param primary_path string primary font file path
-- @param size number font size
-- @param active_fallback_paths table active fallback font paths
-- @return userdata built font handle
local function build_font_with_fallback(primary_path, size, active_fallback_paths)
	local primary = fontcache.load(primary_path, size)

	if #active_fallback_paths == 0 then
		return primary
	end

	return renderer.font.group_lazy(primary, active_fallback_paths, size)
end

--- Return the active fallback path list
-- @return table active fallback font paths
function fonts.get_active_fallback_paths()
	return build_active_fallback_paths("regular")
end

--- Return the canonical face name requested by formatting options
-- @param options table|nil light, bold, heavy, or italic flags
-- @return string face_name bundled UI face name
local function resolve_face_name(options)
	options = type(options) == "table" and options or {}

	if options.heavy == true then
		return options.italic == true and "heavy_italic" or "heavy"
	end
	if options.light == true then
		return options.italic == true and "light_italic" or "light"
	end
	if options.bold == true and options.italic == true then
		return "bold_italic"
	end
	if options.bold == true then
		return "bold"
	end
	if options.italic == true then
		return "italic"
	end

	return "regular"
end

--- Load one styled UI font at an arbitrary pixel size
-- Missing styled fallback faces safely use their regular face
-- @param size number requested font size
-- @param options table|nil light, bold, heavy, italic, or embolden values
-- @return userdata font styled font with active fallbacks
function fonts.load_style(size, options)
	options = type(options) == "table" and options or {}
	local face_name = resolve_face_name(options)
	local primary_path = UI_FONT_PATHS[face_name]
	local fallback_paths = build_active_fallback_paths(face_name)

	return fontcache.load_group_with_fallbacks(primary_path, size, fallback_paths, {
		embolden = options.embolden,
	})
end

--- Select one normal-size style font, loading optional faces only when requested
-- @param style table active style table
-- @param options table|nil light, bold, heavy, or italic flags
-- @return userdata font selected style handle
function fonts.select(style, options)
	local face_name = resolve_face_name(options)

	if face_name == "regular" then
		return style.font
	end

	return fonts.load_style(14 * SCALE, options)
end

--- Rebuild and assign the style font handles
-- @param style table target style table
-- @return nil
local function rebuild_style_fonts(style)
	local active_fallback_paths = build_active_fallback_paths("regular")

	style.font = build_font_with_fallback(
		UI_FONT_PATHS.regular,
		14 * SCALE,
		active_fallback_paths
	)
	style.big_font = build_font_with_fallback(
		UI_FONT_PATHS.regular,
		42 * SCALE,
		active_fallback_paths
	)
	style.code_font = build_font_with_fallback(
		FONTS_DIR .. "mono.ttf",
		15 * SCALE,
		active_fallback_paths
	)
	style.icon_font = fontcache.load(
		FONTS_DIR .. "icons.ttf",
		14 * SCALE
	)
--	style.gridlock_font = fontcache.load(
--		FONTS_DIR .. "sans.ttf",
--		12 * SCALE
--	)
	active_font_signature = build_font_signature(active_fallback_paths)
end

--- Apply font handles to a style table
-- @param style table target style table
-- @return nil
function fonts.apply(style)
	rebuild_style_fonts(style)
end

--- Rebuild style font handles immediately
-- @param style table target style table
-- @return nil
function fonts.reload(style)
	rebuild_style_fonts(style)
end

--- Rebuild style fonts only when fallback selection changed
-- @param style table target style table
-- @return nil
function fonts.reload_if_needed(style)
	local signature = build_font_signature(build_active_fallback_paths("regular"))

	if active_font_signature ~= signature then
		rebuild_style_fonts(style)
	end
end

return fonts
