-- lx-bundle:true

-- This file is part of Lx, created by Liqube <www.liqube.com>
-- Copyright Liqube Audio. All rights reserved.
-- Lx Interim License. See LICENSE for details.

--- Read-only font preview view
-- @module plugins.fontpreview.view

local core = require("core")
local common = require("core.common")
local style = require("core.style")
local DocView = require("core.doc.view")

local FontPreview = DocView:extend()

-- Keep the repertoire compact before the larger size comparison rows
local OVERVIEW_FONT_SIZE = 18

-- Match the familiar Windows preview steps for easy visual comparison
local SAMPLE_FONT_SIZES = { 12, 18, 24, 36, 48, 60, 72, 96 }

-- Keep the common character overview separate from the reading samples
local CHARACTER_LINES = {
	"abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ",
	"1234567890 . : , ; ' \" ( ! ? ) + - * / =",
	-- Cover several extended-Latin groups before sampling other writing systems
	"Äß ĄŁ ČŘ Ğİ ȘȚ ÆØ αΩ ЖЯ אב مب अआ অআ กข 中文 あい アイ 한글 აბ աբ",
	-- Pair common filled and outline symbols, followed by color-emoji candidates
	"★☆ ●○ ■□ ▲△ 😀 😂 ❤️ 👍 🌍 🚀",
}

-- One stable sentence makes differences between sizes immediately visible
local SAMPLE_TEXT = "The quick red fox jumps over the lady dog. 1234567890"

-- Keep current format names separate from the shared preview implementation
local FORMAT_NAMES = {
	["format.otf"] = "OpenType",
	["format.ttf"] = "TrueType",
}

--- Return the display name for one supported font format
-- @param doc table font document
-- @return string format_name
local function get_format_name(doc)
	local module_name = doc and doc.format and doc.format._module_name

	return FORMAT_NAMES[module_name] or "Font"
end

--- Draw one quiet section boundary and return the next content row
-- @param x number left edge
-- @param y number current vertical position
-- @param width number available width
-- @return number next_y
local function draw_separator(x, y, width)
	local divider_height = math.max(1, style.divider_size or 1)
	-- Use the same color as the font so section boundaries stay clear and thin
	renderer.draw_rect(x, y, width, divider_height, style.text)

	return y + divider_height
end

--- Return the height of one thin separator
-- @return number separator_height
local function get_separator_height()
	return math.max(1, style.divider_size or 1)
end

--- Return the exact pixel gap for every side of a section boundary
-- @return number padding
local function get_section_padding()
	return 20
end

--- Return visible row space that survives fonts with broken vertical metrics
-- @param font userdata loaded font
-- @param pixel_size number requested pixel size
-- @return number text_height
local function get_preview_text_height(font, pixel_size)
	-- Some display fonts report a line height smaller than their visible glyphs
	return math.max(font:get_height(), pixel_size)
end

--- Return one size-comparison row including its trailing rhythm
-- @param font userdata loaded font
-- @param pixel_size number requested pixel size
-- @return number row_height
local function get_preview_line_height(font, pixel_size)
	return get_preview_text_height(font, pixel_size) + style.padding.y / 2
end

--- Load every font handle used by one visible zoom level
-- @param filename string font path
-- @param zoom_multiplier number view-local zoom
-- @return table fonts loaded preview handles and sizes
local function load_preview_fonts(filename, zoom_multiplier)
	local scale = (SCALE or 1) * zoom_multiplier
	local overview_pixel_size = OVERVIEW_FONT_SIZE * scale
	local overview_font = renderer.font.load(filename, overview_pixel_size)
	local sample_fonts = {}

	for size_index = 1, #SAMPLE_FONT_SIZES do
		local size = SAMPLE_FONT_SIZES[size_index]
		-- Reuse the overview handle when its logical size appears in the ladder
		local font = size == OVERVIEW_FONT_SIZE
			and overview_font
			or renderer.font.load(filename, size * scale)
		sample_fonts[size_index] = { size = size, pixel_size = size * scale, font = font }
	end

	return {
		overview_font = overview_font,
		overview_pixel_size = overview_pixel_size,
		sample_fonts = sample_fonts,
	}
end

--- Build a read-only preview for one file-backed font document
-- @param doc table binary font document
function FontPreview:new(doc)
	FontPreview.super.new(self, doc)
	-- Font files are presented as content and never expose editing behavior
	self.can_select = false
	self.can_edit = false
	self.has_cursor_model = false
	self.draw_cursor = false
	-- Keep the shared cached font handle alive for as long as this view needs it
	self.preview_font = nil
	self.preview_pixel_size = nil
	self.sample_fonts = nil
	self.font_info = nil
	self.load_error = nil
	self.zoom_multiplier = 1

	local loaded, fonts_or_error = pcall(load_preview_fonts, doc.filename, self.zoom_multiplier)

	if loaded then
		self.preview_font = fonts_or_error.overview_font
		self.preview_pixel_size = fonts_or_error.overview_pixel_size
		self.sample_fonts = fonts_or_error.sample_fonts
		self.font_info = self.preview_font:get_info()
	else
		-- A damaged font still gets a stable document view with a useful error
		self.load_error = tostring(fonts_or_error)
	end
end

--- Return the current view-local font zoom
-- @return number zoom_multiplier
function FontPreview:get_zoom_multiplier()
	return self.zoom_multiplier
end

--- Keep shared zoom from changing editor fonts outside this preview
-- @return boolean apply_absolute_scale
function FontPreview:should_apply_absolute_scale()
	return false
end

--- Return the widest preview row including its left and right padding
-- @return number content_width
function FontPreview:get_content_width()
	local width = 0

	if self.preview_font then
		for line_index = 1, #CHARACTER_LINES do
			width = math.max(width, self.preview_font:get_width(CHARACTER_LINES[line_index]))
		end

		local label_width = style.font:get_width("96") + style.padding.x

		for row_index = 1, #self.sample_fonts do
			local row = self.sample_fonts[row_index]
			width = math.max(width, label_width + row.font:get_width(SAMPLE_TEXT))
		end
	end

	return width + style.padding.x * 2
end

--- Return horizontal room available to middle-drag panning
-- @return number max_scroll_x
function FontPreview:get_scroll_max_x()
	return math.max(0, self:get_content_width() - self.size.x)
end

--- Reload preview sizes and preserve the content beneath the zoom anchor
-- @param zoom_multiplier number requested view-local zoom
-- @param context table|nil command input details
-- @return boolean changed
function FontPreview:set_zoom_multiplier(zoom_multiplier, context)
	local requested_zoom = common.clamp(tonumber(zoom_multiplier) or 1, 0.25, 10)

	if math.abs(requested_zoom - self.zoom_multiplier) < 1e-6 then
		return false
	end

	-- Keyboard zoom keeps the viewport's top-left content fixed
	local anchor_x = self.position.x
	local anchor_y = self.position.y

	if type(context) == "table" and context.origin == "mouse" then
		anchor_x = tonumber(context.mouse_x) or anchor_x
		anchor_y = tonumber(context.mouse_y) or anchor_y
	end

	local local_x = common.clamp(anchor_x - self.position.x, 0, self.size.x)
	local local_y = common.clamp(anchor_y - self.position.y, 0, self.size.y)
	local previous_width = math.max(1, self:get_content_width())
	local previous_height = math.max(1, self:get_scrollable_size())
	local content_anchor_x = (self.scroll.x or 0) + local_x
	local content_anchor_y = (self.scroll.y or 0) + local_y
	local loaded, fonts_or_error = pcall(load_preview_fonts, self.doc.filename, requested_zoom)

	if not loaded then
		return false
	end

	self.preview_font = fonts_or_error.overview_font
	self.preview_pixel_size = fonts_or_error.overview_pixel_size
	self.sample_fonts = fonts_or_error.sample_fonts
	self.zoom_multiplier = requested_zoom

	local target_x = content_anchor_x * (self:get_content_width() / previous_width) - local_x
	local target_y = content_anchor_y * (self:get_scrollable_size() / previous_height) - local_y

	if type(context) == "table" and context.snap_to_col1 == true then
		target_x = 0
	end

	self.scroll.x = common.clamp(target_x, 0, self:get_scroll_max_x())
	self.scroll.to.x = self.scroll.x
	self.scroll.y = common.clamp(target_y, 0, self:get_scroll_max_y())
	self.scroll.to.y = self.scroll.y
	core.redraw = true

	return true
end

--- Return key binding contexts for the font preview surface
-- @return table contexts ordered from most specific to broadest
function FontPreview:get_keymap_contexts()
	return { "font_preview", "doc_view", "global" }
end

--- Return one preview row height
-- @return number row_height
function FontPreview:get_line_height()
	local font = self.preview_font or style.font
	local pixel_size = self.preview_pixel_size or font:get_height()

	return get_preview_text_height(font, pixel_size)
end

--- Return the height of all preview content
-- @return number content_height
function FontPreview:get_scrollable_size()
	local metadata_rows = self.load_error and 1 or 3
	local heading_height = metadata_rows * style.font:get_height()
		+ math.max(0, metadata_rows - 1) * style.padding.y / 2
	local separator_height = get_separator_height()
	local content_height = 0

	if not self.load_error then
		-- Inter-row space belongs only between repertoire rows, not after the last
		content_height = #CHARACTER_LINES * self:get_line_height()
			+ math.max(0, #CHARACTER_LINES - 1) * style.padding.y / 2

		for row_index = 1, #self.sample_fonts do
			local row = self.sample_fonts[row_index]
			content_height = content_height + get_preview_line_height(row.font, row.pixel_size)
		end
	end

	return heading_height + content_height + separator_height * 2
		+ get_section_padding() * 4 + style.padding.y
end

--- Draw the font samples or a readable load error
function FontPreview:draw()
	self:draw_background(style.background)

	local x = self.position.x + style.padding.x - (self.scroll.x or 0)
	local scroll_y = self.scroll.y or 0
	local y = self.position.y + style.padding.y - scroll_y
	local content_width = math.max(
		1,
		self.size.x - style.padding.x * 2,
		self:get_content_width() - style.padding.x * 2
	)

	core.push_clip_rect(self.position.x, self.position.y, self.size.x, self.size.y)
	if self.load_error then
		local message = "Unable to load this font"
		renderer.draw_text(style.font, message, x, y, style.error or style.accent or style.text)
	else
		-- Present the stable face details before rendering its own glyphs
		local info = self.font_info
		local family = info.family_name ~= "" and info.family_name or get_format_name(self.doc)
		local style_name = info.style_name ~= "" and " " .. info.style_name or ""
		local version = info.version ~= "" and info.version or "Unknown"
		local outline = info.outline_format ~= "" and info.outline_format or get_format_name(self.doc)
		local metadata_lines = {
			"Font name: " .. family .. style_name,
			"Version: " .. version,
			string.format("%s outlines · %d glyphs", outline, info.glyph_count),
		}
		local metadata_height = #metadata_lines * style.font:get_height()
			+ math.max(0, #metadata_lines - 1) * style.padding.y / 2
		local divider_height = math.max(1, style.divider_size or 1)
		local section_padding = get_section_padding()
		local top_panel_height = style.padding.y + metadata_height + section_padding + divider_height
		-- Restrict the theme's current-row color to the metadata panel
		renderer.draw_rect(
			self.position.x,
			self.position.y - scroll_y,
			self.size.x,
			top_panel_height,
			style.line_highlight or style.background2 or style.background
		)

		for line_index = 1, #metadata_lines do
			renderer.draw_text(style.font, metadata_lines[line_index], x, y, style.text)
			y = y + style.font:get_height()

			if line_index < #metadata_lines then
				y = y + style.padding.y / 2
			end
		end

		y = y + section_padding
		y = draw_separator(x, y, content_width)
		y = y + section_padding

		for line_index = 1, #CHARACTER_LINES do
			renderer.draw_text(self.preview_font, CHARACTER_LINES[line_index], x, y, style.text)
			y = y + self:get_line_height()

			if line_index < #CHARACTER_LINES then
				y = y + style.padding.y / 2
			end
		end

		y = y + section_padding
		y = draw_separator(x, y, content_width)
		y = y + section_padding

		local label_width = style.font:get_width("72") + style.padding.x

		for row_index = 1, #self.sample_fonts do
			local row = self.sample_fonts[row_index]
			local row_height = get_preview_line_height(row.font, row.pixel_size)
			-- Place both fonts on the same baseline, matching native font previews
			local label_y = y + row.font:get_baseline() - style.font:get_baseline()
			renderer.draw_text(style.font, tostring(row.size), x, label_y, style.dim or style.text)
			renderer.draw_text(row.font, SAMPLE_TEXT, x + label_width, y, style.text)
			y = y + row_height
		end
	end

	core.pop_clip_rect()
	self:draw_scrollbar()
end

--- Return whether one document is a file-backed supported font
-- @param doc table|nil candidate document
-- @return boolean supported
function FontPreview.supports_doc(doc)
	return doc
		and doc.filename ~= nil
		and type(doc.format) == "table"
		and FORMAT_NAMES[doc.format._module_name] ~= nil
		or false
end

return FontPreview
