-- A registry of warnings and errors identified by different processing steps.

local Issues = {}

function Issues.new(cls)
 -- Instantiate the class.
 local self = {}
 setmetatable(self, cls)
 cls.__index = cls
 -- Initialize the class.
 self.errors = {}
 self.warnings = {}
 self.seen_issues = {}
 self.ignored_issues = {}
 return self
end

-- Normalize an issue identifier.
function Issues._normalize_identifier(identifier)
 return identifier:lower()
end

-- Convert an issue identifier to either a table of warnings or a table of errors.
function Issues:_get_issue_table(identifier)
 identifier = self._normalize_identifier(identifier)
 local prefix = identifier:sub(1, 1)
 if prefix == "s" or prefix == "w" then
   return self.warnings
 elseif prefix == "t" or prefix == "e" then
   return self.errors
 else
   assert(false, 'Identifier "' .. identifier .. '" has an unknown prefix "' .. prefix .. '"')
 end
end

-- Add an issue to the table of issues.
function Issues:add(identifier, message, range)
 identifier = self._normalize_identifier(identifier)

 -- Discard duplicate issues.
 local range_start = (range ~= nil and range:start()) or false
 local range_end = (range ~= nil and range:stop()) or false
 if self.seen_issues[identifier] == nil then
   self.seen_issues[identifier] = {}
 end
 if self.seen_issues[identifier][range_start] == nil then
   self.seen_issues[identifier][range_start] = {}
 end
 if self.seen_issues[identifier][range_start][range_end] == nil then
   self.seen_issues[identifier][range_start][range_end] = true
 else
   return
 end

 -- Construct the issue.
 local issue = {identifier, message, range}

 -- Determine if the issue should be ignored.
 for _, ignore_issue in ipairs(self.ignored_issues) do
   if ignore_issue(issue) then
     return
   end
 end

 -- Add the issue to the table of issues.
 local issue_table = self:_get_issue_table(identifier)
 table.insert(issue_table, issue)
end

-- Prevent issues from being present in the table of issues.
function Issues:ignore(identifier, range)
 if identifier ~= nil then
   identifier = self._normalize_identifier(identifier)
 end

 -- Determine which issues should be ignored.
 local function match_issue_range(issue_range)
   return (
     issue_range:start() >= range:start() and issue_range:start() <= range:stop()  -- issue starts within range
     or issue_range:start() <= range:start() and issue_range:stop() >= range:stop()  -- issue is in middle of range
     or issue_range:stop() >= range:start() and issue_range:stop() <= range:stop()  -- issue ends within range
   )
 end
 local function match_issue_identifier(issue_identifier)
   return issue_identifier == identifier
 end

 local ignore_issue, issue_tables
 if identifier == nil then
   -- Prevent any issues within the given range.
   assert(range ~= nil)
   issue_tables = {self.warnings, self.errors}
   ignore_issue = function(issue)
     local issue_range = issue[3]
     if issue_range == nil then  -- file-wide issue
       return false
     else  -- ranged issue
       return match_issue_range(issue_range)
     end
   end
 elseif range == nil then
   -- Prevent any issues with the given identifier.
   assert(identifier ~= nil)
   issue_tables = {self:_get_issue_table(identifier)}
   ignore_issue = function(issue)
     local issue_identifier = issue[1]
     return match_issue_identifier(issue_identifier)
   end
 else
   -- Prevent any issues with the given identifier that are also either within the given range or file-wide.
   assert(range ~= nil and identifier ~= nil)
   issue_tables = {self:_get_issue_table(identifier)}
   ignore_issue = function(issue)
     local issue_identifier = issue[1]
     local issue_range = issue[3]
     if issue_range == nil then  -- file-wide issue
       return match_issue_identifier(issue_identifier)
     else  -- ranged issue
       return match_issue_range(issue_range) and match_issue_identifier(issue_identifier)
     end
   end
 end

 -- Remove the issue if it has already been added.
 for _, issue_table in ipairs(issue_tables) do
   local filtered_issues = {}
   for _, issue in ipairs(issue_table) do
     if not ignore_issue(issue) then
       table.insert(filtered_issues, issue)
     end
   end
   for issue_index, issue in ipairs(filtered_issues) do
     issue_table[issue_index] = issue
   end
   for issue_index = #filtered_issues + 1, #issue_table, 1 do
     issue_table[issue_index] = nil
   end
 end

 -- Prevent the issue from being added later.
 table.insert(self.ignored_issues, ignore_issue)
end

-- Check whether two registries only contain issues with the same codes.
function Issues:has_same_codes_as(other)
 -- Collect codes of all issues.
 local self_codes, other_codes = {}, {}
 for _, table_name in ipairs({'warnings', 'errors'}) do
   for _, tables in ipairs({{self[table_name], self_codes}, {other[table_name], other_codes}}) do
     local issue_table, codes = table.unpack(tables)
     for _, issue in ipairs(issue_table) do
       local code = issue[1]
       codes[code] = true
     end
   end
 end
 -- Check whether this registry has any extra codes.
 for code, _ in pairs(self_codes) do
   if other_codes[code] == nil then
     return false
   end
 end
 -- Check whether the other registry has any extra codes.
 for code, _ in pairs(other_codes) do
   if self_codes[code] == nil then
     return false
   end
 end
 return true
end

-- Sort the warnings/errors using location as the primary key.
function Issues.sort(warnings_and_errors)
 local sorted_warnings_and_errors = {}
 for _, issue in ipairs(warnings_and_errors) do
   local identifier = issue[1]
   local message = issue[2]
   local range = issue[3]
   table.insert(sorted_warnings_and_errors, {identifier, message, range})
 end
 table.sort(sorted_warnings_and_errors, function(a, b)
   local a_identifier, b_identifier = a[1], b[1]
   local a_range, b_range = (a[3] and a[3]:start()) or 0, (b[3] and b[3]:start()) or 0
   return a_range < b_range or (a_range == b_range and a_identifier < b_identifier)
 end)
 return sorted_warnings_and_errors
end

return function()
 return Issues:new()
end