--[[

This library is intended to fix the shortfalls of using debugprofilestop() for
getting accurate sub-second timing in addons. Specifically, this library aims
to prevent any conflicts that may arrise with multiple addons using
debugprofilestart and debugprofilestop. While perfect accuracy is not
guarenteed due to the potential for an addon to load before this library and
use the original debugprofilestart/debugprofilestop functions, this library
provides a best-effort means of correcting any issues if this is the case.
The best solution is for addons to NOT use debugprofilestart() and to NOT store
a local reference to debugprofilestop(), even if they aren't using this library
directly.

-------------------------------------------------------------------------------
 
AccurateTime is hereby placed in the Public Domain
See the wowace page for usage and documentation.
Author: Sapu94 (sapu94@gmail.com)
Website: http://www.wowace.com/addons/accuratetime/
--]]

local _G = _G
local AT_VERSION = 7


-- Check if we're already loaded
-- If this is a newer version, remove the old hooks and we'll re-hook
if _G.AccurateTime then
	if _G.AccurateTime.version > AT_VERSION then
		-- newer (or same) version already loaded - abort
		return
	end
	
	-- undo hook so we can re-hook
	debugprofilestart = _G.AccurateTime._debugprofilestart
	debugprofilestop = _G.AccurateTime._debugprofilestop
end


-- setup global library reference
_G.AccurateTime = {}
AccurateTime = _G.AccurateTime
AccurateTime.version = AT_VERSION

-- Store original functions.
-- debugprofilestart should never be called, but we'll store it just in case.
AccurateTime._debugprofilestop = debugprofilestop
AccurateTime._debugprofilestart = debugprofilestart
AccurateTime._currentDebugprofilestop = debugprofilestop

-- other internal variables
AccurateTime._errorTime = AccurateTime._errorTime or 0
AccurateTime._timers = AccurateTime._timers or {}


-- Gets the current time in milliseconds. Will be directly from the original
-- debugprofilestop() with any error we've detected added in. This error would
-- come solely from an addon calling the unhooked debugprofilestart().
function AccurateTime:GetAbsTime()
	return AccurateTime._debugprofilestop() + AccurateTime._errorTime
end

-- It is up to the caller to ensure the key they are using is unique.
-- Using table reference or description strings is preferable.
-- If no key is specified, a unique key will be created and returned.
-- If the timer is already running, restart it.
-- Usage: local key = AccurateTime:GetTimer([key])
function AccurateTime:StartTimer(key)
	key = key or {}
	AccurateTime._timers[key] = AccurateTime._timers[key] or AccurateTime:GetAbsTime()
	return key
end

-- gets the current value of a timer
-- Usage: local value = AccurateTime:GetTimer(key[, silent])
function AccurateTime:GetTimer(key, silent)
	assert(key, "No key specified.")
	if silent and not AccurateTime._timers[key] then return end
	assert(AccurateTime._timers[key], "No timer currently running for the given key.")
	return AccurateTime:GetAbsTime() - AccurateTime._timers[key]
end

-- Removes a timer and returns its current value.
-- Usage: local value = AccurateTime:StopTimer(key)
function AccurateTime:StopTimer(key)
	local value = AccurateTime:GetTimer(key)
	AccurateTime._timers[key] = nil
	return value
end


-- apply hooks
debugprofilestart = function() error("You should never use debugprofilestart()!", 2) end
debugprofilestop = function() return AccurateTime._currentDebugprofilestop() end


-- Create an OnUpdate script to detect and attempt to correct other addons
-- which use the original (non-hooked) debugprofilestart(). This should in
-- theory never happen, but we'll do a best-effort correction if it does.
local function OnUpdate(self)
	local absTime = AccurateTime:GetAbsTime()
	if absTime < self.lastUpdateAbsTime then
		-- debugprofilestart() was called and the back-end timer was reset
		-- Estimate what the absolute time should be using GetTime() (converted
		-- to ms) and add it to AccurateTime._errorTime.
		local realAbsTime = self.lastUpdateAbsTime + (GetTime() - self.lastUpdateTime) * 1000
		AccurateTime._errorTime = AccurateTime._errorTime + (realAbsTime - absTime)
		if AccurateTime._errorTime > 0 then
			-- update AccurateTime._currentDebugprofilestop() to use our version of the function now that there is some error time
			AccurateTime._currentDebugprofilestop = function() return AccurateTime:GetAbsTime() end
		end
	end
	self.lastUpdateAbsTime = absTime
	self.lastUpdateTime = GetTime()
end
if not AccurateTime._frame then
	-- create frame just once
	AccurateTime._frame = CreateFrame("Frame")
	AccurateTime._frame.lastUpdateTime = GetTime()
	AccurateTime._frame.lastUpdateAbsTime = 0
end
-- upgrade the frame
AccurateTime._frame:SetScript("OnUpdate", OnUpdate)

--@debug@
function AccurateTimeTest()
	local start = debugprofilestop()
	for i=1, 10000000 do
	end
	print("loop", debugprofilestop()-start)
	start = debugprofilestop()
	for i=1, 10000000 do
		debugprofilestop()
	end
	print("overriden", debugprofilestop()-start)
	start = debugprofilestop()
	for i=1, 10000000 do
		AccurateTime._debugprofilestop()
	end
	print("raw", debugprofilestop()-start)
end
--@end-debug@