-- lx-bundle:true

--- Shared native painter for measured document-preview rows
-- Views supply their design and specialized content renderers
-- @module core.preview.render

local core = require("core")
local error_block = require("core.preview.error_block")

local render = {}

--- Scale one logical preview measurement to screen pixels
-- @param value number logical measurement
-- @return number pixels scaled measurement
local function scale_measurement(value)
	return value * (SCALE or 1)
end

--- Draw one horizontal rule through the active preview design
-- @param design table preview design
-- @param x number line left edge
-- @param y number line top edge
-- @param width number line width
local function draw_horizontal_rule(design, x, y, width)
	renderer.draw_rect(
		x,
		y,
		width,
		design.get_horizontal_rule_width(),
		design.get_horizontal_rule_color()
	)
end

--- Draw one heading separator through the active preview design
-- @param design table preview design
-- @param heading_level number heading level
-- @param x number line left edge
-- @param y number line top edge
-- @param width number line width
local function draw_heading_separator(design, heading_level, x, y, width)
	renderer.draw_rect(
		x,
		y,
		width,
		design.get_heading_separator_width(heading_level),
		design.get_heading_separator_color(heading_level)
	)
end

--- Draw the visible part of one code block as a single background
-- @param view table document preview view
-- @param first_row_index number first block row reached by this draw
-- @param x number block left edge
-- @param width number block width
-- @param top number document origin after scrolling
-- @param visible_bottom number viewport bottom in document-layout coordinates
function render.draw_code_block_background(view, first_row_index, x, width, top, visible_bottom)
	local first_row = view.layout_rows[first_row_index]
	local block = first_row.code_block
	local block_top = top + first_row.y
	local block_bottom = block_top + first_row.height

	for row_index = first_row_index + 1, #view.layout_rows do
		local row = view.layout_rows[row_index]

		if row.code_block ~= block then
			break
		end

		block_bottom = top + row.y + row.height

		if row.y > visible_bottom then
			break
		end
	end

	local rounded_top = math.floor(block_top)
	local rounded_bottom = math.ceil(block_bottom)

	renderer.draw_rect(
		x + (first_row.code_block_x_offset or 0),
		rounded_top,
		first_row.code_block_width or width,
		math.max(1, rounded_bottom - rounded_top),
		view.preview_design.get_code_block_background_color()
	)
end

--- Draw the visible measured rows without format-specific interface lanes
-- @param view table document preview view
function render.draw_document(view)
	local design = view.preview_design
	local table_renderer = view.preview_table_renderer
	local image_renderer = view.preview_image_renderer
	local math_renderer = view.preview_math_renderer
	view:draw_background(design.get_document_background_color())
	view:rebuild_layout()
	view:update_horizontal_scrollbar()

	local x = view.position.x
		+ scale_measurement(design.document.padding_horizontal)
		- (view.preview_horizontal_scroll == true and (view.scroll.x or 0) or 0)
	local width = view:get_render_width()
	local top = view.position.y
		+ scale_measurement(design.document.padding_vertical)
		- (view.scroll.y or 0)
	local visible_top = view.position.y - top
	local viewport_height = view:get_scroll_viewport_height()
	local visible_bottom = view.position.y + viewport_height - top
	local first_visible_row = view:get_first_visible_layout_row(visible_top)
	local code_span_padding_vertical = scale_measurement(design.code_spans.padding_vertical)

	core.push_clip_rect(
		view.position.x,
		view.position.y,
		view:get_document_viewport_width(),
		viewport_height
	)
	for row_index = first_visible_row, #view.layout_rows do
		local row = view.layout_rows[row_index]
		local y = top + row.y

		if row.y > visible_bottom then
			break
		end

		do
			local row_x = x + row.x_offset
			local run_x = row_x

			if row.code_block then
				local previous_row = view.layout_rows[row_index - 1]
				local starts_visible_block = row_index == first_visible_row
					or not previous_row
					or previous_row.code_block ~= row.code_block

				if starts_visible_block then
					render.draw_code_block_background(
						view,
						row_index,
						x,
						width,
						top,
						visible_bottom
					)
				end
			end

			if row.code_header then
				-- Header overlays shared block background without creating a gap
				renderer.draw_rect(
					x + (row.code_block_x_offset or 0),
					math.floor(y),
					row.code_block_width or width,
					math.max(1, math.ceil(row.height)),
					design.get_code_block_header_background_color()
				)
			end

			if row.code_badge then
				local badge_design = design.code_blocks.badge
				local badge_font = view.preview_fonts.code_badge
				local badge_padding_x = scale_measurement(badge_design.padding_horizontal)
				local badge_padding_y = scale_measurement(badge_design.padding_vertical)
				local badge_width = badge_font:get_width(row.code_badge) + badge_padding_x * 2
				local badge_height = badge_font:get_height() + badge_padding_y * 2
				local badge_x = x
					+ (row.code_block_x_offset or 0)
					+ (row.code_block_width or width)
					- scale_measurement(badge_design.margin_right)
					- badge_width
				local badge_y = y + scale_measurement(badge_design.margin_top)

				-- Badge floats inside existing top padding and adds no document height
				if badge_design.background_enabled then
					renderer.draw_rect(
						math.floor(badge_x),
						math.floor(badge_y),
						math.ceil(badge_width),
						math.ceil(badge_height),
						design.get_code_block_badge_background_color()
					)
				end
				renderer.draw_text(
					badge_font,
					row.code_badge,
					badge_x + badge_padding_x,
					badge_y + badge_padding_y,
					design.get_code_block_badge_text_color()
				)
			end

			if row.table_row then
				table_renderer.draw_row(row_x, y, row.height, row.table_row)
			end

			if row.table_images and #row.table_images > 0 then
				local previous_row = view.layout_rows[row_index - 1]
				local starts_visible_image_row = row_index == first_visible_row
					or not previous_row
					or previous_row.source_line ~= row.source_line

				if starts_visible_image_row then
					local first_image_row_index = row_index

					while first_image_row_index > 1 do
						local candidate = view.layout_rows[first_image_row_index - 1]
						if candidate.source_line ~= row.source_line
							or not candidate.table_images
							or #candidate.table_images == 0
						then
							break
						end

						first_image_row_index = first_image_row_index - 1
					end

					local first_image_row = view.layout_rows[first_image_row_index]
					local image_y = top + first_image_row.y + first_image_row.content_padding_top

					for image_index = 1, #row.table_images do
						local item = row.table_images[image_index]
						image_renderer.draw_block(
							item.block,
							row_x + item.x_offset,
							image_y,
							view.position.y,
							view.position.y + view.size.y,
							view.preview_fonts.body
						)
					end
				end
			end

			if row.image_block then
				image_renderer.draw_block(
					row.image_block,
					row_x,
					y,
					view.position.y,
					view.position.y + view.size.y,
					view.preview_fonts.body
				)
			end

			error_block.draw_background(row, x, y, width, design)

			if row.math_display then
				local math_box = row.math_display.box
				local math_x = x + math.max(
					row.math_display.padding_horizontal,
					(width - math_box.width) / 2
				)
				local math_y = y + (row.height - math_box.height) / 2

				-- Display math owns one continuous panel independent of its source-line count
				renderer.draw_rect(
					x,
					math.floor(y),
					width,
					math.ceil(row.height),
					design.get_math_background_color()
				)
				math_renderer.draw(
					math_box,
					math_x,
					math_y,
					design.get_text_color(),
					design.get_link_color()
				)
			end

			if row.quote then
				local quote_x = x + row.quote.background_offset
				-- Round shared quote edges outwards so fractional line heights cannot expose seams
				local quote_top = math.floor(y)
				local quote_bottom = math.ceil(y + row.height)
				local quote_height = math.max(1, quote_bottom - quote_top)

				renderer.draw_rect(
					quote_x,
					quote_top,
					math.max(1, width - row.quote.background_offset),
					quote_height,
					design.get_quote_background_color()
				)
				renderer.draw_rect(
					quote_x,
					quote_top,
					row.quote.guide_width,
					quote_height,
					design.get_quote_guide_color()
				)
			end

			if row.rule then
				draw_horizontal_rule(design, x, y + math.floor(row.height / 2), width)
			end

			if row.heading_separator then
				draw_heading_separator(
					design,
					row.heading_separator,
					x,
					y + row.height - row.content_padding_bottom,
					width
				)
			end

			-- Paint every inline-code surface first so horizontal padding cannot cover nearby text
			local background_run_x = row_x

			for run_index = 1, #row.runs do
				local run = row.runs[run_index]
				local run_y = y + row.baseline - run.font:get_baseline()

				if run.code and not row.code_block and not run.spacer then
					-- Run width already contains horizontal padding reserved during wrapping
					renderer.draw_rect(
						background_run_x,
						run_y - code_span_padding_vertical,
						run.width,
						run.font:get_height() + code_span_padding_vertical * 2,
						design.get_code_span_background_color()
					)
				end

				background_run_x = background_run_x + run.width
			end

			for run_index = 1, #row.runs do
				local run = row.runs[run_index]
				local run_y = y + row.baseline
					- (run.math_box and run.math_box.baseline or run.font:get_baseline())
				local text_color = error_block.get_text_color(row, design)
					or row.code_header
					and design.get_code_block_header_text_color()
					or run.options.link and design.get_link_color()
					or design.get_text_color(run.syntax_type)
				if run.math_box then
					math_renderer.draw(
						run.math_box,
						run_x,
						run_y,
						text_color,
						design.get_link_color()
					)
				elseif not run.spacer then
					renderer.draw_text(
						run.font,
						run.text,
						run_x + (run.text_offset or 0),
						run_y,
						text_color
					)
				end

				if run.options.link and not run.spacer then
					-- Link underline shares exact width used by drawing and hit testing
					renderer.draw_rect(
						run_x + (run.link_offset or 0),
						run_y + math.min(
							run.font:get_height() - 1,
							run.font:get_baseline()
								+ (SCALE or 1) * design.links.underline_offset
						),
						run.link_width or run.width,
						math.max(1, (SCALE or 1) * design.links.underline_width),
						text_color
					)
				end

				run_x = run_x + run.width
			end
		end
	end
	core.pop_clip_rect()
end

return render
