-- 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 = "sha1", module = "vendor.sha1"},
	{name = "load", module = "cache"},
	{name = "update", module = "cache"},
	{name = "decode", module = "decoder"},
	{name = "parse", module = "parser"},
	{name = "dump_check_result", module = "serializer"},
	{name = "load_check_result", module = "serializer"},
	{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