--[[
    Commander 3.0 - Expression Parser

    Recursive descent parser for query expressions.
    Supports: Operator precedence, grouping, nested properties.
]]

local ADDON_NAME, C3 = ...

local ExpressionParser = {}
C3.ExpressionParser = ExpressionParser

-- Token types specific to expressions
local TOKEN = {
    IDENTIFIER = "IDENTIFIER",
    NUMBER = "NUMBER",
    STRING = "STRING",
    BOOLEAN = "BOOLEAN",
    NIL = "NIL",
    LPAREN = "LPAREN",
    RPAREN = "RPAREN",
    AND = "AND",
    OR = "OR",
    NOT = "NOT",
    OP = "OP", -- >, <, ==, etc.
    EOF = "EOF"
}

-- Operators mapping
local OPS = {
    [">"] = function(a, b) return (tonumber(a) or 0) > (tonumber(b) or 0) end,
    [">="] = function(a, b) return (tonumber(a) or 0) >= (tonumber(b) or 0) end,
    ["<"] = function(a, b) return (tonumber(a) or 0) < (tonumber(b) or 0) end,
    ["<="] = function(a, b) return (tonumber(a) or 0) <= (tonumber(b) or 0) end,
    ["=="] = function(a, b) return a == b end,
    ["="] = function(a, b) return a == b end,
    ["!="] = function(a, b) return a ~= b end,
    ["~="] = function(a, b) return a ~= b end,
    ["~"] = function(a, b) -- Contains / match
        return tostring(a):lower():find(tostring(b):lower(), 1, true) ~= nil
    end
}

-------------------------------------------------------------------------------
-- Tokenizer
-------------------------------------------------------------------------------

local function tokenize(input)
    local tokens = {}
    local pos = 1
    local len = #input

    while pos <= len do
        local char = input:sub(pos, pos)

        if char:match("%s") then
            pos = pos + 1
        elseif char == "(" then
            table.insert(tokens, { type = TOKEN.LPAREN, value = "(" })
            pos = pos + 1
        elseif char == ")" then
            table.insert(tokens, { type = TOKEN.RPAREN, value = ")" })
            pos = pos + 1
        elseif char == "'" or char == '"' then
            -- String
            local quote = char
            local start = pos + 1
            pos = pos + 1
            while pos <= len and input:sub(pos, pos) ~= quote do
                pos = pos + 1
            end
            local val = input:sub(start, pos - 1)
            table.insert(tokens, { type = TOKEN.STRING, value = val })
            pos = pos + 1
        elseif char:match("[%d%.]") then
            -- Number (simplified)
            local start = pos
            while pos <= len and input:sub(pos, pos):match("[%d%.]") do
                pos = pos + 1
            end
            local val = tonumber(input:sub(start, pos - 1))
            table.insert(tokens, { type = TOKEN.NUMBER, value = val })
        elseif char:match("[><=!~]") then
            -- Operator
            local start = pos
            if input:sub(pos, pos+1):match("[><=!~]=") then
                pos = pos + 2
            else
                pos = pos + 1
            end
            local val = input:sub(start, pos - 1)
            table.insert(tokens, { type = TOKEN.OP, value = val })
        elseif char:match("[%a_]") then
            -- Identifier or Keyword (must start with letter or underscore)
            local start = pos
            while pos <= len and input:sub(pos, pos):match("[%w_%.]") do
                pos = pos + 1
            end
            local word = input:sub(start, pos - 1)
            local lower = word:lower()

            if lower == "and" then
                table.insert(tokens, { type = TOKEN.AND, value = "and" })
            elseif lower == "or" then
                table.insert(tokens, { type = TOKEN.OR, value = "or" })
            elseif lower == "not" then
                table.insert(tokens, { type = TOKEN.NOT, value = "not" })
            elseif lower == "true" then
                table.insert(tokens, { type = TOKEN.BOOLEAN, value = true })
            elseif lower == "false" then
                table.insert(tokens, { type = TOKEN.BOOLEAN, value = false })
            elseif lower == "nil" then
                table.insert(tokens, { type = TOKEN.NIL, value = nil })
            else
                table.insert(tokens, { type = TOKEN.IDENTIFIER, value = word })
            end
        else
            -- Unknown character - skip it to prevent infinite loop
            -- This handles characters like '-', '+', '*', '/', etc.
            pos = pos + 1
        end
    end
    table.insert(tokens, { type = TOKEN.EOF })
    return tokens
end

-------------------------------------------------------------------------------
-- Parser
-------------------------------------------------------------------------------

local function parse(tokens)
    local current = 1

    local function peek() return tokens[current] end
    local function advance()
        local t = tokens[current]
        if t.type ~= TOKEN.EOF then current = current + 1 end
        return t
    end
    local function match(type)
        if peek().type == type then
            advance()
            return true
        end
        return false
    end

    -- Forward declarations
    local expression, or_expr, and_expr, not_expr, comparison, term

    function expression() return or_expr() end

    function or_expr()
        local expr = and_expr()
        while match(TOKEN.OR) do
            local right = and_expr()
            expr = { type = "BINARY", op = "or", left = expr, right = right }
        end
        return expr
    end

    function and_expr()
        local expr = not_expr()
        while match(TOKEN.AND) do
            local right = not_expr()
            expr = { type = "BINARY", op = "and", left = expr, right = right }
        end
        return expr
    end

    function not_expr()
        if match(TOKEN.NOT) then
            return { type = "UNARY", op = "not", right = not_expr() }
        end
        return comparison()
    end

    function comparison()
        local expr = term()
        if peek().type == TOKEN.OP then
            local op = advance().value
            local right = term()
            expr = { type = "BINARY", op = op, left = expr, right = right }
        end
        return expr
    end

    function term()
        local t = peek()
        if match(TOKEN.LPAREN) then
            local expr = expression()
            if not match(TOKEN.RPAREN) then error("Expected ')'") end
            return expr
        elseif match(TOKEN.NUMBER) or match(TOKEN.STRING) or match(TOKEN.BOOLEAN) or match(TOKEN.NIL) then
            return { type = "LITERAL", value = t.value }
        elseif match(TOKEN.IDENTIFIER) then
            return { type = "PROPERTY", value = t.value }
        else
            error("Unexpected token: " .. tostring(t.value or t.type))
        end
    end

    local ast = expression()

    -- Check for unconsumed tokens (indicates malformed expression)
    if peek().type ~= TOKEN.EOF then
        local remaining = {}
        while peek().type ~= TOKEN.EOF do
            local t = advance()
            table.insert(remaining, tostring(t.value or t.type))
        end
        error("Unexpected tokens after expression: " .. table.concat(remaining, " "))
    end

    return ast
end

-------------------------------------------------------------------------------
-- Evaluator
-------------------------------------------------------------------------------

-- Resolve "stats.intellect" from obj
local function resolveProperty(obj, path)
    local current = obj
    for part in path:gmatch("[^%.]+") do
        if type(current) ~= "table" then return nil end
        current = current[part]
    end
    return current
end

local function evaluate(ast, obj)
    if ast.type == "LITERAL" then
        return ast.value
    elseif ast.type == "PROPERTY" then
        return resolveProperty(obj, ast.value)
    elseif ast.type == "UNARY" then
        if ast.op == "not" then
            return not evaluate(ast.right, obj)
        end
    elseif ast.type == "BINARY" then
        local left = evaluate(ast.left, obj)

        -- Short-circuit logic
        if ast.op == "and" then
            return left and evaluate(ast.right, obj)
        elseif ast.op == "or" then
            return left or evaluate(ast.right, obj)
        end

        local right = evaluate(ast.right, obj)
        local opFunc = OPS[ast.op]
        if opFunc then
            return opFunc(left, right)
        end
    end
    return false
end

-------------------------------------------------------------------------------
-- Public API
-------------------------------------------------------------------------------

-- Cache parsed ASTs (stores { ast = ast } on success, { error = msg } on failure)
local cache = {}

---Parse an expression and cache the result
---@param exprString string The expression (e.g. "quality > 3")
---@return table|nil ast The parsed AST, or nil on error
---@return string|nil error The error message, or nil on success
function ExpressionParser:Parse(exprString)
    local cached = cache[exprString]
    if cached then
        return cached.ast, cached.error
    end

    local ok, tokens = pcall(tokenize, exprString)
    if not ok then
        local err = "Tokenizer error: " .. tostring(tokens)
        cache[exprString] = { error = err }
        return nil, err
    end

    local okParse, res = pcall(parse, tokens)
    if not okParse then
        local err = tostring(res)
        cache[exprString] = { error = err }
        return nil, err
    end

    cache[exprString] = { ast = res }
    return res, nil
end

---Parse and evaluate an expression against an object
---@param exprString string The expression (e.g. "quality > 3")
---@param obj table The data object
---@return boolean
function ExpressionParser:Evaluate(exprString, obj)
    local ast, err = self:Parse(exprString)
    if not ast then
        return false
    end

    return evaluate(ast, obj)
end

return ExpressionParser
