--[[
    Commander 3.0 - Text Buffer with Undo/Redo Support

    Ported from CommanderEdit/core/buffer.lua
    Stores text as an array of lines for efficient line-based operations
]]

local ADDON_NAME, C3 = ...

-------------------------------------------------------------------------------
-- TextBuffer Class
-------------------------------------------------------------------------------

local TextBuffer = {}
TextBuffer.__index = TextBuffer
C3.TextBuffer = TextBuffer

-- Operation types for undo/redo
local OP_INSERT = "insert"
local OP_DELETE = "delete"
local OP_REPLACE = "replace"

---Create a new buffer instance
---@param initialText? string Optional initial content
---@return table Buffer instance
function TextBuffer:New(initialText)
    local buf = {
        lines = {},
        cursorLine = 1,
        cursorCol = 1,
        selStart = nil,  -- {line, col} or nil
        selEnd = nil,    -- {line, col} or nil
        undoStack = {},
        redoStack = {},
        dirty = false,
        maxUndo = 100,
    }
    setmetatable(buf, self)

    if initialText and initialText ~= "" then
        buf:SetText(initialText)
    else
        buf.lines = {""}
    end

    return buf
end

-------------------------------------------------------------------------------
-- Text Access
-------------------------------------------------------------------------------

---Get full text as a single string
---@return string
function TextBuffer:GetText()
    return table.concat(self.lines, "\n")
end

---Set full text (clears undo history)
---@param text string
function TextBuffer:SetText(text)
    self.lines = {}
    for line in (text .. "\n"):gmatch("([^\n]*)\n") do
        table.insert(self.lines, line)
    end
    if #self.lines == 0 then
        self.lines = {""}
    end
    self.cursorLine = 1
    self.cursorCol = 1
    self.selStart = nil
    self.selEnd = nil
    self.undoStack = {}
    self.redoStack = {}
    self.dirty = false
end

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

---Get a specific line
---@param lineNum number
---@return string
function TextBuffer:GetLine(lineNum)
    return self.lines[lineNum] or ""
end

-------------------------------------------------------------------------------
-- Cursor Management
-------------------------------------------------------------------------------

---Get cursor position
---@return number line
---@return number col
function TextBuffer:GetCursor()
    return self.cursorLine, self.cursorCol
end

---Set cursor position (clamped to valid range)
---@param line number
---@param col number
function TextBuffer:SetCursor(line, col)
    line = math.max(1, math.min(line, #self.lines))
    local lineLen = #self.lines[line]
    col = math.max(1, math.min(col, lineLen + 1))
    self.cursorLine = line
    self.cursorCol = col
end

---Move cursor relative to current position
---@param dLine number
---@param dCol number
function TextBuffer:MoveCursor(dLine, dCol)
    local newLine = self.cursorLine + dLine
    local newCol = self.cursorCol + dCol

    -- Handle line boundaries
    if newCol < 1 then
        if newLine > 1 then
            newLine = newLine - 1
            newCol = #self.lines[newLine] + 1
        else
            newCol = 1
        end
    elseif newCol > #self.lines[self.cursorLine] + 1 then
        if newLine < #self.lines then
            newLine = newLine + 1
            newCol = 1
        else
            newCol = #self.lines[self.cursorLine] + 1
        end
    end

    self:SetCursor(newLine, newCol)
end

-------------------------------------------------------------------------------
-- Selection Management
-------------------------------------------------------------------------------

---Check if there is an active selection
---@return boolean
function TextBuffer:HasSelection()
    return self.selStart ~= nil and self.selEnd ~= nil
end

---Clear the current selection
function TextBuffer:ClearSelection()
    self.selStart = nil
    self.selEnd = nil
end

---Set selection range
---@param startLine number
---@param startCol number
---@param endLine number
---@param endCol number
function TextBuffer:SetSelection(startLine, startCol, endLine, endCol)
    self.selStart = {line = startLine, col = startCol}
    self.selEnd = {line = endLine, col = endCol}
end

---Get selection (may be in reverse order)
---@return table|nil startPos {line, col}
---@return table|nil endPos {line, col}
function TextBuffer:GetSelection()
    if not self:HasSelection() then return nil end
    return self.selStart, self.selEnd
end

---Get selection normalized (start is always before end)
---@return table|nil startPos {line, col}
---@return table|nil endPos {line, col}
function TextBuffer:GetSelectionNormalized()
    if not self:HasSelection() then return nil end

    local s, e = self.selStart, self.selEnd
    -- Normalize so s is before e
    if s.line > e.line or (s.line == e.line and s.col > e.col) then
        s, e = e, s
    end
    return s, e
end

---Get selected text
---@return string
function TextBuffer:GetSelectedText()
    local s, e = self:GetSelectionNormalized()
    if not s then return "" end

    if s.line == e.line then
        return self.lines[s.line]:sub(s.col, e.col - 1)
    end

    local result = {}
    table.insert(result, self.lines[s.line]:sub(s.col))
    for i = s.line + 1, e.line - 1 do
        table.insert(result, self.lines[i])
    end
    table.insert(result, self.lines[e.line]:sub(1, e.col - 1))
    return table.concat(result, "\n")
end

---Select all text
function TextBuffer:SelectAll()
    self.selStart = {line = 1, col = 1}
    local lastLine = #self.lines
    self.selEnd = {line = lastLine, col = #self.lines[lastLine] + 1}
end

-------------------------------------------------------------------------------
-- Undo/Redo System
-------------------------------------------------------------------------------

---Push an operation onto the undo stack
---@param op table Operation details
local function PushUndo(self, op)
    table.insert(self.undoStack, op)
    if #self.undoStack > self.maxUndo then
        table.remove(self.undoStack, 1)
    end
    self.redoStack = {}  -- Clear redo on new operation
    self.dirty = true
end

---Undo the last operation
---@return boolean success
function TextBuffer:Undo()
    if #self.undoStack == 0 then return false end

    local op = table.remove(self.undoStack)
    table.insert(self.redoStack, op)

    if op.type == OP_INSERT then
        -- Undo insert = delete the text
        self:DeleteTextRaw(op.startLine, op.startCol, op.endLine, op.endCol)
        self:SetCursor(op.startLine, op.startCol)
    elseif op.type == OP_DELETE then
        -- Undo delete = insert the text back
        self:InsertTextRaw(op.startLine, op.startCol, op.text)
        self:SetCursor(op.cursorLine, op.cursorCol)
    elseif op.type == OP_REPLACE then
        -- Undo replace = delete new text, insert old text
        self:DeleteTextRaw(op.startLine, op.startCol, op.endLine, op.endCol)
        self:InsertTextRaw(op.startLine, op.startCol, op.oldText)
        self:SetCursor(op.cursorLine, op.cursorCol)
    end

    return true
end

---Redo the last undone operation
---@return boolean success
function TextBuffer:Redo()
    if #self.redoStack == 0 then return false end

    local op = table.remove(self.redoStack)
    table.insert(self.undoStack, op)

    if op.type == OP_INSERT then
        self:InsertTextRaw(op.startLine, op.startCol, op.text)
        self:SetCursor(op.endLine, op.endCol)
    elseif op.type == OP_DELETE then
        self:DeleteTextRaw(op.startLine, op.startCol, op.endLine, op.endCol)
        self:SetCursor(op.startLine, op.startCol)
    elseif op.type == OP_REPLACE then
        self:DeleteTextRaw(op.startLine, op.startCol, op.oldEndLine, op.oldEndCol)
        self:InsertTextRaw(op.startLine, op.startCol, op.text)
        self:SetCursor(op.endLine, op.endCol)
    end

    return true
end

---Check if undo is available
---@return boolean
function TextBuffer:CanUndo()
    return #self.undoStack > 0
end

---Check if redo is available
---@return boolean
function TextBuffer:CanRedo()
    return #self.redoStack > 0
end

-------------------------------------------------------------------------------
-- Raw Text Operations (no undo tracking)
-------------------------------------------------------------------------------

---Insert text at a position without tracking undo
---@param line number
---@param col number
---@param text string
---@return number endLine
---@return number endCol
function TextBuffer:InsertTextRaw(line, col, text)
    local lines = {}
    for l in (text .. "\n"):gmatch("([^\n]*)\n") do
        table.insert(lines, l)
    end
    if #lines == 0 then return line, col end

    -- Handle single-line insert
    local currentLine = self.lines[line] or ""
    local before = currentLine:sub(1, col - 1)
    local after = currentLine:sub(col)

    if #lines == 1 then
        self.lines[line] = before .. lines[1] .. after
        return line, col + #lines[1]
    end

    -- Multi-line insert
    self.lines[line] = before .. lines[1]
    for i = 2, #lines - 1 do
        table.insert(self.lines, line + i - 1, lines[i])
    end
    local lastLineNum = line + #lines - 1
    if #lines > 1 then
        table.insert(self.lines, lastLineNum, lines[#lines] .. after)
    end

    return lastLineNum, #lines[#lines] + 1
end

---Delete text between two positions without tracking undo
---@param startLine number
---@param startCol number
---@param endLine number
---@param endCol number
function TextBuffer:DeleteTextRaw(startLine, startCol, endLine, endCol)
    if startLine == endLine then
        local line = self.lines[startLine] or ""
        self.lines[startLine] = line:sub(1, startCol - 1) .. line:sub(endCol)
        return
    end

    -- Multi-line delete
    local firstLine = self.lines[startLine] or ""
    local lastLine = self.lines[endLine] or ""
    self.lines[startLine] = firstLine:sub(1, startCol - 1) .. lastLine:sub(endCol)

    -- Remove intermediate lines
    for _ = startLine + 1, endLine do
        table.remove(self.lines, startLine + 1)
    end
end

-------------------------------------------------------------------------------
-- Public Text Operations (with undo tracking)
-------------------------------------------------------------------------------

---Insert text at cursor position
---@param text string
function TextBuffer:InsertText(text)
    if not text or text == "" then return end

    -- If there's a selection, delete it first
    if self:HasSelection() then
        self:DeleteSelection()
    end

    local startLine, startCol = self.cursorLine, self.cursorCol
    local endLine, endCol = self:InsertTextRaw(startLine, startCol, text)

    PushUndo(self, {
        type = OP_INSERT,
        startLine = startLine,
        startCol = startCol,
        endLine = endLine,
        endCol = endCol,
        text = text,
    })

    self:SetCursor(endLine, endCol)
end

---Delete the current selection
function TextBuffer:DeleteSelection()
    local s, e = self:GetSelectionNormalized()
    if not s then return end

    local text = self:GetSelectedText()
    self:DeleteTextRaw(s.line, s.col, e.line, e.col)

    PushUndo(self, {
        type = OP_DELETE,
        startLine = s.line,
        startCol = s.col,
        endLine = e.line,
        endCol = e.col,
        text = text,
        cursorLine = e.line,
        cursorCol = e.col,
    })

    self:SetCursor(s.line, s.col)
    self:ClearSelection()
end

---Delete character at cursor (Delete key)
function TextBuffer:DeleteChar()
    if self:HasSelection() then
        self:DeleteSelection()
        return
    end

    local line, col = self.cursorLine, self.cursorCol
    local currentLine = self.lines[line] or ""

    if col > #currentLine then
        -- At end of line, join with next line
        if line < #self.lines then
            local deleted = "\n"
            self:DeleteTextRaw(line, col, line + 1, 1)
            PushUndo(self, {
                type = OP_DELETE,
                startLine = line,
                startCol = col,
                endLine = line + 1,
                endCol = 1,
                text = deleted,
                cursorLine = line,
                cursorCol = col,
            })
        end
    else
        local deleted = currentLine:sub(col, col)
        self:DeleteTextRaw(line, col, line, col + 1)
        PushUndo(self, {
            type = OP_DELETE,
            startLine = line,
            startCol = col,
            endLine = line,
            endCol = col + 1,
            text = deleted,
            cursorLine = line,
            cursorCol = col,
        })
    end
end

---Delete character before cursor (Backspace key)
function TextBuffer:Backspace()
    if self:HasSelection() then
        self:DeleteSelection()
        return
    end

    local line, col = self.cursorLine, self.cursorCol

    if col > 1 then
        local currentLine = self.lines[line] or ""
        local deleted = currentLine:sub(col - 1, col - 1)
        self:DeleteTextRaw(line, col - 1, line, col)
        PushUndo(self, {
            type = OP_DELETE,
            startLine = line,
            startCol = col - 1,
            endLine = line,
            endCol = col,
            text = deleted,
            cursorLine = line,
            cursorCol = col,
        })
        self:SetCursor(line, col - 1)
    elseif line > 1 then
        -- Join with previous line
        local prevLineLen = #self.lines[line - 1]
        self:DeleteTextRaw(line - 1, prevLineLen + 1, line, 1)
        PushUndo(self, {
            type = OP_DELETE,
            startLine = line - 1,
            startCol = prevLineLen + 1,
            endLine = line,
            endCol = 1,
            text = "\n",
            cursorLine = line,
            cursorCol = col,
        })
        self:SetCursor(line - 1, prevLineLen + 1)
    end
end

---Insert a new line at cursor
function TextBuffer:NewLine()
    self:InsertText("\n")
end

-------------------------------------------------------------------------------
-- Search Functionality
-------------------------------------------------------------------------------

---Find text in buffer
---@param needle string Text to find
---@param caseSensitive? boolean Default false
---@param fromCursor? boolean Start from cursor position
---@return number|nil line
---@return number|nil startCol
---@return number|nil endCol
function TextBuffer:Find(needle, caseSensitive, fromCursor)
    if not needle or needle == "" then return nil end

    local startLine = fromCursor and self.cursorLine or 1
    local startCol = fromCursor and self.cursorCol or 1

    if not caseSensitive then
        needle = needle:lower()
    end

    for i = startLine, #self.lines do
        local line = self.lines[i]
        local searchLine = caseSensitive and line or line:lower()
        local searchStart = (i == startLine) and startCol or 1
        local pos = searchLine:find(needle, searchStart, true)
        if pos then
            return i, pos, pos + #needle - 1
        end
    end

    -- Wrap around
    if fromCursor then
        for i = 1, startLine do
            local line = self.lines[i]
            local searchLine = caseSensitive and line or line:lower()
            local searchEnd = (i == startLine) and startCol - 1 or #line
            local pos = searchLine:find(needle, 1, true)
            if pos and (i < startLine or pos < startCol) then
                return i, pos, pos + #needle - 1
            end
        end
    end

    return nil
end

---Replace first occurrence of text
---@param oldText string
---@param newText string
---@param caseSensitive? boolean
---@return boolean success
function TextBuffer:Replace(oldText, newText, caseSensitive)
    local line, startCol, endCol = self:Find(oldText, caseSensitive, true)
    if not line then return false end

    self:SetCursor(line, startCol)
    self:SetSelection(line, startCol, line, endCol + 1)
    self:DeleteSelection()
    self:InsertText(newText)
    return true
end

---Replace all occurrences of text
---@param oldText string
---@param newText string
---@param caseSensitive? boolean
---@return number count Number of replacements
function TextBuffer:ReplaceAll(oldText, newText, caseSensitive)
    local count = 0
    while self:Replace(oldText, newText, caseSensitive) do
        count = count + 1
        if count > 10000 then break end  -- Safety limit
    end
    return count
end

-------------------------------------------------------------------------------
-- Dirty State
-------------------------------------------------------------------------------

---Mark buffer as clean (after save)
function TextBuffer:MarkClean()
    self.dirty = false
end

---Check if buffer has unsaved changes
---@return boolean
function TextBuffer:IsDirty()
    return self.dirty
end

return TextBuffer
