-- lx-bundle:true

--- Base view for native rendered-document previews
-- Format adapters supply parsing, layout details, design, and specialized content
-- @module core.preview.view

local preview_layout = require("core.preview.layout")
local preview_render = require("core.preview.render")
local common = require("core.common")
local config = require("core.config")
local core = require("core")
local style = require("core.style")
local DocView = require("core.doc.view")

local PreviewView = DocView:extend()

PreviewView.is_document_preview = true

-- Direct bindings keep shared hot paths free of forwarding calls
PreviewView.reset_layout = preview_layout.reset
PreviewView.add_layout_row = preview_layout.add_row
PreviewView.get_first_visible_layout_row = preview_layout.find_first_visible_row
PreviewView.get_visible_source_line = preview_layout.get_visible_source_line
PreviewView.capture_layout_anchor = preview_layout.capture_anchor
PreviewView.restore_layout_anchor = preview_layout.restore_anchor
PreviewView.draw_code_block_background = preview_render.draw_code_block_background
PreviewView.draw_document = preview_render.draw_document

--- Return width available to horizontally scrolling preview content
-- @return number width visible content width
function PreviewView:get_preview_visible_width()
	local design = self.preview_design
	local document_design = design and design.document or {}
	local padding = (tonumber(document_design.padding_horizontal) or 0) * (SCALE or 1)

	return math.max(1, self:get_document_viewport_width() - padding * 2)
end

--- Return complete measured width owned by this preview adapter
-- @return number width measured content width
function PreviewView:get_preview_content_width()
	self:rebuild_layout()

	return math.max(0, tonumber(self.layout_width) or 0)
end

--- Return bounded horizontal range for adapters that opt into wide layouts
-- @return number max_scroll_x horizontal scroll limit
function PreviewView:get_scroll_max_x()
	if self.preview_horizontal_scroll ~= true or config.hscrollbar_enabled == false then
		return 0
	end

	return math.max(0, self:get_preview_content_width() - self:get_preview_visible_width())
end

--- Convert preview content width into the scrollbar widget's full-lane range
-- @param lane_width number complete scrollbar lane width
-- @param content_width number measured preview content width
-- @param visible_width number visible preview content width
-- @return number scrollbar_width virtual widget content width
local function get_scrollbar_width(lane_width, content_width, visible_width)
	if lane_width <= 0 or visible_width <= 0 or content_width <= visible_width then
		return math.max(content_width, lane_width)
	end

	return math.max(content_width, lane_width * content_width / visible_width)
end

--- Remove stale horizontal scrollbar geometry when content fits
-- @param view table preview view
local function hide_horizontal_scrollbar(view)
	view:set_lane_size("bottom", view.LaneIDs.HSCROLL, 0, view.LaneOrders.HSCROLL)
	view.hscrollbar:set_size(0, 0, 0, 0, 0)
	view._preview_horizontal_scrollbar = nil
end

--- Refresh the optional horizontal lane from current measured preview width
-- @return table|nil metrics active scrollbar geometry
function PreviewView:update_horizontal_scrollbar()
	local max_scroll_x = self:get_scroll_max_x()

	if max_scroll_x <= 0 then
		hide_horizontal_scrollbar(self)
		self.scroll.x = 0
		self.scroll.to.x = 0

		return nil
	end

	local lane_height = math.max(
		6,
		math.floor((tonumber(config.hscrollbar_height) or style.scrollbar_size or 12)
			* (SCALE or 1) + 0.5)
	)
	self:set_lane_size("bottom", self.LaneIDs.HSCROLL, lane_height, self.LaneOrders.HSCROLL)
	local x, y, width, height = self:get_lane_rect("bottom", self.LaneIDs.HSCROLL)

	if not x then
		return nil
	end

	local visible_width = self:get_preview_visible_width()
	local content_width = self:get_preview_content_width()
	local scrollbar_width = get_scrollbar_width(width, content_width, visible_width)
	local scroll_x = common.clamp(self.scroll.x or 0, 0, max_scroll_x)
	local metrics = {
		x = x,
		y = y,
		width = width,
		height = height,
		max_scroll_x = max_scroll_x,
		scrollbar_width = scrollbar_width,
	}

	self.scroll.x = scroll_x
	self.scroll.to.x = common.clamp(self.scroll.to.x or scroll_x, 0, max_scroll_x)
	self.hscrollbar:set_size(x, y, width, height, scrollbar_width)
	self.hscrollbar:set_percent(scroll_x / max_scroll_x)
	self._preview_horizontal_scrollbar = metrics

	return metrics
end

--- Apply one normalized horizontal scrollbar position immediately
-- @param view table preview view
-- @param percent number normalized scrollbar position
local function apply_horizontal_scroll(view, percent)
	local metrics = view._preview_horizontal_scrollbar or view:update_horizontal_scrollbar()

	if not metrics then
		return false
	end

	local scroll_x = common.clamp(tonumber(percent) or 0, 0, 1) * metrics.max_scroll_x
	view.scroll.x = scroll_x
	view.scroll.to.x = scroll_x
	view.hscrollbar:set_percent(scroll_x / metrics.max_scroll_x)
	view:keep_mouse_interaction_active()

	return true
end

--- Return whether one point belongs to either preview scrollbar
-- @param x number pointer x
-- @param y number pointer y
-- @return boolean overlaps
function PreviewView:scrollbar_overlaps_point(x, y)
	local vertical = PreviewView.super.scrollbar_overlaps_point(self, x, y)
	local metrics = self:update_horizontal_scrollbar()
	local horizontal = metrics and self.hscrollbar:overlaps(x, y) ~= nil or false

	return vertical or horizontal
end

--- Return key binding contexts for rendered-document previews
-- @return table contexts ordered from most specific to broadest
function PreviewView:get_keymap_contexts()
	return { "document_preview", "doc_view", "global" }
end

--- Scroll vertically by one rendered body row
-- @param direction number negative for up, positive for down
function PreviewView:scroll_vertical_step(direction)
	self.scroll.to.y = (self.scroll.to.y or self.scroll.y or 0)
		+ direction * self:get_line_height()
	self:clamp_scroll_position()
	core.redraw = true
end

--- Scroll vertically by one visible page
-- @param direction number negative for up, positive for down
function PreviewView:scroll_page(direction)
	local viewport_height = self:get_scroll_viewport_height()
	local line_height = self:get_line_height()
	local step = math.max(line_height, viewport_height - line_height)

	self.scroll.to.y = (self.scroll.to.y or self.scroll.y or 0) + direction * step
	self:clamp_scroll_position()

	if config.animate_page_scroll ~= true then
		self.scroll.y = self.scroll.to.y
	end

	core.redraw = true
end

--- Scroll horizontally by three quarters of the visible content width
-- @param direction number negative for left, positive for right
-- @return boolean handled true when horizontal preview scrolling is available
function PreviewView:scroll_horizontal_page(direction)
	local maximum_x = self:get_scroll_max_x()

	if self.preview_horizontal_scroll ~= true or maximum_x <= 0 then
		return false
	end

	local step = self:get_preview_visible_width() * 0.75
	self.scroll.to.x = common.clamp(
		(self.scroll.to.x or self.scroll.x or 0) + direction * step,
		0,
		maximum_x
	)

	if config.animate_page_scroll ~= true then
		self.scroll.x = self.scroll.to.x
	end

	core.redraw = true

	return true
end

--- Return one rendered text link under a pointer position
-- Uses measured row widths so link clicks follow the exact painted text
-- @param x number pointer x
-- @param y number pointer y
-- @return string|nil target adapter-owned link target
function PreviewView:get_link_at(x, y)
	if type(x) ~= "number"
		or type(y) ~= "number"
		or x < self.position.x
		or y < self.position.y
		or x >= self.position.x + self.size.x
		or y >= self.position.y + self.size.y
		or self:scrollbar_overlaps_point(x, y)
	then
		return nil
	end

	self:rebuild_layout()

	local design = self.preview_design
	local document_design = design and design.document

	if not document_design then
		return nil
	end

	local content_x = self.position.x
		+ document_design.padding_horizontal * (SCALE or 1)
		- (self.preview_horizontal_scroll == true and (self.scroll.x or 0) or 0)
	local content_top = self.position.y
		+ document_design.padding_vertical * (SCALE or 1)
		- (self.scroll.y or 0)
	local layout_y = y - content_top
	local row_index = self:get_first_visible_layout_row(layout_y)
	local row = self.layout_rows[row_index]

	if not row or layout_y < row.y or layout_y > row.y + row.height then
		return nil
	end

	local run_x = content_x + row.x_offset

	for run_index = 1, #row.runs do
		local run = row.runs[run_index]
		local options = run.options or {}
		local link_x = run_x + (run.link_offset or 0)
		local link_width = run.link_width or run.width

		if options.link and x >= link_x and x <= link_x + link_width then
			return options.link
		end

		run_x = run_x + run.width
	end

	return nil
end

--- Open a rendered link through its format adapter
-- @param x number pointer x
-- @param y number pointer y
-- @return boolean handled true when a link consumed the click
function PreviewView:open_link_at(x, y)
	local target = self:get_link_at(x, y)

	return target and type(self.open_link) == "function" and self:open_link(target) or false
end

--- Give the horizontal scrollbar first chance at left-button input
-- @param button string mouse button
-- @param x number pointer x
-- @param y number pointer y
-- @param clicks number click count
-- @return boolean|nil handled
function PreviewView:on_mouse_down(button, x, y, clicks)
	local metrics = self:update_horizontal_scrollbar()

	if metrics then
		local pressed = self.hscrollbar:on_mouse_down(button, x, y, clicks)

		if type(pressed) == "number" then
			apply_horizontal_scroll(self, pressed)
			return true
		elseif pressed then
			return true
		end
	end

	return PreviewView.super.on_mouse_down(self, button, x, y, clicks)
end

--- Keep horizontal scrollbar dragging separate from document panning
-- @param x number pointer x
-- @param y number pointer y
-- @param dx number pointer delta x
-- @param dy number pointer delta y
-- @return boolean|nil handled
function PreviewView:on_mouse_move(x, y, dx, dy)
	local metrics = self:update_horizontal_scrollbar()

	if metrics and (self.hscrollbar.dragging or self.hscrollbar:overlaps(x, y)) then
		local moved = self.hscrollbar:on_mouse_move(x, y, dx, dy)

		if type(moved) == "number" then
			apply_horizontal_scroll(self, moved)
		end
		self.mouse_cursor = "arrow"

		return true
	end

	return PreviewView.super.on_mouse_move(self, x, y, dx, dy)
end

--- Release horizontal dragging with shared document input state
-- @param button string mouse button
-- @param x number pointer x
-- @param y number pointer y
-- @return boolean|nil handled
function PreviewView:on_mouse_up(button, x, y)
	local was_dragging = self.hscrollbar.dragging
	local horizontal_handled = self.hscrollbar:on_mouse_up(button, x, y)
	local handled = PreviewView.super.on_mouse_up(self, button, x, y)

	return was_dragging or horizontal_handled or handled
end

--- Clear horizontal hover when pointer leaves preview
-- @return boolean|nil handled
function PreviewView:on_mouse_left(...)
	self.hscrollbar:on_mouse_left()

	return PreviewView.super.on_mouse_left(self, ...)
end

--- Keep horizontal geometry current before shared scroll clamping
function PreviewView:update()
	PreviewView.super.update(self)
	self:update_horizontal_scrollbar()
	self:clamp_scroll_position()
end

--- Draw vertical and optional horizontal preview scrollbars
function PreviewView:draw_scrollbar()
	local metrics = self:update_horizontal_scrollbar()
	PreviewView.super.draw_scrollbar(self)

	if not metrics then
		return
	end

	local scroll_x = common.clamp(self.scroll.x or 0, 0, metrics.max_scroll_x)
	self.hscrollbar:set_percent(scroll_x / metrics.max_scroll_x)
	self.hscrollbar:draw()
end

return PreviewView
