---@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 end-of-line markers without rescanning unchanged lines every frame
-- @module plugins.eolmarkers

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

if config.highlight_eol == nil then
	config.highlight_eol = "trailing"
end

local draw_line_overlay = DocView.draw_line_overlay

--- 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()
	local indent_size = view.doc:get_indent_size()
	if cache.font ~= font or cache.indent_size ~= indent_size then
		cache.font = font
		cache.indent_size = 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

--- Resolve the visible-row position for one logical line ending
-- @param view table document view
-- @param idx number logical line index
-- @param first_row number first visible visual row
-- @param last_row number last visible visual row
-- @param x number text origin x
-- @param y number first visible row y
-- @param info table cached line information
-- @param cache table document EOL cache
-- @return number|nil x_end marker x position
-- @return number|nil row_y marker row y position
local function get_marker_position(view, idx, first_row, last_row, x, y, info, cache)
	local line_end_x = get_line_end_x(view, idx, info, cache)

	if not view:is_wrap_enabled() then
		return x + line_end_x, y
	end

	local marker_row, row_info = view:get_visual_row_for_position(idx, info.len + 1, true)

	if marker_row < first_row or marker_row > last_row then
		return nil, nil
	end

	local x_end = x + row_info.indent_x + line_end_x - row_info.start_x
	local row_y = y + (marker_row - first_row) * view:get_line_height()

	return x_end, row_y
end

--- Draw one dashed EOL marker at a visual-row position
-- @param x_end number marker x position
-- @param row_y number visual row y position
-- @param line_height number visual row height
local function draw_eol_marker(x_end, row_y, line_height)
	local y_top = row_y + 1 * SCALE
	local height = line_height - 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
end

--- Draw cached EOL markers through the shared visual-row overlay phase
-- @param idx number logical line index
-- @param first_row number first visible visual row
-- @param last_row number last visible visual row
-- @param x number text origin x
-- @param y number first visible row y
function DocView:draw_line_overlay(idx, first_row, last_row, x, y)
	draw_line_overlay(self, idx, first_row, last_row, 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

	if config.highlight_eol ~= "all" and not info.has_trailing_ws then
		return
	end

	local x_end, row_y = get_marker_position(self, idx, first_row, last_row, x, y, info, cache)

	if not x_end then
		return
	end

	local line_height = self:get_line_height()
	draw_eol_marker(x_end, row_y, line_height)
end
