Quantcast
-- SageGearLevelTooltip.lua
--[[
/console reloadui
]]

----------------------------------------------------------------------------------------------------
local Class = sage and sage.class and sage.class.Class or error("Need access to sage.class.Class")
local Item = sage.item and sage.item.Item or error("Need access to sage.item.Item")
local Reforgings = sage.reforge and sage.reforge.Reforgings or error("Need access to sage.item.Item")

local function newClass(arg)
	return Class:NewClass(arg)
end

----------------------------------------------------------------------------------------------------
local CachingEvaluator do
	local class = newClass{name="CachingEvaluator"}

	function class:_Initialize(instance, delegate)	-- self is class
		self.super:_Initialize(instance)
		instance.delegate = delegate
		instance.cache = {}
		instance.cacheHitCount = 0
		instance.cacheMissCount = 0
	end

	function class:Name()
		return self.delegate:Name()
	end

	function class:Evaluate(itemLink)
		-- TODO: normalize link first?
		local cached = self.cache[itemLink]
		if cached then
			self.cacheHitCount = self.cacheHitCount + 1
			return unpack(cached)
		end

		local left, leftLabel, right, rightLabel = self.delegate:Evaluate(itemLink)
		if left and leftLabel then
			self.cache[itemLink] = { left, leftLabel, right, rightLabel }
			self.cacheMissCount = self.cacheMissCount + 1
			return left, leftLabel, right, rightLabel
		end
	end

	CachingEvaluator = class
end

----------------------------------------------------------------------------------------------------
--[[
		A base class for Evaluators which *might* need to be cached.
		Just adds a :Cache method which wraps self with a CachingEvaluator
		]]
local CachableEvaluator do
	local class = newClass{name="CachableEvaluator"}

	function class:Cache()
		return CachingEvaluator:New(self)
	end

	CachableEvaluator = class
end

----------------------------------------------------------------------------------------------------
local ItemLevelIdCommenter do
	local class = newClass{name="ItemLevelIdCommenter"}

	function class:Comment(itemLink, tooltip)
		if not itemLink then return end
		local _,_,_,level = GetItemInfo(itemLink)
		local itemId = itemLink:match("item:(%d+):")
		itemId = itemId
		    and tonumber(itemId)
		    or ">>" .. itemLink:gsub("|", "!")
		tooltip:AddDoubleLine(
				"iLevel " .. (level or '??'),
				"id " .. itemId,
				class.r, class.g, class.b,
				1, 1, 1)
	end

	ItemLevelIdCommenter = class
end

----------------------------------------------------------------------------------------------------
local ItemIdEvaluator do
	local class = newClass{name="ItemIdEvaluator"}
	local label = {label = "id", r=0.5, g=1, b=0.5}

	function class:Evaluate(itemLink)
		if not itemLink then return end
		local itemId = itemLink:match("item:(%d+):")
		itemId = itemId
		    and tonumber(itemId)
		    or ">>" .. itemLink:gsub("|", "!")
--		local itemId = Item:New(itemLink):ItemString()
		return itemId, label
	end

	ItemIdEvaluator = class
end

----------------------------------------------------------------------------------------------------
local ITEM_MOD_NAME_MAP = {
	EMPTY_SOCKET_RED="RedSocket",
	EMPTY_SOCKET_BLUE="BlueSocket",
	EMPTY_SOCKET_YELLOW="YellowSocket",
	EMPTY_SOCKET_PRISMATIC="PrismaticSocket",
	ITEM_MOD_FIRE_RESISTANCE_SHORT="FireResist",
	ITEM_MOD_FROST_RESISTANCE_SHORT="FrostResist",

	ITEM_MOD_CRIT_RATING_SHORT="Crit",
	ITEM_MOD_CRIT_RANGED_RATING_SHORT="CritRanged",
	ITEM_MOD_DODGE_RATING_SHORT="Dodge",
	ITEM_MOD_EXPERTISE_RATING_SHORT="Expertise",
	ITEM_MOD_HASTE_RATING_SHORT="Haste",
	ITEM_MOD_HIT_RATING_SHORT="Hit",
	ITEM_MOD_MASTERY_RATING_SHORT="Mastery",
	ITEM_MOD_PARRY_RATING_SHORT="Parry",
	ITEM_MOD_RESILIENCE_RATING_SHORT="Resilience",

	ITEM_MOD_ATTACK_POWER_SHORT="AP",
	ITEM_MOD_DAMAGE_PER_SECOND_SHORT="DPS",
	ITEM_MOD_AGILITY_SHORT="Agility",
	ITEM_MOD_INTELLECT_SHORT="Intellect",
	ITEM_MOD_SPELL_POWER_SHORT="SP",
	ITEM_MOD_SPIRIT_SHORT="Spirit",
	ITEM_MOD_STAMINA_SHORT="Stamina",
	ITEM_MOD_STRENGTH_SHORT="Strength",

	RESISTANCE0_NAME="Armor",
	RESISTANCE2_NAME="FireResist",
	RESISTANCE4_NAME="FrostResist*",		-- duplicate?
}

----------------------------------------------------------------------------------------------------
local SimplePawnEvaluator do

	local class = newClass{name="SimplePawnEvaluator", super=CachableEvaluator}

	function class:_Initialize(instance, label, coefficients)		--self is class
		self.super:_Initialize(instance)
		instance.coefficients = coefficients
		instance.label = label
	end

	function class:Evaluate(itemLink)
		if not itemLink then return end
		local stats = self:_GetItemStats(itemLink)
		local total = 0
		local missedStat, missedStatLabel = nil, nil
		print("stat eval")
		for k,v in pairs(stats) do
			local stat = ITEM_MOD_NAME_MAP[k]
			if not stat then
				missedStat, missedStatLabel = v, {label=k, r=class.r, g=class.g, b=class.b}
			end

			total = total + v * (self.coefficients[stat] or 0)
		end
		if total > 0 then
			return math.floor(0.5 + total), self.label, missedStat, missedStatLabel
		elseif missedStat then
			return missedStat, missedStatLabel
		end
	end

	function class:_GetItemStats(itemLink)		-- broken out to be overridable
		return GetItemStats(itemLink)
	end

	SimplePawnEvaluator = class
end

----------------------------------------------------------------------------------------------------
local AsReforgedPawnEvaluator do
	local class = newClass{name="AsReforgedPawnEvaluator", super=SimplePawnEvaluator}

	local reforgings = Reforgings:New()

	function class:_GetItemStats(itemLink)
		local reforging = reforgings:Find(itemLink)
		return Item:New(itemLink):GetItemStats()
	end

	AsReforgedPawnEvaluator = class
end

----------------------------------------------------------------------------------------------------
local BestReforgedEvaluator do
	local class = newClass{name="BestReforgedEvaluator", super=CachableEvaluator}

	local reforgings = Reforgings:New()
	local reforgingLabel = {label = "Reforge", r=0.6, g=0.6, b=0.6}

	function class:_Initialize(instance, label, coefficients)		--self is class
		self.super:_Initialize(instance)
		instance.coefficients = coefficients
		instance.label = label

		-- handle sockets
		local bestStatValue = 0
		for i,stat in ipairs({"Stamina", "Strength", "Agility", "Intellect", "Spirit"}) do
  		  bestStatValue = math.max(bestStatValue, instance.coefficients[stat] or 0)
		end
		local socketValue = bestStatValue * 40    -- cata blue gems
		instance.coefficients.RedSocket = math.max(socketValue, instance.coefficients.RedSocket or 0)
		instance.coefficients.BlueSocket = math.max(socketValue, instance.coefficients.BlueSocket or 0)
		instance.coefficients.YellowSocket = math.max(socketValue, instance.coefficients.YellowSocket or 0)
	end

	function class:_GetItemStats(itemLink)
		local reforging = reforgings:Find(itemLink)
		return Item:New(itemLink):GetItemStats()
	end

	function class:Name()
		return self.label and self.label.label or "<Unknown>"
	end

	function class:Evaluate(itemLink)
		if not itemLink then return end
		itemLink = Item:New(itemLink):WithoutMods():ItemString()
		local stats = GetItemStats(itemLink)	-- baseline w/o reforging
		local rightValue, rightLabel = nil, nil

		-- if not high enough for reforging, show base stats
		local reforging = self:_ItemHighEnoughForForging(itemLink)
				and self:_FindOptimalReforging(stats)
		if reforging then
			reforging:AdjustStats(stats)
			rightValue, rightLabel = reforging:Describe(), reforgingLabel
		end

		local evaluation = self:_EvalStats(stats)
		if evaluation and (evaluation > 0) then
			return evaluation, self.label, rightValue, rightLabel
		end
	end

	function class:_ItemHighEnoughForForging(itemLink)
		_,_,_,itemLevel = GetItemInfo(itemLink)
		return itemLevel >= 200
	end

	function class:_FindOptimalReforging(stats)
		local maxDelta, maxReforging = 0, nil
		for index,reforging in pairs(reforgings:All()) do
			local from, to = reforging:GetShortNames()
			local c0 = self.coefficients[ITEM_MOD_NAME_MAP[from]] or 0
			local c1 = self.coefficients[ITEM_MOD_NAME_MAP[to]] or 0
			if stats[from] and (not stats[to]) and (c1 > c0) then
				local delta = stats[from] * (c1 - c0)		--don't bother to scale by 40% here
				if delta > maxDelta then
					maxDelta, maxReforging = delta, reforging
				end
			end
		end
		return maxReforging
	end

	function class:_EvalStats(stats)
		local total = 0
		for k,v in pairs(stats) do
			local stat = ITEM_MOD_NAME_MAP[k]
			total = total + v * (self.coefficients[stat] or 0)
		end
		return math.floor(0.5 + total)
	end

	function class:_GetItemStats(itemLink)		-- broken out to be overridable
		return GetItemStats(itemLink)
	end

	BestReforgedEvaluator = class
end

----------------------------------------------------------------------------------------------------
local EvaluatorToLineAdderAdaptor do
	local class = newClass{name="EvaluatorToLineAdderAdaptor"}

	function class:_Initialize(instance, evaluator)
		instance.evaluator = evaluator
	end

	-- Adds lines for the given item
	-- @param itemLink a link for the item
	-- @param tooltip a callback supporting :AddLine and :AddDoubleLine with same args as a GameTooltip
	--        NOTE: tooltip is not guaranteed to be an actual tooltip, or support other methods
	function class:Comment(itemLink, tooltip)
		local left, leftLabel, right, rightLabel = self.evaluator:Evaluate(itemLink)
		local leftText = left and leftLabel and (leftLabel.label .. ": " .. left)
		local rightText = right and rightLabel and (rightLabel.label .. ": " .. right)
		if leftText and rightText then
			tooltip:AddDoubleLine(leftText, rightText,
					leftLabel.r, leftLabel.g, leftLabel.b,
					rightLabel.r, rightLabel.g, rightLabel.b)
		elseif leftText then
			tooltip:AddLine(leftLabel.label .. ": " .. left, leftLabel.r, leftLabel.g, leftLabel.b)
		end
	end

	EvaluatorToLineAdderAdaptor = class
end

--[[
/run function qz(...) local t,i,v={...}; if (#t)==0 then return end i=math.random(#t); v=table.remove(t, i); return v, qz(unpack(t)) end
  ]]
----------------------------------------------------------------------------------------------------
local SageGearLevelTooltipUpdater do
	local class = newClass{name="TooltipUpdater"}

	-- class function
	function class:_Initialize(instance, methodsToHook)
		-- no call to super needed
		instance.evals = {}
		instance.commenters = {}
		return instance:_HookFrameMethods(methodsToHook)
	end

	-- Updates the tooltip with all the comments from the item commenters
	-- @param tooltip A "tooltip" support :AddLine and :AddDoubleLine *only*
	function class:UpdateTooltip(tooltip)
		local _,itemLink = tooltip:GetItem()
		local tooltipAdapter = {
			mustShow = false,
			AddLine = function(adapter, ...) adapter.mustShow = true; tooltip:AddLine(...) end,
			AddDoubleLine = function(adapter, ...) adapter.mustShow = true; tooltip:AddDoubleLine(...) end
		}
		for _,commenter in ipairs(self.commenters) do
			commenter:Comment(itemLink, tooltipAdapter)
		end
		if tooltipAdapter.mustShow then
			tooltip:Show()
		end
	end

	function class:_HookFrameMethods()
		self:Print("SageGearLevelTooltip loaded.")
		return self
	end

	function class:HookTooltipScript(tooltipName)
		local tooltip = _G[tooltipName] or error("Tooltip not found: " .. tooltipName)
		local update =
				function(tooltip, ...)
						self:UpdateTooltip(tooltip)
				end
		tooltip:HookScript("OnTooltipSetItem", update)
		return self
	end

	function class:AddSelfEvaluator(className, e)
		local playerClassLocal, playerClass = UnitClass('player')
		if className == playerClass then
			self:Debug("Installing evaluator for " .. playerClassLocal .. ": " .. (e:Name()))
			return self:AddEvaluator(e)
		end
		return self
	end

	function class:AddEvaluator(e)
		return self:AddItemCommenter(EvaluatorToLineAdderAdaptor:New(e))
	end

	-- Adds an item commenter to this tooltip annotator
	-- @param commenter an ItemCommenter to give comment to this tooltip
	function class:AddItemCommenter(commenter)
		local commenters = self.commenters
		commenters[#commenters + 1] = commenter
		return self
	end

	SageGearLevelTooltipUpdater = class
end

----------------------------------------------------------------------------------------------------
local tooltipUpdater = SageGearLevelTooltipUpdater:New(methodsToHook)
    :HookTooltipScript("GameTooltip")
		:HookTooltipScript("ItemRefTooltip")
		:HookTooltipScript("ShoppingTooltip1")
		:HookTooltipScript("ShoppingTooltip2")
		:AddItemCommenter(ItemLevelIdCommenter:New())

local DEFAULT_COLOR = {r=0.5, g=0.5, b=0.5}
sage.gear.AllStats(function(spec, class, stats, color)
  color = color or DEFAULT_COLOR
  print(spec, class, stats, color)
  local specInfo = {label = spec, r=color.r, g=color.g, b=color.b}
  tooltipUpdater:AddSelfEvaluator(
      class,
      BestReforgedEvaluator:New(specInfo, stats):Cache())
end)

--[[
tooltipUpdater
		--DK
		:AddSelfEvaluator("DEATHKNIGHT", BestReforgedEvaluator:New({label = "Blood DK", r=1.0, g=0.2, b=0.4},
						{MeleeDPS=500, Mastery=100, Stamina=100, Agility=69, Dodge=50, Parry=43, Expertise=38,
								Strength=31, Armor=18, Haste=16, Hit=16, AdditionalArmor=11})
						:Cache())
		:AddSelfEvaluator("DEATHKNIGHT", BestReforgedEvaluator:New({label = "Frost DK", r=0.2, g=0.4, b=1.0},
						{Strength=100, MeleeDPS=100, Hit=43, Expertise=41, Haste=37, Mastery=35, AP=32, Crit=26})
						:Cache())
		:AddSelfEvaluator("DEATHKNIGHT", BestReforgedEvaluator:New({label = "Unholy DK", r=0.6, g=0.6, b=0.6},
						{Strength=100, MeleeDPS=100, Hit=43, Expertise=41, Haste=37, Mastery=35, AP=32, Crit=26})
						:Cache())

		--Druid
		:AddSelfEvaluator("DRUID", BestReforgedEvaluator:New({label = "Moonkin", r=0.3, g=0.3, b=1.0},
						{Hit=100, Master=100, SP=66, Haste=54, Crit=43, Intellect=22, Spirit=22})
						:Cache())
		:AddSelfEvaluator("DRUID", BestReforgedEvaluator:New({label = "Kitty", r=1.0, g=1.0, b=0.5},
						{DPS=151, Agility=100, Strength=78, AP=37, Mastery=35, Haste=32, Expertise=29, Hit=28, Crit=28})
						:Cache())
		:AddSelfEvaluator("DRUID", BestReforgedEvaluator:New({label = "Bear", r=1.0, g=0.4, b=0.4},
            {Agility=100, Dodge=88, Armor=71, Mastery=48, Expertise=30, Crit=28, Hi=15, Stamina=13,
                AP=12, Haste=4})
--						{Stamina=100, Armor=75, Agility=48, Dodge=41, AdditionalArmor=21, Mastery=16, Crit=13,
--								Strength=10, Expertise=10, Health=7, FeralAP=5, AP=5, Hit=5, Haste=1})
						:Cache())
		:AddSelfEvaluator("DRUID", BestReforgedEvaluator:New({label = "Tree", r=0.3, g=1.0, b=0.3},
						{SP=100, Mastery=100, Haste=57, Intellect=51, Spirit=32, Crit=11})
						:Cache())

		-- Hunter
		:AddSelfEvaluator("HUNTER", BestReforgedEvaluator:New({label = "BM Hunter", r=0.7, g=0.3, b=1.0},
				{RangedDPS=213, Hit=100, Mastery=100, Agility=58, Crit=40, Intellect=37, Haste=21})
				:Cache())
		:AddSelfEvaluator("HUNTER", BestReforgedEvaluator:New({label = "MM Hunter", r=1.0, g=0.7, b=0.3},
				{RangedDPS=379, Hit=100, Mastery=100, Agility=74, Crit=57, Intellect=39, Haste=24})
				:Cache())
		:AddSelfEvaluator("HUNTER", BestReforgedEvaluator:New({label = "SV Hunter", r=0.3, g=1.0, b=0.7},
				{RangedDPS=100, Agility=100, Hit=80, Crit=60, Haste=40, Mastery=20})
				:Cache())
		:AddSelfEvaluator("HUNTER", BestReforgedEvaluator:New({label = "SV Hunter (hit-capped)", r=0.2, g=0.8, b=0.5},
				{RangedDPS=100, Agility=100, Crit=60, Haste=40, Mastery=20})
				:Cache())

		-- Mage
		:AddSelfEvaluator("MAGE", BestReforgedEvaluator:New({label = "Arcane Mage", r=1.0, g=1.0, b=1.0},
				{Hit=100, Mastery=100, Haste=54, SP=49, Crit=37, Intellect=34, Spirit=14})
				:Cache())
		:AddSelfEvaluator("MAGE", BestReforgedEvaluator:New({label = "Fire Mage", r=1.0, g=0.8, b=0.4},
				{Hit=100, Mastery=100, Haste=53, SP=46, Crit=43, Intellect=13})
				:Cache())
		:AddSelfEvaluator("MAGE", BestReforgedEvaluator:New({label = "Frost Mage", r=0.7, g=0.7, b=1.0},
				{Hit=100, Mastery=100, Haste=42, SP=39, Crit=19, Intellect=6})
				:Cache())

		-- Paladin
		:AddSelfEvaluator("PALADIN", BestReforgedEvaluator:New({label = "Holy Paladin", r=0.3, g=1.0, b=0.3},
				{Intellect=100, Mastery=100, SP=58, Crit=46, Haste=35})
				:Cache())
		:AddSelfEvaluator("PALADIN", BestReforgedEvaluator:New({label = "Prot Paladin", r=1.0, g=0.4, b=0.4},
		    {Parry=100, Dodge=100, Mastery=81, Stamina=49, Strength=28, Expertise=8, Hit=4})
--				{Mastery=100, Stamina=100, Agility=60, Expertise=59, Dodge=55, Parry=30, Strength=16, Armor=8})
				:Cache())
		:AddSelfEvaluator("PALADIN", BestReforgedEvaluator:New({label = "Ret Paladin", r=1.0, g=1.0, b=0.5},
				{MeleeDPS=470, Mastery=100, Hit=100, Strength=80, Expertise=66, Crit=40, Agility=32, Haste=30, SP=9})
				:Cache())

		--Priest
		:AddSelfEvaluator("PRIEST", BestReforgedEvaluator:New({label = "Disc Priest", r=0.5, g=0.5, b=1.0},
						{Intellect=100, Spirit=80, SP=19, Crit=11, Haste=10, Mastery=6})
						:Cache())
		:AddSelfEvaluator("PRIEST", BestReforgedEvaluator:New({label = "Holy Priest", r=0.3, g=1.0, b=0.3},
						{Intellect=100, Spirit=95, Haste=85, Mastery=80, Crit=30})
						:Cache())
		:AddSelfEvaluator("PRIEST", BestReforgedEvaluator:New({label = "Shadow Priest", r=0.5, g=0.5, b=0.5},
						{Intellect=100, SP=79, Haste=50, Crit=40, Mastery=38, Spirit=37, Hit=37})
						:Cache())

		--Rogue
		:AddSelfEvaluator("ROGUE", BestReforgedEvaluator:New({label = "Assassin", r=0.8, g=1.0, b=0.5},
						{MeleeDPS=100, Agility=100, Hit=67, Mastery=50, Haste=46, Expertise=42, Crit=35})
						:Cache())
		:AddSelfEvaluator("ROGUE", BestReforgedEvaluator:New({label = "Combat Rogue", r=1.0, g=0.8, b=0.5},
						{MeleeDPS=100, Agility=100, Hit=70, Expertise=59, Haste=56, Crit=35, Mastery=33})
						:Cache())
		:AddSelfEvaluator("ROGUE", BestReforgedEvaluator:New({label = "Subtlety", r=1.0, g=1.0, b=0.3},
						{MeleeDPS=100, Agility=100, Hit=40, Haste=37, Expertise=33, Crit=31, Mastery=20})
						:Cache())

		--Shaman
		:AddSelfEvaluator("SHAMAN", BestReforgedEvaluator:New({label = "Ele Shaman", r=0.5, g=0.5, b=1.0},
						{Mastery=100, Hit=100, SP=60, Haste=56, Crit=40, Intellect=11})
						:Cache())
		:AddSelfEvaluator("SHAMAN", BestReforgedEvaluator:New({label = "Enh Shaman", r=1.0, g=1.0, b=0.5},
						{MeleeDPS=124, Agility=100, Hit=60, Expertise=48, Mastery=44, Strength=42, AP=40,
								Intellect=36, SP=36, Crit=28, Haste=16})
						:Cache())
		:AddSelfEvaluator("SHAMAN", BestReforgedEvaluator:New({label = "Resto Shaman", r=0.3, g=1.0, b=0.3},
						{Intellect=100, SP=83, Spriti=75, Haste=67, Crit=58, Mastery=42, Stamina=8})
						:Cache())

		--Warlock
		:AddSelfEvaluator("WARLOCK", BestReforgedEvaluator:New({label = "Aff Warlock", r=1.0, g=0.2, b=1.0},
				{Hit=100, Master=100, SP=72, Haste=61, Crit=38, Spirit=34, Intellect=15})
				:Cache())
		:AddSelfEvaluator("WARLOCK", BestReforgedEvaluator:New({label = "Dem Warlock", r=0.8, g=0.2, b=1.0},
				{Hit=100, Master=100, Haste=50, SP=45, Crit=31, Spirit=29, Intellect=13})
				:Cache())
		:AddSelfEvaluator("WARLOCK", BestReforgedEvaluator:New({label = "Destro Warlock", r=1.0, g=0.6, b=0.0},
				{Mastery=100, Hit=100, SP=47, Haste=46, Spirit=26, Crit=16, Intellect=13})
				:Cache())

		-- Warrior
		:AddSelfEvaluator("WARRIOR", BestReforgedEvaluator:New({label = "Arms Warrior", r=0.8, g=0.8, b=1.0},
				{Mastery=100, Strength=100, Hit=90, Expertise=85, Crit=80, Agility=65, Haste=50, Armor=1})
				:Cache())
		:AddSelfEvaluator("WARRIOR", BestReforgedEvaluator:New({label = "Fury Warrior", r=0.8, g=0.8, b=0.2},
				{Mastery=100, Expertise=100, Strength=82, Crit=66, Agility=53, Hit=48, Haste=36, Armor=5})
				:Cache())
		:AddSelfEvaluator("WARRIOR", BestReforgedEvaluator:New({label = "Prot Warrior", r=1.0, g=0.4, b=0.4},
				{Stamina=100, Mastery=100, Dodge=90, Parry=67, Agility=67, Strength=48, Expertise=19, Hit=10, Crit=7, Armor=6, Haste=1})
				:Cache())
]]