---@diagnostic disable: duplicate-set-field,need-check-nil
-- lx-bundle:true

--[[
Outlinedividers plugin

What this plugin does:
- Draws thin horizontal "section header" markers on lines that match the active
  syntax's 'outline_patterns'.
- Each marker starts after the rendered line text and extends rightward.
- Marker length is constrained so it does not overlap right-side UI overlays:
  it stops before scrollbar/minimap space, and (when configured) before the
  vertical ruler position ('config.line_limit').

How outline lines are resolved:
- Uses syntax-provided 'outline_patterns' (same source used by outline commands).
- Uses line-local outline matching so visible-line redraws do not trigger
  full-document outline rescans after every edit

Performance model:
- Outline matching and caching reuse core.text.outliner shared logic
- Divider rendering only queries the current visible line state
- No arbitrary long-line cutoffs are applied by this plugin
]]

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

-- New config keys
config.outlinedividers_enabled = config.outlinedividers_enabled
if config.outlinedividers_enabled == nil then
	config.outlinedividers_enabled = true
end

-- Pixels between last text character and separator start
config.outlinedividers_left_gap = config.outlinedividers_left_gap or 16

-- Pixels between separator end and right-side minimap/scrollbar area
config.outlinedividers_right_gap = config.outlinedividers_right_gap or 16

-- Extra pixels before the right guide line
config.outlinedividers_guide_padding = config.outlinedividers_guide_padding or 8

-- Compares two paths after normalization (slashes, trailing slash, drive case)
local function same_path(a, b)
	if not (a and b) then
		return false
	end
	local pa = tostring(system.absolute_path(a) or a):gsub("\\", "/"):gsub("/+", "/")
	local pb = tostring(system.absolute_path(b) or b):gsub("\\", "/"):gsub("/+", "/")
	if #pa > 1 then
		pa = pa:gsub("/$", "")
	end
	if #pb > 1 then
		pb = pb:gsub("/$", "")
	end
	if pa:match("^%a:/") then
		pa = pa:lower()
	end
	if pb:match("^%a:/") then
		pb = pb:lower()
	end
	return pa == pb
end

-- Returns true when minimap is active and visible for this doc view
local function minimap_visible_for_view(dv)
	if not package.loaded["plugins.minimap"] then
		return false
	end
	if config.minimap_enabled == false then
		return false
	end
	local min_lines = config.minimap_min_file_lines or 30
	if not (dv and dv.doc and dv.doc.lines and #dv.doc.lines >= min_lines) then
		return false
	end
	-- In diff mode minimap is shown only on the right side
	local ctx = core.diff_context
	if ctx and ctx.enabled and dv.doc and dv.doc.filename and ctx.left then
		if same_path(dv.doc.filename, ctx.left) then
			return false
		end
	end
	return true
end

-- Computes right-side width reserved for scrollbar/minimap overlays
local function get_right_reserved_width(dv)
	local reserved = style.scrollbar_size or 0
	if minimap_visible_for_view(dv) then
		local mw = math.floor((config.minimap_width or 112) * SCALE + 0.5)
		mw = common.clamp(mw, style.scrollbar_size + 2, math.max(style.scrollbar_size + 2, math.floor(dv.size.x * 0.5)))
		reserved = math.max(reserved, mw)
	end
	return reserved
end

local draw_line_overlay = DocView.draw_line_overlay
--- Return the optional divider profile for one view
-- @param view table document view
-- @return table|nil profile active phase counters
local function get_profile(view)
	local profile = view._outlinedividers_profile

	if not profile then
		profile = {
			calls = 0,
			matches = 0,
			drawn = 0,
			geometry = 0,
			match = 0,
			draw = 0,
		}
		view._outlinedividers_profile = profile
	end

	return profile
end

local function resolve_divider_color(level)
	local base = style.outline or style.line_number or style.guide or style.selection
	local n = tonumber(level)
	if n and n > 0 then
		n = math.floor(n)
		local level_color = style["outline" .. tostring(n)]
		if level_color then
			return level_color
		end
	end
	return base
end

--- Return the final visible-row x position after one logical line's text
-- @param view table document view
-- @param idx number logical line index
-- @param len number logical text length
-- @param first_row number first visible visual row
-- @param last_row number last visible visual row
-- @param x number text origin x
-- @return number|nil text_end_x text end screen position
-- @return number|nil marker_row final displayed row
local function get_text_end_position(view, idx, len, first_row, last_row, x)
	local line_width = view:get_col_x_offset(idx, len + 1)

	if not view:is_wrap_enabled() then
		return x + line_width, idx
	end

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

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

	return x + row_info.indent_x + line_width - row_info.start_x, marker_row
end

--- Return divider geometry for one visible logical line
-- @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
-- @return number|nil x1 divider left edge
-- @return number|nil x2 divider right edge
-- @return number|nil marker_row displayed row
-- @return number|nil line_height displayed line height
local function get_divider_geometry(view, idx, first_row, last_row, x)
	local lh = view:get_line_height()
	local left_gap = math.max(0, math.floor((config.outlinedividers_left_gap or 16) * SCALE + 0.5))
	local right_gap = math.max(0, math.floor((config.outlinedividers_right_gap or 16) * SCALE + 0.5))
	local x2 = view.position.x + view.size.x - get_right_reserved_width(view) - right_gap
	-- Stop before the vertical guide line when available
	local guide_pad = math.max(0, math.floor((config.outlinedividers_guide_padding or 8) * SCALE + 0.5))
	local guide_x = view:get_line_screen_position(1) + view:get_font():get_width("n") * config.line_limit
	x2 = math.min(x2, guide_x - guide_pad)

	-- Lines without drawable marker room do not need text measurement
	if x2 - left_gap <= x then
		return nil
	end

	local text = view.doc.lines[idx] or ""
	local len = #text

	if text:sub(-1) == "\n" then
		len = len - 1
	end

	local text_end_x, marker_row = get_text_end_position(view, idx, len, first_row, last_row, x)

	if not text_end_x then
		return nil
	end

	local x1 = text_end_x + left_gap

	if x1 >= x2 then
		return nil
	end

	return x1, x2, marker_row, lh
end

--- Draw outline dividers 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 config.outlinedividers_enabled and self.doc and self.doc.format then
		-- Formats without outline rules have no possible divider lines
		local outline_patterns = self.doc.format.outline_patterns
		if type(outline_patterns) ~= "table" or #outline_patterns == 0 then
			return
		end

		-- Keep timing out of the normal path unless the benchmark explicitly enables it
		local profile = config.text_wrap_profile_enabled == true and get_profile(self) or nil
		if profile then
			profile.calls = profile.calls + 1
		end

		-- Reject ordinary lines before measuring text or resolving wrapped-row geometry
		local match_started_at = profile and system.get_time() or nil
		local outliner = Outliner.for_doc(self.doc)
		local is_outline_line, outline_level = outliner:match_line("divider", idx)
		if profile then
			profile.match = profile.match + system.get_time() - match_started_at
		end
		if not is_outline_line then
			return
		end
		if profile then
			profile.matches = profile.matches + 1
		end

		local geometry_started_at = profile and system.get_time() or nil
		local x1, x2, marker_row, lh = get_divider_geometry(self, idx, first_row, last_row, x)
		if profile then
			profile.geometry = profile.geometry + system.get_time() - geometry_started_at
		end
		if not x1 then
			return
		end

		local draw_started_at = profile and system.get_time() or nil
		local w = math.floor(x2 - x1)
		if w <= 0 then
			if profile then
				profile.draw = profile.draw + system.get_time() - draw_started_at
			end
			return
		end
		local h = math.max(1, math.floor(SCALE + 0.5))
		local color = resolve_divider_color(outline_level)
		local y_mid = y + (marker_row - first_row) * lh + math.floor(lh / 2)
		renderer.draw_rect(x1, y_mid, w, h, color)
		if profile then
			profile.draw = profile.draw + system.get_time() - draw_started_at
			profile.drawn = profile.drawn + 1
		end
	end
end
