--[[
    Commander 3.0 - Virtual Buffer

    The output buffer that holds line objects.
    Similar to a Virtual DOM - we manage the data model,
    then render to the actual UI when needed.
]]

local ADDON_NAME, C3 = ...

-------------------------------------------------------------------------------
-- Virtual Buffer Class
-------------------------------------------------------------------------------

local VirtualBuffer = C3.class()
C3.VirtualBuffer = VirtualBuffer

---Create a new virtual buffer
---@param options? table
function VirtualBuffer:init(options)
    options = options or {}

    self.id = options.id or self:_generateId()
    self.name = options.name or "Buffer"

    -- Line storage
    self.lines = {}           -- Array of Line objects
    self.lineIndex = {}       -- id -> line for fast lookup

    -- Limits
    self.maxLines = options.maxLines or 10000
    self.trimAmount = options.trimAmount or 1000  -- Lines to remove when at limit

    -- Viewport
    self.viewportStart = 1    -- First visible line index
    self.viewportSize = options.viewportSize or 50  -- Visible lines
    self.scrollLock = false   -- Lock to bottom

    -- State
    self.dirty = false        -- Needs re-render
    self.lastModified = GetTime()

    -- Callbacks
    self.onLineAdded = options.onLineAdded
    self.onLineRemoved = options.onLineRemoved
    self.onLineUpdated = options.onLineUpdated
    self.onBufferChanged = options.onBufferChanged
end

---Generate unique buffer ID
---@return string
function VirtualBuffer:_generateId()
    return string.format("buf_%x%x", GetTime() * 1000, math.random(0xFFFF))
end

-------------------------------------------------------------------------------
-- Line Management
-------------------------------------------------------------------------------

---Add a line to the buffer
---@param line table Line object
---@return table line
function VirtualBuffer:AddLine(line)
    -- Auto-wrap raw strings
    if type(line) == "string" then
        line = C3.BufferLine.Text(line)
    elseif type(line) == "table" and not line.type then
        -- Typed object, wrap in output line
        if line._type then
            line = C3.BufferLine.Output(line)
        end
    end

    table.insert(self.lines, line)

    self.lineIndex[line.id] = line
    self:_markDirty()

    -- Trim if over limit
    if #self.lines > self.maxLines then
        self:_trim()
    end

    -- Auto-scroll if at bottom
    if self.scrollLock or self:IsAtBottom() then
        self:ScrollToBottom()
    end

    -- Callback
    if self.onLineAdded then
        self.onLineAdded(line)
    end

    return line
end

---Add multiple lines
---@param lines table Array of lines
function VirtualBuffer:AddLines(lines)
    for _, line in ipairs(lines) do
        self:AddLine(line)
    end
end

---Add a line from command output (handles various types)
---@param output any Command output
---@return number linesAdded
function VirtualBuffer:AddOutput(output)
    local count = 0

    if output == nil then
        return 0
    end

    if type(output) == "string" then
        self:AddLine(C3.BufferLine.Text(output))
        count = 1
    elseif type(output) == "table" then
        if output._type then
            -- Single typed object
            self:AddLine(C3.BufferLine.Output(output))
            count = 1
        elseif output[1] ~= nil then
            -- Array of objects
            for _, item in ipairs(output) do
                self:AddOutput(item)
                count = count + 1
            end
        else
            -- Generic table, wrap it
            self:AddLine(C3.BufferLine.Output(output))
            count = 1
        end
    else
        -- Other types, stringify
        self:AddLine(C3.BufferLine.Text(tostring(output)))
        count = 1
    end

    return count
end

---Add an error line
---@param message string
---@return table line
function VirtualBuffer:AddError(message)
    local line = C3.BufferLine.Error(message)
    return self:AddLine(line)
end

---Add a command input line
---@param command string
---@return table line
function VirtualBuffer:AddInput(command)
    local line = C3.BufferLine.Input("❭ " .. command)
    return self:AddLine(line)
end

---Get line by ID
---@param id string
---@return table?
function VirtualBuffer:GetLine(id)
    return self.lineIndex[id]
end

---Get line by index
---@param index number
---@return table?
function VirtualBuffer:GetLineAt(index)
    return self.lines[index]
end

---Update a line by ID
---@param id string
---@param content any
---@return boolean
function VirtualBuffer:UpdateLine(id, content)
    local line = self.lineIndex[id]
    if line then
        line:SetContent(content)
        self:_markDirty()

        if self.onLineUpdated then
            self.onLineUpdated(line)
        end
        return true
    end
    return false
end

---Remove a line by ID
---@param id string
---@return boolean
function VirtualBuffer:RemoveLine(id)
    local line = self.lineIndex[id]
    if not line then return false end

    -- Find and remove from array
    for i, l in ipairs(self.lines) do
        if l.id == id then
            table.remove(self.lines, i)
            break
        end
    end

    -- Remove from index
    self.lineIndex[id] = nil

    -- Stop live updates if applicable
    if line.StopLive then
        line:StopLive()
    end

    self:_markDirty()

    if self.onLineRemoved then
        self.onLineRemoved(line)
    end

    return true
end

---Clear all lines
function VirtualBuffer:Clear()
    -- Stop all live lines
    for _, line in ipairs(self.lines) do
        if line.StopLive then
            line:StopLive()
        end
    end

    self.lines = {}
    self.lineIndex = {}
    self.viewportStart = 1
    self:_markDirty()

    if self.onBufferChanged then
        self.onBufferChanged()
    end
end

---Get total line count
---@return number
function VirtualBuffer:GetLineCount()
    return #self.lines
end

---Get visible line count
---@return number
function VirtualBuffer:GetVisibleLineCount()
    local count = 0
    for _, line in ipairs(self.lines) do
        if line:IsVisible() then
            count = count + 1
        end
    end
    return count
end

---Trim old lines when over limit
function VirtualBuffer:_trim()
    local removeCount = self.trimAmount

    for i = 1, removeCount do
        local line = self.lines[1]
        if line then
            if line.StopLive then
                line:StopLive()
            end
            self.lineIndex[line.id] = nil
            table.remove(self.lines, 1)
        end
    end

    -- Adjust viewport
    self.viewportStart = math.max(1, self.viewportStart - removeCount)

    C3:Debug("Buffer: Trimmed " .. removeCount .. " lines")
end

---Mark buffer as needing re-render
function VirtualBuffer:_markDirty()
    self.dirty = true
    self.lastModified = GetTime()
end

---Check and clear dirty flag
---@return boolean
function VirtualBuffer:IsDirty()
    local wasDirty = self.dirty
    self.dirty = false
    return wasDirty
end

-------------------------------------------------------------------------------
-- Viewport Management
-------------------------------------------------------------------------------

---Get visible lines (in viewport)
---@return table lines
---@return number startIndex
---@return number endIndex
function VirtualBuffer:GetViewport()
    local startIndex = self.viewportStart
    local endIndex = math.min(startIndex + self.viewportSize - 1, #self.lines)

    local visible = {}
    for i = startIndex, endIndex do
        local line = self.lines[i]
        if line and line:IsVisible() then
            table.insert(visible, line)
        end
    end

    return visible, startIndex, endIndex
end

---Set viewport size
---@param size number
function VirtualBuffer:SetViewportSize(size)
    self.viewportSize = size
    self:_markDirty()
end

---Scroll to specific line index
---@param index number
function VirtualBuffer:ScrollTo(index)
    index = math.max(1, math.min(index, #self.lines))
    self.viewportStart = index
    self:_markDirty()
end

---Scroll up by lines
---@param lines? number
function VirtualBuffer:ScrollUp(lines)
    lines = lines or 1
    self.viewportStart = math.max(1, self.viewportStart - lines)
    self:_markDirty()
end

---Scroll down by lines
---@param lines? number
function VirtualBuffer:ScrollDown(lines)
    lines = lines or 1
    local maxStart = math.max(1, #self.lines - self.viewportSize + 1)
    self.viewportStart = math.min(maxStart, self.viewportStart + lines)
    self:_markDirty()
end

---Scroll up by page
function VirtualBuffer:PageUp()
    self:ScrollUp(self.viewportSize - 1)
end

---Scroll down by page
function VirtualBuffer:PageDown()
    self:ScrollDown(self.viewportSize - 1)
end

---Scroll to top
function VirtualBuffer:ScrollToTop()
    self.viewportStart = 1
    self:_markDirty()
end

---Scroll to bottom
function VirtualBuffer:ScrollToBottom()
    self.viewportStart = math.max(1, #self.lines - self.viewportSize + 1)
    self:_markDirty()
end

---Check if viewport is at bottom
---@return boolean
function VirtualBuffer:IsAtBottom()
    local maxStart = math.max(1, #self.lines - self.viewportSize + 1)
    return self.viewportStart >= maxStart
end

---Set scroll lock (auto-scroll to bottom on new lines)
---@param locked boolean
function VirtualBuffer:SetScrollLock(locked)
    self.scrollLock = locked
    if locked then
        self:ScrollToBottom()
    end
end

-------------------------------------------------------------------------------
-- Search & Filter
-------------------------------------------------------------------------------

---Find lines matching pattern
---@param pattern string
---@param options? table
---@return table matches
function VirtualBuffer:Find(pattern, options)
    options = options or {}
    local matches = {}
    local ignoreCase = options.ignoreCase ~= false

    if ignoreCase then
        pattern = pattern:lower()
    end

    for i, line in ipairs(self.lines) do
        local text = line:GetText()
        if ignoreCase then
            text = text:lower()
        end

        if text:find(pattern, 1, true) then
            table.insert(matches, {
                index = i,
                line = line,
            })
        end
    end

    return matches
end

---Get lines with specific tag
---@param tag string
---@return table lines
function VirtualBuffer:GetByTag(tag)
    local matches = {}
    for _, line in ipairs(self.lines) do
        if line:HasTag(tag) then
            table.insert(matches, line)
        end
    end
    return matches
end

---Get lines of specific type
---@param lineType string
---@return table lines
function VirtualBuffer:GetByType(lineType)
    local matches = {}
    for _, line in ipairs(self.lines) do
        if line.type == lineType then
            table.insert(matches, line)
        end
    end
    return matches
end

-------------------------------------------------------------------------------
-- Export
-------------------------------------------------------------------------------

---Export buffer contents as text
---@param options? table
---@return string
function VirtualBuffer:ExportText(options)
    options = options or {}
    local lines = {}

    for _, line in ipairs(self.lines) do
        if line:IsVisible() or options.includeHidden then
            table.insert(lines, line:GetText())
        end
    end

    return table.concat(lines, "\n")
end

---Export buffer contents with character offset mapping for each line
---Used by the Editor component for highlight-in-place functionality
---@param options? table { stripColors: boolean, includeHidden: boolean }
---@return string text, table lineMap
function VirtualBuffer:ExportTextWithMap(options)
    options = options or {}
    local lines = {}
    local lineMap = {}  -- lineID -> { startOffset, endOffset, lineNumber }
    local currentOffset = 0

    for _, line in ipairs(self.lines) do
        if line:IsVisible() or options.includeHidden then
            local text = line:GetText()

            -- Strip WoW color codes and formatting if requested
            if options.stripColors then
                text = text
                    :gsub("|c%x%x%x%x%x%x%x%x", "")  -- Color start
                    :gsub("|r", "")                   -- Color reset
                    :gsub("|T[^|]*|t", "")            -- Textures
                    :gsub("|H[^|]*|h", "")            -- Hyperlink start
                    :gsub("|h", "")                   -- Hyperlink end
            end

            local length = #text

            -- Store mapping: line ID -> { start, end, lineNumber }
            lineMap[line.id] = {
                startOffset = currentOffset,
                endOffset = currentOffset + length,
                lineNumber = #lines + 1
            }

            table.insert(lines, text)

            -- +1 for newline character
            currentOffset = currentOffset + length + 1
        end
    end

    return table.concat(lines, "\n"), lineMap
end

---Export buffer contents as array
---@return table
function VirtualBuffer:ExportArray()
    local result = {}
    for _, line in ipairs(self.lines) do
        table.insert(result, {
            id = line.id,
            type = line.type,
            content = line.content,
            text = line:GetText(),
            created = line.created,
        })
    end
    return result
end

-------------------------------------------------------------------------------
-- Rendering Helpers
-------------------------------------------------------------------------------

---Get render data for a line
---@param line table
---@return table
function VirtualBuffer:GetRenderData(line)
    return {
        id = line.id,
        type = line.type,
        text = line:GetText(),
        color = line.color,
        indent = line.indent,
        height = line.height,
        visible = line.visible,
        isLive = line:IsLive(),
        component = line.component,
        componentData = line.componentData,
    }
end

---Get all render data for viewport
---@return table
function VirtualBuffer:GetViewportRenderData()
    local lines = self:GetViewport()
    local data = {}

    for _, line in ipairs(lines) do
        table.insert(data, self:GetRenderData(line))
    end

    return data
end

return VirtualBuffer
