-- lx-bundle:true

--- Fixed-page PNG export shared by rendered-document previews
-- @module core.preview.export

local common = require("core.common")
local config = require("core.config")
local core = require("core")

local export = {}

config.plugins.preview = common.merge({
	-- Export dimensions stay independent of the current window
	export_width = 2560,
	export_height = 3840,
}, config.plugins.preview)

--- Return configured fixed output dimensions
-- @return number width output page width
-- @return number height output page height
function export.get_page_size()
	local preview_config = config.plugins.preview or {}
	local width = math.max(1, math.floor(tonumber(preview_config.export_width) or 2560))
	local height = math.max(1, math.floor(tonumber(preview_config.export_height) or 3840))

	return width, height
end

--- Build one numbered PNG path beside its source document
-- @param filename string source document path
-- @param page_number number one-based page number
-- @return string path numbered PNG path
function export.get_output_path(filename, page_number)
	-- Pattern breakdown: final dot-extension after last path separator is removed
	local path_without_extension = filename:gsub("%.[^%.\\/]+$", "")

	return string.format("%s-%03d.png", path_without_extension, page_number)
end

--- Remove numbered pages left over from a longer completed export
-- @param filename string source document path
-- @param page_count number number of pages in latest export
-- @return number removed_count removed stale page count
function export.remove_stale_pages(filename, page_count)
	-- Pattern breakdown: final dot-extension after last path separator is removed
	local path_without_extension = filename:gsub("%.[^%.\\/]+$", "")
	local directory = system.path_dirname(path_without_extension)
	directory = directory == "" and "." or directory
	local base_name = path_without_extension:match("[^\\/]+$") or path_without_extension
	local prefix = base_name .. "-"
	local names, list_error = system.list_dir(directory)
	local removed_count = 0

	if not names then
		error(list_error or "Could not inspect previous preview images", 0)
	end

	for _, name in ipairs(names) do
		if name:sub(1, #prefix) == prefix then
			-- Pattern breakdown: numbered page suffix ends with lowercase PNG extension
			local number_text = name:sub(#prefix + 1):match("^(%d+)%.png$")
			local page_number = number_text and tonumber(number_text)
			local path = system.join_path(directory, name)

			if page_number and #number_text >= 3 and page_number > page_count
				and system.is_file(path) == true
			then
				local removed, remove_error = os.remove(path)

				if not removed then
					error(remove_error or "Could not remove stale preview image", 0)
				end

				removed_count = removed_count + 1
			end
		end
	end

	return removed_count
end

--- Return page starts that keep ordinary rendered rows intact
-- @param view table detached fully laid-out preview
-- @param page_height number output page height
-- @return table starts document scroll position for every page
function export.get_page_starts(view, page_height)
	local starts = { 0 }
	local page_start = 0
	local design = assert(view.preview_design, "Preview export requires a document design")
	local padding_top = (design.document.padding_vertical or 0) * (SCALE or 1)

	for row_index = 1, #view.layout_rows do
		local row = view.layout_rows[row_index]
		local row_top = padding_top + row.y
		local row_bottom = row_top + row.height

		-- A single row taller than a whole page is the only unavoidable clipped case
		if row_top > page_start and row_bottom > page_start + page_height then
			page_start = row_top
			starts[#starts + 1] = page_start
		end
	end

	return starts
end

--- Render one prepared page without preview interface controls
-- @param export_view table detached fully laid-out preview
-- @param export_width number output page width
-- @param export_height number output page height
-- @param page_start number document scroll position at page top
-- @return userdata page rendered page pixels
function export.render_page(export_view, export_width, export_height, page_start)
	local live_clip_rect_stack = core.clip_rect_stack

	export_view.scroll.y = page_start
	export_view.scroll.to.y = page_start
	-- Core clip nesting still describes the visible window during off-screen rendering
	core.clip_rect_stack = { { 0, 0, export_width, export_height } }
	local rendered, page = pcall(renderer.render_to_canvas, export_width, export_height, function()
		renderer.set_clip_rect(0, 0, export_width, export_height)
		export_view:draw_document()
	end)
	core.clip_rect_stack = live_clip_rect_stack

	if not rendered then
		error(page, 0)
	end

	return page
end

--- Render the first configured page for one live preview
-- @param view table live rendered-document preview
-- @return userdata page rendered page pixels
function export.render_first_page(view)
	local export_view, export_width, export_height = view:create_export_view()

	return export.render_page(export_view, export_width, export_height, 0)
end

--- Render and save every page beside its source document
-- @param view table live rendered-document preview
-- @param on_page function|nil callback before each page is rendered
-- @return table paths saved PNG paths
function export.export_images(view, on_page)
	local filename = view.doc and view.doc.filename

	if type(filename) ~= "string" or filename == "" then
		error("Preview document must be saved before exporting images", 0)
	end

	local paths = {}
	local exported, export_error = pcall(function()
		local export_view, export_width, export_height = view:create_export_view()
		local page_starts = export.get_page_starts(export_view, export_height)

		for page_number = 1, #page_starts do
			local path = export.get_output_path(filename, page_number)

			if on_page then
				on_page(page_number, path)
			end

			-- First draw starts any asynchronous glyph rasterization for this page
			export.render_page(
				export_view,
				export_width,
				export_height,
				page_starts[page_number]
			)

			-- Main renderer frames apply completed glyphs between cooperative yields
			while renderer.has_pending_glyphs() do
				core.redraw = true
				coroutine.yield()
			end

			local page = export.render_page(
				export_view,
				export_width,
				export_height,
				page_starts[page_number]
			)
			local saved, save_error = canvas.save_png(page, path)

			if not saved then
				error(save_error or "Could not save exported preview image", 0)
			end

			paths[#paths + 1] = path
		end
	end)

	if not exported then
		-- Failed or old pages above the last completed page must not look current
		local cleaned, cleanup_error = pcall(export.remove_stale_pages, filename, #paths)

		if not cleaned then
			error(string.format(
				"%s\nCleanup failed: %s",
				tostring(export_error),
				tostring(cleanup_error)
			), 0)
		end

		error(export_error, 0)
	end

	-- Higher old pages stay in place until every replacement page is safely saved
	export.remove_stale_pages(filename, #paths)

	return paths
end

--- Start responsive multi-page export for one preview
-- @param view table live rendered-document preview
-- @return boolean started true when export was scheduled
function export.start(view)
	core.add_coroutine(function()
		local paths = export.export_images(view, function(page_number)
			if core.status_view then
				core.status_view:show_notice(string.format("Rendering image %03d...", page_number))
			end

			-- Let the status bar paint before the next off-screen page is processed
			core.redraw = true
			coroutine.yield()
		end)

		if core.status_view then
			core.status_view:show_success(string.format("Exported %d images", #paths))
		end
		core.redraw = true
	end, view)

	return true
end

return export
