Quantcast
--[[--------------------------------------------------------------------
    Ovale Spell Priority
    Copyright (C) 2012 Sidoine
    Copyright (C) 2012, 2013 Johnny C. Lam

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License in the LICENSE
    file accompanying this program.
--]]--------------------------------------------------------------------

local _, Ovale = ...
local OvaleBestAction = Ovale:NewModule("OvaleBestAction")
Ovale.OvaleBestAction = OvaleBestAction

--<private-static-properties>
local OvalePool = Ovale.OvalePool
local OvaleTimeSpan = Ovale.OvaleTimeSpan

-- Forward declarations for module dependencies.
local OvaleActionBar = nil
local OvaleCondition = nil
local OvaleCooldown = nil
local OvaleData = nil
local OvaleEquipement = nil
local OvaleFuture = nil
local OvalePower = nil
local OvaleSpellBook = nil
local OvaleStance = nil

local abs = math.abs
local floor = math.floor
local ipairs = ipairs
local loadstring = loadstring
local pairs = pairs
local tonumber = tonumber
local tostring = tostring
local wipe = table.wipe
local Complement = OvaleTimeSpan.Complement
local CopyTimeSpan = OvaleTimeSpan.CopyTo
local HasTime = OvaleTimeSpan.HasTime
local Intersect = OvaleTimeSpan.Intersect
local IntersectInterval = OvaleTimeSpan.IntersectInterval
local Measure = OvaleTimeSpan.Measure
local Union = OvaleTimeSpan.Union

local API_GetTime = GetTime
local API_GetActionCooldown = GetActionCooldown
local API_GetActionTexture = GetActionTexture
local API_GetItemIcon = GetItemIcon
local API_GetItemCooldown = GetItemCooldown
local API_GetItemSpell = GetItemSpell
local API_GetSpellInfo = GetSpellInfo
local API_GetSpellTexture = GetSpellTexture
local API_IsActionInRange = IsActionInRange
local API_IsCurrentAction = IsCurrentAction
local API_IsItemInRange = IsItemInRange
local API_IsSpellInRange = IsSpellInRange
local API_IsUsableAction = IsUsableAction
local API_IsUsableItem = IsUsableItem

-- Profiling set-up.
local Profiler = Ovale.Profiler
local profiler = nil
do
	local group = OvaleBestAction:GetName()

	local function EnableProfiling()
		API_GetActionCooldown = Profiler:Wrap(group, "OvaleBestAction_API_GetActionCooldown", GetActionCooldown)
		API_GetActionTexture = Profiler:Wrap(group, "OvaleBestAction_API_GetActionTexture", GetActionTexture)
		API_GetItemIcon = Profiler:Wrap(group, "OvaleBestAction_API_GetItemIcon", GetItemIcon)
		API_GetItemCooldown = Profiler:Wrap(group, "OvaleBestAction_API_GetItemCooldown", GetItemCooldown)
		API_GetItemSpell = Profiler:Wrap(group, "OvaleBestAction_API_GetItemSpell", GetItemSpell)
		API_GetSpellInfo = Profiler:Wrap(group, "OvaleBestAction_API_GetSpellInfo", GetSpellTexture)
		API_GetSpellTexture = Profiler:Wrap(group, "OvaleBestAction_API_GetSpellTexture", GetSpellTexture)
		API_IsActionInRange = Profiler:Wrap(group, "OvaleBestAction_API_IsActionInRange", IsActionInRange)
		API_IsCurrentAction = Profiler:Wrap(group, "OvaleBestAction_API_IsCurrentAction", IsCurrentAction)
		API_IsItemInRange = Profiler:Wrap(group, "OvaleBestAction_API_IsItemInRange", IsItemInRange)
		API_IsSpellInRange = Profiler:Wrap(group, "OvaleBestAction_API_IsSpellInRange", IsSpellInRange)
		API_IsUsableAction = Profiler:Wrap(group, "OvaleBestAction_API_IsUsableAction", IsUsableAction)
		API_IsUsableItem = Profiler:Wrap(group, "OvaleBestAction_API_IsUsableItem", IsUsableItem)
	end

	local function DisableProfiling()
		API_GetTime = GetTime
		API_GetActionCooldown = GetActionCooldown
		API_GetActionTexture = GetActionTexture
		API_GetItemIcon = GetItemIcon
		API_GetItemCooldown = GetItemCooldown
		API_GetItemSpell = GetItemSpell
		API_GetSpellInfo = GetSpellInfo
		API_GetSpellTexture = GetSpellTexture
		API_IsActionInRange = IsActionInRange
		API_IsCurrentAction = IsCurrentAction
		API_IsItemInRange = IsItemInRange
		API_IsSpellInRange = IsSpellInRange
		API_IsUsableAction = IsUsableAction
		API_IsUsableItem = IsUsableItem
	end

	Profiler:RegisterProfilingGroup(group, EnableProfiling, DisableProfiling)
	profiler = Profiler:GetProfilingGroup(group)
end

local OVALE_DEFAULT_PRIORITY = 3

-- Age of the current computation.
local self_serial = 0
-- Pool of time-span tables.
local self_pool = OvalePool("OvaleBestAction_pool")
--</private-static-properties>

--<private-static-methods>
local function PutValue(element, value, origin, rate)
	if not element.result then
		element.result = { type = "value" }
	end
	local result = element.result
	result.value = value
	result.origin = origin
	result.rate = rate
	return result
end

local function ComputeAction(element, state)
	profiler.Start("OvaleBestAction_ComputeAction")
	local self = OvaleBestAction
	local action = element.params[1]
	local actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration,
		actionUsable, actionShortcut, actionIsCurrent, actionEnable, actionType, actionId = self:GetActionInfo(element, state)
	local timeSpan = element.timeSpan
	timeSpan:Reset()

	if not actionTexture then
		Ovale:Logf("Action %s not found", action)
		profiler.Stop("OvaleBestAction_ComputeAction")
		return timeSpan
	elseif not (actionEnable and actionEnable > 0) then
		Ovale:Logf("Action %s not enabled", action)
		profiler.Stop("OvaleBestAction_ComputeAction")
		return timeSpan
	elseif element.params.usable == 1 and not actionUsable then
		Ovale:Logf("Action %s not usable", action)
		profiler.Stop("OvaleBestAction_ComputeAction")
		return timeSpan
	end

	-- Set the cast time of the action.
	if actionType == "spell" then
		local spellId = actionId
		local si = spellId and OvaleData.spellInfo[spellId]
		if si and si.casttime then
			element.castTime = si.casttime
		else
			local _, _, _, _, _, _, castTime = API_GetSpellInfo(spellId)
			if castTime then
				element.castTime = castTime / 1000
			else
				element.castTime = nil
			end
		end
	else
		element.castTime = 0
	end

	-- If the action is not on cooldown, then treat it like it's immediately ready.
	local start
	if actionCooldownDuration and actionCooldownStart and actionCooldownStart > 0 then
		start = actionCooldownDuration + actionCooldownStart
	else
		start = state.currentTime
	end

	Ovale:Logf("start=%f nextCast=%s [%d]", start, state.nextCast, element.nodeId)

	-- If the action is available before the end of the current spellcast, then wait until we can first cast the action.
	if start < state.nextCast then
		-- Default to starting at next available cast time.
		local newStart = state.nextCast
		-- If we are currently channeling a spellcast, then see if it is interruptible.
		-- If we are allowed to interrupt it, then start after the next tick of the channel.
		if state.isChanneling then
			local spellId = state.currentSpellId
			local si = spellId and OvaleData.spellInfo[spellId]
			if si then
				-- "channel=N" means that the channel has N total ticks and can be interrupted.
				local channel = si.channel or si.canStopChannelling
				if channel then
					local hasteMultiplier = 1
					if si.haste == "spell" then
						hasteMultiplier = state:GetSpellHasteMultiplier()
					elseif si.haste == "melee" then
						hasteMultiplier = state:GetMeleeHasteMultiplier()
					end
					local numTicks = floor(channel * hasteMultiplier + 0.5)
					local tick = (state.nextCast - state.startCast) / numTicks
					local tickTime = state.startCast
					for i = 1, numTicks do
						tickTime = tickTime + tick
						if start <= tickTime then
							break
						end
					end
					newStart = tickTime
					Ovale:Logf("%s start=%f, numTicks=%d, tick=%f, tickTime=%f", spellId, newStart, numTicks, tick, tickTime)
				end
			end
		end
		start = newStart
	end
	Ovale:Logf("Action %s can start at %f", action, start)
	timeSpan[1], timeSpan[2] = start, math.huge

	--[[
		Allow for the return value of an to be "typecast" to a constant value by specifying
		asValue=1 as a parameter.

		Return 1 if the action is off of cooldown, or 0 if it is on cooldown.
	--]]
	local value
	if element.params.asValue and element.params.asValue == 1 then
		local atTime = state.currentTime
		if HasTime(timeSpan, atTime) then
			value = 1
		else
			value = 0
		end
		timeSpan[1], timeSpan[2] = 0, math.huge
	end

	local priority = element.params.priority or OVALE_DEFAULT_PRIORITY
	if value then
		local result = PutValue(element, value, 0, 0)
		profiler.Stop("OvaleBestAction_ComputeAction")
		return timeSpan, priority, result
	else
		profiler.Stop("OvaleBestAction_ComputeAction")
		return timeSpan, priority, element
	end
end

local function ComputeAnd(element, state)
	profiler.Start("OvaleBestAction_Compute")
	Ovale:Logf("%s [%d]", element.type, element.nodeId)
	local self = OvaleBestAction
	local timeSpanA = self:ComputeBool(element.a, state)
	local timeSpan = element.timeSpan

	-- Short-circuit evaluation of left argument to AND.
	if Measure(timeSpanA) == 0 then
		timeSpan:Reset(timeSpanA)
	else
		local timeSpanB = self:ComputeBool(element.b, state)
		-- Take intersection of A and B.
		timeSpan:Reset()
		Intersect(timeSpanA, timeSpanB, timeSpan)
	end
	Ovale:Logf("%s returns %s [%d]", element.type, tostring(timeSpan), element.nodeId)
	profiler.Stop("OvaleBestAction_Compute")
	return timeSpan
end

local function ComputeArithmetic(element, state)
	profiler.Start("OvaleBestAction_Compute")
	local self = OvaleBestAction
	local timeSpanA, _, elementA = self:Compute(element.a, state)
	local timeSpanB, _, elementB = self:Compute(element.b, state)
	local timeSpan = element.timeSpan
	timeSpan:Reset()

	-- Take intersection of A and B.
	Intersect(timeSpanA, timeSpanB, timeSpan)
	if Measure(timeSpan) == 0 then
		Ovale:Logf("%s return %s [%d]", element.type, tostring(timeSpan), element.nodeId)
		local result = PutValue(element, 0, 0, 0)
		profiler.Stop("OvaleBestAction_Compute")
		return timeSpan, OVALE_DEFAULT_PRIORITY, result
	end

	--[[
		A(t) = a + (t - b)*c
		B(t) = x + (t - y)*z

		Silently "typecast" non-values to a constant value of 0.
	--]]
	local a = elementA and elementA.value or 0
	local b = elementA and elementA.origin or 0
	local c = elementA and elementA.rate or 0
	local x = elementB and elementB.value or 0
	local y = elementB and elementB.origin or 0
	local z = elementB and elementB.rate or 0
	local atTime = state.currentTime

	Ovale:Logf("%f+(t-%f)*%f %s %f+(t-%f)*%f [%d]", a, b, c, element.operator, x, y, z, element.nodeId)

	-- result(t) = l + (t - m) * n
	local l, m, n

	--[[
		A(t) = a + (t - b)*c = a + (t - t0 + t0 - b)*c = [a + (t0 - b)*c] + (t - t0)*c = A(t0) + (t - t0)*c
		B(t) = x + (t - y)*z = x + (t - t0 + t0 - y)*z = [x + (t0 - y)*z] + (t - t0)*z = B(t0) + (t - t0)*z
	--]]
	local A = a + (atTime - b)*c
	local B = x + (atTime - y)*z

	if element.operator == "+" then
		--[[
			A(t) = A(t0) + (t - t0)*c = A + (t - t0)*c
			B(t) = B(t0) + (t - t0)*z = B + (t - t0)*z

			A(t) + B(t) = (A + B) + (t - t0)*(c + z)
		--]]
		l = A + B
		m = atTime
		n = c + z
	elseif element.operator == "-" then
		--[[
			A(t) = A(t0) + (t - t0)*c = A + (t - t0)*c
			B(t) = B(t0) + (t - t0)*z = B + (t - t0)*z

			A(t) - B(t) = (A - B) + (t - t0)*(c - z)
		--]]
		l = A - B
		m = atTime
		n = c - z
	elseif element.operator == "*" then
		--[[
				 A(t) = A(t0) + (t - t0)*c = A + (t - t0)*c
				 B(t) = B(t0) + (t - t0)*z = B + (t - t0)*z
			A(t)*B(t) = A*B + (t - t0)*[A*z + B*c] + [(t - t0)^2]*(c*z)
					  = A*B + (t - t0)*[A*z + B*c] + O(t^2) converges everywhere.
		--]]
			l = A*B
			m = atTime
			n = A*z + B*c
	elseif element.operator == "/" then
		--[[
				 A(t) = A(t0) + (t - t0)*c = A + (t - t0)*c
				 B(t) = B(t0) + (t - t0)*z = B + (t - t0)*z
			A(t)/B(t) = A/B + (t - t0)*[(B*c - A*z)/B^2] + O(t^2) converges when |t - t0| < |B/z|.
		--]]
		l = A/B
		m = atTime
		n = (B*c - A*z)/(B^2)
		local bound
		if z == 0 then
			bound = math.huge
		else
			bound = abs(B/z)
		end
		local scratch = OvaleTimeSpan(self_pool:Get())
		scratch:Reset(timeSpan)
		timeSpan:Reset()
		IntersectInterval(scratch, atTime - bound, atTime + bound, timeSpan)
		self_pool:Release(scratch)
	elseif element.operator == "%" then
		if c == 0 and z == 0 then
			l = A % B
			m = atTime
			n = 0
		else
			Ovale:Error("Parameters of % must be constants")
			l = 0
			m = 0
			n = 0
			timeSpan:Reset()
		end
	end
	Ovale:Logf("result = %f+(t-%f)*%f [%d]", l, m, n, element.nodeId)
	local result = PutValue(element, l, m, n)
	profiler.Stop("OvaleBestAction_Compute")
	return timeSpan, OVALE_DEFAULT_PRIORITY, result
end

local function ComputeCompare(element, state)
	profiler.Start("OvaleBestAction_Compute")
	local self = OvaleBestAction
	local timeSpanA, _, elementA = self:Compute(element.a, state)
	local timeSpanB, _, elementB = self:Compute(element.b, state)
	local timeSpan = element.timeSpan
	timeSpan:Reset()

	-- Take intersection of A and B.
	Intersect(timeSpanA, timeSpanB, timeSpan)
	if Measure(timeSpan) == 0 then
		profiler.Stop("OvaleBestAction_Compute")
		return timeSpan
	end

	--[[
		A(t) = a + (t - b)*c
		B(t) = x + (t - y)*z

		Silently "typecast" non-values to a constant value of 0.
	--]]
	local a = elementA and elementA.value or 0
	local b = elementA and elementA.origin or 0
	local c = elementA and elementA.rate or 0
	local x = elementB and elementB.value or 0
	local y = elementB and elementB.origin or 0
	local z = elementB and elementB.rate or 0
	local operator = element.operator

	Ovale:Logf("%f+(t-%f)*%f %s %f+(t-%f)*%f [%d]", a, b, c, operator, x, y, z, element.nodeId)

	--[[
				 A(t) = B(t)
		a + (t - b)*c = x + (t - y)*z
		a + t*c - b*c = x + t*z - y*z
			t*c - t*z = (x - y*z) - (a - b*c)
			t*(c - z) = B(0) - A(0)
	--]]
	local A = a - b*c
	local B = x - y*z
	if c == z then
		if not ((operator == "==" and A == B)
				or (operator == "<" and A < B)
				or (operator == "<=" and A <= B)
				or (operator == ">" and A > B)
				or (operator == ">=" and A >= B)) then
			timeSpan:Reset()
		end
	else
		local scratch = OvaleTimeSpan(self_pool:Get())
		scratch:Reset(timeSpan)
		timeSpan:Reset()
		local t = (B - A)/(c - z)
		t = (t > 0) and t or 0
		Ovale:Logf("t = %f", t)
		if (c > z and operator == "<")
				or (c > z and operator == "<=")
				or (c < z and operator == ">")
				or (c < z and operator == ">=") then
			IntersectInterval(scratch, 0, t, timeSpan)
		elseif (c < z and operator == "<")
				or (c < z and operator == "<=")
				or (c > z and operator == ">")
				or (c > z and operator == ">=") then
			IntersectInterval(scratch, t, math.huge, timeSpan)
		end
		self_pool:Release(scratch)
	end
	Ovale:Logf("compare %s returns %s [%d]", operator, tostring(timeSpan), element.nodeId)
	profiler.Stop("OvaleBestAction_Compute")
	return timeSpan
end

local function ComputeCustomFunction(element, state)
	profiler.Start("OvaleBestAction_Compute")
	Ovale:Logf("custom function %s", element.name)
	local self = OvaleBestAction
	if not element.serial or element.serial < self_serial then
		-- Cache new values in element.
		element.timeSpanA, element.priorityA, element.elementA = self:Compute(element.a, state)
		element.serial = self_serial
	else
		Ovale:Logf("Using cached values for %s", element.name)
	end

	local timeSpanA, priorityA, elementA = element.timeSpanA, element.priorityA, element.elementA
	local timeSpan = element.timeSpan
	timeSpan:Reset()

	if element.params.asValue and element.params.asValue == 1 then
		--[[
			Allow for the return value of a custom function to be "typecast" to a constant value.

			If the return value is a time span (a "boolean" value), then if the current time of
			the simulation is within the time span, then return 1, or 0 otherwise.

			If the return value is a linear function, then if the current time of the simulation
			is within the function's domain, then the function is simply evaluated at the current
			time, or 0 otherwise.

			If the return value is an action, then return 1 if the action is off of cooldown, or
			0 if it is on cooldown.
		--]]
		local atTime = state.currentTime
		local value = 0
		if HasTime(timeSpanA, atTime) then
			if not elementA then	-- boolean
				value = 1
			elseif elementA.type == "value" then
				value = elementA.value + (atTime - elementA.origin) * elementA.rate
			elseif elementA.type == "action" then
				value = 1
			end
		end
		timeSpan[1], timeSpan[2] = 0, math.huge
		local result = PutValue(element, value, 0, 0)
		profiler.Stop("OvaleBestAction_Compute")
		return timeSpan, priorityA, result
	else
		CopyTimeSpan(timeSpanA, timeSpan)
		profiler.Stop("OvaleBestAction_Compute")
		return timeSpan, priorityA, elementA
	end
end

local function ComputeFunction(element, state)
	profiler.Start("OvaleBestAction_ComputeFunction")
	local timeSpan = element.timeSpan
	timeSpan:Reset()

	if not OvaleCondition:IsCondition(element.func) then
		Ovale:Errorf("Condition %s not found", element.func)
		profiler.Stop("OvaleBestAction_ComputeFunction")
		return timeSpan
	end

	local start, ending, value, origin, rate = OvaleCondition:EvaluateCondition(element.func, element.params)
	if start and ending then
		timeSpan[1], timeSpan[2] = start, ending
	end

	if Ovale.trace then
		local conditionCall = element.func .. "("
		for k, v in pairs(element.params) do
			conditionCall = conditionCall .. k .. "=" .. v .. ","
		end
		conditionCall = conditionCall .. ")"
		Ovale:FormatPrint("Condition %s returned %s, %s, %s, %s, %s", conditionCall, start, ending, value, origin, rate)
	end

	--[[
		Allow for the return value of a script condition to be "typecast" to a constant value
		by specifying asValue=1 as a script parameter.

		If the return value is a time span (a "boolean" value), then if the current time of
		the simulation is within the time span, then return 1, or 0 otherwise.

		If the return value is a linear function, then if the current time of the simulation
		is within the function's domain, then the function is simply evaluated at the current
		time, or 0 otherwise.
	--]]
	if element.params.asValue and element.params.asValue == 1 then
		local atTime = state.currentTime
		if HasTime(timeSpan, atTime) then
			if value then
				value = value + (atTime - origin) * rate
			else
				value = 1
			end
		else
			value = 0
		end
		origin, rate = 0, 0
		timeSpan[1], timeSpan[2] = 0, math.huge
	end

	if value then
		local result = PutValue(element, value, origin, rate)
		profiler.Stop("OvaleBestAction_ComputeFunction")
		return timeSpan, OVALE_DEFAULT_PRIORITY, result
	else
		profiler.Stop("OvaleBestAction_ComputeFunction")
		return timeSpan
	end
end

local function ComputeGroup(element, state)
	profiler.Start("OvaleBestAction_Compute")
	local self = OvaleBestAction
	local bestTimeSpan, bestPriority, bestElement, bestCastTime
	local timeSpan = element.timeSpan
	timeSpan:Reset()

	Ovale:Logf("%s [%d]", element.type, element.nodeId)

	if #element.nodes == 1 then
		profiler.Stop("OvaleBestAction_Compute")
		return self:Compute(element.nodes[1], state)
	end

	local best = OvaleTimeSpan(self_pool:Get())
	local current = OvaleTimeSpan(self_pool:Get())

	for k, v in ipairs(element.nodes) do
		local currentTimeSpan, currentPriority, currentElement = self:Compute(v, state)
		-- We only care about actions that are available at time t > state.currentTime.
		current:Reset()
		IntersectInterval(currentTimeSpan, state.currentTime, math.huge, current)
		if Measure(current) > 0 then
			Ovale:Logf("    group checking %s [%d]", tostring(current), element.nodeId)
			local currentCastTime
			if currentElement then
				currentCastTime = currentElement.castTime
			end
			local gcd = OvaleCooldown:GetGCD()
			if not currentCastTime or currentCastTime < gcd then
				currentCastTime = gcd
			end

			local replace = false
			if Measure(best) == 0 then
				Ovale:Logf("    group first best %s [%d]", tostring(current), element.nodeId)
				replace = true
			elseif not currentPriority or not bestPriority or currentPriority == bestPriority then
				-- If the spells have the same priority, then pick the one with an earlier cast time.
				local threshold = (bestElement and bestElement.params) and bestElement.params.wait or 0
				if best[1] - current[1] > threshold then
					Ovale:Logf("    group new best %s [%d]", tostring(current), element.nodeId)
					replace = true
				end
			elseif currentPriority > bestPriority then
				-- If the current spell has a higher priority than the best one found, then choose the
				-- higher priority spell if its cast is pushed back too far by the lower priority one.
				local threshold = (currentElement and currentElement.params) and currentElement.params.wait or (bestCastTime * 0.75)
				if current[1] - best[1] < threshold then
					Ovale:Logf("    group new best (lower prio) %s [%d]", tostring(current), element.nodeId)
					replace = true
				end
			elseif currentPriority < bestPriority then
				-- If the current spell has a lower priority than the best one found, then choose the
				-- lower priority spell only if it doesn't push back the cast of the higher priority
				-- one by too much.
				local threshold = (bestElement and bestElement.params) and bestElement.params.wait or (currentCastTime * 0.75)
				if best[1] - current[1] > threshold then
					Ovale:Logf("    group new best (higher prio) %s [%d]", tostring(current), element.nodeId)
					replace = true
				end
			end
			if replace then
				best:Reset(current)
				bestTimeSpan = currentTimeSpan
				bestPriority = currentPriority
				bestElement = currentElement
				bestCastTime = currentCastTime
			end
			-- If the node is a "wait" node, then skip the remaining nodes.
			if currentElement and currentElement.wait then break end
		end
	end

	self_pool:Release(best)
	self_pool:Release(current)

	if not bestTimeSpan then
		Ovale:Logf("group return %s [%d]", tostring(timeSpan), element.nodeId)
		profiler.Stop("OvaleBestAction_Compute")
		return timeSpan
	else
		CopyTimeSpan(bestTimeSpan, timeSpan)
		if bestElement then
			local id = bestElement.value
			if bestElement.params then
				id = bestElement.params[1]
			end
			Ovale:Logf("group best action %s remains %s [%d]", id, tostring(timeSpan), element.nodeId)
		else
			Ovale:Logf("group no best action returns %s [%d]", tostring(timeSpan), element.nodeId)
		end
		profiler.Stop("OvaleBestAction_Compute")
		return timeSpan, bestPriority, bestElement
	end
end

local function ComputeIf(element, state)
	profiler.Start("OvaleBestAction_Compute")
	Ovale:Logf("%s [%d]", element.type, element.nodeId)
	local self = OvaleBestAction

	local timeSpanA = self:ComputeBool(element.a, state)
	local timeSpan = element.timeSpan
	timeSpan:Reset()

	local conditionTimeSpan = OvaleTimeSpan(self_pool:Get())
	if element.type == "if" then
		conditionTimeSpan:Reset(timeSpanA)
	elseif element.type == "unless" then
		-- "unless A B" is equivalent to "if (not A) B", so take the complement of A.
		Complement(timeSpanA, conditionTimeSpan)
	end
	-- Short-circuit evaluation of left argument to IF.
	if Measure(conditionTimeSpan) == 0 then
		timeSpan:Reset(conditionTimeSpan)
		self_pool:Release(conditionTimeSpan)
		Ovale:Logf("%s return %s [%d]", element.type, tostring(timeSpan), element.nodeId)
		local result = PutValue(element, 0, 0, 0)
		profiler.Stop("OvaleBestAction_Compute")
		return timeSpan, OVALE_DEFAULT_PRIORITY, result
	end

	local timeSpanB, priorityB, elementB = self:Compute(element.b, state)
	-- If the "then" clause is a "wait" node, then only wait if the conditions are true.
	if elementB and elementB.wait and not HasTime(conditionTimeSpan, state.currentTime) then
		elementB.wait = nil
	end
	-- Take intersection of the condition and B.
	Intersect(conditionTimeSpan, timeSpanB, timeSpan)
	self_pool:Release(conditionTimeSpan)

	Ovale:Logf("%s return %s [%d]", element.type, tostring(timeSpan), element.nodeId)
	profiler.Stop("OvaleBestAction_Compute")
	return timeSpan, priorityB, elementB
end

local function ComputeLua(element, state)
	profiler.Start("OvaleBestAction_ComputeLua")
	local ret = loadstring(element.lua)()
	Ovale:Logf("lua %s [%d]", ret, element.nodeId)

	local timeSpan = element.timeSpan
	timeSpan:Reset()

	timeSpan[1], timeSpan[2] = 0, math.huge
	local result = PutValue(element, ret, 0, 0)
	profiler.Stop("OvaleBestAction_ComputeLua")
	return timeSpan, OVALE_DEFAULT_PRIORITY, result
end

local function ComputeNot(element, state)
	profiler.Start("OvaleBestAction_Compute")
	Ovale:Logf("%s [%d]", element.type, element.nodeId)
	local self = OvaleBestAction
	local timeSpanA = self:ComputeBool(element.a, state)
	local timeSpan = element.timeSpan
	timeSpan:Reset()

	Complement(timeSpanA, timeSpan)
	Ovale:Logf("%s returns %s [%d]", element.type, tostring(timeSpan), element.nodeId)
	profiler.Stop("OvaleBestAction_Compute")
	return timeSpan
end

local function ComputeOr(element, state)
	profiler.Start("OvaleBestAction_Compute")
	Ovale:Logf("%s [%d]", element.type, element.nodeId)
	local self = OvaleBestAction
	local timeSpanA = self:ComputeBool(element.a, state)
	local timeSpanB = self:ComputeBool(element.b, state)
	local timeSpan = element.timeSpan
	timeSpan:Reset()

	-- Take union of A and B.
	Union(timeSpanA, timeSpanB, timeSpan)
	Ovale:Logf("%s returns %s [%d]", element.type, tostring(timeSpan), element.nodeId)
	profiler.Stop("OvaleBestAction_Compute")
	return timeSpan
end

local function ComputeValue(element, state)
	profiler.Start("OvaleBestAction_ComputeValue")
	Ovale:Logf("value %s", element.value)
	local timeSpan = element.timeSpan
	timeSpan:Reset()

	timeSpan[1], timeSpan[2] = 0, math.huge
	profiler.Stop("OvaleBestAction_ComputeValue")
	return timeSpan, OVALE_DEFAULT_PRIORITY, element
end

local function ComputeWait(element, state)
	profiler.Start("OvaleBestAction_Compute")
	Ovale:Logf("%s [%d]", element.type, element.nodeId)
	local self = OvaleBestAction
	local timeSpanA, priorityA, elementA = self:Compute(element.a, state)
	local timeSpan = element.timeSpan
	timeSpan:Reset()

	if elementA then
		elementA.wait = true
		CopyTimeSpan(timeSpanA, timeSpan)
		Ovale:Logf("%s return %s [%d]", element.type, tostring(timeSpan), element.nodeId)
	end
	profiler.Stop("OvaleBestAction_Compute")
	return timeSpan, priorityA, elementA
end
--</private-static-methods>

--<private-static-properties>
local OVALE_COMPUTE_VISITOR =
{
	["action"] = ComputeAction,
	["and"] = ComputeAnd,
	["arithmetic"] = ComputeArithmetic,
	["compare"] = ComputeCompare,
	["customfunction"] = ComputeCustomFunction,
	["function"] = ComputeFunction,
	["group"] = ComputeGroup,
	["if"] = ComputeIf,
	["lua"] = ComputeLua,
	["not"] = ComputeNot,
	["or"] = ComputeOr,
	["unless"] = ComputeIf,
	["value"] = ComputeValue,
	["wait"] = ComputeWait,
}
--</private-static-properties>

--<public-static-methods>
function OvaleBestAction:OnInitialize()
	-- Resolve module dependencies.
	OvaleActionBar = Ovale.OvaleActionBar
	OvaleCooldown = Ovale.OvaleCooldown
	OvaleCondition = Ovale.OvaleCondition
	OvaleData = Ovale.OvaleData
	OvaleEquipement = Ovale.OvaleEquipement
	OvaleFuture = Ovale.OvaleFuture
	OvalePower = Ovale.OvalePower
	OvaleSpellBook = Ovale.OvaleSpellBook
	OvaleStance = Ovale.OvaleStance
end

function OvaleBestAction:StartNewAction(state)
	state:Reset()
	OvaleFuture:ApplyInFlightSpells(state)
	self_serial = self_serial + 1
end

function OvaleBestAction:GetActionInfo(element, state)
	if not element then
		return nil
	end

	profiler.Start("OvaleBestAction_GetActionInfo")
	local target = element.params.target or OvaleCondition.defaultTarget
	local action
	local actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration,
		actionUsable, actionShortcut, actionIsCurrent, actionEnable,
		actionType, actionId

	if element.func == "spell" then
		local spellId = element.params[1]
		action = OvaleActionBar:GetForSpell(spellId)
		if not OvaleSpellBook:IsKnownSpell(spellId) and not action then
			Ovale:Logf("Spell %s not learnt", spellId)
			profiler.Stop("OvaleBestAction_GetActionInfo")
			return nil
		end

		actionTexture = actionTexture or API_GetSpellTexture(spellId)
		actionInRange = API_IsSpellInRange(OvaleSpellBook:GetSpellName(spellId), target)
		actionCooldownStart, actionCooldownDuration, actionEnable = state:GetSpellCooldown(spellId)
		actionType = "spell"
		actionId = spellId

		-- Verify that the spell may be cast given restrictions specified in SpellInfo().
		local si = OvaleData.spellInfo[spellId]
		if si then
			if si.stance and not OvaleStance:IsStance(si.stance) then
				-- Spell requires a stance that player is not in.
				profiler.Stop("OvaleBestAction_GetActionInfo")
				return nil
			end
			if si.combo then
				-- Spell requires combo points.
				local cost = state:ComboPointCost(spellId)
				if state.combo < cost then
					profiler.Stop("OvaleBestAction_GetActionInfo")
					return nil
				end
			end
			for powerType in pairs(OvalePower.SECONDARY_POWER) do
				if si[powerType] then
					-- Spell requires "secondary" resources, e.g., chi, focus, rage, etc.,
					local cost = state:PowerCost(spellId, powerType)
					if state[powerType] < cost then
						profiler.Stop("OvaleBestAction_GetActionInfo")
						return nil
					end
				end
			end

			if actionCooldownStart and actionCooldownDuration then
				-- Get the maximum time before all "primary" resources are ready.
				local atTime = state.currentTime
				for powerType in pairs(OvalePower.PRIMARY_POWER) do
					if si[powerType] then
						local t = state.currentTime + state:TimeToPower(spellId, powerType)
						if atTime < t then
							atTime = t
						end
					end
				end
				if actionCooldownStart > 0 then
					if atTime > actionCooldownStart + actionCooldownDuration then
						Ovale:Logf("Delaying spell %s for primary resource.", spellId)
						actionCooldownDuration = atTime - actionCooldownStart
					end
				else
					actionCooldownStart = state.currentTime
					actionCooldownDuration = atTime - actionCooldownStart
				end

				if si.blood or si.frost or si.unholy or si.death then
					-- Spell requires runes.
					local needRunes = true
					-- "buff_runes_none" is the spell ID of the buff that makes casting the spell cost no runes.
					local buffNoRunes = si.buff_runes_none
					if buffNoRunes then
						local aura = state:GetAura("player", buffNoRunes)
						if state:IsActiveAura(aura) then
							needRunes = false
						end
					end
					if needRunes then
						local ending = state.currentTime + state:GetRunesCooldown(si.blood, si.unholy, si.frost, si.death)
						if ending > actionCooldownStart + actionCooldownDuration then
							actionCooldownDuration = ending - actionCooldownStart
						end
					end
				end
			end
			-- Use a custom texture if given.
			if si.texture then
				actionTexture = "Interface\\Icons\\" .. si.texture
			end
		end

		actionUsable = OvaleSpellBook:IsUsableSpell(spellId)

	elseif element.func == "macro" then
		local macro = element.params[1]
		action = OvaleActionBar:GetForMacro(macro)
		if not action then
			Ovale:Logf("Unknown macro %s", macro)
			profiler.Stop("OvaleBestAction_GetActionInfo")
			return nil
		end
		actionTexture = API_GetActionTexture(action)
		actionInRange = API_IsActionInRange(action, target)
		actionCooldownStart, actionCooldownDuration, actionEnable = API_GetActionCooldown(action)
		actionUsable = API_IsUsableAction(action)
		actionType = "macro"
		actionId = macro

	elseif element.func == "item" then
		local itemId = element.params[1]
		if itemId and type(itemId) ~= "number" then
			itemId = OvaleEquipement:GetEquippedItem(itemId)
		end
		if not itemId then
			Ovale:Logf("Unknown item %s", element.params[1])
			profiler.Stop("OvaleBestAction_GetActionInfo")
			return nil
		end
		Ovale:Logf("Item %s", itemId)
		action = OvaleActionBar:GetForItem(itemId)

		actionTexture = API_GetItemIcon(itemId)
		actionInRange = API_IsItemInRange(itemId, target)
		actionCooldownStart, actionCooldownDuration, actionEnable = API_GetItemCooldown(itemId)

		local spellName = API_GetItemSpell(itemId)
		actionUsable = spellName and API_IsUsableItem(action)
		actionType = "item"
		actionId = itemId

	elseif element.func == "texture" then
		local texture = element.params[1]
		actionTexture = "Interface\\Icons\\" .. texture
		actionInRange = nil
		actionCooldownStart = API_GetTime()
		actionCooldownDuration = 0
		actionEnable = 1
		actionUsable = true
		actionType = "texture"
		actionId = texture
	end

	if action then
		actionShortcut = OvaleActionBar:GetBinding(action)
		actionIsCurrent = API_IsCurrentAction(action)
	end

	profiler.Stop("OvaleBestAction_GetActionInfo")
	return actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration,
		actionUsable, actionShortcut, actionIsCurrent, actionEnable, actionType, actionId, target, element.params.nored
end

function OvaleBestAction:Compute(element, state)
	if not element or (Ovale.bug and not Ovale.trace) then
		return nil
	end

	local visitor = OVALE_COMPUTE_VISITOR[element.type]
	if visitor then
		return visitor(element, state)
	end

	Ovale:Logf("unknown element %s, return nil", element.type)
	return nil
end

function OvaleBestAction:ComputeBool(element, state)
	local timeSpan, _, newElement = self:Compute(element, state)
	-- Match SimC: 0 is false, non-zero is true.
	--	(https://code.google.com/p/simulationcraft/wiki/ActionLists#Logical_operators)
	if newElement and newElement.type == "value" and newElement.value == 0 and newElement.rate == 0 then
		return nil
	else
		return timeSpan
	end
end
--</public-static-methods>