-- lx-bundle:true

--- Fast, lazy color lookup shared by Lx and its plugins
-- @module core.colors

local colors = {}

colors.COLOR_RGBI = 10000
colors.COLOR_RGBF = 10001
colors.COLOR_GRAYF = 10002
colors.COLOR_CMYKF = 10003

--- Lowercase only ASCII letters used by case-insensitive color tables
-- @param value string color name
-- @return string normalized lowercase name
local function ascii_lower(value)
	-- Match only ASCII uppercase letters so locale and Unicode rules cannot change lookup
	return (value:gsub("[A-Z]", function(character)
		return string.char(character:byte() + 32)
	end))
end

--- Convert one normalized channel to the renderer's byte range
-- @param value number channel from zero through one
-- @return number byte rounded from zero through 255
local function to_byte(value)
	return math.floor(math.max(0, math.min(1, value)) * 255 + 0.5)
end

--- Convert one raw color table entry only after its name is requested
-- @param entry table model followed by its source channels
-- @return table rgba renderer color
local function to_rgba(entry)
	local model = entry[1]

	if model == colors.COLOR_RGBI then
		return { entry[2], entry[3], entry[4], entry[5] or 255 }
	elseif model == colors.COLOR_RGBF then
		return { to_byte(entry[2]), to_byte(entry[3]), to_byte(entry[4]), 255 }
	elseif model == colors.COLOR_GRAYF then
		local gray = to_byte(entry[2])

		return { gray, gray, gray, 255 }
	elseif model == colors.COLOR_CMYKF then
		-- CMYK adds black to each ink channel before taking its screen complement
		return {
			to_byte(1 - math.min(1, entry[2] + entry[5])),
			to_byte(1 - math.min(1, entry[3] + entry[5])),
			to_byte(1 - math.min(1, entry[4] + entry[5])),
			255,
		}
	end

	error("unknown color model " .. tostring(model))
end

--- Parse one hexadecimal color without loading a named color table
-- @param value string three, four, six, or eight digit hexadecimal color
-- @return table|nil rgba renderer color
function colors.parse(value)
	if type(value) ~= "string" or value:sub(1, 1) ~= "#" then
		return nil
	end

	local digits = value:sub(2)
	-- Reject every character outside the ASCII hexadecimal set
	if digits:find("[^0-9A-Fa-f]") then
		return nil
	end

	if #digits == 3 or #digits == 4 then
		local expanded = {}

		for index = 1, #digits do
			local digit = digits:sub(index, index)
			expanded[#expanded + 1] = digit .. digit
		end
		digits = table.concat(expanded)
	elseif #digits ~= 6 and #digits ~= 8 then
		return nil
	end

	return {
		tonumber(digits:sub(1, 2), 16),
		tonumber(digits:sub(3, 4), 16),
		tonumber(digits:sub(5, 6), 16),
		#digits == 8 and tonumber(digits:sub(7, 8), 16) or 255,
	}
end

--- Combine color tables or module names with later color tables taking precedence
-- Color table modules and individual names resolve only when lookup first reaches them
-- @param ... table|string color tables or color table module names in lookup order
-- @return function resolve cached name lookup returning renderer RGBA or nil
function colors.combine(...)
	local color_tables = { ... }
	local cache = {}

	return function(name)
		if type(name) ~= "string" then
			return nil
		end

		local cached = cache[name]
		if cached ~= nil then
			return cached or nil
		end

		local literal = colors.parse(name)
		if literal then
			cache[name] = literal

			return literal
		end

		local lowercase_name

		for color_table_index = #color_tables, 1, -1 do
			local color_table = color_tables[color_table_index]

			if type(color_table) == "string" then
				color_table = require(color_table)
				color_tables[color_table_index] = color_table
			end

			local lookup_name = name
			if color_table.caseless then
				lowercase_name = lowercase_name or ascii_lower(name)
				lookup_name = lowercase_name
			end

			local entry = color_table.names[lookup_name]
			if entry then
				local resolved = to_rgba(entry)
				cache[name] = resolved

				return resolved
			end
		end

		cache[name] = false

		return nil
	end
end

return colors
