--[[
    Commander 3.0 - Virtual List Renderer

    Renders VirtualBuffer using a pool of FontStrings instead of ScrollingMessageFrame.
    This allows true in-place updates - changing one line doesn't require reprinting all lines.
]]

local ADDON_NAME, C3 = ...

-------------------------------------------------------------------------------
-- Debug Helper
-------------------------------------------------------------------------------

local DEBUG = false  -- Set to true to enable debug output

local function dbg(...)
    if DEBUG then
        print("|cFF00FFFF[VLR Debug]|r", ...)
    end
end

-------------------------------------------------------------------------------
-- Virtual List Renderer Class
-------------------------------------------------------------------------------

local VirtualListRenderer = C3.class()
C3.UI = C3.UI or {}
C3.UI.VirtualListRenderer = VirtualListRenderer

-- Default settings
local DEFAULTS = {
    lineHeight = 14,
    maxVisibleLines = 50,
    padding = 4,
    fontSize = 12,
    fontFlags = "",
}

---Create a new virtual list renderer
---@param parent frame Parent frame
---@param options? table
function VirtualListRenderer:init(parent, options)
    options = options or {}

    dbg("init() called, parent:", parent:GetName() or "unnamed")

    self.parent = parent
    self.buffer = nil

    -- Settings
    self.lineHeight = options.lineHeight or DEFAULTS.lineHeight
    self.maxVisibleLines = options.maxVisibleLines or DEFAULTS.maxVisibleLines
    self.padding = options.padding or DEFAULTS.padding
    self.fontSize = options.fontSize or DEFAULTS.fontSize
    self.fontFlags = options.fontFlags or DEFAULTS.fontFlags
    self.scrollbarAnchor = options.scrollbarAnchor  -- Optional: anchor scrollbar to different frame

    dbg("Settings: lineHeight=", self.lineHeight, "maxVisible=", self.maxVisibleLines, "fontSize=", self.fontSize)

    -- State
    self._visibleCount = 0
    self._scrollOffset = 0
    self._fontStrings = {}
    self._renderPending = false  -- Deferred render flag
    self._renderLines = {}
    self._renderLineCount = 0
    self._renderLinesDirty = true

    -- Create the container frame
    self:_createContainer()

    -- Create FontString pool
    self:_createFontStringPool()

    -- Create scrollbar
    self:_createScrollbar()

    -- Wire up buffer if provided
    if options.buffer then
        dbg("Buffer provided in options, calling SetBuffer")
        self:SetBuffer(options.buffer)
    end

    dbg("init() complete")
end

-------------------------------------------------------------------------------
-- Frame Creation
-------------------------------------------------------------------------------

function VirtualListRenderer:_createContainer()
    dbg("_createContainer() called")

    -- Main container that clips content
    self.container = CreateFrame("Frame", nil, self.parent)
    self.container:SetPoint("TOPLEFT", self.padding, -self.padding)
    self.container:SetPoint("BOTTOMRIGHT", -self.padding - 12, self.padding)
    self.container:SetClipsChildren(true)

    -- Content frame holds the FontStrings
    self.content = CreateFrame("Frame", nil, self.container)
    self.content:SetAllPoints()  -- Fill the container

    -- Enable mouse wheel on container
    self.container:EnableMouseWheel(true)
    self.container:SetScript("OnMouseWheel", function(_, delta)
        self:Scroll(delta > 0 and -3 or 3)
    end)

    -- Track size changes
    self.container:SetScript("OnSizeChanged", function(_, width, height)
        dbg("OnSizeChanged: width=", width, "height=", height)
        self:_onSizeChanged(width, height)
    end)

    -- Create hyperlink click handler
    self:_createHyperlinkHandler()

    dbg("_createContainer() complete")
end

function VirtualListRenderer:_createHyperlinkHandler()
    -- Invisible button overlay for click detection
    self.clickOverlay = CreateFrame("Button", nil, self.container)
    self.clickOverlay:SetAllPoints()
    self.clickOverlay:SetFrameLevel(self.container:GetFrameLevel() + 10)

    -- Track hover state for tooltips
    self._hoverLink = nil
    self._hoverText = nil

    -- Click handler
    self.clickOverlay:SetScript("OnClick", function(_, button)
        -- Right-click: fire context menu callback
        if button == "RightButton" then
            local line = self:_getLineAtCursor()
            if line and self.onContextMenu then
                local x, y = GetCursorPosition()
                self.onContextMenu(line, x, y)
            end
            -- Still request focus on right-click
            if self.onFocusRequested then
                self.onFocusRequested()
            end
            return
        end

        -- Left-click: handle hyperlinks
        local link, text = self:_getHyperlinkAtCursor()
        if link then
            self:_handleHyperlinkClick(link, text, button)
        end

        -- Always request focus on any click (even if hyperlink was clicked)
        if self.onFocusRequested then
            self.onFocusRequested()
        end
    end)

    -- Hover handlers for tooltips
    self.clickOverlay:SetScript("OnEnter", function()
        self.clickOverlay:SetScript("OnUpdate", function()
            self:_updateHyperlinkHover()
        end)
    end)

    self.clickOverlay:SetScript("OnLeave", function()
        self.clickOverlay:SetScript("OnUpdate", nil)
        if self._hoverLink then
            self:_handleHyperlinkLeave()
            self._hoverLink = nil
            self._hoverText = nil
        end
    end)

    -- Enable mouse
    self.clickOverlay:EnableMouse(true)
    self.clickOverlay:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    self.clickOverlay:RegisterForDrag("LeftButton")

    -- Forward drag events to the terminal window via callbacks
    -- This allows the terminal window to use DragController for merge detection
    self.clickOverlay:SetScript("OnDragStart", function()
        -- Bring window to front on drag start
        if self.onFocusRequested then
            self.onFocusRequested()
        end
        -- Use callback if set (allows DragController integration)
        if self.onDragStart then
            self.onDragStart()
        else
            -- Fallback: walk up parent chain to find the movable frame
            local frame = self.container:GetParent()
            while frame and frame ~= UIParent do
                if frame:IsMovable() then
                    frame:StartMoving()
                    return
                end
                frame = frame:GetParent()
            end
        end
    end)
    self.clickOverlay:SetScript("OnDragStop", function()
        -- Use callback if set
        if self.onDragStop then
            self.onDragStop()
        else
            -- Fallback: walk up parent chain
            local frame = self.container:GetParent()
            while frame and frame ~= UIParent do
                if frame:IsMovable() then
                    frame:StopMovingOrSizing()
                    if self.onFocusRequested then
                        self.onFocusRequested()
                    end
                    return
                end
                frame = frame:GetParent()
            end
        end
    end)

    -- Pass mouse wheel through to container for scrolling
    self.clickOverlay:EnableMouseWheel(true)
    self.clickOverlay:SetScript("OnMouseWheel", function(_, delta)
        self:Scroll(delta > 0 and -3 or 3)
    end)
end

function VirtualListRenderer:_getLineIndexAtY(y)
    -- For bottom-up: y is relative to container
    -- y=0 at bottom of container, negative going up
    -- lineIndex=1 is the bottommost line
    local containerHeight = self.container:GetHeight() or 0
    -- Convert y from top-relative to bottom-relative
    local yFromBottom = containerHeight + y  -- y is negative from top, so this gives distance from bottom
    local lineIndex = math.floor(yFromBottom / self.lineHeight) + 1
    if lineIndex < 1 or lineIndex > self._visibleCount then
        return nil
    end
    return lineIndex
end

---Get the BufferLine at the current cursor position
---@return table|nil line The BufferLine object or nil
function VirtualListRenderer:_getLineAtCursor()
    local x, y = GetCursorPosition()
    local scale = self.container:GetEffectiveScale()
    x, y = x / scale, y / scale

    local left, bottom = self.container:GetLeft(), self.container:GetBottom()
    if not left or not bottom then return nil end

    local relY = y - bottom - self.container:GetHeight()  -- y from top
    local lineIndex = self:_getLineIndexAtY(relY)

    if not lineIndex then return nil end

    local entry = self._fontStrings[lineIndex]
    if not entry or not entry.visible or not entry.lineId then return nil end

    return self.buffer:GetLine(entry.lineId)
end

function VirtualListRenderer:_getHyperlinkAtCursor()
    local x, y = GetCursorPosition()
    local scale = self.container:GetEffectiveScale()
    x, y = x / scale, y / scale

    -- Convert to container-relative coordinates
    local left, bottom = self.container:GetLeft(), self.container:GetBottom()
    if not left or not bottom then return nil, nil end

    local relX = x - left
    local relY = y - bottom - self.container:GetHeight()  -- y from top

    -- Get which line
    local lineIndex = self:_getLineIndexAtY(relY)
    if not lineIndex then return nil, nil end

    local entry = self._fontStrings[lineIndex]
    if not entry or not entry.visible then return nil, nil end

    -- Get the line's text
    local text = entry.fontString:GetText()
    if not text then return nil, nil end

    -- Calculate link positions with X coordinates
    local linkPositions = self:_calculateLinkPositions(text)
    if #linkPositions == 0 then return nil, nil end

    -- Find which link (if any) contains cursor X position
    for _, lp in ipairs(linkPositions) do
        if relX >= lp.startX and relX <= lp.endX then
            return lp.link, lp.text
        end
    end

    -- Cursor is on the line but not over any hyperlink
    return nil, nil
end

function VirtualListRenderer:_calculateLinkPositions(text)
    local positions = {}
    local fs = self._measureFontString

    -- Find all hyperlinks with their positions in the raw string
    local searchStart = 1
    while true do
        -- Find next hyperlink: |Htype:data|hDisplayText|h
        local hStart = text:find("|H", searchStart, true)
        if not hStart then break end

        local hEnd = text:find("|h", hStart + 2, true)
        if not hEnd then break end

        local link = text:sub(hStart + 2, hEnd - 1)
        local displayStart = hEnd + 2
        local displayEnd = text:find("|h", displayStart, true)
        if not displayEnd then break end

        local displayText = text:sub(displayStart, displayEnd - 1)

        -- Measure visual width of everything before this link
        local beforeText = text:sub(1, hStart - 1)
        fs:SetText(beforeText)
        local startX = fs:GetStringWidth()

        -- Measure visual width of the link's display text
        fs:SetText(displayText)
        local linkWidth = fs:GetStringWidth()

        positions[#positions + 1] = {
            startX = startX,
            endX = startX + linkWidth,
            link = link,
            text = displayText,
        }

        searchStart = displayEnd + 2
    end

    return positions
end

function VirtualListRenderer:_updateHyperlinkHover()
    local link, text = self:_getHyperlinkAtCursor()

    if link ~= self._hoverLink then
        -- Link changed
        if self._hoverLink then
            self:_handleHyperlinkLeave()
        end

        self._hoverLink = link
        self._hoverText = text

        if link then
            self:_handleHyperlinkEnter(link, text)
        end
    end
end

function VirtualListRenderer:_handleHyperlinkClick(link, text, button)
    local linkType = link:match("^(%w+):")

    if linkType == "item" then
        if IsShiftKeyDown() then
            -- Insert into chat
            ChatEdit_InsertLink("|H" .. link .. "|h" .. text .. "|h")
        elseif IsControlKeyDown() then
            -- Dress up
            DressUpItemLink(link)
        else
            -- Default: show tooltip (SetItemRef handles this)
            SetItemRef(link, text, button)
        end

    elseif linkType == "spell" then
        if IsShiftKeyDown() then
            ChatEdit_InsertLink("|H" .. link .. "|h" .. text .. "|h")
        else
            SetItemRef(link, text, button)
        end

    elseif linkType == "achievement" or linkType == "quest" or linkType == "currency" then
        SetItemRef(link, text, button)

    elseif linkType == "unit" or linkType == "player" then
        SetItemRef(link, text, button)

    elseif linkType == "c3cmd" then
        -- Commander internal command link
        local cmd = link:match("^c3cmd:(.+)$")
        if cmd and C3.Execute then
            C3:Execute(cmd)
        end
    else
        -- Default handler
        SetItemRef(link, text, button)
    end

    -- Fire callback if set
    if self.onHyperlinkClick then
        self.onHyperlinkClick(link, text, button)
    end
end

function VirtualListRenderer:_handleHyperlinkEnter(link, text)
    local linkType = link:match("^(%w+):")

    if linkType == "item" or linkType == "spell" or linkType == "achievement"
        or linkType == "quest" or linkType == "currency" or linkType == "enchant" then
        GameTooltip:SetOwner(self.clickOverlay, "ANCHOR_CURSOR")
        GameTooltip:SetHyperlink(link)
        GameTooltip:Show()
    elseif linkType == "unit" or linkType == "player" then
        GameTooltip:SetOwner(self.clickOverlay, "ANCHOR_CURSOR")
        GameTooltip:SetHyperlink(link)
        GameTooltip:Show()
    end

    -- Fire callback if set
    if self.onHyperlinkEnter then
        self.onHyperlinkEnter(link, text)
    end
end

function VirtualListRenderer:_handleHyperlinkLeave()
    GameTooltip:Hide()

    -- Fire callback if set
    if self.onHyperlinkLeave then
        self.onHyperlinkLeave()
    end
end

function VirtualListRenderer:_createFontStringPool()
    dbg("_createFontStringPool() creating", self.maxVisibleLines, "FontStrings")

    -- Use theme font, fallback to WoW built-in
    local themeFont = C3.UI.Theme and C3.UI.Theme.Font or "Fonts\\FRIZQT__.TTF"
    self._fontPath = themeFont

    -- Get theme text color (content.fg), convert from hex to RGB
    local textColorHex = C3.UI.Theme and C3.UI.Theme:GetColor("content.fg") or "D8DEE9"
    local r, g, b = 0.85, 0.87, 0.91  -- Default nord4
    if type(textColorHex) == "string" and #textColorHex >= 6 then
        r = tonumber(textColorHex:sub(1, 2), 16) / 255
        g = tonumber(textColorHex:sub(3, 4), 16) / 255
        b = tonumber(textColorHex:sub(5, 6), 16) / 255
    end

    dbg("Using theme font:", themeFont, "text color:", textColorHex)

    for i = 1, self.maxVisibleLines do
        local fs = self.content:CreateFontString(nil, "OVERLAY", "GameFontNormal")

        -- Apply theme font
        fs:SetFont(themeFont, self.fontSize, self.fontFlags)
        fs:SetShadowOffset(0, 0)

        -- Apply theme text color (overrides GameFontNormal's yellow)
        fs:SetTextColor(r, g, b)

        fs:SetJustifyH("LEFT")
        fs:SetJustifyV("BOTTOM")
        fs:SetWordWrap(false)

        -- Position each FontString from BOTTOM going UP
        -- i=1 is the bottommost line (most recent), higher i goes upward
        local yOffset = (i - 1) * self.lineHeight
        fs:SetPoint("BOTTOMLEFT", self.content, "BOTTOMLEFT", 0, yOffset)
        fs:SetPoint("RIGHT", self.content, "RIGHT", 0, 0)
        fs:SetHeight(self.lineHeight)

        -- Start hidden
        fs:Hide()

        self._fontStrings[i] = {
            fontString = fs,
            lineId = nil,
            visible = false,
        }

        -- Debug: show first few
        if i <= 3 then
            dbg("FontString", i, "created, yOffset=", yOffset, "(from bottom)")
        end
    end

    -- Hidden FontString for measuring text widths (hyperlink hit-testing)
    self._measureFontString = self.content:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    self._measureFontString:SetFont(themeFont, self.fontSize, self.fontFlags)
    self._measureFontString:Hide()

    dbg("_createFontStringPool() complete, created", #self._fontStrings, "FontStrings")
end

function VirtualListRenderer:_createScrollbar()
    -- Scrollbar dimensions
    local SCROLLBAR_WIDTH_THIN = 4      -- Default thin width
    local SCROLLBAR_WIDTH_HOVER = 10    -- Expanded width on hover
    local THUMB_MIN_HEIGHT = 20         -- Minimum thumb height

    -- Use scrollbarAnchor if provided (for consistent position regardless of content shifts)
    local scrollbarParent = self.scrollbarAnchor or self.parent
    self.scrollbar = CreateFrame("Slider", nil, scrollbarParent)

    if self.scrollbarAnchor then
        -- Anchored to main window frame - use fixed offsets for consistency
        -- Top: -30 clears tabs area, Bottom: +34 clears input area
        local Theme = C3.UI.Theme
        local borderSize = Theme and Theme.dimensions and Theme.dimensions.borderSize or 1
        local xInset = -borderSize
        local bottomOffset = Theme and (Theme:GetEffectiveBottomReserve() + 8) or 34
        self.scrollbar:SetPoint("TOPRIGHT", xInset, -30)
        self.scrollbar:SetPoint("BOTTOMRIGHT", xInset, bottomOffset)
    else
        -- Default: anchor to parent with padding
        local Theme = C3.UI.Theme
        local borderSize = Theme and Theme.dimensions and Theme.dimensions.borderSize or 1
        local xInset = -borderSize
        self.scrollbar:SetPoint("TOPRIGHT", xInset, -self.padding)
        self.scrollbar:SetPoint("BOTTOMRIGHT", xInset, self.padding)
    end
    self.scrollbar:SetWidth(SCROLLBAR_WIDTH_THIN)
    self.scrollbar:SetOrientation("VERTICAL")

    -- Track (subtle, almost invisible when thin)
    local track = self.scrollbar:CreateTexture(nil, "BACKGROUND")
    track:SetAllPoints()
    track:SetColorTexture(0.15, 0.17, 0.22, 0.2)
    self.scrollbar._track = track

    -- Thumb (the draggable part)
    local thumb = self.scrollbar:CreateTexture(nil, "ARTWORK")
    thumb:SetColorTexture(0.45, 0.55, 0.70, 0.6)
    self.scrollbar:SetThumbTexture(thumb)
    self.scrollbar._thumb = thumb

    -- Store dimensions for hover expansion
    self.scrollbar._widthThin = SCROLLBAR_WIDTH_THIN
    self.scrollbar._widthHover = SCROLLBAR_WIDTH_HOVER
    self.scrollbar._thumbMinHeight = THUMB_MIN_HEIGHT
    self.scrollbar._isHovered = false

    -- Helper to update thumb size based on content
    local function updateThumbSize()
        local height = self.scrollbar:GetHeight() or 100
        local totalLines = self:_getRenderLineCount()
        local visibleRatio = totalLines > 0 and (self._visibleCount / totalLines) or 1
        visibleRatio = math.min(visibleRatio, 1)

        local thumbHeight = math.max(THUMB_MIN_HEIGHT, height * visibleRatio)
        local currentWidth = self.scrollbar:GetWidth()
        thumb:SetSize(currentWidth, thumbHeight)
    end
    self.scrollbar._updateThumbSize = updateThumbSize

    -- Hover expansion
    local function expandScrollbar()
        self.scrollbar._isHovered = true
        self.scrollbar:SetWidth(SCROLLBAR_WIDTH_HOVER)
        track:SetColorTexture(0.15, 0.17, 0.22, 0.4)
        thumb:SetColorTexture(0.55, 0.65, 0.80, 0.85)
        updateThumbSize()
    end

    local function shrinkScrollbar()
        self.scrollbar._isHovered = false
        self.scrollbar:SetWidth(SCROLLBAR_WIDTH_THIN)
        track:SetColorTexture(0.15, 0.17, 0.22, 0.2)
        thumb:SetColorTexture(0.45, 0.55, 0.70, 0.6)
        updateThumbSize()
    end

    self.scrollbar:SetScript("OnEnter", expandScrollbar)
    self.scrollbar:SetScript("OnLeave", function()
        -- Only shrink if not actively dragging
        if not self.scrollbar._isDragging then
            shrinkScrollbar()
        end
    end)

    -- Track dragging state to keep expanded while dragging
    self.scrollbar:HookScript("OnMouseDown", function()
        self.scrollbar._isDragging = true
        expandScrollbar()
    end)

    self.scrollbar:HookScript("OnMouseUp", function()
        self.scrollbar._isDragging = false
        if not self.scrollbar:IsMouseOver() then
            shrinkScrollbar()
        end
    end)

    -- Value change handler
    self.scrollbar:SetMinMaxValues(0, 1)
    self.scrollbar:SetValue(0)
    self.scrollbar:SetValueStep(0.01)

    self.scrollbar:SetScript("OnValueChanged", function(_, value, userInput)
        if userInput and self.buffer then
            local totalLines = self:_getRenderLineCount()
            local maxOffset = math.max(0, totalLines - self._visibleCount)
            -- For bottom-up: invert the value (value=1 at bottom means scrollOffset=0)
            local newOffset = math.floor((1 - value) * maxOffset + 0.5)
            if newOffset ~= self._scrollOffset then
                self._scrollOffset = newOffset
                self:Render()
            end
        end
    end)

    -- Initial thumb size
    updateThumbSize()

    self.scrollbar:Hide()
end

-------------------------------------------------------------------------------
-- Size Management
-------------------------------------------------------------------------------

function VirtualListRenderer:_onSizeChanged(width, height)
    local newVisibleCount = math.floor(height / self.lineHeight)
    newVisibleCount = math.min(newVisibleCount, self.maxVisibleLines)
    newVisibleCount = math.max(newVisibleCount, 1)  -- At least 1

    dbg("_onSizeChanged: height=", height, "lineHeight=", self.lineHeight, "newVisibleCount=", newVisibleCount)

    if newVisibleCount ~= self._visibleCount then
        self._visibleCount = newVisibleCount
        self:Render()
    end

    self:_updateScrollbar()
end

function VirtualListRenderer:_updateScrollbar()
    if not self.buffer then
        self.scrollbar:Hide()
        return
    end

    local totalLines = self:_getRenderLineCount()

    if totalLines <= self._visibleCount then
        self.scrollbar:Hide()
        return
    end

    self.scrollbar:Show()

    -- Update thumb size to reflect content ratio
    if self.scrollbar._updateThumbSize then
        self.scrollbar._updateThumbSize()
    end

    local maxOffset = math.max(0, totalLines - self._visibleCount)
    if self._scrollOffset > maxOffset then
        self._scrollOffset = maxOffset
    end
    -- For bottom-up: invert the scrollbar (0 at bottom, 1 at top)
    -- scrollOffset=0 means at bottom (newest), so scrollbar should be at bottom (value=1)
    local value = maxOffset > 0 and (1 - self._scrollOffset / maxOffset) or 1
    self.scrollbar:SetValue(value)
end

-------------------------------------------------------------------------------
-- Buffer Binding
-------------------------------------------------------------------------------

function VirtualListRenderer:SetBuffer(buffer)
    dbg("SetBuffer() called, buffer has", buffer:GetLineCount(), "lines, buffer=", tostring(buffer))

    self.buffer = buffer
    self._renderLinesDirty = true

    -- Wire up callbacks
    buffer.onLineAdded = function(line)
        self:_onLineAdded(line)
    end

    buffer.onLineUpdated = function(line)
        self:_onLineUpdated(line)
    end

    buffer.onLineRemoved = function(line)
        self:_onLineRemoved(line)
    end

    buffer.onBufferChanged = function()
        dbg("onBufferChanged callback")
        self._renderLinesDirty = true
        self:Render()
    end

    -- Force initial render
    C_Timer.After(0, function()
        dbg("Delayed initial render")
        self:Render()
    end)
end

function VirtualListRenderer:GetBuffer()
    return self.buffer
end

-------------------------------------------------------------------------------
-- Rendering
-------------------------------------------------------------------------------

local function splitLines(text)
    local lines = {}
    for line in string.gmatch((text or "") .. "\n", "([^\n]*)\n") do
        lines[#lines + 1] = line
    end
    return lines
end

function VirtualListRenderer:_getLineText(line)
    if line.content then
        if type(line.content) == "table" and line.content._type then
            return C3.Formatter and C3.Formatter:Format(line.content) or tostring(line.content)
        end
        return line:GetText()
    end
    return line:GetText()
end

function VirtualListRenderer:_applyColor(text, color)
    if not color then
        return text
    end
    if type(color) == "string" then
        return string.format("|cFF%s%s|r", color, text)
    end
    if type(color) == "table" then
        local r = math.floor((color.r or 1) * 255)
        local g = math.floor((color.g or 1) * 255)
        local b = math.floor((color.b or 1) * 255)
        return string.format("|cFF%02X%02X%02X%s|r", r, g, b, text)
    end
    return text
end

function VirtualListRenderer:_getLineRenderParts(line)
    local cache = line._renderCache
    if cache and cache.modified == line.modified then
        return cache.parts
    end

    local text = self:_getLineText(line)
    local parts = splitLines(text)

    if line.indent and line.indent > 0 then
        local prefix = string.rep("  ", line.indent)
        for i, part in ipairs(parts) do
            parts[i] = prefix .. part
        end
    end

    if line.color then
        for i, part in ipairs(parts) do
            parts[i] = self:_applyColor(part, line.color)
        end
    end

    line._renderCache = {
        modified = line.modified,
        parts = parts,
    }

    return parts
end

function VirtualListRenderer:_buildRenderLines()
    local renderLines = {}
    local lines = (self.buffer and self.buffer.lines) or {}

    for _, line in ipairs(lines) do
        if line:IsVisible() then
            local parts = self:_getLineRenderParts(line)
            for _, part in ipairs(parts) do
                renderLines[#renderLines + 1] = {
                    line = line,
                    lineId = line.id,
                    text = part,
                    visible = true,
                }
            end
        else
            -- Preserve scroll position by keeping a hidden slot.
            renderLines[#renderLines + 1] = {
                line = line,
                lineId = line.id,
                text = "",
                visible = false,
            }
        end
    end

    self._renderLines = renderLines
    self._renderLineCount = #renderLines
    self._renderLinesDirty = false
end

function VirtualListRenderer:_ensureRenderLines()
    if self._renderLinesDirty then
        self:_buildRenderLines()
    end
end

function VirtualListRenderer:_getRenderLineCount()
    self:_ensureRenderLines()
    return self._renderLineCount or 0
end

function VirtualListRenderer:Render()
    if not self.buffer then
        dbg("Render() - no buffer, skipping")
        return
    end

    self:_ensureRenderLines()

    local renderLines = self._renderLines
    local totalLines = self._renderLineCount or 0

    dbg("Render() buffer check: totalLines=", totalLines, "buffer=", tostring(self.buffer))

    -- Get container size if visibleCount not set
    if self._visibleCount == 0 then
        local height = self.container:GetHeight() or 0
        dbg("Render() - visibleCount is 0, container height=", height)
        if height > 0 then
            self._visibleCount = math.floor(height / self.lineHeight)
            self._visibleCount = math.min(self._visibleCount, self.maxVisibleLines)
            self._visibleCount = math.max(self._visibleCount, 1)
        else
            self._visibleCount = 20  -- Default fallback
            dbg("Render() - using fallback visibleCount=", self._visibleCount)
        end
    end

    -- Bottom-up rendering: show most recent lines at bottom
    -- scrollOffset=0 means we're at the bottom (showing latest lines)
    local maxOffset = math.max(0, totalLines - self._visibleCount)
    if self._scrollOffset > maxOffset then
        self._scrollOffset = maxOffset
    end

    -- Calculate which render lines to show
    local endIndex = totalLines - self._scrollOffset  -- Last line to show (most recent visible)
    local startIndex = math.max(1, endIndex - self._visibleCount + 1)  -- First line to show

    dbg("Render(): totalLines=", totalLines, "visibleCount=", self._visibleCount, "startIndex=", startIndex, "endIndex=", endIndex)

    -- Hide all FontStrings first
    for i = 1, self.maxVisibleLines do
        local entry = self._fontStrings[i]
        if entry then
            entry.fontString:Hide()
            entry.fontString:SetText("")
            entry.lineId = nil
            entry.visible = false
        end
    end

    -- Assign visible lines (bottom-up: fsIndex=1 is bottommost/most recent)
    local linesRendered = 0

    -- Iterate from endIndex down to startIndex, placing into FontStrings from bottom (1) up
    for i = endIndex, startIndex, -1 do
        local lineEntry = renderLines[i]
        if lineEntry and lineEntry.visible then
            linesRendered = linesRendered + 1
            local entry = self._fontStrings[linesRendered]
            if entry then
                entry.fontString:SetText(lineEntry.text or "")
                entry.fontString:Show()
                entry.lineId = lineEntry.lineId
                entry.visible = true

                if linesRendered <= 3 then
                    dbg("Rendered line", linesRendered, "(render idx", i, ") text=", string.sub(lineEntry.text or "", 1, 50))
                end

                if linesRendered >= self._visibleCount then break end
            end
        end
    end

    dbg("Render() complete, rendered", linesRendered, "lines (bottom-up)")

    self:_updateScrollbar()
end

function VirtualListRenderer:_renderLine(line)
    local parts = self:_getLineRenderParts(line)
    return parts[1] or ""
end

function VirtualListRenderer:UpdateLine(lineId)
    if not self.buffer or not self.buffer:GetLine(lineId) then
        return false
    end

    self._renderLinesDirty = true
    self:_scheduleRender()
    return true
end

-------------------------------------------------------------------------------
-- Buffer Event Handlers
-------------------------------------------------------------------------------

function VirtualListRenderer:_onLineAdded(line)
    dbg("_onLineAdded:", line.id, "content=", string.sub(tostring(line.content or ""), 1, 30))
    self._renderLinesDirty = true

    -- For bottom-up rendering, scrollOffset=0 means we're at the bottom (most recent)
    -- Keep scrollOffset at 0 (showing latest lines) unless user has scrolled up
    local wasAtBottom = self._scrollOffset == 0
    if wasAtBottom then
        -- Stay at bottom - new content appears automatically
        self._scrollOffset = 0
    end
    -- If user scrolled up, don't auto-scroll (they're reading history)

    -- Use deferred rendering to avoid re-rendering on every line add
    -- This is critical for performance when adding thousands of lines
    self:_scheduleRender()
end

---Schedule a deferred render (once per frame max)
function VirtualListRenderer:_scheduleRender()
    if self._renderPending then
        return  -- Already scheduled
    end
    self._renderPending = true

    -- Use C_Timer.After(0) to defer to next frame
    C_Timer.After(0, function()
        self._renderPending = false
        self:Render()
    end)
end

function VirtualListRenderer:_onLineUpdated(line)
    self._renderLinesDirty = true
    self:_scheduleRender()
end

function VirtualListRenderer:_onLineRemoved(line)
    self._renderLinesDirty = true
    local totalLines = self:_getRenderLineCount()
    -- For bottom-up: scrollOffset is distance from bottom, cap it to max possible
    local maxOffset = math.max(0, totalLines - self._visibleCount)
    self._scrollOffset = math.min(self._scrollOffset, maxOffset)
    self:Render()
end

-------------------------------------------------------------------------------
-- Scrolling
-------------------------------------------------------------------------------

function VirtualListRenderer:Scroll(delta)
    if not self.buffer then return end

    local totalLines = self:_getRenderLineCount()
    local maxOffset = math.max(0, totalLines - self._visibleCount)

    -- For bottom-up: positive delta scrolls UP (show older content = increase offset)
    -- Mouse wheel up = negative delta from WoW, but we want to scroll up (older)
    -- So we invert: scrolling "up" in the UI means going back in history = increase offset
    local newOffset = self._scrollOffset - delta  -- Invert for natural scroll direction
    newOffset = math.max(0, math.min(newOffset, maxOffset))

    if newOffset ~= self._scrollOffset then
        self._scrollOffset = newOffset
        self:Render()
        self:_updateScrollbar()
    end
end

function VirtualListRenderer:ScrollToTop()
    -- For bottom-up: "top" means oldest content = max offset
    if not self.buffer then return end

    local totalLines = self:_getRenderLineCount()
    local maxOffset = math.max(0, totalLines - self._visibleCount)

    if self._scrollOffset ~= maxOffset then
        self._scrollOffset = maxOffset
        self:Render()
        self:_updateScrollbar()
    end
end

function VirtualListRenderer:ScrollToBottom()
    -- For bottom-up: "bottom" means newest content = offset 0
    if self._scrollOffset ~= 0 then
        self._scrollOffset = 0
        self:Render()
        self:_updateScrollbar()
    end
end

function VirtualListRenderer:PageUp()
    -- Page up shows older content = increase offset
    self:Scroll(-self._visibleCount + 1)
end

function VirtualListRenderer:PageDown()
    -- Page down shows newer content = decrease offset
    self:Scroll(self._visibleCount - 1)
end

function VirtualListRenderer:IsAtBottom()
    -- For bottom-up: at bottom means scrollOffset == 0 (showing newest)
    return self._scrollOffset == 0
end

-------------------------------------------------------------------------------
-- Font Settings
-------------------------------------------------------------------------------

function VirtualListRenderer:SetFont(fontName, fontSize, fontFlags)
    self._fontPath = fontName
    self.fontSize = fontSize or self.fontSize
    self.fontFlags = fontFlags or self.fontFlags

    -- Get theme text color
    local textColorHex = C3.UI.Theme and C3.UI.Theme:GetColor("content.fg") or "D8DEE9"
    local r, g, b = 0.85, 0.87, 0.91
    if type(textColorHex) == "string" and #textColorHex >= 6 then
        r = tonumber(textColorHex:sub(1, 2), 16) / 255
        g = tonumber(textColorHex:sub(3, 4), 16) / 255
        b = tonumber(textColorHex:sub(5, 6), 16) / 255
    end

    for i = 1, self.maxVisibleLines do
        local entry = self._fontStrings[i]
        if entry then
            entry.fontString:SetFont(fontName, self.fontSize, self.fontFlags)
            entry.fontString:SetShadowOffset(0, 0)
            entry.fontString:SetTextColor(r, g, b)
        end
    end

    if self._measureFontString then
        self._measureFontString:SetFont(fontName, self.fontSize, self.fontFlags)
    end

    self._renderLinesDirty = true
    self:Render()
end

function VirtualListRenderer:SetFontSize(size)
    self.fontSize = size
    local fontPath = self._fontPath or C3.UI.Theme and C3.UI.Theme.Font or "Fonts\\FRIZQT__.TTF"

    -- Get theme text color
    local textColorHex = C3.UI.Theme and C3.UI.Theme:GetColor("content.fg") or "D8DEE9"
    local r, g, b = 0.85, 0.87, 0.91
    if type(textColorHex) == "string" and #textColorHex >= 6 then
        r = tonumber(textColorHex:sub(1, 2), 16) / 255
        g = tonumber(textColorHex:sub(3, 4), 16) / 255
        b = tonumber(textColorHex:sub(5, 6), 16) / 255
    end

    for i = 1, self.maxVisibleLines do
        local entry = self._fontStrings[i]
        if entry then
            entry.fontString:SetFont(fontPath, self.fontSize, self.fontFlags)
            entry.fontString:SetShadowOffset(0, 0)
            entry.fontString:SetTextColor(r, g, b)
        end
    end
    if self._measureFontString then
        self._measureFontString:SetFont(fontPath, self.fontSize, self.fontFlags)
    end
    self._renderLinesDirty = true
    self:Render()
end

function VirtualListRenderer:SetLineHeight(height)
    self.lineHeight = height

    for i = 1, self.maxVisibleLines do
        local entry = self._fontStrings[i]
        if entry then
            local fs = entry.fontString
            fs:ClearAllPoints()
            -- Position from BOTTOM going UP
            local yOffset = (i - 1) * self.lineHeight
            fs:SetPoint("BOTTOMLEFT", self.content, "BOTTOMLEFT", 0, yOffset)
            fs:SetPoint("RIGHT", self.content, "RIGHT", 0, 0)
            fs:SetHeight(self.lineHeight)
        end
    end

    -- Update scrollbar position if using scrollbarAnchor (bottom offset depends on font size)
    self:_updateScrollbarPosition()

    local containerHeight = self.container:GetHeight()
    if containerHeight and containerHeight > 0 then
        self:_onSizeChanged(self.container:GetWidth(), containerHeight)
    end
    self._renderLinesDirty = true
end

---Update scrollbar anchor positions (called when zoom changes)
function VirtualListRenderer:_updateScrollbarPosition()
    if not self.scrollbar or not self.scrollbarAnchor then return end

    local Theme = C3.UI.Theme
    local borderSize = Theme and Theme.dimensions and Theme.dimensions.borderSize or 1
    local xInset = -borderSize
    local bottomOffset = Theme and (Theme:GetEffectiveBottomReserve() + 8) or 34

    self.scrollbar:ClearAllPoints()
    self.scrollbar:SetPoint("TOPRIGHT", xInset, -30)
    self.scrollbar:SetPoint("BOTTOMRIGHT", xInset, bottomOffset)
end

-------------------------------------------------------------------------------
-- Utility
-------------------------------------------------------------------------------

function VirtualListRenderer:Clear()
    for i = 1, self.maxVisibleLines do
        local entry = self._fontStrings[i]
        if entry then
            entry.fontString:SetText("")
            entry.fontString:Hide()
            entry.lineId = nil
            entry.visible = false
        end
    end
    self._scrollOffset = 0
    self._renderLines = {}
    self._renderLineCount = 0
    self._renderLinesDirty = true
    self:_updateScrollbar()
end

function VirtualListRenderer:GetVisibleLineCount()
    return self._visibleCount
end

function VirtualListRenderer:GetScrollOffset()
    return self._scrollOffset
end

function VirtualListRenderer:Layout()
    local width = self.container:GetWidth()
    local height = self.container:GetHeight()
    dbg("Layout() called, width=", width, "height=", height)
    if width and height and width > 0 and height > 0 then
        self:_onSizeChanged(width, height)
    end
end

---Show or hide the renderer content (for TUI takeover)
---@param visible boolean
function VirtualListRenderer:SetContentVisible(visible)
    if visible then
        self.content:Show()
        if self.scrollbar then
            self:_updateScrollbar()
        end
    else
        self.content:Hide()
        if self.scrollbar then
            self.scrollbar:Hide()
        end
    end
end

return VirtualListRenderer
