Quantcast
--[[--------------------------------------------------------------------
    Ovale Spell Priority
    Copyright (C) 2014 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 OvaleSimulationCraft = {}
Ovale.OvaleSimulationCraft = OvaleSimulationCraft

--<private-static-properties>
local OvaleLexer = Ovale.OvaleLexer

local format = string.format
local gmatch = string.gmatch
local gsub = string.gsub
local setmetatable = setmetatable
local strfind = string.find
local strlower = string.lower
local strmatch = string.match
local strsplit = strsplit
local strupper = string.upper
local strsub = string.sub
local tconcat = table.concat
local tinsert = table.insert
local tonumber = tonumber
local tremove = table.remove
local tsort = table.sort
local type = type
local wipe = table.wipe
local yield = coroutine.yield
local RAID_CLASS_COLORS = RAID_CLASS_COLORS

local INDENTATION = {}
do
	INDENTATION[0] = ""
	local metatable = {
		__index = function(tbl, key)
				key = tonumber(key)
				if key > 0 then
					local s = tbl[key - 1] .. "	"
					rawset(tbl, key, s)
					return s
				end
				return INDENTATION[0]
			end,
	}
	setmetatable(INDENTATION, metatable)
end

local SIMC_CLASS = {}
do
	for class in pairs(RAID_CLASS_COLORS) do
		SIMC_CLASS[strlower(class)] = true
	end
end
--<private-static-properties>

--<public-static-properties>
OvaleSimulationCraft.simcString = nil
OvaleSimulationCraft.simcComments = nil
OvaleSimulationCraft.indent = 0
OvaleSimulationCraft.profile = {}
OvaleSimulationCraft.symbols = {}
OvaleSimulationCraft.script = {}
OvaleSimulationCraft.__index = OvaleSimulationCraft
do
	-- Class constructor
	setmetatable(OvaleSimulationCraft, { __call = function(self, ...) return self:New(...) end })
end
--</public-static-properties>

--<private-static-methods>
local function NameValuePair(expr)
	local name, value = strmatch(expr, "^([^=]*)=(.*)")
	if strmatch(value, "^[%-]?%d+%.?%d*$") then
		value = tonumber(value)
	end
	return name, value
end
--</private-static-methods>

--<public-static-methods>
function OvaleSimulationCraft:New(simcString)
	local obj = {
		simcString = nil,
		simcComments = nil,
		indent = 0,
		profile = {},
		symbols = {},
		script = {},
	}
	setmetatable(obj, self)
	if simcString then
		obj:ParseProfile(simcString)
	end
	return obj
end

function OvaleSimulationCraft:Indent()
	self.indent = self.indent + 1
end

function OvaleSimulationCraft:UnIndent()
	self.indent = (self.indent > 0) and (self.indent - 1) or 0
end

function OvaleSimulationCraft:Append(script, ...)
	local s = format("%s%s", INDENTATION[self.indent], format(...))
	tinsert(script, s)
end

function OvaleSimulationCraft:ParseProfile(simcString)
	self.simcString = simcString
	local profile = self.profile
	for line in gmatch(simcString, "[^\r\n]+") do
		line = strmatch(line, "^%s*(.-)%s*$")
		if not (strmatch(line, "^#.*") or strmatch(line, "^$")) then
			local key, value = strmatch(line, "([^%+=]+)%+?=(.*)")
			if not profile[key] then
				profile[key] = value
			elseif type(profile[key]) == "table" then
				tinsert(profile[key], value)
			else
				local oldValue = profile[key]
				profile[key] = {}
				tinsert(profile[key], oldValue)
				tinsert(profile[key], value)
			end
		end
	end
	-- Concatenate variables defined over multiple lines using +=
	for k, v in pairs(profile) do
		if type(v) == "table" then
			local value = tconcat(v)
			profile[k] = value
		end
	end
	for k, v in pairs(profile) do
		if strmatch(k, "^actions") then
			local listName = strmatch(k, "^actions[.]([%w_]+)") or "default"
			profile[k] = nil
			profile.actionList = profile.actionList or {}
			local tbl = { strsplit("/", v) }
			for i, action in ipairs(tbl) do
				local line = tbl[i]
				tbl[i] = { strsplit(",", action) }
				tbl[i].action = tremove(tbl[i], 1)
				tbl[i].line = line
			end
			profile.actionList[listName] = tbl
		elseif k == "glyphs" then
			profile[k] = { strsplit("/", v) }
		elseif k == "professions" then
			local tbl = { strsplit("/", v) }
			for i, profession in ipairs(tbl) do
				local prof, level = NameValuePair(profession)
				tbl[i] = nil
				tbl[prof] = level
			end
			profile[k] = tbl
		end
	end
end

do
	local symbols = {}

	function OvaleSimulationCraft:GenerateScript(script)
		script = script or {}
		local profile = self.profile
		for class in pairs(SIMC_CLASS) do
			local simcName = profile[class]
			if simcName then
				profile.simcName = strsub(simcName, 2, -2)
				profile.class = class
				break
			end
		end

		self:Append(script, "local _, Ovale = ...")
		self:Append(script, "local OvaleScripts = Ovale.OvaleScripts")
		self:Append(script, "")
		self:Append(script, "do")
		self:Append(script, [[	local name = "SimulationCraft: %s"]], profile.simcName)
		self:Append(script, [[	local desc = "[5.4] SimulationCraft: %s" ]], profile.simcName)
		self:Append(script, "	local code = [[")

		self:Append(script, [[# Based on SimulationCraft profile "%s".]], profile.simcName)
		if profile.class then
			self:Append(script, "#	class=%s", profile.class)
		end
		if profile.spec then
			self:Append(script, "#	spec=%s", profile.spec)
		end
		if profile.talents then
			self:Append(script, "#	talents=%s", profile.talents)
		end
		if profile.glyphs then
			self:Append(script, "#	glyphs=%s", tconcat(profile.glyphs, "/"))
		end
		if profile.default_pet then
			self:Append(script, "#	pet=%s", profile.default_pet)
		end

		self:Append(script, "")
		self:Append(script, "Include(ovale_common)")
		self:Append(script, format("Include(ovale_%s_common)", profile.class))

		if profile.actionList then
			for listName, actionList in pairs(profile.actionList) do
				self:ParseActionList(script, listName, actionList)
			end
		end
		self:Append(script, "")

		if profile.spec then
			self:Append(script, format("AddIcon specialization=%s help=main", profile.spec))
		else
			self:Append(script, "AddIcon help=main")
		end
		self:Append(script, "{")
		self:Indent()
		if profile.actionList.precombat then
			self:Append(script, format("if InCombat(no) %s()", self:FunctionName("precombat")))
		end
		self:Append(script, format("%s()", self:FunctionName("default")))
		self:UnIndent()
		self:Append(script, "}")
		self:Append(script, "")

		tsort(self.symbols)
		wipe(symbols)
		for _, v in ipairs(self.symbols) do
			if not symbols[v] then
				symbols[v] = true
				tinsert(symbols, v)
			end
		end
		self:Append(script, "### Required symbols")
		for _, v in ipairs(symbols) do
			self:Append(script, format("# %s", v))
		end
		self:Append(script, "]]")
		if profile.class then
			self:Append(script, [[	OvaleScripts:RegisterScript("%s", name, desc, code, "reference")]], strupper(profile.class))
		end
		self:Append(script, "end")
		self:Append(script, "")

		return script
	end
end

do
	local function TitleCase(first, rest)
		return strupper(first) .. strlower(rest)
	end

	function OvaleSimulationCraft:FunctionName(listName)
		if self.profile.spec then
			listName = format("%s_%s_actions", self.profile.spec, listName)
		else
			listName = format("%s_actions", listName)
		end
		listName = gsub(listName, "_", " ")
		listName = gsub(listName, "(%a)(%w*)", TitleCase)
		listName = gsub(listName, "%s", "")
		return listName
	end
end

function OvaleSimulationCraft:ParseActionList(script, listName, actionList)
	self:Append(script, "")
	self:Append(script, "AddFunction %s", self:FunctionName(listName))
	self:Append(script, "{")
	self:Indent()
	local indent = self.indent
	for i, actionLine in ipairs(actionList) do
		self:ParseActionLine(script, actionLine)
	end
	while self.indent > indent do
		self:UnIndent()
		self:Append(script, "}")
	end
	self:UnIndent()
	self:Append(script, "}")
end

do
	local TO_NAME = {
		deathknight = {
			blood = {
				["soul_reaper"] = "soul_reaper_blood",
			},
			frost = {
				["soul_reaper"] = "soul_reaper_frost",
			},
			unholy = {
				["soul_reaper"] = "soul_reaper_unholy",
			},
		},
		druid = {
			balance = {
				["dream_of_cenarius"] = "dream_of_cenarius_caster",
				["force_of_nature"] = "force_of_nature_caster",
				["heart_of_the_wild"] = "heart_of_the_wild_caster",
				["wild_mushroom"] = "wild_mushroom_caster",
			},
			feral = {
				["berserk"] = "berserk_cat",
				["dream_of_cenarius"] = "dream_of_cenarius_melee",
				["force_of_nature"] = "force_of_nature_melee",
				["heart_of_the_wild"] = "heart_of_the_wild_melee",
				["omen_of_clarity"] = "omen_of_clarity_melee",
				["stealth"] = "prowl",
			},
			restoration = {
				["force_of_nature"] = "force_of_nature_heal",
				["heart_of_the_wild"] = "heart_of_the_wild_heal",
				["omen_of_clarity"] = "omen_of_clarity_heal",
			},
		},
		paladin = {
			protection = {
				["arcane_torrent"] = "arcane_torrent_mana",
				["guardian_of_ancient_kings"] = "guardian_of_ancient_kings_tank",
			},
			retribution = {
				["arcane_torrent"] = "arcane_torrent_mana",
				["guardian_of_ancient_kings"] = "guardian_of_ancient_kings_melee",
			},
		},
		priest = {
			shadow = {
				["arcane_torrent"] = "arcane_torrent_mana",
			},
		},
		rogue = {
			assassination = {
				["arcane_torrent"] = "arcane_torrent_energy",
			},
			combat = {
				["arcane_torrent"] = "arcane_torrent_energy",
			},
			subtlety = {
				["arcane_torrent"] = "arcane_torrent_energy",
			},
		},
		shaman = {
			elemental = {
				["ascendance"] = "ascendance_caster",
			},
			enhancement = {
				["ascendance"] = "ascendance_melee",
			},
			restoration = {
				["ascendance"] = "ascendance_heal",
			},
		},
		warlock = {
			affliction = {
				["dark_soul"] = "dark_soul_misery",
			},
			demonology = {
				["dark_soul"] = "dark_soul_knowledge",
			},
			destruction = {
				["dark_soul"] = "dark_soul_instability",
				["rain_of_fire"] = "rain_of_fire_aftermath",
			},
		},
		warrior = {
			arms = {
				["cooldown_reduction"] = "cooldown_reduction_strength",
			},
			fury = {
				["cooldown_reduction"] = "cooldown_reduction_strength",
			},
		}
	}

	function OvaleSimulationCraft:Name(name)
		local class, spec = self.profile.class, self.profile.spec
		if TO_NAME[class] and TO_NAME[class][spec] and TO_NAME[class][spec][name] then
			return TO_NAME[class][spec][name]
		end
		return name
	end
end

do
	local SIMC_ACTION = {
		["^jade_serpent_potion$"] = "UsePotionIntellect()",
		["^mogu_power_potion$"] = "UsePotionStrength()",
		["^virmens_bite_potion$"] = "UsePotionAgility()",
		["^$"] = false,
		["^auto_attack$"] = false,
		["^auto_shot$"] = false,
		["^elixir$"] = false,
		["^flask$"] = false,
		["^food$"] = false,
		["^snapshot_stats$"] = false,
		-- Death Knight
		["^blood_presence$"] = "if not Stance(deathknight_blood_presence) Spell(blood_presence)",
		["^frost_presence$"] = "if not Stance(deathknight_frost_presence) Spell(frost_presence)",
		["^unholy_presence$"] = "if not Stance(deathknight_unholy_presence) Spell(unholy_presence)",
		["^blood_tap$"] = "BloodTap()",
		["^plague_leech$"] = "PlagueLeech()",
		-- Druid
		["^cat_form$"] = "if not Stance(druid_cat_form) Spell(cat_form)",
		["^moonkin_form$"] = "if not Stance(druid_moonkin_form) Spell(moonkin_form)",
		["^prowl$"] = "if Stealthed(no) Spell(prowl)",
		["^ravage$"] = "Spell(ravage usable=1)",
		["^savage_roar$"] = "SavageRoar()",
		["^skull_bash_cat$"] = "FeralInterrupt()",
		-- Hunter
		["^aspect_of_the_"] = function(simc, action) return format("if not Stance(hunter_%s) Spell(%s)", action, action) end,
		["^kill_command$"] = "KillCommand()",
		["^kill_shot$"] = "Spell(kill_shot usable=1)",
		["^summon_pet$"] = "SummonPet()",
		-- Mage
		["^arcane_brilliance$"] = "if BuffExpires(critical_strike any=1) or BuffExpires(spell_power_multiplier any=1) Spell(arcane_brilliance)",
		["^cancel_buff$"] = false,
		["^conjure_mana_gem$"] = "ConjureManaGem()",
		["^mana_gem$"] = "UseManaGem()",
		["^frost_armor$"] = function(simc, action)
				tinsert(simc.symbols, "frost_armor_buff")
				return "if BuffExpires(frost_armor_buff) Spell(frost_armor)"
			end,
		["^icy_veins$"] = "IcyVeins()",
		["^molten_armor$"] = function(simc, action)
				tinsert(simc.symbols, "molten_armor_buff")
				return "if BuffExpires(molten_armor_buff) Spell(molten_armor)"
			end,
		["^water_elemental$"] = "if pet.Present(no) Spell(water_elemental)",
		-- Monk
		["^chi_sphere$"] = false,
		-- Paladin
		["^hammer_of_wrath$"] = "Spell(hammer_of_wrath usable=1)",
		["^rebuke$"] = "Interrupt()",
		["^seal_of_"] = function(simc, action) return format("if not Stance(paladin_%s) Spell(%s)", action, action) end,
		-- Priest
		["^inner_fire$"] = function(simc, action)
				tinsert(simc.symbols, "inner_fire_buff")
				return "if BuffExpires(inner_fire_buff) Spell(inner_fire)"
			end,
		["^mind_flay_insanity$"] = function(simc, action)
				tinsert(simc.symbols, "mind_flay")
				return "Spell(mind_flay)"
			end,
		["^shadowform$"] = "if not Stance(priest_shadowform) Spell(shadowform)",
		["^shadow_word_death$"] = "Spell(shadow_word_death usable=1)",
		-- Rogue
		["^ambush$"] = "Spell(ambush usable=1)",
		["^apply_poison$"] = "ApplyPoisons()",
		["^backstab$"] = "Spell(backstab usable=1)",
		["^dispatch$"] = "Spell(dispatch usable=1)",
		["^kick$"] = "if target.IsInterruptible() Spell(kick)",
		["^premeditation$"] = "Spell(premeditation usable=1)",
		["^stealth$"] = "if not IsStealthed() Spell(stealth)",
		["^tricks_of_the_trade$"] = "TricksOfTheTrade()",
		-- Shaman
		["^bloodlust$"] = "Bloodlust()",
		["^wind_shear$"] = "Interrupt()",
		-- Warlock
		["^service_pet$"] = "ServicePet()",
		-- Warrior
		["^execute$"] = "Spell(execute usable=1)",
		["^heroic_leap$"] = "HeroicLeap()",
		["^raging_blow$"] = "RagingBlow()",
	}

	local scriptLine = {}

	function OvaleSimulationCraft:ParseActionLine(script, actionLine)
		wipe(scriptLine)
		if self.simcComments then
			self:Append(script, "#%s", actionLine.line)
		end

		local action = self:Name(gsub(actionLine.action, ":", "_"))
		local matchedAction = false
		for pattern, result in pairs(SIMC_ACTION) do
			if strmatch(action, pattern) then
				matchedAction = true
				scriptLine.action = (type(result) == "function") and result(self, action) or result
				break
			end
		end
		if not matchedAction then
			scriptLine.action = format("Spell(%s)", action)
		end

		if scriptLine.action then
			local addActionSymbol = false
			if #actionLine == 0 then
				addActionSymbol = true
			else
				for i, expr in ipairs(actionLine) do
					local name, value = NameValuePair(expr)
					if action == "use_item" then
						scriptLine.action = "UseItemActions()"
						--[[
						if name == "slot" then
							if value == "hands" then
								scriptLine.action = "Item(HandsSlot usable=1)"
							elseif value == "trinket" then
								scriptLine.action = "{ Item(Trinket0Slot usable=1) Item(Trinket1Slot usable=1) }"
							end
						elseif name == "name" then
							if strmatch(value, "gauntlets") or strmatch(value, "gloves") or strmatch(value, "grips") or strmatch(value, "handguards") then
								scriptLine.action = "Item(HandsSlot usable=1)"
							end
						end
						]]--
					elseif action == "wait" then
						if name == "sec" then
							if type(value) == "number" then
								scriptLine.action = nil
							else
								local spellName = strmatch(value, "^cooldown%.([%w_]+)%.remains$")
								if spellName then
									scriptLine.action = format("wait Spell(%s)", spellName)
								end
							end
						end
					elseif action == "swap_action_list" then
						if name == "name" then
							scriptLine.action = format("%s()", self:FunctionName(value))
						end
					elseif action == "run_action_list" then
						if name == "name" then
							scriptLine.action = format("%s()", self:FunctionName(value))
						end
					elseif action == "pool_resource" then
						scriptLine.action = nil
						if name == "for_next" and value == 1 then
							script.for_next = true
						end
					elseif action == "stance" then
						-- Don't add symbol for "stance".
					else
						addActionSymbol = true
					end
					if name == "if" then
						scriptLine.if_expr = self:ParseExpression(action, value, addActionSymbol)
					else
						scriptLine[name] = value
					end
				end
			end
			if addActionSymbol then
				tinsert(self.symbols, action)
			end

			if scriptLine.if_expr
				or scriptLine.moving == 1
				or scriptLine.sync
				or action == "focus_fire" and scriptLine.five_stacks == 1
				or action == "grimoire_of_sacrifice"
				or action == "kill_command"
				or action == "mind_flay_insanity"
				or action == "stance" and scriptLine.choose
				or scriptLine.weapon
				or scriptLine.max_cycle_targets
			then
				local needAnd = false
				if action == "pool_resource" and not script.for_next then
					tinsert(scriptLine, "unless")
				else
					tinsert(scriptLine, "if")
				end
				if scriptLine.moving == 1 then
					tinsert(scriptLine, "Speed() > 0")
					needAnd = true
				end
				if scriptLine.sync then
					if needAnd then
						tinsert(scriptLine, "and")
					end
					tinsert(scriptLine, format("not SpellCooldown(%s) > 0", scriptLine.sync))
					needAnd = true
				end
				if scriptLine.weapon then
					if needAnd then
						tinsert(scriptLine, "and")
					end
					tinsert(scriptLine, format("WeaponEnchantExpires(%s)", scriptLine.weapon))
					needAnd = true
				end
				if action == "focus_fire" and scriptLine.five_stacks == 1 then
					if needAnd then
						tinsert(scriptLine, "and")
					end
					tinsert(scriptLine, "BuffStacks(frenzy_buff any=1) == 5")
					needAnd = true
				elseif action == "grimoire_of_sacrifice" then
					if needAnd then
						tinsert(scriptLine, "and")
					end
					tinsert(scriptLine, "pet.Present()")
					needAnd = true
				elseif action == "kill_command" then
					if needAnd then
						tinsert(scriptLine, "and")
					end
					tinsert(scriptLine, "pet.Present()")
					needAnd = true
				elseif action == "mind_flay_insanity" then
					if needAnd then
						tinsert(scriptLine, "and")
					end
					tinsert(scriptLine, "TalentPoints(solace_and_insanity_talent) and target.DebuffPresent(devouring_plague_debuff)")
					tinsert(self.symbols, "solace_and_insanity_talent")
					tinsert(self.symbols, "devouring_plague_debuff")
					needAnd = true
				elseif action == "stance" and scriptLine.choose then
					if needAnd then
						tinsert(scriptLine, "and")
					end
					local class = self.profile.class
					if class == "deathknight" then
						local spellName = format("%s_presence", scriptLine.choose)
						scriptLine.action = format("Spell(%s)", spellName)
						tinsert(scriptLine, format("not Stance(%s_%s)", class, spellName))
					elseif class == "monk" then
						local spellName = format("stance_of_the_%s", scriptLine.choose)
						scriptLine.action = format("Spell(%s)", spellName)
						tinsert(scriptLine, format("not Stance(%s_%s)", class, spellName))
					elseif class == "warrior" then
						local spellName = format("%s_stance", scriptLine.choose)
						scriptLine.action = format("Spell(%s)", spellName)
						tinsert(scriptLine, format("not Stance(%s_%s)", class, spellName))
					else
						tinsert(scriptLine, format("not Stance(%s_%s)", class, scriptLine.choose))
					end
					needAnd = true
				end
				if scriptLine.if_expr then
					if needAnd then
						tinsert(scriptLine, "and")
					end
					if needAnd and strmatch(scriptLine.if_expr, " or ") then
						tinsert(scriptLine, format("{ %s }", scriptLine.if_expr))
					else
						tinsert(scriptLine, scriptLine.if_expr)
					end
					needAnd = true
				end
				if scriptLine.max_cycle_targets then
					if needAnd then
						tinsert(scriptLine, "and")
					end
					tinsert(scriptLine, format("DebuffCountOnAny(%s_debuff) <= %d", action, scriptLine.max_cycle_targets))
					needAnd = true
				end
			end

			if action ~= "pool_resource" then
				if script.for_next then
					tinsert(scriptLine, "wait")
					script.for_next = nil
				end
				tinsert(scriptLine, scriptLine.action)
			end

			if scriptLine[1] then
				self:Append(script, tconcat(scriptLine, " "))
			end
			if action == "pool_resource" and not script.for_next then
				self:Append(script, "{")
				self:Indent()
			end
		end
	end
end

do
	-- Table of matching tokens for SimC conditional expressions for OvaleLexer.scan().
	local MATCHES = {}
	local function chdump(tok, options)
		if options and options.string then
			tok = strsub(tok, 2, -2)
		end
		return yield("char", tok)
	end

	local function ndump(tok, options)
		if options and options.number then
			tok = tonumber(tok)
		end
		return yield("number", tok)
	end

	local function tdump(tok)
		return yield(tok, tok)
	end

	local function vdump(tok)
		return yield("iden", tok)
	end

	local function wsdump(tok)
		return yield("space", tok)
	end

	-- whitespace
	tinsert(MATCHES, { '^%s+', wsdump })
	-- numbers
	tinsert(MATCHES, { '^[%-]?%d+%.?%d*', ndump })
	-- floor/ceil
	tinsert(MATCHES, { '^floor', tdump })
	tinsert(MATCHES, { '^ceil', tdump })
	-- identifiers (foo.bar.baz.etc)
	tinsert(MATCHES, { '^[%a_][%w_%.]*[%w_]', vdump })
	-- not-equal
	tinsert(MATCHES, { '^!=', tdump })
	-- less-than-equal
	tinsert(MATCHES, { '^<=', tdump })
	-- greater-than-equal
	tinsert(MATCHES, { '^>=', tdump })
	-- exclusion
	tinsert(MATCHES, { '^!~', tdump })
	-- catch-all
	tinsert(MATCHES, { '^.', tdump })

	--[[
		The "buff" list also contains special buffs such as:
			"bleeding" (target is bleeding),
			"casting" (character is casting),
			"raid_movement" (character is moving because of a "movement" raid event),
			"poisoned" (target is poisoned),
			"self_movement" (character is moving because of a start_move action),
			"stunned" (character is stunned),
			"vulnerable" (target is vulnerable),
			"stealthed" (character is stealthed).
	--]]
	local SPECIAL_PROPERTY = {
		["^debuff%.casting%.react$"] = "IsInterruptible()",
		["^debuff%.flying%.down$"] = "True(debuff_flying_down)",
		["^buff%.raid_movement%.duration$"] = "0",
		-- Pretend the target can never be invulnerable.
		["^debuff%.invulnerable%.react$"] = "InCombat(no)",
		["^buff%.bloodlust%.react$"] = "BuffPresent(burst_haste any=1)",
		["^buff%.bloodlust%.up$"] = "BuffPresent(burst_haste any=1)",
		["^buff%.bloodlust%.down$"] = "BuffExpires(burst_haste any=1)",
		["^buff%.stealthed%.down$"] = "Stealthed(no)",
		["^buff%.stealthed%.up$"] = "Stealthed()",
		["^debuff%.weakened_armor%.stack$"] = function(simc, action)
				tinsert(simc.symbols, "weakened_armor_debuff")
				return "target.DebuffStacks(weakened_armor_debuff any=1)"
			end,
		["^buff%.vicious%.react$"] = function(simc, action)
				tinsert(simc.symbols, "trinket_proc_agility_buff")
				return "BuffPresent(trinket_proc_agility_buff)"
			end,
		["^buff%.vicious%.remains$"] = function(simc, action)
				tinsert(simc.symbols, "trinket_proc_agility_buff")
				return "BuffRemains(trinket_proc_agility_buff)"
			end,
		["^distance$"] = "Distance()",
		-- Druid
		["^buff%.wild_mushroom%.max_stack$"] = function(simc, action)
				local class, spec = simc.profile.class, simc.profile.spec
				if class == "druid" and spec == "restoration" then
					return "1"
				end
				return "3"
			end,
		["^buff%.wild_mushroom%.stack$"] = "WildMushroomCount()",
		-- Hunter
		["^debuff%.ranged_vulnerability%.up$"] = "target.DebuffPresent(ranged_vulnerability any=1)",
		["^buff%.beast_cleave$.down$"] = function(simc, action)
				tinsert(simc.symbols, "pet_beast_cleave_buff")
				return "pet.BuffExpires(pet_beast_cleave_buff any=1)"
			end,
		-- Mage
		["^buff%.arcane_charge%.stack$"] = function(simc, action)
				tinsert(simc.symbols, "arcane_charge_debuff")
				return "DebuffStacks(arcane_charge_debuff)"
			end,
		["^buff%.rune_of_power%.remains$"] = "RuneOfPowerRemains()",
		-- Monk
		["^dot%.zen_sphere%.ticking$"] = function(simc, action)
				tinsert(simc.symbols, "zen_sphere_buff")
				return "BuffPresent(zen_sphere_buff)"
			end,
		-- Priest
		["^buff%.surge_of_darkness%.react$"] = function(simc, action)
				tinsert(simc.symbols, "surge_of_darkness_buff")
				return "BuffStacks(surge_of_darkness_buff)"
			end,
		["^dot%.devouring_plague_tick%.ticks_remain$"] = function(simc, action)
				tinsert(simc.symbols, "devouring_plague_debuff")
				return "TicksRemain(devouring_plague_debuff)"
			end,
		-- Rogue
		["^buff%.stealth%.down$"] = "Stealthed(no)",
		["^buff%.stealth%.up$"] = "Stealthed()",
		-- Shaman
		["^active_flame_shock$"] = function(simc, action)
				tinsert(simc.symbols, "flame_shock_debuff")
				return "DebuffCountOnAny(flame_shock_debuff)"
			end,
		["^buff%.lightning_shield%.max_stack$"] = "7",
		["^buff%.lightning_shield%.react$"] = function(simc, action)
				tinsert(simc.symbols, "lightning_shield_buff")
				return "BuffStacks(lightning_shield_buff)"
			end,
		["^buff%.maelstrom_weapon%.react$"] = function(simc, action)
				tinsert(simc.symbols, "maelstrom_weapon_buff")
				return "BuffStacks(maelstrom_weapon_buff)"
			end,
		["^pet%.greater_fire_elemental%.active$"] = "TotemPresent(fire totem=fire_elemental_totem)",
		["^pet%.primal_fire_elemental%.active$"] = "TotemPresent(fire totem=fire_elemental_totem)",
		-- Warlock
		["^buff%.havoc%.remains$"] = function(simc, action)
				tinsert(simc.symbols, "havoc_debuff")
				return "DebuffRemainsOnAny(havoc_debuff)"
			end,
		["^buff%.havoc%.stack$"] = function(simc, action)
				tinsert(simc.symbols, "havoc_debuff")
				return "DebuffStacksOnAny(havoc_debuff)"
			end,
		["^buff%.molten_core%.react$"] = function(simc, action)
				tinsert(simc.symbols, "molten_core_buff")
				return "BuffStacks(molten_core_buff)"
			end,
		["^debuff%.magic_vulnerability%.down$"] = "target.DebuffExpires(magic_vulnerability any=1)",
		["^debuff%.magic_vulnerability%.up$"] = "target.DebuffPresent(magic_vulnerability any=1)",
	}

	-- totem.<totem_name>.<totem_property>
	local TOTEM_TYPE = {
		["capacitor_totem"] = "air",
		["earth_elemental_totem"] = "earth",
		["earthbind_totem"] = "earth",
		["earthgrab_totem"] = "earth",
		["fire_elemental_totem"] = "fire",
		["grounding_totem"] = "air",
		["healing_stream_totem"] = "water",
		["healing_tide_totem"] = "water",
		["magma_totem"] = "fire",
		["mana_tide_totem"] = "water",
		["searing_totem"] = "fire",
		["stone_bulwark_totem"] = "earth",
		["stormlash_totem"] = "air",
		["tremor_totem"] = "earth",
		["windwalk_totem"] = "air",
	}
	local TOTEM_PROPERTY_PATTERN = "^totem%.([%w_]+)%.([%w_]+)$"
	local TOTEM_PROPERTY = {
		["^active$"] = function(simc, totemName)
				if TOTEM_TYPE[totemName] then
					return format("TotemPresent(%s totem=%s)", TOTEM_TYPE[totemName], totemName)
				else
					return format("TotemPresent(%s)", totemName)
				end
			end,
		["^remains$"] = function(simc, totemName)
				if TOTEM_TYPE[totemName] then
					return format("TotemRemains(%s totem=%s)", TOTEM_TYPE[totemName], totemName)
				else
					return format("TotemRemains(%s)", totemName)
				end
			end,
	}

	local ACTION_PROPERTY_PATTERN = "^action%.([%w_]+)%.([%w_]+)$"
	local ACTION_PROPERTY = {
		-- TODO: Buff?
		["^active$"] = function(simc, actionName)
				if strmatch(actionName, "_totem$") then
					return TOTEM_PROPERTY["^active$"](simc, actionName)
				else
					local symbol = format("%s_debuff", actionName)
					tinsert(simc.symbols, symbol)
					return format("target.DebuffPresent(%s)", symbol)
				end
			end,
		["^add_ticks$"] = function(simc, actionName)
				local symbol = format("%s_debuff", actionName)
				tinsert(simc.symbols, symbol)
				return format("TicksAdded(%s)", symbol)
			end,
		["^ember_react$"] = "BurningEmbers() >= 10",
		["^cast_delay$"] = "True(cast_delay)",
		["^cast_time$"] = function(simc, actionName) return format("CastTime(%s)", actionName) end,
		["^charges$"] = function(simc, actionName) return format("Charges(%s)", actionName) end,
		-- TODO: ItemCooldown?
		["^cooldown$"] = function(simc, actionName) return format("SpellCooldown(%s)", actionName) end,
		-- TODO: Item?
		["^cooldown_react$"] = function(simc, actionName) return format("Spell(%s)", actionName) end,
		["^crit_damage$"] = function(simc, actionName) return format("target.CritDamage(%s)", actionName) end,
		-- TODO: Melee/Ranged/Spell crit chance depending on type of attack, or at least class of player.
		["^crit_pct_current$"] = function(simc, actionName) return format("SpellCritChance()", actionName) end,
		["^crit_tick_damage$"] = function(simc, actionName)
				local symbol = format("%s_debuff", actionName)
				tinsert(simc.symbols, symbol)
				return format("target.CritDamage(%s)", symbol)
			end,
		["^duration$"] = function(simc, actionName)
				local symbol = format("%s_debuff", actionName)
				tinsert(simc.symbols, symbol)
				return format("SpellData(%s duration)", symbol)
			end,
		["^enabled$"] = function(simc, actionName)
				local symbol = format("%s_talent", actionName)
				tinsert(simc.symbols, symbol)
				return format("TalentPoints(%s)", symbol)
			end,
		["^gcd$"] = function(simc, actionName) return format("GCD()", actionName) end,
		["^hit_damage$"] = function(simc, actionName) return format("target.Damage(%s)", actionName) end,
		["^in_flight$"] = function(simc, actionName) return format("InFlightToTarget(%s)", actionName) end,
		["^in_flight_to_target$"] = function(simc, actionName) return format("InFlightToTarget(%s)", actionName) end,
		["^miss_react$"] = "True(miss_react)",
		-- TODO: Buff?
		["^n_ticks$"] = function(simc, actionName)
				local symbol = format("%s_debuff", actionName)
				tinsert(simc.symbols, symbol)
				return format("target.Ticks(%s)", symbol)
			end,
		["^recharge_time$"] = function(simc, actionName) return format("SpellChargeCooldown(%s)", actionName) end,
		-- TODO: Buff?
		["^remains$"] = function(simc, actionName)
				if strmatch(actionName, "_totem$") then
					return TOTEM_PROPERTY["^remains$"](simc, actionName)
				else
					local symbol = format("%s_debuff", actionName)
					tinsert(simc.symbols, symbol)
					return format("target.DebuffRemains(%s)", symbol)
				end
			end,
		["^shard_react$"] = "SoulShards() >= 1",
		["^spell_power$"] = function(simc, actionName)
				local symbol = format("%s_debuff", actionName)
				tinsert(simc.symbols, symbol)
				return format("target.DebuffSpellpower(%s)", symbol)
			end,
		-- TODO: Buff?
		["^tick_damage$"] = function(simc, actionName)
				local symbol = format("%s_debuff", actionName)
				tinsert(simc.symbols, symbol)
				return format("target.Damage(%s)", symbol)
			end,
		["^tick_multiplier$"] = function(simc, actionName) return format("target.DamageMultiplier(%s)", actionName) end,
		-- TODO: Buff?
		["^tick_time$"] = function(simc, actionName)
				local symbol = format("%s_debuff", actionName)
				tinsert(simc.symbols, symbol)
				return format("target.TickTime(%s)", symbol)
			end,
		-- TODO: Buff?
		["^ticking$"] = function(simc, actionName)
				local symbol = format("%s_debuff", actionName)
				tinsert(simc.symbols, symbol)
				return format("target.DebuffPresent(%s)", symbol)
			end,
		-- TODO: Buff?
		["^ticks_remain$"] = function(simc, actionName)
				local symbol = format("%s_debuff", actionName)
				tinsert(simc.symbols, symbol)
				return format("target.TicksRemain(%s)", symbol)
			end,
		-- TODO: Assume travel time of a spell is always 0.5s.
		["^travel_time$"] = function(simc, actionName) return "0.5" end,
	}

	local CHARACTER_PROPERTY = {
		["^anticipation_charges"] = function(simc, pattern, token)
				tinsert(simc.symbols, "anticipation_buff")
				return "BuffStacks(anticipation_buff)"
			end,
		["^burning_ember$"] = "BurningEmbers() / 10",
		["^chi$"] = "Chi()",
		["^chi%.max$"] = "MaxChi()",
		["^combo_points$"] = "ComboPoints()",
		["^demonic_fury$"] = "DemonicFury()",
		["^eclipse$"] = "Eclipse()",
		["^eclipse_dir$"] = "EclipseDir()",
		["^energy$"] = "Energy()",
		["^energy%.regen$"] = "EnergyRegen()",
		["^energy%.time_to_max$"] = "TimeToMaxEnergy()",
		["^focus$"] = "Focus()",
		["^focus%.regen$"] = "FocusRegen()",
		["^focus%.time_to_max$"] = "TimeToMaxFocus()",
		["^health$"] = "Health()",
		["^health%.deficit$"] = "HealthMissing()",
		["^health%.max$"] = "MaxHealth()",
		["^health%.pct$"] = "HealthPercent()",
		["^holy_power$"] = "HolyPower()",
		["^incoming_damage_([%d]+)(m?s)$"] = function(simc, pattern, token)
				local seconds, measure = strmatch(token, pattern)
				seconds = tonumber(seconds)
				if measure == "ms" then
					seconds = seconds / 1000
				end
				return format("IncomingDamage(%.3f)", seconds)
			end,
		["^in_combat$"] = "InCombat()",
		["^level$"] = "Level()",
		["^mana$"] = "Mana()",
		["^mana%.deficit$"] = "ManaDeficit()",
		["^mana%.max$"] = "MaxMana()",
		["^mana%.max_nonproc$"] = "MaxMana()",
		["^mana%.pct$"] = "ManaPercent()",
		["^mana%.pct_nonproc$"] = "ManaPercent()",
		["^mana_gem_charges$"] = function(simc, pattern, token)
				tinsert(simc.symbols, "mana_gem")
				return "ItemCharges(mana_gem)"
			end,
		["^mastery_value$"] = "MasteryEffect() / 100",
		["^multiplier$"] = "DamageMultiplier()",
		["^position_front$"] = "False(position_front)",	-- XXX
		["^rage$"] = "Rage()",
		["^rage%.max$"] = "MaxRage()",
		["^runic_power$"] = "RunicPower()",
		["^shadow_orb$"] = "ShadowOrbs()",
		["^soul_shards$"] = "SoulShards()",
		["^stat%.agility$"] = "Agility()",
		["^stat%.attack_power$"] = "AttackPower()",
		["^stat%.crit$"] = "CritRating()",
		["^stat%.crit_rating$"] = "CritRating()",
		["^stat%.energy$"] = "Energy()",
		["^stat%.focus$"] = "Focus()",
		["^stat%.haste_rating$"] = "HasteRating()",
		["^stat%.health$"] = "Health()",
		["^stat%.intellect$"] = "Intellect()",
		["^stat%.mana$"] = "Mana()",
		["^stat%.mastery_rating$"] = "MasteryRating()",
		["^stat%.maximum_energy$"] = "MaxEnergy()",
		["^stat%.maximum_focus$"] = "MaxFocus()",
		["^stat%.maximum_health$"] = "MaxHealth()",
		["^stat%.maximum_mana$"] = "MaxMana()",
		["^stat%.maximum_runic$"] = "MaxRunicPower()",
		["^stat%.rage$"] = "Rage()",
		["^stat%.runic$"] = "RunicPower()",
		["^stat%.spell_power$"] = "Spellpower()",
		["^stat%.spirit$"] = "Spirit()",
		["^stat%.stamina$"] = "Stamina()",
		["^stat%.strength$"] = "Strength()",
		["^time_to_die$"] = "TimeToDie()",
	}

	-- aura.<aura_name>.<aura_property>
	local AURA_PROPERTY_PATTERN = "^aura%.([%w_]+)%.([%w_]+)$"
	local AURA_PROPERTY = {
		["^down$"] = function(simc, name) return format("BuffExpires(%s any=1)", name) end,
		["^stack$"] = function(simc, name) return format("BuffStacks(%s any=1)", name) end,
		["^react$"] = function(simc, name) return format("BuffPresent(%s any=1)", name) end,
		["^remains$"] = function(simc, name) return format("BuffRemains(%s any=1)", name) end,
		["^up$"] = function(simc, name) return format("BuffPresent(%s any=1)", name) end,
	}

	-- (buff|debuff).<aura_name>.<aura_property>
	local function BuffType(buffType) return (buffType == "debuff") and "Debuff" or "Buff" end
	local BUFF_PROPERTY_PATTERN = "^(d?e?buff)%.([%w_]+)%.([%w_]+)$"
	local BUFF_PROPERTY = {
		-- XXX: Assume that the spell and the buff have the same name.
		["^cooldown_remains$"] = function(simc, buffType, name) return format("SpellCooldown(%s)", name) end,
		["^down$"] = function(simc, buffType, name) return format("%sExpires(%s_%s)", BuffType(buffType), name, buffType) end,
		["^duration$"] = function(simc, buffType, name) return format("SpellData(%s_%s duration)", name, buffType) end,
		["^stack$"] = function(simc, buffType, name) return format("%sStacks(%s_%s)", BuffType(buffType), name, buffType) end,
		-- "react" is supposed to be a stack count, but it's used in almost every script as "up".
		["^react$"] = function(simc, buffType, name) return format("%sPresent(%s_%s)", BuffType(buffType), name, buffType) end,
		["^remains$"] = function(simc, buffType, name) return format("%sRemains(%s_%s)", BuffType(buffType), name, buffType) end,
		["^up$"] = function(simc, buffType, name) return format("%sPresent(%s_%s)", BuffType(buffType), name, buffType) end,
		["^value$"] = function(simc, buffType, name) return format("%sAmount(%s_%s)", BuffType(buffType), name, buffType) end,
	}

	-- cooldown.<spell_name>.<cooldown_property>
	local COOLDOWN_PROPERTY_PATTERN = "^cooldown%.([%w_]+)%.([%w_]+)$"
	local COOLDOWN_PROPERTY = {
		-- TODO: ItemCooldown?
		["^remains$"] = function(simc, spellName) return format("SpellCooldown(%s)", spellName) end,
	}

	-- dot.<dot_name>.<dot_property>
	local DOT_PROPERTY_PATTERN = "^dot%.([%w_]+)%.([%w_]+)$"
	local DOT_HELPFUL = {
		["sacred_shield"] = true,
	}
	local function DotBuffName(dotName) return DOT_HELPFUL[dotName] and "Buff" or "Debuff" end
	local function DotBuffSuffix(dotName) return DOT_HELPFUL[dotName] and "buff" or "debuff" end
	local DOT_PROPERTY = {
		["^attack_power$"] = function(simc, dotName) return format("%sAttackPower(%s_%s)", DotBuffName(dotName), dotName, DotBuffSuffix(dotName)) end,
		["^crit_pct$"] = function(simc, dotName) return format("%sSpellCritChance(%s_%s)", DotBuffName(dotName), dotName, DotBuffSuffix(dotName)) end,
		["^duration$"] = function(simc, dotName) return format("%sDuration(%s_%s)", DotBuffName(dotName), dotName, DotBuffSuffix(dotName)) end,
		["^multiplier$"] = function(simc, dotName) return format("%sDamageMultiplier(%s_%s)", DotBuffName(dotName), dotName, DotBuffSuffix(dotName)) end,
		["^remains$"] = function(simc, dotName) return format("%sRemains(%s_%s)", DotBuffName(dotName), dotName, DotBuffSuffix(dotName)) end,
		["^spell_power$"] = function(simc, dotName) return format("%sSpellpower(%s_%s)", DotBuffName(dotName), dotName, DotBuffSuffix(dotName)) end,
		-- TODO: Should really implement BuffDamage() that's akin to Damage/LastEstimatedDamage.
		["^tick_dmg$"] = function(simc, dotName) return format("LastEstimatedDamage(%s_%s)", dotName, DotBuffSuffix(dotName)) end,
		["^ticking$"] = function(simc, dotName) return format("%sPresent(%s_%s)", DotBuffName(dotName), dotName, DotBuffSuffix(dotName)) end,
		["^ticks$"] = function(simc, dotName) return format("Ticks(%s_%s)", dotName, DotBuffSuffix(dotName)) end,
		["^ticks_remain$"] = function(simc, dotName) return format("TicksRemain(%s_%s)", dotName, DotBuffSuffix(dotName)) end,
	}

	-- talent.<talent_name>.<talent_property>
	local TALENT_PROPERTY_PATTERN = "^talent%.([%w_]+)%.([%w_]+)$"
	local TALENT_PROPERTY = {
		["^enabled$"] = function(simc, talentName) return format("TalentPoints(%s_talent)", talentName) end,
		["^disabled$"] = function(simc, talentName) return format("not TalentPoints(%s_talent)", talentName) end,
	}

	-- glyph.<glyph_name>.<glyph_property>
	local GLYPH_PROPERTY_PATTERN = "^glyph%.([%w_]+)%.([%w_]+)$"
	local GLYPH_PROPERTY = {
		["^enabled$"] = function(simc, glyphName) return format("Glyph(glyph_of_%s)", glyphName) end,
		["^disabled$"] = function(simc, glyphName) return format("not Glyph(glyph_of_%s)", glyphName) end,
	}

	-- trinket.<proc_type>.<stat>.<trinket_property>
	local TRINKET_PROPERTY_PATTERN = "^trinket%.([%w_]+)%.([%w_]+)%.([%w_]+)$"
	local TRINKET_PROPERTY = {
		["^cooldown_remains"] = function(simc, procType, statName) return ("{ ItemCooldown(Trinket0Slot) + ItemCooldown(Trinket1Slot) }") end,
		["^down$"] = function(simc, procType, statName) return format("BuffPresent(trinket_%s_%s_buff)", procType, statName) end,
		["^react$"] = function(simc, procType, statName)
				if strfind(procType, "stacking") then
					return format("BuffStacks(trinket_%s_%s_buff)", procType, statName)
				end
				return format("BuffPresent(trinket_%s_%s_buff)", procType, statName)
			end,
		["^remains$"] = function(simc, procType, statName) return format("BuffRemains(trinket_%s_%s_buff)", procType, statName) end,
		["^stack$"] = function(simc, procType, statName) return format("BuffStacks(trinket_%s_%s_buff)", procType, statName) end,
		["^up$"] = function(simc, procType, statName) return format("BuffPresent(trinket_%s_%s_buff)", procType, statName) end,
	}

	local GENERAL_PROPERTY = {
		["^active_enemies$"] = "Enemies()",
		["^adds$"] = "Enemies()",
		["^ptr$"] = "PTR()",
		["^time$"] = "TimeInCombat()",
		["^time_to_bloodlust$"] = "TimeToBloodlust()",
	}

	-- Runes
	local RUNE_PATTERN = {
		["^(blood)$"] = true,
		["^(death)$"] = true,
		["^(frost)$"] = true,
		["^(unholy)$"] = true,
		["^rune%.([%w_]+)$"] = true,
	--	["^rune%.([%w_]+)%.([%w_]+)$"] = true,
	}

	local TRANSLATED_TOKEN = {
		["^!$"] = "not",
		["^!=$"] = "!=",
		["^!~$"] = "!~",
		["^%%$"] = "/",
		["^%($"] = "{",
		["^%)$"] = "}",
		["^%*$"] = "*",
		["^%+$"] = "+",
		["^&$"] = "and",
		["^-$"] = "-",
		["^<$"] = "<",
		["^<=$"] = "<=",
		["^=$"] = "==",
		["^>$"] = ">",
		["^>=$"] = ">=",
		["^|$"] = "or",
		["^~$"] = "~",
	}

	function OvaleSimulationCraft:ParseExpression(action, expr, addActionSymbol)
		local translatedList = {}
		local tokenIterator = OvaleLexer.scan(expr, MATCHES)
		local tokenType, token = tokenIterator()
		while tokenType do
			local translated
			if tokenType == "iden" then
				-- Strip off "target." if present.
				local isTargetFound = strmatch(token, "^target%.([%w_%.]+)")
				if isTargetFound then
					token = isTargetFound
				end
				if not translated then
					-- Handle properties that have a special interpretation.
					for pattern, result in pairs(SPECIAL_PROPERTY) do
						if strmatch(token, pattern) then
							translated = (type(result) == "function") and result(self, action) or result
							break
						end
					end
				end
				if not translated then
					-- Bare action properties.
					if addActionSymbol then
						tinsert(self.symbols, action)
					end
					for pattern, result in pairs(ACTION_PROPERTY) do
						if strmatch(token, pattern) then
							translated = (type(result) == "function") and result(self, action) or result
							break
						end
					end
				end
				if not translated then
					-- Action properties for actions other than the one for this action line.
					local name, property = strmatch(token, ACTION_PROPERTY_PATTERN)
					if name then
						name = self:Name(name)
						tinsert(self.symbols, name)
						for pattern, result in pairs(ACTION_PROPERTY) do
							if strmatch(property, pattern) then
								translated = (type(result) == "function") and result(self, name) or result
								break
							end
						end
					end
				end
				if not translated then
					for pattern, result in pairs(CHARACTER_PROPERTY) do
						if strmatch(token, pattern) then
							translated = (type(result) == "function") and result(self, pattern, token) or result
							break
						end
					end
				end
				if not translated then
					local name, property = strmatch(token, AURA_PROPERTY_PATTERN)
					if name then
						name = self:Name(name)
						tinsert(self.symbols, name)
						for pattern, result in pairs(AURA_PROPERTY) do
							if strmatch(property, pattern) then
								translated = (type(result) == "function") and result(self, name) or result
								break
							end
						end
					end
				end
				if not translated then
					local buffType, name, property = strmatch(token, BUFF_PROPERTY_PATTERN)
					if buffType then
						if buffType == "debuff" then
							-- Debuffs default to checking on the target.
							isTargetFound = true
						end
						name = self:Name(name)
						tinsert(self.symbols, format("%s_%s", name, buffType))
						for pattern, result in pairs(BUFF_PROPERTY) do
							if strmatch(property, pattern) then
								translated = (type(result) == "function") and result(self, buffType, name) or result
								break
							end
						end
					end
				end
				if not translated then
					local name, property = strmatch(token, TOTEM_PROPERTY_PATTERN)
					if name then
						name = self:Name(name)
						if TOTEM_TYPE[name] then
							tinsert(self.symbols, name)
						end
						for pattern, result in pairs(TOTEM_PROPERTY) do
							if strmatch(property, pattern) then
								translated = (type(result) == "function") and result(self, name) or result
								break
							end
						end
					end
				end
				if not translated then
					local name, property = strmatch(token, COOLDOWN_PROPERTY_PATTERN)
					if name then
						name = self:Name(name)
						tinsert(self.symbols, name)
						local remains = false
						for pattern, result in pairs(COOLDOWN_PROPERTY) do
							if strmatch(property, pattern) then
								translated = (type(result) == "function") and result(self, name) or result
								break
							end
						end
						-- XXX
						if strmatch(token, "^cooldown%.icy_veins%.remains$") then
							translated = "IcyVeinsCooldownRemains()"
							remains = true
						end
						if strmatch(property, "^remains$") then
							remains = true
						end
						if remains then
							tokenType, token = tokenIterator()
							if tokenType then
								if token == "=" then
									translated = format("not %s >", translated)
								else
									local nextTranslated
									for pattern, result in pairs(TRANSLATED_TOKEN) do
										if strmatch(token, pattern) then
											nextTranslated = (type(result) == "function") and result(self, token) or result
											break
										end
									end
									if not nextTranslated then
										nextTranslated = format("FIXME_%s", token)
									end
									translated = format("%s %s", translated, nextTranslated)
								end
							end
						end
					end
				end
				if not translated then
					local name, property = strmatch(token, TALENT_PROPERTY_PATTERN)
					if name then
						tinsert(self.symbols, format("%s_talent", name))
						for pattern, result in pairs(TALENT_PROPERTY) do
							if strmatch(property, pattern) then
								translated = (type(result) == "function") and result(self, name) or result
								break
							end
						end
					end
				end
				if not translated then
					local name, property = strmatch(token, GLYPH_PROPERTY_PATTERN)
					if name then
						tinsert(self.symbols, format("glyph_of_%s", name))
						for pattern, result in pairs(GLYPH_PROPERTY) do
							if strmatch(property, pattern) then
								translated = (type(result) == "function") and result(self, name) or result
								break
							end
						end
					end
				end
				if not translated then
					local name, property = strmatch(token, DOT_PROPERTY_PATTERN)
					if name then
						-- DoTs default to checking on the target.
						isTargetFound = true
						name = self:Name(name)
						tinsert(self.symbols, format("%s_%s", name, DotBuffSuffix(name)))
						for pattern, result in pairs(DOT_PROPERTY) do
							if strmatch(property, pattern) then
								translated = (type(result) == "function") and result(self, name) or result
								break
							end
						end
					end
				end
				if not translated then
					local procType, name, property = strmatch(token, TRINKET_PROPERTY_PATTERN)
					if procType then
						tinsert(self.symbols, format("trinket_%s_%s_buff", procType, name))
						for pattern, result in pairs(TRINKET_PROPERTY) do
							if strmatch(property, pattern) then
								translated = (type(result) == "function") and result(self, procType, name) or result
								break
							end
						end
					end
				end
				if not translated then
					-- set_bonus.<set_name>_<N>pc_<role>
					local name, count, role = strmatch(token, "^set_bonus%.(%w+)_(%d+)pc_(%w+)$")
					if name and count and role then
						local tierLevel = strmatch(name, "^tier(%d+)")
						if tierLevel then
							name = format("T%s", tierLevel)
						end
						translated = format("ArmorSetBonus(%s_%s %s)", name, role, count)
					end
				end
				if not translated then
					for pattern, result in pairs(GENERAL_PROPERTY) do
						if strmatch(token, pattern) then
							translated = (type(result) == "function") and result(self, action) or result
							break
						end
					end
				end
				if not translated then
					for pattern in pairs(RUNE_PATTERN) do
						local rune = strmatch(token, pattern)
						if rune then
							-- Look ahead at the next token to see which rune condition is needed.
							local _, comparatorToken = tokenIterator()
							if comparatorToken == "=" then
								local operandTokenType, operandToken = tokenIterator()
								if operandTokenType == "number" then
									local number = tonumber(operandToken)
									local maxRunes = (rune == "death") and 6 or 2
									if number == 0 then
										translated = format("Rune(%s) < 1", rune)
									elseif number == maxRunes then
										translated = format("Rune(%s) >= %d", rune, number)
									else -- if 0 < number and number < maxRunes then
										translated = format("{ Rune(%s) >= %d and Rune(%s) < %d }", rune, number, rune, number + 1)
									end
								else
									translated = format("RuneCount(%s) == %s", rune, operandToken)
								end
							elseif comparatorToken == "!=" then
								local operandTokenType, operandToken = tokenIterator()
								if operandTokenType == "number" then
									local number = tonumber(operandToken)
									local maxRunes = (rune == "death") and 6 or 2
									if number == 0 then
										translated = format("Rune(%s) >= 1", rune)
									elseif number == maxRunes then
										translated = format("Rune(%s) < %d", rune, maxRunes)
									else -- if 0 < number and number < maxRunes then
										translated = format("{ Rune(%s) < %d or Rune(%s) >= %d }", rune, number, rune, number + 1)
									end
								else
									translated = format("not RuneCount(%s) == %s", rune, operandToken)
								end
							elseif comparatorToken == "<=" or comparatorToken == ">=" then
								translated = format("Rune(%s) %s", rune, comparatorToken)
							elseif comparatorToken == "<" or comparatorToken == ">" then
								local operandTokenType, operandToken = tokenIterator()
								if operandTokenType == "number" then
									translated = format("Rune(%s) %s= %d", rune, comparatorToken, 1 + tonumber(operandToken))
								else
									translated = format("Rune(%s) %s= 1 + %s", rune, comparatorToken, operandToken)
								end
							end
							break
						end
					end
				end
				if not translated then
					translated = format("FIXME_%s", token)
				end
				if isTargetFound then
					translated = "target." .. translated
				end
			elseif tokenType == "number" then
				translated = token
			end
			if not translated then
				for pattern, result in pairs(TRANSLATED_TOKEN) do
					if strmatch(token, pattern) then
						translated = (type(result) == "function") and result(self, token) or result
						break
					end
				end
			end
			if not translated then
				translated = format("FIXME_%s", token)
			end
			tinsert(translatedList, translated)
			tokenType, token = tokenIterator()
		end
		local translation = tconcat(translatedList, " ")
		return translation
	end
end
--</public-static-methods>