-- lx-bundle:true

--- Shared names and defaults for what the user wants to do with an opened document
-- Handlers remain separate so bytes, hex, images, and extraction are never modes
-- @module core.open_mode

local config = require("core.config")

local open_mode = {
	edit = "edit",		-- Editable surface
	view = "view",		-- Read-only surface
	preview = "preview",	-- Format-specific visual surface
}

local valid_modes = {
	[open_mode.edit] = true,
	[open_mode.view] = true,
	[open_mode.preview] = true,
}

local default_modes = {}
local format_modes = {}

--- Build one case-insensitive map while rejecting ambiguous configured keys
-- @param source table|nil configured values
-- @param setting_name string setting path used in errors
-- @return table index
local function build_mode_index(source, setting_name)
	local index = {}

	if source == nil then
		return index
	end
	if type(source) ~= "table" then
		error(setting_name .. " must be a table", 3)
	end

	for configured_key, configured_mode in pairs(source) do
		if type(configured_key) ~= "string" or configured_key == "" then
			error(setting_name .. " keys must be non-empty names", 3)
		end
		if type(configured_mode) ~= "string" or valid_modes[configured_mode] ~= true then
			error(string.format(
				'%s[%q] must be "edit", "view", or "preview"',
				setting_name,
				configured_key
			), 3)
		end

		local key = configured_key:lower()
		local previous = index[key]

		if previous and previous ~= configured_mode then
			error(string.format(
				"%s contains conflicting case-insensitive key %q",
				setting_name,
				configured_key
			), 3)
		end

		index[key] = configured_mode
	end

	return index
end

--- Rebuild direct case-insensitive mode lookups after settings change
function open_mode.refresh_config()
	default_modes = build_mode_index(config.default_modes, "config.default_modes")
	format_modes = build_mode_index(config.format_modes, "config.format_modes")
end

--- Return the user-facing key for one resolved format
-- @param format_definition table|nil detected format
-- @return string|nil format_name
local function get_format_name(format_definition)
	local module_name = type(format_definition) == "table" and format_definition._module_name or nil

	if type(module_name) ~= "string" or module_name == "" then
		return nil
	end
	if module_name:sub(1, 7) == "format." then
		return module_name:sub(8):lower()
	end

	return module_name:lower()
end

--- Return one format's declared mode without making formats depend on view code
-- @param format_definition table|nil detected format
-- @return string|nil mode
local function get_declared_format_mode(format_definition)
	if type(format_definition) ~= "table" then
		return nil
	end

	local format_registry = package.loaded["core.format"] or require("core.format")
	if type(format_registry.get_default_mode) ~= "function" then
		return nil
	end

	return format_registry.get_default_mode(format_definition)
end

--- Return the general handling kind for one document
-- @param doc table|nil resolved document
-- @return string kind
local function get_document_kind(doc)
	if doc and type(doc.kind) == "string" and doc.kind ~= "" and doc.kind ~= "text" then
		return doc.kind
	end
	if doc and (doc.is_binary_placeholder == true or doc.is_binary == true) then
		return "binary"
	end
	if doc and doc.is_image == true then
		return "image"
	end

	return doc and doc.kind or "text"
end

--- Check one optional open mode at the shared boundary
-- @param value string|nil requested mode
-- @param level integer|nil caller-facing error level
-- @return string|nil value
function open_mode.check(value, level)
	if value ~= nil and valid_modes[value] ~= true then
		error("unknown document open mode " .. tostring(value), level or 2)
	end

	return value
end

--- Resolve a mode from an explicit request, format, kind, and global defaults
-- @param format_definition table|nil detected format
-- @param kind string|nil general document kind
-- @param requested_mode string|nil explicit caller choice
-- @param action_default_mode string|nil source action fallback
-- @return string mode
function open_mode.resolve_format(format_definition, kind, requested_mode, action_default_mode)
	requested_mode = open_mode.check(requested_mode, 2)
	if requested_mode ~= nil then
		return requested_mode
	end

	local format_name = get_format_name(format_definition)
	local configured_format_mode = format_name and format_modes[format_name] or nil

	if configured_format_mode then
		return configured_format_mode
	end

	if action_default_mode ~= nil then
		return open_mode.check(action_default_mode, 2)
	end

	local declared_format_mode = get_declared_format_mode(format_definition)
	if declared_format_mode ~= nil then
		return open_mode.check(declared_format_mode, 2)
	end

	local kind_mode = type(kind) == "string" and default_modes[kind:lower()] or nil

	return kind_mode or default_modes.default or open_mode.edit
end

--- Resolve one document through the same shared mode defaults
-- @param doc table|nil resolved document
-- @param requested_mode string|nil explicit caller choice
-- @return string mode
function open_mode.resolve(doc, requested_mode)
	local format_definition = doc and doc.format or nil
	local kind = get_document_kind(doc)

	return open_mode.resolve_format(
		format_definition,
		kind,
		requested_mode,
		doc and doc.action_default_mode or nil
	)
end

open_mode.refresh_config()

return open_mode
