-- lx-bundle:true

--- Shared row storage and lookup for rendered document previews
-- @module core.preview.layout

local common = require("core.common")

local layout = {}

--- Clear measured preview rows and both document dimensions
-- @param view table preview view whose layout is being rebuilt
function layout.reset(view)
	view.layout_rows = {}
	view.layout_height = 0
	view.layout_width = 0
end

--- Add one measured row to a preview's cached document layout
-- @param view table preview view receiving the row
-- @param row table wrapped styled row
-- @param gap_before number space before this row
-- @param decoration table|nil optional block drawing details
function layout.add_row(view, row, gap_before, decoration)
	decoration = decoration or {}
	local height = row.min_height or 0
	local baseline = 0
	local row_width = tonumber(row.width)

	-- Wrapped adapters can omit aggregate width because runs already own exact measurements
	if not row_width then
		row_width = 0

		for run_index = 1, #row.runs do
			row_width = row_width + (tonumber(row.runs[run_index].width) or 0)
		end
	end

	for run_index = 1, #row.runs do
		local run = row.runs[run_index]
		height = math.max(height, run.math_box and run.math_box.height or run.font:get_height())
		baseline = math.max(
			baseline,
			run.math_box and run.math_box.baseline or run.font:get_baseline()
		)
	end

	local natural_height = height
	local line_height_scale = decoration.fixed_height and 1
		or decoration.line_height_scale
		or view.preview_line_height_scale
		or 1
	local line_height = natural_height * line_height_scale
	local line_leading = math.max(0, line_height - natural_height)
	local content_padding_top = decoration.content_padding_top or 0
	local content_padding_bottom = decoration.content_padding_bottom or 0
	height = line_height + content_padding_top + content_padding_bottom
	baseline = baseline + line_leading / 2 + content_padding_top
	local x_offset = decoration.x_offset or 0
	local content_width = math.max(row_width, tonumber(decoration.content_width) or 0)

	-- First row also resets width for adapters that replace their row list directly
	if #view.layout_rows == 0 then
		view.layout_width = x_offset + content_width
	else
		view.layout_width = math.max(view.layout_width or 0, x_offset + content_width)
	end

	view.layout_rows[#view.layout_rows + 1] = {
		runs = row.runs,
		width = content_width,
		height = height,
		baseline = baseline,
		y = view.layout_height + gap_before,
		x_offset = x_offset,
		quote = decoration.quote,
		code_block = decoration.code_block,
		code_block_x_offset = decoration.code_block_x_offset,
		code_block_width = decoration.code_block_width,
		code_header = decoration.code_header == true,
		code_badge = decoration.code_badge,
		rule = decoration.rule == true,
		heading_separator = decoration.heading_separator,
		content_padding_bottom = content_padding_bottom,
		content_padding_top = content_padding_top,
		table_row = decoration.table_row,
		table_images = decoration.table_images,
		image_block = decoration.image_block,
		math_display = decoration.math_display,
		error_block = decoration.error_block == true,
		source_line = view.layout_source_line,
	}
	view.layout_height = view.layout_height + gap_before + height
end

--- Find the first cached row that can appear inside a preview viewport
-- @param view table preview view with vertically ordered layout rows
-- @param visible_top number top edge in document-layout coordinates
-- @return number row_index first potentially visible row
function layout.find_first_visible_row(view, visible_top)
	local rows = view.layout_rows
	local low = 1
	local high = #rows
	local first_visible = high + 1

	while low <= high do
		local middle = math.floor((low + high) / 2)
		local row = rows[middle]

		if row.y + row.height >= visible_top then
			first_visible = middle
			high = middle - 1
		else
			low = middle + 1
		end
	end

	return first_visible
end

--- Find the first rendered row for one source line
-- Preview layout keeps source lines in ascending order
-- @param rows table vertically ordered layout rows
-- @param source_line number requested source line
-- @return number|nil row_index first exact source row
local function find_first_source_row(rows, source_line)
	local low = 1
	local high = #rows
	local first_match

	while low <= high do
		local middle = math.floor((low + high) / 2)
		local row_source_line = rows[middle].source_line or 0

		if row_source_line >= source_line then
			if row_source_line == source_line then
				first_match = middle
			end

			high = middle - 1
		else
			low = middle + 1
		end
	end

	return first_match
end

--- Return the source line at one vertical position inside a preview viewport
-- @param view table document preview view
-- @param viewport_offset number|nil distance below viewport top
-- @return number source_line
function layout.get_visible_source_line(view, viewport_offset)
	local document_y = (view.scroll.y or 0) + (viewport_offset or 0)
	local row_index = layout.find_first_visible_row(view, document_y)
	local row = view.layout_rows[row_index]

	return row and row.source_line or 1
end

--- Capture the source row currently touching a preview's top edge
-- @param view table document preview view
-- @return table anchor source line and pixel offset inside its rendered row
function layout.capture_anchor(view)
	local scroll_y = view.scroll.y or 0
	local row_index = layout.find_first_visible_row(view, scroll_y)
	local anchor_row = view.layout_rows[row_index]

	if not anchor_row then
		row_index = 1
		anchor_row = view.layout_rows[1]
	end
	if not anchor_row then
		return { source_line = 1, source_row = 1, offset = 0, target_delta = 0 }
	end

	local source_row = 1

	for previous_index = row_index - 1, 1, -1 do
		if view.layout_rows[previous_index].source_line ~= anchor_row.source_line then
			break
		end

		source_row = source_row + 1
	end

	return {
		source_line = anchor_row.source_line or 1,
		source_row = source_row,
		offset = scroll_y - anchor_row.y,
		target_delta = (view.scroll.to.y or scroll_y) - scroll_y,
	}
end

--- Restore a captured source row after its wrapped layout changes
-- @param view table document preview view
-- @param anchor table source-line viewport anchor
function layout.restore_anchor(view, anchor)
	if type(anchor) ~= "table" then
		return
	end

	local target_index = find_first_source_row(view.layout_rows, anchor.source_line)

	if not target_index then
		return
	end

	local requested_source_row = math.max(1, anchor.source_row or 1)
	local source_row = 1

	while source_row < requested_source_row do
		local next_row = view.layout_rows[target_index + 1]

		if not next_row or next_row.source_line ~= anchor.source_line then
			break
		end

		target_index = target_index + 1
		source_row = source_row + 1
	end

	local target_row = view.layout_rows[target_index]
	local max_scroll_y = math.max(0, view.layout_height - view:get_scroll_viewport_height())
	local target_y = common.clamp(target_row.y + (anchor.offset or 0), 0, max_scroll_y)
	view.scroll.y = target_y
	view.scroll.to.y = common.clamp(target_y + (anchor.target_delta or 0), 0, max_scroll_y)
end

return layout
