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

--- Register, activate, and resolve themes for the new UI system
-- Theme modules provide colors and a small set of DIP metrics; prepared snapshots stay private here
-- @module qui.theme

local colors = require("qui.colors")
local metrics = require("qui.metrics")
local palette = require("qui.palette")
local window_api = assert(window, "the Window API is not loaded")

local theme = {}
local registered_by_name = {}	-- Metadata and prepared values stay together without exposing mutable definitions
local default_colors = colors.prepare {}	-- Emergency colors remain usable until the one normal default set is loaded
local default_metrics
local defaults_set = false
local active_colors = default_colors
local active_metrics
local active_name
local apply_listeners = {}	-- QUI registers framework-owned updates without exposing prepared Theme tables to Forms

local metadata_fields = {
	name = true,
	author = true,
	dark = true,
}

--- Find every native Window currently displaying a Form
-- Unrooted utility Windows and the current-era workspace are deliberately left alone
-- @return table|nil window_ids
-- @return string|nil error_text
local function form_window_ids()
	local listed, windows, list_error = pcall(window_api.list)
	if not listed then
		return nil, tostring(windows)
	end
	if windows == nil then
		return nil, tostring(list_error or "Window list is unavailable")
	end

	local window_ids = {}
	for _, info in ipairs(windows) do
		local checked, form_handle, form_error = pcall(window_api.get_form, info.id)
		if not checked then
			return nil, tostring(form_handle)
		end
		if form_error ~= nil then
			return nil, tostring(form_error)
		end
		if form_handle ~= nil then
			window_ids[#window_ids + 1] = info.id
		end
	end

	return window_ids
end

--- Activate one registered Theme and redraw every Window that displays a Form
-- Redraw requests are accepted before publication so a failed request leaves the old theme active everywhere
-- @param name string registered Theme name
-- @return boolean|nil applied
-- @return string|nil error_text
function theme.apply(name)
	assert(type(name) == "string" and name ~= "", "theme.apply requires a non-empty name")
	local registered = registered_by_name[name]
	if registered == nil then
		return nil, "Theme is not registered: " .. name
	end
	if name == active_name then
		return true
	end

	local window_ids, list_error = form_window_ids()
	if window_ids == nil then
		return nil, "could not list themed Windows: " .. tostring(list_error)
	end
	for _, window_id in ipairs(window_ids) do
		local requested, invalidated, invalidate_error = pcall(window_api.invalidate, window_id)
		if not requested or not invalidated then
			local error_text = requested and invalidate_error or invalidated
			return nil, "could not redraw themed Window " .. window_id .. ": " .. tostring(error_text)
		end
	end

	local previous_name = active_name
	local previous_colors = active_colors
	local previous_metrics = active_metrics
	active_name = name
	active_colors = registered.colors
	active_metrics = registered.metrics
	for _, listener in ipairs(apply_listeners) do
		local called, result, listener_error = pcall(listener)
		if not called or not result then
			active_name = previous_name
			active_colors = previous_colors
			active_metrics = previous_metrics
			for _, rollback_listener in ipairs(apply_listeners) do
				pcall(rollback_listener)
			end

			return nil, "could not apply Theme to live Forms: " ..
				tostring(called and listener_error or result)
		end
	end

	return true
end

--- Register one private framework callback that refreshes live Theme-owned values
-- @param listener function callback returning true or nil plus an error
function theme.on_apply(listener)
	assert(type(listener) == "function", "theme.on_apply requires a function")
	apply_listeners[#apply_listeners + 1] = listener
end

--- Flatten one readable selector tree into separate color and metric identifiers
-- @param definition table complete Theme declaration
-- @return table color_values
-- @return table metric_values
local function flatten_values(definition, allow_metadata)
	local color_values = {}
	local metric_values = {}
	for selector, parts in next, definition do
		if metadata_fields[selector] then
			assert(allow_metadata, "Theme defaults cannot contain " .. selector)
		elseif selector == "palette" then
			local palette_values = palette.flatten(parts)
			for identifier, value in pairs(palette_values) do
				color_values[identifier] = value
			end
		else
			assert(type(selector) == "string" and
				(colors.has_selector(selector) or metrics.has_selector(selector)),
				"unknown qui Theme selector " .. tostring(selector))
			assert(type(parts) == "table", "qui Theme selector " .. selector .. " must be a table")

			for what, value in next, parts do
				assert(type(what) == "string", "qui Theme property names must be strings")
				if type(value) == "number" then
					local identifier = metrics.identifier(selector, what)
					assert(identifier ~= nil, "unknown qui Theme metric " .. selector .. "." .. what)
					metric_values[identifier] = value
				else
					assert(type(value) == "table",
						"qui Theme " .. selector .. "." .. what .. " must be a color-state table or DIP metric")
					for state, color in next, value do
						local identifier = type(state) == "string" and colors.identifier(selector, what, state) or nil
						assert(identifier ~= nil,
							"unknown qui Theme color " .. selector .. "." .. tostring(what) .. "." .. tostring(state))
						color_values[identifier] = color
					end
				end
			end
		end
	end

	return color_values, metric_values
end

--- Set the one normal baseline shared by every Theme
-- Colors omitted here retain their clear emergency colors, while every metric must receive a usable value
-- @param definition table standard colors and metrics without Theme metadata
function theme.set_defaults(definition)
	assert(type(definition) == "table", "theme.set_defaults expects a table")
	assert(not defaults_set, "qui Theme defaults are already set")
	assert(next(registered_by_name) == nil, "qui Theme defaults must be set before registering Themes")
	local color_values, metric_values = flatten_values(definition, false)
	local prepared_colors = colors.prepare(color_values)
	local prepared_metrics = metrics.prepare_defaults(metric_values)

	-- Publish only after the whole declaration succeeds so a mistake can be corrected during startup
	default_colors = prepared_colors
	default_metrics = prepared_metrics
	active_colors = prepared_colors
	active_metrics = prepared_metrics
	defaults_set = true
end

--- Register one named Theme after fully validating and preparing its readable declaration
-- @param definition table metadata plus top-level Control and family selectors
function theme.register(definition)
	assert(type(definition) == "table", "theme.register expects a table")
	assert(defaults_set, "theme.set_defaults must run before theme.register")
	local name = rawget(definition, "name")
	assert(type(name) == "string" and name ~= "", "theme.register requires a non-empty name")
	assert(registered_by_name[name] == nil, "theme already exists " .. name)
	local author = rawget(definition, "author")
	local dark = rawget(definition, "dark")
	assert(author == nil or type(author) == "string" and author ~= "", "Theme author must be a non-empty string")
	assert(dark == nil or type(dark) == "boolean", "Theme dark must be a boolean")
	local color_values, metric_values = flatten_values(definition, true)

	-- Preparation happens before registration so a bad module cannot leave a partial Theme behind
	registered_by_name[name] = {
		name = name,
		author = author,
		dark = dark,
		colors = colors.prepare(color_values, default_colors),
		metrics = metrics.prepare(metric_values, default_metrics),
	}
end

--- Resolve one central identifier or direct color through the currently active prepared Theme
-- @param value string|table central identifier or direct dynamic color
-- @return table resolved color
function theme.resolve(value)
	return colors.resolve(value, active_colors)
end

--- Resolve one central metric identifier through the active Theme
-- @param identifier string metric identifier from 'qui.metrics'
-- @return integer dips
function theme.resolve_metric(identifier)
	assert(active_metrics ~= nil, "qui Theme defaults are not set")

	return metrics.resolve(identifier, active_metrics)
end

return theme
