-- Pawn by Vger-Azjol-Nerub
-- www.vgermods.com
-- © 2006-2019 Green Eclipse.  This mod is released under the Creative Commons Attribution-NonCommercial-NoDerivs 3.0 license.
-- See Readme.htm for more information.

-- 
-- Main non-UI code
------------------------------------------------------------

PawnVersion = 2.0311

-- Pawn requires this version of VgerCore:
local PawnVgerCoreVersionRequired = 1.11

-- Floating point math
local PawnEpsilon = 0.0000000001

-- Set to true once initialization completes
local PawnIsInitialized

-- Name of our private tooltip defined in PawnUI.xml
PawnPrivateTooltipName = "PawnPrivateTooltip1"

-- Caching
-- 	An item in the cache has the following properties: Name, NumLines, UnknownLines, Stats, SocketBonusStats, UnenchantedStats, UnenchantedSocketBonusStats, Values, Link, PrettyLink, Level, Rarity, ID, InvType, Texture, ShouldUseGems
--	(See PawnGetEmptyCachedItem.)
--	An entry in the Values table is an ordered array in the following format:
--	{ ScaleName, Value, UnenchantedValue }
local PawnItemCache = nil
local PawnItemCacheMaxSize = 200 -- thanks to bag arrows, this should be greater than the number of possible inventory slots

local PawnScaleTotals = { }


-- Best gem data
--	Best gem data is broken down first by scale name, then by socket, then by minimum item level.  "Gem info" is yet another table.
-- PawnScaleBestGems["Scale name"] = {
--	["PrismaticSocket"] = { [0] = { gem info }, },
--	["PrismaticSocketValue"] = { [0] = 234.56, } }
PawnScaleBestGems = { }

PawnPlayerFullName = nil

-- Formatting
local PawnEnchantedAnnotationFormat = nil
local PawnUnenchantedAnnotationFormat = nil
local PawnNoValueAnnotationFormat = nil

-- Plugin scale providers

-- PawnScaleProviders["Wowhead"] = { ["Name"] = "Wowhead scales", ["Function"] = <function> }
PawnScaleProviders = { }
local PawnScaleProvidersInitialized = nil

-- "Constants"
local PawnCurrentScaleVersion = 1

local PawnTooltipAnnotation = " " .. PawnDiamondTexture -- diamond texture defined in Core.lua

local PawnScaleColorDarkFactor = 0.75 -- the unenchanted color is 75% of the enchanted color

PawnButtonPositionHidden = 0
PawnButtonPositionLeft = 1
PawnButtonPositionRight = 2

PawnImportScaleResultSuccess = 1
PawnImportScaleResultAlreadyExists = 2
PawnImportScaleResultTagError = 3

PawnIgnoreStatValue = -1000000
PawnBigUpgradeThreshold = 100 -- = 10000% upgrade: don't display upgrade numbers that large

-- Data used by PawnGetSlotsForItemType.
local PawnItemEquipLocToSlot1 = 
{
	INVTYPE_HEAD = 1,
	INVTYPE_NECK = 2,
	INVTYPE_SHOULDER = 3,
	INVTYPE_BODY = 4,
	INVTYPE_CHEST = 5,
	INVTYPE_ROBE = 5,
	INVTYPE_WAIST = 6,
	INVTYPE_LEGS = 7,
	INVTYPE_FEET = 8,
	INVTYPE_WRIST = 9,
	INVTYPE_HAND = 10,
	INVTYPE_FINGER = 11,
	INVTYPE_TRINKET = 13,
	INVTYPE_CLOAK = 15,
	INVTYPE_WEAPON = 16,
	INVTYPE_SHIELD = 17,
	INVTYPE_2HWEAPON = 16,
	INVTYPE_WEAPONMAINHAND = 16,
	INVTYPE_RANGED = 16,
	INVTYPE_RANGEDRIGHT = 16,
	INVTYPE_WEAPONOFFHAND = 17,
	INVTYPE_HOLDABLE = 17,
	INVTYPE_TABARD = 19,
}
local PawnItemEquipLocToSlot2 = 
{
	INVTYPE_FINGER = 12,
	INVTYPE_TRINKET = 14,
	INVTYPE_WEAPON = 17,
}

-- Don't taint the global variable "_".
local _


------------------------------------------------------------


-- Called when an event that Pawn cares about is fired.
function PawnOnEvent(Event, arg1, arg2, ...)
	if Event == "UNIT_INVENTORY_CHANGED" and arg1 == "player" then
		PawnOnInventoryChanged()
	elseif Event == "ITEM_LOCKED" then
		PawnOnItemLocked(arg1, arg2)
		PawnOnInventoryChanged()
	elseif Event == "ADDON_LOADED" then
		PawnOnAddonLoaded(arg1)
	elseif Event == "PLAYER_SPECIALIZATION_CHANGED" and arg1 == "player" then
		PawnOnSpecChanged()
	elseif Event == "ARTIFACT_UPDATE" then
		PawnOnArtifactUpdated(arg1)
	elseif Event == "PLAYER_LOGIN" then
		PawnInitialize()
	elseif Event == "PLAYER_LOGOUT" then
		PawnOnLogout()
	end 
end

-- Initializes Pawn after all saved variables have been loaded.
function PawnInitialize()
	-- This only needs to happen once.  If it's ever triggered again for any reason, bail out now.
	if PawnIsInitialized then return end

	local _

	-- Check the current version of VgerCore.
	if (not VgerCore) or (not VgerCore.Version) or (VgerCore.Version < PawnVgerCoreVersionRequired) then
		if DEFAULT_CHAT_FRAME then DEFAULT_CHAT_FRAME:AddMessage("|cfffe8460" .. PawnLocal.NeedNewerVgerCoreMessage) end
		message(PawnLocal.NeedNewerVgerCoreMessage)
		return
	end

	-- Check the user's current locale, and show a message if it isn't the right one for this version of Pawn.
	local CurrentLocale = GetLocale()
	local CurrentLocaleIsSupported
	local SupportedLocale
	local LanguageList = PawnLocalizedLanguages
	for _, SupportedLocale in pairs(LanguageList) do
		if CurrentLocale == SupportedLocale then
			CurrentLocaleIsSupported = true
			break
		end
	end
	if not CurrentLocaleIsSupported then
		-- No need to translate this string...
		local WrongLocaleMessage = "Sorry, this version of Pawn is for English, French, German, Italian, Korean, Portuguese, Russian, Spanish, Simplified Chinese, and Traditional Chinese only."
		VgerCore.Message(VgerCore.Color.Salmon .. WrongLocaleMessage)
		message(WrongLocaleMessage)
	end

	-- Set up slash commands
	SLASH_PAWN1 = "/pawn"
	SlashCmdList["PAWN"] = PawnCommand
	
	-- Set any unset options to their default values.  If the user is a new Pawn user, all options
	-- will be set to default values.  If upgrading, only missing options will be set to default values.
	PawnInitializeOptions()

	-- Check for and set default keybindings.
	PawnSetDefaultKeybindings()

	-- Adjust UI elements.
	PawnUI_InventoryPawnButton_Move()
	
	-- Hook into events.
	
	-- UI functions
	hooksecurefunc("DeleteCursorItem",
		function()
			PawnOnItemLost(PawnLastCursorItemLink)
		end)
	hooksecurefunc("UseContainerItem",
		function(BagID, Slot)
			if MerchantFrame:IsShown() then
				if ItemLink then PawnOnItemLost(GetContainerItemLink(BagID, Slot)) end
			end
		end)
	hooksecurefunc("PickupMerchantItem",
		function(Index)
			if Index == 0 then PawnOnItemLost(PawnLastCursorItemLink) end
		end)
	
	-- Main game tooltip
	hooksecurefunc(GameTooltip, "SetAuctionItem", function(self, ...) PawnUpdateTooltip("GameTooltip", "SetAuctionItem", ...) end)
	hooksecurefunc(GameTooltip, "SetAuctionSellItem", function(self, ...) PawnUpdateTooltip("GameTooltip", "SetAuctionSellItem", ...) end)
	hooksecurefunc(GameTooltip, "SetBagItem", function(self, ...) PawnUpdateTooltip("GameTooltip", "SetBagItem", ...) end)
	hooksecurefunc(GameTooltip, "SetBuybackItem", function(self, ...) PawnUpdateTooltip("GameTooltip", "SetBuybackItem", ...) end)
	if SetExistingSocketGem then
		-- Gems don't exist in WoW Classic.
		hooksecurefunc(GameTooltip, "SetExistingSocketGem", function(self, ...) PawnUpdateTooltip("GameTooltip", "SetExistingSocketGem", ...) end)
	end
	if SetGuildBankItem then
		-- Guild banks don't exist in WoW Classic.
		hooksecurefunc(GameTooltip, "SetGuildBankItem", function(self, ...) PawnUpdateTooltip("GameTooltip", "SetGuildBankItem", ...) end)
	end
	if SetHeirloomByItemID then
		-- ...and neither do heirlooms.
		hooksecurefunc(GameTooltip, "SetHeirloomByItemID", function(self, ...) PawnUpdateTooltip("GameTooltip", "SetHeirloomByItemID", ...) end)
	end
	hooksecurefunc(GameTooltip, "SetHyperlink", function(self, ...) PawnUpdateTooltip("GameTooltip", "SetHyperlink", ...) end)
	hooksecurefunc(GameTooltip, "SetInboxItem", function(self, ...) PawnUpdateTooltip("GameTooltip", "SetInboxItem", ...) end)
	hooksecurefunc(GameTooltip, "SetInventoryItem", function(self, ...) PawnUpdateTooltip("GameTooltip", "SetInventoryItem", ...) end)
	hooksecurefunc(GameTooltip, "SetItemByID", function(self, ...) PawnUpdateTooltip("GameTooltip", "SetItemByID", ...) end)
	hooksecurefunc(GameTooltip, "SetLootItem", function(self, ...) PawnUpdateTooltip("GameTooltip", "SetLootItem", ...) end)
	hooksecurefunc(GameTooltip, "SetLootRollItem", function(self, ...) PawnUpdateTooltip("GameTooltip", "SetLootRollItem", ...) end)
	hooksecurefunc(GameTooltip, "SetMerchantItem", function(self, ...) PawnUpdateTooltip("GameTooltip", "SetMerchantItem", ...) end)
	hooksecurefunc(GameTooltip, "SetQuestItem",
		function(self, ...)
			-- BUG IN WoW 6.2: This item will come through with an item ID of 0 and we'll fail to get stats from it normally.
			-- Special thanks to Phanx for suggesting this workaround!
			local ItemLink = GetQuestItemLink(...)
			if ItemLink then
				PawnUpdateTooltip("GameTooltip", "SetHyperlink", ItemLink)
			else
				PawnUpdateTooltip("GameTooltip", "SetQuestItem", ...)
			end
		end)
	hooksecurefunc(GameTooltip, "SetQuestLogItem",
		function(self, ...)
			-- BUG IN WoW 6.2: This item will come through with an item ID of 0 and we'll fail to get stats from it normally.
			-- Special thanks to Phanx for suggesting this workaround!
			local ItemLink = GetQuestLogItemLink(...)
			if ItemLink then
				PawnUpdateTooltip("GameTooltip", "SetHyperlink", ItemLink)
			else
				PawnUpdateTooltip("GameTooltip", "SetQuestLogItem", ...)
			end
		end)
	hooksecurefunc(GameTooltip, "SetSendMailItem", function(self, ...) PawnUpdateTooltip("GameTooltip", "SetSendMailItem", ...) end)
	if SetSocketGem then
		-- Gems don't exist in Classic.
		hooksecurefunc(GameTooltip, "SetSocketGem", function(self, ...) PawnUpdateTooltip("GameTooltip", "SetSocketGem", ...) end)
	end
	hooksecurefunc(GameTooltip, "SetTradePlayerItem", function(self, ...) PawnUpdateTooltip("GameTooltip", "SetTradePlayerItem", ...) end)
	if SetRecipeResultItem then
		hooksecurefunc(GameTooltip, "SetRecipeResultItem",
			function(self, ...)
				local ItemLink = C_TradeSkillUI.GetRecipeItemLink(...)
				PawnUpdateTooltip("GameTooltip", "SetHyperlink", ItemLink)
			end)
	end
	hooksecurefunc(GameTooltip, "SetTradeTargetItem", function(self, ...) PawnUpdateTooltip("GameTooltip", "SetTradeTargetItem", ...) end)
	if SetVoidItem then
		hooksecurefunc(GameTooltip, "SetVoidItem", function(self, ...) PawnUpdateTooltip("GameTooltip", "SetVoidItem", ...) end)
		hooksecurefunc(GameTooltip, "SetVoidDepositItem", function(self, ...) PawnUpdateTooltip("GameTooltip", "SetVoidDepositItem", ...) end)
		hooksecurefunc(GameTooltip, "SetVoidWithdrawalItem", function(self, ...) PawnUpdateTooltip("GameTooltip", "SetVoidWithdrawalItem", ...) end)
	end
	hooksecurefunc(GameTooltip, "SetTrainerService",
		function(self, Index)
			local ItemLink = GetTrainerServiceItemLink(Index)
			if ItemLink then PawnUpdateTooltip("GameTooltip", "SetHyperlink", ItemLink) end
		end)
	hooksecurefunc(GameTooltip, "Hide", function(self, ...) PawnLastHoveredItem = nil end)

	-- World quest embedded tooltips
	hooksecurefunc("EmbeddedItemTooltip_SetItemByQuestReward",
		function(self, QuestLogIndex, QuestID, ...)
			if PawnCommon.ShowQuestUpgradeAdvisor then
				local ItemName, ItemTexture = GetQuestLogRewardInfo(QuestLogIndex, QuestID)
				if ItemName and ItemTexture then
					PawnUpdateTooltip(self.Tooltip:GetName(), "SetQuestLogItem", "reward", QuestLogIndex, QuestID, ...)
					self.Tooltip:Show() -- resizes the tooltip's boundaries in case our annotation made it wider
				end
			end
		end)
	
	-- The item link tooltip (only hook it if it's an actual item)
	hooksecurefunc(ItemRefTooltip, "SetHyperlink",
		function(self, ItemLink, ...)
			-- Attach an icon to the tooltip first so that an existing icon can be hidden if the new hyperlink doesn't have one.
			PawnAttachIconToTooltip(ItemRefTooltip, false, ItemLink)
			if PawnGetHyperlinkType(ItemLink) ~= "item" then return end
			PawnUpdateTooltip("ItemRefTooltip", "SetHyperlink", ItemLink, ...)
		end)
	ItemRefTooltip:HookScript("OnEnter", function() local _; _, PawnLastHoveredItem = ItemRefTooltip:GetItem() end)
	ItemRefTooltip:HookScript("OnLeave", function() PawnLastHoveredItem = nil end)
	ItemRefTooltip:HookScript("OnMouseUp",
		function(object, button)
			if button == "RightButton" then
				local _, ItemLink = ItemRefTooltip:GetItem()
				PawnUI_SetCompareItemAndShow(2, ItemLink)
			end
		end)
	
	-- The group loot roll window
	local LootRollClickHandler =
		function(object, button)
			if button == "RightButton" then
				local ItemLink = GetLootRollItemLink(object:GetParent().rollID)
				PawnUI_SetCompareItemAndShow(2, ItemLink)
			end
		end
	GroupLootFrame1.IconFrame:HookScript("OnMouseUp", LootRollClickHandler)
	GroupLootFrame2.IconFrame:HookScript("OnMouseUp", LootRollClickHandler)
	GroupLootFrame3.IconFrame:HookScript("OnMouseUp", LootRollClickHandler)
	GroupLootFrame4.IconFrame:HookScript("OnMouseUp", LootRollClickHandler)
	GroupLootFrame1:HookScript("OnShow", PawnUI_GroupLootFrame_OnShow)
	GroupLootFrame2:HookScript("OnShow", PawnUI_GroupLootFrame_OnShow)
	GroupLootFrame3:HookScript("OnShow", PawnUI_GroupLootFrame_OnShow)
	GroupLootFrame4:HookScript("OnShow", PawnUI_GroupLootFrame_OnShow)
	
	-- The loot history window
	hooksecurefunc("LootHistoryFrame_UpdateItemFrame", PawnUI_LootHistoryFrame_UpdateItemFrame)
	
	-- The loot won window
	hooksecurefunc("LootWonAlertFrame_SetUp", PawnUI_LootWonAlertFrame_SetUp)
	
	-- The "currently equipped" tooltips (two, in case of rings, trinkets, and dual wielding)
	hooksecurefunc(ShoppingTooltip1, "SetCompareItem",
		function(self, ...)
			local _, ItemLink1 = ShoppingTooltip1:GetItem()
			PawnUpdateTooltip("ShoppingTooltip1", "SetCompareItem", ItemLink1, ...)
			PawnAttachIconToTooltip(ShoppingTooltip1, true)
			local _, ItemLink2 = ShoppingTooltip2:GetItem()
			if ItemLink2 and ShoppingTooltip2:IsShown() then
				PawnUpdateTooltip("ShoppingTooltip2", "SetHyperlink", ItemLink2, ...)
				PawnAttachIconToTooltip(ShoppingTooltip2, true)
			end
		end)
	hooksecurefunc(ItemRefShoppingTooltip1, "SetCompareItem",
		function(self, ...)
			local _, ItemLink1 = ItemRefShoppingTooltip1:GetItem()
			PawnUpdateTooltip("ItemRefShoppingTooltip1", "SetCompareItem", ItemLink1, ...)
			PawnAttachIconToTooltip(ItemRefShoppingTooltip1, true)
			local _, ItemLink2 = ItemRefShoppingTooltip2:GetItem()
			if ItemLink2 and ItemRefShoppingTooltip2:IsShown() then
				PawnUpdateTooltip("ItemRefShoppingTooltip2", "SetHyperlink", ItemLink2, ...)
				PawnAttachIconToTooltip(ItemRefShoppingTooltip2, true)
			end
		end)
		
	-- MultiTips compatibility
	if MultiTips then
		VgerCore.HookInsecureFunction(ItemRefTooltip2, "SetHyperlink", function(self, ItemLink, ...) PawnUpdateTooltip("ItemRefTooltip2", "SetHyperlink", ItemLink, ...) PawnAttachIconToTooltip(ItemRefTooltip2, false, ItemLink) end)
		VgerCore.HookInsecureFunction(ItemRefTooltip3, "SetHyperlink", function(self, ItemLink, ...) PawnUpdateTooltip("ItemRefTooltip3", "SetHyperlink", ItemLink, ...) PawnAttachIconToTooltip(ItemRefTooltip3, false, ItemLink) end)
		VgerCore.HookInsecureFunction(ItemRefTooltip4, "SetHyperlink", function(self, ItemLink, ...) PawnUpdateTooltip("ItemRefTooltip4", "SetHyperlink", ItemLink, ...) PawnAttachIconToTooltip(ItemRefTooltip4, false, ItemLink) end)
		VgerCore.HookInsecureFunction(ItemRefTooltip5, "SetHyperlink", function(self, ItemLink, ...) PawnUpdateTooltip("ItemRefTooltip5", "SetHyperlink", ItemLink, ...) PawnAttachIconToTooltip(ItemRefTooltip5, false, ItemLink) end)
	end
	
	-- EquipCompare compatibility
	if ComparisonTooltip1 then
		VgerCore.HookInsecureFunction(ComparisonTooltip1, "SetHyperlinkCompareItem", function(self, ItemLink, ...) PawnUpdateTooltip("ComparisonTooltip1", "SetHyperlinkCompareItem", ItemLink, ...) PawnAttachIconToTooltip(ComparisonTooltip1, true) end)
		VgerCore.HookInsecureFunction(ComparisonTooltip2, "SetHyperlinkCompareItem", function(self, ItemLink, ...) PawnUpdateTooltip("ComparisonTooltip2", "SetHyperlinkCompareItem", ItemLink, ...) PawnAttachIconToTooltip(ComparisonTooltip2, true) end)
		VgerCore.HookInsecureFunction(ComparisonTooltip1, "SetInventoryItem", function(self, ...) PawnUpdateTooltip("ComparisonTooltip1", "SetInventoryItem", ...) PawnAttachIconToTooltip(ComparisonTooltip1, true) end) -- EquipCompare with CharactersViewer
		VgerCore.HookInsecureFunction(ComparisonTooltip2, "SetInventoryItem", function(self, ...) PawnUpdateTooltip("ComparisonTooltip2", "SetInventoryItem", ...) PawnAttachIconToTooltip(ComparisonTooltip2, true) end) -- EquipCompare with CharactersViewer
		VgerCore.HookInsecureFunction(ComparisonTooltip1, "SetHyperlink", function(self, ItemLink, ...) PawnUpdateTooltip("ComparisonTooltip1", "SetHyperlink", ItemLink, ...) PawnAttachIconToTooltip(ComparisonTooltip1, true) end) -- EquipCompare with Armory
		VgerCore.HookInsecureFunction(ComparisonTooltip2, "SetHyperlink", function(self, ItemLink, ...) PawnUpdateTooltip("ComparisonTooltip2", "SetHyperlink", ItemLink, ...) PawnAttachIconToTooltip(ComparisonTooltip2, true) end) -- EquipCompare with Armory
	end
	
	-- Outfitter compatibility
	if Outfitter and Outfitter._ExtendedCompareTooltip then
		VgerCore.HookInsecureFunction(Outfitter._ExtendedCompareTooltip, "AddShoppingLink", function(self, pTitle, pName, pLink, ...) PawnUpdateTooltip("OutfitterCompareTooltip" .. self.NumTooltipsShown, "SetHyperlink", pLink) end)
	end
	
	-- AtlasLoot Enhanced compatibility
	if AtlasLootTooltip then
		VgerCore.HookInsecureFunction(AtlasLootTooltip, "SetHyperlink", function(self, ...) PawnUpdateTooltip("AtlasLootTooltip", "SetHyperlink", ...) end)
		VgerCore.HookInsecureFunction(AtlasLootTooltip, "SetItemByID", function(self, ...) PawnUpdateTooltip("AtlasLootTooltip", "SetItemByID", ...) end)
	end

	-- LinkWrangler compatibility -- hook the Link Wrangler item link tooltips.
	if LinkWrangler then
		LinkWrangler.RegisterCallback("Pawn", PawnLinkWranglerOnTooltip, "refresh")
		LinkWrangler.RegisterCallback("Pawn", PawnLinkWranglerOnTooltip, "refreshcomp")
	end

	-- In-bag upgrade icons
	if ContainerFrame_UpdateItemUpgradeIcons then
		PawnOriginalIsContainerItemAnUpgrade = IsContainerItemAnUpgrade
		IsContainerItemAnUpgrade = function(bagID, slot, ...)
			if PawnCommon.ShowBagUpgradeAdvisor then
				local _, Count, _, _, _, _, ItemLink = GetContainerItemInfo(bagID, slot)
				if not Count then return false end -- If the stack count is 0, it's clearly not an upgrade
				if not ItemLink then return nil end -- If we didn't get an item link, but there's an item there, try again later
				return PawnShouldItemLinkHaveUpgradeArrow(ItemLink, true) -- true means to check player level
			else
				return PawnOriginalIsContainerItemAnUpgrade(bagID, slot, ...)
			end
			-- FUTURE: Consider hooking ContainerFrameItemButton_UpdateItemUpgradeIcon / ContainerFrame_UpdateItemUpgradeIcons instead, but then Pawn would need its own "retry when not enough information is available" logic.  And Pawn also would no longer automatically integrate with other bag addons.
		end
	end

	-- We're now effectively initialized.  Just the last steps of scale initialization remain.
	PawnIsInitialized = true

	-- If any of our dependencies have already loaded, pretend that they just loaded now.
	if IsAddOnLoaded("Blizzard_InspectUI") then PawnOnAddonLoaded("Blizzard_InspectUI") end
	if IsAddOnLoaded("Blizzard_ItemSocketingUI") then PawnOnAddonLoaded("Blizzard_ItemSocketingUI") end
	if IsAddOnLoaded("Blizzard_ArtifactUI") then PawnOnAddonLoaded("Blizzard_ArtifactUI") end

	-- Now, load any plugins that are ready to be loaded.
	PawnInitializePlugins()
	
	-- Go through the user's scales and check them for errors.
	for ScaleName, _ in pairs(PawnCommon.Scales) do
		PawnCorrectScaleErrors(ScaleName)
	end

	-- Warn them if Pawn might be broken due to changing the thousands or decimal separator.
	if (LARGE_NUMBER_SEPERATOR and PawnLocal.ThousandsSeparator ~= LARGE_NUMBER_SEPERATOR) or
	(DECIMAL_SEPERATOR and PawnLocal.DecimalSeparator ~= DECIMAL_SEPERATOR) then
		VgerCore.Fail("Pawn may provide incorrect advice due to a potential addon conflict: Pawn is not compatible with Combat Numbers Separator, Titan Panel Artifact Power, or other addons that change the way that numbers appear.")
	end

	-- If auto-spec is on, check their spec now in case they switched on a different PC.
	if GetSpecialization then
		PawnOnSpecChanged()
	end
	
	-- Then, recalculate totals.
	-- This must be done after checking for errors is completed on all scales because it can trigger other recalculations.
	for ScaleName, _ in pairs(PawnCommon.Scales) do
		PawnRecalculateScaleTotal(ScaleName)
	end
	
end

function PawnOnLogout()
	-- Uninitialize all scale provider plugins.
	PawnUnitializePlugins()
	-- Clear out cached best item lists for all disabled scales.
	if (not PawnCommon.ShowUpgradesOnTooltips) and (not PawnCommon.ShowLootUpgradeAdvisor) and (not PawnCommon.ShowQuestUpgradeAdvisor) then
		-- The user has disabled all upgrade options, so clear out all upgrade information.
		PawnInvalidateBestItems()
	else
		local Scale, _
		for _, Scale in pairs(PawnCommon.Scales) do
			local CharacterOptions = Scale.PerCharacterOptions[PawnPlayerFullName]
			if CharacterOptions and not CharacterOptions.Visible then
				-- If this scale is hidden for this character, we can remove the character options table entirely since
				-- Visible and BestItems are currently the only two members and we want to clear out BestItems for
				-- invisible scales.  (If more members are added in the future, this will need to be fleshed out.)
				Scale.PerCharacterOptions[PawnPlayerFullName] = nil
			end
		end
	end
end

function PawnOnAddonLoaded(AddonName)
	-- Is Pawn hasn't initialized yet, skip this.  We'll rerun this from PawnInitialize later.
	if not PawnIsInitialized then return end

	if AddonName == "Blizzard_InspectUI" then
		-- After the inspect UI is loaded, we want to hook it to add the Pawn button.
		PawnUI_InspectPawnButton_Attach()
	elseif AddonName == "Blizzard_ItemSocketingUI" then
		-- After the socketing UI is loaded, it gets a Pawn button too.
		PawnUI_SocketingPawnButton_Attach()
	elseif AddonName == "Blizzard_ArtifactUI" then
		-- After the artifact UI is loaded, watch the relic sockets.
		PawnUI_HookArtifactUI()
	end
end

-- Resets all Pawn options and scales.  Used to set the saved variable to a default state.
function PawnResetOptions()
	PawnCommon = nil
	PawnOptions = nil
	PawnInitializeOptions()
end

-- Sets values for any options that don't have a value set yet.  Useful when upgrading.  This method can also be
-- called by any code that might run before initialization finishes to ensure that PawnCommon exists and is set up.
function PawnInitializeOptions()
	local _

	-- If either of the options tables don't exist yet, create them now.
	if not PawnCommon then PawnCommon = {} end
	if not PawnOptions then PawnOptions = {} end
	
	-- We need to know the player's full name for some server-specific settings.
	PawnPlayerFullName = UnitName("player") .. "-" .. GetRealmName()
	-- Save the last known player name to PawnOptions so that we can detect character renames and server
	-- transfers in the future.
	PawnOptions.LastPlayerFullName = PawnPlayerFullName
	
	-- Now, migrate all settings over to PawnCommon, and upgrade to the current version from any previous version
	-- of Pawn (or none at all).  Settings are respected in this order of preference:
	-- 1. Global settings in PawnCommon
	-- 2. Per-character settings in PawnOptions (used prior to Pawn 1.3)
	-- 3. The default values for the settings.
	PawnMigrateSetting("Debug", false)
	PawnMigrateSetting("Digits", 1)
	PawnMigrateSetting("ShowItemID", false)
	PawnMigrateSetting("AlignNumbersRight", false)
	PawnMigrateSetting("ButtonPosition", PawnButtonPositionRight)
	PawnMigrateSetting("ShowTooltipIcons", true)
	
	-- Set default values for other new options.
	if PawnCommon.ShowUpgradesOnTooltips == nil then PawnCommon.ShowUpgradesOnTooltips = true end
	if PawnCommon.ShowValuesForUpgradesOnly == nil then PawnCommon.ShowValuesForUpgradesOnly = true end
	if PawnCommon.ColorTooltipBorder == nil then PawnCommon.ColorTooltipBorder = true end
	if PawnCommon.ShowUpgradeAdvisors ~= nil then
		-- If the user's upgrading from Pawn 1.5.4 or earlier, migrate the single upgrade advisors setting into the two settings in 1.5.5.
		if PawnCommon.ShowLootUpgradeAdvisor == nil then PawnCommon.ShowLootUpgradeAdvisor = PawnCommon.ShowUpgradeAdvisors end
		if PawnCommon.ShowQuestUpgradeAdvisor == nil then PawnCommon.ShowQuestUpgradeAdvisor = PawnCommon.ShowUpgradeAdvisors end
		PawnCommon.ShowUpgradeAdvisors = nil
	else
		-- Otherwise just default them to on.
		if PawnCommon.ShowLootUpgradeAdvisor == nil then PawnCommon.ShowLootUpgradeAdvisor = true end
		if PawnCommon.ShowQuestUpgradeAdvisor == nil then PawnCommon.ShowQuestUpgradeAdvisor = true end
	end
	if PawnCommon.ShowSocketingAdvisor == nil then PawnCommon.ShowSocketingAdvisor = true end

	-- Now, migrate all scales from this character over to PawnCommon.
	local ScaleName, Scale
	if not PawnCommon.Scales then PawnCommon.Scales = {} end
	if PawnOptions.Scales then
		-- Looks like there's one or more scales on this character that need to be migrated.
		for ScaleName, Scale in pairs(PawnOptions.Scales) do
			if PawnCommon.Scales[ScaleName] then
				-- This scale name already exists, so we have to make it unique first.
				-- First, try just appending the player name.
				-- If that's not good enough, start trying sequential numbers.  (Sigh; why do people need
				-- to make things so complicated?  Did you really need ten characters with the same name
				-- and identically named scales on each one?)
				ScaleName = ScaleName .. " (" .. UnitName("player") .. ")"
				local ScaleNameBase = ScaleName .. " ("
				local i = 0
				while PawnCommon.Scales[ScaleName] do
					i = i + 1
					ScaleName = ScaleNameBase .. i .. ")"
				end
			end
			
			-- We now have a unique name for this scale, so transfer it over to the master scale list.
			PawnCommon.Scales[ScaleName] = Scale
			Scale.PerCharacterOptions = { }
			Scale.PerCharacterOptions[PawnPlayerFullName] = { }
			if not Scale.Hidden then
				Scale.PerCharacterOptions[PawnPlayerFullName].Visible = true
			end
			Scale.NormalizationFactor = PawnOptions.NormalizationFactor
			Scale.Hidden = nil
		end
	end
	-- Now that migration is complete, remove all migrated scales from the per-character options.
	PawnOptions.Scales = nil
	
	-- These options have been removed or otherwise are no longer useful.
	PawnOptions.ShowItemLevel = nil
	PawnOptions.ShownGettingStarted = nil
	PawnOptions.NormalizationFactor = nil
	PawnCommon.ShowUnenchanted = nil
	PawnCommon.ShowAsterisks = nil
	PawnCommon.ShowBoth1HAnd2HUpgrades = nil
	PawnCommon.ShowSpace = nil

	-- Remove any stale scales from previous versions that might have accumulated.
	-- the user might have accumulated.
	local ScalesToDelete = { }
	for ScaleName, Scale in pairs(PawnCommon.Scales) do
		if Scale.Provider == "PawnPlaceholder" or Scale.Provider == "Starter" or Scale.Provider == "Wowhead" then tinsert(ScalesToDelete, ScaleName) end
	end
	for _, ScaleName in pairs(ScalesToDelete) do
		PawnCommon.Scales[ScaleName] = nil
		PawnRecalculateScaleTotal(ScaleName) -- removes information from the cache
	end

	-- Some features were deleted in WoW 6.0.
	PawnCommon.ShowReforgingAdvisor = nil

	-- And some more in WoW 7.1.
	PawnCommon.IgnoreItemUpgrades = nil

	-- Any new stuff since the last version they used?
	if not PawnCommon.LastVersion then PawnCommon.LastVersion = 0 end
	if not PawnOptions.LastVersion then PawnOptions.LastVersion = 0 end
	if PawnCommon.LastVersion < 1.9 then
		-- When upgrading to 1.9, enable the "ignore sockets on low-level items" option.
		PawnCommon.IgnoreGemsWhileLeveling = true
	end
	if PawnCommon.LastVersion < 2.0000 then
		-- The new "show spec icons" option is enabled by default.
		PawnCommon.ShowSpecIcons = true
	end
	if PawnOptions.LastVersion < 2.0000 then
		-- When upgrading each character to 2.0, turn on the auto-scale option, but just once.
		PawnOptions.AutoSelectScales = true
	end
	if PawnCommon.LastVersion < 2.0101 then
		-- The new Bag Upgrade Advisor is on by default, but it's not supported in Classic.
		if VgerCore.IsClassic then
			PawnCommon.ShowBagUpgradeAdvisor = false
		else
			PawnCommon.ShowBagUpgradeAdvisor = true
		end
	end
	if PawnOptions.LastVersion < 2.0219 then
		-- The item squish happened in WoW 8.0, so relic item levels changed.
		PawnOptions.Artifacts = nil
	end
	if PawnOptions.LastVersion < 2.0227 then
		-- The relic advisor is off by default as of 2.2.27.
		PawnCommon.ShowRelicUpgrades = false
	end
	if PawnCommon.LastVersion < 2.0232 then
		-- When upgrading to 2.2.32, turn off this annoying debug option if they still had it on.
		PawnCommon.DebugCache = nil
	end
	if PawnCommon.LastVersion < 2.0244 then
		-- The "show item level upgrades" option is new for 2.2.44 and on by default, but NOT in Classic.
		if VgerCore.IsClassic then
			PawnCommon.ShowItemLevelUpgrades = false
		else
			PawnCommon.ShowItemLevelUpgrades = true
		end
	end
	if PawnCommon.LastVersion < PawnMrRobotLastUpdatedVersion then
		-- If the Ask Mr. Robot scales have been updated since the last time they used Pawn, re-scan gear.
		PawnInvalidateBestItems()
	end
	PawnCommon.LastVersion = PawnVersion
	PawnOptions.LastVersion = PawnVersion

	-- Pawn on WoW Classic doesn't have Automatic mode.
	if VgerCore.IsClassic then
		PawnOptions.AutoSelectScales = false
	end

	-- Finally, this stuff needs to get done after options are changed.
	PawnRecreateAnnotationFormats()
	
end

-- If the specified setting does not exist in the common settings list, this function first tries to migrate it from the
-- current character's settings (from Pawn 1.2 or earlier).  If it's not there either, it's set to a default value.
function PawnMigrateSetting(SettingName, Default)
	if PawnCommon[SettingName] ~= nil then
		PawnOptions[SettingName] = nil
		return
	end
	if PawnOptions[SettingName] ~= nil then
		PawnCommon[SettingName] = PawnOptions[SettingName]
		PawnOptions[SettingName] = nil
		return
	end
	PawnCommon[SettingName] = Default
end

-- Once per new version of Pawn that adds keybindings, bind the new actions to default keys.
function PawnSetDefaultKeybindings()
	-- SaveBindings doesn't work on WoW Classic.
	if VgerCore.IsClassic then return end

	-- It's possible that this will happen before the main initialization code, so we need to ensure that the
	-- default Pawn options have been set already.  Doing this multiple times is harmless.
	if not PawnCommon then VgerCore.Fail("Can't set keybindings until Pawn starts to initialize.") return end

	if PawnOptions.LastKeybindingsSet == nil then PawnOptions.LastKeybindingsSet = 0 end
	local BindingSet = false
	
	-- Keybindings for opening the Pawn UI and setting comparison items.
	if PawnOptions.LastKeybindingsSet < 1 then
		BindingSet = PawnSetKeybindingIfAvailable(PAWN_TOGGLE_UI_DEFAULT_KEY, "PAWN_TOGGLE_UI") or BindingSet
		BindingSet = PawnSetKeybindingIfAvailable(PAWN_COMPARE_LEFT_DEFAULT_KEY, "PAWN_COMPARE_LEFT") or BindingSet
		BindingSet = PawnSetKeybindingIfAvailable(PAWN_COMPARE_RIGHT_DEFAULT_KEY, "PAWN_COMPARE_RIGHT") or BindingSet
	end
	
	-- If any keybindings were changed, save the user's bindings.
	if BindingSet then
		local CurrentBindingSet = GetCurrentBindingSet()
		if CurrentBindingSet == 1 or CurrentBindingSet == 2 then
			SaveBindings(CurrentBindingSet)
		else
			VgerCore.Fail("GetCurrentBindingSet() returned unexpected value: " .. tostring(CurrentBindingSet))
		end
	end
	
	-- Record that we've set those keybindings, so we don't try to set them again in the future, even if
	-- the user clears them.
	PawnOptions.LastKeybindingsSet = 1
end

-- Sets a keybinding to its default value if it's not already assigned to something else.  Returns true if anything was changed.
function PawnSetKeybindingIfAvailable(Key, Binding)
	-- Is this key already bound?
	local ExistingBinding = GetBindingAction(Key, true) -- true: check overrides as well (ElvUI compatibility)
	if not ExistingBinding or ExistingBinding == "" then
		-- Bind this key to its default Pawn action.
		SetBinding(Key, Binding)
		return true
	else
		-- This key is already bound, so do nothing.
		return false
	end
end

-- Returns an empty Pawn scale table.
function PawnGetEmptyScale()
	return
	{
		["UpgradesFollowSpecialization"] = true,
		["PerCharacterOptions"] = { },
		["Values"] = { },
	}
end

-- Returns the default Pawn scale table, either for the current player's spec, or for the supplied class and spec if non-nil.
function PawnGetDefaultScale(ClassID, SpecID, NoStats)
	local _
	if ClassID == nil or SpecID == nil then
		_, _, ClassID = UnitClass("player")
		SpecID = GetSpecialization()
	end
	local Template = PawnFindScaleTemplate(ClassID, SpecID)
	local ScaleValues = PawnGetStatValuesForTemplate(Template, NoStats)
	return 
	{
		["ClassID"] = ClassID,
		["SpecID"] = SpecID,
		["UpgradesFollowSpecialization"] = true,
		["PerCharacterOptions"] = { },
		["Values"] = ScaleValues,
	}
end

-- LinkWrangler compatibility
function PawnLinkWranglerOnTooltip(Tooltip, ItemLink)
	if not PawnIsInitialized then return end
	if not Tooltip then return end
	PawnUpdateTooltip(Tooltip:GetName(), "SetHyperlink", ItemLink)
	PawnAttachIconToTooltip(Tooltip, false, ItemLink)
end

-- If debugging is enabled, show a message; otherwise, do nothing.
function PawnDebugMessage(Message)
	if PawnCommon.Debug then
		VgerCore.Message(Message)
	end
end

-- Processes an Pawn slash command.
function PawnCommand(Command)
	if Command == "" then
		PawnUIShow()
	elseif Command == "debug on" then
		PawnCommon.Debug = true
		PawnResetTooltips()
		if PawnUIFrame_DebugCheck then PawnUIFrame_DebugCheck:SetChecked(PawnCommon.Debug) end
	elseif Command == "debug off" then
		PawnCommon.Debug = false
		PawnResetTooltips()
		if PawnUIFrame_DebugCheck then PawnUIFrame_DebugCheck:SetChecked(PawnCommon.Debug) end
	elseif Command == "backup" then
		PawnUIExportAllScales()
	elseif strsub(Command, 1, 7) == "compare" then
		local CompareIndex, ItemLink1, ItemLink2
		if strsub(Command, 9, 13) == "left " then
			local SplitIndex = strfind(Command, " right ", 13, true)
			if SplitIndex and SplitIndex + 7 < strlen(Command) then
				-- Left Item1 Right Item2
				ItemLink1 = strsub(Command, 14, SplitIndex - 1)
				ItemLink2 = strsub(Command, SplitIndex + 7)
			else
				-- Left Item1
				ItemLink1 = strsub(Command, 14)
			end
		elseif strsub(Command, 9, 14) == "right " then
			-- Right Item2
			ItemLink2 = strsub(Command, 15)
		else
			-- Item2
			ItemLink2 = strsub(Command, 9)
		end
		if ItemLink1 and strlen(ItemLink1) == 0 then ItemLink1 = nil end
		if ItemLink2 and strlen(ItemLink2) == 0 then ItemLink2 = nil end
		if ItemLink1 or ItemLink2 then
			if ItemLink2 then
				local IsReady2 = (GetItemInfo(ItemLink2) ~= nil)
				if IsReady2 then
					PawnUI_SetCompareItemAndShow(2, ItemLink2)
				else
					C_Timer.After(1, function() PawnUI_SetCompareItemAndShow(2, ItemLink2) end)
				end
			end
			if ItemLink1 then
				local IsReady1 = (GetItemInfo(ItemLink1) ~= nil)
				if IsReady1 then
					PawnUI_SetCompareItemAndShow(1, ItemLink1)
				else
					C_Timer.After(1, function() PawnUI_SetCompareItemAndShow(1, ItemLink1) end)
				end
			end
		else
			VgerCore.Message("Usage: /pawn compare [ left ItemID | ItemLink [ right ]] ItemID | ItemLink")
			VgerCore.Message("  /pawn compare 16795")
			VgerCore.Message("  /pawn compare left item:16795:0:0")
			VgerCore.Message("  /pawn compare left |cffa335ee|Hitem:16795|h[Arcanist Crown]|h|r right 16914")
		end
	else
		PawnUsage()
	end
end

-- Displays Pawn usage information.
function PawnUsage()
	VgerCore.Message(" ")
	VgerCore.MultilineMessage(PawnLocal.Usage)
	VgerCore.Message(" ")
end

-- Returns an empty item for use in the item cache.
function PawnGetEmptyCachedItem(NewItemLink, NewItemName, NewNumLines)
	-- Also includes properties set to nil by default: Stats, SocketBonusStats, UnenchantedState, UnenchantedSocketBonusStats, Values, Level, ItemID
	return { Name = NewItemName, NumLines = NewNumLines, UnknownLines = {}, Link = NewItemLink }
end

-- Searches the item cache for an item, and either returns the correct cached item, or nil.
function PawnGetCachedItem(ItemLink, ItemName, NumLines)
	-- If the item cache is empty, skip all this...
	if (not PawnItemCache) or (#PawnItemCache == 0) then return end
	-- If debug mode is on, the cache is disabled.
	if PawnCommon.Debug then return end
	-- If this is WoW Classic, the cache is also disabled.
	-- (There's a problem I haven't tracked down yet where item tooltips are returned with incomplete stats and then get cached in that incomplete state.)
	if VgerCore.IsClassic then return end

	-- Otherwise, search the item cache for this item.
	local _
	for _, CachedItem in pairs(PawnItemCache) do
		if (not NumLines) or (NumLines == CachedItem.NumLines) then
			if ItemLink and CachedItem.Link then
				if ItemLink == CachedItem.Link then
					return CachedItem
				end
			end
		end
	end
end

-- Adds an item to the item cache, removing existing items if necessary.
function PawnCacheItem(CachedItem)
	-- If debug mode is on, the cache is disabled.
	if PawnCommon.Debug then return end
	-- If this is WoW Classic, the cache is also disabled.
	-- (There's a problem I haven't tracked down yet where item tooltips are returned with incomplete stats and then get cached in that incomplete state.)
	if VgerCore.IsClassic then return end
	
	-- Cache it.
	if PawnItemCacheMaxSize <= 0 then return end
	if not PawnItemCache then PawnItemCache = {} end
	tinsert(PawnItemCache, CachedItem)
	while #PawnItemCache > PawnItemCacheMaxSize do
		tremove(PawnItemCache, 1)
	end
end

-- Clears the item cache.
function PawnClearCache()
	PawnItemCache = nil
	-- We should also clear out the gem cache when doing this.
	PawnClearCacheValuesOnly()
end

-- Clears only the calculated values for items in the cache, retaining things like stats.
function PawnClearCacheValuesOnly()
	local CachedItem, _
	-- First, the main item cache.
	if PawnItemCache then
		for _, CachedItem in pairs(PawnItemCache) do
			CachedItem.Values = nil
		end
	end
	-- Then, the gem caches.  For each gem meta-table, look at the gem table (which is in
	-- column 3) and then clear out the contents of column 9 of that table.
	local GemTable
	for _, GemTable in pairs(PawnGemQualityLevels) do
		for _, CachedItem in pairs(GemTable[2]) do
			CachedItem[9] = nil
		end
	end
	-- Then, the Compare tab's cache.
	if PawnUI_ClearCacheValues then PawnUI_ClearCacheValues() end
end

-- Clears all calculated values and causes them to be recalculated the next time tooltips are displayed.  The stats
-- will not be re-read next time, however.
function PawnResetTooltips()
	-- Clear out the calculated values in the cache, leaving item data.
	PawnClearCacheValuesOnly()
	-- Then, attempt to reset tooltips where possible.  On-hover tooltips don't need to be reset manually, but the
	-- item link tooltip does.
	PawnResetTooltip("ItemRefTooltip")
	PawnResetTooltip("ItemRefTooltip2") -- MultiTips compatibility
	PawnResetTooltip("ItemRefTooltip3") -- MultiTips compatibility
	PawnResetTooltip("ItemRefTooltip4") -- MultiTips compatibility
	PawnResetTooltip("ItemRefTooltip5") -- MultiTips compatibility
	PawnResetTooltip("ComparisonTooltip1") -- EquipCompare compatibility
	PawnResetTooltip("ComparisonTooltip2") -- EquipCompare compatibility
	PawnResetTooltip("AtlasLootTooltip") -- AtlasLoot compatibility
	-- Update advisors.
	if LootHistoryFrame then LootHistoryFrame_FullUpdate(LootHistoryFrame) end
end

-- Attempts to reset a single tooltip, causing Pawn values to be recalculated.  Returns true if successful.
function PawnResetTooltip(TooltipName)
	local Tooltip = _G[TooltipName]
	if not Tooltip or not Tooltip.IsShown or not Tooltip:IsShown() or not Tooltip.GetItem then return end
	
	local _, ItemLink = Tooltip:GetItem()
	if not ItemLink then return end
	
	Tooltip:SetOwner(UIParent, "ANCHOR_PRESERVE")
	Tooltip:SetHyperlink(ItemLink)
	Tooltip:Show()
	return true
end

-- Recalculates the total value of all stats in a scale, as well as all of the best gems for that scale.
function PawnRecalculateScaleTotal(ScaleName)
	local _

	-- Find the appropriate scale.
	local ThisScale = PawnCommon.Scales[ScaleName]
	local ThisScaleValues
	if ThisScale then ThisScaleValues = ThisScale.Values end
	if not ThisScaleValues then
		-- If the passed-in scale doesn't exist, remove it from our cache and exit.
		PawnScaleTotals[ScaleName] = nil
		PawnScaleBestGems[ScaleName] = nil
		return
	end
	
	-- Calculate the total.  When calculating the total value for a scale, ignore sockets.
	local Total = 0
	for StatName, Value in pairs(ThisScaleValues) do
		if Value and Value > 0 then
			if StatName ~= "PrismaticSocket" then
				Total = Total + Value
			else
				VgerCore.Fail("Socket values should always be zero, but " .. StatName .. " is " .. Value .. ".")
			end
		end
	end
	PawnScaleTotals[ScaleName] = Total
	
	-- Now recalculate all of the best gems for this scale.
	if not PawnScaleBestGems[ScaleName] then
		PawnScaleBestGems[ScaleName] =
		{
			["PrismaticSocket"] = { },
			["PrismaticSocketValue"] = { },
		}
	end
	local ThisScaleBestGems = PawnScaleBestGems[ScaleName]

	local QualityLevelData
	for _, QualityLevelData in pairs(PawnGemQualityLevels) do
		local ItemLevel = QualityLevelData[1]
		local GemData = QualityLevelData[2]

		if PawnCommon.Debug then
			VgerCore.Message("")
			VgerCore.Message("GEMS FOR ITEM LEVEL " .. tostring(ItemLevel))
			VgerCore.Message("")
		end

		local BestPrismatic
		BestPrismatic, ThisScaleBestGems.PrismaticSocket[ItemLevel] = PawnFindBestGems(ScaleName, GemData, true, true, true)
		ThisScaleBestGems.PrismaticSocketValue[ItemLevel] = BestPrismatic
	end

end

-- Recreates the tooltip annotation format strings.
function PawnRecreateAnnotationFormats()
	-- REVIEW: When the number of digits is set to 0, we should use commas (PawnLocal.ThousandsSeparator) instead
	-- of a format string.  (These would have to be functions instead of just a format string.)
	PawnNoValueAnnotationFormat = "%s%s:"
	PawnUnenchantedAnnotationFormat = PawnNoValueAnnotationFormat .. "  %." .. PawnCommon.Digits .. "f"
	PawnEnchantedAnnotationFormat = PawnUnenchantedAnnotationFormat .. "  %s(%." .. PawnCommon.Digits .. "f " .. PawnLocal.BaseValueWord .. ")"
end

local function PawnCheckItemTypeCore(ItemLink, AllowEquippable, AllowStatGems, AllowRelics)
	local _, _, _, InvType, _, ItemClassID, ItemSubClassID = GetItemInfoInstant(ItemLink)
	if (InvType == nil or InvType == "") then
		-- If the item isn't equippable don't bother parsing it, unless it's a gem or relic.
		return
			(AllowStatGems and ItemClassID == LE_ITEM_CLASS_GEM and ItemSubClassID ~= LE_ITEM_GEM_ARTIFACTRELIC) or
			(AllowRelics and ItemClassID == LE_ITEM_CLASS_GEM and ItemSubClassID == LE_ITEM_GEM_ARTIFACTRELIC)
	elseif InvType == "INVTYPE_RELIC" or InvType == "INVTYPE_THROWN" or InvType == "INVTYPE_TABARD" or InvType == "INVTYPE_BAG" or InvType == "INVTYPE_BODY" then
		-- Old (grey, pre-artifact) relics might have sockets and therefore "stats" but they aren't equippable anymore so they shouldn't get values, so just bail out now.
		-- Thrown items, tabards, bags, and shirts (invtype_body) can also never have stats.
		return false
	end
	-- Otherwise, this is an equippable item.
	return AllowEquippable
end

function PawnCanItemHaveStats(ItemLink)
	return PawnCheckItemTypeCore(ItemLink, true, true, false) -- filter for equippable items and stat gems
end

function PawnCanItemBeArtifactUpgrade(ItemLink)
	return PawnCheckItemTypeCore(ItemLink, false, false, true) -- filter for relics only
end

function PawnCanItemHaveUpgradeArrow(ItemLink)
	return PawnCheckItemTypeCore(ItemLink, true, true, true)
end

-- Gets the item data for a specific item link.  Retrieves the information from the cache when possible; otherwise, it gets fresh information.
-- Return value type is the same as PawnGetCachedItem.
function PawnGetItemData(ItemLink)
	if not ItemLink then return end
	
	-- Only item links are supported; other links are not.
	if PawnGetHyperlinkType(ItemLink) ~= "item" then return end

	-- If this type of item can't ever have stats (food, for example), just bail out.
	-- We don't use this codepath in the case of artifact relics.
	if not PawnCanItemHaveStats(ItemLink) then return end
	
	-- If we have an item link, we can extract basic data from it from the user's WoW cache (not the Pawn item cache).
	-- We get a new, normalized version of ItemLink so that items don't end up in the cache multiple times if they're requested
	-- using different styles of links that all point to the same item.
	local ItemID = GetItemInfoInstant(ItemLink)
	local ItemName, NewItemLink, ItemRarity, ItemLevel, _, _, _, _, InvType, ItemTexture = GetItemInfo(ItemLink)
	if NewItemLink then
		ItemLink = NewItemLink
	else
		-- We didn't get a new item link.  This is almost certainly because the item is not in the user's local WoW cache.
		-- REVIEW: In the future, would it be possible to detect this case, and then poll the tooltip until item information
		-- comes back, and THEN parse and annotate it?  There's also an OnTooltipSetItem event.
	end
	
	-- Now, with that information, we can look up the item in the Pawn item cache.
	local Item = PawnGetCachedItem(ItemLink, ItemName)
	if not Item and not NewItemLink then
		-- The item isn't in the user's WoW cache or Pawn cache.  Bail out now.
		if PawnCommon.DebugCache then VgerCore.Message("Pawn debug cache: PawnGetItemData is bailing out because it didn't get item data in time for " .. ItemLink) end
		return 
	end
	if Item and Item.Values then
		return Item
	end
	-- If Item is non-null but Item.Values is null, we're not done yet!

	-- If we don't have a cached item at all, that means we have to load a tooltip and parse it.
	if not Item then
		Item = PawnGetEmptyCachedItem(ItemLink, ItemName)
		Item.Rarity = ItemRarity
		Item.Level = GetDetailedItemLevelInfo(ItemLink) or ItemLevel -- The level from GetItemInfo doesn't take into effect upgrades or heirloom scaling
		Item.ID = ItemID
		if InvType ~= "" then Item.InvType = InvType end
		Item.Texture = ItemTexture
		if PawnCommon.Debug then
			PawnDebugMessage(" ")
			PawnDebugMessage("====================")
			local IDs = PawnGetItemIDsForDisplay(ItemLink)
			if IDs then
				PawnDebugMessage(ItemLink .. VgerCore.Color.Green .. " (" .. tostring(IDs) .. VgerCore.Color.Green .. ")")
			else
				PawnDebugMessage(ItemLink)
			end
		end

		-- First the enchanted stats.
		Item.Stats, Item.SocketBonusStats, Item.UnknownLines, Item.PrettyLink = PawnGetStatsFromTooltipWithMethod(PawnPrivateTooltipName, true, "SetHyperlink", Item.Link)
		Item.NumLines = (_G[PawnPrivateTooltipName]):NumLines()

		if InvType == "INVTYPE_RANGED" or InvType == "INVTYPE_RANGEDRIGHT" then
			-- We convert ranged weapons into the correct "handedness" of weapons since there's no ranged slot anymore.
			-- REVIEW: *** This will cause problems on WoW Classic
			if Item.Stats and Item.Stats.IsWand then
				InvType = "INVTYPE_WEAPONMAINHAND"
			else
				InvType = "INVTYPE_2HWEAPON"
			end
			Item.InvType = InvType
		elseif InvType == "INVTYPE_ROBE" then
			-- Robes are just really long chest armor.
			InvType = "INVTYPE_CHEST"
			Item.InvType = InvType
		end	

		-- Then, the unenchanted stats.  But, we only need to do this if the item is enchanted or socketed.  PawnUnenchantItemLink
		-- will return nil if the item isn't enchanted, so we can skip that process.
		local UnenchantedItemLink = PawnUnenchantItemLink(ItemLink)
		-- As of WoW 6.2.3, Item.Level is incorrect for upgraded items because GetItemInfo returns the wrong value.  PawnUnenchantItemLink could
		-- be enhanced here to get a number to add (+10) for upgradeable items if we decide we need that info.
		if UnenchantedItemLink then
			PawnDebugMessage(" ")
			PawnDebugMessage(PawnLocal.UnenchantedStatsHeader)
			--if PawnCommon.Debug then VgerCore.Message("  Base item link: " .. tostring(PawnEscapeString(UnenchantedItemLink))) end
			Item.UnenchantedStats, Item.UnenchantedSocketBonusStats = PawnGetStatsForItemLink(UnenchantedItemLink, true)
			if not Item.UnenchantedStats then
				PawnDebugMessage(PawnLocal.FailedToGetUnenchantedItemMessage)
			end
		else
			-- If there was no unenchanted item link, then it's because the original item was not
			-- enchanted.  So, the unenchanted item is the enchanted item; copy the stats over.
			-- (Don't just copy the reference, because then changes to one stat table would also
			-- change the other!)
			local TableCopy = {}
			local StatName
			if Item.Stats then
				for StatName, Value in pairs(Item.Stats) do
					TableCopy[StatName] = Value
				end
			end
			Item.UnenchantedStats = TableCopy
			TableCopy = {}
			if Item.SocketBonusStats then
				for StatName, Value in pairs(Item.SocketBonusStats) do
					TableCopy[StatName] = Value
				end
			end
			Item.UnenchantedSocketBonusStats = TableCopy
		end
		
		-- Enchanted items should not get points for empty sockets, nor do they get socket bonuses if there are any empty sockets.
		if Item.Stats and Item.Stats.PrismaticSocket then
			Item.SocketBonusStats = {}
			Item.Stats.PrismaticSocket = nil
		end

		-- If the item doesn't have any stats, don't cache it.  This is done to work around a problem a few people were seeing where
		-- Pawn would get item data, then fail to get it, and then cache the results anyway.  This is a pretty crappy solution, but
		-- hopefully it works until something better can be found.  It's not as bad now that Pawn doesn't bother scanning tooltips for
		-- items that can never have stats, like food and junk.
		if Item.UnenchantedStats == nil or next(Item.UnenchantedStats) == nil then
			if PawnCommon.DebugCache then
				-- You should only see this on purely cosmetic equippable items, like stuff from Griftah, holiday gear, and and few particularly
				-- odd trinkets.
				VgerCore.Message("Not caching because the item didn't have any stats: " .. tostring(ItemLink))
			end
			return
		end

		-- Determine if this item could ever be equipped by this class.
		Item.CanEquip = true
		local StatName
		local _, _, ClassID = UnitClass("player")
		for _, StatName in pairs(PawnNeverUsableStats[ClassID]) do
			if Item.Stats[StatName] then
				Item.CanEquip = false
				break
			end
		end

		-- Cache this item so we don't have to re-parse next time.
		PawnCacheItem(Item)
	end -- if item isn't cached
	
	-- Recalculate the scale values for the item only if necessary.
	PawnRecalculateItemValuesIfNecessary(Item)
	
	return Item
end

-- Gets the item data for a gem, given a table of gem data from Gems.lua.
-- This function does not add gem details to the Pawn item cache.
-- Return value type is the same as PawnGetCachedItem.
function PawnGetGemData(GemData)
	-- If we've already called this function for this gem, keep the stored data.
	if GemData[6] then return GemData[6] end
	
	local ItemID = GemData[1]
	local ItemName, ItemLink, ItemRarity, ItemLevel, _, _, _, _, _, ItemTexture = GetItemInfo(ItemID)
	if ItemLink == nil or ItemName == nil then
		-- If the gem doesn't exist in the user's local cache, we'll have to fake up some info for it.
		ItemLink = format(PawnLocal.GenericGemLink, ItemID, ItemID)
		ItemName = format(PawnLocal.GenericGemName, ItemID)
	end
	local Item = PawnGetEmptyCachedItem(ItemLink, ItemName)
	Item.ID = ItemID
	Item.Rarity = ItemRarity
	Item.Level = GetDetailedItemLevelInfo(ItemLink) or ItemLevel
	Item.Texture = ItemTexture
	Item.UnenchantedStats = { }
	if GemData[2] then
		Item.UnenchantedStats[GemData[2]] = GemData[3]
	end
	if GemData[4] then
		Item.UnenchantedStats[GemData[4]] = GemData[5]
	end
	PawnRecalculateItemValuesIfNecessary(Item, true) -- Ignore the user's normalization factor when determining these gem values.
	
	-- Save this value for next time.
	GemData[6] = Item
	return Item
end

function PawnGetItemLinkFromTooltip(TooltipName, MethodName, Param1, ...)
	VgerCore.Assert(TooltipName, "TooltipName must be non-null!")
	VgerCore.Assert(MethodName, "MethodName must be non-null!")
	if (not TooltipName) or (not MethodName) then return end
	
	-- First, find the tooltip.
	local Tooltip = _G[TooltipName]
	if not Tooltip then return end
	
	-- If we have a tooltip, try to get an item link from it.
	local ItemLink, _
	if (MethodName == "SetHyperlink") and Param1 then
		-- Special case: if the method is SetHyperlink, then we already have an item link.
		-- (Normally, GetItem will work, but SetHyperlink is used by some mod compatibility code.)
		ItemLink = Param1
	elseif Tooltip.GetItem then
		_, ItemLink = Tooltip:GetItem()
	end

	return ItemLink
end

-- Gets the item data for a specific item.  Retrieves the information from the cache when possible; otherwise, gets it from the tooltip specified.  Only use this if you don't have an item link; otherwise use PawnGetItemData(ItemLink).
-- Return value type is the same as PawnGetCachedItem.
function PawnGetItemDataFromTooltip(TooltipName, MethodName, Param1, ...)
	local ItemName, ItemNameLineNumber = PawnGetItemNameFromTooltip(TooltipName)
	if (not ItemName) or (not ItemNameLineNumber) then return end
	local Tooltip = _G[TooltipName]
	local ItemNumLines = Tooltip:NumLines()
	local ItemLink = PawnGetItemLinkFromTooltip(TooltipName, MethodName, Param1, ...)
	local Item = PawnGetCachedItem(ItemLink, ItemName, ItemNumLines)
	if Item and Item.Values then
		return Item
	end
	-- If Item is non-null but Item.Values is null, we're not done yet!
	
	-- Ugh, the tooltip doesn't have item information and this item isn't in the Pawn item cache, so we'll have to try to parse this tooltip.	
	if not Item then
		Item = PawnGetEmptyCachedItem(ItemLink, ItemName, ItemNumLines)
		PawnDebugMessage(" ")
		PawnDebugMessage("====================")
		PawnDebugMessage(VgerCore.Color.Green .. ItemName)
		
		-- Since we don't have an item link, we have to just read stats from the original tooltip, so we only get enchanted values.
		PawnFixStupidTooltipFormatting(TooltipName)
		Item.Stats, Item.SocketBonusStats, Item.UnknownLines = PawnGetStatsFromTooltip(TooltipName, true)
		PawnDebugMessage(PawnLocal.FailedToGetItemLinkMessage)
		
		-- Cache this item so we don't have to re-parse next time.
		PawnCacheItem(Item)
	end
	
	-- Recalculate the scale values for the item only if necessary.
	PawnRecalculateItemValuesIfNecessary(Item)
	
	return Item
end

-- 	Returns: Item, SlotHadItem
-- 		Item: The same information as PawnGetItemData, but based on an inventory slot index instead of an item link.
--		SlotHadItem: true if the slot had an item in it, whether or not we were successful in getting item data.
-- If requested, data for the base unenchanted item can be returned instead; otherwise, the actual item is returned.
function PawnGetItemDataForInventorySlot(Slot, Unenchanted, UnitName)
	if UnitName == nil then UnitName = "player" end
	local ItemLink = GetInventoryItemLink(UnitName, Slot)
	if not ItemLink then return nil, false end
	if Unenchanted then
		local UnenchantedItem = PawnUnenchantItemLink(ItemLink)
		if UnenchantedItem then ItemLink = UnenchantedItem end
	end
	local Item = PawnGetItemData(ItemLink)

	-- Workaround for game bug with artifact off-hands
	if Slot == INVSLOT_OFFHAND and Item and Item.Rarity == 6 then
		local MainHandLink = GetInventoryItemLink("player", INVSLOT_MAINHAND)
		if MainHandLink then 
			Item.Level = GetDetailedItemLevelInfo(MainHandLink) or Item.Level
		end
	end

	return Item, true
end

-- Recalculates the scale values for a cached item if necessary, and returns them.
--	 Parameters: Item, NoNormalization
--		Item: The item to update.
--		NoNormalization: If true, ignores the user's normalization factor.
-- 	Returns: Values
--		Values: The item's table of item values.  Only includes enabled scales with nonzero values.
function PawnRecalculateItemValuesIfNecessary(Item, NoNormalization)
	-- We now have stats for the item.  If values aren't already calculated for the item, calculate those.  This happens when we have
	-- just retrieved the stats for the item, and also when the item values were cleared from the cache but not the stats.
	if not Item.Values then
		-- Calculate each of the values for which there are scales.
		Item.Values = PawnGetAllItemValues(Item.Stats, Item.Level, Item.SocketBonusStats, Item.UnenchantedStats, Item.UnenchantedSocketBonusStats, true, NoNormalization)

		PawnDebugMessage(" ")
	end
	
	return Item.Values
end

-- Returns a single scale value (in both its enchanted and unenchanted forms) for a cached item.  Returns nil for any values that are not present.
function PawnGetSingleValueFromItem(Item, ScaleName)
	if PawnIsScaleVisible(ScaleName) then
		-- If they've enabled this scale, its value should already be calculated.
		local ValuesTable = PawnRecalculateItemValuesIfNecessary(Item)
		if not ValuesTable then return end
		
		-- The scale values are sorted alphabetically, so we need to go through the list.
		local Count = #ValuesTable
		for i = 1, Count do
			local Value = ValuesTable[i]
			if Value[1] == ScaleName then
				return Value[2], Value[3]
			end
		end
		
		-- If we didn't find a value, it's because this item doesn't have a value for this scale.
		return 0, 0
	end
	
	-- If this scale isn't enabled, just calculate it as a one-off.
	local Value, UnenchantedValue
	Value = PawnGetItemValue(Item.Stats, Item.Level, Item.SocketBonusStats, ScaleName, false, false)
	UnenchantedValue = PawnGetItemValue(Item.UnenchantedStats, Item.Level, Item.UnenchantedSocketBonusStats, ScaleName, false, false)
	return Value, UnenchantedValue
end

local ItemLevelSearchPattern1 = gsub(ITEM_LEVEL, "%%d", "(%%d+)")
local ItemLevelSearchPattern2 = gsub(ITEM_LEVEL_PLUS, "%%d%+", "(%%d+)%%+")

-- Updates a specific tooltip.
function PawnUpdateTooltip(TooltipName, MethodName, Param1, ...)
	if not PawnCommon.Scales then return end
	
	local Tooltip = _G[TooltipName]
	if not Tooltip then
		VgerCore.Fail("Where'd the tooltip go?  I seem to have misplaced it.")
		return
	end

	-- Start by getting the item link.
	local ItemLink = PawnGetItemLinkFromTooltip(TooltipName, MethodName, Param1, ...)

	-- Then get the item or relic data.
	local Item, IsRelic
	if ItemLink then
		if PawnCanItemBeArtifactUpgrade(ItemLink) then
			-- This is a relic item, so we just get upgrade info directly.
			IsRelic = true
		else
			-- This is the normal case: a normal item for which we have an item link.
			Item = PawnGetItemData(ItemLink)
		end
	else
		-- If there was no item link, try the degenerate case of trying to read directly from the tooltip instead of using our special parsing tooltip.
		Item = PawnGetItemDataFromTooltip(TooltipName, MethodName, Param1, ...)
	end

	-- If there's no item data, then something failed, so we can't update this tooltip, except to show item IDs.
	local TooltipWasUpdated, UpgradeInfo, ItemLevelIncrease, BestItemFor, SecondBestItemFor, NeedsEnhancements

	if Item then
		-- If this is a regular item, do the regular calculations to see if it's an upgrade.
		if PawnCommon.ShowUpgradesOnTooltips then UpgradeInfo, ItemLevelIncrease, BestItemFor, SecondBestItemFor, NeedsEnhancements = PawnIsItemAnUpgrade(Item) end
		
		-- If this is the main GameTooltip, remember the item that was hovered over.
		-- AtlasLoot compatibility: enable hover comparison for AtlasLoot tooltips too.
		if ItemLink and TooltipName == "GameTooltip" or TooltipName == "AtlasLootTooltip" or TooltipName == "WorldMapTooltipTooltip" then -- "TooltipTooltip" isn't a typo; it's an embedded tooltip
			PawnLastHoveredItem = Item.Link
		end
	elseif IsRelic then
		-- If this is a relic, we use a special relic-only codepath for this.
		if not PawnIsHoveringSocketedRelic then
			if PawnCommon.ShowRelicUpgrades then UpgradeInfo = PawnGetRelicUpgradeInfo(ItemLink) end
		end
	end
		
	-- If necessary, add a blank line to the tooltip.
	if
		(Item and not PawnCommon.ShowValuesForUpgradesOnly and #Item.Values > 0) or
		(Item and PawnCommon.ShowUpgradesOnTooltips and (UpgradeInfo or BestItemFor or SecondBestItemFor))
	then
		Tooltip:AddLine(" ")
	end

	local Annotated, TooltipWasUpdated

	if Item then
		-- Add the scale values and upgrade info to the tooltip.
		PawnAddValuesToTooltip(Tooltip, Item.Values, UpgradeInfo, BestItemFor, SecondBestItemFor, NeedsEnhancements, Item.InvType)

		local Annotated
		if Item.UnknownLines and #Item.Values > 0 then
			Annotated = PawnAnnotateTooltipLines(TooltipName, Item.UnknownLines)
		end

		-- Add the item level info to the tooltip.
		if ItemLevelIncrease and PawnCommon.ShowItemLevelUpgrades then
			-- Find which line of the tooltip (2-4) contains the text "Item Level" and annotate that.
			local i, AnnotatedItemLevel
			for i = 2, 5 do
				local LeftLine = _G[TooltipName .. "TextLeft" .. i]
				if LeftLine then
					local LeftLineText = LeftLine:GetText()
					if LeftLineText and LeftLineText ~= "" and (strfind(LeftLineText, ItemLevelSearchPattern1) or strfind(LeftLineText, ItemLevelSearchPattern2)) then
						-- This is the line.  Add an arrow to the end.
						AnnotatedItemLevel = true
						LeftLine:SetText(LeftLineText .. "  |TInterface\\AddOns\\Pawn\\Textures\\UpgradeArrow:0|t|cff00ff00+" .. ItemLevelIncrease)
					end
				end
			end
			if not AnnotatedItemLevel then
				--VgerCore.Assert(AnnotatedItemLevel, "This is an item level upgrade, but we didn't find it in the tooltip.")
				PawnAddTooltipLine(Tooltip, PawnLocal.ItemLevelTooltipLine .. ":  |TInterface\\AddOns\\Pawn\\Textures\\UpgradeArrow:0|t|cff00ff00+" .. ItemLevelIncrease, VgerCore.Color.OrangeR, VgerCore.Color.OrangeG, VgerCore.Color.OrangeB)
			end
			TooltipWasUpdated = true
		end

		TooltipWasUpdated = true
	elseif IsRelic then
		-- Add relic upgrade info to the tooltip.
		PawnAddRelicUpgradesToTooltip(TooltipName, UpgradeInfo)
		TooltipWasUpdated = true
	end

	-- Color or reset the tooltip border as necessary.
	if PawnCommon.ColorTooltipBorder then
		if UpgradeInfo or (ItemLevelIncrease and PawnCommon.ShowItemLevelUpgrades) then Tooltip:SetBackdropBorderColor(0, 1, 0) else Tooltip:SetBackdropBorderColor(1, 1, 1) end
	end
	
	-- Add the item ID to the tooltip if known.
	if PawnCommon.ShowItemID and ItemLink then
		local IDs = PawnGetItemIDsForDisplay(ItemLink)
		if IDs then
			PawnAddTooltipLine(Tooltip, PawnLocal.ItemIDTooltipLine .. ":  " .. IDs, VgerCore.Color.OrangeR, VgerCore.Color.OrangeG, VgerCore.Color.OrangeB)
			TooltipWasUpdated = true
		end
	end
	
	-- Show the updated tooltip.	
	if TooltipWasUpdated then Tooltip:Show() end

	if Item and PawnCommon.DebugDoubleTooltips and TooltipName == "GameTooltip" then
		VgerCore.Message("===== Annotating " .. TooltipName .. " for " .. tostring(Item.Name) .. ": =====")
		VgerCore.Message(debugstack(5))
	end
end

-- Returns a sorted list of all scale values for an item (and its unenchanted version, if supplied).
-- Parameters:
-- 	Item: A table of item stats in the format returned by GetStatsFromTooltip.
--	ItemLevel: The item's level.
-- 	SocketBonus: A table of socket bonus values in the format returned by GetStatsFromTooltip.
-- 	UnenchantedItem: A table of unenchanted item values in the format returned by GetStatsFromTooltip.
-- 	UnenchantedItemSocketBonus: A table of unenchanted item socket bonuses in the format returned by GetStatsFromTooltip.
--	DebugMessages: If true, debug messages will be printed.
--	NoNormalization: If true, the user's normalization factor will be ignored.
-- Return value: ItemValues
-- 	ItemValues: A sorted table of scale values in the following format: { {"Scale 1", 100, 90, ...}, {"\"Provider\":Scale2", 200, 175, ...} }.
--	Values for scales that are not currently enabled are not included.
function PawnGetAllItemValues(Item, ItemLevel, SocketBonus, UnenchantedItem, UnenchantedItemSocketBonus, DebugMessages, NoNormalization)
	local ItemValues = {}
	for ScaleName, Scale in pairs(PawnCommon.Scales) do
		local ShowScale = PawnIsScaleVisible(ScaleName)
		if ShowScale then -- Skip all disabled scales.  PawnGetSingleValueFromItem will calculate them on-demand if necessary.
			if ShowScale and DebugMessages then
				PawnDebugMessage(" ")
				PawnDebugMessage(PawnGetScaleLocalizedName(ScaleName) .. " --------------------")
			end
			local Value
			local UnenchantedValue
			if UnenchantedItem then
				UnenchantedValue = PawnGetItemValue(UnenchantedItem, ItemLevel, UnenchantedItemSocketBonus, ScaleName, ShowScale and DebugMessages, NoNormalization)
			end
			if Item then
				if ShowScale and DebugMessages and PawnCommon.ShowEnchanted then
					PawnDebugMessage(" ")
					PawnDebugMessage(PawnLocal.EnchantedStatsHeader)
				end
				Value = PawnGetItemValue(Item, ItemLevel, SocketBonus, ScaleName, ShowScale and DebugMessages and PawnCommon.ShowEnchanted, NoNormalization)
			end
			
			-- Add these values to the table.
			if Value == nil then Value = 0 end
			if UnenchantedValue == nil then UnenchantedValue = 0 end
			if Value > 0 or UnenchantedValue > 0 then
				tinsert(ItemValues, {ScaleName, Value, UnenchantedValue, PawnGetScaleLocalizedName(ScaleName)})
			end
		end
	end
	
	-- Sort the table, then return it.
	sort(ItemValues, PawnItemValueCompare)
	return ItemValues
end

-- Adds an array of item values to a tooltip, handling formatting options.
-- Parameters: Tooltip, ItemValues, UpgradeInfo, BestItemFor, SecondBestItemFor, InvType, OnlyFirstValue
-- 	Tooltip: The tooltip to annotate.  (Not a name.)
-- 	ItemValues: An array of item values to use to annotate the tooltip, in the format returned by PawnGetAllItemValues.
--	UpgradeInfo: An array of item upgrade information, in the format returned by PawnIsItemAnUpgrade.
--	BestItemFor, SecondBestItemFor: A table of scales for which this is the best or second-best item available, in the format returned by PawnIsItemAnUpgrade.
-- 	NeedsEnhancements: True if the item needs enhancements.
--	InvType: Optionally, the type of item this is.
--	OnlyFirstValue: If true, only the first value (the "enchanted" one) is used, regardless of the user's settings.
function PawnAddValuesToTooltip(Tooltip, ItemValues, UpgradeInfo, BestItemFor, SecondBestItemFor, NeedsEnhancements, InvType, OnlyFirstValue)
	-- First, check input arguments.
	if type(Tooltip) ~= "table" then
		VgerCore.Fail("Tooltip must be a valid tooltip, not '" .. type(Tooltip) .. "'.")
		return
	end
	if not ItemValues then return end

	-- Loop through all of the item value subtables.
	local _, _, ClassID = UnitClass("player")
	local Entry, _
	for _, Entry in pairs(ItemValues) do
		local ScaleName, Value, UnenchantedValue, LocalizedName = Entry[1], Entry[2], Entry[3], Entry[4]
		local Scale = PawnCommon.Scales[ScaleName]
		VgerCore.Assert(Scale ~= nil, "Scale name in item value list doesn't exist!")
		
		if PawnIsScaleVisible(ScaleName) then
			-- Ignore values that we don't want to display.
			if OnlyFirstValue then
				UnenchantedValue = 0
			else
				if not PawnCommon.ShowEnchanted then Value = 0 end
			end

			-- Override the localized name if the scale was designed for only the current class, and it's not a user scale.
			if Scale.ClassID == ClassID and Scale.SpecID and Scale.Provider then
				local _, LocalizedSpecName = GetSpecializationInfoForClassID(ClassID, Scale.SpecID)
				LocalizedName = LocalizedSpecName
			end
			-- Add the spec icon if present, and if that feature isn't disabled.
			if PawnCommon.ShowSpecIcons and Scale.IconTexturePath then
				LocalizedName = "|T" .. Scale.IconTexturePath .. ":0|t " .. LocalizedName
			end

			local TooltipText = nil
			local TextColor = PawnGetScaleColor(ScaleName)
			local UnenchantedTextColor = PawnGetScaleColor(ScaleName, true)
			
			if PawnCommon.ShowValuesForUpgradesOnly then
				TooltipText = format(PawnNoValueAnnotationFormat, TextColor, LocalizedName)
			elseif Value and Value > 0 and UnenchantedValue and UnenchantedValue > 0 and math.abs(Value - UnenchantedValue) >= ((10 ^ -PawnCommon.Digits) / 2) then
				TooltipText = format(PawnEnchantedAnnotationFormat, TextColor, LocalizedName, tostring(Value), UnenchantedTextColor, tostring(UnenchantedValue))
			elseif Value and Value > 0 then
				TooltipText = format(PawnUnenchantedAnnotationFormat, TextColor, LocalizedName, tostring(Value))
			elseif UnenchantedValue and UnenchantedValue > 0 then
				TooltipText = format(PawnUnenchantedAnnotationFormat, TextColor, LocalizedName, tostring(UnenchantedValue))
			end
			
			-- Add info to the tooltip if this item is an upgrade or best-in-slot.
			local ThisUpgrade, _
			WasUpgradeOrBest = false
			if UpgradeInfo then
				for _, ThisUpgrade in pairs(UpgradeInfo) do
					if ThisUpgrade.ScaleName == ScaleName then
						if ThisUpgrade.PercentUpgrade >= PawnBigUpgradeThreshold then -- 100 = 10,000%
							-- For particularly huge upgrades, don't say ridiculous things like "999999999% upgrade"
							TooltipText = format(PawnLocal.TooltipBigUpgradeAnnotation, TooltipText, "")
						elseif NeedsEnhancements then
							TooltipText = format(PawnLocal.TooltipUpgradeNeedsEnhancementsAnnotation, TooltipText, 100 * ThisUpgrade.PercentUpgrade, "")
						else
							TooltipText = format(PawnLocal.TooltipUpgradeAnnotation, TooltipText, 100 * ThisUpgrade.PercentUpgrade, "")
						end
						WasUpgradeOrBest = true
						break
					end
				end
			elseif BestItemFor and BestItemFor[ScaleName] then
				WasUpgradeOrBest = true
				if PawnCommon.ShowValuesForUpgradesOnly then
					TooltipText = format(PawnLocal.TooltipBestAnnotationSimple, TooltipText)
				else
					TooltipText = format(PawnLocal.TooltipBestAnnotation, TooltipText)
				end
			elseif SecondBestItemFor and SecondBestItemFor[ScaleName] then
				WasUpgradeOrBest = true
				if PawnCommon.ShowValuesForUpgradesOnly then
					TooltipText = format(PawnLocal.TooltipSecondBestAnnotationSimple, TooltipText)
				else
					TooltipText = format(PawnLocal.TooltipSecondBestAnnotation, TooltipText)
				end
			end
			if not WasUpgradeOrBest and PawnCommon.ShowValuesForUpgradesOnly then TooltipText = nil end
			
			PawnAddTooltipLine(Tooltip, TooltipText)
		end
	end

end

function PawnAddTooltipLine(Tooltip, Text, r, g, b)
	if not Text then return end
	VgerCore.Assert(Tooltip, "PawnAddTooltipLine: Tooltip must not be nil")

	-- This could be optimized a bit, but it's not incredibly necessary.
	if PawnCommon.AlignNumbersRight then
		local Pos = VgerCore.StringFindReverse(Text, ": ") -- search for a colon followed by a space so we don't get texture escape sequences
		local Left = strsub(Text, 0, Pos - 1) -- ignore the colon
		local Right = strsub(Text, 0, 10) .. strsub(Text, Pos + 3) -- add the color string and ignore the spaces following the colon
		Tooltip:AddDoubleLine(Left, Right, r, g, b)
	else
		Tooltip:AddLine(Text, r, g, b)
	end
end

-- Returns the total scale values of all equipped items.  Only counts enchanted values.  Returns nil if item information
-- is unavailable for any item currently equipped.
-- Parameters: UnitName
--		UnitName: The name of the unit from whom the inventory item should be retrieved.  Defaults to "player".
-- Return value: ItemValues, Count, EpicItemLevel, AverageItemLevel
-- 		ItemValues: Same as PawnGetAllItemValues, or nil if unsuccessful.
--		Count: The number of item values calculated.
--		AverageItemLevel: An average item level for all equipped items.
function PawnGetInventoryItemValues(UnitName)
	local Total = {}
	local TotalItemLevel = 0
	local SlotStats
	local Slot
	local _
	local MainHandArtifactLevel
	for Slot = 1, 17 do
		if Slot ~= 4 then -- Skip slots 0, 4, 18, and 19 (they're not gear).
			local ItemID = GetInventoryItemID(UnitName, Slot)
			local Item = PawnGetItemDataForInventorySlot(Slot, false, UnitName)
			if Item then
				ItemValues = PawnGetAllItemValues(Item.Stats, Item.Level, Item.SocketBonusStats)
				-- Add the item's level to our running total.  If it's a 2H weapon AND the off-hand slot is empty, double its value.
				-- (We can't assume that the off-hand is empty just because the main hand slot contains a 2H weapon... stupid
				-- Titan's Grip warriors.)
				local ThisItemLevel = Item.Level
				if not ThisItemLevel then return end -- If we have item information but no level, bail out rather than return inaccurate totals.
				if Slot == 16 then
					local _, _, _, _, _, _, _, _, InvType = GetItemInfo(GetInventoryItemLink(UnitName, Slot))
					if (InvType == "INVTYPE_2HWEAPON" or InvType == "INVTYPE_RANGED" or InvType == "INVTYPE_RANGEDRIGHT") and GetInventoryItemID(UnitName, 17) == nil then
						-- Some ranged weapons are now two-handed too.  If they're using a ranged weapon with no off-hand, count it as a two-hander.
						ThisItemLevel = ThisItemLevel * 2
					end
					if Item.Rarity == 6 then MainHandArtifactLevel = Item.Level end
				elseif Slot == 17 and MainHandArtifactLevel then
					-- If we're checking the off-hand slot and the main-hand slot contained an artifact, use that item level instead of
					-- what the game reports for the off-hand, because the off-hand appears as level 750 when inspecting someone, sigh.
					-- (Bug introduced in 7.0; still present as of 7.1.5.)
					ThisItemLevel = MainHandArtifactLevel
				end
				if ThisItemLevel then
					TotalItemLevel = TotalItemLevel + ThisItemLevel
				end
				-- Now, add these values to our running totals.
				for _, Entry in pairs(ItemValues) do
					local ScaleName, Value = Entry[1], Entry[2]
					PawnAddStatToTable(Total, ScaleName, Value) -- (not actually stats, but the function does what we want)
				end
			elseif ItemID then
				-- If we have an item link but no item data, then the player HAS an item in that slot but we don't have data.
				-- So we should just bail out now to avoid reporting inaccurate totals.  BUT, we should go ahead and query for
				-- information on the other items before we do so that we have everything by the next time this is called.
				while Slot <= 17 do
					Item = PawnGetItemDataForInventorySlot(Slot, false, UnitName)
					Slot = Slot + 1
				end
				return
			end
		end
	end
	-- Once we're done, we need to convert our addition table to one that we can return.
	local TotalValues = {}
	local Count = 0
	for ScaleName, Value in pairs(Total) do
		tinsert(TotalValues, { ScaleName, Value, 0, PawnGetScaleLocalizedName(ScaleName) })
		Count = Count + 1
	end
	sort(TotalValues, PawnItemValueCompare)
	-- Return our totals.
	TotalItemLevel = math.floor(TotalItemLevel / 16 + .05)
	return TotalValues, Count, TotalItemLevel
end

-- Works around annoying inconsistencies in the way that Blizzard formats tooltip text.
-- Enchantments and random item properties ("of the whale") are formatted like this: "|cffffffff+15 Intellect|r\r\n".
-- We correct this here.
function PawnFixStupidTooltipFormatting(TooltipName)
	local Tooltip = _G[TooltipName]
	if not Tooltip then return end
	local i
	for i = 1, Tooltip:NumLines() do
		local LeftLine = _G[TooltipName .. "TextLeft" .. i]
		local Text = LeftLine:GetText()
		local Updated = false
		if Text and strfind(Text, "\n", 1, true) ~= 1 then
			-- First, look for a color.
			if strfind(Text, "|cffffffff", 1, true) == 1 then
				Text = strsub(Text, 11)
				LeftLine:SetTextColor(1, 1, 1)
				Updated = true
			end
			-- Then, look for a trailing \r\n, unless that's all that's left of the string.
			if (strlen(Text) > 2) and (strbyte(Text, -1) == 10) then
				Text = strsub(Text, 1, -4)
				Updated = true
			end
			-- Then, look for a trailing color restoration flag.
			if strfind(Text, "|r", strlen(Text) - 1, true) then
				Text = strsub(Text, 1, -3)
				Updated = true
			end
			-- Update the tooltip with the new string.
			if Updated then
				--VgerCore.Message("Old: [" .. PawnEscapeString(LeftLine:GetText()) .. "]")
				LeftLine:SetText(Text)
				--VgerCore.Message("New: [" .. PawnEscapeString(Text) .. "]")
			end
		end
	end
end

-- Calls a method on a tooltip and then returns stats from that tooltip.
-- Parameters: TooltipName, DebugMessages, Method
--		TooltipName: The name of the tooltip to use.
--		DebugMessages: If true, debug messages will be shown.
--		Method: The name of the method to call on the tooltip, followed optionally by arguments to that method.
-- Return value: Same as PawnGetStatsFromTooltip, or nil if unsuccessful.
function PawnGetStatsFromTooltipWithMethod(TooltipName, DebugMessages, MethodName, ...)
	if not TooltipName or not MethodName then
		VgerCore.Fail("PawnGetStatsFromTooltipWithMethod requires a valid tooltip name and method name.")
		return
	end
	local Tooltip = _G[TooltipName]
	Tooltip:ClearLines() -- Without ClearLines, sometimes SetHyperlink seems to fail when called rapidly
	if TooltipName == PawnPrivateTooltipName then
		-- Pawn 1.9.18: Curse user Bodar suggests reanchoring the tooltip to ensure that future updates work
		Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
	end
	local Method = Tooltip[MethodName]
	Method(Tooltip, ...)
	PawnFixStupidTooltipFormatting(TooltipName)
	return PawnGetStatsFromTooltip(TooltipName, DebugMessages)
end

-- Reads the stats for a given item ID, eventually calling PawnGetStatsFromTooltip.
-- Parameters: ItemID, DebugMessages
--		ItemID: The item ID for which to get stats.
--		DebugMessages: If true, debug messages will be shown.
-- Return value: Same as PawnGetStatsFromTooltip, or nil if unsuccessful.
function PawnGetStatsForItemID(ItemID, DebugMessages)
	if not ItemID then
		VgerCore.Fail("PawnGetStatsForItemID requires a valid item ID.")
		return
	end
	return PawnGetStatsForItemLink("item:" .. ItemID, DebugMessages)
end

-- Reads the stats for a given item link, eventually calling PawnGetStatsFromTooltip.
-- Parameters: ItemLink, DebugMessages
--		ItemLink: The item link for which to get stats.
--		DebugMessages: If true, debug messages will be shown.
-- Return value: Same as PawnGetStatsFromTooltip, or nil if unsuccessful.
function PawnGetStatsForItemLink(ItemLink, DebugMessages)
	if not ItemLink then
		VgerCore.Fail("PawnGetStatsForItemLink requires a valid item link.")
		return
	end
	-- Other types of hyperlinks, such as enchant, quest, or spell are ignored by Pawn.
	if PawnGetHyperlinkType(ItemLink) ~= "item" then return end
	
	local Tooltip = _G[PawnPrivateTooltipName]
	Tooltip:ClearLines() -- Without this, sometimes SetHyperlink seems to fail when called rapidly
	Tooltip:SetHyperlink(ItemLink)
	PawnFixStupidTooltipFormatting(PawnPrivateTooltipName)
	return PawnGetStatsFromTooltip(PawnPrivateTooltipName, DebugMessages)
end

-- Returns the stats of an equipped item, eventually calling PawnGetStatsFromTooltip.
-- 	Parameters: Slot
-- 		Slot: The slot number (0-19).  If not looping through all slots, use GetInventorySlotInfo("HeadSlot") to get the number.
--		DebugMessages: If true, debug messages will be shown.
--		UnitName: The name of the unit from whom the inventory item should be retrieved.  Defaults to "player".
-- Return value: Same as PawnGetStatsFromTooltip, or nil if unsuccessful.
function PawnGetStatsForInventorySlot(Slot, DebugMessages, UnitName)
	if type(Slot) ~= "number" then
		VgerCore.Fail("PawnGetStatsForInventorySlot requires a valid slot number.  Did you mean to use GetInventorySlotInfo to get a number?")
		return
	end
	if not UnitName then UnitName = "player" end
	return PawnGetStatsFromTooltipWithMethod(PawnPrivateTooltipName, DebugMessages, "SetInventoryItem", UnitName, Slot)
end

-- Reads the stats from a tooltip.
-- Returns a table mapping stat name with a quantity of that statistic.
-- For example, ReturnValue["Strength"] = 12.
-- Parameters: TooltipName, DebugMessages
--		TooltipName: The tooltip to read.
--		DebugMessages: If true (default), debug messages will be shown.
-- Return value: Stats, UnknownLines
--		Stats: The table of stats for the item.
--		SocketBonusStats: The table of stats for the item's socket bonus.
--		UnknownLines: A list of lines in the tooltip that were not understood.
--		PrettyLink: A beautified item link, if available.
function PawnGetStatsFromTooltip(TooltipName, DebugMessages)
	local Tooltip = _G[TooltipName]
	if DebugMessages == nil then DebugMessages = true end
	
	-- Get the item name.  It could be on line 2 if the first line is "Currently Equipped".
	local ItemName, ItemNameLineNumber = PawnGetItemNameFromTooltip(TooltipName)
	if (not ItemName) or (not ItemNameLineNumber) then
		if PawnCommon.DebugCache then VgerCore.Message("Pawn debug cache: PawnGetStatsFromTooltip exiting because the item in " .. TooltipName .. " had no name") end
		return
	end

	-- Now, read the tooltip for stats.
	local Stats, SocketBonusStats, UnknownLines = {}, {}, {}
	local HadUnknown = false
	local SocketBonusIsValid = false

	for i = ItemNameLineNumber + 1, Tooltip:NumLines() do
		local LeftLine = _G[TooltipName .. "TextLeft" .. i]
		local LeftLineText = LeftLine:GetText()
		if not LeftLineText then break end
		
		-- Look for this line in the "kill lines" list.  If it's there, we're done.
		local IsKillLine = false
		-- Dirty, dirty hack for artifacts: check the color of the text; if it's artifact gold and it's not at the beginning of the tooltip, then treat it as a kill line.
		if i > ItemNameLineNumber + 2 and strfind(LeftLineText, "|cFFE6CC80", 1, true) == 1 then
			IsKillLine = true
		end
		if not IsKillLine then
			local ThisKillLine
			for _, ThisKillLine in pairs(PawnKillLines) do
				if strfind(LeftLineText, ThisKillLine) then
					-- This is a known ignored kill line; stop now.
					IsKillLine = true
					break
				end
			end
		end
		if IsKillLine then break end
		
		for Side = 1, 2 do
			local CurrentParseText, RegexTable, CurrentDebugMessages, IgnoreErrors
			if Side == 1 then
				CurrentParseText = LeftLineText
				RegexTable = PawnRegexes
				CurrentDebugMessages = DebugMessages
				IgnoreErrors = false
			else
				local RightLine = _G[TooltipName .. "TextRight" .. i]
				CurrentParseText = RightLine:GetText()
				if (not CurrentParseText) or (CurrentParseText == "") then break end
				RegexTable = PawnRightHandRegexes
				CurrentDebugMessages = false
				IgnoreErrors = true
			end
			
			local ThisLineIsSocketBonus = false
			if Side == 1 and strfind(CurrentParseText, PawnLocal.TooltipParsing.SocketBonusPrefix, 1, true) then
				-- This line is the socket bonus.
				ThisLineIsSocketBonus = true
				if LeftLine.GetTextColor then
					SocketBonusIsValid = (LeftLine:GetTextColor() == 0) -- green's red component is 0, but grey's red component is .5	
				else
					PawnDebugMessage(VgerCore.Color.Blue .. "Failed to determine whether socket bonus was valid, so assuming that it is indeed valid.")
					SocketBonusIsValid = true
				end
				CurrentParseText = strsub(CurrentParseText, strlen(PawnLocal.TooltipParsing.SocketBonusPrefix) + 1)
			end
			
			local Understood
			if ThisLineIsSocketBonus then
				Understood = PawnLookForSingleStat(RegexTable, SocketBonusStats, CurrentParseText, CurrentDebugMessages)
			else
				Understood = PawnLookForSingleStat(RegexTable, Stats, CurrentParseText, CurrentDebugMessages)
			end
			
			if not Understood then
				-- We don't understand this line.  Let's see if it's a complex stat.
				
				-- First, check to see if it starts with any of the ignore prefixes, such as "Use:".
				local IgnoreLine = false
				local ThisPrefix
				for _, ThisPrefix in pairs(PawnSeparatorIgnorePrefixes) do
					if strfind(CurrentParseText, ThisPrefix, 1, true) == 1 then
						-- We know that this line doesn't contain a complex stat, so ignore it.
						IgnoreLine = true
						if CurrentDebugMessages then PawnDebugMessage(VgerCore.Color.Blue .. format(PawnLocal.DidntUnderstandMessage, PawnEscapeString(CurrentParseText))) end
						if not Understood and not IgnoreErrors then HadUnknown = true UnknownLines[CurrentParseText] = 1 end
						break
					end
				end
				
				-- If this line wasn't ignorable, try to break it up.
				if not IgnoreLine then
					-- We'll assume the entire line was understood for now, but if we find any PART that
					-- we don't understand, we'll clear the "understood" flag again.
					Understood = true
					
					local Pos = 1
					local NextPos = 0
					local InnerStatLine = nil
					local InnerUnderstood = nil
					
					while Pos < strlen(CurrentParseText) do
						local ThisSeparator
						for _, ThisSeparator in pairs(PawnSeparators) do
							NextPos = strfind(CurrentParseText, ThisSeparator, Pos, false)
							if NextPos then
								-- One of the separators was found.  Check this string.
								InnerStatLine = strsub(CurrentParseText, Pos, NextPos - 1)
								if ThisLineIsSocketBonus then
									InnerUnderstood = PawnLookForSingleStat(RegexTable, SocketBonusStats, InnerStatLine, CurrentDebugMessages)
								else
									InnerUnderstood = PawnLookForSingleStat(RegexTable, Stats, InnerStatLine, CurrentDebugMessages)
								end
								if not InnerUnderstood then
									-- We don't understand this line.
									Understood = false
									if CurrentDebugMessages then PawnDebugMessage(VgerCore.Color.Blue .. format(PawnLocal.DidntUnderstandMessage, PawnEscapeString(InnerStatLine))) end
									if not Understood and not IgnoreErrors then HadUnknown = true UnknownLines[InnerStatLine] = 1 end
								end
								-- Regardless of the outcome, advance to the next position.
								Pos = NextPos + strlen(ThisSeparator)
								break
							end -- (if NextPos...)
							-- If we didn't find that separator, continue the for loop to try the next separator.
						end -- (for ThisSeparator...)
						if (Pos > 1) and (not NextPos) then
							-- If there are no more separators left in the string, but we did find one before that, then we have
							-- one last string to check: everything after the last separator.
							InnerStatLine = strsub(CurrentParseText, Pos)
							if ThisLineIsSocketBonus then
								InnerUnderstood = PawnLookForSingleStat(RegexTable, SocketBonusStats, InnerStatLine, CurrentDebugMessages)
							else
								InnerUnderstood = PawnLookForSingleStat(RegexTable, Stats, InnerStatLine, CurrentDebugMessages)
							end
							if not InnerUnderstood then
								-- We don't understand this line.
								Understood = false
								if CurrentDebugMessages then PawnDebugMessage(VgerCore.Color.Blue .. format(PawnLocal.DidntUnderstandMessage, PawnEscapeString(InnerStatLine))) end
								if not Understood and not IgnoreErrors then HadUnknown = true UnknownLines[InnerStatLine] = 1 end
							end
							break
						elseif not NextPos then
							-- If there are no more separators in the string and we hadn't found any before that, we're done.
							Understood = false
							if CurrentDebugMessages then PawnDebugMessage(VgerCore.Color.Blue .. format(PawnLocal.DidntUnderstandMessage, PawnEscapeString(CurrentParseText))) end
							if not Understood and not IgnoreErrors then HadUnknown = true UnknownLines[CurrentParseText] = 1 end
							break
						end 
						-- Continue on to the next portion of the string.  The loop ends when we run out of string.
					end -- (while Pos...)
				end -- (if not IgnoreLine...)
			end
		end
	end

	-- Before returning, some stats require special handling.

	if Stats["IsRanged"] and Stats["IsRanged"] > 1 then
		-- Fix for non-Russian locales: INVTYPE_RANGED and INVTYPE_RANGEDRIGHT evaluate to the same thing, so don't count
		-- IsRanged for double.  (In Russian, they're translated differently.)
		Stats["IsRanged"] = 1
	end
	
	if Stats["IsMainHand"] or Stats["IsOneHand"] or Stats["IsOffHand"] or Stats["IsTwoHand"] or Stats["IsRanged"] then
		-- Only perform this conversion if this is an actual weapon.  This works around a problem that occurs when you
		-- enchant your ring with weapon damage and then Pawn would try to calculate DPS for your ring with no Min/MaxDamage.
		local Min = Stats["MinDamage"]
		if not Min then Min = 0 end
		local Max = Stats["MaxDamage"]
		if not Max then Max = 0 end
		if (Min > 0 or Max > 0) and Stats["Speed"] then
			-- Convert damage to DPS if *either* minimum or maximum damage is present.  (A few annoying items
			-- like the Brewfest steins have only max damage.)
			PawnAddStatToTable(Stats, "Dps", (Min + Max) / Stats["Speed"] / 2)
		else
			local WeaponStats = 0
			if Stats["MinDamage"] then WeaponStats = WeaponStats + 1 end
			if Stats["MaxDamage"] then WeaponStats = WeaponStats + 1 end
			if Stats["Speed"] then WeaponStats = WeaponStats + 1 end
			VgerCore.Assert(WeaponStats == 0 or WeaponStats == 3, "Pawn couldn't read speed and damage stats from " .. ItemName .. "; translation problem?")
		end
	end
	
	if Stats["IsMainHand"] then
		PawnAddStatToTable(Stats, "MainHandDps", Stats["Dps"])
		PawnAddStatToTable(Stats, "MainHandSpeed", Stats["Speed"])
		PawnAddStatToTable(Stats, "MainHandMinDamage", Stats["MinDamage"])
		PawnAddStatToTable(Stats, "MainHandMaxDamage", Stats["MaxDamage"])
		PawnAddStatToTable(Stats, "IsMelee", 1)
		Stats["IsMainHand"] = nil
	end

	if Stats["IsShield"] then
		-- Shields aren't off-hand weapons.
		Stats["IsOffHand"] = nil
	end
	if Stats["IsOffHand"] then
		PawnAddStatToTable(Stats, "OffHandDps", Stats["Dps"])
		PawnAddStatToTable(Stats, "OffHandSpeed", Stats["Speed"])
		PawnAddStatToTable(Stats, "OffHandMinDamage", Stats["MinDamage"])
		PawnAddStatToTable(Stats, "OffHandMaxDamage", Stats["MaxDamage"])
		PawnAddStatToTable(Stats, "IsMelee", 1)
	end

	if Stats["IsOneHand"] then
		PawnAddStatToTable(Stats, "OneHandDps", Stats["Dps"])
		PawnAddStatToTable(Stats, "OneHandSpeed", Stats["Speed"])
		PawnAddStatToTable(Stats, "OneHandMinDamage", Stats["MinDamage"])
		PawnAddStatToTable(Stats, "OneHandMaxDamage", Stats["MaxDamage"])
		PawnAddStatToTable(Stats, "IsMelee", 1)
		Stats["IsOneHand"] = nil
	end

	if Stats["IsTwoHand"] then
		PawnAddStatToTable(Stats, "TwoHandDps", Stats["Dps"])
		PawnAddStatToTable(Stats, "TwoHandSpeed", Stats["Speed"])
		PawnAddStatToTable(Stats, "TwoHandMinDamage", Stats["MinDamage"])
		PawnAddStatToTable(Stats, "TwoHandMaxDamage", Stats["MaxDamage"])
		PawnAddStatToTable(Stats, "IsMelee", 1)
		-- Also need to convert weapon stats for two-handed weapons, since two-handed appears on the left and weapon type appears on the right.
		PawnAddStatToTable(Stats, "Is2HAxe", Stats["IsAxe"])
		Stats["IsAxe"] = nil
		PawnAddStatToTable(Stats, "Is2HMace", Stats["IsMace"])
		Stats["IsMace"] = nil
		PawnAddStatToTable(Stats, "Is2HSword", Stats["IsSword"])
		Stats["IsSword"] = nil

		Stats["IsTwoHand"] = nil
	end

	if Stats["IsMelee"] and Stats["IsRanged"] then
		VgerCore.Fail("Weapon that is both melee and ranged was converted to both Melee* and Ranged* stats")
	end	
	
	if Stats["IsMelee"] then
		PawnAddStatToTable(Stats, "MeleeDps", Stats["Dps"])
		PawnAddStatToTable(Stats, "MeleeSpeed", Stats["Speed"])
		PawnAddStatToTable(Stats, "MeleeMinDamage", Stats["MinDamage"])
		PawnAddStatToTable(Stats, "MeleeMaxDamage", Stats["MaxDamage"])
		Stats["IsMelee"] = nil
	end

	if Stats["IsRanged"] then
		PawnAddStatToTable(Stats, "RangedDps", Stats["Dps"])
		PawnAddStatToTable(Stats, "RangedSpeed", Stats["Speed"])
		PawnAddStatToTable(Stats, "RangedMinDamage", Stats["MinDamage"])
		PawnAddStatToTable(Stats, "RangedMaxDamage", Stats["MaxDamage"])
		Stats["IsRanged"] = nil
	end

	-- Now, socket bonuses require special handling.
	if SocketBonusIsValid then
		-- If the socket bonus is valid (green), then just add those stats directly to the main stats table and be done with it.
		PawnAddStatsToTable(Stats, SocketBonusStats)
		SocketBonusStats = {}
	else
		-- If the socket bonus is not valid, then we need to check for sockets.
		if Stats["PrismaticSocket"] then
			-- There are sockets left, so the player could still meet the requirements.
		else
			-- There are no sockets left and the socket bonus requirements were not met.  Ignore the
			-- socket bonus, since the user purposely chose to mis-socket.
			SocketBonusStats = {}
		end
	end

	-- Done!
	local _, PrettyLink = Tooltip:GetItem()
	if not HadUnknown then UnknownLines = nil end
	return Stats, SocketBonusStats, UnknownLines, PrettyLink
end

-- Looks for a single string in the regex table, and adds it to the stats table if it finds it.
-- Parameters: Stats, ThisString, DebugMessages
--		RegexTable: The regular expression table to look through.
--		Stats: The stats table to modify if anything is found.
--		ThisString: The string to look for.
--		DebugMessages: If true, debug messages will be shown.
-- Return value: Understood
--		Understood: True if the string was understood (even if empty or ignored), otherwise false.
function PawnLookForSingleStat(RegexTable, Stats, ThisString, DebugMessages)
	-- First, perform a series of normalizations on the string.  For example, "Stamina +5" should
	-- be converted to "+5 Stamina" so we don't need two strings for everything.
	ThisString = strtrim(ThisString)
	local Entry, Count
	for _, Entry in pairs(PawnNormalizationRegexes) do
		local Regex, Replacement = unpack(Entry)
		local OldString = ThisString
		ThisString, Count = gsub(ThisString, Regex, Replacement, 1)
		--if Count > 0 then PawnDebugMessage("Normalized string using \"" .. PawnEscapeString(Regex) .. "\" -- was " .. PawnEscapeString(OldString) .. " and is now " .. PawnEscapeString(ThisString)) end
	end

	-- Now, look for the string in the main regex table.
	local Props, Matches = PawnFindStringInRegexTable(ThisString, RegexTable)
	if not Props then
		-- We don't understand this.  Return false to indicate this, so the caller can handle the case.
		return false
	else
		-- We understand this.  It could either be an ignored line like "Soulbound", or an actual stat.
		-- The same code handles both cases; just keep going until we find a stat of nil; in the ignored case, we hit this immediately.
		local Index = 2
		while true do
			local Stat, Number, Source = Props[Index], tonumber(Props[Index + 1]), Props[Index + 2]
			if not Stat then break end -- There are no more stats left to process.
			if not Number then Number = 1 end
			
			if Source == PawnMultipleStatsExtract or Source == PawnSingleStatMultiplier or Source == nil then
				-- This is a variable number of a stat, the standard case.
				local MatchIndex
				if Source == PawnMultipleStatsExtract then
					MatchIndex = math.abs(Number)
				else
					MatchIndex = 1
				end
				local ExtractedValue = Matches[MatchIndex]
				if not ExtractedValue then
					VgerCore.Fail("Didn't extract a value for " .. Stat .. ".  Is the translation missing a capture (#)?")
					ExtractedValue = 0
				end
				if Stat ~= "Speed" and (PawnLocal.ThousandsSeparator ~= "" or (PawnLocal.ThousandsSeparator == PawnLocal.DecimalSeparator)) then
					-- Skip this for Speed because Spanish uses the wrong character for speed, and speed would never be >=1,000
					-- In 7.0, Russian also used the comma for both thousands and decimal separators, so use the same logic then.
					-- Remove commas in numbers.  We need to use % in case it's a dot, and we need to 
					-- skip this entirely in case there's no large number separator at all (Spanish).
					ExtractedValue = gsub(ExtractedValue, "%" .. PawnLocal.ThousandsSeparator, "")
				end
				if PawnLocal.DecimalSeparator ~= "." then
					-- If this is the German client or any other version that uses something other than "." for
					-- the decimal separator, we need to substitute here, because tonumber() parses things
					-- in English format only.
					ExtractedValue = gsub(ExtractedValue, PawnLocal.DecimalSeparator, ".")
				end
				ExtractedValue = tonumber(ExtractedValue) -- broken onto multiple lines because gsub() returns multiple values and tonumber accepts multiple arguments
				if Number < 0 then ExtractedValue = -ExtractedValue end
				if Source == PawnSingleStatMultiplier then ExtractedValue = ExtractedValue * Number end
				if DebugMessages then PawnDebugMessage(format(PawnLocal.FoundStatMessage, ExtractedValue, Stat)) end
				PawnAddStatToTable(Stats, Stat, ExtractedValue)
			elseif Source == PawnMultipleStatsFixed then
				-- This is a fixed number of a stat, such as a socket (1).
				if DebugMessages then PawnDebugMessage(format(PawnLocal.FoundStatMessage, Number, Stat)) end
				PawnAddStatToTable(Stats, Stat, Number)
			else
				VgerCore.Fail("Incorrect source value of '" .. Source .. "' for regex: " .. Props[1])
			end
			
			Index = Index + 3
		end
	end

	return true
end

-- Gets the name of an item given a tooltip name, and the line on which the item appears.
-- Normally this is line 1, but it can be line 2 if the first line is "Currently Equipped".
-- Parameters: TooltipName
--		TooltipName: The name of the tooltip to read.
-- Return value: ItemName, LineNumber
--		ItemName: The name of the item in the tooltip, or nil if the tooltip didn't have one.
--		LineNumber: The line number on which the name was found, or nil if no item was found.
function PawnGetItemNameFromTooltip(TooltipName)
	-- First, get the tooltip details.
	local TooltipTopLine = _G[TooltipName .. "TextLeft1"]
	if not TooltipTopLine then return end
	local ItemName = TooltipTopLine:GetText()
	if not ItemName or ItemName == "" or ItemName == RETRIEVING_ITEM_INFO then return end
	
	-- If this is a Currently Equipped tooltip, skip the first line.
	if ItemName == CURRENTLY_EQUIPPED then
		ItemNameLineNumber = 2
		TooltipTopLine = _G[TooltipName .. "TextLeft2"]
		if not TooltipTopLine then return end
		return TooltipTopLine:GetText(), 2
	end
	return ItemName, 1
end

-- Annotates zero or more lines in a tooltip with the name TooltipName, adding a (?) to the end
-- of each line specified by index in the list Lines.
-- Returns true if any lines were annotated.
function PawnAnnotateTooltipLines(TooltipName, Lines)
	-- Temporarily disabling this feature to see if anyone misses it.
	if not PawnCommon.ShowAsterisks then return end

	if not Lines then return false end
	local Annotated = false
	local Tooltip = _G[TooltipName]
	local LineCount = Tooltip:NumLines()
	for i = 2, LineCount do
		local LeftLine = _G[TooltipName .. "TextLeft" .. i]
		if LeftLine then
			local LeftLineText = LeftLine:GetText()
			if Lines[LeftLineText] then
				-- Getting the line text can fail in the following scenario, observable with MobInfo-2:
				-- 1. Other mod modifies a tooltip to include unrecognized text.
				-- 2. Pawn reads the tooltip, noting those unrecognized lines and remembering them so that they
				-- can get marked with (?) later.
				-- 3. Something causes the tooltip to be refreshed.  For example, picking up the item.  All customizations
				-- by Pawn and other mods are lost.
				-- 4. Pawn re-annotates the tooltip with (?) before the other mod has added the lines that are supposed
				-- to get the (?).
				-- In this case, we just ignore the problem and leave off the (?), since we can't really come back later.
				LeftLine:SetText(LeftLineText .. PawnTooltipAnnotation)
				Annotated = true
			end
		end
	end
	return Annotated
end

-- Adds an amount of one stat to a table of stats, increasing the value if
-- it's already there, or adding it if it isn't.
function PawnAddStatToTable(Stats, Stat, Amount)
	if not Amount or Amount == 0 then return end
	if Stats[Stat] then
		Stats[Stat] = Stats[Stat] + Amount
	else
		Stats[Stat] = Amount
	end
end

-- Adds the contents of one stat table to another.
function PawnAddStatsToTable(Dest, Source)
	if not Dest then
		VgerCore.Fail("PawnAddStatsToTable requires a destination table!")
		return
	end
	if not Source then return end
	for Stat, Quantity in pairs(Source) do
		PawnAddStatToTable(Dest, Stat, Quantity)
	end
end

local PawnFindStringInRegexTable_ShownOneError
-- Looks for the first regular expression in a given table that matches the given string.
-- Parameters: String, RegexTable
--		String: The string to look for.
--		RegexTable: The table of regular expressions to look through.
--	Return value: Props, Matches
--		Props: The row from the table with a matching regex.
--		Matches: The array of captured matches.
-- 		Returns nil, nil if no matches were found.
--		Returns {}, {} if the string was ignored.
function PawnFindStringInRegexTable(String, RegexTable)
	if (String == nil) or (String == "") or (String == " ") then return {}, {} end
	local Entry, LastRegex
	for _, Entry in pairs(RegexTable) do
		if Entry[1] == nil then
			if not PawnFindStringInRegexTable_ShownOneError then
				VgerCore.Fail("Localization error in regex table in the entry after \"" .. VgerCore.Color.Blue .. PawnEscapeString(tostring(LastRegex)) .. "|r\".")
				PawnFindStringInRegexTable_ShownOneError = true
			end
		else
			LastRegex = Entry[1]
			local StartPos, EndPos, m1, m2, m3, m4, m5 = strfind(String, LastRegex)
			if StartPos then return Entry, { m1, m2, m3, m4, m5 } end
		end
	end
	return nil, nil
end

-- Calculates the value of an item.
--	Parameters: Item, ItemLevel, SocketBonus, ScaleName, DebugMessages, NoNormalization
--		Item: Item stats in the format returned by GetStatsFromTooltip.
--		ItemLevel: The item's level.
--		SocketBonus: Socket bonus stats in the format returned by GetStatsFromTooltip.
--		ScaleName: The scale to use.
--		DebugMessages: If true, debug messages will be shown if appropriate.
--		NoNormalization: If true, the user's normalization factor will be ignored.
--	Returns: Value, TotalSocketValue
--		Value: The numeric value of an item based on the given scale values.  (example: 21.75)
--		TotalSocketValue: The total value of just sockets and socket bonuses.  (This is already factored into the total value.)
function PawnGetItemValue(Item, ItemLevel, SocketBonus, ScaleName, DebugMessages, NoNormalization)
	-- If either the item or scale is empty, exit now.
	if (not Item) or (not ScaleName) then return end
	local ScaleOptions = PawnCommon.Scales[ScaleName]
	if not ScaleOptions then return end
	local ScaleValues = ScaleOptions.Values
	if not ScaleValues then return end
	
	-- Calculate the value.
	local Total = 0
	local TotalSocketValue = 0
	local IsUnusable
	local ThisValue, Stat, Quantity
	for Stat, Quantity in pairs(Item) do
		ThisValue = ScaleValues[Stat]
		if not VgerCore.IsClassic then
			-- When not in Classic:
			-- Attack Power gets converted into Strength or Agility, whichever is most valuable.
			-- BUG: Since Attack Power doesn't appear in the Values tab, it also won't show on the Compare tab.  The Compare tab
			-- would need extra handling for Attack Power.
			if Stat == "Ap" then
				local StrengthValue = ScaleValues["Strength"] or 0
				local AgilityValue = ScaleValues["Agility"] or 0
				if AgilityValue > StrengthValue then
					Stat = "Agility"
				else
					Stat = "Strength"
				end
				ThisValue = ScaleValues[Stat]
			end
		end
		-- This isn't an unusable stat, is it?
		if ThisValue and ThisValue <= PawnIgnoreStatValue then
			Total = 0
			IsUnusable = true
			if DebugMessages then PawnDebugMessage(format(PawnLocal.UnusableStatMessage, Stat)) end
			break
		end
		-- Sockets need to be considered separately since their value depends on the item's level.
		if Stat ~= "PrismaticSocket" then
			if ThisValue then
				-- This stat has a value; add it to the running total.
				if ScaleValues.SpeedBaseline and (
					Stat == "Speed" or
					Stat == "MeleeSpeed" or
					Stat == "MainHandSpeed" or
					Stat == "OffHandSpeed" or
					Stat == "OneHandSpeed" or
					Stat == "TwoHandSpeed" or
					Stat == "RangedSpeed"	
				) then
					-- Speed is a special case; subtract SpeedBaseline from the speed value.
					Quantity = Quantity - ScaleValues.SpeedBaseline
				end
				Total = Total + Quantity * ThisValue
				if DebugMessages then PawnDebugMessage(format(PawnLocal.ValueCalculationMessage, Quantity, Stat, ThisValue, Quantity * ThisValue)) end
			end
		end
	end

	local ThisScaleBestGems = PawnScaleBestGems[ScaleName]
	if not IsUnusable then
		if ThisScaleBestGems then

			local ShouldIncludeSockets = (not PawnCommon.IgnoreGemsWhileLeveling) or (ItemLevel ~= nil and ItemLevel >= PawnMinimumItemLevelToConsiderGems)
			local GemQualityLevel

			-- Decide what to do with sockets.
			if ShouldIncludeSockets then
				Stat = "PrismaticSocket" Quantity = Item[Stat]
				if Quantity then
					GemQualityLevel = PawnGetGemQualityForItem(PawnGemQualityLevels, ItemLevel)
					ThisValue = ThisScaleBestGems[Stat .. "Value"][GemQualityLevel]
					if ThisValue then
						TotalSocketValue = TotalSocketValue + Quantity * ThisValue
						Total = Total + Quantity * ThisValue
						if DebugMessages then PawnDebugMessage(format(PawnLocal.ValueCalculationMessage, Quantity, Stat, ThisValue, Quantity * ThisValue)) end
					end
				end
			end
			
			-- Decide what to do with socket bonuses.
			if SocketBonus then
				local SocketBonusValue = 0
				for Stat, Quantity in pairs(SocketBonus) do
					ThisValue = ScaleValues[Stat]
					if ThisValue then
						SocketBonusValue = SocketBonusValue + ThisValue * Quantity
						if DebugMessages and ShouldIncludeSockets then PawnDebugMessage(format(PawnLocal.ValueCalculationMessage, Quantity, Stat, ThisValue, Quantity * ThisValue)) end
					end
				end
				if DebugMessages and ShouldIncludeSockets then
					PawnDebugMessage(format(PawnLocal.SocketBonusValueCalculationMessage, SocketBonusValue))
				end
				if ShouldIncludeSockets then
					Total = Total + SocketBonusValue
					TotalSocketValue = TotalSocketValue + SocketBonusValue
				end
			end

		else
			-- This error case is acceptable if we're calculating data FOR the gems themselves.  (In that case, normalization will be off.)
			-- If it happens at any other time, we'd want to know about it.
			VgerCore.Assert(NoNormalization, "Item value calculation will be incomplete because we don't have best gem data and thus can't calculate values for sockets.")
		end
	end
	
	-- Perform normalizations on the total if that option is enabled.
	if (not IsUnusable) and (not NoNormalization) and PawnScaleTotals[ScaleName] and PawnScaleTotals[ScaleName] > 0 then
		if ScaleOptions.NormalizationFactor and ScaleOptions.NormalizationFactor > 0 then
			if DebugMessages then PawnDebugMessage(format(PawnLocal.NormalizationMessage, PawnScaleTotals[ScaleName])) end
			Total = ScaleOptions.NormalizationFactor * Total / PawnScaleTotals[ScaleName]
		end
	end
	
	if DebugMessages then PawnDebugMessage(format(PawnLocal.TotalValueMessage, Total)) end
	
	return Total, TotalSocketValue
end

-- Returns a friendly description of the best gems to use for a given scale.
function PawnGetGemListString(ScaleName, ListAll, ItemLevel)
	local Gems = PawnScaleBestGems[ScaleName]
	if Gems and Gems.PrismaticSocket then
		local GemQuality = PawnGetGemQualityForItem(PawnGemQualityLevels, ItemLevel)
		local GemTable = Gems.PrismaticSocket[GemQuality]

		if ListAll then
			local _, GemInfo, GemList
			for _, GemInfo in pairs(GemTable) do
				local ThisGemName
				local Item = PawnGetItemData("item:" .. GemInfo.ID)
				if Item and Item.Name then
					ThisGemName = Item.Name
				else
					GemList = nil
					break
				end
				if GemList then
					GemList = GemList .. ("\n" .. ThisGemName)
				else
					GemList = ThisGemName
				end
			end
			if GemList then return GemList end
		end
		if #GemTable == 1 then
			-- There's exactly one best gem for this scale, so return its name.
			-- If it's in the Pawn cache, use its name from there.
			local Item = PawnGetItemData("item:" .. GemTable[1].ID)
			if Item and Item.Name then
				return Item.Name
			end
		elseif #GemTable == 2 then
			-- If there are exactly two best gems, we can include those names.
			local Item = PawnGetItemData("item:" .. GemTable[1].ID)
			if Item and Item.Name then
				local ItemName1 = Item.Name
				Item = PawnGetItemData("item:" .. GemTable[2].ID)
				if Item and Item.Name then
					return format(PawnLocal.GemList2, ItemName1, Item.Name)
				end
			end
		end
		if #GemTable > 0 then
			return format(PawnLocal.GemListMany, #GemTable)
		end
	end

	-- If we don't have something better to display, so be it.
	return nil
end

-- Returns the type of hyperlink passed in, or nil if it's not a hyperlink.
-- Possible values include: item, enchant, quest, spell
function PawnGetHyperlinkType(Hyperlink)
	-- First, try colored links.
	local _, _, LinkType = strfind(Hyperlink, "^|c%x%x%x%x%x%x%x%x|H(.-):")
	if not LinkType then
		-- Then, try links prepended with |H.  (Outfitter does this.)
		_, _, LinkType = strfind(Hyperlink, "^|H(.-):")
	end
	if not LinkType then
		-- Then, try raw links.
		_, _, LinkType = strfind(Hyperlink, "^(.-):")
	end
	return LinkType
end

-- If the item link is of the clickable form, strip off the initial hyperlink portion.
function PawnStripLeftOfItemLink(ItemLink)
	local _, _, InnerLink = strfind(ItemLink, "^|%x+|H(.+)")
	if InnerLink then return InnerLink else return ItemLink end
end

-- Extracts the item ID from an ItemLink string and returns it, or nil if unsuccessful.
function PawnGetItemIDFromLink(ItemLink)
	local Pos, _, ItemID = strfind(PawnStripLeftOfItemLink(ItemLink), "^item:(%-?%d+):?")
	if Pos then return tonumber(ItemID) else return nil end
end

-- If the upgrade level passed in is upgradeable with valor, return the new upgrade level, otherwise return nil. 
local function PawnDoValorUpgrade(UpgradeLevel)
	if UpgradeLevel1 == "529" or UpgradeLevel1 == "530" then
		-- Note: This only covers Warlords of Draenor upgradeable items, not legacy items, but realistically you're probably
		-- not ever going to want to upgrade those.  (If it ever becomes useful, it sounds like LibItemUpgradeInfo handles all of the edge cases.)
		return "531"
	else
		return nil
	end
end

-- Returns a new item link that represents an unenchanted version of the original item link.
-- Return values:
--		ItemLink - The unenchanted item link, or nil if unsuccessful or the item is not unenchanted.
--		WasUpgraded - True if the item was upgraded while being "unenchanted."  (Always false if "ignore valor and baleful upgrades" is off.)
-- (But if EvenIfNotEnchanted is true, the item link will be processed even if the item wasn't enchanted.)
function PawnUnenchantItemLink(ItemLink, EvenIfNotEnchanted)
	local TrimmedItemLink = PawnStripLeftOfItemLink(ItemLink)
	local Pos, _, ItemID, EnchantID, GemID1, GemID2, GemID3, GemID4, SuffixID, MoreInfo, ViewAtLevel, SpecializationID, UpgradeLevel1, Difficulty, NumBonusIDs, BonusID1, BonusID2, BonusID3, BonusID4, BonusID5, BonusID6, BonusID7, BonusID8 = strfind(TrimmedItemLink, "^item:(%-?%d+):?(%d*):?(%d*):?(%d*):?(%d*):?(%d*):?(%-?%d*):?(%-?%d*):?(%d*):?(%d*):?(%d*):?(%d*):?(%d*):?(%d*):?(%d*):?(%d*):?(%d*):?(%d*):?(%d*):?(%d*):?(%d*)")
	-- Note: After the specified number of bonus IDs would be UpgradeLevel2, which could be the level at which the item was acquired for timewarped items, or
	-- the Valor upgrade level.

	if Pos then
	
		-- If this is a valor-upgradeable item that isn't fully upgraded, for purposes of calculation we always assume a fully-upgraded item is the "base."
		-- The upgrade value will always come after the list of bonus IDs, as UpgradeLevel2.
		NumBonusIDs = tonumber(NumBonusIDs) or 0
		VgerCore.Assert(NumBonusIDs <= 8, "Didn't expect to find " .. tostring(NumBonusIDs) .. " bonus IDs on an item. Item stats may not be correct.")
		local WasUpgraded = false -- This feature was removed in Pawn 2.1.4.

		if
			EvenIfNotEnchanted or
			EnchantID ~= "0" or EnchantID == "" or EnchantID == nil or
			GemID1 ~= "0" or GemID1 == "" or GemID1 == nil or
			GemID2 ~= "0" or GemID2 == "" or GemID2 == nil or
			GemID3 ~= "0" or GemID3 == "" or GemID3 == nil or
			GemID4 ~= "0" or GemID4 == "" or GemID4 == nil or
			WasUpgraded
		then
			-- This item is enchanted.  Return a new link.
			if SuffixID == nil or SuffixID == "" then SuffixID = "0" end
			if MoreInfo == nil or MoreInfo == "" then MoreInfo = "0" end
			if SpecializationID == nil or SpecializationID == "" then SpecializationID = "0" end
			if UpgradeLevel1 == nil or UpgradeLevel1 == "" then UpgradeLevel1 = "0" end
			if Difficulty == nil or Difficulty == "" then Difficulty = "0" end
			if NumBonusIDs == nil or NumBonusIDs == "" then NumBonusIDs = "0" end
			if BonusID1 == nil or BonusID1 == "" then BonusID1 = "0" end
			if BonusID2 == nil or BonusID2 == "" then BonusID2 = "0" end
			if BonusID3 == nil or BonusID3 == "" then BonusID3 = "0" end
			if BonusID4 == nil or BonusID4 == "" then BonusID4 = "0" end
			if BonusID5 == nil or BonusID5 == "" then BonusID5 = "0" end
			if BonusID6 == nil or BonusID6 == "" then BonusID6 = "0" end
			if BonusID7 == nil or BonusID7 == "" then BonusID7 = "0" end
			if BonusID8 == nil or BonusID8 == "" then BonusID8 = "0" end
			return "item:" .. ItemID .. ":0:0:0:0:0:" .. SuffixID .. ":" .. MoreInfo .. ":" .. 0 .. ":" .. SpecializationID .. ":" .. UpgradeLevel1 .. ":" .. Difficulty .. ":" .. NumBonusIDs .. ":" .. BonusID1 .. ":" .. BonusID2 .. ":" .. BonusID3 .. ":" .. BonusID4 .. ":" .. BonusID5 .. ":" .. BonusID6 .. ":" .. BonusID7 .. ":" .. BonusID8, WasUpgraded
		else
			-- This item is not enchanted.  Return nil.
			return nil
		end
	else
		-- We couldn't parse this item link.  Return nil.
		-- This is a normal case—for example, battle pet item links.
		--VgerCore.Fail("Could not parse the item link: " .. PawnEscapeString(ItemLink))
		return nil
	end
end

-- Returns a nice-looking string that shows the item IDs for an item, its enchantments, and its gems.
function PawnGetItemIDsForDisplay(ItemLink, Formatted)
	local Pos, _, ItemID, MoreInfo = strfind(ItemLink, "^|%x+|Hitem:(%-?%d+)([^|]+)|")
	if not Pos then return end
	if Formatted == nil then Formatted = true end

	if MoreInfo and MoreInfo ~= "" then
		if Formatted then
			return ItemID .. VgerCore.Color.Silver .. MoreInfo .. "|r"
		else
			return "item:" .. ItemID .. MoreInfo
		end
	else
		if Formatted then
			return ItemID
		else
			return "item:" .. ItemID
		end
	end
end

-- Reads a Pawn scale tag, and breaks it into parts.
-- 	Parameters: ScaleTag
--		ScaleTag: A Pawn scale tag.  Example:  '(Pawn:v1:"Healbot":Stamina=1,Intellect=1.24)'
--	Return value: Name, Values; or nil if unsuccessful, or if the version number is too high.
--		Name: The scale name.
--		Values: A table of scale stats and values.  Example: {["Stamina"] = 1, ["Intellect"] = 1.24}
function PawnParseScaleTag(ScaleTag)
	-- Read the scale and perform basic validation.
	local Pos, _, Version, Name, ValuesString = strfind(ScaleTag, "^%s*%(%s*Pawn%s*:%s*v(%d+)%s*:%s*\"([^\"]+)\"%s*:%s*(.+)%s*%)%s*$")
	Version = tonumber(Version)
	if (not Pos) or (not Version) or (not Name) or (Name == "") or (not ValuesString) or (ValuesString == "") then return end
	if Version > PawnCurrentScaleVersion then return end
	
	-- Now, parse the values string for stat names and values.
	local Values = {}
	local SpecID
	local function SplitStatValuePair(Pair)
		local Pos, _, Stat, Value = strfind(Pair, "^%s*([%a%d]+)%s*=%s*(%-?[%d%.a-zA-Z]+)%s*,$")
		if Stat == "Class" then
			Value = PawnGetClassIDFromName(Value)
		elseif Stat == "Spec" then
			SpecID = Value -- processed later, in case they list Spec before Class
		else
			Value = tonumber(Value)
		end
		if Pos and Stat and (Stat ~= "") and Value then 
			Values[Stat] = Value
		end
	end
	gsub(ValuesString .. ",", "[^,]*,", SplitStatValuePair)
	if SpecID and Values.Class then
		Values.Spec = PawnGetSpecIDFromName(Values.Class, SpecID)
	end
	if (not Values.Class) or (not Values.Spec) then
		-- Never return just a class without a spec.
		Values.Spec = nil
		Values.Class = nil
	end
	
	-- Looks like everything worked.
	return Name, Values
end

local ClassNameToIDMap =
{
	["WARRIOR"] = 1, ["PALADIN"] = 2, ["HUNTER"] = 3, ["ROGUE"] = 4, ["PRIEST"] = 5, ["DEATHKNIGHT"] = 6, ["SHAMAN"] = 7, ["MAGE"] = 8, ["WARLOCK"] = 9, ["MONK"] = 10, ["DRUID"] = 11, ["DEMONHUNTER"] = 12
}
local SpecNameToIDMap =
{
	[1] = { ARMS = 1, FURY = 2, PROTECTION = 3 },
	[2] = { HOLY = 1, PROTECTION = 2, RETRIBUTION = 3 },
	[3] = { BEASTMASTERY = 1, MARKSMANSHIP = 2, SURVIVAL = 3 },
	[4] = { ASSASSINATION = 1, OUTLAW = 2, SUBTLETY = 3 },
	[5] = { DISCIPLINE = 1, HOLY = 2, SHADOW = 3 },
	[6] = { BLOOD = 1, FROST = 2, UNHOLY = 3 },
	[7] = { ELEMENTAL = 1, ENHANCEMENT = 2, RESTORATION = 3 },
	[8] = { ARCANE = 1, FIRE = 2, FROST = 3 },
	[9] = { AFFLICTION = 1, DEMONOLOGY = 2, DESTRUCTION = 3 },
	[10] = { BREWMASTER = 1, MISTWEAVER = 2, WINDWALKER = 3 },
	[11] = { BALANCE = 1, FERAL = 2, GUARDIAN = 3, RESTORATION = 4 },
	[12] = { HAVOC = 1, VENGEANCE = 2 },
}
local ClassIDToEnglishNameMap =
{
	[1] = "Warrior", [2] = "Paladin", [3] = "Hunter", [4] = "Rogue", [5] = "Priest", [6] = "DeathKnight", [7] = "Shaman", [8] = "Mage", [9] = "Warlock", [10] = "Monk", [11] = "Druid", [12] = "DemonHunter" 
}
local SpecIDToEnglishNameMap =
{
	[1] = { [1] = "Arms", [2] = "Fury", [3] = "Protection" },
	[2] = { [1] = "Holy", [2] = "Protection", [3] = "Retribution" },
	[3] = { [1] = "BeastMastery", [2] = "Marksmanship", [3] = "Survival" },
	[4] = { [1] = "Assassination", [2] = "Outlaw", [3] = "Subtlety" },
	[5] = { [1] = "Discipline", [2] = "Holy", [3] = "Shadow" },
	[6] = { [1] = "Blood", [2] = "Frost", [3] = "Unholy" },
	[7] = { [1] = "Elemental", [2] = "Enhancement", [3] = "Restoration" },
	[8] = { [1] = "Arcane", [2] = "Fire", [3] = "Frost" },
	[9] = { [1] = "Affliction", [2] = "Demonology", [3] = "Destruction" },
	[10] = { [1] = "Brewmaster", [2] = "Mistweaver", [3] = "Windwalker" },
	[11] = { [1] = "Balance", [2] = "Feral", [3] = "Guardian", [4] = "Restoration" },
	[12] = { [1] = "Havoc", [2] = "Vengeance" },
}

-- Returns a class ID number (1-12) from the string passed in, or nil if the string isn't a class name.
function PawnGetClassIDFromName(Name)
	local ClassID = ClassNameToIDMap[string.upper(Name)]
	if ClassID then return ClassID end

	-- If it's not a name, try a number.
	ClassID = tonumber(Name)
	if ClassID and ClassIDToEnglishNameMap[ClassID] then return ClassID end -- make sure it's a real class ID

	return nil
end

-- Returns a spec ID number (1-4) from the string passed in, or nil if the string isn't a spec name.
function PawnGetSpecIDFromName(ClassID, Name)
	local SpecTable = SpecNameToIDMap[ClassID]
	if not SpecTable then return end
	local SpecID = SpecTable[string.upper(Name)]
	if SpecID then return SpecID end

	-- If it's not a name, try a number.
	SpecID = tonumber(Name)
	if SpecID and SpecIDToEnglishNameMap[ClassID][SpecID] then return SpecID end -- make sure it's a real spec ID for this class

	return nil
end

-- Returns an unlocalized English class name from the class ID number.
function PawnGetEnglishClassNameFromID(ClassID)
	return ClassIDToEnglishNameMap[ClassID]
end

-- Returns an unlocalized English class name from the class and spec ID numbers.
function PawnGetEnglishSpecNameFromID(ClassID, SpecID)
	local SpecTable = SpecIDToEnglishNameMap[ClassID]
	if not SpecTable then return end
	return SpecTable[SpecID]
end

-- Escapes a string so that it can be more easily printed.
function PawnEscapeString(String)
	return gsub(gsub(gsub(String, "\r", "\\r"), "\n", "\\n"), "|", "||")
end

-- Corrects errors in scales: either human errors, or to correct for bugs in current or past versions of Pawn.
function PawnCorrectScaleErrors(ScaleName)
	local ThisScaleOptions = PawnCommon.Scales[ScaleName]
	if not ThisScaleOptions then return end
	local ThisScale = ThisScaleOptions.Values
	if not ThisScale then
		ThisScale = { }
		ThisScaleOptions.Values = ThisScale
	end
	
	-- Pawn 1.5.5 adds an option to follow specialization when upgrading.
	if ThisScaleOptions.UpgradesFollowSpecialization == nil then ThisScaleOptions.UpgradesFollowSpecialization = true end
	
	-- Pawn 1.3 adds per-character options to each scale.
	if ThisScaleOptions.PerCharacterOptions == nil then ThisScaleOptions.PerCharacterOptions = {} end
	if ThisScaleOptions.PerCharacterOptions[PawnPlayerFullName] == nil then ThisScaleOptions.PerCharacterOptions[PawnPlayerFullName] = {} end
	
	-- Some versions of Pawn call resilience rating Resilience and some call it ResilienceRating.
	PawnReplaceStat(ThisScale, "Resilience", "ResilienceRating")
	
	-- Early versions of Pawn 0.7.x had a typo in the configuration UI so that none of the special DPS stats worked.
	PawnReplaceStat(ThisScale, "MeleeDPS", "MeleeDps")
	PawnReplaceStat(ThisScale, "RangedDPS", "RangedDps")
	PawnReplaceStat(ThisScale, "MainHandDPS", "MainHandDps")
	PawnReplaceStat(ThisScale, "OffHandDPS", "OffHandDps")
	PawnReplaceStat(ThisScale, "OneHandDPS", "OneHandDps")
	PawnReplaceStat(ThisScale, "TwoHandDPS", "TwoHandDps")
	
	-- Pawn 1.6.1 removed IsRelic and IsThrown

	-- Pawn 1.7 makes smart gem socketing mandatory.
	ThisScaleOptions.SmartGemSocketing = nil
	ThisScaleOptions.SmartMetaGemSocketing = nil
	ThisScaleOptions.GemQualityLevel = nil
	ThisScaleOptions.MetaGemQualityLevel = nil
	ThisScaleOptions.CogwheelQualityLevel = nil
	ThisScale.PrismaticSocket = nil
	ThisScale.RedSocket = nil
	ThisScale.YellowSocket = nil
	ThisScale.BlueSocket = nil
	ThisScale.CogwheelSocket = nil
	ThisScale.ColorlessSocket = nil
	ThisScale.MetaSocket = nil
	ThisScale.MetaSocketEffect = nil

	-- These stats aren't used in the live OR classic realms.
	ThisScale.ExpertiseRating = nil
	ThisScale.ArmorPenetration = nil
	ThisScale.Mana = nil
	ThisScale.Health = nil
	ThisScale.BaseArmor = nil
	ThisScale.IsRelic = nil
	ThisScale.IsThrown = nil
	ThisScale.BonusArmor = nil
	ThisScale.Multistrike = nil
	ThisScale.SpellPower = nil
	ThisScale.ResilienceRating = nil
	ThisScale.SpellPenetration = nil

	-- Pawn 1.9.7 makes it impossible to ignore primary stats, since they're on all armor now.
	if ThisScale.Stamina == PawnIgnoreStatValue then ThisScale.Stamina = nil end
	if ThisScale.Strength == PawnIgnoreStatValue then ThisScale.Strength = nil end
	if ThisScale.Agility == PawnIgnoreStatValue then ThisScale.Agility = nil end
	if ThisScale.Intellect == PawnIgnoreStatValue then ThisScale.Intellect = nil end

	-- Versions of Pawn before 2.1.16 had an error where the Role property was being incorrectly set to true/false
	-- due to an API change.  If that happened to this scale, fix it.
	if ThisScaleOptions.Role == true or ThisScaleOptions.Role == false then ThisScaleOptions.Role = nil end
end

-- Replaces one incorrect stat with a correct stat.
function PawnReplaceStat(ThisScale, OldStat, NewStat)
	if ThisScale[OldStat] then
		if not ThisScale[NewStat] then ThisScale[NewStat] = ThisScale[OldStat] end
		ThisScale[OldStat] = nil
	end
end

-- Combines two stats into one.  For example, combines HitRating and SpellHitRating, putting the larger of the 
-- two values in HitRating.
function PawnCombineStats(ThisScale, PrimaryStat, SecondaryStat)
	if ThisScale[SecondaryStat] then
		if ThisScale[PrimaryStat] and ThisScale[PrimaryStat] > ThisScale[SecondaryStat] then
			-- If the primary stat is larger, do nothing.
		else
			-- If the secondary stat is larger, increase the value of the primary to the secondary.
			ThisScale[PrimaryStat] = ThisScale[SecondaryStat]
		end
		-- Regardless, clear out the secondary stat afterward.
		ThisScale[SecondaryStat] = nil
	end
end

-- Causes the Pawn private tooltip to be shown when next hovering an item.
--function PawnTestShowPrivateTooltip()
--	_G[PawnPrivateTooltipName]:SetOwner(UIParent, "ANCHOR_TOPRIGHT")
--end

-- Hides the Pawn private tooltip (normal).
--function PawnTestHidePrivateTooltip()
--	_G[PawnPrivateTooltipName]:SetOwner(UIParent, "ANCHOR_NONE")
--	_G[PawnPrivateTooltipName]:Hide()
--end

-- Depending on the user's current tooltip icon settings, show and hide icons as appropriate.
function PawnToggleTooltipIcons()
	PawnAttachIconToTooltip(ItemRefTooltip)
	PawnAttachIconToTooltip(ShoppingTooltip1, true)
	PawnAttachIconToTooltip(ShoppingTooltip2, true)
	
	-- MultiTips compatibility
	PawnAttachIconToTooltip(ItemRefTooltip2)
	PawnAttachIconToTooltip(ItemRefTooltip3)
	PawnAttachIconToTooltip(ItemRefTooltip4)
	PawnAttachIconToTooltip(ItemRefTooltip5)
	
	-- EquipCompare compatibility
	PawnAttachIconToTooltip(ComparisonTooltip1, true)
	PawnAttachIconToTooltip(ComparisonTooltip2, true)
end

-- If tooltip icons are enabled, attaches an icon to the upper-left corner of a tooltip.  Otherwise, hides
-- any icons attached to that tooltip if they exist.
-- Optionally, the caller may include an item link so this function doesn't need to get one.
function PawnAttachIconToTooltip(Tooltip, AttachAbove, ItemLink)
	-- If the tooltip doesn't exist, exit now.
	if not Tooltip then return end

	-- Find the right texture to use, but skip all this if the user has icons turned off.
	local TextureName
	if PawnCommon.ShowTooltipIcons then
		-- Don't retrieve an item link if one was passed in.
		if not ItemLink then
			local _
			_, ItemLink = Tooltip:GetItem()
		end
		if ItemLink then
			TextureName = GetItemIcon(ItemLink)
		end
	end
	
	-- Now, if we don't have a texture to use, or icons are disabled, hide this icon if it's visible
	-- and then exit.
	local IconFrame = Tooltip.PawnIconFrame
	if not TextureName then
		if IconFrame then
			IconFrame:Hide()
			IconFrame.PawnIconTexture = nil
			Tooltip.PawnIconFrame = nil
		end
		return
	end
	
	-- Create the icon's frame if it doesn't already exist.
	if not IconFrame then
		IconFrame = CreateFrame("Frame", nil, Tooltip)
		Tooltip.PawnIconFrame = IconFrame
		IconFrame:SetWidth(37)
		IconFrame:SetHeight(37)
		
		local IconTexture = IconFrame:CreateTexture(nil, "BACKGROUND")
		IconTexture:SetTexture(TextureName)
		IconTexture:SetAllPoints(IconFrame)
		IconFrame.PawnIconTexture = IconTexture
	else
		-- If the icon already existed, then we just need to update the texture.
		IconFrame.PawnIconTexture:SetTexture(TextureName)
	end

	-- Attach the icon frame and show it.
	if AttachAbove then
		IconFrame:SetPoint("BOTTOMLEFT", Tooltip, "TOPLEFT", 2, -2)
	else
		IconFrame:SetPoint("TOPRIGHT", Tooltip, "TOPLEFT", 2, -2)
	end
	IconFrame:Show()
	
	return IconFrame
end

-- Hides any icons on a tooltip, if there are any.
function PawnHideTooltipIcon(TooltipName)
	-- Find the tooltip.  If it doesn't exist, we can skip out now.
	local Tooltip = _G[TooltipName]
	if not Tooltip then return end
	
	-- Is there an icon on it?  If not, exit.
	local IconFrame = Tooltip.PawnIconFrame
	if not IconFrame then return end
	
	-- Hide the icon frame if it's there, and remove the reference to it so it can be garbage-collected.
	IconFrame:Hide()
	IconFrame.PawnIconTexture = nil
	Tooltip.PawnIconFrame = nil
end

-- Comparer function for use in sort that sorts strings alphabetically, ignoring case, and also ignoring a
-- 10-character color format at the beginning of the string.
function PawnColoredStringCompare(a, b)
	return strlower(strsub(a, 11)) < strlower(strsub(b, 11))
end

-- Comparer function for use in sort that sorts sub-tables alphabetically by the localized name in the sub-table, ignoring case.
function PawnItemValueCompare(a, b)
	return strlower(a[4]) < strlower(b[4])
end

-- Takes an ItemEquipLoc and returns one or two slot IDs where that item type can be equipped.
-- Bags are not supported.
function PawnGetSlotsForItemType(ItemEquipLoc)
	if (not ItemEquipLoc) or (ItemEquipLoc == "") then return end
	return PawnItemEquipLocToSlot1[ItemEquipLoc], PawnItemEquipLocToSlot2[ItemEquipLoc]
end

-- Given a particular item level and a list of gem tables, return the appropriate gem quality level for an item of the given level.
-- If ItemLevel is nil, then the highest gem quality is assumed.
function PawnGetGemQualityForItem(GemQualityLevels, ItemLevel)
	if not ItemLevel then return GemQualityLevels[1][1] end

	local _, GemData, GemLevel
	for _, GemData in pairs(GemQualityLevels) do
		GemLevel = GemData[1]
		if ItemLevel >= GemLevel then return GemLevel end
	end
	VgerCore.Fail("Couldn't find an appropriate gem quality level for an item of level " .. tostring(ItemLevel) .. " in the specified item table.")
	return GemLevel
end

-- Finds the best gems for a particular scale in one or more colors.
-- 	Parameters: ScaleName, GemTable
--		ScaleName: The name of the scale for which to find gems.
--		GemTable: The gem table to search through.
--	Return value: Value, GemList
--		Value: The value of the best gem or gems for the chosen colors.
--		GemList: A table of gems of that value.  Each item in the list is in the standard Pawn item table format, and
--			the list is sorted alphabetically by name.
function PawnFindBestGems(ScaleName, GemTable)
	local BestScore = 0
	local BestItems = { }

	-- Go through the list of gems, checking each item that matches one of the find criteria.
	local GemData, ThisGem, _
	for _, GemData in pairs(GemTable) do
		ThisGem = PawnGetGemData(GemData)
		if ThisGem then
			local ThisValue = PawnGetItemValue(ThisGem.UnenchantedStats, ThisGem.Level, nil, ScaleName, false, true)
			if ThisValue and ThisValue > BestScore then
				-- This gem is better than any we've found so far.
				BestScore = ThisValue
				wipe(BestItems)
				tinsert(BestItems, ThisGem)
			elseif ThisValue and ThisValue == BestScore then
				-- This gem is tied with the best gems we've found so far.
				tinsert(BestItems, ThisGem)
			end
		else
			VgerCore.Fail("Failed to get information about gem " .. GemData[1])
		end
	end
	
	-- Now we have a list of the best gems.  Sort them alphabetically.
	sort(BestItems, PawnItemComparer)
	
	-- In debug mode, display them.
	if PawnCommon.Debug then
		VgerCore.Message("=== Best gems for " .. PawnGetScaleLocalizedName(ScaleName) .. ": ===")
		for _, ThisGem in pairs(BestItems) do
			VgerCore.Message("  " .. ThisGem.Link)
		end
		VgerCore.Message(" --> Score: " .. tostring(BestScore))
	end
	
	-- Return the value and list of gems.
	return BestScore, BestItems
	
end

-- Refreshes a cached item with new information if available.  Currently meant only for refreshing
-- best-gem item data, which often doesn't have a name or texture, with that information.
-- Returns true if it did anything.
function PawnRefreshCachedItem(Item)
	if not Item then
		VgerCore.Fail("PawnRefreshCachedItem requires an item table.")
		return false
	end
	
	-- Request the new information.
	local ItemName, _, _, _, _, _, _, _, _, ItemTexture = GetItemInfo(Item.ID)
	if not ItemName then
		-- The client doesn't have any further information on this item yet, so bail out.
		return false
	end
	
	-- Save this new information into the cached item record.
	Item.Name = ItemName
	Item.Texture = ItemTexture
	return true
end

-- Determines if an item is an upgrade for any of the current user's enabled scales, and if so, by how much.
-- Parameters:
--	Item: The item table for the item in question.
--	DoNotRescan: If best item data is not available, just return nil instead of rescanning.
-- Returns nothing if the item is not an upgrade and is not one of the player's best items.  Otherwise:
--	UpgradeInfo, ItemLevelIncrease, BestItemFor, SecondBestItemFor, NeedsEnhancements
--	UpgradeInfo: a sorted table of upgrades, with each element being another table:
--		{ { ScaleName, LocalizedScaleName, PercentUpgrade, ExistingItem }, ... }
--		ScaleName: the raw scale name.
--		LocalizedScaleName: the localized scale name, suitable for display.
--		PercentUpgrade: for example, for an item that is a 15% upgrade over the current item, .15.
--		ExistingItemLink: the item that would be replaced if the user upgraded to the item passed in, in its unenchanted (not display-ready) form.
--	ItemLevelIncrease: the number of item levels that this item increases the player's score, or nil if it's not an item level upgrade.
--	BestItemFor, SecondBestItemFor: a table of scales for which this item is already the player's best or second-best item, or nil if none.
--		{ ["Scale1"] = true, ["Scale2"] = true }
--	NeedsEnhancements: true if the item requires enhancements (Valor or Empowered Apexis Fragment).
function PawnIsItemAnUpgrade(Item, DoNotRescan)

	-- Before we begin, check for unsupported item types and bail out early if appropriate.
	if Item and type(Item) ~= "table" then
		VgerCore.Fail("Item must be a table of item stats, not '" .. type(Item) .. "'.")
		return
	end
	-- If an artifact is involved, we only compare using item level, since artifacts are complicated and one item can fill multiple slots.
	local CompareUsingItemLevelOnly = (Item.Rarity == 6)
	local InvType = Item.InvType
	if not InvType or InvType == "" or InvType == "INVTYPE_BAG" or InvType == "INVTYPE_QUIVER" or InvType == "INVTYPE_TABARD" or InvType == "INVTYPE_BODY" or InvType == "INVTYPE_THROWN" or InvType == "INVTYPE_AMMO" or InvType == "INVTYPE_RELIC" then return nil end
	local SkipScoreBasedUpgrades = InvType == "INVTYPE_TRINKET"
	local UnenchantedItemLink, NeedsEnhancements = PawnUnenchantItemLink(Item.Link, true)
	VgerCore.Assert(UnenchantedItemLink ~= nil, "PawnIsItemAnUpgrade failed to get an item link for item " .. tostring(Item.ID))

	-- Don't show 1H upgrades if using a 2H item, and vice-versa.
	-- And, don't show any weapons or off-hands as upgrades if they have an artifact equipped.
	local MainWeaponLink = GetInventoryItemLink("player", INVSLOT_MAINHAND) or GetInventoryItemLink("player", INVSLOT_OFFHAND)
	if MainWeaponLink then
		local MainWeapon = PawnGetItemData(MainWeaponLink)
		if MainWeapon then
			if MainWeapon.Rarity == 6 then
				-- They're wielding an artifact, so ignore handedness checks.
				if InvType == "INVTYPE_WEAPON" or InvType == "INVTYPE_WEAPONMAINHAND" or InvType == "INVTYPE_WEAPONOFFHAND" or InvType == "INVTYPE_SHIELD" or InvType == "INVTYPE_HOLDABLE" or InvType == "INVTYPE_2HWEAPON" then
					-- If they're wielding an artifact weapon and also looking at an artifact weapon, it can't be an upgrade, since artifact weapons are spec-specific.  So, bail out.
					if Item.Rarity == 6 then
						SkipScoreBasedUpgrades = true
					end
					-- Otherwise, compare the new non-artifact weapon using item level only.
					CompareUsingItemLevelOnly = true
				end
			elseif MainWeapon.InvType == "INVTYPE_2HWEAPON" then
				-- They're using a two-handed weapon.  Bail out now if this is a one-handed weapon.
				if InvType == "INVTYPE_WEAPON" or InvType == "INVTYPE_WEAPONMAINHAND" or InvType == "INVTYPE_WEAPONOFFHAND" or InvType == "INVTYPE_SHIELD" or InvType == "INVTYPE_HOLDABLE" then SkipScoreBasedUpgrades = true end
			else
				-- They're using a one-handed weapon.  Bail out now if this is a two-handed weapon.
				if InvType == "INVTYPE_2HWEAPON" then SkipScoreBasedUpgrades = true end
			end
		end
	end

	-- Is this item an heirloom or artifact that will continue to either scale or provide an XP boost?
	local IsScalingHeirloom = (UnitLevel("player") <= PawnGetMaxLevelItemIsUsefulHeirloom(Item))
	
	local _
	local UpgradeTable, BestItemTable, SecondBestItemTable
	local ScaleName, Scale
	if not SkipScoreBasedUpgrades then
		for ScaleName, Scale in pairs(PawnCommon.Scales) do
			InvType = Item.InvType -- need to reset this here since it gets nil'ed out in the coming while loop
			
			if PawnIsScaleVisible(ScaleName) and not
				(Scale.DoNotShow1HUpgrades and (InvType == "INVTYPE_WEAPON" or InvType == "INVTYPE_WEAPONMAINHAND" or InvType == "INVTYPE_WEAPONOFFHAND" or InvType == "INVTYPE_SHIELD" or InvType == "INVTYPE_HOLDABLE")) and not
				(Scale.DoNotShow2HUpgrades and InvType == "INVTYPE_2HWEAPON") and
				((not Scale.UpgradesFollowSpecialization) or PawnIsArmorBestTypeForPlayer(Item))
			then
				-- Find the best item for that slot.  Or, if a second-best item is available, compare versus that.
				local CharacterOptions = Scale.PerCharacterOptions[PawnPlayerFullName]
				if not CharacterOptions then
					VgerCore.Fail("PerCharacterOptions should be initialized before using PawnIsItemAnUpgrade.")
					return nil
				end
				if PawnOptions.UpgradeTracking and not CharacterOptions.BestItems then
					if DoNotRescan then return nil end
					-- If best item data hasn't been calculated yet, go ahead and calculate it now.
					PawnFindBestItems(ScaleName)
				end
				
				local InvType2 = nil
				local TwoSlotsForThisItemType = (InvType == "INVTYPE_FINGER")
				if InvType == "INVTYPE_WEAPON" then
					-- Check one-handed weapons against both the main hand and off hand, and report the best upgrade.
					-- (One-handed weapons aren't stored past the initial scan, so we don't need to check those.)
					if PawnOptions.UpgradeTracking then InvType = "INVTYPE_WEAPONMAINHAND" end
					if Scale.Values.IsOffHand == nil or Scale.Values.IsOffHand > PawnIgnoreStatValue then
						-- Only try putting off-hand weapons in the off hand if they fit there!
						-- And don't bother if upgrade tracking is off; we'll check both weapon slots anyway.
						if PawnOptions.UpgradeTracking then InvType2 = "INVTYPE_WEAPONOFFHAND" end
						TwoSlotsForThisItemType = true
					end
				elseif InvType == "INVTYPE_ROBE" then
					-- Robes are chest armor.
					InvType = "INVTYPE_CHEST"
				elseif InvType == "INVTYPE_SHIELD" or InvType == "INVTYPE_HOLDABLE" then
					-- Treat shields and held in off hand items the same as off-hand weapons since they go into the same slot.
					InvType = "INVTYPE_WEAPONOFFHAND"
				end
				-- TODO: A MH weapon can actually upgrade an OH weapon if the following are true:
				-- 1) The new MH weapon isn't better than the current MH one (because then it would upgrade that instead)
				-- 2) The current best MH weapon is a one-hander
				-- 3) The new MH weapon is better than the old OH weapon
				-- But, it's not clear how best to present this to the user, so this case (and the vice-versa case) is ignored for now.
				local ThisValue = nil
				local NewTableEntry = nil

				while InvType do
					local BestData = nil
					if PawnOptions.UpgradeTracking and not CompareUsingItemLevelOnly then
						BestData = CharacterOptions.BestItems[InvType]
					else
						-- If upgrade tracking is disabled, manually create a BestData table based on the currently-equipped items for this slot.
						local Slot1, Slot2, Item1, Item2, SlotHadItem1, SlotHadItem2, ItemLink1, ItemLink2, Value1, Value2
						Slot1 = PawnItemEquipLocToSlot1[InvType]
						if Slot1 then Item1, SlotHadItem1 = PawnGetItemDataForInventorySlot(Slot1, true) end
						if SlotHadItem1 and not Item1 then return end -- If there is an item in the slot but we don't have data yet, we can't evaluate upgrades yet.
						if Item1 then ItemLink1 = PawnUnenchantItemLink(Item1.Link, true) end
						if not TwoSlotsForThisItemType and ItemLink1 and UnenchantedItemLink == ItemLink1 then return end -- If this item is already equipped, it can't be an upgrade for any scale. 
						if TwoSlotsForThisItemType then Slot2 = PawnItemEquipLocToSlot2[InvType] end -- Don't check the off-hand slot for weapon upgrades if they can't dual-wield
						if Slot2 then Item2 = PawnGetItemDataForInventorySlot(Slot2, true) end
						if SlotHadItem2 and not Item2 then return end -- If there is an item in the slot but we don't have data yet, we can't evaluate upgrades yet.
						if Item2 then ItemLink2 = PawnUnenchantItemLink(Item2.Link, true) end
						if not TwoSlotsForThisItemType and ItemLink2 and UnenchantedItemLink == PawnUnenchantItemLink(Item2.Link, true) then return end
						VgerCore.Assert(Value1 == nil and Value2 == nil, "Where's the value coming from?")
						if CompareUsingItemLevelOnly then
							if Item1 then Value1 = Item1.Level end
							if Item2 then Value2 = Item2.Level end
						else
							if Item1 then _, Value1 = PawnGetSingleValueFromItem(Item1, ScaleName) end
							if Item2 then _, Value2 = PawnGetSingleValueFromItem(Item2, ScaleName) end
						end

						if Value1 and Value2 then
							if Value1 >= Value2 then
								BestData = { Value1, ItemLink1, PawnGetMaxLevelItemIsUsefulHeirloom(Item1), Value2, ItemLink2, PawnGetMaxLevelItemIsUsefulHeirloom(Item2) }
							else
								BestData = { Value2, ItemLink2, PawnGetMaxLevelItemIsUsefulHeirloom(Item2), Value1, ItemLink1, PawnGetMaxLevelItemIsUsefulHeirloom(Item1) }
							end
						elseif TwoSlotsForThisItemType then
							-- If it's possible to equip two of these and the player only has one, then any new item is an upgrade.
						elseif Value1 and not Value2 then
							BestData = { Value1, ItemLink1, PawnGetMaxLevelItemIsUsefulHeirloom(Item1) }
						elseif Value2 and not Value1 then
							BestData = { Value2, ItemLink2, PawnGetMaxLevelItemIsUsefulHeirloom(Item2) }
						end
					end
					if BestData then
						local BestValue = BestData[4] or BestData[1]
						local BestItem = BestData[5] or BestData[2]
						local BestMaxHeirloomLevel = BestData[6] or BestData[3]
						if BestValue then
							-- Don't bother looking for this item's value if we don't have a best item for this slot.
							if not ThisValue then
								if CompareUsingItemLevelOnly then
									ThisValue = Item.Level
								else
									_, ThisValue = PawnGetSingleValueFromItem(Item, ScaleName)
								end
							end
							
							if UnenchantedItemLink == BestData[2] and not (Item.InvType == "INVTYPE_WEAPON" and InvType == "INVTYPE_WEAPONOFFHAND") then
								-- If the item IS the first best item for a scale, then it can't be an upgrade.  (Technically that's only the case if
								-- the item is unique or unique-equipped, but Pawn currently can't determine that.)
								-- If the item is a one-handed weapon and we're currently evaluating the off-hand weapon slot, call it a second-best item instead of best.
								NewTableEntry = nil
								if BestItemTable == nil then BestItemTable = { [ScaleName] = true } else BestItemTable[ScaleName] = true end
								break
							elseif UnenchantedItemLink == BestData[5] or UnenchantedItemLink == BestData[2] then
								-- If it's the second-best item for a scale it's not an upgrade either.
								NewTableEntry = nil
								if SecondBestItemTable == nil then SecondBestItemTable = { [ScaleName] = true } else SecondBestItemTable[ScaleName] = true end
								break
							elseif TwoSlotsForThisItemType and BestData[4] == nil then
								-- There's an empty slot for this item to go into.
								NewTableEntry = { ["ScaleName"] = ScaleName, ["LocalizedScaleName"] = Scale.LocalizedName or ScaleName, ["PercentUpgrade"] = PawnBigUpgradeThreshold }
							elseif ThisValue > BestValue * 1.005 and (IsScalingHeirloom or UnitLevel("player") > BestMaxHeirloomLevel) then
								-- Hooray, it's an upgrade!  Add it to the table.
								-- (Only count upgrades that are at least 0.5% better.)
								-- If the best item is an heirloom, either the new one must be or the player must have outleveled it.
								local Difference = ThisValue - BestValue
								local PercentUpgrade
								if CompareUsingItemLevelOnly then
									if Item.CanEquip then
										PercentUpgrade = PawnBigUpgradeThreshold
									end
								else
									PercentUpgrade = Difference / (BestValue + PawnEpsilon) -- Epsilon is abused here to account for no-stat items.
								end
								if NewTableEntry then
									-- We already found a best item for another inventory type.
									if PercentUpgrade > NewTableEntry.PercentUpgrade then
										NewTableEntry.PercentUpgrade = PercentUpgrade
										NewTableEntry.ExistingItemLink = BestItem
									end
								else
									if PercentUpgrade then
										NewTableEntry = { ["ScaleName"] = ScaleName, ["LocalizedScaleName"] = Scale.LocalizedName or ScaleName, ["PercentUpgrade"] = PercentUpgrade, ["ExistingItemLink"] = BestItem }
									end
								end
							end
						end
					else
						-- Not having best item data for a particular slot isn't a bizarre case; it will happen often for low-level characters
						-- who don't have any helms or shoulders.
						if not ThisValue then _, ThisValue = PawnGetSingleValueFromItem(Item, ScaleName) end
						if ThisValue and ThisValue > 0 then
							NewTableEntry = { ["ScaleName"] = ScaleName, ["LocalizedScaleName"] = Scale.LocalizedName or ScaleName, ["PercentUpgrade"] = PawnBigUpgradeThreshold }
						end
					end
					
					-- If this item counts as two types of items, now scan the other type.
					InvType = InvType2
					InvType2 = nil
				end -- loop through inventory types
				
				if NewTableEntry then
					if not UpgradeTable then UpgradeTable = { } end
					tinsert(UpgradeTable, NewTableEntry)
				end
				
			end -- if scale is visible
		end -- loop through scales
	end -- skipping trinkets
		
	if UpgradeTable then sort(UpgradeTable, PawnLocalizedScaleNameComparer) end

	local ItemLevelIncrease
	if PawnCommon.ShowItemLevelUpgrades then
		ItemLevelIncrease = PawnIsItemAnItemLevelUpgrade(Item)
	end
	
	-- -- Print out the contents of the upgrade table for debugging purposes.
	-- if UpgradeTable then
	-- 	VgerCore.Message(tostring(Item.Link) .. " is an upgrade for:")
	-- 	local Upgrade
	-- 	for _, Upgrade in pairs(UpgradeTable) do
	-- 		VgerCore.Message("    " .. Upgrade.LocalizedScaleName .. ": " .. tostring(100 * Upgrade.PercentUpgrade) .. "% upgrade from " .. tostring(Upgrade.ExistingItemLink))
	-- 	end
	-- else
	-- 	VgerCore.Message(tostring(Item.Link) .. " is not an upgrade.")
	-- end
	-- if BestItemTable then
	-- 	for ScaleName in pairs(BestItemTable) do
	-- 		VgerCore.Message("    " .. tostring(ScaleName) .. ": best item")
	-- 	end
	-- end
	-- if SecondBestItemTable then
	-- 	for ScaleName in pairs(SecondBestItemTable) do
	-- 		VgerCore.Message("    " .. tostring(ScaleName) .. ": second-best item")
	-- 	end
	-- end
	
	return UpgradeTable, ItemLevelIncrease, BestItemTable, SecondBestItemTable, NeedsEnhancements
end

-- Comparer function for tables with subtables including a LocalizedScaleName entry.
function PawnLocalizedScaleNameComparer(a, b)
	if not b then return a end
	if not a then return b end
	return strlower(a.LocalizedScaleName) < strlower(b.LocalizedScaleName)
end

-- Invalidates all lists of best items for all characters and scales.
function PawnInvalidateBestItems()
	local ScaleName
	for ScaleName, _ in pairs(PawnCommon.Scales) do
		PawnInvalidateBestItemsForScale(ScaleName)
	end
end

-- Invalidates ALL characters' best items for a single scale.  (This happens whenever that scale's values change.)
function PawnInvalidateBestItemsForScale(ScaleName)
	local Scale = PawnCommon.Scales[ScaleName]
	if not Scale then
		VgerCore.Fail("PawnInvalidateBestItemsForScale: ScaleName must be the name of an existing scale.")
		return
	end
	local CharacterName, CharacterOptions
	for CharacterName, CharacterOptions in pairs(Scale.PerCharacterOptions) do
		CharacterOptions.BestItems = nil
	end
end

-- Checks the player's equipped items for any new upgrades to his or her item sets.
function PawnCheckInventoryForUpgrades()
	local ScaleName
	for ScaleName, _ in pairs(PawnCommon.Scales) do
		PawnFindBestItems(ScaleName, true)
	end
	local Slot
	for Slot = 1, 18 do
		PawnAddItemToLevelTracker(PawnGetItemDataForInventorySlot(Slot))
	end
end

-- Looks through a player's gear and finds the best items they have for that scale.
-- If InventoryOnly is true, then only check currently equipped items, not other things like equipment sets, unless no best
-- item data is currently available, in which case everything will be scanned as normal.
function PawnFindBestItems(ScaleName, InventoryOnly)
	local Scale = PawnCommon.Scales[ScaleName]
	if not Scale then
		VgerCore.Fail("PawnFindBestItems: ScaleName must be the name of an existing scale.")
		return
	end
	local CharacterOptions = Scale.PerCharacterOptions[PawnPlayerFullName]
	if not CharacterOptions then
		VgerCore.Fail("PerCharacterOptions should be initialized before using PawnFindBestItems on that scale.")
		return
	end
	
	if not CharacterOptions.Visible then
		-- This scale is not visible for this character.  We should invalidate best item data for the scale if it's present,
		-- and if it's not, just exit quickly.
		CharacterOptions.BestItems = nil
		return
	end

	-- If upgrade tracking is disabled, skip all this; it's unnecessary.
	if not PawnOptions.UpgradeTracking then return end
	
	local OldCacheSize = PawnItemCacheMaxSize
	PawnItemCacheMaxSize = 500 -- temporarily increase maximum cache size for performance reasons
	
	-- Start with the best items that we already know of.  Obviously in most cases, your existing best items
	-- are already your best ones.  This is a persistent per-scale per-character option, not just per-scale.
	local BestItems = CharacterOptions.BestItems
	if not BestItems then
		BestItems = { }
		InventoryOnly = false -- need to scan everything if we don't have existing valid data
		CharacterOptions.BestItems = BestItems
	end
	-- = { ["INVTYPE_HEAD"] = { 123.45, "item:12345:0:0:3", heirloom level }, ...,  ["INVTYPE_FINGER"] = { 67.8, "item:67890:0:0:3", level, 50.50, second item link, second level }, ... }
	-- (Include a second-best item for INVTYPE_FINGER and INVTYPE_WEAPON)
	-- (ugh, if you ever have to change this again in the future, don't do raw array indices again... super-pain to add another index...)
	
	
	-- Helper function to check an item to see if it should go into BestItems, since we'll be performing this
	-- operation in different ways from multiple blocks of code.
	-- Returns true if the item is a new best or second-best item.
	local function CheckItem(ScaleName, BestItems, Item, PreviousItemLink)
		-- Skip trinkets because we can't reliably tell which trinkets are best.
		-- Also skip item classes that don't have stats, and items that have a zero value.
		if not Item then return end
		-- Never show upgrade information for artifacts.  Preventing them from going into your best item list
		-- ensures that you don't have to worry about an unequippable Havoc artifact being considered your best Vengeance
		-- item before you get your actual Vengeance artifact.
		if Item.Rarity == 6 then return end
		local InvType = Item.InvType
		if not InvType or InvType == "" or InvType == "INVTYPE_TRINKET" or InvType == "INVTYPE_BAG" or InvType == "INVTYPE_QUIVER" or InvType == "INVTYPE_TABARD" or InvType == "INVTYPE_BODY" then return end
		local _, Value = PawnGetSingleValueFromItem(Item, ScaleName)
		if Value <= 0 then return end
		local UnenchantedItemLink = PawnUnenchantItemLink(Item.Link, true)
		VgerCore.Assert(UnenchantedItemLink ~= nil, "PawnFindBestItems's CheckItem lambda failed to get an item link for item " .. tostring(Item.ID))
		
		-- For purposes of upgrades, treat all items that go in the off-hand as off-hand weapons.
		if InvType == "INVTYPE_SHIELD" or InvType == "INVTYPE_HOLDABLE" then
			InvType = "INVTYPE_WEAPONOFFHAND"
		elseif InvType == "INVTYPE_ROBE" then
			InvType = "INVTYPE_CHEST"
		elseif InvType == "INVTYPE_RANGED" or InvType == "INVTYPE_RANGEDRIGHT" then
			-- A ranged weapon could be one-handed (wands) or two-handed (everything else) but it always goes in the main hand.
			InvType = "INVTYPE_WEAPONMAINHAND"
		end
		
		-- Okay, now do the calculations.
		local BestOfType = BestItems[InvType]
		if BestOfType == nil or BestOfType[1] == nil or Value > (BestOfType[1] + PawnEpsilon) then
			-- This item's an upgrade.
			if BestOfType == nil then
				BestOfType = { }
				BestItems[InvType] = BestOfType
			end
			if InvType == "INVTYPE_FINGER" or InvType == "INVTYPE_WEAPON" then
				-- If this is a ring or weapon, keep the current best item as a second-best item.
				BestOfType[4] = BestOfType[1]
				BestOfType[5] = BestOfType[2]
				BestOfType[6] = BestOfType[3]
			end
			BestOfType[1] = Value
			BestOfType[2] = UnenchantedItemLink
			BestOfType[3] = PawnGetMaxLevelItemIsUsefulHeirloom(Item)
			return true
		elseif
			-- Only rings and one-handed weapons have second-best versions tracked.
			(InvType == "INVTYPE_FINGER" or InvType == "INVTYPE_WEAPON") and
			-- If we don't have a second item stored at all, this is obviously the second best.
			-- Otherwise, the new item's value must be greater than the current second best.
			(BestOfType[4] == nil or Value > (BestOfType[4] + PawnEpsilon)) and
			-- The item's ID must be different from the best item of this type, OR it must be the same
			-- as the last item that was scanned, indicating that the player has two copies of that item.
			-- Otherwise, we assume that the player only has one, so it can't be both first and second best.
			(UnenchantedItemLink == PreviousItemLink or UnenchantedItemLink ~= BestOfType[2])
		then
			-- This item's an upgrade of the current second-best item.
			BestOfType[4] = Value
			BestOfType[5] = UnenchantedItemLink
			BestOfType[6] = PawnGetMaxLevelItemIsUsefulHeirloom(Item)
			return true
		end
		return false
	end
	-- (end of CheckItem)
	
	-- Obviously, check the player's currently equipped gear.
	local Slot, PreviousItemLink
	for Slot = 1, 17 do if Slot ~= 4 and Slot ~= 13 and Slot ~= 14 then -- Skip slots 0 (ammo), 4 (shirt), 13-14 (trinkets), 18 (ranged/relic), and 19 (tabard)
		local Item = PawnGetItemDataForInventorySlot(Slot, true, "player")
		if Item then
			CheckItem(ScaleName, BestItems, Item, PreviousItemLink)
			PreviousItemLink = PawnUnenchantItemLink(Item.Link, true)
		end
	end end
	
	-- Now, scan all of the items in the player's equipment sets.
	if not InventoryOnly then
		local _, i
		for _, i in pairs(C_EquipmentSet.GetEquipmentSetIDs()) do
			local _, _, EquipmentSetID = C_EquipmentSet.GetEquipmentSetInfo(i)
			local ItemLocations = C_EquipmentSet.GetItemLocations(EquipmentSetID)
			PreviousItemLink = nil
			for Slot = 1, 17 do if Slot ~= 4 and Slot ~= 13 and Slot ~= 14 then
				local Location = ItemLocations[Slot]
				if Location and Location > 1 then
					-- Getting the item link for an equipment set item is a pain in the ass...
					local ItemLink
					local IsOnPlayer, IsInBank, IsInBags, IsInVoidStorage, Slot, Bag, Tab, VoidSlot = EquipmentManager_UnpackLocation(Location)
					if IsInVoidStorage then
						-- The item link for this item should be GetVoidItemHyperlinkString(VoidSlot), but we'll never get here; location will
						-- be -1 (item unavailable) for items in void storage.
						ItemLink = nil --GetVoidItemHyperlinkString(VoidSlot) -- API no longer exists in 8.0
						VgerCore.Fail("Didn't expect to find an equipment set item in void storage!")
					elseif not IsInBags then
						VgerCore.Assert(IsOnPlayer or IsInBank, "Equipment set contains new location data that Pawn doesn't understand; EquipmentManager_UnpackLocation may have been updated.")
						ItemLink = GetInventoryItemLink("player", Slot)
					else
						ItemLink = GetContainerItemLink(Bag, Slot)
					end

					-- Now that we have an item link we can proceed as usual.
					if ItemLink then
						local Item = PawnGetItemData(PawnUnenchantItemLink(ItemLink, true))
						CheckItem(ScaleName, BestItems, Item, PreviousItemLink)
					end
				end
				PreviousItemLink = ItemLink
			end end
			wipe(ItemLocations)
		end
	end

	-- Now we've scanned all of the items we're going to scan.  Next we have to assign out one-handed items to the main
	-- hand and off-hand slots as appropriate.
	PawnAssignOneHandedBestItems(Scale, BestItems)
	
	-- Finally, to test this out, print out the list of best items to the console.
	--VgerCore.Message("------------------------------------------------------------")
	--VgerCore.Message(VgerCore.Color.Blue .. "Pawn found the best items for " .. PawnGetScaleLocalizedName(ScaleName) .. ".")
	--VgerCore.Message(" ")
	--local InvType, BestOfType
	--for InvType, BestOfType in pairs(BestItems) do
	--	local _, ItemLink = GetItemInfo(BestOfType[2])
	--	VgerCore.Message(InvType .. ": " .. ItemLink .. " = " .. tostring(BestOfType[1]))
	--	if BestOfType[4] then
	--		_, ItemLink = GetItemInfo(BestOfType[5])
	--		VgerCore.Message("    and " .. ItemLink .. " = " .. BestOfType[4])
	--	end
	--end
	--VgerCore.Message(" ")
	--VgerCore.Message(string.format("The item cache now contains %s items.", #PawnItemCache))
	
	-- Restore the original cache size.  The cache will be trimmed the next time we try to add an item to it.
	PawnItemCacheMaxSize = OldCacheSize
end

-- Helper function to assign one-handed weapons to the main hand and off-hand best item slots.
function PawnAssignOneHandedBestItems(Scale, BestItems)
	-- First, get the values of the four weapons we're considering.
	local BestMH, BestOH, Best1H, SecondBest1H = 0, 0, 0, 0
	if BestItems.INVTYPE_WEAPONMAINHAND then BestMH = BestItems.INVTYPE_WEAPONMAINHAND[1] or 0 end
	if BestItems.INVTYPE_WEAPONOFFHAND then BestOH = BestItems.INVTYPE_WEAPONOFFHAND[1] or 0 end
	if BestItems.INVTYPE_WEAPON then
		-- Before continuing, if the best one-hand weapons have already been assigned to the main hand and off
		-- hand slots, remove them.  (This needs to happen in two separate phases; if a weapon appears in both one-hand
		-- slots, then having it as the best main hand item should not remove it from both slots.)
		if BestItems.INVTYPE_WEAPONMAINHAND and BestItems.INVTYPE_WEAPONMAINHAND[2] == BestItems.INVTYPE_WEAPON[2] then
			BestItems.INVTYPE_WEAPON[1] = BestItems.INVTYPE_WEAPON[4]
			BestItems.INVTYPE_WEAPON[2] = BestItems.INVTYPE_WEAPON[5]
			BestItems.INVTYPE_WEAPON[3] = BestItems.INVTYPE_WEAPON[6]
			BestItems.INVTYPE_WEAPON[4] = nil
			BestItems.INVTYPE_WEAPON[5] = nil
			BestItems.INVTYPE_WEAPON[6] = nil
		elseif BestItems.INVTYPE_WEAPONMAINHAND and BestItems.INVTYPE_WEAPONMAINHAND[2] == BestItems.INVTYPE_WEAPON[5] then
			BestItems.INVTYPE_WEAPON[4] = nil
			BestItems.INVTYPE_WEAPON[5] = nil
			BestItems.INVTYPE_WEAPON[6] = nil
		end
		if BestItems.INVTYPE_WEAPONOFFHAND and BestItems.INVTYPE_WEAPONOFFHAND[2] == BestItems.INVTYPE_WEAPON[2] then
			BestItems.INVTYPE_WEAPON[1] = BestItems.INVTYPE_WEAPON[4]
			BestItems.INVTYPE_WEAPON[2] = BestItems.INVTYPE_WEAPON[5]
			BestItems.INVTYPE_WEAPON[3] = BestItems.INVTYPE_WEAPON[6]
			BestItems.INVTYPE_WEAPON[4] = nil
			BestItems.INVTYPE_WEAPON[5] = nil
			BestItems.INVTYPE_WEAPON[6] = nil
		elseif BestItems.INVTYPE_WEAPONOFFHAND and BestItems.INVTYPE_WEAPONOFFHAND[2] == BestItems.INVTYPE_WEAPON[5] then
			BestItems.INVTYPE_WEAPON[4] = nil
			BestItems.INVTYPE_WEAPON[5] = nil
			BestItems.INVTYPE_WEAPON[6] = nil
		end
		
		Best1H = BestItems.INVTYPE_WEAPON[1] or 0
		SecondBest1H = BestItems.INVTYPE_WEAPON[4] or 0
	end
	
	-- Also check if the user is allowed to put one-handed weapons in the off hand.  (Remember that shields
	-- and off-hand frill have been converted into off-hand weapons, and real off-hand weapons will have a
	-- value of zero if they can't be wielded.)
	local CanUseOH = (Scale.Values.IsOffHand == nil or Scale.Values.IsOffHand > PawnIgnoreStatValue)
	-- Now, find which of the scenarios produces the largest final value.
	if		CanUseOH and
			(Best1H + SecondBest1H) >= (Best1H + BestOH) and
			(Best1H + SecondBest1H) >= (BestMH + Best1H) and
			(Best1H + SecondBest1H) >= (BestMH + BestOH)
	then
		-- The best scenario is to use both of the one-handed weapons.
		if BestItems.INVTYPE_WEAPON and BestItems.INVTYPE_WEAPON[1] then
			BestItems.INVTYPE_WEAPONMAINHAND = { BestItems.INVTYPE_WEAPON[1], BestItems.INVTYPE_WEAPON[2], BestItems.INVTYPE_WEAPON[3] }
		else
			BestItems.INVTYPE_WEAPONMAINHAND = nil
		end
		if BestItems.INVTYPE_WEAPON and BestItems.INVTYPE_WEAPON[4] then
			BestItems.INVTYPE_WEAPONOFFHAND = { BestItems.INVTYPE_WEAPON[4], BestItems.INVTYPE_WEAPON[5], BestItems.INVTYPE_WEAPON[6] }
		else
			BestItems.INVTYPE_WEAPONOFFHAND = nil
		end
	elseif	CanUseOH and
			((Best1H + BestOH) >= (Best1H + SecondBest1H) and
			(Best1H + BestOH) >= (BestMH + Best1H) and
			(Best1H + BestOH) >= (BestMH + BestOH))
		or ((not CanUseOH) and
			(Best1H + BestOH) >= (BestMH + BestOH))
	then
		-- The best scenario is to use the best one-handed weapon in the main hand with the best off-hand weapon.
		if BestItems.INVTYPE_WEAPON and BestItems.INVTYPE_WEAPON[1] then
			BestItems.INVTYPE_WEAPONMAINHAND = { BestItems.INVTYPE_WEAPON[1], BestItems.INVTYPE_WEAPON[2], BestItems.INVTYPE_WEAPON[3] }
		else
			BestItems.INVTYPE_WEAPONMAINHAND = nil
		end
	elseif	CanUseOH and
			(BestMH + Best1H) >= (Best1H + SecondBest1H) and
			(BestMH + Best1H) >= (Best1H + BestOH) and
			(BestMH + Best1H) >= (BestMH + BestOH)
	then
		-- The best scenario is to use the best main hand weapon with the best one-handed weapon in the off hand.
		if BestItems.INVTYPE_WEAPON and BestItems.INVTYPE_WEAPON[1] then
			BestItems.INVTYPE_WEAPONOFFHAND = { BestItems.INVTYPE_WEAPON[1], BestItems.INVTYPE_WEAPON[2], BestItems.INVTYPE_WEAPON[3] }
		else
			BestItems.INVTYPE_WEAPONOFFHAND = nil
		end
	else
		-- The best scenario is to use the best main and off-hand weapons and ignore the one-handed weapons.
	end
	BestItems.INVTYPE_WEAPON = nil
end

-- Comparer function for use in sort that sorts items by their name.
function PawnItemComparer(a, b)
	if not b then return a end
	if not a then return b end
	return a.Name < b.Name
end

-- Called whenever the player's inventory changed.  We need to check their currently-equipped items whenever this happens.
PawnOnInventoryChanged = VgerCore.Throttle(.250, function()
	-- Ignore inventory change events before we've finished loading.
	if not PawnIsInitialized then return end
	if not PawnScaleProvidersInitialized then return end
	-- Ignore equipment change events when the player is in combat: they're unlikely to equip a NEW item in combat,
	-- and we wouldn't want this procedure to slow them down even a little.
	if InCombatLockdown() then return end

	PawnCheckInventoryForUpgrades()
end)

-- Called whenever the player vendors or destroys an item.  We need to check their best-item sets for that item and remove it
-- if they just got rid of one of their best items.
function PawnOnItemLost(ItemLink)
	if not ItemLink then return end
	ItemLink = PawnUnenchantItemLink(ItemLink, true)
	local _, _, _, _, _, _, _, _, InvType = GetItemInfo(ItemLink)
	if not InvType or InvType == "" or InvType == "INVTYPE_TRINKET" or InvType == "INVTYPE_BAG" or InvType == "INVTYPE_QUIVER" or InvType == "INVTYPE_TABARD" or InvType == "INVTYPE_BODY" then return end
	if InvType == "INVTYPE_SHIELD" or InvType == "INVTYPE_HOLDABLE" then
		InvType = "INVTYPE_WEAPONOFFHAND"
	elseif InvType == "INVTYPE_ROBE" then
		InvType = "INVTYPE_CHEST"
	elseif InvType == "INVTYPE_RANGED" or InvType == "INVTYPE_RANGEDRIGHT" then
		-- A ranged weapon could be one-handed (wands) or two-handed (everything else) but it always goes in the main hand.
		InvType = "INVTYPE_WEAPONMAINHAND"
	end
	local InvType2 = nil
	if InvType == "INVTYPE_WEAPON" then
		InvType = "INVTYPE_WEAPONMAINHAND"
		InvType2 = "INVTYPE_WEAPONOFFHAND"
	end
	
	local ScaleName, Scale
	for ScaleName, Scale in pairs(PawnCommon.Scales) do
		local CharacterOptions = Scale.PerCharacterOptions[PawnPlayerFullName]
		if CharacterOptions then
			local BestItems = CharacterOptions.BestItems
			if BestItems then
				local RemovedItem = false
				-- Okay, this scale has a list of best items.  Look through the list and remove this item if present.
				local ItemsForSlot = BestItems[InvType]
				if ItemsForSlot and (ItemsForSlot[2] == ItemLink or ItemsForSlot[5] == ItemLink) then
					RemovedItem = true
				elseif InvType2 then
					ItemsForSlot = BestItems[InvType2]
					if ItemsForSlot and (ItemsForSlot[2] == ItemLink or ItemsForSlot[5] == ItemLink) then
						RemovedItem = true
					end
				end
				-- If they lost an item from this scale's list of best items, we need to invalidate the best items list for the scale.
				if RemovedItem then
					--VgerCore.Message(VgerCore.Color.Blue .. "Pawn: Warning! You just lost your best " .. _G[InvType] .. " from " .. PawnGetScaleLocalizedName(ScaleName) .. ". :(")
					CharacterOptions.BestItems = nil
				end
			end
		end
	end

	for Slot = 1, 18 do
		if PawnOptions.ItemLevels[Slot] and PawnOptions.ItemLevels[Slot].Link == ItemLink then
			if PawnCommon.ShowSlotDebugInfo then
				VgerCore.Message(Item.Link .. " was destroyed but it was your best in slot " .. Slot .. ", so clearing.")
			end
			PawnOptions.ItemLevels[Slot] = nil
		end
	end
end

-- When an item is locked due to being picked up, remember its ID in case they decide to delete it.
function PawnOnItemLocked(arg1, arg2)
	local ItemLink
	if arg2 == nil then
		ItemLink = GetInventoryItemLink("player", arg1)
	else
		ItemLink = GetContainerItemLink(arg1, arg2)
	end
	if ItemLink then
		PawnLastCursorItemLink = PawnUnenchantItemLink(ItemLink, true)
	else
		PawnLastCursorItemLink = nil
	end
end

-- Takes a list of items and trims it down to only those items that are interesting upgrades.  If none are upgrades, then
-- one of the items is chosen as most valuable.
-- Arguments: List
--	List: A table of items in this format: { Item, RewardType, Usable }
--		Item: An item data table.
--		RewardType: "reward" or "choice".  If "reward", the player is guaranteed to get this item, and it should not be selected as most valuable.
--		Usable: true or false.  If false, the player cannot currently use this item and thus it should not be presented as an upgrade.
--		Additional fields can be present.
-- Returns: List
--	List: The same table passed in, with one additional field added: { ..., Result }
--		Result: "upgrade" if the item is an upgrade, "vendor" if the item is the most valuable choice, "trinket" if the item is a trinket or relic, or nil if none of the above.
function PawnFindInterestingItems(List)
	local Info, _
	local HighestValue, HighestValueInfo = 0, nil
	local DoNotVendor
	
	for _, Info in pairs(List) do
		if Info.Item.InvType == "INVTYPE_TRINKET" or (Info.Item.ID and (IsArtifactRelicItem and IsArtifactRelicItem(Info.Item.ID))) then
			Info.Result = "trinket"
		end
		local UpgradeInfo, ItemLevelIncrease = PawnIsItemAnUpgrade(Info.Item)
		if Info.Usable and (UpgradeInfo or (PawnCommon.ShowItemLevelUpgrades and ItemLevelIncrease)) then
			-- If it's usable and an upgrade, mark it as such.
			Info.Result = "upgrade"
			-- If it's a choice item, then we shouldn't pick a choice item to vendor.
			if Info.RewardType == "choice" then DoNotVendor = true end
		elseif Info.RewardType == "choice" and (Info.Item.InvType == "INVTYPE_TRINKET" or (Info.Item.ID and (IsArtifactRelicItem and IsArtifactRelicItem(Info.Item.ID)))) then
			-- If one of the choices is a trinket, don't mark any items as vendor items.
			DoNotVendor = true
		elseif (not DoNotVendor) and Info.RewardType == "choice" then
			-- If we haven't already found a choice item upgrade, and this is a choice item, see
			-- if it's the best thing to vendor.
			local _, _, _, _, _, _, _, _, _, _, Value = GetItemInfo(Info.Item.Link)
			if Value and Value > HighestValue then
				HighestValue = Value
				HighestValueInfo = Info
			end
		end
	end
	
	if (not DoNotVendor) and HighestValueInfo then
		HighestValueInfo.Result = "vendor"
	end
	
	return List
end

-- Given an item table, returns true if the item is of the player's armor specialization OR if it's not armor
-- at all.  Returns false if it's the wrong type of armor for the current character's class and level.
function PawnIsArmorBestTypeForPlayer(Item)
	local Stats = Item.UnenchantedStats
	if not Stats then return false end
	-- If the item isn't armor then we don't need to check anything.	
	if (not Stats.IsCloth) and (not Stats.IsLeather) and (not Stats.IsMail) and (not Stats.IsPlate) then return true end
	-- Before level 50 it's fine if the player is wearing the wrong type of armor.
	local Level = UnitLevel("player")
	local IsLevelForSpecialization = (Level >= 50)
	-- Now, the rest depends on the user's class.
	local _, Class = UnitClass("player")
	if Class == "MAGE" or Class == "PRIEST" or Class == "WARLOCK" then
		if Stats.IsCloth then return true else return false end
	elseif Class == "DRUID" or Class == "ROGUE" or Class == "MONK" or Class == "DEMONHUNTER" then
		if IsLevelForSpecialization then
			if Stats.IsLeather then return true else return false end
		elseif Stats.IsLeather or Stats.IsCloth then
			return true
		else
			return false
		end
	elseif Class == "HUNTER" or Class == "SHAMAN" then
		if IsLevelForSpecialization then
			if Stats.IsMail then return true else return false end
		elseif Stats.IsMail or Stats.IsLeather or Stats.IsCloth then
			return true
		else
			return false
		end
	elseif Class == "DEATHKNIGHT" or Class == "PALADIN" or Class == "WARRIOR" then
		if IsLevelForSpecialization then
			if Stats.IsPlate then return true else return false end
		else
			return true
		end
	end
	VgerCore.Fail("PawnIsArmorBestTypeForPlayer didn't know how to handle this item.")
end

-- Appends the strings in a table together with commas and a conjunction ("or ") as appropriate.  The conjunction can be nil, but if it isn't, it should end in a space.
function PawnConcatenateWithConjunction(Table, Conjunction)
	local Size = #Table
	if Conjunction == nil then Conjunction = "" end
	if Size == 0 then
		return ""
	elseif Size == 1 then
		return Table[1]
	elseif Size == 2 then
		return Table[1] .. " " .. Conjunction .. Table[2]
	else
		local Concatenated = ""
		local Index = 1
		local Item, _
		for _, Item in ipairs(Table) do
			if Index == Size then
				Concatenated = Concatenated .. Conjunction .. Item
			else
				Concatenated = Concatenated .. Item .. ", "
			end
			Index = Index + 1
		end
		return Concatenated
	end
end

-- Returns the maximum level an item is a useful heirloom item, or 0 if it never is.  As long as the player's level
-- is equal to or less than this number, this item is always considered superior to other items that don't meet
-- these same requirements.
function PawnGetMaxLevelItemIsUsefulHeirloom(Item)
	if Item.Rarity == 6 then
		-- This is an artifact, so the player won't get anything better until level 110 at the earliest.
		-- (Battle for Azeroth leveling dungeons provide weapons that would be higher ilvl than most players' artifacts.)
		return 109
	elseif Item.UnenchantedStats and Item.UnenchantedStats.MaxScalingLevel then
		-- This item scales until you reach MaxScalingLevel.
		return Item.UnenchantedStats.MaxScalingLevel - 1
	else
		-- This item doesn't scale.
		return 0
	end
end

function PawnIsItemAnArtifact(Item)
	if Item and Item.Rarity and Item.Rarity == 6 then return true else return false end
end

function PawnOnSpecChanged()
	if not PawnIsReady() then return end

	-- Don't do anything if they've turned off auto-scales.
	if not PawnOptions.AutoSelectScales then return end

	local _, _, ClassID = UnitClass("player")
	local SpecID = GetSpecialization()

	-- Disable all scales that don't match the current spec, activate any that do, and then select one
	-- of them in the UI.
	-- Right now, we only take scales from a provider into account, because some code assumes that only one
	-- scale can ever be enabled in Automatic mode. 
	local ScaleName, Scale, LastEnabledScaleName, MadeChanges
	for ScaleName, Scale in pairs(PawnCommon.Scales) do
		local IsVisible = PawnIsScaleVisible(ScaleName) 
		if Scale.ClassID == ClassID and Scale.SpecID == SpecID and Scale.Provider ~= nil then
			if not IsVisible then
				PawnSetScaleVisible(ScaleName, true)
				LastEnabledScaleName = ScaleName
				MadeChanges = true
			end
		else
			if IsVisible then
				PawnSetScaleVisible(ScaleName, false)
				MadeChanges = true
			end
		end
	end
	if MadeChanges then
		-- Don't reset the UI if their spec didn't actually change—this notification can be a bit spammy.
		PawnClearCache()
		PawnInvalidateBestItems()

		PawnUICurrentScale = nil -- Let the refresh method re-set this
		PawnUIFrame_ScaleSelector_Refresh()
		PawnUI_SelectScale(PawnUICurrentScale)
	end
end

function PawnEnableAllScalesForClass()
	local _, _, ClassID = UnitClass("player")
	local ScaleName, Scale, LastEnabledScaleName
	for ScaleName, Scale in pairs(PawnCommon.Scales) do
		PawnSetScaleVisible(ScaleName, Scale.ClassID == ClassID)
	end
	PawnUIFrame_ScaleSelector_Refresh()
end

-- Returns the name of a scale that's designed for the specified class and spec, if there is one,
-- or nil if there isn't.
function PawnFindScaleForSpec(ClassID, SpecID)
	if (not ClassID) or (not SpecID) then return nil end

	local ScaleName, Scale
	for ScaleName, Scale in pairs(PawnCommon.Scales) do
		if Scale.ClassID == ClassID and Scale.SpecID == SpecID and Scale.Provider then return ScaleName end
	end

	return nil
end

-- Called whenever the artifact UI is used.
function PawnOnArtifactUpdated(NewItem)
	-- Compatibility fix with AethysRotation and any other addon that scans artifacts at startup
	if not PawnOptions then return nil end

	-- Get details about this artifact and then cache them.
	local ArtifactItemID, _, ArtifactName = C_ArtifactUI.GetArtifactInfo()
	if not ArtifactItemID then return end
	-- Workaround: the hunter artifact Talonclaw has a longer name than GetArtifactInfo() returns, so when it's seen on tooltips, the strings don't match.  Use the longer name if available.
	--   Item and GetArtifactInfo return: "Talonclaw"
	--   Artifact UI and relic tooltips show: "Talonclaw, Spear of the Wild Gods"
	ArtifactName = C_ArtifactUI.GetArtifactArtInfo().titleName or ArtifactName

	local Artifacts = PawnOptions.Artifacts
	if not Artifacts then
		Artifacts = {}
		PawnOptions.Artifacts = Artifacts
	end	
	local ThisArtifact = Artifacts[ArtifactItemID]
	if not ThisArtifact then
		ThisArtifact = { ["Relics"] = {} }
		Artifacts[ArtifactItemID] = ThisArtifact
	end

	ThisArtifact.Name = ArtifactName

	local NumRelicSlots = C_ArtifactUI.GetNumRelicSlots() or 0
	local RelicIndex
	for RelicIndex = 1, NumRelicSlots do
		local _, _, _, ThisRelicItemLink = C_ArtifactUI.GetRelicInfo(RelicIndex)
		local LockedReason = C_ArtifactUI.GetRelicLockedReason(RelicIndex)

		if LockedReason == nil then
			-- Only count relic slots that are unlocked.  (Previously, this had "or UnitLevel("player") >= 110", but that ended up being annoying.)
			local ThisRelic = ThisArtifact.Relics[RelicIndex]
			if not ThisRelic then
				ThisRelic = {}
				ThisArtifact.Relics[RelicIndex] = ThisRelic
			end
			ThisRelic.Type = C_ArtifactUI.GetRelicSlotType(RelicIndex)
			if ThisRelicItemLink then
				-- And, there's a relic for this slot.
				local RelicItemLevel = PawnGetItemLevelIncreaseProvidedByRelic(ThisRelicItemLink)
				ThisRelic.ItemLevel = RelicItemLevel
			end
			-- It's possible for us to not get an item link for the exiting relic if this is called right after logging in (because, for example, another
			-- addon is simulating the shift-right-click).  In that case, we want to just keep whatever we already had cached—don't clear it out.
		else
			-- This relic slot is locked.  If we have anything saved for this slot, clear it out; it was probably saved due to a bug in an earlier Pawn version.
			ThisArtifact.Relics[RelicIndex] = nil
		end

	end
end

-- This function does the same as C_ArtifactUI.GetItemLevelIncreaseProvidedByRelic, but provides two fixes:
-- (1) It works in WoW 7.2, which the in-game method doesn't
-- (2) It works on level-scaled relics, which GetItemStats doesn't
--
-- Also, this method ignores bonus item levels imbued by the Netherlight Crucible.
local PawnTempRelicTable = {}
function PawnGetItemLevelIncreaseProvidedByRelic(ItemLink)
	local Parts = PawnTempRelicTable
	wipe(Parts)

	PawnGetItemLinkParts(ItemLink, Parts)
	-- GetItemStats() on a relic that was looted at 110 and then passed on to a lower-level character will return the stats for the item as if it were dropped at the current, lower level, even though it has the proper stats for a level 110 relic and has a level 110 requirement.  So, adjust the item link to work around that GetItemStats bug.
	local NumParts = #Parts
	if NumParts >= 14 then
		-- Copy the looted ilvl portion of the item link to the player ilvl part of the link.
		local NumBonusIDs = Parts[13]
		local ScalingLevelIndex = 13 + NumBonusIDs + 1
		if NumParts >= ScalingLevelIndex then
			local ScalingLevel = Parts[ScalingLevelIndex]
			if ScalingLevel >= 98 and ScalingLevel <= 110 then
				Parts[9] = ScalingLevel
				ItemLink = PawnGetItemLinkFromParts(Parts)
			else
				-- 0 is a normal case, for non-scaling relics, which don't have this problem and don't need adjustment.
				VgerCore.Assert(ScalingLevel == 0, "Relic level adjustment may have done the wrong thing: changing from " .. tostring(Parts[9]) .. " to " .. tostring(ScalingLevel) .. " due to NumBonusIDs = " .. NumBonusIDs)
			end
		end
	end

	local Stats = Parts
	wipe(Stats)
	Stats = GetItemStats(ItemLink, Stats)
	return Stats.RELIC_ITEM_LEVEL_INCREASE
end

function PawnGetItemLinkParts(ItemLink, ReusableTable)
   local Parts = ReusableTable or {}
   local Match
   for Match in gmatch(ItemLink, ":(%-?%d*)") do
      if strlen(Match) == 0 then
         tinsert(Parts, 0)
      else
         tinsert(Parts, tonumber(Match))
      end
   end
   return Parts
end

function PawnGetItemLinkFromParts(Parts)
	return "item:" .. table.concat(Parts, ":")
end

function PawnPrintArtifactDebugInfo()
	VgerCore.Message(" ")
	VgerCore.Message("Pawn knows about these artifacts:")
	local ArtifactItemID, Artifact, UpgradeInfo
	for ArtifactItemID, Artifact in pairs(PawnOptions.Artifacts) do
		VgerCore.Message(ITEM_QUALITY_COLORS[LE_ITEM_QUALITY_ARTIFACT].hex .. Artifact.Name .. "|r:")
		for RelicIndex = 1, 3 do
			local ThisRelic = Artifact.Relics[RelicIndex]
			if ThisRelic then
				local ContentsString = "  Slot " .. RelicIndex .. ": " .. tostring(ThisRelic.Type) .. ", "
				if ThisRelic.ItemLevel then
					ContentsString = ContentsString .. "+" .. ThisRelic.ItemLevel .. " item levels"
				else
					ContentsString = ContentsString .. "empty"
				end
				VgerCore.Message(ContentsString)
			end
		end
	end
end

function PawnGetRelicUpgradeInfo(RelicItemLink)
	-- If we haven't cached any artifacts yet, this can't possibly be an upgrade.
	if not PawnOptions.Artifacts then return end

	local RelicItemID = GetItemInfoInstant(RelicItemLink)
	local _, _, RelicType = C_ArtifactUI.GetRelicInfoByItemID(RelicItemID)
	if not RelicType then return end
	local RelicItemLevel = PawnGetItemLevelIncreaseProvidedByRelic(RelicItemLink)
	if not RelicItemLevel then return end

	local ArtifactItemID, Artifact, UpgradeInfo
	for ArtifactItemID, Artifact in pairs(PawnOptions.Artifacts) do
		local BestRelicItemLevelUpgrade = 0
		local RelicIndex, SlottedRelic
		for RelicIndex, SlottedRelic in pairs(Artifact.Relics) do
			--VgerCore.Message(Artifact.Name .. " slot " .. RelicIndex .. ": " .. SlottedRelic.Type .. " +" .. tostring(SlottedRelic.ItemLevel))
			if RelicType == SlottedRelic.Type then
				local ThisRelicItemLevelUpgrade = RelicItemLevel - (SlottedRelic.ItemLevel or 0)
				--VgerCore.Message("   Increase found: " .. ThisRelicItemLevelUpgrade)
				if ThisRelicItemLevelUpgrade > BestRelicItemLevelUpgrade then BestRelicItemLevelUpgrade = ThisRelicItemLevelUpgrade end
			end 
		end
		if BestRelicItemLevelUpgrade > 0 then
			-- Hooray! This relic is an item level upgrade.
			if not UpgradeInfo then UpgradeInfo = {} end
			UpgradeInfo[Artifact.Name] = { ["ItemLevelIncrease"] = BestRelicItemLevelUpgrade }
		end
	end

	return UpgradeInfo
end

function PawnAddRelicUpgradesToTooltip(TooltipName, UpgradeInfo)
	if not UpgradeInfo then return end
	local Tooltip = _G[TooltipName]
	if not Tooltip then
		VgerCore.Fail("Unable to update tooltip " .. tostring(TooltipName) .. " with relic upgrade info because we couldn't find it")
		return
	end

	local i
	local Lines = Tooltip:NumLines()
	for i = 1, Lines do
		local LeftLine = _G[TooltipName .. "TextLeft" .. i]
		local ArtifactName = LeftLine:GetText()

		local ArtifactUpgradeInfo = UpgradeInfo[ArtifactName]
		if ArtifactUpgradeInfo then
			if PawnCommon.AlignNumbersRight then
				local RightLine = _G[TooltipName .. "TextRight" .. i]
				RightLine:SetText(format(PawnLocal.TooltipRelicUpgradeAnnotation, "", ArtifactUpgradeInfo.ItemLevelIncrease, ""))
				RightLine:Show()
			else
				LeftLine:SetText(format(PawnLocal.TooltipRelicUpgradeAnnotation, tostring(ArtifactName) .. ":", ArtifactUpgradeInfo.ItemLevelIncrease, ""))
			end
		end
	end
end

function PawnAddItemToLevelTracker(Item)
	-- PawnOptions.ItemLevels = {
	--   [1] = {
	--      [1] = {
	--        ["Link"] = "|Hitem:12345:...",
	--        ["ID"] = 12345,
	--        ["Level"] = 450,
	--        -- ["AlsoFitsIn"] = 17, -- for items that can fit in a second slot
	--      },
	--      -- [2] also present for ring/trinket/weapon
	-- 	  }
	--   ...
	--   [18] -- one entry for each slot 1-18, skipping 14
	-- }

	if not Item then
		return
	end

	local Slot1, Slot2 = PawnGetSlotsForItemType(Item.InvType)
	if not Slot1 then return end
	if not Item.Level then return end

	if not PawnOptions.ItemLevels then
		PawnOptions.ItemLevels = {}
	end

	-- First, check to see if we already know about this item.
	local Known1 = PawnOptions.ItemLevels[Slot1]
	if Known1 and Item.Link == Known1.Link then return end
	local Known2 = PawnOptions.ItemLevels[Slot2]
	if Known2 and Item.Link == Known2.Link then return end

	-- Okay, it's an equippable item that we aren't tracking, so check it.
	local PutNewItemInSlot
	if not Known1 then
		-- There's not already an item in that slot, so let's put this one there.
		PutNewItemInSlot = Slot1
	elseif Item.Level > Known1.Level then
		-- This item is better than their current best in the primary slot.
		-- Before overwriting it, let's see if that item needs to be bumped down to another slot.
		local BumpSlot = Known1.AlsoFitsIn
		if BumpSlot and (PawnOptions.ItemLevels[BumpSlot] == nil or Known1.Level > PawnOptions.ItemLevels[BumpSlot].Level) then
			-- Yes, bump the existing item in the primary slot down to the other slot it fits in.
			if PawnCommon.ShowSlotDebugInfo then
				VgerCore.Message("Bumping " .. Known1.Link .. " down from slot " .. Slot1 .. " to " .. Known1.AlsoFitsIn .. ".")
			end
			PawnOptions.ItemLevels[BumpSlot] = Known1
			PawnOptions.ItemLevels[BumpSlot].AlsoFitsIn = Slot1
		end
		-- Okay, now put this item in our records for this slot.
		PutNewItemInSlot = Slot1
	elseif Slot2 and not Known2 then
		PutNewItemInSlot = Slot2
	elseif Slot2 and Item.Level > Known2.Level then
		-- This item is better than their current best in the secondary slot.
		-- But before overwriting, check to see if it needs to be bumped UP to the primary slot: rare, but possible.
		local BumpSlot = Known2.AlsoFitsIn
		if BumpSlot and (PawnOptions.ItemLevels[BumpSlot] == nil or Known2.Level > PawnOptions.ItemLevels[BumpSlot].Level) then
			-- Yes, bump the existing item in the primary slot down to the other slot it fits in.
			if PawnCommon.ShowSlotDebugInfo then
				VgerCore.Message("Bumping " .. Known2.Link .. " up from slot " .. Slot2 .. " to " .. Known2.AlsoFitsIn .. ".")
			end
			PawnOptions.ItemLevels[BumpSlot] = Known2
			PawnOptions.ItemLevels[BumpSlot].AlsoFitsIn = Slot2
		end
		-- Okay, now put this item in our records for this slot.
		PutNewItemInSlot = Slot2
	end

	-- TODO: *** Rings and trinkets with the same ID should only be able to upgrade weaker versions of the same item

	if PutNewItemInSlot then
		if PawnCommon.ShowSlotDebugInfo then
			VgerCore.Message(Item.Link .. " is now your best item in slot " .. PutNewItemInSlot .. ".")
		end
		local AlsoFitsIn
		if PutNewItemInSlot == Slot1 and Slot2 ~= nil then
			AlsoFitsIn = Slot2
		elseif PutNewItemInSlot == Slot2 then
			AlsoFitsIn = Slot1
		end
		PawnOptions.ItemLevels[PutNewItemInSlot] = { ["Link"] = Item.Link, ["ID"] = PawnGetItemIDFromLink(Item.Link), ["Level"] = Item.Level, ["AlsoFitsIn"] = AlsoFitsIn }

		if PawnCommon.ShowSlotDebugInfo then
			PawnShowSlotDebugInfo()
		end
	end
end

function PawnShowSlotDebugInfo()
	if not PawnOptions.ItemLevels then return end
	local BestItemsDebugInfo = "Best items: "
	local Slot
	for Slot = 1, 18 do
		if PawnOptions.ItemLevels[Slot] then
			BestItemsDebugInfo = BestItemsDebugInfo .. tostring(PawnOptions.ItemLevels[Slot].Link) .. "=" .. PawnOptions.ItemLevels[Slot].Level .. " "
		end
	end
	VgerCore.Message(BestItemsDebugInfo)
end

-- If this is an item level upgrade versus any of your items, returns the ilvl difference, or otherwise nil.
function PawnIsItemAnItemLevelUpgrade(Item)
	if not Item or not Item.Level or not PawnOptions.ItemLevels then return end
	if not Item.CanEquip then return end

	local Slot1, Slot2 = PawnGetSlotsForItemType(Item.InvType)
	if (Slot1 and PawnOptions.ItemLevels[Slot1] and Item.Link == PawnOptions.ItemLevels[Slot1].Link) or (Slot2 and PawnOptions.ItemLevels[Slot2] and Item.Link == PawnOptions.ItemLevels[Slot2].Link) then
		-- If the item is already one of your best, it can't be an upgrade.
		return
	end

	-- TODO: *** Rings and trinkets with the same ID should only be able to upgrade weaker versions of the same item

	local Difference
	if Slot1 and PawnOptions.ItemLevels[Slot1] and Item.Level > PawnOptions.ItemLevels[Slot1].Level then
		Difference = Item.Level - PawnOptions.ItemLevels[Slot1].Level
	end
	if Slot2 and PawnOptions.ItemLevels[Slot2] and Item.Level > PawnOptions.ItemLevels[Slot2].Level + (Difference or 0) then
		Difference = Item.Level - PawnOptions.ItemLevels[Slot2].Level
	end
	return Difference
end

------------------------------------------------------------
-- Pawn API
------------------------------------------------------------

-- Returns true if Pawn is ready to calculate scores and upgrades and all of that stuff.  (It's false while the user's UI is still loading.)
function PawnIsReady()
	return PawnIsInitialized
end

-- Resets all custom Pawn scales.
function PawnResetScales()
	return PawnResetScalesCore(true, false)
end

-- Resets all read-only scales from scale providers.
function PawnResetProviderScales()
	return PawnResetScalesCore(false, true)
end

-- Common code for scale resetting functions.
function PawnResetScalesCore(ResetCustomScales, ResetProviderScales)
	if not PawnIsInitialized then VgerCore.Fail("Can't reset scales until Pawn is initialized") return end

	local ScaleName, Scale, _
	local ScalesToRemove = {}
	for ScaleName, Scale in pairs(PawnCommon.Scales) do
		if (ResetProviderScales and Scale.Provider) or (ResetCustomScales and ScaleProvider == nil) then tinsert(ScalesToRemove, ScaleName) end
	end
	for _, ScaleName in pairs(ScalesToRemove) do
		PawnCommon.Scales[ScaleName] = nil
	end
	PawnResetTooltips()
	return true
end

-- Adds a new scale with no values.  Returns true if successful.
function PawnAddEmptyScale(ScaleName)
	if not PawnIsInitialized then VgerCore.Fail("Can't add scales until Pawn is initialized") return end

	if (not ScaleName) or (ScaleName == "") then
		VgerCore.Fail("ScaleName cannot be empty.  Usage: PawnAddEmptyScale(\"ScaleName\")")
		return false
	elseif PawnCommon.Scales[ScaleName] then
		VgerCore.Fail("ScaleName cannot be the name of an existing scale, and is case-sensitive.")
		return false
	end
	
	PawnCommon.Scales[ScaleName] = PawnGetEmptyScale()
	PawnCommon.Scales[ScaleName].PerCharacterOptions[PawnPlayerFullName] = { }
	PawnCommon.Scales[ScaleName].PerCharacterOptions[PawnPlayerFullName].Visible = true
	PawnRecalculateScaleTotal(ScaleName)
	return true
end

-- Adds a new scale with the default values.  Returns true if successful.
-- The scale returned will be for the current class and spec unless they're supplied as parameters.
function PawnAddDefaultScale(ScaleName, ClassID, SpecID)
	if not PawnIsInitialized then VgerCore.Fail("Can't add scales until Pawn is initialized") return end

	if (not ScaleName) or (ScaleName == "") then
		VgerCore.Fail("ScaleName cannot be empty.  Usage: PawnAddDefaultScale(\"ScaleName\")")
		return false
	elseif PawnCommon.Scales[ScaleName] then
		VgerCore.Fail("ScaleName cannot be the name of an existing scale, and is case-sensitive.")
		return false
	end
	
	PawnCommon.Scales[ScaleName] = PawnGetDefaultScale(ClassID, SpecID)
	PawnCommon.Scales[ScaleName].PerCharacterOptions[PawnPlayerFullName] = { }
	PawnCommon.Scales[ScaleName].PerCharacterOptions[PawnPlayerFullName].Visible = true
	PawnRecalculateScaleTotal(ScaleName)
	PawnResetTooltips()
	return true
end

-- Deletes a scale.  Returns true if successful.
function PawnDeleteScale(ScaleName)
	if not PawnIsInitialized then VgerCore.Fail("Can't delete scales until Pawn is initialized") return end

	if (not ScaleName) or (ScaleName == "") then
		VgerCore.Fail("ScaleName cannot be empty.  Usage: PawnDeleteScale(\"ScaleName\")")
		return false
	elseif not PawnCommon.Scales[ScaleName] then
		VgerCore.Fail("ScaleName must be the name of an existing scale, and is case-sensitive.")
		return false
	elseif PawnScaleIsReadOnly(ScaleName) then
		VgerCore.Fail("ScaleName cannot be the name of a read-only scale.")
		return false
	end
	
	PawnCommon.Scales[ScaleName] = nil
	PawnRecalculateScaleTotal(ScaleName)
	PawnResetTooltips()
	return true
end

-- Renames an existing scale.  Returns true if successful.
function PawnRenameScale(OldScaleName, NewScaleName)
	if not PawnIsInitialized then VgerCore.Fail("Can't rename scales until Pawn is initialized") return end

	if (not OldScaleName) or (OldScaleName == "") or (not NewScaleName) or (NewScaleName == "") then
		VgerCore.Fail("OldScaleName and NewScaleName cannot be empty.  Usage: PawnRenameScale(\"OldScaleName\", \"NewScaleName\")")
		return false
	elseif OldScaleName == NewScaleName then
		VgerCore.Fail("OldScaleName and NewScaleName cannot be the same.")
		return false
	elseif not PawnCommon.Scales[OldScaleName] then
		VgerCore.Fail("OldScaleName must be the name of an existing scale, and is case-sensitive.")
		return false
	elseif PawnCommon.Scales[NewScaleName] then
		VgerCore.Fail("NewScaleName cannot be the name of an existing scale, and is case-sensitive.")
		return false
	elseif PawnScaleIsReadOnly(ScaleName) then
		VgerCore.Fail("ScaleName cannot be the name of a read-only scale.")
		return false
	end
	
	PawnCommon.Scales[NewScaleName] = PawnCommon.Scales[OldScaleName]
	PawnCommon.Scales[OldScaleName] = nil
	PawnRecalculateScaleTotal(OldScaleName)
	PawnRecalculateScaleTotal(NewScaleName)
	PawnResetTooltips()
	return true
end

-- Creates a new scale based on an old one.  Returns true if successful.
function PawnDuplicateScale(OldScaleName, NewScaleName)
	if not PawnIsInitialized then VgerCore.Fail("Can't duplicate scales until Pawn is initialized") return end

	if (not OldScaleName) or (OldScaleName == "") or (not NewScaleName) or (NewScaleName == "") then
		VgerCore.Fail("OldScaleName and NewScaleName cannot be empty.  Usage: PawnDuplicateScale(\"OldScaleName\", \"NewScaleName\")")
		return false
	elseif OldScaleName == NewScaleName then
		VgerCore.Fail("OldScaleName and NewScaleName cannot be the same.")
		return false
	elseif not PawnCommon.Scales[OldScaleName] then
		VgerCore.Fail("OldScaleName must be the name of an existing scale, and is case-sensitive.")
		return false
	elseif PawnCommon.Scales[NewScaleName] then
		VgerCore.Fail("NewScaleName cannot be the name of an existing scale, and is case-sensitive.")
		return false
	end

	-- Create the copy.
	PawnCommon.Scales[NewScaleName] = {}
	PawnCommon.Scales[NewScaleName].Color = PawnCommon.Scales[OldScaleName].Color
	PawnCommon.Scales[NewScaleName].Role = PawnCommon.Scales[OldScaleName].Role
	PawnCommon.Scales[NewScaleName].ClassID = PawnCommon.Scales[OldScaleName].ClassID
	PawnCommon.Scales[NewScaleName].SpecID = PawnCommon.Scales[OldScaleName].SpecID
	PawnCommon.Scales[NewScaleName].IconTexturePath = PawnCommon.Scales[OldScaleName].IconTexturePath
	PawnCommon.Scales[NewScaleName].NormalizationFactor = PawnCommon.Scales[OldScaleName].NormalizationFactor
	PawnCommon.Scales[NewScaleName].PerCharacterOptions = {}
	PawnCommon.Scales[NewScaleName].PerCharacterOptions[PawnPlayerFullName] = {}
	PawnCommon.Scales[NewScaleName].PerCharacterOptions[PawnPlayerFullName].Visible = true
	PawnCommon.Scales[NewScaleName].Values = {}
	local NewScale = PawnCommon.Scales[NewScaleName].Values
	for StatName, Value in pairs(PawnCommon.Scales[OldScaleName].Values) do
		NewScale[StatName] = Value
	end
	
	-- Do post-copy calculations, and we're done.
	PawnRecalculateScaleTotal(NewScaleName)
	PawnResetTooltips()
	return true
end

-- Returns the value of one stat in a scale, or nil if unsuccessful.
function PawnGetStatValue(ScaleName, StatName)
	if not PawnIsInitialized then VgerCore.Fail("Can't get a stat value until Pawn is initialized") return end

	if (not ScaleName) or (ScaleName == "") or (not StatName) or (StatName == "") then
		VgerCore.Fail("ScaleName and StatName cannot be empty.  Usage: x = PawnGetStatValue(\"ScaleName\", \"StatName\")")
		return nil
	elseif not PawnCommon.Scales[ScaleName] then
		VgerCore.Fail("ScaleName must be the name of an existing scale, and is case-sensitive.")
		return nil
	end
	
	return PawnCommon.Scales[ScaleName].Values[StatName]
end

-- Returns true if a particular scale exists, or false if not.
function PawnDoesScaleExist(ScaleName)
	if not PawnIsInitialized then VgerCore.Fail("Can't check scales until Pawn is initialized") return end

	if (not ScaleName) or (ScaleName == "") then
		VgerCore.Fail("ScaleName cannot be empty.  Usage: x = PawnDoesScaleExist(\"ScaleName\")")
		return false
	end
	
	if PawnCommon.Scales[ScaleName] then
		return true
	else
		return false
	end
end

-- Returns a table of all stats and their values for a particular scale, or nil if unsuccessful.
-- This returns the actual internal table of stat values, so be careful not to modify it!
function PawnGetAllStatValues(ScaleName)
	if not PawnIsInitialized then VgerCore.Fail("Can't get scale info until Pawn is initialized") return end

	if (not ScaleName) or (ScaleName == "") then
		VgerCore.Fail("ScaleName cannot be empty.  Usage: x = PawnGetAllStatValues(\"ScaleName\")")
		return nil
	elseif not PawnCommon.Scales[ScaleName] then
		VgerCore.Fail("ScaleName must be the name of an existing scale, and is case-sensitive.")
		return nil
	end
	
	--local TableCopy = {}
	--for StatName, Value in pairs(PawnCommon.Scales[ScaleName].Values) do
	--	TableCopy[StatName] = Value
	--end
	--return TableCopy
	return PawnCommon.Scales[ScaleName].Values
end

-- Sets the value of one stat in a scale.  Returns true if successful.
-- Use 0 or nil as the Value to remove a stat from the scale.
function PawnSetStatValue(ScaleName, StatName, Value)
	if not PawnIsInitialized then VgerCore.Fail("Can't change scales until Pawn is initialized") return end

	if (not ScaleName) or (ScaleName == "") or (not StatName) or (StatName == "") then
		VgerCore.Fail("ScaleName and StatName cannot be empty.  Usage: PawnSetStatValue(\"ScaleName\", \"StatName\", Value)")
		return false
	elseif not PawnCommon.Scales[ScaleName] then
		VgerCore.Fail("ScaleName must be the name of an existing scale, and is case-sensitive.")
		return false
	elseif PawnScaleIsReadOnly(ScaleName) then
		VgerCore.Fail("ScaleName cannot be the name of a read-only scale.")
		return false
	end
	
	if Value == 0 then Value = nil end
	PawnCommon.Scales[ScaleName].Values[StatName] = Value
	PawnRecalculateScaleTotal(ScaleName) -- also recalculates socket values
	PawnInvalidateBestItemsForScale(ScaleName)
	PawnResetTooltips()
	return true
end

-- Returns a table of all Pawn scale names.  Returns all custom scales not from scale providers, whether visible or not.
-- For more information in one big table, use PawnGetAllScalesEx.  This method is provided here for backwards compatibility.
-- DEPRECATED
function PawnGetAllScales()
	if not PawnIsInitialized then VgerCore.Fail("Can't get scale list until Pawn is initialized") return end

	local TableCopy = {}
	local ScaleName, Scale
	for ScaleName, Scale in pairs(PawnCommon.Scales) do
		if (not Scale.Provider) or (Scale.ProviderActive) then
			-- Don't include scales from a provider that isn't active any longer.  (Abandoned provider scales)
			tinsert(TableCopy, ScaleName)
		end
	end
	sort(TableCopy, VgerCore.CaseInsensitiveComparer)
	return TableCopy
end

-- Return a sorted table of all Pawn scale names and some data about each scale.
-- Each element in the table returned is a table with these values:
-- 	{ Name, LocalizedName, Header, IsVisible }
-- 	Name: The internal name of the scale.  Examples: "My custom scale"; "\"Wowhead\":DruidFeralDps"
-- 	LocalizedName: The display name of the scale.  Examples: "My custom scale"; "Druid feral DPS"
-- 	Header: The header text to display above this scale.  Examples: "Vger's scales"; "Wowhead scales"
-- 	IsVisible: Whether or not this scale is visible for the current character.  Examples: true, true
--	IsProvider: Whether or not this scale comes from a scale provider.  Examples: true, false
function PawnGetAllScalesEx()
	if not PawnIsInitialized then VgerCore.Fail("Can't get scale list until Pawn is initialized") return end

	local TableCopy = {}
	local ScaleName, Scale
	local ActiveScalesHeader = format(PawnLocal.VisibleScalesHeader, UnitName("player"))
	for ScaleName, Scale in pairs(PawnCommon.Scales) do
		local IsVisible = PawnIsScaleVisible(ScaleName)
		local ScaleData =
		{
			["Name"] = ScaleName,
			["LocalizedName"] = Scale.LocalizedName or ScaleName,
			["IsVisible"] = IsVisible,
			["IsProvider"] = Scale.Provider ~= nil
		}
		if IsVisible then
			ScaleData.Header = ActiveScalesHeader
		elseif Scale.Provider and Scale.ProviderActive then
			ScaleData.Header = PawnScaleProviders[Scale.Provider].Name
		else
			ScaleData.Header = PawnLocal.HiddenScalesHeader
		end
		if (not Scale.Provider) or (Scale.ProviderActive) then
			-- Don't include scales from a provider that isn't active any longer.  (Abandoned provider scales)
			tinsert(TableCopy, ScaleData)
		--else
		--	VgerCore.Message("Not including " .. ScaleName .. " because it seems to be abandoned.")
		end
	end
	sort(TableCopy, PawnGetAllScalesExComparer)
	
	return TableCopy
end

-- Sort function used by PawnGetAllScalesEx.  Returns true if a should sort before b.
function PawnGetAllScalesExComparer(a, b)
	if not b then return a end
	if not a then return b end
	-- First, if one is visible and the other is not, then sort the visible ones first.
	local AVisible = a.IsVisible
	local BVisible = b.IsVisible
	if AVisible and not BVisible then return true end
	if BVisible and not AVisible then return false end
	-- They're both the same visibility.  Sort custom (non-provider) scales first.
	local AIsProvider = a.IsProvider
	local BIsProvider = b.IsProvider
	if AIsProvider and not BIsProvider then return false end
	if BIsProvider and not AIsProvider then return true end
	-- Then, sort by provider.
	if AIsProvider or BIsProvider then
		local AProvider = PawnGetProviderNameFromScale(a.Name)
		local BProvider = PawnGetProviderNameFromScale(b.Name)
		if AProvider and BProvider and AProvider ~= BProvider then
			return strlower(PawnGetProviderLocalizedName(AProvider)) < strlower(PawnGetProviderLocalizedName(BProvider))
		end
	end
	-- If both scales are from the same provider, then just sort by display name, case-insensitive.
	return strlower(a.LocalizedName) < strlower(b.LocalizedName)
end

-- Changes the normalization factor for a scale.  (Expected values are 1/true and 0/false/nil.)
function PawnSetScaleNormalizationFactor(ScaleName, Value)
	if not PawnIsInitialized then VgerCore.Fail("Can't alter scales until Pawn is initialized") return end

	local Scale = PawnCommon.Scales[ScaleName]
	if not ScaleName or ScaleName == "" or not Scale then
		VgerCore.Fail("ScaleName must be the name of an existing scale, and is case-sensitive.")
		return false
	elseif PawnScaleIsReadOnly(ScaleName) then
		VgerCore.Fail("Can't change a read-only scale.")
		return false
	end
	
	if Value == true then Value = 1 elseif Value == 0 or Value == false then Value = nil end
	Scale.NormalizationFactor = Value
	PawnInvalidateBestItemsForScale(PawnUICurrentScale) -- it'll be the same items as before, but with new values
	PawnResetTooltips()
	return true
end

-- Creates a Pawn scale tag for a scale.
--	Parameters: ScaleName
--		ScaleName: The name of a Pawn scale.
--	Return value: ScaleTag, or nil if unsuccessful.
--		ScaleTag: A Pawn scale tag.  Example:  '( Pawn: v1: "Healbot": Stamina=1, Intellect=1.24 )'
function PawnGetScaleTag(ScaleName)
	if not PawnIsInitialized then VgerCore.Fail("Can't export scales until Pawn is initialized") return end

	local Scale = PawnCommon.Scales[ScaleName]
	if (not ScaleName) or (ScaleName == "") then
		VgerCore.Fail("ScaleName cannot be empty.  Usage: PawnGetScaleTag(\"ScaleName\")")
		return
	elseif not Scale then
		VgerCore.Fail("ScaleName must be the name of an existing scale, and is case-sensitive.")
		return
	elseif not Scale.Values then
		return
	end
	
	-- Concatenate the stats.
	local ScaleFriendlyName = PawnGetScaleLocalizedName(ScaleName)
	local ScaleTag = "( Pawn: v" .. PawnCurrentScaleVersion .. ": \"" .. ScaleFriendlyName .. "\": "
	local AddComma = false
	local TemplateStats
	if Scale.ClassID and Scale.SpecID then
		ScaleTag = ScaleTag .. "Class=" .. PawnGetEnglishClassNameFromID(Scale.ClassID) .. ", Spec=" .. PawnGetEnglishSpecNameFromID(Scale.ClassID, Scale.SpecID)
		AddComma = true
		TemplateStats = PawnGetStatValuesForTemplate(PawnFindScaleTemplate(Scale.ClassID, Scale.SpecID), true)
	end
	local IncludeThis
	for StatName, Value in pairs(Scale.Values) do
		local IncludeThis = (Value and Value ~= 0)
		if IncludeThis and TemplateStats and Value == TemplateStats[StatName] then
			-- If class and spec are included, don't include things that are already in the template.
			IncludeThis = false
		end
		if IncludeThis then
			if AddComma then ScaleTag = ScaleTag .. ", " end
			ScaleTag = ScaleTag .. StatName .. "=" .. tostring(Value)
			AddComma = true
		end
	end
	
	ScaleTag = ScaleTag .. " )"
	return ScaleTag
end

-- Imports a Pawn scale tag, adding that scale to the current character.
--	Parameters: ScaleTag, Overwrite
--		ScaleTag: A Pawn scale tag to add.  Example:  '( Pawn: v1: "Healbot": Stamina=1, Intellect=1.24 )'
--		Overwrite: If true, this function will overwrite an existing scale with the same name.
--	Return value: Status, ScaleName
--		Status: One of the PawnImportScaleResult* constants.
--		ScaleName: The name of the Pawn scale specified by ScaleTag, or nil if ScaleTag could not be parsed.
function PawnImportScale(ScaleTag, Overwrite)
	if not PawnIsInitialized then VgerCore.Fail("Can't import scales until Pawn is initialized") return end

	local ScaleName, Values = PawnParseScaleTag(ScaleTag)
	if not ScaleName then
		-- This tag couldn't be parsed.
		return PawnImportScaleResultTagError
	end

	-- The "Class" and "Spec" parameters aren't actually stat values, so take them out of the list now.
	local ClassID, SpecID
	local UnlocalizedClassName, IconTexturePath, Role
	if GetClassInfo then
		-- Specializations don't exist in Classic as they do on live, so skip that.
		ClassID = Values.Class
		Values.Class = nil
		SpecID = Values.Spec
		Values.Spec = nil
		if ClassID and not SpecID then
			ClassID = nil
		elseif SpecID and not ClassID then
			SpecID = nil
		elseif ClassID and SpecID then
			_, UnlocalizedClassName = GetClassInfo(ClassID)
			_, _, _, IconTexturePath, Role = GetSpecializationInfoForClassID(ClassID, SpecID)
		end
	end
		
	local AlreadyExists = PawnCommon.Scales[ScaleName] ~= nil
	if AlreadyExists and (PawnScaleIsReadOnly(ScaleName) or not Overwrite) then
		-- A scale with this name already exists.  You can't import a scale with the same name as an existing one,
		-- unless you specify Overwrite = true.
		return PawnImportScaleResultAlreadyExists, ScaleName
	end
	
	-- Looks like everything's okay.  Import the scale.  If the scale already exists but Overwrite = true was passed,
	-- don't change other options about this scale, such as the color.

	if not AlreadyExists then
		if ClassID and SpecID then
			PawnCommon.Scales[ScaleName] = PawnGetDefaultScale(ClassID, SpecID, true)
			local Color = strsub(RAID_CLASS_COLORS[UnlocalizedClassName].colorStr, 3)
			-- Choose a lighter color for death knights so it's easier to read.
			if ClassID == 6 then Color = "ff4d6b" end
			PawnCommon.Scales[ScaleName].Color = Color
		else
			PawnCommon.Scales[ScaleName] = PawnGetEmptyScale()
		end	
		PawnCommon.Scales[ScaleName].PerCharacterOptions[PawnPlayerFullName] = { }
		PawnCommon.Scales[ScaleName].PerCharacterOptions[PawnPlayerFullName].Visible = true
	end
	local NewScale = PawnCommon.Scales[ScaleName]
	NewScale.ClassID = ClassID
	NewScale.SpecID = SpecID
	NewScale.IconTexturePath = IconTexturePath
	NewScale.Role = Role

	-- Merge the scale tag's stats into the template stats.
	local StatName, Value
	for StatName, Value in pairs(Values) do
		NewScale.Values[StatName] = Value
	end
	PawnCorrectScaleErrors(ScaleName)
	
	PawnRecalculateScaleTotal(ScaleName)
	if AlreadyExists then PawnInvalidateBestItemsForScale(ScaleName) end
	PawnResetTooltips()
	return PawnImportScaleResultSuccess, ScaleName
end

-- Sets the visibility of all scales from a particular scale provider to be visible or hidden in a single operation.
function PawnSetAllScaleProviderScalesVisible(ProviderInternalName, Visible)
	if not PawnIsInitialized then VgerCore.Fail("Can't show or hide providers until Pawn is initialized") return end

	if (not PawnPlayerFullName) then
		VgerCore.Fail("PawnSetAllScaleProviderScalesVisible failed because Pawn didn't know your character's name yet.")
		return nil
	end
	if (not ProviderInternalName) or (ProviderInternalName == "") then
		VgerCore.Fail("ProviderInternalName cannot be empty.  Usage: PawnSetAllScaleProviderScalesVisible(\"ProviderInternalName\", Visible)")
		return nil
	end
	
	-- Using the provider internal name provided, produce a prefix that we'll search for in the list of scales.  This works because
	-- the format of a provider scale is "ProviderName":ScaleName.
	local ScaleNamePrefix = PawnGetProviderScaleName(ProviderInternalName, "")
	
	-- Loop through all scales and turn them on or off.
	local ScaleName, Scale
	for ScaleName, Scale in pairs(PawnCommon.Scales) do
		if strfind(ScaleName, ScaleNamePrefix, 1, true) == 1 then
			if Scale.PerCharacterOptions == nil then Scale.PerCharacterOptions = {} end
			if Scale.PerCharacterOptions[PawnPlayerFullName] == nil then Scale.PerCharacterOptions[PawnPlayerFullName] = {} end
			Scale.PerCharacterOptions[PawnPlayerFullName].Visible = Visible
			PawnResetTooltips()
		end
	end
	return true
end

-- Enables or disables the auto-scale feature.
function PawnSetAutoSelectScales(Enable)
	VgerCore.Assert(Enable ~= nil, "Enable parameter must be true or false.")
	if PawnOptions.AutoSelectScales == Enable then return end

	PawnOptions.AutoSelectScales = Enable
	if Enable then
		PawnOnSpecChanged()
	else
		PawnEnableAllScalesForClass()
	end
end

-- Sets whether or not a scale is visible.  If Visible is nil, it will be considered as false.
function PawnSetScaleVisible(ScaleName, Visible)
	if not PawnIsInitialized then VgerCore.Fail("Can't show and hide scales until Pawn is initialized") return end

	if (not ScaleName) or (ScaleName == "") then
		VgerCore.Fail("ScaleName cannot be empty.  Usage: PawnSetScaleVisible(\"ScaleName\", Visible)")
		return nil
	elseif not PawnCommon.Scales[ScaleName] then
		VgerCore.Fail("ScaleName (" .. tostring(ScaleName) .. ") must be the name of an existing scale, and is case-sensitive.")
		return nil
	end
	
	local Scale = PawnCommon.Scales[ScaleName]
	if Scale.PerCharacterOptions[PawnPlayerFullName].Visible ~= Visible then
		Scale.PerCharacterOptions[PawnPlayerFullName].Visible = Visible
		PawnResetTooltips()
	end
	return true
end

local TEMP_PawnDebugPerCharacterOptionsFailure

-- Returns true if a given scale is visible in tooltips.
function PawnIsScaleVisible(ScaleName)
	if not PawnCommon then VgerCore.Fail("Can't check scale visibility until Pawn is initialized") return end
	
	if (not ScaleName) or (ScaleName == "") then
		VgerCore.Fail("ScaleName cannot be empty.  Usage: x = PawnIsScaleVisible(\"ScaleName\")")
		return nil
	elseif not PawnCommon.Scales[ScaleName] then
		VgerCore.Fail("ScaleName must be the name of an existing scale, and is case-sensitive.")
		return nil
	end
	
	local Scale = PawnCommon.Scales[ScaleName]
	if Scale.PerCharacterOptions == nil then
		VgerCore.Fail("All per-character options for " .. ScaleName .. " were missing.")
		return false
	end
	if Scale.PerCharacterOptions[PawnPlayerFullName] == nil then
		if TEMP_PawnDebugPerCharacterOptionsFailure == nil then
			TEMP_PawnDebugPerCharacterOptionsFailure = true
			VgerCore.Fail("Per-character options for this character (" .. PawnPlayerFullName .. ") and scale (" .. ScaleName .. ") were missing.")
		end
		if PawnCommon.DebugPerCharacterOptionsFailure then
			VgerCore.Fail(debugstack())
		end
		return false
	end
	return Scale.PerCharacterOptions[PawnPlayerFullName].Visible
end

-- Gets the color of a scale in hex format.  If the scale doesn't specify a color, the default is returned.
-- If Unenchanted is true, then the unenchanted color for the scale is returned.
function PawnGetScaleColor(ScaleName, Unenchanted)
	if not PawnIsInitialized then VgerCore.Fail("Can't get scale colors until Pawn is initialized") return end
	
	if (not ScaleName) or (ScaleName == "") then
		VgerCore.Fail("ScaleName cannot be empty.  Usage: rrggbb = PawnGetScaleColor(\"ScaleName\", Unenchanted)")
		return VgerCore.Color.Blue
	end
	local Scale = PawnCommon.Scales[ScaleName]
	if not Scale then
		VgerCore.Fail("ScaleName must be the name of an existing scale, and is case-sensitive.")
		return VgerCore.Color.Blue
	end
	
	if Unenchanted then
		if Scale.UnenchantedColor and strlen(Scale.UnenchantedColor) == 6 then return "|cff" .. Scale.UnenchantedColor end
		return VgerCore.Color.DarkBlue
	else
		if Scale.Color and strlen(Scale.Color) == 6 then return "|cff" .. Scale.Color end
		return VgerCore.Color.Blue
	end
end

-- Sets the color of a scale in six-character hex format.  The unenchanted color for the scale will also be set
-- to a slightly darker color.
function PawnSetScaleColor(ScaleName, HexColor)
	if not PawnIsInitialized then VgerCore.Fail("Can't change scale colors until Pawn is initialized") return end
	
	if (not ScaleName) or (ScaleName == "") then
		VgerCore.Fail("ScaleName cannot be empty.  Usage: PawnGetScaleColor(\"ScaleName\", \"rrggbb\")")
		return nil
	end
	local Scale = PawnCommon.Scales[ScaleName]
	if not Scale then
		VgerCore.Fail("ScaleName must be the name of an existing scale, and is case-sensitive.")
		return nil
	end
	if not HexColor or strlen(HexColor) ~= 6 then
		VgerCore.Fail("HexColor must be a six-digit hexadecimal color code, such as '66c0ff'.")
		return nil
	end

	local r, g, b = VgerCore.HexToRGB(HexColor)
	Scale.Color = HexColor
	Scale.UnenchantedColor = VgerCore.RGBToHex(r * PawnScaleColorDarkFactor, g * PawnScaleColorDarkFactor, b * PawnScaleColorDarkFactor)
end

-- Gets whether a stat is a 1-handed weapon stat, a 2-handed weapon stat, or neither.  (Only tracks things that go into either the main hand
-- or off-hand slot.  Ranged weapons are neither.)
function PawnGetWeaponSetForStat(StatName)
	if StatName == "IsAxe" or StatName == "IsDagger" or StatName == "IsFist" or StatName == "IsMace" or StatName == "IsSword" or StatName == "IsWarglaive" or StatName == "IsOffHand" or StatName == "IsFrill" then
		return 1
	elseif StatName == "Is2HAxe" or StatName == "Is2HMace" or StatName == "IsPolearm" or StatName == "IsStaff" or StatName == "Is2HSword" then
		return 2
	else
		return nil
	end
end

-- Gets whether or not upgrades are shown for either 1- or 2-handed weapons.
function PawnGetShowUpgradesForWeapons(ScaleName, WeaponSet)
	if not PawnIsInitialized then VgerCore.Fail("Can't change upgrade settings until Pawn is initialized") return end
	
	local Scale = PawnCommon.Scales[ScaleName]
	if (not ScaleName) or (ScaleName == "") or not Scale then
		VgerCore.Fail("ScaleName must be the name of an existing scale, and is case-sensitive.")
		return nil
	end
	if WeaponSet ~= 1 and WeaponSet ~= 2 then
		VgerCore.Fail("WeaponSet must be 1 or 2.")
		return nil
	end
	
	if WeaponSet == 1 then
		return not Scale.DoNotShow1HUpgrades
	else
		return not Scale.DoNotShow2HUpgrades
	end
end

-- Sets whether or not upgrades are shown for either 1- or 2-handed weapons.
function PawnSetShowUpgradesForWeapons(ScaleName, WeaponSet, ShowUpgrades)
	if not PawnIsInitialized then VgerCore.Fail("Can't change weapon upgrade settings until Pawn is initialized") return end
	
	local Scale = PawnCommon.Scales[ScaleName]
	if (not ScaleName) or (ScaleName == "") or not Scale then
		VgerCore.Fail("ScaleName must be the name of an existing scale, and is case-sensitive.")
		return nil
	end
	if WeaponSet ~= 1 and WeaponSet ~= 2 then
		VgerCore.Fail("WeaponSet must be 1 or 2.")
		return nil
	end

	if WeaponSet == 1 then
		Scale.DoNotShow1HUpgrades = not ShowUpgrades
	else
		Scale.DoNotShow2HUpgrades = not ShowUpgrades
	end
	PawnInvalidateBestItemsForScale(ScaleName)
	PawnResetTooltips()
end

-- Gets whether only the current player's best armor type is shown for upgrades after level 50.
function PawnGetUpgradesFollowSpecialization(ScaleName)
	if not PawnIsInitialized then VgerCore.Fail("Can't get armor upgrade settings until Pawn is initialized") return end
	
	local Scale = PawnCommon.Scales[ScaleName]
	if (not ScaleName) or (ScaleName == "") or not Scale then
		VgerCore.Fail("ScaleName must be the name of an existing scale, and is case-sensitive.")
		return nil
	end
	
	return Scale.UpgradesFollowSpecialization
end

-- Sets whether only the current player's best armor type is shown for upgrades after level 50.
function PawnSetUpgradesFollowSpecialization(ScaleName, FollowSpecialization)
	if not PawnIsInitialized then VgerCore.Fail("Can't change armor upgrade settings until Pawn is initialized") return end
	
	local Scale = PawnCommon.Scales[ScaleName]
	if (not ScaleName) or (ScaleName == "") or not Scale then
		VgerCore.Fail("ScaleName must be the name of an existing scale, and is case-sensitive.")
		return nil
	end
	
	if FollowSpecialization then
		Scale.UpgradesFollowSpecialization = true
	else
		Scale.UpgradesFollowSpecialization = false
	end
	PawnInvalidateBestItemsForScale(ScaleName)
	PawnResetTooltips()
end

-- Sets whether the upgrade tracking feature is enabled for this character.
function PawnSetUpgradeTracking(Enabled)
	if PawnOptions.UpgradeTracking == Enabled then return end

	PawnOptions.UpgradeTracking = Enabled
	PawnInvalidateBestItems()
	PawnResetTooltips()
end

-- Returns true if a scale is read-only.
function PawnScaleIsReadOnly(ScaleName)
	if not PawnCommon then VgerCore.Fail("Can't check scale read-only status until Pawn is initialized") return end
	
	local Scale = PawnCommon.Scales[ScaleName]
	return Scale and Scale.Provider ~= nil
end

-- Returns the localized name for a scale if it has one.  Otherwise, it returns the scale's unlocalized name.
function PawnGetScaleLocalizedName(ScaleName)
	if not PawnCommon then VgerCore.Fail("Can't get scale names until Pawn is initialized") return end
	
	local Scale = PawnCommon.Scales[ScaleName]
	if Scale and Scale.LocalizedName then
		return Scale.LocalizedName
	else
		return ScaleName
	end
end

-- Uninitialize the plugin infrastructure and clean up our stale data.  We'll do this upon logging out or reloading the UI.
function PawnUnitializePlugins()
	-- Remove values from all read-only scales from providers so they don't get serialized to SavedVariables unnecessarily.
	local ScaleName, Scale
	for ScaleName, Scale in pairs(PawnCommon.Scales) do
		if Scale.Provider then
			Scale.ProviderActive = nil
			Scale.Values = nil
			Scale.Header = nil
		end
	end
	
	-- Clear out the provider data.
	PawnScaleProviders = nil
end

-- Initializes all delay-loaded scale providers.
function PawnInitializePlugins()
	-- This only needs to be done once.  PawnAddPluginScaleProvider will take care of anything that needs to
	-- happen after this is called.
	if PawnScaleProvidersInitialized then return end
	PawnScaleProvidersInitialized = true
	
	-- Go through the list of scale providers and call their initialization function.  They'll create all of their
	-- scales as necessary.
	local Provider, _
	for _, Provider in pairs(PawnScaleProviders) do
		if Provider.Function then
			-- After we call each provider's initialization function, empty it out so that function can be
			-- garbage-collected if necessary.
			Provider.Function()
			Provider.Function = nil
		end
	end
end

-- Registers a plugin scale provider.
-- Arguments: ProviderInternalName, LocalizedName
--	ProviderInternalName: An unlocalized internal name for the scale provider.
--	LocalizedName: The localized name for the scale provider, to show up in the UI.
-- 	Function: A function to call that adds the scales when it is time.
function PawnAddPluginScaleProvider(ProviderInternalName, LocalizedName, Function)
	-- If the scale provider already exists, ignore the second registration.
	if PawnScaleProviders[ProviderInternalName] then return end
	
	if strfind(ProviderInternalName, "\"") then
		VgerCore.Fail("Pawn scale providers cannot include double quotes ('\"') in their name.")
		return
	end
	
	if PawnScaleProvidersInitialized then
		-- If we've already initialized scale providers, just do this one immediately.
		PawnScaleProviders[ProviderInternalName] = { ["Name"] = LocalizedName }
		Function()
	else
		-- Otherwise, we'll get to it later.
		VgerCore.Assert(Function, "Scale provider \"" .. LocalizedName .. "\" was registered but won't initialize properly because no initialization function was specified.")
		PawnScaleProviders[ProviderInternalName] = { ["Name"] = LocalizedName, ["Function"] = Function }
	end
end

-- Given a scale provider name and a scale name, returns the full name of a scale from a provider.
function PawnGetProviderScaleName(ProviderInternalName, ScaleInternalName)
	return "\"" .. ProviderInternalName .. "\":" .. ScaleInternalName
end

-- Given a scale internal name, return the provider internal name.
function PawnGetProviderNameFromScale(ScaleInternalName)
	-- If this isn't a provider scale, then just return the scale name.
	if not ScaleInternalName or strbyte(ScaleInternalName) ~= 34 then return ScaleInternalName end -- 34 is "

	-- Otherwise, get the provider name.
	local Pos = strfind(ScaleInternalName, "\"", 2, true)
	if not Pos then
		VgerCore.Fail("Didn't understand the provider name in the scale " .. tostring(ScaleInternalName) .. ".")
		return ScaleInternalName
	end
	return strsub(ScaleInternalName, 2, Pos - 1)
end

-- Given a provider internal name, return the localized name.
function PawnGetProviderLocalizedName(ProviderInternalName)
	local Provider = PawnScaleProviders[ProviderInternalName]
	if not Provider then
		VgerCore.Fail("Pawn scale provider \"" .. tostring(ProviderInternalName) .. " is not registered yet so we can't get its display name.")
		return ProviderInternalName
	end

	return Provider.Name
end

-- Adds a plugin scale to Pawn.  Plugin scales are read-only once added, and are not saved; they must be added on every login.
-- If this plugin scale already exists (it was added this session), it will be overwritten.
function PawnAddPluginScale(ProviderInternalName, ScaleInternalName, LocalizedName, Color, Values, NormalizationFactor, WeaponSetToHideUpgradesFor)
	if not PawnScaleProviders[ProviderInternalName] then
		VgerCore.Fail("A scale provider with that name is not registered.  Use PawnAddPluginScaleProvider first.")
		return
	end
	
	if not PawnCommon then VgerCore.Fail("Can't add plugin scales until Pawn starts to initialize.") return end
	
	-- Now, add this new scale to the master list (starting with default scale options), or if it's already there, update it with the data from the scale provider.
	local ScaleFullName = PawnGetProviderScaleName(ProviderInternalName, ScaleInternalName)
	local NewScale
	if PawnCommon.Scales[ScaleFullName] then
		NewScale = PawnCommon.Scales[ScaleFullName]
	else
		NewScale = PawnGetEmptyScale()
	end
	NewScale.ProviderActive = true
	NewScale.Provider = ProviderInternalName
	NewScale.LocalizedName = LocalizedName
	NewScale.Header = PawnScaleProviders[ProviderInternalName].Name
	NewScale.NormalizationFactor = NormalizationFactor
	-- If the plugin supplied any stat values of 0, remove them now.
	for StatName, Value in pairs(Values) do
		if Value == 0 then Values[StatName] = nil end
	end
	NewScale.Values = Values
	if not NewScale.PerCharacterOptions then NewScale.PerCharacterOptions = {} end
	if not NewScale.PerCharacterOptions[PawnPlayerFullName] then NewScale.PerCharacterOptions[PawnPlayerFullName] = {} end
	if not PawnCommon.Scales[ScaleFullName] then PawnCommon.Scales[ScaleFullName] = NewScale end
	if NewScale.DoNotShow1HUpgrades == nil then NewScale.DoNotShow1HUpgrades = (WeaponSetToHideUpgradesFor == 1) end
	if NewScale.DoNotShow2HUpgrades == nil then NewScale.DoNotShow2HUpgrades = (WeaponSetToHideUpgradesFor == 2) end
	
	if not NewScale.Color then PawnSetScaleColor(ScaleFullName, Color) end -- If the user has customized the color, don't overwrite theirs.
end

-- Adds a plugin scale from Pawn, starting from one of Pawn's existing templates.
function PawnAddPluginScaleFromTemplate(ProviderInternalName, ClassID, SpecID, Stats, NormalizationFactor)
	if not PawnScaleProviders[ProviderInternalName] then
		VgerCore.Fail("A scale provider with that name is not registered.  Use PawnAddPluginScaleProvider first.")
		return
	end

	if not PawnCommon then VgerCore.Fail("Can't add plugin scales until Pawn starts to initialize.") return end

	local LocalizedClassName, UnlocalizedClassName = GetClassInfo(ClassID)
	local _, LocalizedSpecName, _, IconTexturePath, Role = GetSpecializationInfoForClassID(ClassID, SpecID)

	local Template = PawnFindScaleTemplate(ClassID, SpecID)
	if not Template then VgerCore.Fail("Can't add this plugin scale because the class" .. tostring(LocalizedClassName) .. " ID " .. tostring(ClassID) .. " and/or spec " .. tostring(LocalizedSpecName) .. " ID " .. tostring(SpecID) .. " wasn't found.") return end

	-- Build up the values table.
	local ScaleValues = PawnGetStatValuesForTemplate(Template)
	if Stats then
		local StatName, Value
		for StatName, Value in pairs(Stats) do
			ScaleValues[StatName] = Stats[StatName]
		end
	end

	local Color = strsub(RAID_CLASS_COLORS[UnlocalizedClassName].colorStr, 3)
	-- Choose a lighter color for death knights so it's easier to read.
	if ClassID == 6 then Color = "ff4d6b" end

	-- Then, transfer control to the regular plugin scale codepath.
	local ScaleInternalName = UnlocalizedClassName .. SpecID
	PawnAddPluginScale(
		ProviderInternalName,
		ScaleInternalName,
		LocalizedClassName .. ": " .. LocalizedSpecName, -- LocalizedScaleName
		Color,
		ScaleValues,
		NormalizationFactor,
		Template.HideUpgrades
	)
	
	-- Finally, make a few more customizations to that scale.
	local NewScale = PawnCommon.Scales[PawnGetProviderScaleName(ProviderInternalName, ScaleInternalName)]
	if not NewScale then return end

	NewScale.ClassID = ClassID
	NewScale.SpecID = SpecID
	NewScale.IconTexturePath = IconTexturePath
	NewScale.Role = Role
end

-- Returns the unenchanted item link of the best item that a user has for a particular scale and inventory type.
-- Parameters:
--	ScaleName: The scale to find a best item for.
--	InvType: An inventory type (ItemEquipLoc) for which to find a best item, such as "INVTYPE_HEAD".
--	Index: If 1 or nil, return the best item for that slot.  If 2, return the second-best item for that slot.  (Most types don't have a second-best item.)
--	DoNotRescan: If true, don't rescan for best items if information is not available.
function PawnGetBestItemLink(ScaleName, InvType, Index, DoNotRescan)
	if not PawnIsInitialized then VgerCore.Fail("Can't get best item lists until Pawn is initialized") return nil end
	if not InvType or InvType == "" or InvType == "INVTYPE_TRINKET" or InvType == "INVTYPE_BAG" or InvType == "INVTYPE_QUIVER" or InvType == "INVTYPE_TABARD" or InvType == "INVTYPE_BODY" then return nil end

	Index = Index or 1
	if Index ~= 1 and Index ~= 2 then
		VgerCore.Fail("Index must be 1 or 2.")
		return nil
	end
	local Scale = PawnCommon.Scales[ScaleName]
	if not Scale then
		VgerCore.Fail("ScaleName must be the name of an existing scale, and is case-sensitive.")
		return nil
	end
	local CharacterOptions = Scale.PerCharacterOptions[PawnPlayerFullName]
	if not CharacterOptions then
		VgerCore.Fail("This method can't be used if per-character options haven't been set up for this scale yet.")
		return nil
	end
	local BestItems = CharacterOptions.BestItems
	if not BestItems then
		if DoNotRescan then return end
		PawnFindBestItems(ScaleName)
		if not BestItems then return end
	end
	
	if InvType == "INVTYPE_WEAPON" then
		if Index == 2 then InvType = "INVTYPE_WEAPONOFFHAND" Index = 1 else InvType = "INVTYPE_WEAPONMAINHAND" end
	elseif InvType == "INVTYPE_SHIELD" or InvType == "INVTYPE_HOLDABLE" then
		InvType = "INVTYPE_WEAPONOFFHAND"
	elseif InvType == "INVTYPE_ROBE" then
		InvType = "INVTYPE_CHEST"
	elseif InvType == "INVTYPE_RANGED" or InvType == "INVTYPE_RANGEDRIGHT" then
		-- All ranged weapons go in the main hand now.
		InvType = "INVTYPE_WEAPONMAINHAND"
	end
	local BestData = BestItems[InvType]
	if not BestData then return nil end
	
	return BestData[3 * Index - 1]
end

-- Shortcut for PawnIsItemAnUpgrade that takes in an item ID.
-- Returns data in the same format as PawnIsItemAnUpgrade.
function PawnIsItemIDAnUpgrade(ItemID)
	if not PawnIsInitialized then VgerCore.Fail("Can't check to see if items are upgrades until Pawn is initialized") return end
	local Item = PawnGetItemData("item:" .. ItemID)
	if not Item then return end
	return PawnIsItemAnUpgrade(Item)
end

-- This is largely the same as getting the item data for a link and then calling PawnIsItemAnUpgrade on it,
-- but this one also works with relics, can support minimum level requirements, and so on.  It's intended as the
-- easiest way to answer the question "should this item have a green arrow?".
-- Returns:
--   true: This item is indeed an upgrade for something.
--   false: This item is not an upgrade.
--   nil: We're not sure yet.
function PawnShouldItemLinkHaveUpgradeArrow(ItemLink, CheckLevel)
	if not PawnIsInitialized then VgerCore.Fail("Can't check to see if items are upgrades until Pawn is initialized") return end

	--if PawnOptions.DebugBagArrows then VgerCore.Message("Checking upgrade information for " .. tostring(ItemLink)) end

	local _, _, _, _, MinLevel = GetItemInfo(ItemLink)
	if MinLevel == nil then return nil end
	if CheckLevel and UnitLevel("player") < MinLevel then return false end
	if PawnCanItemHaveStats(ItemLink) then
		local Item = PawnGetItemData(ItemLink)
		if Item == nil or Item.Link == nil then return nil end -- If we don't have stats for the item yet, ask again later.
		if PawnOptions.DebugBagArrows then
			local UpgradeInfo, ItemLevelIncrease, BestItemFor, SecondBestItemFor, NeedsEnhancements = PawnIsItemAnUpgrade(Item)
			if UpgradeInfo ~= nil then
				if PawnOptions.DebugBagArrows then VgerCore.Message("Found upgrade for " .. ItemLink) end
				local i
				for i = 1, #UpgradeInfo do
					local u = UpgradeInfo[i]
					if PawnOptions.DebugBagArrows then VgerCore.Message("  " .. u.LocalizedScaleName .. ": +" .. u.PercentUpgrade .. "% vs. " .. tostring(u.ExistingItemLink)) end
				end
			end
			return UpgradeInfo ~= nil
		else
			local UpgradeInfo, ItemLevelIncrease = PawnIsItemAnUpgrade(Item)
			return UpgradeInfo ~= nil or (PawnCommon.ShowItemLevelUpgrades and ItemLevelIncrease ~= nil)
		end
	elseif PawnCommon.ShowRelicUpgrades and PawnCanItemBeArtifactUpgrade(ItemLink) then
		return PawnGetRelicUpgradeInfo(ItemLink) ~= nil
	else
		return false -- If the item can never be an upgrade to anything, don't check again.
	end
end

-- Clears the best item level data for this character only.
function PawnClearBestItemLevelData()
	PawnOptions.ItemLevels = nil
	local Slot
	for Slot = 1, 18 do
		PawnAddItemToLevelTracker(PawnGetItemDataForInventorySlot(Slot))
	end
end

-- Shows or hides the Pawn UI.
function PawnUIShow()
	if not PawnIsInitialized or not PawnUIFrame then
		VgerCore.Fail("Pawn UI is not loaded!")
		return
	end
	if PawnUIFrame:IsShown() then
		PawnUIFrame:Hide()
	else
		PawnUIFrame:Show()
	end
end
