-- lx-bundle:true

--- Shared registration and selection for opening detected files
-- Handlers may open document bytes, create a view, or provide both parts
-- @module core.open_handler

local config = require("core.config")

local open_handler = {}
local handlers = {}
local handlers_by_name = {}
local configured_handlers = {}
local reported_config_errors = {}

--- Copy case-insensitive capability names into a direct lookup map
-- @param values table|string|nil declared names
-- @param field_name string setting name used in errors
-- @return table names
local function build_name_set(values, field_name)
	local names = {}

	if values == nil then
		return names
	end
	if type(values) == "string" then
		values = { values }
	end
	if type(values) ~= "table" then
		error(field_name .. " must be a string or table", 3)
	end

	for key, value in pairs(values) do
		local name = type(key) == "string" and value == true and key or value

		if type(name) ~= "string" or name == "" then
			error(field_name .. " must contain non-empty names", 3)
		end

		names[name:lower()] = true
	end

	return names
end

--- Return a format's stable user-facing key
-- @param format_definition table|nil detected format
-- @return string|nil name
function open_handler.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

--- Rebuild direct case-insensitive handler choices from user configuration
function open_handler.refresh_config()
	local source = config.format_handlers
	local index = {}

	if source == nil then
		source = {}
	elseif type(source) ~= "table" then
		error("config.format_handlers must be a table", 2)
	end

	for configured_format, configured_handler in pairs(source) do
		if type(configured_format) ~= "string" or configured_format == "" then
			error("config.format_handlers keys must be non-empty format names", 2)
		end
		if type(configured_handler) ~= "string" or configured_handler == "" then
			error("config.format_handlers values must be non-empty handler names", 2)
		end

		local format_name = configured_format:lower()
		local handler_name = configured_handler:lower()
		local previous = index[format_name]

		if previous and previous ~= handler_name then
			error(string.format(
				"config.format_handlers contains conflicting case-insensitive key %q",
				configured_format
			), 2)
		end

		index[format_name] = handler_name
	end

	configured_handlers = index
	reported_config_errors = {}
end

--- Register one core or plugin handler
-- Plugin handlers normally use the default priority; core fallbacks use zero or lower
-- @param definition table handler capabilities and callbacks
-- @return table handler normalized registered handler
function open_handler.register(definition)
	assert(type(definition) == "table", "open_handler.register: definition table expected")
	assert(type(definition.name) == "string" and definition.name ~= "",
		"open_handler.register: non-empty name expected")
	assert(type(definition.open) == "function" or type(definition.create_view) == "function",
		"open_handler.register: open or create_view callback expected")

	local name = definition.name:lower()
	assert(handlers_by_name[name] == nil, string.format("open handler %q is already registered", definition.name))

	local handler = {
		name = name,
		priority = tonumber(definition.priority) or 100,
		automatic = definition.automatic ~= false,
		raw = definition.raw == true,
		formats = build_name_set(definition.formats, "open handler formats"),
		kinds = build_name_set(definition.kinds, "open handler kinds"),
		modes = build_name_set(definition.modes, "open handler modes"),
		actions = build_name_set(definition.actions, "open handler actions"),
		probe = definition.probe,
		open = definition.open,
		create_view = definition.create_view,
	}

	if handler.probe ~= nil then
		assert(type(handler.probe) == "function", "open_handler.register: probe must be a function")
	end

	handlers[#handlers + 1] = handler
	handlers_by_name[name] = handler

	table.sort(handlers, function(left, right)
		if left.priority ~= right.priority then
			return left.priority > right.priority
		end

		return left.name < right.name
	end)

	return handler
end

--- Return one registered handler by its case-insensitive name
-- @param name string|nil requested handler
-- @return table|nil handler
function open_handler.get(name)
	return type(name) == "string" and handlers_by_name[name:lower()] or nil
end

--- Return whether one declared capability group accepts the request value
-- An empty group accepts every value
-- @param names table capability lookup
-- @param value string|nil request value
-- @return boolean accepts
local function accepts_name(names, value)
	if next(names) == nil then
		return true
	end
	if names.any == true then
		return true
	end

	return type(value) == "string" and names[value:lower()] == true
end

--- Return whether a handler's cheap declarations accept one request
-- @param handler table registered handler
-- @param request table normalized open request
-- @param callback_name string callback needed for this phase
-- @return boolean accepts
local function accepts_request(handler, request, callback_name)
	if callback_name ~= nil and type(handler[callback_name]) ~= "function" then
		return false
	end
	if not accepts_name(handler.formats, request.format_name) then
		return false
	end
	if not accepts_name(handler.kinds, request.kind) then
		return false
	end
	if not accepts_name(handler.modes, request.mode) then
		return false
	end
	if request.action ~= nil then
		if next(handler.actions) == nil or not accepts_name(handler.actions, request.action) then
			return false
		end
	end

	return true
end

--- Run an optional handler probe without letting one automatic plugin block later handlers
-- @param handler table registered handler
-- @param request table open request
-- @param strict boolean whether failure belongs to an explicit choice
-- @return boolean accepts
-- @return string|nil err
local function probe_handler(handler, request, strict)
	if type(handler.probe) ~= "function" then
		return true
	end

	local called, accepted = pcall(handler.probe, request)

	if not called then
		if strict then
			return false, tostring(accepted)
		end

		local core = package.loaded["core"]
		if core and type(core.warning) == "function" then
			core.warning('Open handler "%s" probe failed: %s', handler.name, tostring(accepted))
		end

		return false
	end

	return accepted ~= false
end

--- Return the configured handler name for one detected format
-- @param format_definition table|nil detected format
-- @return string|nil handler_name
function open_handler.get_configured_name(format_definition)
	local format_name = open_handler.get_format_name(format_definition)

	return format_name and configured_handlers[format_name] or nil
end

--- Build automatic handlers whose declarations support one phase
-- @param request table open request
-- @param callback_name string needed callback
-- @return table candidates
local function collect_automatic_candidates(request, callback_name)
	local candidates = {}

	for index = 1, #handlers do
		local handler = handlers[index]

		if handler.automatic and accepts_request(handler, request, callback_name) then
			candidates[#candidates + 1] = handler
		end
	end

	return candidates
end

--- Prefer an applicable selected action handler, otherwise use automatic action handlers
-- A presentation-only selection is deliberately ignored until the action finishes
-- @param request table open request
-- @param callback_name string|nil needed callback
-- @return table candidates
local function collect_action_candidates(request, callback_name)
	local requested_name = type(request.handler) == "string" and request.handler:lower() or nil
	local configured_name = not requested_name and open_handler.get_configured_name(request.format) or nil
	local preferred = handlers_by_name[requested_name or configured_name]

	if preferred and accepts_request(preferred, request, callback_name) then
		return { preferred }
	end

	return collect_automatic_candidates(request, callback_name)
end

--- Build the preferred or automatic handlers for one presentation phase
-- @param request table open request
-- @param callback_name string needed callback
-- @return table candidates
-- @return boolean strict
-- @return string|nil err
local function collect_candidates(request, callback_name)
	local requested_name = type(request.handler) == "string" and request.handler:lower() or nil
	local configured_name = not requested_name and open_handler.get_configured_name(request.format) or nil
	local preferred_name = requested_name or configured_name

	if preferred_name then
		local preferred = handlers_by_name[preferred_name]

		if preferred and accepts_request(preferred, request, nil) then
			if callback_name == nil or type(preferred[callback_name]) == "function" then
				return { preferred }, requested_name ~= nil
			end

			-- A chosen view-only handler deliberately prevents source transformation
			return {}, requested_name ~= nil
		end

		if requested_name then
			return {}, true, string.format(
				'Open handler "%s" does not support this request',
				requested_name
			)
		end

		if not reported_config_errors[preferred_name] then
			reported_config_errors[preferred_name] = true
			local core = package.loaded["core"]

			if core and type(core.warning) == "function" then
				core.warning(
					'Configured open handler "%s" is unavailable for format "%s"; using automatic selection',
					preferred_name,
					tostring(request.format_name or "unknown")
				)
			end
		end
	end

	return collect_automatic_candidates(request, callback_name), false
end

--- Copy one request without its already handled source action
-- @param request table original open request
-- @return table presentation_request
local function without_action(request)
	local presentation_request = {}

	for name, value in pairs(request) do
		presentation_request[name] = value
	end

	presentation_request.action = nil

	return presentation_request
end

--- Describe an explicit action that no matching handler completed
-- @param request table open request
-- @return string err
local function unsupported_action_error(request)
	return string.format('No open handler accepted action "%s"', tostring(request.action))
end

--- Return the first applicable handler without running its open or view callback
-- Used while classifying raw handlers before a document model is chosen
-- @param request table open request
-- @return table|nil handler
-- @return string|nil err
function open_handler.resolve(request)
	assert(type(request) == "table", "open_handler.resolve: request table expected")

	request.format_name = request.format_name or open_handler.get_format_name(request.format)

	if request.action ~= nil then
		local action_candidates = collect_action_candidates(request, nil)

		for index = 1, #action_candidates do
			local handler = action_candidates[index]
			local accepted = probe_handler(handler, request, false)

			if accepted then
				return handler
			end
		end

		-- A final raw presentation must not replace the source action during classification
		return nil
	end

	local candidates, strict, candidate_error = collect_candidates(request, nil)

	if candidate_error then
		return nil, candidate_error
	end

	for index = 1, #candidates do
		local handler = candidates[index]
		local accepted, probe_error = probe_handler(handler, request, strict)

		if accepted then
			return handler
		end
		if probe_error then
			return nil, probe_error
		end
	end

	return nil
end

--- Run applicable source-opening callbacks
-- @param candidates table ordered handlers
-- @param request table open request
-- @param strict boolean whether probe failures belong to an explicit choice
-- @return table|nil result
-- @return string|nil err
-- @return table|nil handler
local function run_open_candidates(candidates, request, strict)
	for index = 1, #candidates do
		local handler = candidates[index]
		local accepted, probe_error = probe_handler(handler, request, strict)

		if accepted then
			local called, result, open_error = pcall(handler.open, request)

			if not called then
				return nil, tostring(result), handler
			end
			if result ~= nil or open_error ~= nil then
				return result, open_error, handler
			end
		elseif probe_error then
			return nil, probe_error, handler
		end
	end

	return nil
end

--- Let applicable handlers open source bytes before normal document loading
-- A nil result without an error declines the request and gives the next handler a chance
-- @param request table open request with complete source data
-- @return table|nil result
-- @return string|nil err
-- @return table|nil handler
function open_handler.open(request)
	assert(type(request) == "table", "open_handler.open: request table expected")

	request.format_name = request.format_name or open_handler.get_format_name(request.format)

	if request.action ~= nil then
		local action_candidates = collect_action_candidates(request, "open")
		local action_result, action_error, action_handler = run_open_candidates(
			action_candidates,
			request,
			true
		)

		if action_result ~= nil or action_error ~= nil then
			return action_result, action_error, action_handler
		end
		if #action_candidates > 0
			or #collect_action_candidates(request, "create_view") == 0
		then
			return nil, unsupported_action_error(request)
		end
	end

	local presentation_request = request.action ~= nil and without_action(request) or request
	local candidates, strict, candidate_error = collect_candidates(presentation_request, "open")

	if candidate_error then
		return nil, candidate_error
	end

	return run_open_candidates(candidates, presentation_request, strict)
end

--- Run applicable document-view callbacks
-- @param candidates table ordered handlers
-- @param request table view request
-- @param strict boolean whether probe failures belong to an explicit choice
-- @return table|nil view
-- @return string|nil err
-- @return table|nil handler
local function run_view_candidates(candidates, request, strict)
	for index = 1, #candidates do
		local handler = candidates[index]
		local accepted, probe_error = probe_handler(handler, request, strict)

		if accepted then
			local called, view, view_error = pcall(handler.create_view, request)

			if not called then
				return nil, tostring(view), handler
			end
			if view ~= nil or view_error ~= nil then
				return view, view_error, handler
			end
		elseif probe_error then
			return nil, probe_error, handler
		end
	end

	return nil
end

--- Let applicable handlers create a document view
-- A nil result without an error declines the request and gives the next handler a chance
-- @param request table open request with document and view options
-- @return table|nil view
-- @return string|nil err
-- @return table|nil handler
function open_handler.create_view(request)
	assert(type(request) == "table", "open_handler.create_view: request table expected")

	request.format = request.format or (request.doc and request.doc.format)
	request.format_name = request.format_name or open_handler.get_format_name(request.format)
	request.kind = request.kind or (request.doc and request.doc.kind)

	if request.action ~= nil then
		local action_candidates = collect_action_candidates(request, "create_view")
		local action_view, action_error, action_handler = run_view_candidates(
			action_candidates,
			request,
			true
		)

		if action_view ~= nil or action_error ~= nil then
			return action_view, action_error, action_handler
		end
		if #action_candidates > 0
			or #collect_action_candidates(request, "open") == 0
		then
			return nil, unsupported_action_error(request)
		end
	end

	local presentation_request = request.action ~= nil and without_action(request) or request
	local candidates, strict, candidate_error = collect_candidates(presentation_request, "create_view")

	if candidate_error then
		return nil, candidate_error
	end

	return run_view_candidates(candidates, presentation_request, strict)
end

open_handler.refresh_config()

return open_handler
