--[[
    Commander 3.0 - Lexer (Tokenizer)

    Converts input strings into token streams.
    Supports: quotes, pipes, chains, variables, subshells.
]]

local ADDON_NAME, C3 = ...

local Lexer = {}
Lexer.__index = Lexer
C3.Lexer = Lexer

-- Token types
Lexer.TOKEN = {
    WORD = "WORD",           -- bare_word
    STRING = "STRING",       -- "quoted string"
    NUMBER = "NUMBER",       -- 42, 3.14
    PIPE = "PIPE",           -- |
    AND = "AND",             -- &&
    OR = "OR",               -- ||
    SEMI = "SEMI",           -- ;
    VARIABLE = "VARIABLE",   -- $name
    SUBSHELL = "SUBSHELL",   -- $(command)
    FLAG = "FLAG",           -- -f, --flag
    GT = "GT",               -- >
    LT = "LT",               -- <
    GTE = "GTE",             -- >=
    LTE = "LTE",             -- <=
    EQ = "EQ",               -- ==
    NEQ = "NEQ",             -- !=
    LPAREN = "LPAREN",       -- (
    RPAREN = "RPAREN",       -- )
    EOF = "EOF",
    ERROR = "ERROR",
}

---Create a new Lexer
---@param input string
---@return table
function Lexer.new(input)
    local self = setmetatable({}, Lexer)
    -- Unescape WoW's || back to single | for pipeline support
    -- In WoW, || is how you type a literal | (since | starts escape sequences)
    self.input = input:gsub("||", "|")
    self.pos = 1
    self.len = #self.input
    self.tokens = {}
    return self
end

---Peek at current character
---@return string?
function Lexer:Peek()
    if self.pos > self.len then
        return nil
    end
    return self.input:sub(self.pos, self.pos)
end

---Peek at next character
---@return string?
function Lexer:PeekNext()
    if self.pos + 1 > self.len then
        return nil
    end
    return self.input:sub(self.pos + 1, self.pos + 1)
end

---Advance and return current character
---@return string?
function Lexer:Advance()
    local char = self:Peek()
    self.pos = self.pos + 1
    return char
end

---Skip whitespace
function Lexer:SkipWhitespace()
    while self:Peek() and self:Peek():match("%s") do
        self:Advance()
    end
end

---Emit a token
---@param tokenType string
---@param value any
function Lexer:Emit(tokenType, value)
    table.insert(self.tokens, {
        type = tokenType,
        value = value,
        pos = self.pos,
    })
end

---Read a quoted string
---@param quoteChar string
function Lexer:ReadString(quoteChar)
    self:Advance() -- Skip opening quote

    local start = self.pos
    local escaped = false

    while self.pos <= self.len do
        local char = self:Peek()

        if escaped then
            escaped = false
            self:Advance()
        elseif char == "\\" then
            escaped = true
            self:Advance()
        elseif char == quoteChar then
            local value = self.input:sub(start, self.pos - 1)
            self:Advance() -- Skip closing quote
            self:Emit(Lexer.TOKEN.STRING, value)
            return
        else
            self:Advance()
        end
    end

    -- Unclosed quote - take rest of input
    local value = self.input:sub(start)
    self:Emit(Lexer.TOKEN.STRING, value)
end

---Read a word (unquoted token)
function Lexer:ReadWord()
    local start = self.pos

    while self.pos <= self.len do
        local char = self:Peek()
        -- Stop at whitespace or special characters (including parentheses)
        -- But allow apostrophes followed by a letter (contractions like "Mimiron's")
        if char == "'" then
            local nextChar = self:PeekNext()
            if nextChar and nextChar:match("[%a]") then
                -- Apostrophe followed by letter - include it in the word
                self:Advance()
            else
                break
            end
        elseif char:match("[%s|&;><=\"()]") then
            break
        else
            self:Advance()
        end
    end

    local value = self.input:sub(start, self.pos - 1)

    -- Check if it's a number
    local num = tonumber(value)
    if num then
        self:Emit(Lexer.TOKEN.NUMBER, num)
    else
        self:Emit(Lexer.TOKEN.WORD, value)
    end
end

---Read a variable ($name or $(subshell))
function Lexer:ReadVariable()
    self:Advance() -- Skip $

    local char = self:Peek()

    if char == "(" then
        -- Subshell: $(command)
        self:Advance() -- Skip (
        local depth = 1
        local start = self.pos

        while self.pos <= self.len and depth > 0 do
            char = self:Advance()
            if char == "(" then
                depth = depth + 1
            elseif char == ")" then
                depth = depth - 1
            end
        end

        local content = self.input:sub(start, self.pos - 2)
        self:Emit(Lexer.TOKEN.SUBSHELL, content)
    else
        -- Variable: $name or $name.property
        local start = self.pos
        while self.pos <= self.len do
            char = self:Peek()
            if not char or not char:match("[%w_.]") then
                break
            end
            self:Advance()
        end

        local name = self.input:sub(start, self.pos - 1)
        self:Emit(Lexer.TOKEN.VARIABLE, name)
    end
end

---Read a flag (-f or --flag)
function Lexer:ReadFlag()
    local start = self.pos
    self:Advance() -- Skip first -

    if self:Peek() == "-" then
        self:Advance() -- Skip second - for --flag
    end

    while self.pos <= self.len do
        local char = self:Peek()
        if not char or not char:match("[%w_-]") then
            break
        end
        self:Advance()
    end

    local value = self.input:sub(start, self.pos - 1)
    self:Emit(Lexer.TOKEN.FLAG, value)
end

---Read operator (|, &&, ||, ;, >, <, etc.)
function Lexer:ReadOperator()
    local char = self:Peek()
    local nextChar = self:PeekNext()

    if char == "|" then
        if nextChar == "|" then
            self:Advance()
            self:Advance()
            self:Emit(Lexer.TOKEN.OR, "||")
        else
            self:Advance()
            self:Emit(Lexer.TOKEN.PIPE, "|")
        end
    elseif char == "&" then
        if nextChar == "&" then
            self:Advance()
            self:Advance()
            self:Emit(Lexer.TOKEN.AND, "&&")
        else
            self:Advance() -- Single & treated as word
            self:Emit(Lexer.TOKEN.WORD, "&")
        end
    elseif char == ";" then
        self:Advance()
        self:Emit(Lexer.TOKEN.SEMI, ";")
    elseif char == ">" then
        if nextChar == "=" then
            self:Advance()
            self:Advance()
            self:Emit(Lexer.TOKEN.GTE, ">=")
        else
            self:Advance()
            self:Emit(Lexer.TOKEN.GT, ">")
        end
    elseif char == "<" then
        if nextChar == "=" then
            self:Advance()
            self:Advance()
            self:Emit(Lexer.TOKEN.LTE, "<=")
        else
            self:Advance()
            self:Emit(Lexer.TOKEN.LT, "<")
        end
    elseif char == "=" then
        if nextChar == "=" then
            self:Advance()
            self:Advance()
            self:Emit(Lexer.TOKEN.EQ, "==")
        else
            self:Advance()
            self:Emit(Lexer.TOKEN.WORD, "=")
        end
    elseif char == "!" then
        if nextChar == "=" then
            self:Advance()
            self:Advance()
            self:Emit(Lexer.TOKEN.NEQ, "!=")
        else
            self:Advance()
            self:Emit(Lexer.TOKEN.WORD, "!")
        end
    end
end

---Tokenize the input
---@return table tokens
function Lexer:Tokenize()
    while self.pos <= self.len do
        self:SkipWhitespace()

        if self.pos > self.len then
            break
        end

        local char = self:Peek()

        if char == '"' or char == "'" then
            self:ReadString(char)
        elseif char == "$" then
            self:ReadVariable()
        elseif char == "-" and self:PeekNext() and self:PeekNext():match("[%a-]") then
            self:ReadFlag()
        elseif char == "(" then
            self:Advance()
            self:Emit(Lexer.TOKEN.LPAREN, "(")
        elseif char == ")" then
            self:Advance()
            self:Emit(Lexer.TOKEN.RPAREN, ")")
        elseif char:match("[|&;><!=]") then
            self:ReadOperator()
        elseif char:match("[%w_]") or char:match("[^%s|&;><=\"'()]") then
            self:ReadWord()
        else
            -- Unknown character, skip
            self:Advance()
        end
    end

    self:Emit(Lexer.TOKEN.EOF, nil)
    return self.tokens
end

---Convenience: Tokenize a string (Static Method)
---@param input string
---@return table tokens
function Lexer.TokenizeString(input)
    local lexer = Lexer.new(input or "")
    return lexer:Tokenize()
end

return Lexer
