-- Evaluation the analysis results, both for individual files and in aggregate.

local semantic_analysis = require("explcheck-semantic-analysis")

local token_types = require("explcheck-lexical-analysis").token_types
local statement_types = semantic_analysis.statement_types
local statement_subtypes = semantic_analysis.statement_subtypes

local ARGUMENT = token_types.ARGUMENT

local FUNCTION_DEFINITION = statement_types.FUNCTION_DEFINITION
local FUNCTION_DEFINITION_DIRECT = statement_subtypes.FUNCTION_DEFINITION.DIRECT

local FileEvaluationResults = {}
local AggregateEvaluationResults = {}

-- Create a new evaluation results for the analysis results of an individual file.
function FileEvaluationResults.new(cls, content, analysis_results, issues)
  -- Instantiate the class.
  local self = {}
  setmetatable(self, cls)
  cls.__index = cls
  -- Evaluate the pre-analysis information.
  local num_total_bytes = #content
  -- Evaluate the issues.
  local num_warnings = #issues.warnings
  local num_errors = #issues.errors
  -- Evaluate the results of the preprocessing.
  local num_expl_bytes
  if analysis_results.expl_ranges ~= nil then
    num_expl_bytes = 0
    for _, range in ipairs(analysis_results.expl_ranges) do
      num_expl_bytes = num_expl_bytes + #range
    end
  end
  -- Evaluate the results of the lexical analysis.
  local num_tokens
  if analysis_results.tokens ~= nil then
    num_tokens = 0
    for _, part_tokens in ipairs(analysis_results.tokens) do
      for _, token in ipairs(part_tokens) do
        assert(token.type ~= ARGUMENT)
        num_tokens = num_tokens + 1
      end
    end
  end
  local num_groupings, num_unclosed_groupings
  if analysis_results.groupings ~= nil then
    num_groupings, num_unclosed_groupings = 0, 0
    for _, part_groupings in ipairs(analysis_results.groupings) do
      for _, grouping in pairs(part_groupings) do
        num_groupings = num_groupings + 1
        if grouping.stop == nil then
          num_unclosed_groupings = num_unclosed_groupings + 1
        end
      end
    end
  end
  -- Evaluate the results of the syntactic analysis.
  local num_calls, num_call_tokens
  local num_calls_total
  if analysis_results.calls ~= nil then
    num_calls, num_call_tokens = {}, {}
    num_calls_total = 0
    for _, part_calls in ipairs(analysis_results.calls) do
      for _, call in ipairs(part_calls) do
        if num_calls[call.type] == nil then
          assert(num_call_tokens[call.type] == nil)
          num_calls[call.type] = 0
          num_call_tokens[call.type] = 0
        end
        num_calls[call.type] = num_calls[call.type] + 1
        num_call_tokens[call.type] = num_call_tokens[call.type] + #call.token_range
        num_calls_total = num_calls_total + 1
      end
    end
  end
  local num_replacement_text_calls, num_replacement_text_call_tokens
  local num_replacement_text_calls_total
  if analysis_results.replacement_texts ~= nil then
    num_replacement_text_calls, num_replacement_text_call_tokens = {}, {}
    num_replacement_text_calls_total = 0
    for _, part_replacement_texts in ipairs(analysis_results.replacement_texts) do
      for _, replacement_text_calls in ipairs(part_replacement_texts.calls) do
        for _, call in pairs(replacement_text_calls) do
          if num_replacement_text_calls[call.type] == nil then
            assert(num_replacement_text_call_tokens[call.type] == nil)
            num_replacement_text_calls[call.type] = 0
            num_replacement_text_call_tokens[call.type] = 0
          end
          num_replacement_text_calls[call.type] = num_replacement_text_calls[call.type] + 1
          num_replacement_text_call_tokens[call.type] = num_replacement_text_call_tokens[call.type] + #call.token_range
          num_replacement_text_calls_total = num_replacement_text_calls_total + 1
        end
      end
    end
  end
  -- Evaluate the results of the semantic analysis.
  local num_statements, num_statement_tokens
  local num_statements_total
  if analysis_results.statements ~= nil then
    num_statements, num_statement_tokens = {}, {}
    num_statements_total = 0
    for part_number, part_statements in ipairs(analysis_results.statements) do
      local seen_call_numbers = {}
      local part_calls = analysis_results.calls[part_number]
      for _, statement in ipairs(part_statements) do
        if num_statements[statement.type] == nil then
          assert(num_statement_tokens[statement.type] == nil)
          num_statements[statement.type] = 0
          num_statement_tokens[statement.type] = 0
        end
        num_statements[statement.type] = num_statements[statement.type] + 1
        for call_number, call in statement.call_range:enumerate(part_calls) do
          if seen_call_numbers[call_number] == nil then
            seen_call_numbers[call_number] = true
            num_statement_tokens[statement.type] = num_statement_tokens[statement.type] + #call.token_range
          end
        end
        num_statements_total = num_statements_total + 1
      end
    end
  end
  local num_replacement_text_statements, num_replacement_text_statement_tokens
  local num_replacement_text_statements_total, replacement_text_max_nesting_depth
  local seen_replacement_text_call_numbers = {}
  if analysis_results.replacement_texts ~= nil then
    num_replacement_text_statements, num_replacement_text_statement_tokens = {}, {}
    num_replacement_text_statements_total = 0
    replacement_text_max_nesting_depth = {}

    for _, part_replacement_texts in ipairs(analysis_results.replacement_texts) do
      for replacement_text_number, replacement_text_statements in ipairs(part_replacement_texts.statements) do
        seen_replacement_text_call_numbers[replacement_text_number] = {}
        local nesting_depth = part_replacement_texts.nesting_depth[replacement_text_number]
        for _, statement in pairs(replacement_text_statements) do
          if num_replacement_text_statements[statement.type] == nil then
            assert(num_replacement_text_statement_tokens[statement.type] == nil)
            num_replacement_text_statements[statement.type] = 0
            num_replacement_text_statement_tokens[statement.type] = 0
            replacement_text_max_nesting_depth[statement.type] = 0
          end
          num_replacement_text_statements[statement.type] = num_replacement_text_statements[statement.type] + 1
          if nesting_depth == 1 or statement.type ~= FUNCTION_DEFINITION or statement.subtype ~= FUNCTION_DEFINITION_DIRECT then
            -- prevent counting overlapping tokens from nested function definitions several times
            for call_number, call in statement.call_range:enumerate(part_replacement_texts.calls[replacement_text_number]) do
              if seen_replacement_text_call_numbers[replacement_text_number][call_number] == nil then
                seen_replacement_text_call_numbers[replacement_text_number][call_number] = true
                num_replacement_text_statement_tokens[statement.type]
                  = num_replacement_text_statement_tokens[statement.type] + #call.token_range
              end
            end
          end
          num_replacement_text_statements_total = num_replacement_text_statements_total + 1
          replacement_text_max_nesting_depth[statement.type]
            = math.max(replacement_text_max_nesting_depth[statement.type], nesting_depth)
        end
      end
    end
  end
  -- Initialize the class.
  self.num_total_bytes = num_total_bytes
  self.num_warnings = num_warnings
  self.num_errors = num_errors
  self.num_expl_bytes = num_expl_bytes
  self.num_tokens = num_tokens
  self.num_groupings = num_groupings
  self.num_unclosed_groupings = num_unclosed_groupings
  self.num_calls = num_calls
  self.num_call_tokens = num_call_tokens
  self.num_calls_total = num_calls_total
  self.num_replacement_text_calls = num_replacement_text_calls
  self.num_replacement_text_call_tokens = num_replacement_text_call_tokens
  self.num_replacement_text_calls_total = num_replacement_text_calls_total
  self.num_statements = num_statements
  self.num_statement_tokens = num_statement_tokens
  self.num_statements_total = num_statements_total
  self.num_replacement_text_statements = num_replacement_text_statements
  self.num_replacement_text_statement_tokens = num_replacement_text_statement_tokens
  self.num_replacement_text_statements_total = num_replacement_text_statements_total
  self.replacement_text_max_nesting_depth = replacement_text_max_nesting_depth
  return self
end

-- Create an aggregate evaluation results.
function AggregateEvaluationResults.new(cls)
  -- Instantiate the class.
  local self = {}
  setmetatable(self, cls)
  cls.__index = cls
  -- Initialize the class.
  self.num_files = 0
  self.num_total_bytes = 0
  self.num_warnings = 0
  self.num_errors = 0
  self.num_expl_bytes = 0
  self.num_tokens = 0
  self.num_groupings = 0
  self.num_unclosed_groupings = 0
  self.num_calls = {}
  self.num_call_tokens = {}
  self.num_calls_total = 0
  self.num_replacement_text_calls = {}
  self.num_replacement_text_call_tokens = {}
  self.num_replacement_text_calls_total = 0
  self.num_statements = {}
  self.num_statement_tokens = {}
  self.num_statements_total = 0
  self.num_replacement_text_statements = {}
  self.num_replacement_text_statement_tokens = {}
  self.num_replacement_text_statements_total = 0
  self.replacement_text_max_nesting_depth = {_how = math.max}
  return self
end

-- Add evaluation results of an individual file to the aggregate.
function AggregateEvaluationResults:add(evaluation_results)
  local function aggregate_table(self_table, evaluation_result_table)
    for key, value in pairs(evaluation_result_table) do
      if type(value) == "number" then  -- a simple count
        if self_table[key] == nil then
          self_table[key] = 0
        end
        assert(key ~= "_how")
        if self_table._how ~= nil then
          self_table[key] = self_table._how(self_table[key], value)
        else
          self_table[key] = self_table[key] + value
        end
      elseif type(value) == "table" then  -- a table of counts
        if self_table[key] == nil then
          self_table[key] = {}
        end
        aggregate_table(self_table[key], value)
      else
        error('Unexpected field type "' .. type(value) .. '"')
      end
    end
  end

  self.num_files = self.num_files + 1
  aggregate_table(self, evaluation_results)
end

return {
  new_file_results = function(...)
    return FileEvaluationResults:new(...)
  end,
  new_aggregate_results = function(...)
    return AggregateEvaluationResults:new(...)
  end
}