local parser = require "luacheck.parser"
local linearize = require "luacheck.linearize"
local analyze = require "luacheck.analyze"
local reachability = require "luacheck.reachability"
local inline_options = require "luacheck.inline_options"
local utils = require "luacheck.utils"
local check_whitespace = require "luacheck.whitespace"
local detect_globals = require "luacheck.detect_globals"

local function is_secondary(value)
   return value.secondaries and value.secondaries.used
end

local ChState = utils.class()

function ChState:__init()
   self.warnings = {}
end

function ChState:warn(warning, implicit_self)
   if not warning.end_column then
      warning.end_column = implicit_self and warning.column or (warning.column + #warning.name - 1)
   end

   table.insert(self.warnings, warning)
end

local action_codes = {
   set = "1",
   mutate = "2",
   access = "3"
}

local type_codes = {
   var = "1",
   func = "1",
   arg = "2",
   loop = "3",
   loopi = "3"
}

-- `index` describes an indexing, where `index[1]` is a global node
-- and other items describe keys: each one is a string node, "not_string",
-- or "unknown". `node` is literal base node that's indexed.
-- E.g. in `local a = table.a; a.b = "c"` `node` is `a` node of the second
-- statement and `index` describes `table.a.b`.
-- `index.previous_indexing_len` is optional length of prefix of `index` array representing last assignment
-- in the aliasing chain, e.g. `2` in the previous example (because last indexing
-- is `table.a`).
function ChState:warn_global(node, index, is_lhs, is_top_scope)
   local global = index[1]
   local action = is_lhs and (#index == 1 and "set" or "mutate") or "access"

   local indexing = {}

   for i, field in ipairs(index) do
      if field == "unknown" then
         indexing[i] = true
      elseif field == "not_string" then
         indexing[i] = false
      else
         indexing[i] = field[1]
      end
   end

   -- and filter out the warning if the base of last indexing is already
   -- undefined and has been reported.
   -- E.g. avoid useless warning in the second statement of `local t = tabell; t.concat(...)`.
   self:warn({
      code = "11" .. action_codes[action],
      name = global[1],
      indexing = indexing,
      previous_indexing_len = index.previous_indexing_len,
      line = node.location.line,
      column = node.location.column,
      end_column = node.location.column + #node[1] - 1,
      top = is_top_scope and (action == "set") or nil,
      indirect = node ~= global or nil
   })
end

-- W12* (read-only global) and W131 (unused global) are patched in during filtering.

function ChState:warn_unused_variable(value, recursive, self_recursive, useless)
   self:warn({
      code = "21" .. type_codes[value.var.type],
      name = value.var.name,
      line = value.location.line,
      column = value.location.column,
      secondary = is_secondary(value) or nil,
      func = (value.type == "func") or nil,
      mutually_recursive = not self_recursive and recursive or nil,
      recursive = self_recursive,
      self = value.var.self,
      useless = value.var.name == "_" and useless or nil
   }, value.var.self)
end

function ChState:warn_unset(var)
   self:warn({
      code = "221",
      name = var.name,
      line = var.location.line,
      column = var.location.column
   })
end

function ChState:warn_unaccessed(var, mutated)
   -- Mark as secondary if all assigned values are secondary.
   -- It is guaranteed that there are at least two values.
   local secondary = true

   for _, value in ipairs(var.values) do
      if not value.empty and not is_secondary(value) then
         secondary = nil
         break
      end
   end

   self:warn({
      code = "2" .. (mutated and "4" or "3") .. type_codes[var.type],
      name = var.name,
      line = var.location.line,
      column = var.location.column,
      secondary = secondary
   }, var.self)
end

function ChState:warn_unused_value(value, mutated)
   self:warn({
      code = "3" .. (mutated and "3" or "1") .. type_codes[value.type],
      name = value.var.name,
      line = value.location.line,
      column = value.location.column,
      secondary = is_secondary(value) or nil,
   }, value.type == "arg" and value.var.self)
end

function ChState:warn_unused_field_value(node)
   self:warn({
      code = "314",
      field = node.field,
      index = node.is_index,
      line = node.location.line,
      column = node.location.column,
      end_column = node.location.column + #node.first_token - 1
   })
end

function ChState:warn_uninit(node, mutation)
   self:warn({
      code = mutation and "341" or "321",
      name = node[1],
      line = node.location.line,
      column = node.location.column
   })
end

function ChState:warn_redefined(var, prev_var, same_scope)
   if var.name ~= "..." then
      self:warn({
         code = "4" .. (same_scope and "1" or (var.line == prev_var.line and "2" or "3")) .. type_codes[prev_var.type],
         name = var.name,
         line = var.location.line,
         column = var.location.column,
         self = var.self and prev_var.self,
         prev_line = prev_var.location.line,
         prev_column = prev_var.location.column
      }, var.self)
   end
end

function ChState:warn_unreachable(location, unrepeatable, token)
   self:warn({
      code = "51" .. (unrepeatable and "2" or "1"),
      line = location.line,
      column = location.column,
      end_column = location.column + #token - 1
   })
end

function ChState:warn_unused_label(label)
   self:warn({
      code = "521",
      label = label.name,
      line = label.location.line,
      column = label.location.column,
      end_column = label.end_column
   })
end

function ChState:warn_unbalanced(location, shorter_lhs)
   -- Location points to `=`.
   self:warn({
      code = "53" .. (shorter_lhs and "1" or "2"),
      line = location.line,
      column = location.column,
      end_column = location.column
   })
end

function ChState:warn_empty_block(location, do_end)
   -- Location points to `do`, `then` or `else`.
   self:warn({
      code = "54" .. (do_end and "1" or "2"),
      line = location.line,
      column = location.column,
      end_column = location.column + (do_end and 1 or 3)
   })
end

function ChState:warn_empty_statement(location)
   self:warn({
      code = "551",
      line = location.line,
      column = location.column,
      end_column = location.column
   })
end

local function check_or_throw(src)
   local ast, comments, code_lines, line_endings, semicolons = parser.parse(src)
   local chstate = ChState()
   local line = linearize(chstate, ast)

   for _, location in ipairs(semicolons) do
      chstate:warn_empty_statement(location)
   end

   local lines = utils.split_lines(src)
   local line_lengths = utils.map(function(s) return #s end, lines)
   check_whitespace(chstate, lines, line_endings)
   analyze(chstate, line)
   reachability(chstate, line)
   detect_globals(chstate, line)
   local events, per_line_opts = inline_options.get_events(ast, comments, code_lines, chstate.warnings)
   return {events = events, per_line_options = per_line_opts, line_lengths = line_lengths}
end

--- Checks source.
-- Returns a table with results, with the following fields:
--    `events`: array of issues and inline option events (options, push, or pop).
--    `per_line_options`: map from line numbers to arrays of inline option events.
local function check(src)
   local ok, res = utils.try(check_or_throw, src)

   if ok then
      return res
   elseif utils.is_instance(res.err, parser.SyntaxError) then
      local syntax_error = {
         code = "011",
         line = res.err.line,
         column = res.err.column,
         end_column = res.err.end_column,
         msg = res.err.msg
      }

      return {events = {syntax_error}, per_line_options = {}, line_lengths = {}}
   else
      error(res, 0)
   end
end

return check
