-- Copyright 2022-2023 Mitchell. See LICENSE.
-- LPeg lexer for tool output.
-- If a warning or error is recognized, tags its filename, line, column (if available),
-- and message, and sets the line state to 1 for an error (first bit), and 2 for a warning
-- (second bit).
-- This is similar to Lexilla's errorlist lexer.
local lexer = lexer
local starts_line = lexer.starts_line
local P, S = lpeg.P, lpeg.S

local lex = lexer.new(..., {lex_by_line = true})

-- Tags a pattern as plain text.
local function text(patt) return lex:tag(lexer.DEFAULT, patt) end

-- Tags a pattern as a filename.
local function filename(patt) return lex:tag('filename', patt) end

-- Typical line and column number patterns.
local line = text('line ') ^ -1 * lex:tag('line', lexer.dec_num)
local column = lex:tag('column', lexer.dec_num)

-- Tags a pattern as an error/warning/etc. message.
local function message(patt) return lex:tag('message', patt) end

-- Immediately marks the current line as an error.
-- This should only be specified at the end of a rule, or else LPeg may backtrack and mistakenly
-- mark a non-error line.
local function mark_error(_, pos)
    lexer.line_state[lexer.line_from_position(pos)] = 1
    return true
end

-- Immediately marks the current line as a warning.
-- This should only be specified at the end of a rule, or else LPeg may backtrack and mistakenly
-- mark a non-warning line.
local function mark_warning(_, pos)
    lexer.line_state[lexer.line_from_position(pos)] = 2
    return true
end

-- filename:line: message (ruby)
-- filename:line:col: message (c, cpp, go, ...)
-- filename: line X: message (bash)
local c_filename = filename((lexer.nonnewline - ':') ^ 1)
local colon = text(':' * P(' ') ^ -1)
local warning = message(lexer.to_eol('warning: ')) * mark_warning
local note = message(lexer.to_eol('note: ')) -- do not mark
local error = message(lexer.to_eol()) * mark_error
lex:add_rule('common',
             starts_line(c_filename) * colon * line * colon * (column * colon) ^
                 -1 * (warning + note + error))

-- prog: filename:line: message (awk, lua)
lex:add_rule('prog',
             starts_line(text(lexer.word)) * colon * c_filename * colon * line *
                 colon * (warning + error))

-- File "filename", line X (python)
local py_filename = filename((lexer.nonnewline - '"') ^ 1)
lex:add_rule('python',
             starts_line(text('File "'), true) * py_filename * text('", ') *
                 line * mark_error)

-- filename(line): error: message (d, cuda)
local lparen, rparen = text('('), text(')')
local d_filename = filename((lexer.nonnewline - '(') ^ 1)
local d_error = message(lexer.to_eol(S('Ee') * 'rror')) * mark_error
lex:add_rule('dmd',
             starts_line(d_filename) * lparen * line * rparen * colon * d_error)

-- "filename" line X: message (gnuplot)
local gp_filename = filename((lexer.nonnewline - '"') ^ 1)
lex:add_rule('gnuplot',
             starts_line(text('"')) * gp_filename * text('" ') * line * colon *
                 error)

-- at com.path(filename:line) (java)
lex:add_rule('java',
             starts_line(text('at ' * (lexer.nonnewline - '(') ^ 1), true) *
                 lparen * c_filename * colon * line * rparen * mark_error)

-- message in filename on line X (php)
lex:add_rule('php',
             starts_line(message((lexer.nonnewline - ' in ') ^ 1)) *
                 text(' in ') * filename((lexer.nonnewline - ' on ') ^ 1) *
                 text(' on ') * line * mark_error)

-- filename(line, col): message (vb, csharp, fsharp, ...)
lex:add_rule('vb',
             starts_line(filename((lexer.nonnewline - '(') ^ 1)) * lparen * line *
                 text(', ') * column * rparen * colon * error)

-- message at filename line X (perl)
lex:add_rule('perl',
             starts_line(message((lexer.nonnewline - ' at ') ^ 1)) *
                 text(' at ') * filename((lexer.nonnewline - ' line ') ^ 1) *
                 text(' line ') * line * mark_error)

-- CMake Error at filename:line: (cmake)
lex:add_rule('cmake',
             starts_line(text('CMake Error at ')) * c_filename * colon * line *
                 colon * mark_error)

lex:add_rule('any_line', lex:tag(lexer.DEFAULT, lexer.to_eol()))

return lex
