local len, match = string.len, string.match

local expr_lexer = {}

local lexer_mt = {
    __index = expr_lexer,
}

local literal_chars = {['('] = 1, [')'] = 1, ['~'] = 1, [','] = 1, ['|'] = 1, [':'] = 1}

local oper_table = {['+'] = 2, ['-'] = 2, ['*'] = 3, ['/'] = 3, ['^'] = 4, ['!='] = 1, ['='] = 1, ['>'] = 1, ['>='] = 1, ['<'] = 1, ['<='] = 1, ['and'] = 0, ['or'] = 0, ['%'] = -1}
local word_operators = {'and', 'or'}
local multi_char_operators = {'!=', '>=', '<='}

expr_lexer.operators = oper_table
expr_lexer.max_oper_prio = 4

local oper_start_sym
local function compile_oper_pattern()
    local oper_start_set = {}
    for k in pairs(oper_table) do
        oper_start_set[k:sub(1,1)] = true
    end

    oper_start_sym = '['
    for k in pairs(oper_start_set) do
        local str = (k == '-' or k == '%' or k == '^') and ('%' .. k) or k
        oper_start_sym = oper_start_sym .. str
    end
    oper_start_sym = oper_start_sym .. ']'
end

function expr_lexer.new(src)
    local lexer = {n = 1, src= src}
    setmetatable(lexer, lexer_mt)
    lexer:next()
    return lexer
end

function expr_lexer.char(lexer)
    local n = lexer.n
    return lexer.src:sub(n, n)
end

function expr_lexer.incr(lexer, n)
    lexer.n_current = lexer.n
    lexer.n = lexer.n + (n or 1)
end

function expr_lexer.match(lexer, pattern)
    return match(lexer.src, '^' .. pattern, lexer.n)
end

function expr_lexer.consume(lexer, pattern)
    local m = match(lexer.src, '^' .. pattern, lexer.n)
    if m then
        lexer.n_current = lexer.n
        lexer.n = lexer.n + len(m)
        return m
    end
end

function expr_lexer.skip(lexer, pattern)
    local m = match(lexer.src, '^' .. pattern, lexer.n)
    if m then lexer.n = lexer.n + len(m) end
end

local function consume_oper(lexer, c)
    local op
    for i, ops in ipairs(word_operators) do
        if lexer:match(ops .. '[^%l%u_]') then
            lexer:consume(ops)
            op = ops
            break
        end
    end
    if not op then
        for i, ops in ipairs(multi_char_operators) do
            if lexer:match(ops) then
                lexer:consume(ops)
                op = ops
                break
            end
        end
    end
    if not op then
        op = oper_table[c] and c
        if op then
            lexer:incr()
        end
    end
    if op then
        local prio = oper_table[op]
        return {type= 'operator', symbol= op, priority = prio}
    end
end

function expr_lexer.next_token(lexer)
    lexer:skip('%s*')
    if lexer.n > len(lexer.src) then return {type= 'EOF'} end
    local c = lexer:char()
    if c == '\'' then
        local str = lexer:consume("'[^']+'")
        return {type= 'literal', value = str:sub(2, -2)}
    end
    if c == '[' then
        local str = lexer:consume('%b[]')
        return {type= 'ident', value= str:sub(2,-2)}
    end
    if lexer:match(oper_start_sym) then
        local elt = consume_oper(lexer, c)
        if elt then return elt end
    end
    if literal_chars[c] then
        lexer:incr()
        return {type= c}
    end
    if lexer:match('[%l%u_]') then
        local str = lexer:consume('[%l%u_][%l%u%d_.$]*')
        return {type= 'ident', value= str}
    end
    if lexer:match('%d') then
        local str =  lexer:consume('%d+%.%d*[Ee]%+?%d+')
        str = str or lexer:consume('%d+%.%d*[Ee]%-?%d+')
        str = str or lexer:consume('%d+%.%d*')
        str = str or lexer:consume('%d+')
        return {type= 'number', value= tonumber(str)}
    end
    lexer:local_error("syntax error in expression:", lexer.n)
end

function expr_lexer.next(lexer)
    lexer.token = lexer:next_token()
end

function expr_lexer.local_error(lexer, msg, n_pos)
    n_pos = n_pos or lexer.n_current
    local line = string.format('   %s', lexer.src)
    local pos = string.format('   %s^', string.rep(' ', n_pos - 1))
    error(string.format("%s\n%s\n%s", msg, line, pos))
end

compile_oper_pattern()

return expr_lexer
