-- Require luasocket only when needed.
local socket

local profiler = {}

local metrics = {
   {name = "Wall", get = function() return socket.gettime() end},
   {name = "CPU", get = os.clock},
   {name = "Memory", get = function() return collectgarbage("count") end}
}

local functions = {
   {name = "load", module = "cache"},
   {name = "update", module = "cache"},
   {name = "decode", module = "decoder"},
   {name = "parse", module = "parser"},
   {name = "run", module = "stages.unwrap_parens"},
   {name = "run", module = "stages.parse_inline_options"},
   {name = "run", module = "stages.linearize"},
   {name = "run", module = "stages.name_functions"},
   {name = "run", module = "stages.resolve_locals"},
   {name = "run", module = "stages.detect_bad_whitespace"},
   {name = "run", module = "stages.detect_cyclomatic_complexity"},
   {name = "run", module = "stages.detect_empty_blocks"},
   {name = "run", module = "stages.detect_empty_statements"},
   {name = "run", module = "stages.detect_globals"},
   {name = "run", module = "stages.detect_reversed_fornum_loops"},
   {name = "run", module = "stages.detect_unbalanced_assignments"},
   {name = "run", module = "stages.detect_uninit_accesses"},
   {name = "run", module = "stages.detect_unreachable_code"},
   {name = "run", module = "stages.detect_unused_fields"},
   {name = "run", module = "stages.detect_unused_locals"},
   {name = "filter", module = "filter"},
   {name = "normalize", module = "options"}
}

local stats = {}
local start_values = {}

local function start_phase(name)
   for _, metric in ipairs(metrics) do
      start_values[metric][name] = metric.get()
   end
end

local function stop_phase(name)
   for _, metric in ipairs(metrics) do
      local increment = metric.get() - start_values[metric][name]
      stats[metric][name] = (stats[metric][name] or 0) + increment
   end
end

local phase_stack = {}

local function push_phase(name)
   local prev_name = phase_stack[#phase_stack]

   if prev_name then
      stop_phase(prev_name)
   end

   table.insert(phase_stack, name)
   start_phase(name)
end

local function pop_phase(name)
   assert(name == table.remove(phase_stack))
   stop_phase(name)
   local prev_name = phase_stack[#phase_stack]

   if prev_name then
      start_phase(prev_name)
   end
end

local function continue_wrapper(name, ...)
   pop_phase(name)
   return ...
end

local function wrap(fn, name)
   return function(...)
      push_phase(name)
      return continue_wrapper(name, fn(...))
   end
end

local function patch(fn)
   local mod = require("luacheck." .. fn.module)
   local orig = mod[fn.name]
   local new = wrap(orig, fn.module .. "." .. fn.name)
   mod[fn.name] = new
end

function profiler.init()
   socket = require "socket"
   collectgarbage("stop")

   for _, metric in ipairs(metrics) do
      stats[metric] = {}
      start_values[metric] = {}
   end

   for _, fn in ipairs(functions) do
      patch(fn)
   end

   push_phase("other")
end

function profiler.report()
   pop_phase("other")

   for _, metric in ipairs(metrics) do
      local names = {}
      local total = 0

      for name, value in pairs(stats[metric]) do
         table.insert(names, name)
         total = total + value
      end

      table.sort(names, function(name1, name2)
         local stats1 = stats[metric][name1]
         local stats2 = stats[metric][name2]

         if stats1 ~= stats2 then
            return stats1 > stats2
         else
            return name1 < name2
         end
      end)

      print(metric.name)
      print()

      for _, name in ipairs(names) do
         print(("%s - %f (%f%%)"):format(name, stats[metric][name], stats[metric][name] / total * 100))
      end

      print(("Total - %f"):format(total))
      print()
   end
end

return profiler
