local DGV = DugisGuideViewer
local _

function DGV:NoOp()
end

local unpack, ipairs, tremove, assert, tinsert, select, tContains, type, GetTime, tostring, wipe, setmetatable, next = 
	unpack, ipairs, tremove, assert, tinsert, select, tContains, type, GetTime, tostring, wipe, setmetatable, next

local GetCreateTable
local function BindToAutoroutineLifetime(object, disposeFunc)
	local autoroutine = DGV.currentAutoroutine
	if not autoroutine then return object end
	autoroutine.boundObjects[object] = disposeFunc
	return object
end

local function TryReleaseLifetime(object, disposeFunc)
	local autoroutine = DGV.currentAutoroutine
	if not autoroutine then return object end
	if autoroutine.boundObjects then
		local func = autoroutine.boundObjects[object]
		if func==disposeFunc then
			autoroutine.boundObjects[object] = nil
--DGV:DebugFormat("TryReleaseLifetime hit", "object", tostring(object))
			return object
		end
	end
	return object
--DGV:DebugFormat("TryReleaseLifetime miss")
end

local autoroutines
local function IterateAutoroutines(invariant, control)
	if not autoroutines then return end
	if not control then control = 0 end
	control = control +1
	return autoroutines[control] and control, autoroutines[control]
end

local function IsBoundToAutoroutineLifetime(object)
	for _,autoroutine in IterateAutoroutines do
		if autoroutine.boundObjects and autoroutine.boundObjects[object] then
			return autoroutine.boundObjects[object] and true
		end
	end
end

local function DisposeAllBoundObjects(autoroutine)
	for object,disposeFunc in pairs(autoroutine.boundObjects) do
		if type(object)=="table" and object~=autoroutine.boundObjects then
			disposeFunc(object)
		end
	end
end

-- Pooled Table Functions
local function Unpack(self, from, to)
	if self.n then
		return unpack(self, from or 1, to or self.n)
	else
		return unpack(self, from, to)
	end
end
DGV.tUnpack = Unpack

-- function DGV.CountTables()
	-- local count = 0
	-- for _,_ in pairs(DGV.tables) do
		-- count = count+1
	-- end
	-- return count
-- end

-- function DGV.GetTable(index)
	-- local count = 0
	-- for t in pairs(DGV.tables) do
		-- count = count+1
		-- if count==index then
			-- return t
		-- end
	-- end
	-- return count
-- end

local tablePool = {}
-- local tables = {}
DGV.tablePool = tablePool
--DGV.tables = tables
local function Pool(self, from, to)
	-- tables[self] = nil
	tablePool[self] = self
	TryReleaseLifetime(self, Pool)
	return Unpack(self, from, to)
end
DGV.tPool = Pool

local function Insert(self, value, pos)
	local n = self.n or 0
	if (not n or n == 0) and value then 
        self.n = self.n + 1; 
		if pos then 
            return tinsert(self, pos, value) 
		else  
            return tinsert(self, value)  
        end
	end
	
	local newN = (n and n+1) or #self+1
	if not pos then
		pos = newN
	end
	self.n = newN
	for i=self.n,pos+1,-1 do
		self[i] = self[i-1]
	end
	self[pos] = value
end

local function InsertList(self, ...)
    local inputN = select("#", ...)
	local exisingN = self.n or #self
	local newN = inputN + exisingN
	for i=exisingN+1,newN do
		Insert(self, (select(i-exisingN, ...)), i)
	end
end

local function PrependList(self, ...)
	local inputN = select("#", ...)
	for i=1,inputN do
		Insert(self, (select(i, ...)), i)
	end
end

local function SetList(self, ...)
	wipe(self)
	InsertList(self, ...)
end

local function IPairsIterator(invariant, control)
	control = control + 1
	local n = invariant.n
	if n<control then return end
	return control, invariant[control]
end

local function IPairs(self)
	if not self.n or self.n == 0 then return ipairs(self) end
	return IPairsIterator, self, 0
end

local function IndexOfFirst(self, item)
	for i,value in IPairs(self) do
		if value==item then
			return i
		end
	end
end

DGV.tIndexOfFirst = IndexOfFirst

local function Remove(self, index)
	local n = self.n or 0
	if n == 0 then
		return tremove(self, index)
	end
	local match = self[index]
	for i=index+1,n do
		self[i-1] = self[i]
	end
	self[n] = nil
	self.n = n-1
	return match
end


local function Length(self)
	return self.n
end

local function RemoveFirst(self, item)
	local matchIndex = IndexOfFirst(self, item)
	if matchIndex then
		return Remove(self, matchIndex)
	end
end
DGV.tRemoveFirst = RemoveFirst

local tableClassIndexTable = {
	["n"] = 0,
	["Length"] = Length,
	["InsertList"] = InsertList,
	["PrependList"] = PrependList,
	["SetList"] = SetList,
	["Insert"] = Insert,
	["Pool"] = Pool,
	["IPairs"] = IPairs,
	["IndexOfFirst"] = IndexOfFirst,
	["RemoveFirst"] = RemoveFirst,
	["Remove"] = Remove,
	["Unpack"] = Unpack,
	["BindToAutoroutineLifetime"] = BindToAutoroutineLifetime,
	["IsBoundToAutoroutineLifetime"] = IsBoundToAutoroutineLifetime,
	["TryReleaseLifetime"] = TryReleaseLifetime
}
local tableClassMetaTable = { __index = tableClassIndexTable }
GetCreateTable = function(arg, ...)
	local t = next(tablePool)
	if not t then
		t = {}
		setmetatable(t, tableClassMetaTable)
	else
		tablePool[t] = nil
		wipe(t)
	end
	--assert(not tContains(tables, t), "GetCreateTable adding existing table again")
	--assert(not tContains(tablePool, t), "GetCreateTable return value cannot exist in table pool")
	--tables[t] = true
	if arg then
		InsertList(t, arg, ...)
	end
	return t
end
DGV.GetCreateTable = GetCreateTable

function DGV.IterateReturns(...)
	local iteratedVarargs = GetCreateTable(...):BindToAutoroutineLifetime(tPool)

	return function (invariant, control)
		if not control then control = 0 end
		control = control + 1
		if control <= iteratedVarargs:Length() then
			return control, iteratedVarargs[control]
		else
			iteratedVarargs:Pool()
		end
	end
end

local function SelectControlAndIndex(selectIndex, control, ...)
	return control, select(selectIndex, control, ...)
end

function DGV.UnpackIterator(iterator, selectIndex, invariant, initialControl)
	local control = initialControl
	if not selectIndex then selectIndex = 1 end
	local temp = GetCreateTable()
	repeat
		control, target = SelectControlAndIndex(selectIndex, iterator(invariant, control))
		if target then
			temp:Insert(target)
		end
	until(not control or not target)
	return temp:Pool()
end

--Default time limit
local THEORETICAL_TIME_LIMIT = 20 --100 seems to work w/o "script too long", but tanks FPS

local functionKey2TimeLimit = {}
functionKey2TimeLimit["RecalculateRoutes"] = 10

local function GetTicks()
  return debugprofilestop()
end
DGV.GetTicks = GetTicks

local function TakeNext(invariant)
	if #invariant==0 then return end
	return invariant:Remove(1)
end

local function DisposeAutoroutine(autoroutine)
--DGV:DebugFormat("DisposeAutoroutine", "key", autoroutine.key,)
	DisposeAllBoundObjects(autoroutine)
	Pool(autoroutine.boundObjects)
	Pool(autoroutine)
end

local function AutoroutinesOnUpdate()
	if autoroutines then
		local startTime = GetTicks()
		
		local i=1
		while true do
			local autoroutine = autoroutines[i]
--autoroutine.startTime = startTime
			if not autoroutine.interrupt then
				-- local result = GetCreateTable()
				DGV.currentAutoroutine = autoroutine
				-- InsertList(result, coroutine.resume(autoroutine.coroutine, Unpack(autoroutine)))
				
				local status = coroutine.status(autoroutine.coroutine)
				local result, message
				
				if status ~="dead" and not UnitAffectingCombat("player") then
					result, message = coroutine.resume(autoroutine.coroutine, Unpack(autoroutine))
				end
				
				DGV.currentAutoroutine = nil
				status = coroutine.status(autoroutine.coroutine)
				if status=="dead" then
					autoroutines:Remove(i)
					local onCompletion = autoroutine.onCompletion
					DisposeAutoroutine(autoroutine)
					if not result  then
						if autoroutines:Length()==0 then
							Pool(autoroutines)
							autoroutines = nil
						end
						assert(false, message.."\n\n"..debugstack(autoroutine.coroutine))
					end
					if onCompletion then
						DGV.QueueInvocation(onCompletion)
					end
				else
					i = i + 1
				end
				-- Pool(result)
			else
				if autoroutine.onInterrupt then
					autoroutine:onInterrupt()
				end
				DisposeAutoroutine(autoroutine)
				autoroutines:Remove(i)
			end
			if autoroutines:Length()>0 then
				local now = GetTicks()
                
                local timeLimit = THEORETICAL_TIME_LIMIT
                
                if autoroutine.key and functionKey2TimeLimit[autoroutine.key] then
                    timeLimit = functionKey2TimeLimit[autoroutine.key]
                end
                
				--Mechanism to keep the FPS not below certain level (about 30 FPS) during working on heavy tasks. For example Path searching, Gear Finder.
				--Thanks to this mechanism we make sure it is comfortable to play the game with FPS not below about 30FPS and at the same time the given process/tasks 
				--ends as quickly aspossible. 
                if (now-startTime)>=timeLimit then
					return
				end
				
				--Preventing low FPS during a combat (when all threads/coroutines are "paused" anyway)
				if UnitAffectingCombat("player") then
					return 
				end
			else
				Pool(autoroutines)
				autoroutines = nil
				return
			end
			if i>#autoroutines then
				i=1
			end
		end
	end
end

local function YieldAutoroutine(...)
--DGV:DebugFormat("YieldAutoroutine", "stack", debugstack(1,24,20))
	if DGV.currentAutoroutine then
		coroutine.yield(...)
	end
end
DGV.YieldAutoroutine = YieldAutoroutine

local function GetRunningAutoroutine(key)
	if autoroutines then
		for _,autoroutine in IPairs(autoroutines) do
            if autoroutine then
                if key==autoroutine.key then
                    return autoroutine
                end
            end
		end
	end
end
DGV.GetRunningAutoroutine = GetRunningAutoroutine

local function InterruptAutoroutine(key)
	local running = GetRunningAutoroutine(key)
	if running then
		running.interrupt = true
	end
end
DGV.InterruptAutoroutine = InterruptAutoroutine

local function OnCompletion(autoroutine, func)
	autoroutine.onCompletion = func
	return autoroutine
end

local function OnError(autoroutine, func)
	autoroutine.onError = func
	return autoroutine
end

local function OnInterrupt(autoroutine, func)
	autoroutine.onInterrupt = func
	return autoroutine
end

local function BeginAutoroutine(key, func, ...)
--local debugTime = GetTicks()
	local autoroutine
	if autoroutines then
		for _,iteratedAutoroutine in IPairs(autoroutines) do
			if iteratedAutoroutine and key==iteratedAutoroutine.key and not iteratedAutoroutine.interrupt then
				autoroutine = iteratedAutoroutine
				break
			end
		end
	end
	if not autoroutine then
		autoroutine = GetCreateTable()
		autoroutine.boundObjects = GetCreateTable()
		autoroutine.coroutine = coroutine.create(func)
		autoroutine.key = key
		autoroutine.OnCompletion = OnCompletion
		autoroutine.OnError = OnError
		autoroutine.OnInterrupt = OnInterrupt
		InsertList(autoroutine, ...)
		if not autoroutines then
			autoroutines = GetCreateTable(autoroutine)
		else
			Insert(autoroutines, autoroutine)
		end
	end
--DGV:DebugFormat("BeginAutoroutine", "key", key, "GetTicks()-debugTime", GetTicks()-debugTime)
	return autoroutine
end

DGV.BeginAutoroutine = BeginAutoroutine

local invocations
local function QueueInvocation(func, ...)
	--assert(type(func) == "function", "func must be a function")
	if not invocations then
		invocations = GetCreateTable()
		--DGV.invocations = invocations
	end
	local invoke = GetCreateTable(func, ...)
	--DGV:DebugFormat("QueueInvocation")
	--assert(not IndexOfFirst(invocations, invoke), "Invocation matches previously existing")
	Insert(invocations, invoke)
end
DGV.QueueInvocation = QueueInvocation

local function InvocationsOnUpdate()
	if not invocations then return end
	local now = GetTime()
	for invoke in TakeNext,invocations do
		Insert(invoke, now)
		invoke[1](select(2, Pool(invoke)))
	end
	Pool(invocations)
	invocations = nil
end

local cacheHash = {}
DGV.cacheHash = cacheHash
local function DisposeReaction(reaction)
	if reaction.disposed then return end
	reaction.disposed = true
	
	if reaction.cache then
		Pool(reaction.cache)
		reaction.cache = nil
	end
	if reaction.cacheKey then
		cacheHash[reaction.cacheKey] = nil
	end
--DGV:DebugFormat("DisposeReaction TryReleaseLifetime")
	TryReleaseLifetime(reaction, reaction.Dispose)
end

local function ReactionDoOnce(reaction)
	reaction.doOnce = true
	return reaction
end

local function ReactionDoOutOfCombat(reaction)
	reaction.doOutOfCombat = true
	return reaction
end

local function ReactionReset(reaction)
	reaction.invoked = nil
	return reaction
end

local function ReactionWithPredicate(reaction, predicate, ...)
	reaction.predicate = predicate
	InsertList(reaction, ...)
	return reaction
end

local function ReactionWithAction(reaction, action, ...)
	reaction.action = action
	InsertList(reaction, ...)
	return reaction
end

local function ReactionWithArgs(reaction, ...)
	InsertList(reaction, ...)
	return reaction
end

local function ReactionInvokePassively(reaction)
	reaction.passiveInvoke = true
	return reaction
end

local function ReactionDefer(reaction)
	reaction.defer = true
	return reaction
end

local function ReactionAfterWait(reaction, seconds)
	reaction.wait = seconds
	return reaction
end

local function ReactionDisposeOn(reaction, disposeReaction)
	ReactionInvokePassively(ReactionWithAction(ReactionDoOnce(disposeReaction), reaction.Dispose, reaction))
	return reaction
end

local function StopwatchReset(reaction)
	ReactionReset(reaction)
	reaction.time = GetTime()+reaction.seconds
	return reaction
end

local function CacheReactionUnpackCache(reaction)
	return Unpack(reaction.cache)
end

local function CacheReactionSetCache(reaction, cacheKey,  ...)
	if reaction.cache then 
		--DGV:DebugFormat("CacheReactionSetCache Pooling Extisting Cache")
		reaction.cache:Pool() 
		reaction.cache = nil
	end
	reaction.cacheKey = cacheKey
	cacheHash[cacheKey] = reaction
	reaction.cache = GetCreateTable(...)
	reaction.UnpackCache = CacheReactionUnpackCache
	ReactionDoOnce(reaction)
    
	return reaction
end


local function CacheReactionSetTargetCache(reaction, targetCacheTable, cacheKey,  ...)

	if reaction.cache then 
		--DGV:DebugFormat("CacheReactionSetCache Pooling Extisting Cache")
		reaction.cache:Pool() 
		reaction.cache = nil
	end
	reaction.cacheKey = cacheKey
	cacheHash[cacheKey] = reaction
	reaction.cache = GetCreateTable(...)
	reaction.UnpackCache = CacheReactionUnpackCache
	reaction.UnpackTargetCache = CacheReactionUnpackTargetCache
	ReactionDoOnce(reaction)
    
    targetCacheTable[cacheKey] = {...}
    
	return reaction
end

local args = {}
setmetatable(args, tableClassMetaTable)
local function GetArgs(reaction, ...)
	wipe(args)
	InsertList(args, Unpack(reaction))
	InsertList(args, ...)
	--if not reaction.passiveInvoke then
		Insert(args, reaction, 1)
	-- end
	return args
end

local function PassiveInvokeAction(reaction, action,  ...)
	if action then return action(...) end
end

local function DoOnceAction(reaction, action, ...)
	if action then action(reaction, ...) end
	reaction:Dispose()
end

local function DeferAction(reaction, action, ...)
	QueueInvocation(action, reaction, ...)
end

local function WaitAction(reaction, action, ...)
	ReactionDoOnce(DGV.RegisterStopwatchReaction(reaction.wait, nil, action, ...))
end

local function OutOfCombatAction(reaction, action, ...)
	DGV.DoOutOfCombat(action, reaction, ...)
end

local function InvokePredicate(reaction, ...)
	if reaction.passiveInvoke then
		return PassiveInvokeAction(reaction, reaction.predicate, ...)
	else
		return reaction.predicate(reaction, ...)
	end
end

local function TryInvokeReaction(event, reaction, ...)
	local now = event=="time" and GetTime()
	local rtime,predicate = reaction.time, reaction.predicate
	if 
		((event~="time" and reaction.event==event) or (now and rtime and rtime<=now)) and
		not reaction.disposed
	then
		local args
		if predicate then
			args = GetArgs(reaction, ...)
		end
		if not predicate or InvokePredicate(Unpack(args)) then
			if not args then
				args = GetArgs(reaction, ...)
			end
			local action = reaction.action
			if reaction.passiveInvoke then
				Insert(args, action, 2)
				action = PassiveInvokeAction
			end
			if reaction.doOnce then
				Insert(args, action, 2)
				action = DoOnceAction
			end
			if action and reaction.defer then
				Insert(args, action, 2)
				action = DeferAction
			elseif action and reaction.wait then
				Insert(args, action, 2)
				action = WaitAction
			end
			if action and reaction.doOutOfCombat then
				Insert(args, action, 2)
				action = OutOfCombatAction
			end
			reaction.invoked = true
			if action then
				action(Unpack(args))
			end
		end
	end
end

local reactions, TryUnsubscribeEvent
local function ClearReaction(reaction)
	if reactions then
		local event = reaction.event
		local reactionOrGroup = reactions[event]
		if reactionOrGroup then
			if reactionOrGroup.isGroup then
				local i=1
				while i<=#reactionOrGroup do
					local groupedReaction = reactionOrGroup[i]
					if groupedReaction.disposed then
						Pool(groupedReaction)
						reactionOrGroup:Remove(i)
						if groupedReaction==reaction then break end
					else
						i = i + 1
					end
				end
				if reactionOrGroup:Length()==0 then
					Pool(reactionOrGroup)
					reactions[event] = nil
					TryUnsubscribeEvent(event)
				end
			else
				if reactionOrGroup.disposed then
					Pool(reactionOrGroup)
					reactions[event] = nil
					TryUnsubscribeEvent(event)
				end
			end	
			if (not next(reactions)) and reactions:Length()==0 then
				Pool(reactions)
				reactions = nil
			end
		end
	end
end

local function FireIntersectionReaction(reaction, ...)
	reaction.parent:TryInvoke(...)
	for _,child in IPairs(reaction.parent.children) do --clear siblings to avoid memory leak
		if child~=reaction then
			ClearReaction(child)
		end
	end
end

local function IntersectionReactionDispose(reaction)
	for _,child in IPairs(reaction.children) do
		if child then 
			child:Dispose()
		end
	end
	Pool(reaction.children)
	DisposeReaction(reaction)
end

local function IntersectionReactionReset(reaction)
	ReactionReset(reaction)
	for _,child in IPairs(reaction.children) do
		child:Reset()
	end
end

local CommonRegisterReaction
local function IntersectionReactionOr(reaction, additionalReaction)
	if not reaction.parent and not reaction.children then
		local originalReaction = reaction
		reaction = CommonRegisterReaction(nil, nil, originalReaction.action, Unpack(originalReaction))
		originalReaction.parent = reaction
		additionalReaction.parent = reaction
		originalReaction.action = FireIntersectionReaction
		reaction.children = GetCreateTable(originalReaction, additionalReaction)
		reaction.Dispose = IntersectionReactionDispose
		reaction.Reset = IntersectionReactionReset
	elseif not reaction.children then
		Insert(reaction.parent.children, additionalReaction)
		additionalReaction.parent = reaction.parent
		reaction = reaction.parent
	else
		Insert(reaction.children, additionalReaction)
		additionalReaction.parent = reaction
	end
	additionalReaction.action = FireIntersectionReaction
	return reaction
end

local function ReactionTryInvoke(reaction, ...)
	if not reaction.disposed then
		TryInvokeReaction(nil, reaction, ...)
	end
	if reaction.disposed then
		local i=1
		while i<=reactions:Length() do
			local noEventReaction = reactions[i]
			if noEventReaction and noEventReaction.disposed then
				Pool(noEventReaction)
				reactions:Remove(i)
			else
				i = i + 1
			end
			if noEventReaction==reaction then break end
		end
	end
	return reaction
end

local function TryGetReactionGroup(event)
	local reaction = reactions[event]
	if reaction then
		if not reaction.isGroup then
			local group = GetCreateTable()
			group.isGroup = true
			group.event = event
			Insert(group, reaction)
			reactions[event] = group
			return group
		else
			return reaction
		end
	end
end

CommonRegisterReaction = function(event, predicate, action, ...)
	if not reactions then 
		reactions = GetCreateTable()
		DGV.reactions = reactions
	end
	local reaction = GetCreateTable(...)
	reaction.event = event
	reaction.predicate = predicate
	reaction.action = action
	reaction.Once = ReactionDoOnce
	reaction.OutOfCombat = ReactionDoOutOfCombat
	reaction.Reset = ReactionReset
	reaction.WithPredicate = ReactionWithPredicate
	reaction.WithAction = ReactionWithAction
	reaction.WithArgs = ReactionWithArgs
	reaction.InvokePassively = ReactionInvokePassively
	reaction.Defer = ReactionDefer
	reaction.AfterWait = ReactionAfterWait
	reaction.DisposeOn = ReactionDisposeOn
	reaction.Dispose = DisposeReaction
	reaction.SetCache = CacheReactionSetCache
	reaction.SetTargetCache = CacheReactionSetTargetCache
	reaction.Or = IntersectionReactionOr
	reaction.TryInvoke = ReactionTryInvoke
	if event then
		local group = TryGetReactionGroup(event)
		if group then
			Insert(group, reaction)
		else
			reactions[event] = reaction
		end
	else
		Insert(reactions, reaction)
	end
	return reaction
end

local function RegisterReaction(event, predicate, action, ...)
	if event then
		DGV.eventFrame:RegisterEvent(event)
	end
	return CommonRegisterReaction(event, predicate, action, ...)
end
DGV.RegisterReaction = RegisterReaction

local function TryInvokeReactions(event, ...)
	if reactions then
		local reactionOrGroup = reactions[event]
		if reactionOrGroup then
			if reactionOrGroup.isGroup then
				local i=1
				while i<=#reactionOrGroup do
					local groupedReaction = reactionOrGroup[i]
					if groupedReaction and not groupedReaction.disposed then
						TryInvokeReaction(event, groupedReaction, ...)
					end
					if groupedReaction and groupedReaction.disposed then
						Pool(groupedReaction)
						reactionOrGroup:Remove(i)
					else
						i = i + 1
					end
				end
				if reactionOrGroup:Length()==0 then
					Pool(reactionOrGroup)
					reactions[event] = nil
					TryUnsubscribeEvent(event)
				end
			else
				if not reactionOrGroup.disposed then
					TryInvokeReaction(event, reactionOrGroup, ...)
				end
				if reactionOrGroup.disposed then
					Pool(reactionOrGroup)
					reactions[event] = nil
					TryUnsubscribeEvent(event)
				end
			end	
			if (not next(reactions)) and reactions:Length()==0 then
				Pool(reactions)
				reactions = nil
			end
		end
	end
end

local functionHookRegistry
local function RegisterFunctionReaction(functionName, predicate, action, ...)
	if not functionHookRegistry then functionHookRegistry = {} end
	if not functionHookRegistry[functionName] then
		hooksecurefunc(functionName, 
			function(...)
				TryInvokeReactions(functionName, ...)
			end)
		functionHookRegistry[functionName] = true
	end
	return CommonRegisterReaction(functionName, predicate, action, ...)
end
DGV.RegisterFunctionReaction = RegisterFunctionReaction

TryUnsubscribeEvent = function(event)
	if not event or event=="time" or functionHookRegistry[event] then return end --not an event reaction
	if DGV.events[event] then return end --traditional DGV event
	DGV.eventFrame:UnregisterEvent(event)
end

local function EvaluatePath(...)
	local parent
	for i=1,select("#", ...) do
		local val = select(i, ...)
		if not parent then
			parent = _G[val]
		else
			parent = parent[val]
		end
	end
	return parent
end

local function GetByGlobalPath(path)
	return EvaluatePath(strsplit(".", path))
end

local function RegisterMemberFunctionReaction(functionParent, functionName, predicate, action, ...)
	local functionParentString = type(functionParent)=="string" and functionParent or functionParent:GetName()
	local key = string.format("%s:%s", functionParentString, functionName)
	
	if not functionHookRegistry then functionHookRegistry = {} end
	if not functionHookRegistry[key] then
		local parentObject = type(functionParent)=="string" and GetByGlobalPath(functionParent) or functionParent
		hooksecurefunc(parentObject, functionName,
			function(...)
				TryInvokeReactions(key, ...)
			end)
		functionHookRegistry[key] = true
	end
	return CommonRegisterReaction(key, predicate, action, ...)
end
DGV.RegisterMemberFunctionReaction = RegisterMemberFunctionReaction

local function RegisterStopwatchReaction(seconds, predicate, action, ...)
	local reaction = CommonRegisterReaction("time", predicate, action, ...)
	reaction.seconds = seconds
	reaction.Reset = StopwatchReset
	reaction:Reset()
	return reaction
end
DGV.RegisterStopwatchReaction = RegisterStopwatchReaction

local function TryGetCacheReaction(cacheKey)
	return cacheHash[cacheKey]
end
DGV.TryGetCacheReaction = TryGetCacheReaction

DGV.eventFrame:HookScript("OnEvent", function(self, event, ...)
	TryInvokeReactions(event, event, ...)
end)

local runInTheNextFrameQueue = {}

DGV.eventFrame:HookScript("OnUpdate", function(self)
    while #runInTheNextFrameQueue > 0 do
        local functionToRun = runInTheNextFrameQueue[1]
        table.remove(runInTheNextFrameQueue, 1)
        functionToRun()
    end

	AutoroutinesOnUpdate()
	InvocationsOnUpdate()
	if reactions and reactions["time"] then
		TryInvokeReactions("time", "time", GetTime())
	end
end)

local function DoOutOfCombat(action, ...)
	if not InCombatLockdown() then
		action(...)
		return true
	else
		RegisterReaction("PLAYER_REGEN_ENABLED", nil, 
		function(reaction, ...) 
			action(...)
		end, 
		...):Once()
	end
end
DGV.DoOutOfCombat = DoOutOfCombat

local awaitingActions = {}
local function DoOutOfCombatCancelOld(name, action, ...)
	if awaitingActions[name] and InCombatLockdown() then
		return
	end

	if not InCombatLockdown() then
		awaitingActions[name] = nil
		action(...)
		return true
	else
		awaitingActions[name] = true
		RegisterReaction("PLAYER_REGEN_ENABLED", nil, 
		function(reaction, ...) 
			awaitingActions[name] = nil
			action(...)
		end, 
		...):Once()
	end
end

DGV.DoOutOfCombatCancelOld = DoOutOfCombatCancelOld


--example for:    PackStrings("class", "spec", "PVP")   gives:   
--   "class:spec:PVP"
local function PackStrings(...)
	local value
	for i=1,select("#", ...) do
		local ith = tostring((select(i, ...)))
		if not value then value = ith
		else value = value..":"..ith end
	end
	return value
end
DGV.PackStrings = PackStrings

local function ListContains(contains, argSelector, ...)
	if type(argSelector)~="function" then
		if argSelector==contains then return argSelector end
	end
	for i=1,select("#", ...) do
		local arg,selectedArg = select(i, ...)
		if type(argSelector)=="function" then
			selectedArg = argSelector(arg)
		end
		if selectedArg==contains then return arg end
	end
	return false
end
DGV.ListContains = ListContains

--UI
function DGV.CreateStandardPreloader(name, parent)
    local preloader = CreateFrame("Frame", name , parent, "DugisPreloader")
    preloader:SetSize(171, 40)
    preloader:SetParent(parent)
    preloader:SetPoint("TOPLEFT", 0, 0)
    preloader:Hide()

    local animationGroup = preloader.Icon:CreateAnimationGroup()
    animationGroup:SetLooping("REPEAT")
    local animation = animationGroup:CreateAnimation("Rotation")
    animation:SetDegrees(-360)
    animation:SetDuration(1)
    animation:SetOrder(1)
    preloader.animationGroup = animationGroup
    preloader.animation = animation
    return preloader
end

