-- ------------------------------------------------------------------------------ --
--                                TradeSkillMaster                                --
--                          https://tradeskillmaster.com                          --
--    All Rights Reserved - Detailed license information included with addon.     --
-- ------------------------------------------------------------------------------ --

if not TSMDEV then return end

local _, TSM = ...
TSMDEV.Profiling = {}
local Profiling = TSMDEV.Profiling
local Math = TSM.Include("Util.Math")
local private = {
	startTime = nil,
	nodes = {},
	nodeRuns = {},
	nodeStart = {},
	nodeTotal = {},
	nodeMaxContext = {},
	nodeMaxTime = {},
	nodeParent = {},
	nodeStack = {},
}
local NODE_PATH_SEP = "`"



-- ============================================================================
-- Module Functions
-- ============================================================================

--- Starts profiling.
function Profiling.Start()
	assert(not private.startTime, "Profiling already started")
	private.startTime = debugprofilestop()
end

--- Starts profiling of a node.
-- Profiling must have been started for this to have any effect.
-- @tparam string node The name of the profiling node
function Profiling.StartNode(node)
	if not private.startTime then
		-- profiling is not running
		return
	end
	local nodeStackLen = #private.nodeStack
	local parentNode = nodeStackLen > 0 and table.concat(private.nodeStack, NODE_PATH_SEP) or nil
	private.nodeStack[nodeStackLen + 1] = node
	node = table.concat(private.nodeStack, NODE_PATH_SEP)
	if private.nodeStart[node] then
		error("Node already started", 2)
	end
	if not private.nodeTotal[node] then
		tinsert(private.nodes, node)
		private.nodeTotal[node] = 0
		private.nodeRuns[node] = 0
		private.nodeMaxContext[node] = nil
		private.nodeMaxTime[node] = 0
		private.nodeParent[node] = parentNode
	elseif private.nodeParent[node] ~= parentNode then
		error("Node changed parents", 2)
	end
	private.nodeStart[node] = debugprofilestop()
end

--- Ends profiling of a node.
-- Profiling of this node must have been started for this to have any effect.
-- @tparam string node The name of the profiling node
-- @param[opt] arg An extra argument which is printed if this invocation represents the max duration for the node
function Profiling.EndNode(node, arg)
	if not private.startTime then
		-- profiling is not running
		return
	end
	local endTime = debugprofilestop()
	local nodeStackLen = #private.nodeStack
	if node ~= private.nodeStack[nodeStackLen] then
		error("Node isn't at the top of the stack", 2)
	end
	node = table.concat(private.nodeStack, NODE_PATH_SEP)
	if not private.nodeStart[node] then
		error("Node hasn't been started", 2)
	end
	private.nodeStack[nodeStackLen] = nil
	local nodeTime = endTime - private.nodeStart[node]
	private.nodeRuns[node] = private.nodeRuns[node] + 1
	private.nodeTotal[node] = private.nodeTotal[node] + nodeTime
	private.nodeStart[node] = nil
	if nodeTime > private.nodeMaxTime[node] then
		private.nodeMaxContext[node] = arg
		private.nodeMaxTime[node] = nodeTime
	end
end

--- Ends profiling and prints the results to chat.
-- @tparam[opt=0] number minTotalTime The minimum total time to print the profiling info
function Profiling.End(minTotalTime)
	if not private.startTime then
		-- profiling is not running
		return
	end
	local totalTime = debugprofilestop() - private.startTime
	if totalTime > (minTotalTime or 0) then
		print(format("Total: %.03f", Math.Round(totalTime, 0.001)))
		for _, node in ipairs(private.nodes) do
			local parentNode = private.nodeParent[node]
			local parentTotalTime = nil
			if parentNode then
				parentTotalTime = private.nodeTotal[parentNode]
			else
				parentTotalTime = totalTime
			end
			local nodeTotalTime = Math.Round(private.nodeTotal[node], 0.001)
			local pctTime = Math.Round(nodeTotalTime * 100 / parentTotalTime)
			local nodeRuns = private.nodeRuns[node]
			local nodeMaxContext = private.nodeMaxContext[node]
			local level = private.GetLevel(node)
			local name = strmatch(node, NODE_PATH_SEP.."?([^"..NODE_PATH_SEP.."]+)$")
			if nodeMaxContext ~= nil then
				local nodeMaxTime = private.nodeMaxTime[node]
				print(format("%s%s | %d%% | %.03f | %d | %.03f | %s", strrep("  ", level), name, pctTime, nodeTotalTime, nodeRuns, nodeMaxTime, tostring(nodeMaxContext)))
			else
				print(format("%s%s | %d%% | %.03f | %d", strrep("  ", level), name, pctTime, nodeTotalTime, nodeRuns))
			end
		end
	end
	private.startTime = nil
	wipe(private.nodes)
	wipe(private.nodeRuns)
	wipe(private.nodeStart)
	wipe(private.nodeTotal)
	wipe(private.nodeMaxContext)
	wipe(private.nodeMaxTime)
end

--- Checks whether or not we're currently profiling.
-- @treturn boolean Whether or not we're currently profiling.
function Profiling.IsActive()
	return private.startTime and true or false
end

--- Gets the total memory used by TSM.
-- @treturn number The amount of memory being used in bytes
function Profiling.GetMemoryUsage()
	collectgarbage()
	UpdateAddOnMemoryUsage("TradeSkillMaster")
	return GetAddOnMemoryUsage("TradeSkillMaster") * 1024
end



-- ============================================================================
-- Private Helper Functions
-- ============================================================================

function private.GetLevel(node)
	local level = 0
	while node do
		level = level + 1
		node = private.nodeParent[node]
	end
	return level
end
