-- lx-bundle:true

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

--- Define the one central color catalog used by the new UI system
-- Themes provide values, while this catalog provides dependable fallbacks when values are absent
-- @module qui.colors

local colors = {}
local catalog = {}
local selectors = {}
local families = {}

-- Public primitive colors are convenient for custom drawing and never back the private absence copies
colors.black = { 0, 0, 0, 255 }
colors.white = { 255, 255, 255, 255 }
colors.gray = { 128, 128, 128, 255 }
colors.red = { 255, 0, 0, 255 }
colors.transparent = { 0, 0, 0, 0 }

--- Validate one renderer-neutral color without changing the caller's table
-- @param value table expected red, green, blue, and optional alpha channels
-- @param name string readable value location
-- @return table value same validated table
local function check_color(value, name)
	assert(type(value) == "table", name .. " must be a color table")

	for channel = 1, 4 do
		local component = value[channel]
		if channel == 4 and component == nil then
			component = 255
		end
		assert(
			type(component) == "number" and component >= 0 and component <= 255 and component % 1 == 0,
			name .. " channel " .. channel .. " must be a whole number from 0 to 255"
		)
	end

	return value
end

--- Copy one checked color so later theme-table changes cannot alter the prepared snapshot
-- @param value table source color
-- @param name string readable value location
-- @return table copied color with explicit alpha
local function copy_color(value, name)
	check_color(value, name)

	return { value[1], value[2], value[3], value[4] == nil and 255 or value[4] }
end

--- Parse one compact theme color without depending on the current-era style helpers
-- Pattern breakdown: '#' is literal, each '%x%x' captures one hexadecimal byte, and the optional fourth byte is alpha
-- @param value string expected '#RRGGBB' or '#RRGGBBAA'
-- @param name string readable value location
-- @return table parsed color
local function parse_hex_color(value, name)
	local red, green, blue, alpha = value:match("^#(%x%x)(%x%x)(%x%x)(%x%x)$")
	if red == nil then
		red, green, blue = value:match("^#(%x%x)(%x%x)(%x%x)$")
		alpha = "ff"
	end
	assert(red ~= nil, name .. " must use #RRGGBB or #RRGGBBAA")

	return {
		tonumber(red, 16),
		tonumber(green, 16),
		tonumber(blue, 16),
		tonumber(alpha, 16),
	}
end

--- Build the compact identifier used after a readable Theme declaration has been prepared
-- Normal omits its state name so the most common identifiers stay short
-- @param selector string concrete Control kind or shared family
-- @param what string visual property such as 'bg', 'text', or 'frame'
-- @param state string visual state such as 'normal', 'hover', or 'focused'
-- @return string name central catalog identifier
local function identifier_name(selector, what, state)
	if state == "normal" then
		return selector .. "_" .. what
	end

	return selector .. "_" .. state .. "_" .. what
end

--- Add one stable identifier to the single catalog
-- @param name string global color identifier
-- @param absence table final no-theme color
-- @param fallbacks table ordered identifiers used only when the Theme supplies them
local function define(name, absence, fallbacks)
	assert(catalog[name] == nil, "duplicate qui color " .. name)

	catalog[name] = {
		absence = copy_color(absence, "absence color for " .. name),
		fallbacks = fallbacks,
	}
	colors[name] = name
end

--- Register one fixed Theme token owned by another QUI catalog module
-- This keeps all prepared colors in one fast lookup table while public modules retain readable trees
-- @param name string complete stable token such as 'palette.blue.strong.color'
-- @param absence table final no-theme color
-- @return string token
function colors.register_token(name, absence)
	assert(type(name) == "string" and name ~= "", "qui color token name must be a non-empty string")
	assert(type(absence) == "table", "qui color token " .. name .. " requires an absence color")
	define(name, absence, {})

	return name
end

local form_parts = {
	bg = {
		normal = colors.black,
	},
	frame = {
		normal = colors.gray,
	},
}

-- Panel is a visual surface whose missing colors naturally follow the surrounding Form
local panel_parts = {
	bg = {
		normal = true,
	},
	frame = {
		normal = true,
	},
}

-- Separator reuses the Form's quiet structural frame color
local separator_parts = {
	frame = {
		normal = true,
	},
}

-- Section is the modern form-group treatment: a quiet title joined to structural divider lines
local section_parts = {
	text = {
		normal = true,
	},
	frame = {
		normal = true,
	},
}

-- This first assortment stays limited to distinctions the current Forms, Buttons, and Textboxes actually draw
local input_parts = {
	bg = {
		normal = colors.black,
		hover = colors.black,
		down = colors.white,
		pressing = colors.white,
		disabled = colors.black,
		selection = colors.red,
	},
	checkmark = {
		normal = colors.white,
		disabled = colors.gray,
	},
	symbol = {
		normal = colors.gray,
		hover = colors.white,
		disabled = colors.gray,
	},
	text = {
		normal = colors.gray,
		hover = colors.gray,
		down = colors.black,
		pressing = colors.white,
		disabled = colors.black,
		placeholder = colors.gray,
		selection = colors.white,
	},
	frame = {
		normal = colors.white,
		hover = colors.white,
		focused = colors.white,
		down = colors.white,
		pressing = colors.white,
		disabled = colors.gray,
	},
	inner_frame = {
		down = colors.white,
		pressing = colors.white,
	},
	caret = {
		normal = colors.white,
	},
	thumb = {
		normal = colors.gray,
		hover = colors.white,
		pressing = colors.white,
		disabled = colors.black,
	},
}

-- Concrete selectors list only colors their Control can request
-- 'true' keeps the declaration short by reusing the input family's absence color
local button_parts = {
	bg = {
		normal = true,
		down = true,
		pressing = true,
		disabled = true,
	},
	text = {
		normal = true,
		hover = true,
		down = true,
		pressing = true,
		disabled = true,
	},
	frame = {
		normal = true,
		hover = true,
		focused = true,
		down = true,
		pressing = true,
		disabled = true,
	},
	inner_frame = {
		down = true,
		pressing = true,
	},
}

local textbox_parts = {
	bg = {
		normal = true,
		selection = true,
	},
	text = {
		normal = true,
		disabled = true,
		placeholder = true,
	},
	frame = {
		normal = true,
		hover = true,
		focused = true,
	},
	caret = {
		normal = true,
	},
}

-- Label backgrounds remain transparent unless a Theme or one instance deliberately supplies a surface
local label_parts = {
	bg = {
		normal = colors.transparent,
		disabled = colors.transparent,
	},
	text = {
		normal = true,
		disabled = true,
	},
}

-- Checkbox owns a complete Control background separately from its square indicator
local checkbox_parts = {
	bg = {
		normal = colors.transparent,
		hover = colors.transparent,
		down = colors.transparent,
		pressing = colors.transparent,
		disabled = colors.transparent,
	},
	indicator = {
		normal = colors.black,
		disabled = colors.black,
	},
	checkmark = {
		normal = true,
		disabled = true,
	},
	text = {
		normal = true,
		disabled = true,
	},
	frame = {
		normal = true,
		hover = true,
		focused = true,
	},
}

-- Radio leaves its outlined center transparent and uses background only for the complete Control area
local radio_parts = {
	bg = {
		normal = colors.transparent,
		hover = colors.transparent,
		down = colors.transparent,
		pressing = colors.transparent,
		disabled = colors.transparent,
	},
	checkmark = {
		normal = true,
		disabled = true,
	},
	text = {
		normal = true,
		disabled = true,
	},
	frame = {
		normal = true,
		hover = true,
		focused = true,
	},
}

-- Listbox draws selected rows itself while ordinary input colors keep it consistent with other fields
local listbox_parts = {
	bg = {
		normal = true,
		hover = true,
		selection = true,
	},
	text = {
		normal = true,
		disabled = true,
		selection = true,
	},
	frame = {
		normal = true,
		hover = true,
		focused = true,
	},
}

-- Tree rows use Listbox-style surfaces while keeping separate Theme names for later branch-specific tuning
local tree_parts = {
	bg = {
		normal = true,
		hover = true,
		selection = true,
	},
	text = {
		normal = true,
		disabled = true,
		selection = true,
	},
	frame = {
		normal = true,
		hover = true,
		focused = true,
	},
	symbol = {
		normal = true,
		hover = true,
		disabled = true,
	},
}

-- Scrollbar uses the input surface plus one movable thumb shared by both directions
local scrollbar_parts = {
	bg = {
		normal = true,
		disabled = true,
	},
	thumb = {
		normal = true,
		hover = true,
		pressing = true,
		disabled = true,
	},
}

-- Combobox uses the same stepped surface states as Button while remaining an input-family descendant
local combobox_parts = {
	bg = {
		normal = true,
		down = true,
		pressing = true,
		disabled = true,
	},
	text = {
		normal = true,
		hover = true,
		down = true,
		pressing = true,
		disabled = true,
	},
	frame = {
		normal = true,
		hover = true,
		focused = true,
		down = true,
		pressing = true,
		disabled = true,
	},
	inner_frame = {
		down = true,
		pressing = true,
	},
}

-- Menus are Window popup surfaces rather than input Controls, so they own one small color set
local menu_parts = {
	bg = {
		normal = colors.black,
		hover = colors.gray,
	},
	text = {
		normal = colors.white,
		critical = colors.red,
		notable = colors.white,
		disabled = colors.gray,
	},
	detail = {
		normal = colors.gray,
	},
	frame = {
		normal = colors.gray,
	},
	separator = {
		normal = colors.gray,
	},
	symbol = {
		normal = colors.white,
	},
}

--- Register one Theme selector and let its shared family provide automatic color fallback
-- Every input Control needs only one family declaration here; individual colors do not repeat that relationship
-- @param selector string concrete Control selector or shared family name
-- @param family string|nil shared selector used before normal-state fallback
-- @param parts table supported visual properties and states, with absence colors or inherited 'true' markers
-- @param independent_parts table|nil visual properties that must not inherit the family color with the same name
local function define_selector(selector, family, parts, independent_parts)
	assert(selectors[selector] == nil, "duplicate qui Theme selector " .. selector)
	assert(family == nil or selectors[family] ~= nil, "unknown qui Theme family " .. tostring(family))
	selectors[selector] = {}
	families[selector] = family

	for what, states in pairs(parts) do
		selectors[selector][what] = {}
		for state, absence in pairs(states) do
			local name = identifier_name(selector, what, state)
			local fallbacks = {}
			local inherits_part = family ~= nil and not (independent_parts and independent_parts[what])
			if inherits_part then
				local family_name = identifier_name(family, what, state)
				local family_entry = catalog[family_name]
				if absence == true then
					assert(family_entry ~= nil,
						"missing " .. family_name .. " required by qui Theme selector " .. selector)
					absence = family_entry.absence
				end
			end
			assert(type(absence) == "table", "qui Theme selector " .. selector .. " requires an absence color for " .. what .. "." .. state)

			-- Check the same state through every family before falling back to normal-state colors
			local ancestor = inherits_part and family or nil
			while ancestor ~= nil do
				local family_name = identifier_name(ancestor, what, state)
				if catalog[family_name] ~= nil then
					fallbacks[#fallbacks + 1] = family_name
				end
				ancestor = families[ancestor]
			end

			-- State-only parts such as Button inner frames do not invent a normal value that cannot be drawn
			if state ~= "normal" and states.normal ~= nil then
				fallbacks[#fallbacks + 1] = identifier_name(selector, what, "normal")
			end
			if state ~= "normal" then
				ancestor = inherits_part and family or nil
				while ancestor ~= nil do
					local family_name = identifier_name(ancestor, what, "normal")
					if catalog[family_name] ~= nil then
						fallbacks[#fallbacks + 1] = family_name
					end
					ancestor = families[ancestor]
				end
			end

			define(name, absence, fallbacks)
			selectors[selector][what][state] = name
		end
	end
end

define_selector("form", nil, form_parts)
define_selector("panel", "form", panel_parts)
define_selector("separator", "form", separator_parts)
define_selector("input", nil, input_parts)
define_selector("section", "input", section_parts)
define_selector("button", "input", button_parts)
define_selector("textbox", "input", textbox_parts)
define_selector("label", "input", label_parts, { bg = true })
define_selector("checkbox", "input", checkbox_parts, { bg = true })
define_selector("radio", "input", radio_parts, { bg = true })
define_selector("listbox", "input", listbox_parts)
define_selector("tree", "input", tree_parts)
define_selector("scrollbar", "input", scrollbar_parts)
define_selector("combobox", "button", combobox_parts)
define_selector("menu", nil, menu_parts)

--- Add one deliberate fallback between otherwise separate Theme selectors
-- Menu surfaces visually belong to input surfaces without making Menu an input Control
-- @param name string color receiving the fallback
-- @param fallback string color tried when the Theme omits the first color
local function add_fallback(name, fallback)
	assert(catalog[name] ~= nil, "unknown qui color " .. tostring(name))
	assert(catalog[fallback] ~= nil, "unknown fallback " .. tostring(fallback))
	table.insert(catalog[name].fallbacks, 1, fallback)
end

add_fallback(colors.menu_bg, colors.input_bg)
add_fallback(colors.menu_symbol, colors.menu_text)
add_fallback(colors.checkbox_indicator, colors.input_bg)
add_fallback(colors.checkbox_disabled_indicator, colors.input_disabled_bg)

-- Catalog mistakes must fail at startup instead of entering every later theme resolution
for name, entry in pairs(catalog) do
	for _, fallback in ipairs(entry.fallbacks) do
		assert(catalog[fallback] ~= nil, "unknown fallback " .. fallback .. " for qui color " .. name)
	end
end

--- Return the central identifier represented by one author-facing Theme path
-- @param selector string top-level Theme selector such as 'input' or 'button'
-- @param what string visual property inside the selector
-- @param state string state inside the visual property
-- @return string|nil name identifier when the complete path exists
function colors.identifier(selector, what, state)
	local selector_entry = selectors[selector]
	local part_entry = selector_entry and selector_entry[what]

	return part_entry and part_entry[state] or nil
end

--- Report whether a top-level Theme selector is known even when its table is empty
-- @param selector string requested selector name
-- @return boolean known
function colors.has_selector(selector)
	return selectors[selector] ~= nil
end

--- Prepare one complete, immutable-by-convention color snapshot from partial Theme values
-- Theme fallbacks are checked first, followed by the shared defaults and the entry's emergency color
-- @param values table partial colors indexed by identifiers from this module
-- @param defaults table|nil complete shared defaults
-- @return table resolved colors indexed by the same identifiers
function colors.prepare(values, defaults)
	assert(type(values) == "table", "qui theme colors must be a table")
	assert(defaults == nil or type(defaults) == "table", "qui default colors must be a table")

	for name, value in pairs(values) do
		assert(type(name) == "string" and catalog[name] ~= nil, "unknown qui theme color " .. tostring(name))
		assert(type(value) == "table" or type(value) == "string", "theme color " .. name .. " must be a color or identifier")
		if type(value) == "table" then
			check_color(value, "theme color " .. name)
		elseif value:sub(1, 1) == "#" then
			parse_hex_color(value, "theme color " .. name)
		else
			assert(catalog[value] ~= nil, "unknown qui theme color reference " .. value)
		end
	end
	if defaults ~= nil then
		for name in pairs(catalog) do
			assert(type(defaults[name]) == "table", "qui default color is missing " .. name)
			check_color(defaults[name], "qui default color " .. name)
		end
	end

	local resolved = {}
	local present = {}
	local resolving = {}
	local resolve_name

	--- Resolve an explicitly supplied value without applying that entry's automatic fallbacks
	-- @param name string catalog identifier
	-- @return table|nil color
	-- @return boolean supplied whether the Theme ultimately supplied this value
	local function resolve_configured(name)
		local configured = values[name]
		if configured == nil then
			return nil, false
		elseif type(configured) == "table" then
			return copy_color(configured, "theme color " .. name), true
		elseif configured:sub(1, 1) == "#" then
			return parse_hex_color(configured, "theme color " .. name), true
		end

		local referenced, referenced_supplied = resolve_name(configured)
		if referenced_supplied or defaults ~= nil then
			return referenced, true	-- An explicit reference may deliberately point at a shared default
		end

		return nil, false	-- An absent reference must not hide this entry's own absence color
	end

	--- Resolve one catalog entry while detecting explicit references that return to themselves
	-- @param name string catalog identifier
	-- @return table color
	resolve_name = function(name)
		if resolved[name] ~= nil then
			return resolved[name], present[name]
		end
		assert(not resolving[name], "recursive qui theme color " .. name)
		resolving[name] = true

		local color, supplied = resolve_configured(name)
		local entry = catalog[name]
		if color == nil then
			for _, fallback in ipairs(entry.fallbacks) do
				color, supplied = resolve_configured(fallback)
				if supplied then
					break
				end
			end
		end
		if color == nil then
			color = defaults and defaults[name] or entry.absence
		end

		resolving[name] = nil
		resolved[name] = copy_color(color, "resolved color " .. name)
		present[name] = supplied

		return resolved[name], supplied
	end

	for name in pairs(catalog) do
		resolve_name(name)
	end

	return resolved
end

--- Resolve a catalog identifier or accept one direct dynamic color
-- @param value string|table identifier or direct color returned by a Control
-- @param prepared table complete snapshot returned by 'prepare'
-- @return table resolved color
function colors.resolve(value, prepared)
	assert(type(prepared) == "table", "prepared qui theme colors are unavailable")
	if type(value) == "table" then
		return check_color(value, "dynamic Control color")
	end
	assert(type(value) == "string" and catalog[value] ~= nil, "unknown qui color " .. tostring(value))

	local resolved = prepared[value]
	assert(resolved ~= nil, "prepared qui color is missing " .. value)

	return resolved
end

return colors
