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

--- Draw end-of-line markers without rescanning unchanged lines every frame
-- @module plugins.eolmarkers

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

config.highlight_eol = config.highlight_eol or "trailing"

local blink_period = 0.8
local draw_line_body = DocView.draw_line_body

--- Return the current EOL marker cache for one document
-- Cache line analysis per change so redraws reuse expensive results
-- @param doc table document
-- @return table cache
local function get_eol_cache(doc)
	local change_id = (doc.get_change_id and doc:get_change_id()) or 0
	local cache = rawget(doc, "_eolmarkers_cache")
	if cache and cache.change_id == change_id then
		return cache
	end

	cache = {
		change_id = change_id,
		lines = {},
	}

	doc._eolmarkers_cache = cache

	return cache
end

--- Return cached line analysis for EOL marker decisions
-- @param text string line text
-- @param idx number line index
-- @param cache table EOL cache
-- @return table info
local function get_line_info(text, idx, cache)
	local cached = cache.lines[idx]
	if cached then
		return cached
	end

	local len = #text
	if len > 0 and text:byte(len) == 10 then
		len = len - 1
	end

	local has_trailing_ws = false
	local scan = len
	while scan > 0 do
		local byte = text:byte(scan)
		if byte == 32 or byte == 9 then
			has_trailing_ws = true
			scan = scan - 1
		else
			break
		end
	end

	local info = {
		len = len,
		has_trailing_ws = has_trailing_ws,
		x_end = nil,
	}

	cache.lines[idx] = info

	return info
end

--- Return cached line-end x position for one line
-- Width depends on the active view font, so reset when that changes
-- @param view table doc view
-- @param idx number line index
-- @param info table line info
-- @param cache table EOL cache
-- @return number x_end
local function get_line_end_x(view, idx, info, cache)
	local font = view:get_font()
	if cache.font ~= font or cache.indent_size ~= config.indent_size then
		cache.font = font
		cache.indent_size = config.indent_size
		for _, line_info in pairs(cache.lines) do
			line_info.x_end = nil
		end
	end

	if info.x_end == nil then
		info.x_end = view:get_col_x_offset(idx, info.len + 1)
	end

	return info.x_end
end

function DocView:draw_line_body(idx, x, y)
	draw_line_body(self, idx, x, y)

	if not config.highlight_eol then
		return
	end

	local text = self.doc.lines[idx]
	if not text then
		return
	end

	local cache = get_eol_cache(self.doc)
	local info = get_line_info(text, idx, cache)

	-- Skip expensive EOL marker placement on very long lines
	if info.len > (config.long_line_threshold or 5000) then
		return
	end

	local should_draw = (config.highlight_eol == "all")
	if not should_draw then
		should_draw = info.has_trailing_ws
	end
	if not should_draw then
		return
	end

	local x_end = x + get_line_end_x(self, idx, info, cache)
	local lh = self:get_line_height()
	local y_top = y + 1 * SCALE
	local height = lh - 2 * SCALE
	local width = math.max(1, common.round(1 * SCALE))
	local dot_h = math.max(1, common.round(1 * SCALE))
	local gap_h = math.max(1, common.round(2 * SCALE))
	local y_dash = y_top
	local y_bottom = y_top + height
	while y_dash < y_bottom do
		local h = math.min(dot_h, y_bottom - y_dash)
		renderer.draw_rect(x_end, y_dash, width, h, style.eol)
		y_dash = y_dash + dot_h + gap_h
	end

	-- Keep cursor visible if it overlaps the marker column
	local line, col = self.doc:get_selection()
	if
		line == idx
		and core.active_view == self
		and self.blink_timer < blink_period / 2
		and system.window_has_focus()
	then
		local x1 = x + self:get_col_x_offset(line, col)
		renderer.draw_rect(x1, y, style.cursor_width, lh, style.cursor)
	end
end
