local m = {}

local function get_filename(chunk)
 local filename = chunk:match("([^\n^%(]+)")
 if not filename then
   return false, "No filename detected"
 end
 local first = filename:match("^[%./\\]+")
 if first then return filename end
 return false
end

local function get_chunks(text)
 -- parse log for particular included files
 local chunks = {}
 -- each file is enclosed in matching () brackets
 local newtext = text:gsub("(%b())", function(a)
   local chunk = string.sub(a,2,-2)
   -- if no filename had been found in the chunk, it is probably not file chunk
   -- so just return the original text
   local filename = get_filename(chunk)
   if not filename then return a end
   local children, text = get_chunks(chunk)
   table.insert(chunks, {filename = filename, text = text, children = children})
   return ""
 end)
 return chunks, newtext
end


function print_chunks(chunks, level)
 local level = level or 0
 local indent = string.rep("  ", level)
 for k,v in ipairs(chunks) do
   print(indent .. (v.filename or "?"), string.len(v.text))
   print_chunks(v.children, level + 1)
 end
end

local function parse_default_error(lines, i)
 local line = lines[i]
 -- get the error message "! msg text"
 local err = line:match("^!(.+)")
 -- the next line should contain line number where error happened
 local next_line = lines[i+1] or ""
 local msg = {}
 -- get the line number and first line of the error context
 local line_no, msg_start = next_line:match("^l%.(%d+)(.+)")
 line_no = line_no or false
 msg_start = msg_start or ""
 msg[#msg+1] = msg_start .. " <-"
 -- try to find rest of the error context.
 for x = i+2, i+5 do
   local next_line = lines[x] or ""
   -- break on blank lines
   if next_line:match("^%s*$") then break end
   msg[#msg+1] = next_line:gsub("^%s*", ""):gsub("%s$", "")
 end
 return err, line_no, table.concat(msg, " ")
end

local  function parse_linenumber_error(lines, i)
 -- parse errors from log created with the -file-line-number option
 local line = lines[i]
 local filename, line_no, err = line:match("^([^%:]+)%:(%d+)%:%s*(.*)")
 local msg = {}
 -- get error context
 for x = i+1, i+2 do
   local next_line = lines[x] or ""
   -- break on blank lines
   if next_line:match("^%s*$") then break end
   msg[#msg+1] = next_line:gsub("^%s*", ""):gsub("%s$", "")
 end
 -- insert mark to the error
 if #msg > 1 then
   table.insert(msg, 2, "<-")
 end
 return err, line_no, table.concat(msg, " ")
end

--- get error messages, linenumbers and contexts from a log file chunk
---@param text string chunk from the long file where we should find errors
---@return table errors error messages
---@return table error_lines error line number
---@return table error_messages error line contents
local function parse_errors(text)
 local lines = {}
 local errors = {}
 local find_line_no = false
 local error_lines = {}
 local error_messages = {}
 for line in text:gmatch("([^\n]+)") do
   lines[#lines+1] = line
 end
 for i = 1, #lines do
   local line = lines[i]
   local err, line_no, msg
   if line:match("^!(.+)") then
     err, line_no, msg = parse_default_error(lines, i)
   elseif line:match("^[^%:]+%:%d+%:.+") then
     err, line_no, msg = parse_linenumber_error(lines, i)
   end
   if err then
     errors[#errors+1] = err
     error_lines[#errors] = line_no
     error_messages[#errors] = msg
   end
 end
 return errors, error_lines, error_messages
end


local function get_errors(chunks, errors)
 local errors =  errors or {}
 for _, v in ipairs(chunks) do
   local current_errors, error_lines, error_contexts = parse_errors(v.text)
   for i, err in ipairs(current_errors) do
     table.insert(errors, {filename = v.filename, error = err, line = error_lines[i], context = error_contexts[i] })
   end
   errors = get_errors(v.children, errors)
 end
 return errors
end

function m.get_missing_4ht_files(log)
 local used_files = {}
 local used_4ht_files = {}
 local missing_4ht_files = {}
 local pkg_names = {sty=true, cls=true}
 for filename, ext in log:gmatch("[^%s]-([^%/^%\\^%.%s]+)%.([%w][%w]+)") do
   -- break ak
   if ext == "aux" then break end
   if pkg_names[ext] then
     used_files[filename .. "." .. ext] = true
   elseif ext == "4ht" then
     used_4ht_files[filename] = true
   end
 end
 for filename, _ in pairs(used_files) do
   if not used_4ht_files[mkutils.remove_extension(filename)] then
     table.insert(missing_4ht_files, filename)
   end
 end
 return missing_4ht_files
end


function m.parse(log)
 local chunks, newtext = get_chunks(log)
 -- save the unparsed text that contains system messages
 table.insert(chunks, {text = newtext, children = {}})
 -- print_chunks(chunks)
 local errors = get_errors(chunks)
 -- for _,v in ipairs(errors) do
   -- print("error", v.filename, v.line, v.error)
 -- end
 return errors, chunks
end


m.print_chunks = print_chunks

return m