---@diagnostic disable: duplicate-set-field
-- 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.

--- Draw horizontal overflow indicators without remeasuring unchanged long lines
-- @module plugins.hscrollindicator

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

config.hscroll_indicator_enabled = config.hscroll_indicator_enabled ~= false
config.hscroll_indicator_width = config.hscroll_indicator_width or 12

local _original_draw = DocView.draw
local _original_viewer_draw = TextViewer.draw

-- Return scaled bar metrics that keep the zoomed indicator glyph inside the bar
local function get_indicator_metrics(dv)
	local font = dv:get_font()
	local configured_width = math.max(6, math.floor((config.hscroll_indicator_width or 12) * SCALE + 0.5))
	local side_padding = math.max(1, math.floor(math.max(SCALE * 2, font:get_width(" ") * 0.35) + 0.5))
	local left_glyph_width = font:get_width("<")
	local right_glyph_width = font:get_width(">")
	local glyph_width = math.max(left_glyph_width, right_glyph_width)
	local bar_width = math.max(configured_width, glyph_width + side_padding * 2)

	return {
		bar_width = bar_width,
		left_glyph_x = math.floor((bar_width - left_glyph_width) / 2 + 0.5),
		right_glyph_x = math.floor((bar_width - right_glyph_width) / 2 + 0.5),
	}
end

--- Return cached visual width for one document line
-- Cache exact line widths per document change so right-edge checks stay cheap
-- @param dv table doc view
-- @param line number line index
-- @return number width
local function line_visual_width(dv, line)
	local doc = dv.doc
	local change_id = (doc.get_change_id and doc:get_change_id()) or 0
	local cache = dv._hscrollindicator_width_cache
	local font = dv:get_font()
	local indent_size = doc:get_indent_size()
	if
		not cache
		or cache.doc ~= doc
		or cache.change_id ~= change_id
		or cache.font ~= font
		or cache.indent_size ~= indent_size
	then
		cache = {
			doc = doc,
			change_id = change_id,
			font = font,
			indent_size = indent_size,
			lines = {},
		}
		dv._hscrollindicator_width_cache = cache
	end

	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

-- Returns marked visible lines for right indicator drawing around long lines
local function collect_right_indicator_lines(dv, sx, visible_w)
	local minline, maxline = dv:get_visible_line_range()
	local right_edge = sx + visible_w
	local marks = {}
	local any = false
	for line = minline, maxline do
		if line_visual_width(dv, line) > right_edge + 0.5 then
			any = true
			marks[line] = true
			if line > minline then
				marks[line - 1] = true
			end
			if line < maxline then
				marks[line + 1] = true
			end
		end
	end
	return any, marks, minline, maxline
end

-- Draws a vertical hscroll indicator bar using one glyph per visible line
local function draw_indicator_bar(dv, bar_x, bar_y, bar_w, bar_h, glyph, bg, fg)
	local minline, maxline = dv:get_visible_line_range()
	local _, y = dv:get_line_screen_position(minline)
	local lh = dv:get_line_height()
	local yoff = dv:get_line_text_y_offset()
	local font = dv:get_font()
	local metrics = get_indicator_metrics(dv)
	local tx = bar_x + ((glyph == "<") and metrics.left_glyph_x or metrics.right_glyph_x)

	core.push_clip_rect(bar_x, bar_y, bar_w, bar_h)
	renderer.draw_rect(bar_x, bar_y, bar_w, bar_h, bg)
	for _ = minline, maxline do
		renderer.draw_text(font, glyph, tx, y + yoff, fg)
		y = y + lh
	end
	core.pop_clip_rect()
end

-- Draws a vertical hscroll indicator bar only on marked visible lines
local function draw_sparse_indicator_bar(dv, bar_x, bar_y, bar_w, bar_h, glyph, bg, fg, marks, minline, maxline)
	local _, y = dv:get_line_screen_position(minline)
	local lh = dv:get_line_height()
	local yoff = dv:get_line_text_y_offset()
	local font = dv:get_font()
	local metrics = get_indicator_metrics(dv)
	local tx = bar_x + ((glyph == "<") and metrics.left_glyph_x or metrics.right_glyph_x)

	core.push_clip_rect(bar_x, bar_y, bar_w, bar_h)
	renderer.draw_rect(bar_x, bar_y, bar_w, bar_h, bg)
	for line = minline, maxline do
		if marks[line] then
			renderer.draw_text(font, glyph, tx, y + yoff, fg)
		end
		y = y + lh
	end
	core.pop_clip_rect()
end

local function draw_indicators(dv)
	if not config.hscroll_indicator_enabled then
		return
	end

	if not (dv.doc and dv.doc.lines) then
		return
	end

	if dv.is_wrap_enabled and dv:is_wrap_enabled() then
		dv._hscrollindicator_width_cache = nil
		return
	end

	-- Indicators follow same current scroll position used to draw text
	local sx = dv.scroll.x or 0

	local metrics = get_indicator_metrics(dv)
	local bar_w = metrics.bar_width
	local _, bar_y, viewport_w, bar_h = dv:get_content_viewport_rect()
	local text_x = dv.position.x + dv:get_text_x_offset()
	local right_anchor = dv:get_overlay_right_lane_limit()
	local right_occlusion = ((dv:get_right_lane_width() or 0) > 0) and 0 or style.scrollbar_size
	local visible_w = math.max(20, viewport_w - dv:get_text_x_offset() - right_occlusion)
	local bar_bg = style.minimap_background or style.background
	local color = style.text

	if sx > 0.01 then
		local left_bar_x = text_x
		draw_indicator_bar(dv, left_bar_x, bar_y, bar_w, bar_h, "<", bar_bg, color)
	end

	local right_overflow, marks, minline, maxline = collect_right_indicator_lines(dv, sx, visible_w)
	if right_overflow then
		local right_bar_x = math.floor(right_anchor - right_occlusion - bar_w + 0.5)
		if right_bar_x >= text_x + bar_w then
			draw_sparse_indicator_bar(
				dv,
				right_bar_x,
				bar_y,
				bar_w,
				bar_h,
				">",
				bar_bg,
				color,
				marks,
				minline,
				maxline
			)
		end
	end
end

function DocView:draw(...)
	_original_draw(self, ...)
	draw_indicators(self)
end

function TextViewer:draw(...)
	_original_viewer_draw(self, ...)
	draw_indicators(self)
end
