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

--- Draw indent guides without rescanning large blank runs on every row
-- @module plugins.indentguide

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

--- Return the current indent cache for one document
-- Cache results per change id so large blank-line ranges stay cheap to draw
-- @param doc table document
-- @return table cache
local function get_indent_cache(doc)
	local change_id = (doc.get_change_id and doc:get_change_id()) or 0
	local cache = rawget(doc, "_indentguide_cache")
	if cache and cache.change_id == change_id then
		return cache
	end

	cache = {
		change_id = change_id,
		line_info = {},
		guide_spaces = {},
		prev_spaces = {},
		next_spaces = {},
		active_line = nil,
		active_col = nil,
		active_spaces = nil,
	}

	doc._indentguide_cache = cache

	return cache
end

--- Return cached indent metadata for one line
-- @param doc table document
-- @param idx number line index
-- @param cache table indent cache
-- @return table|nil line info
local function get_line_info(doc, idx, cache)
	local cached = cache.line_info[idx]
	if cached then
		return cached
	end

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

	-- Common case where a visible character starts the line
	local first_byte = text:byte(1)

	if first_byte and first_byte ~= 9 and first_byte ~= 10 and first_byte ~= 13 and first_byte ~= 32 then
		local info = {
			is_blank = false,
			leading_spaces = 0,
		}
		cache.line_info[idx] = info
		return info
	end

	local _, leading_stop = text:find("^%s*")
	local info = {
		is_blank = leading_stop == #text,
		leading_spaces = 0,
	}

	if not info.is_blank then
		for i = 1, leading_stop do
			info.leading_spaces = info.leading_spaces + (text:byte(i) == 9 and config.indent_size or 1)
		end
	end

	cache.line_info[idx] = info

	return info
end

--- Return nearest non-blank indent width from one direction
-- Cache and reuse the resolved width for every blank line in the walked range
-- @param doc table document
-- @param start_idx number line index
-- @param dir number scan direction
-- @param cache table indent cache
-- @return number spaces
local function get_neighbor_spaces(doc, start_idx, dir, cache)
	local store = dir < 0 and cache.prev_spaces or cache.next_spaces
	local visited = {}
	local idx = start_idx
	local spaces = 0

	while true do
		local cached = store[idx]
		if cached ~= nil then
			spaces = cached
			break
		end

		local info = get_line_info(doc, idx, cache)
		if not info then
			spaces = 0
			break
		end

		if not info.is_blank then
			spaces = info.leading_spaces
			store[idx] = spaces
			break
		end

		visited[#visited + 1] = idx
		idx = idx + dir
	end

	for i = 1, #visited do
		store[visited[i]] = spaces
	end

	return spaces
end

--- Return indent guide width for one line
-- Blank lines inherit the stronger indent from the closest text above or below
-- @param doc table document
-- @param idx number line index
-- @param cache table indent cache
-- @return number spaces
local function get_line_indent_guide_spaces(doc, idx, cache)
	local cached = cache.guide_spaces[idx]
	if cached ~= nil then
		return cached
	end

	local info = get_line_info(doc, idx, cache)
	if not info then
		return 0
	end

	local spaces = info.leading_spaces
	if info.is_blank then
		spaces = math.max(
			get_neighbor_spaces(doc, idx - 1, -1, cache),
			get_neighbor_spaces(doc, idx + 1, 1, cache)
		)
	end

	cache.guide_spaces[idx] = spaces

	return spaces
end

--- Convert a document column to visual spaces with tab expansion
-- @param doc table document
-- @param line number line index
-- @param col number column index
-- @return number spaces
local function get_col_spaces(doc, line, col)
	local text = doc.lines[line] or ""
	local limit = math.max(0, math.min(col - 1, #text))
	local spaces = 0

	for i = 1, limit do
		local byte = text:byte(i)
		if byte == 9 then
			local step = config.indent_size - (spaces % config.indent_size)
			spaces = spaces + step
		else
			spaces = spaces + 1
		end
	end

	return spaces
end

--- Return active indent guide width for the current cursor
-- Cache the selection result so each redraw only computes it once
-- @param doc table document
-- @param cache table indent cache
-- @return number|nil spaces
local function get_active_guide_spaces(doc, cache)
	local line, col = doc:get_selection()
	if cache.active_line == line and cache.active_col == col then
		return cache.active_spaces
	end

	local line_spaces = get_line_indent_guide_spaces(doc, line, cache)
	local active_spaces = nil

	if line_spaces > 0 then
		local col_spaces = get_col_spaces(doc, line, col)
		local max_level = math.max(0, line_spaces - config.indent_size)

		-- In text or at end-of-line, keep highlighting the innermost guide
		if col_spaces >= line_spaces then
			active_spaces = max_level
		else
			-- In leading whitespace, follow cursor depth
			local level = math.floor(col_spaces / config.indent_size) * config.indent_size
			active_spaces = math.min(level, max_level)
		end
	end

	cache.active_line = line
	cache.active_col = col
	cache.active_spaces = active_spaces

	return active_spaces
end

local draw_line_text = DocView.draw_line_text

--- Draw indent guides before line text
-- @param idx number line index
-- @param x number screen x
-- @param y number screen y
function DocView:draw_line_text(idx, x, y)
	local cache = get_indent_cache(self.doc)
	local spaces = get_line_indent_guide_spaces(self.doc, idx, cache)

	-- Skip all guide drawing work for lines that cannot show a guide
	if spaces <= 0 then
		draw_line_text(self, idx, x, y)
		return
	end

	-- Only the active pane tracks the highlighted guide depth
	local active_spaces = (self == core.active_view) and get_active_guide_spaces(self.doc, cache) or nil
	local sw = self:get_font():get_width(" ")
	local w = math.ceil(1 * SCALE)
	local h = self:get_line_height()

	-- Shift guides slightly right for better visual centering
	local x_shift = 1

	-- Keep the active guide fallback chain unchanged
	local active_color = style.guide_active
		or style.bracketmatch_color
		or style.syntax["function"]
		or style.guide
		or style.selection

	for i = 0, spaces - 1, config.indent_size do
		local color = (active_spaces and i == active_spaces) and active_color or (style.guide or style.selection)
		renderer.draw_rect(x + sw * i + x_shift, y, w, h, color)
	end

	draw_line_text(self, idx, x, y)
end
