-- lx-bundle:true

--- Syntax-owned text formatting command forwarding
-- @module core.text.format

local core = require("core")

local format = {}

--- Return the active syntax handler for one generic format
-- @param doc table|nil text document
-- @param format_name string generic format name
-- @return function|nil handler syntax-owned handler
local function get_handler(doc, format_name)
	local commands = doc and doc.format and doc.format.format_commands
	local handler = type(commands) == "table" and commands[format_name] or nil

	return type(handler) == "function" and handler or nil
end

--- Return whether one document syntax supports a generic format
-- @param doc table|nil text document
-- @param format_name string generic format name
-- @return boolean supported
function format.supports(doc, format_name)
	return get_handler(doc, format_name) ~= nil
end

--- Build one detached selection while preserving direction and identity
-- @param line number source line
-- @param col1 number selected text start column
-- @param col2 number selected text end column
-- @param reversed boolean true when the original selection ran backwards
-- @param id number|nil stable selection identifier
-- @return table selection detached selection snapshot
local function make_selection(line, col1, col2, reversed, id)
	if reversed then
		return { line, col2, line, col1, id = id }
	end

	return { line, col1, line, col2, id = id }
end

--- Transform every non-empty single-line selection as one undoable edit
-- The callback may expand each source range and chooses the inner result selection by offsets
-- @param doc table text document
-- @param transform function callback receiving one prepared selection
-- @return boolean changed
-- @return string|nil transform_error friendly rejection reason
function format.transform_inline_selections(doc, transform)
	local selections, primary_index = doc:get_selections()
	local prepared = {}

	-- Validate every selection before asking the syntax to transform text
	for index = 1, #selections do
		local selection = selections[index]
		local line1, col1, line2, col2 = selection[1], selection[2], selection[3], selection[4]
		local reversed = line1 > line2 or line1 == line2 and col1 > col2

		if reversed then
			line1, col1, line2, col2 = line2, col2, line1, col1
		end

		if line1 == line2 and col1 == col2 then
			return false, "Select text to format"
		end
		if line1 ~= line2 then
			return false, "Inline formatting requires single-line selections"
		end

		prepared[index] = {
			line = line1,
			col1 = col1,
			col2 = col2,
			text = doc:get_text(line1, col1, line2, col2),
			line_text = doc.lines[line1],
			reversed = reversed,
			id = selection.id,
		}
	end

	local replacements = {}
	local after_selections = {}
	local column_shifts = {}

	for index = 1, #prepared do
		local selected = prepared[index]
		local replacement, transform_error = transform(selected)

		if type(replacement) ~= "table" then
			return false, transform_error or "Syntax could not format the selection"
		end

		local previous = replacements[index - 1]
		-- Reject expanded marker ranges before any document change begins
		if previous
			and previous.line2 == selected.line
			and replacement.range_col1 < previous.col2
		then
			return false, "Formatted selections overlap after marker expansion"
		end

		local preceding_shift = column_shifts[selected.line] or 0
		local result_col1 = replacement.range_col1
			+ preceding_shift
			+ replacement.selection_offset1
		local result_col2 = replacement.range_col1
			+ preceding_shift
			+ replacement.selection_offset2

		replacements[index] = {
			line1 = selected.line,
			col1 = replacement.range_col1,
			line2 = selected.line,
			col2 = replacement.range_col2,
			text = replacement.text,
		}
		after_selections[index] = make_selection(
			selected.line,
			result_col1,
			result_col2,
			selected.reversed,
			selected.id
		)
		column_shifts[selected.line] = preceding_shift
			+ #replacement.text
			- (replacement.range_col2 - replacement.range_col1)
	end

	return doc:replace_ranges(replacements, after_selections, primary_index), nil
end

--- Forward one generic format request to the active syntax
-- Syntax handlers return a friendly reason when the current selection cannot be changed
-- @param doc table|nil text document
-- @param format_name string generic format name
-- @return boolean changed
function format.perform(doc, format_name)
	local handler = get_handler(doc, format_name)

	if not handler then
		return false
	end

	local changed, format_error = handler(doc, format_name)

	if changed == false and format_error then
		core.notice("Format: %s", format_error)
	end

	return changed == true
end

return format
