---@diagnostic disable: duplicate-set-field
-- lx-bundle:true

--- Draw horizontal scrollbar metrics without remeasuring unchanged long lines
-- @module plugins.hscrollbar

local common = require("core.common")
local config = require("core.config")
local style = require("core.style")
local DocView = require("core.text.editor")
local TextViewer = require("core.text.viewer")

config.hscrollbar_enabled = config.hscrollbar_enabled ~= false
config.hscrollbar_height = config.hscrollbar_height or style.scrollbar_size
config.hscrollbar_scan_budget_ms = config.hscrollbar_scan_budget_ms or 1.0
config.hscrollbar_full_scan_max_lines = config.hscrollbar_full_scan_max_lines or 1000
config.hscrollbar_estimate_only_min_longest_line_chars = config.hscrollbar_estimate_only_min_longest_line_chars or 2000
config.hscrollbar_estimate_only_min_lines = config.hscrollbar_estimate_only_min_lines or 25000
config.hscrollbar_end_padding = config.hscrollbar_end_padding or 32

local LANE_ID = DocView.LaneIDs.HSCROLL
local LANE_ORDER = DocView.LaneOrders.HSCROLL

--- Return the current horizontal width cache for one view
-- Cache exact line widths per document change so width scans stay incremental
-- @param dv table doc view
-- @return table cache
local function get_line_width_cache(dv)
	local doc = dv.doc
	local change_id = (doc.get_change_id and doc:get_change_id()) or 0
	local font = dv:get_font()
	local indent_size = config.indent_size
	local cache = dv._hscrollbar_line_width_cache
	if
		cache
		and cache.doc == doc
		and cache.change_id == change_id
		and cache.font == font
		and cache.indent_size == indent_size
	then
		return cache
	end

	cache = {
		doc = doc,
		change_id = change_id,
		font = font,
		indent_size = indent_size,
		lines = {},
	}
	dv._hscrollbar_line_width_cache = cache

	return cache
end

--- Return cached visual width for one document line
-- @param dv table doc view
-- @param line number line index
-- @return number width
local function line_visual_width(dv, line)
	local cache = get_line_width_cache(dv)
	local cached = cache.lines[line]
	if cached ~= nil then
		return cached
	end

	local text = dv.doc.lines[line] or ""
	local len = #text
	if text:sub(-1) == "\n" then
		len = len - 1
	end

	local width = dv:get_col_x_offset(line, len + 1)
	cache.lines[line] = width

	return width
end

local function get_visible_lines_width(dv, viewport_h)
	local lh = dv:get_line_height()
	local y = dv.scroll.y or 0
	local minline = math.max(1, math.floor(y / lh))
	local maxline = math.min(#dv.doc.lines, math.floor((y + viewport_h) / lh) + 1)
	local max_w = 0
	for line = minline, maxline do
		max_w = math.max(max_w, line_visual_width(dv, line))
	end
	return max_w
end

local function ensure_width_cache(dv)
	local change_id = dv.doc:get_change_id()
	local cache = dv._hscrollbar_cache
	if cache and cache.change_id == change_id then
		return cache
	end
	cache = {
		change_id = change_id,
		width = 0,
		scan_idx = 1,
		done = false,
	}

	local li = dv.doc.longest_line_index
	local ll = dv.doc.longest_line_length
	if type(li) == "number" and type(ll) == "number" and ll > 0 and li >= 1 and li <= #dv.doc.lines then
		cache.width = math.max(cache.width, line_visual_width(dv, li))
	elseif type(ll) == "number" and ll > 0 then
		cache.width = math.max(cache.width, dv:get_font():get_width(" ") * ll)
	end

	local min_chars = tonumber(config.hscrollbar_estimate_only_min_longest_line_chars) or 0
	local min_lines = tonumber(config.hscrollbar_estimate_only_min_lines) or 0
	if min_chars > 0 and min_lines > 0 and type(ll) == "number" and ll >= min_chars and #dv.doc.lines >= min_lines then
		cache.done = true
		cache.scan_idx = #dv.doc.lines + 1
	end

	dv._hscrollbar_cache = cache
	return cache
end

local function scan_width_chunk(dv, cache)
	if cache.done then
		return
	end
	local line_count = #dv.doc.lines
	if line_count <= (config.hscrollbar_full_scan_max_lines or 1000) then
		for i = cache.scan_idx, line_count do
			cache.width = math.max(cache.width, line_visual_width(dv, i))
		end
		cache.scan_idx = line_count + 1
		cache.done = true
		return
	end

	local budget_ms = math.max(0.1, tonumber(config.hscrollbar_scan_budget_ms) or 1.0)
	local start_t = system.get_time()
	local i = cache.scan_idx
	while i <= line_count do
		cache.width = math.max(cache.width, line_visual_width(dv, i))
		i = i + 1
		if (system.get_time() - start_t) * 1000 >= budget_ms then
			break
		end
	end
	cache.scan_idx = i
	if i > line_count then
		cache.done = true
	end
end

--- Return widget content width that preserves text-viewport thumb math across a wider lane
-- Draw the track across the full bottom lane while keeping thumb visibility tied to the text viewport width
-- @param lane_w number full bottom lane width
-- @param content_w number full horizontal content width
-- @param content_visible_w number visible text viewport width
-- @return number scrollbar_content_w virtual scrollable width for the widget
local function get_scrollbar_content_width(lane_w, content_w, content_visible_w)
	if lane_w <= 0 or content_visible_w <= 0 or content_w <= content_visible_w then
		return math.max(content_w, lane_w)
	end

	return math.max(content_w, (lane_w * content_w) / content_visible_w)
end

local function compute_metrics(dv)
	if not config.hscrollbar_enabled or not dv.doc or not dv.doc.lines then
		dv:set_lane_size("bottom", LANE_ID, 0, LANE_ORDER)
		return nil
	end

	if dv.is_wrap_enabled and dv:is_wrap_enabled() then
		dv:set_lane_size("bottom", LANE_ID, 0, LANE_ORDER)
		return nil
	end

	local lane_h = math.max(6, math.floor((config.hscrollbar_height or style.scrollbar_size) * SCALE + 0.5))
	local _, _, viewport_w, viewport_h = dv:get_content_viewport_rect()
	viewport_w = math.max(1, viewport_w)
	viewport_h = math.max(1, viewport_h)
	local text_offset_x = dv.get_text_x_offset and dv:get_text_x_offset() or dv:get_gutter_width()
	local content_visible_w = math.max(20, viewport_w - text_offset_x)
	local visible_lines_w = get_visible_lines_width(dv, viewport_h)
	local end_pad = math.max(0, math.floor((config.hscrollbar_end_padding or 32) * SCALE + 0.5))
	local cache = ensure_width_cache(dv)
	cache.width = math.max(cache.width, visible_lines_w)
	local content_w = math.max(cache.width + end_pad, content_visible_w)
	local max_scroll_x = math.max(0, content_w - content_visible_w)

	if max_scroll_x <= 0 then
		dv:set_lane_size("bottom", LANE_ID, 0, LANE_ORDER)
		return { max_scroll_x = 0 }
	end

	dv:set_lane_size("bottom", LANE_ID, lane_h, LANE_ORDER)
	local x, y, w, h = dv:get_lane_rect("bottom", LANE_ID)
	if not x then
		return { max_scroll_x = max_scroll_x }
	end

	local scrollbar_content_w = get_scrollbar_content_width(w, content_w, content_visible_w)
	local sx = common.clamp(dv.scroll.x or 0, 0, max_scroll_x)
	local t = sx / max_scroll_x
	dv.hscrollbar:set_size(x, y, w, h, scrollbar_content_w)
	dv.hscrollbar:set_percent(common.clamp(t, 0, 1))

	return {
		x = x,
		y = y,
		w = w,
		h = h,
		content_w = content_w,
		scrollbar_content_w = scrollbar_content_w,
		max_scroll_x = max_scroll_x,
	}
end

local function sync_hscrollbar(dv, metrics)
	if not (metrics and metrics.x) then
		return
	end
	local t = (metrics.max_scroll_x > 0) and common.clamp((dv.scroll.x or 0) / metrics.max_scroll_x, 0, 1) or 0
	dv.hscrollbar:set_size(
		metrics.x,
		metrics.y,
		metrics.w,
		metrics.h,
		metrics.scrollbar_content_w or metrics.content_w
	)
	dv.hscrollbar:set_percent(t)
end

local _original_update = DocView.update
local _original_draw = DocView.draw
local _original_on_mouse_wheel = DocView.on_mouse_wheel
local _original_viewer_get_scroll_max_x = TextViewer.get_scroll_max_x
local _original_viewer_update = TextViewer.update
local _original_viewer_draw = TextViewer.draw
local _original_viewer_on_mouse_wheel = TextViewer.on_mouse_wheel
local _original_viewer_on_mouse_down = TextViewer.on_mouse_down
local _original_viewer_on_mouse_move = TextViewer.on_mouse_move
local _original_viewer_on_mouse_up = TextViewer.on_mouse_up
local _original_viewer_on_mouse_left = TextViewer.on_mouse_left

local lane_handler = {}

function lane_handler.on_mouse_down(dv, button, x, y, clicks)
	if button ~= "left" then
		return false
	end
	local metrics = dv._hscrollbar_metrics or compute_metrics(dv)
	if not (metrics and metrics.x) then
		return false
	end
	sync_hscrollbar(dv, metrics)
	local pressed = dv.hscrollbar:on_mouse_down(button, x, y, clicks)
	dv.dragging_hscrollbar = dv.hscrollbar.dragging or false
	if type(pressed) == "number" then
		local sx = common.clamp(pressed * metrics.max_scroll_x, 0, metrics.max_scroll_x)
		dv.scroll.to.x = sx
		dv.scroll.x = sx
	end
	return pressed and true or false
end

function lane_handler.on_mouse_move(dv, x, y, dx, dy)
	local metrics = dv._hscrollbar_metrics or compute_metrics(dv)
	if metrics and metrics.x then
		sync_hscrollbar(dv, metrics)
	end
	local moved = dv.hscrollbar:on_mouse_move(x, y, dx, dy)
	if type(moved) == "number" and metrics and metrics.x then
		local sx = common.clamp(moved * metrics.max_scroll_x, 0, metrics.max_scroll_x)
		dv.scroll.to.x = sx
		dv.scroll.x = sx
	end
	dv.dragging_hscrollbar = dv.hscrollbar.dragging or false
	if dv.dragging_hscrollbar then
		return true
	end
	if metrics and metrics.x and dv.hscrollbar:overlaps(x, y) then
		return true
	end
	return false
end

function lane_handler.on_mouse_up(dv, button, x, y)
	dv.hscrollbar:on_mouse_up(button, x, y)
	dv.dragging_hscrollbar = dv.hscrollbar.dragging or false
	return true
end

function lane_handler.get_mouse_cursor(dv, x, y)
	local metrics = dv._hscrollbar_metrics or compute_metrics(dv)
	if dv.dragging_hscrollbar then
		return "arrow"
	end
	if metrics and metrics.x and dv.hscrollbar:overlaps(x, y) then
		return "arrow"
	end
	return nil
end

function lane_handler.on_mouse_left(dv, x, y)
	dv.hscrollbar:on_mouse_left()
	dv.dragging_hscrollbar = false
end

local function update_metrics_and_lane(dv)
	local cache = ensure_width_cache(dv)
	scan_width_chunk(dv, cache)
	local metrics = compute_metrics(dv)
	dv._hscrollbar_metrics = metrics
	dv:clamp_scroll_position()
	dv:set_lane_input_handler("bottom", LANE_ID, (metrics and metrics.x) and lane_handler or nil)
	return metrics
end

function DocView:get_scroll_max_x()
	local metrics = self._hscrollbar_metrics or compute_metrics(self)
	return math.max(0, (metrics and metrics.max_scroll_x) or 0)
end

function DocView:update(...)
	update_metrics_and_lane(self)
	return _original_update(self, ...)
end

function DocView:on_mouse_wheel(...)
	local result = _original_on_mouse_wheel(self, ...)
	self:clamp_scroll_position()
	return result
end

function DocView:draw(...)
	_original_draw(self, ...)
	local metrics = self._hscrollbar_metrics
	if not metrics then
		metrics = compute_metrics(self)
	end
	self._hscrollbar_metrics = metrics
	self:clamp_scroll_position()
	if not (metrics and metrics.x) then
		return
	end
	sync_hscrollbar(self, metrics)
	self.hscrollbar:draw()
end

function TextViewer:get_scroll_max_x()
	local metrics = self._hscrollbar_metrics
	if metrics and metrics.max_scroll_x ~= nil then
		return math.max(0, metrics.max_scroll_x)
	end

	return _original_viewer_get_scroll_max_x(self)
end

function TextViewer:update(...)
	-- Keep the viewer lane layout ready before the shared text draw path runs
	update_metrics_and_lane(self)
	return _original_viewer_update(self, ...)
end

function TextViewer:on_mouse_wheel(...)
	local result = _original_viewer_on_mouse_wheel(self, ...)
	self:clamp_scroll_position()
	return result
end

function TextViewer:on_mouse_down(button, x, y, clicks)
	-- Let the viewer drag the shared horizontal scrollbar without enabling selection
	local side, id, handler = self:resolve_lane_input_handler_at(x, y)

	if handler and handler.on_mouse_down and handler.on_mouse_down(self, button, x, y, clicks) then
		self._active_lane_input = { side = side, id = id, handler = handler }
		return true
	end

	return _original_viewer_on_mouse_down(self, button, x, y, clicks)
end

function TextViewer:on_mouse_move(x, y, dx, dy)
	local active = self._active_lane_input

	if active and active.handler then
		if active.handler.on_mouse_move then
			active.handler.on_mouse_move(self, x, y, dx, dy)
		end
		local mouse_cursor = active.handler.get_mouse_cursor and active.handler.get_mouse_cursor(self, x, y, dx, dy)
		self.mouse_cursor = mouse_cursor or "arrow"
		return
	end

	_original_viewer_on_mouse_move(self, x, y, dx, dy)

	local side, id, hover_handler = self:resolve_lane_input_handler_at(x, y)
	local previous_hover = self._hover_lane_input
	local same_hover = previous_hover and hover_handler and previous_hover.side == side and previous_hover.id == id

	if previous_hover and not same_hover then
		if previous_hover.handler and previous_hover.handler.on_mouse_left then
			previous_hover.handler.on_mouse_left(self, x, y, dx, dy)
		end
		self._hover_lane_input = nil
	end

	if hover_handler then
		if hover_handler.on_mouse_move then
			hover_handler.on_mouse_move(self, x, y, dx, dy)
		end
		local mouse_cursor = hover_handler.get_mouse_cursor and hover_handler.get_mouse_cursor(self, x, y, dx, dy)
		self.mouse_cursor = mouse_cursor or "arrow"
		self._hover_lane_input = { side = side, id = id, handler = hover_handler }
	end
end

function TextViewer:on_mouse_up(button, x, y)
	local active = self._active_lane_input

	if active and active.handler then
		if active.handler.on_mouse_up then
			active.handler.on_mouse_up(self, button, x, y)
		end
		self._active_lane_input = nil
	end

	return _original_viewer_on_mouse_up(self, button, x, y)
end

function TextViewer:on_mouse_left(...)
	local active = self._active_lane_input

	if active and active.handler and active.handler.on_mouse_left then
		active.handler.on_mouse_left(self, ...)
	end
	self._active_lane_input = nil

	local hover = self._hover_lane_input

	if hover and hover.handler and hover.handler.on_mouse_left then
		hover.handler.on_mouse_left(self, ...)
	end
	self._hover_lane_input = nil

	return _original_viewer_on_mouse_left(self, ...)
end

function TextViewer:draw(...)
	local metrics = self._hscrollbar_metrics or update_metrics_and_lane(self)

	_original_viewer_draw(self, ...)

	self._hscrollbar_metrics = metrics
	self:clamp_scroll_position()
	self:set_lane_input_handler("bottom", LANE_ID, (metrics and metrics.x) and lane_handler or nil)
	if not (metrics and metrics.x) then
		return
	end

	sync_hscrollbar(self, metrics)
	self.hscrollbar:draw()
end
