local addonName, addon = ...
local print = function( ... ) print( "|cFF5AB45A"..addonName.."|r:" , ... ) end
local debug = function( ... )
	if not _G.PredacifyDebug then return end
	print( ... )
end

local OLD = "Voidstorm"
local NEW = "Predacea"
local ARTICLES = { "[tT]he" } -- articles in front of "Voidstorm" that should be removed for "Predacea"
local SPECIAL = {} -- special cases of articles/prepositions

local fullLocale, locale = GetLocale():match("((..)..)")
if locale == "zh" then locale = localeFull end -- for our purposes, there's no difference between esEs and esMX (the original Quel'thalas map tiles have the same word formatted slightly different, esES has thinner letters)
local  mapLocale = locale
if locale == "de" then
	OLD = "Leerensturm"
	ARTICLES = {"[dD]er","[dD]en","[dD]em"}
	SPECIAL = {
		["des Leerensturms"] = "von Predacea", -- trailing genitive -> preposition ("of") that doesn't require an article
		["im Leerensturm"] = "in Predacea", -- "im" = short form of "in the" (m/n) so remove the article
		["zum Leerensturm"] = "nach Predacea", -- different ways of stating "to", but the former wouldn't fit Predacea as a proper name (the m is again a portion of an article, too)
	}
	mapLocale = "en" -- same name as en
elseif locale == "es" then
	OLD = "[tT]ormenta del Vacío"
	ARTICLES = { "[lL]a" }
	mapLocale = "en" -- same name as en
elseif locale == "fr" then
	OLD = "[tT]empête du Vide" -- noticed lower-case versions in frFR, applied this to other Romance, too, just in case
	NEW = "Prédécéa"
	ARTICLES = { } -- after consulting with a friend, in French the article "la" should stay
elseif locale == "it" then
	OLD = "[tT]empesta del Vuoto"
	ARTICLES = { "[lL]a" }
	mapLocale = "en" -- same name as en
elseif locale == "ko" then
	OLD = "공허폭풍"
	NEW = "프레다시아"
elseif locale == "pt" then
	OLD = "[tT]empestade do Caos"
	NEW = "Predaceia"
	ARTICLES = { "[aA]" }
elseif locale == "ru" then
	OLD = "Буря Бездны"
	NEW = "Предицея"
-- TODO Wowhead has no Completion text of quest #92944 for either zh locale, so we need to know how "Predacea" is spelled.
elseif locale == "zhCN" then -- 
	OLD = "虚影风暴"
	mapLocale = "en" -- same name as en - TEMPORARY
elseif locale == "zhTW" then
	OLD = "虛無風暴"
	mapLocale = "en" -- same name as en - TEMPORARY
end

local PREDACEA_MAPID = 2405
local QUELTHALAS_MAPID = 2537

local predacify = function( input, ignoreArticles )
	if not input:find(OLD) then return input end
	--if input:find(OLD) then debug(debugstack(2,3,2)) end
	for o,n in pairs(SPECIAL) do
		input = tostring(input):gsub( o, n )
	end
	for i,a in ipairs(ignoreArticles and {} or ARTICLES) do
		input = tostring(input):gsub( a.." "..OLD, NEW )
	end
	return tostring(input):gsub( OLD, NEW )
end


local hookTextFrame = function( frame )
	--if frame.NulHook then return end
	--frame.NulHook = true
	local func = frame["DisplayLine"] and "DisplayLine" or "SetText" -- special case for Storyline addon
	hooksecurefunc( frame, func, function( self, text, ... )
		if text and text:find(OLD) then
			text = predacify( text )
			if text:find(OLD) then return end -- we fucked up, this would cause an infinite recursion
			frame:SetText( text, ... )
		end
	end)
end

-- MAP STUFF

-- mouseover on Quel'Thalas map, as well as POIs (for portals)
local areaLabelFound, zoneLabelFound
local findDataProvider = function(self)
	for dataProvider in pairs(self.dataProviders) do
		if dataProvider.OnSetAreaLabel then
			hookTextFrame( dataProvider.Label.Name )
			areaLabelFound = true
		end
		if dataProvider.ZoneLabel then
			hookTextFrame( dataProvider.ZoneLabel.Text )
			zoneLabelFound = true
		end
	end
end
if WorldMapFrame then
	findDataProvider(WorldMapFrame) -- SHOULD already exist
end
if not WorldMapFrame or not areaLabelFound then
	hooksecurefunc( WorldMapFrame or WorldMapMixin, "AddStandardDataProviders", findDataProvider ) -- ...but if not...
end
-- Flight Map: see addon hooks

-- NavBar above worldmap
local zones = { -- juuust in case there's like subzones, delves etc.
	[PREDACEA_MAPID] = NEW,
}
local listFuncOrig = WorldMapNavBarButtonMixin.GetDropdownList
local listFunc = function(self)
	local list, found = listFuncOrig(self)
	for i, dropdownZone in ipairs(list) do
		if zones[dropdownZone.id] then
			found = true
			dropdownZone.text = zones[dropdownZone.id]
		end
	end
	if found then
		-- and sort it again
		table.sort(list, function(entry1, entry2) return entry1.text < entry2.text end)
	end
	return list
end
local NavBarAddButtonFunc = function( self, buttonData )
	if not buttonData or not zones[buttonData.id] -- no need to even look at other zones
		or self ~= WorldMapFrame.NavBar -- also called for the NavBar in the Encounter Journal (and possibly elsewhere) where it uses the journalInstanceID, which will eventually reach 2405, too
	then return end
	
	local navButton
	for i, button in ipairs(self.navList) do -- SHOULD be the last one, but this call might theoretically be delayed until after another AddButton
		if button.id == buttonData.id then
			navButton = button
			break
		end
	end
	if not navButton then return end
	
	navButton:SetText( zones[buttonData.id] )
	navButton:SetWidth( navButton.text:GetStringWidth() + ( not self.oldStyle and 53 or 30 ) )
	navButton.listFunc = listFunc
	
	NavBar_CheckLength( self )
end
hooksecurefunc( "NavBar_AddButton", NavBarAddButtonFunc )

-- minimap
hookTextFrame( MinimapZoneText )
-- and just in case you logged into an area where it said "Voidstorm":
if MinimapZoneText:GetText() == OLD then MinimapZoneText:SetText(NEW) end
-- tooltip
hooksecurefunc( "Minimap_SetTooltip", function()
	--local text = GameTooltipTextLeft1:GetText() -- can be secret
	if GetZoneText() == OLD then
		GameTooltipTextLeft1:SetText(NEW)
	end
end)

-- popup when changing (sub)zones
hookTextFrame( ZoneTextString )
hookTextFrame( SubZoneTextString )

-- MAP ART
local texturePath = "Interface/Addons/%s/Worldmap/%s/EK_Midnight_Continent%d.png"
local texturePathFunc = function(index)
	return texturePath:format( addonName, mapLocale, index )
end
local replaceMapTiles = {
	-- areas where "Voidstorm" may show up - in some locales, that includes 37-40 (the latter not really, but for symmetry's sake...) which we'll replace with blank tiles (actual files from the blank flightmaster map)
	[7336234] = 7517408, -- EK_Midnight_Continent37 => EasternKingdoms_Midnight_Taxi36
	[7336235] = 7517409, -- EK_Midnight_Continent38 => EasternKingdoms_Midnight_Taxi37
	[7336236] = 7517410, -- EK_Midnight_Continent39 => EasternKingdoms_Midnight_Taxi38
	[7336238] = 7517411, -- EK_Midnight_Continent40 => EasternKingdoms_Midnight_Taxi39
	[7336251] = texturePathFunc(52), -- EK_Midnight_Continent52 => custom
	[7336252] = texturePathFunc(53), -- EK_Midnight_Continent53 => custom
	[7336253] = texturePathFunc(54), -- EK_Midnight_Continent54 => custom
	[7336254] = texturePathFunc(55), -- EK_Midnight_Continent55 => custom
}
texturePath, texturePathFunc = nil, nil
hooksecurefunc( MapCanvasDetailLayerMixin, "RefreshDetailTiles", function( self, mapCanvas )
	if self.mapID == QUELTHALAS_MAPID and self.layerIndex == 1 then
		for detailTile in self.textureLoadGroup:EnumerateTextures() do
			local texture = detailTile:GetTexture()
			if replaceMapTiles[texture] then
				detailTile:SetTexture( replaceMapTiles[texture], nil, nil, "TRILINEAR" )
				--detailTile:Show()
			end
		end
	end
end)

-- TODO moar map stuff

-- getting summoned
local GetExpirationTextOrig = StaticPopupDialogs["CONFIRM_SUMMON"].GetExpirationText
StaticPopupDialogs["CONFIRM_SUMMON"].GetExpirationText = function( dialog, data, timeleft )
	if C_SummonInfo.GetSummonConfirmAreaName() == OLD then
		return string.format( dialog.dialogInfo.text, C_SummonInfo.GetSummonConfirmSummoner() or "", NEW, timeleft < 60 and timeleft or _G.ceil(timeleft / 60), timeleft < 60 and _G.SECONDS or _G.MINUTES )
	end
	return GetExpirationTextOrig( dialog, data, timeleft )
end

-- Entries only mentioned as comments are the exception that mention "the Voidstorm" as the beam, the reason why we whitelist
-- Note: if your character's name is "Voidstorm", this will also replace that name in a handful of quests. I see no reason to handle this, since there's only a couple dozen characters of that name on either NA and EU.
local quests = {
	[90914] = true, -- A Born Killer
	[89507] = true, -- Abundant Offerings
	[91561] = true, -- Bloodborne Pathogen
	[93801] = true, -- Calculated Culling
	[90690] = true, -- Charge of the Vanguard -- THE THE THE
	[91887] = true, -- Dominion of Deceit
	-- 86510 Domus Penumbra
	[91885] = true, -- Drain You
	-- 86737 Fair Breeze, Light Bloom
	[90777] = true, -- Feeding the Flame
	[94920] = true, -- Hal'hadar Battery Core
	-- 90862 In Times of Need
	-- 86543 Magisters' Terrace: Homecoming
	-- 93752 Murder Row
	[93758] = true, -- Nexus-Point Xenas
	-- 86549 No Fear of the Dark - "a portal to Voidstorm", but at the point, we don't know the planet's name yet - TODO include on characters in Adventure Mode?
	[92509] = true, -- One Cruel Implement
	-- 90910 Overwhelming Darkness - edge case: "The Voidstorm has made my voidwalker unruly and disobedient.", whether that is caused by the Voidstorm or Predacea is unclear
	[88982] = true, -- Prey Contract: Renegade Magister - is this a regular quest you accept, or just for display in the Prey selection?
	-- 90858 Repress the Oppressors - "She leverages the power of the Voidstorm..."
	[94790] = true, -- Research Console: Exploring the Void - eh...
	[93970] = true, -- Researching the Storm
	[93426] = true, -- Sparks of War: Voidstorm
	[94361] = true, -- The Abundant Life
	[86559] = true, -- The Far, Far Frontier
	[95276] = true, -- The Last Push
	-- 86626 The Ransacked Lab
	-- 86621 The Wayward Magister
	[88708] = true, -- Violent Conclusions
	[91583] = true, -- Voidscar Arena: The Bastion of Valor
	[89388] = true, -- Voidstorm - this should be only offered in Adventure Mode, when your Warband has learned the name Predacea yet
	-- 91967 You Know This Evil?
	
	-- term only in Progress/Completion text (not searchable via Wowhead)
	[92939] = true, -- It's Not Just a Rock! - TODO: find more quests with "Voidstorm" in Completion text
	
	-- 12.0.7 additions
	[96705] = true, -- [DNT] - seems to be an unused version of "Veterans of the Great Dark", let's keep it juuust in case...
	[97215] = true, -- A Secondary Strike
	[96051] = true, -- Through the Cold Rift
	[96708] = true, -- To the Voidstorm and Beyond! - again a bit of an edge case, "The energies of Voidstorm...", but it does indeed rather talk about the zone (the energies are from the Void, but belong to the planet now, property-wise, yadda yadda)

}
-- quest "greeting" panel, list of available and active/completed quests
local questButtonFunc = function( self, questID, titleText, isIgnored, isTrivial )
	if quests[questID] and titleText and titleText:find(OLD) then
		titleText = predacify( self:GetText() ) -- title is formatted already, take it from there instead of copying the logic
		self:SetText( titleText, true )
	end
end
-- UpdateTitleForQuest is from GossipSharedQuestButtonMixin, but the Mixins below are already created at this point
hooksecurefunc( GossipAvailableQuestButtonMixin, "UpdateTitleForQuest", questButtonFunc )
hooksecurefunc( GossipActiveQuestButtonMixin, "UpdateTitleForQuest", questButtonFunc ) -- also used for completed quests
hooksecurefunc( GossipQuestButtonMixin, "UpdateTitleForQuest", questButtonFunc ) -- not used, generic? keeping it just in case
-- quest description text
local hookQuestTextFrame = function( self, text, selfcall )
	if selfcall then return end
	local questID = QuestInfoFrame.questLog and C_QuestLog.GetSelectedQuest() or GetQuestID()
	--debug(self:GetName(),"quest",questID,text)
	if quests[questID] and text and text:find(OLD) then
		text = predacify( text )
		self:SetText( text, true )
	end
end
-- quest title and description (when selected)
hooksecurefunc( QuestInfoTitleHeader, "SetText", hookQuestTextFrame )
hooksecurefunc( QuestInfoDescriptionText, "SetText", hookQuestTextFrame )
hooksecurefunc( QuestInfoObjectivesText, "SetText", hookQuestTextFrame )
-- quest completion for quests 88708, 91887, 92939, possibly more (not searchable on Wowhead)
hooksecurefunc( QuestProgressText, "SetText", hookQuestTextFrame )
hooksecurefunc( QuestInfoRewardText, "SetText", hookQuestTextFrame )
-- quest title and objectives in list
hooksecurefunc( QuestLogTitleMixin,"OnLoad", function(self)
	hooksecurefunc( self, "SetHeight", function(self)
		if not quests[self.questID] then return end
		self.Text:SetText( predacify(self.Text:GetText()) )
		-- and check the corresponding objective text, too - no OnLoad there, but it should have an associated questID at this point
		for line in QuestScrollFrame.objectiveFramePool:EnumerateActive() do
			if ( line.questID == self.questID ) then
				line.Text:SetText( predacify(line.Text:GetText()) )
			end
		end
	end)
end)
-- category name in list
hooksecurefunc( "QuestLogQuests_Update", function()
	for header in QuestScrollFrame.headerFramePool:EnumerateActive() do
		if header:GetText() == OLD then
			header:SetText( NEW )
		end
	end
end)
-- quest abandon popup
hooksecurefunc( "StaticPopup_Show", function( which, text_arg1, text_arg2, data )
	if not which == "ABANDON_QUEST" then return end
	local questID = QuestInfoFrame.questLog and C_QuestLog.GetSelectedQuest() or GetQuestID()
	if not quests[questID] then return end
	local dialog = StaticPopup_FindVisible( which )
	if not dialog then return end -- maybe already deleted, possibly by another addon auto-confirming the abandon
	dialog.Text:SetText( predacify(dialog.Text:GetText()) )
end)

-- quest pins on map
local questPinZones = { -- pins in Silvermoon/Eversong and Harandar/The Den might have the line "Take the portal from XY to Voidstorm"
	[2393] = true, -- Silvermoon City
	[2395] = true, -- Eversong Woods
	[2413] = true, -- Harandar
	[2576] = true, -- The Den
}
hooksecurefunc( QuestPinMixin, "OnMouseEnter", function(self)
	if quests[self:GetQuestID()] or questPinZones[C_Map.GetBestMapForUnit("player")] then
		for i = 1, GameTooltip:NumLines() do
			local text = _G["GameTooltipTextLeft"..i]:GetText()
			if text and text:find(OLD) then
				_G["GameTooltipTextLeft"..i]:SetText( predacify(text) )
			end
		end
	end
end)
-- pin on Eversong map for Silvermoon, might mention the "Voidstorm" quest
hooksecurefunc( QuestHubPinMixin, "BuildRelatedQuests", function(self)
	if not questPinZones[self.lastOwningMapID] then return end
	local dataProvider = self:GetDataProvider()
	for questID, questOffer in pairs(dataProvider:GetQuestOffers()) do
		if quests[questID] then 
			questOffer.questName = predacify( questOffer.questName )
		end
	end
end)

-- quest tooltips
local questTooltipFunc = function( tooltip, data )
	if not quests[data.id] then return end
	for _, line in pairs(data.lines) do
		if not issecretvalue(line.leftText) and line.leftText and line.leftText:find(OLD) then
			line.leftText = predacify( line.leftText )
		end
	end
end
TooltipDataProcessor.AddTooltipPreCall( Enum.TooltipDataType.Quest, questTooltipFunc )
TooltipDataProcessor.AddTooltipPreCall( Enum.TooltipDataType.QuestPartyProgress, questTooltipFunc )

-- item tooltips
local items = {
		-- by name
		-- 237854 Voidstorm Bardiche - Trading Post item from before Midnight, and definitely more related to the Voidstorm (or rather, Void Elves) than Predacea. Also, hooking Trading Post would be additional effort for just one item, so, nah :)
		[238533] = true, -- Vial of Voidstorm Oddities
		[238544] = true, -- Voidstorm Defense Spear
		[238581] = true, -- Speculative Voidstorm Crystal
		[238596] = true, -- Miner's Guide to Voidstorm
		[238598] = true, -- Lost Voidstorm Satchel
		[238631] = true, -- Voidstorm Leather Sample
		[238655] = true, -- Majestic Voidstorm Lure
		-- 244355 Voidstorm Obsidian - random item dropping in the TWW Delve Archival Assault. Definitely a teaser, but not really from Predacea
		[259192] = true, -- Voidstorm Ashes
		[264972] = true, -- Voidstorm Victuals
		
		-- by effect text
		-- 243343 Angler's Anomaly - "the nebulous nature of Voidstorm", that's not Predacea... or is it?
		
		-- by flavor text
		[238373] = true, -- Ominous Octopus
		[238377] = true, -- Blood Hunter
		[238378] = true, -- Shimmersiren
		[238379] = true, -- Warping Wise
		[238380] = true, -- Null Voidfish
		[238381] = true, -- Hollow Grouper
		[238500] = true, -- Maella's Report -- edge case: "Extensive notes on the Voidstorm...", but the text is about the zone and its wildlife in general
		[246661] = true, -- Glaring Glowcap
		[248086] = true, -- Void Essence
		[251905] = true, -- Specialized Liver
		[268552] = true, -- Ascendant Voidcore
		[269668] = true, -- Hungering Oblivium

}
TooltipDataProcessor.AddTooltipPreCall( Enum.TooltipDataType.Item, function( tooltip, data )
	if issecretvalue(data) or not data or issecretvalue(data.id) or not items[data.id] then return end
	for i, line in pairs(data.lines) do
		if not issecretvalue(line.leftText) and line.leftText and line.leftText:find(OLD) then
			line.leftText = predacify( line.leftText ) -- TODO check articles in effect/flavor text
		end
	end
end)
-- item loot alerts
hooksecurefunc( LootAlertSystem,  "setUpFunction", function( frame, link )
	local itemID = GetItemInfoFromHyperlink( link )
	if not items[itemID] then return end
	frame.ItemName:SetText( predacify(frame.ItemName:GetText()) )
end)
-- TODO think of more places where item names can show up

-- object tooltips - sadly no ID for this tooltipData, but the list of objects is short enough that we can say "ok, all of those should be modified"
TooltipDataProcessor.AddTooltipPreCall( Enum.TooltipDataType.Object, function( tooltip, data )
	if issecretvalue(data) or not data then return end
	local line = data.lines[1]
	if line and not issecretvalue(line.leftText) and line.leftText and line.leftText:find(OLD) then
		line.leftText = predacify( line.leftText )
	end
end)

-- minimap tooltips - basically the same as objects. The portals and profession knowledge items should be the only things this applies to
TooltipDataProcessor.AddTooltipPreCall( Enum.TooltipDataType.MinimapMouseover, function( tooltip, data )
	if issecretvalue(data) or not data then return end
	gData = data
	local line = data.lines[1]
	if line and not issecretvalue(line.leftText) and line.leftText and line.leftText:find(OLD) then
		line.leftText = predacify( line.leftText )
	end
end)

-- spell tooltips
local spells = {
		[1225312] = true, -- Amassing Voidlust
		--[1235221] = true, -- Anomaly Filament - see item Angler's Anomaly
		[1225274] = true, -- Blood Hunter
		[1264312] = true, -- Bombing Run Complete: Vehicle Exit [DNT]
		[1265428] = true, -- Cataloging Vehicle
		[1227430] = true, -- Death from Above
		[1270737] = true, -- Forward Positions
		[1225267] = true, -- Hollow Grouper
		[1251978] = true, -- Hungering Presence
		[1227420] = true, -- Hungering Presence
		[1225946] = true, -- Majestic Voidstorm Lure
		[1226229] = true, -- Majestic Voidstorm Lure
		[1225268] = true, -- Null Voidfish
		[1225273] = true, -- Ominous Octopus
		[1232189] = true, -- Overflowing Power
		[1214506] = true, -- Portal Effect: Voidstorm
		[1258057] = true, -- Portal: Argus to Voidstorm [DNT]
		[1214507] = true, -- Portal: Voidstorm
		[1225695] = true, -- Portal: Voidstorm
		[1285019] = true, -- Portal: Voidstorm Muster Point [DNT]
		[1242359] = true, -- Portal: Voidstorm Muster Point [DNT]
		[1255688] = true, -- Protocol of Predation
		[1255687] = true, -- Protocol of Sustenance
		[1255685] = true, -- Protocol of Violence
		[1225963] = true, -- Quest Launch: Voidstorm
		[1273314] = true, -- Resting
		[1225271] = true, -- Shimmersiren
		[1261842] = true, -- Start Voidstorm
		[1226237] = true, -- Summon Voidstorm Majestic Beast
		--[1279716] = true, -- Targeted by Voidstorm Turrets - not sure where these are used, skipping for now
		--[1279714] = true, -- Targeted by Voidstorm Turrets
		[1295992] = true, -- Teleport to Voidstorm
		[1295993] = true, -- Teleport to Voidstorm
		[1297765] = true, -- Teleport to Voidstorm
		[1297766] = true, -- Teleport to Voidstorm
		--[1217174] = true, -- TEMP [DNT]
		--[1253798] = true, -- Void Breach - "The Sunwell is weakening against the Voidstorm." so the actual Voidstorm
		[1224880] = true, -- Void Herbs
		--[1252642] = true, -- Void Infusion - "The Voidstorm overwhelms you with Void power"
		--[286301] = true, -- Void Storm - older spells that are translated the same as "Voidstorm" in certain locales
		--[260524] = true, -- Void Storm
		--[171714] = true, -- Void Storm
		--[316016] = true, -- Void Storm
		--[244928] = true, -- Void Storm
		--[260229] = true, -- Void Storm
		--[260524] = true, -- Void Storm
		--[260525] = true, -- Void Storm
		--[165812] = true, -- Void Storm
		--[244928] = true, -- Void Storm
		[1224897] = true, -- Voidbound Argentleaf
		[1224900] = true, -- Voidbound Azeroot
		[1224899] = true, -- Voidbound Mana Lily
		[1224901] = true, -- Voidbound Sanguithorn
		[1224898] = true, -- Voidbound Tranquility Bloom
		[1222911] = true, -- Voidlust
		[1255158] = true, -- Voidsated - this feels like a test spell, but does refer to the Voidstorm as a zone
		[1225336] = true, -- Voidstorm - related to Fishing
		--[1226380] = true, -- Voidstorm - Reshanor (K'aresh world boss) ability and thus referring to an actual Voidstorm
		--[1259408] = true, -- Voidstorm
		--[1259263] = true, -- Voidstorm - hmm...
		--[1263767] = true, -- Voidstorm
		--[1227729] = true, -- Voidstorm
		--[473345] = true, -- Voidstorm [DNT]
		--[1223300] = true, -- Voidstorm [DNT]
		--[1261902] = true, -- Voidstorm Pockets - does rather refer to the beam
		[1272997] = true, -- Voidstorm Victuals - likely related to the item, most likely not shown ingame, but just in case
		[1273308] = true, -- Warmed By A Cold Fire
		[1225279] = true, -- Warping Wise
		[1270742] = true, -- Wings of the Void
		--[1227730] = true, -- Wrath of Void - no actual mention of "Voidstorm", not sure why this came up on the search, but would likely refer to the actual Voidstorm
		--[1227783] = true, -- Wrath of Void
}
TooltipDataProcessor.AddTooltipPreCall( Enum.TooltipDataType.Spell, function( tooltip, data )
	if issecretvalue(data) or not data or issecretvalue(data.id) or not spells[data.id] then return end
	for i, line in pairs(data.lines) do
		if not issecretvalue(line.leftText) and line.leftText and line.leftText:find(OLD) then
			line.leftText = predacify( line.leftText ) -- TODO check articles in effect/flavor text
		end
	end
end)
-- learned tradeskill alerts - TODO actually test this
hooksecurefunc( NewRecipeLearnedAlertSystem,  "setUpFunction", function( frame, recipeID )
	if not spells[recipeID] then return end
	frame.Name:SetText( predacify(frame.Name:GetText()) )
end)
-- cast bar (item 238655 has a 2sec cast time)
hookTextFrame( PlayerCastingBarFrame.Text )
hookTextFrame( OverlayPlayerCastingBarFrame.Text )

-- NPC tooltips
local NPCs = {
	-- 243235 Voidstorm Bombardier - from the Sunwell intro, so related to the actual Voidstorm
	[252194] = true, -- Voidstorm Void Flame Guardian
}
TooltipDataProcessor.AddTooltipPreCall( Enum.TooltipDataType.Unit, function( tooltip, data )
	if issecretvalue(data) or not data or issecretvalue(data.id) or not NPCs[data.id] then return end
	for i, line in pairs(data.lines) do
		if not issecretvalue(line.leftText) and line.leftText and line.leftText:find(OLD) then
			line.leftText = predacify( line.leftText ) -- TODO check articles in effect/flavor text
		end
	end
end)

-- achievement tooltips
local achievements = {
		[61940] = true, -- Abundance: Artisan of Loanite
		[41806] = true, -- Breaching the Voidstorm
		[61857] = true, -- Explore Voidstorm
		[62105] = true, -- Lysikas Would Be Proud
		[61091] = true, -- Midnight Safari
		[61861] = true, -- Oh, No You Don't!
		[61563] = true, -- Skyriding Glyphs: Ethereum Refinery
		[61558] = true, -- Skyriding Glyphs: Gnawing Reach
		[61564] = true, -- Skyriding Glyphs: Hanaar Outpost
		[61562] = true, -- Skyriding Glyphs: Masters' Perch
		[61561] = true, -- Skyriding Glyphs: Obscurion Citadel
		[61560] = true, -- Skyriding Glyphs: Shadowguard Point
		[61557] = true, -- Skyriding Glyphs: The Bladeburrows
		[61559] = true, -- Skyriding Glyphs: The Gorging Pit
		[61556] = true, -- Skyriding Glyphs: The Ingress
		[61555] = true, -- Skyriding Glyphs: The Molt
		[61552] = true, -- Skyriding Glyphs: The Voidspire
		[61864] = true, -- Sojourner of Voidstorm
		[62385] = true, -- Staring Into The Void
		[62130] = true, -- The Ultimate Predator
		[62133] = true, -- Thrill of the Chase
		[61224] = true, -- Tour of Duty: Voidstorm
		[62126] = true, -- Treasures of Voidstorm
		[61583] = true, -- Voidstorm Glyph Hunter
		[62291] = true, -- Voidstorm: The Highest Peaks
		[62256] = true, -- Yelling into the Voidstorm
}
TooltipDataProcessor.AddTooltipPreCall( Enum.TooltipDataType.Achievement, function( tooltip, data )
	if issecretvalue(data) or not data or issecretvalue(data.id) or not achievements[data.id] then return end
	for i, line in pairs(data.lines) do
		if line.leftText and line.leftText:find(OLD) then
			line.leftText = predacify( line.leftText, i > 0 ) -- only remove "the" in the title, not in the description: "Complete the Voidstorm world quest...", "Complete all of the Voidstorm achievements..."
		end
	end
end)
-- achievement alerts
hooksecurefunc( AchievementAlertSystem, "setUpFunction", function( frame, achievementID, alreadyEarned )
	if not achievements[achievementID] then return end
	frame.Name:SetText( predacify(frame.Name:GetText()) )
end)
hooksecurefunc( CriteriaAlertSystem,  "setUpFunction", function( frame, achievementID, criteriaString )
	if not achievements[achievementID] then return end
	frame.Name:SetText( predacify(frame.Name:GetText()) )
end)
-- hook for achievement frame below

-- links in chat
local getAchievementName = function(achievementID) return select( 2, GetAchievementInfo(achievementID) ) end
local fixLinks = { -- separate table for only things that have "Voidstorm" in the name
	-- quests and achievements have a specific value so we can un-predacify them before being sent in chat messages, invalid names cause the link to be turned into just plain text
	["quest:93426"] = C_QuestLog.GetTitleForQuestID(93426), -- Sparks of War: Voidstorm
	["quest:89388"] = C_QuestLog.GetTitleForQuestID(89388), -- Voidstorm
	["quest:96708"] = C_QuestLog.GetTitleForQuestID(96708), -- To the Voidstorm and Beyond!
	["achievement:41806"] = getAchievementName(41806), -- Breaching the Voidstorm
	["achievement:61857"] = getAchievementName(61857), -- Explore Voidstorm
	["achievement:61864"] = getAchievementName(61864), -- Sojourner of Voidstorm
	["achievement:61224"] = getAchievementName(61224), -- Tour of Duty: Voidstorm
	["achievement:62126"] = getAchievementName(62126), -- Treasures of Voidstorm
	["achievement:61583"] = getAchievementName(61583), -- Voidstorm Glyph Hunter
	["achievement:62291"] = getAchievementName(62291), -- Voidstorm: The Highest Peaks
	["achievement:62256"] = getAchievementName(62256), -- Yelling into the Voidstorm
	-- items and spells auto-fix themselves, so just true
	["item:238533"] = true, -- Vial of Voidstorm Oddities
	["item:238544"] = true, -- Voidstorm Defense Spear
	["item:238581"] = true, -- Speculative Voidstorm Crystal
	["item:238596"] = true, -- Miner's Guide to Voidstorm
	["item:238598"] = true, -- Lost Voidstorm Satchel
	["item:238631"] = true, -- Voidstorm Leather Sample
	["item:238655"] = true, -- Majestic Voidstorm Lure
	["item:259192"] = true, -- Voidstorm Ashes
	["item:264972"] = true, -- Voidstorm Victuals
	["spell:1225946"] = true, -- Majestic Voidstorm Lure
	["spell:1226229"] = true, -- Majestic Voidstorm Lure
	["spell:1214506"] = true, -- Portal Effect: Voidstorm
	["spell:1214507"] = true, -- Portal: Voidstorm
	["spell:1225695"] = true, -- Portal: Voidstorm
	["spell:1226237"] = true, -- Summon Voidstorm Majestic Beast
	["spell:1225336"] = true, -- Voidstorm - related to Fishing
	["spell:1259408"] = true, -- Voidstorm
	["spell:1259263"] = true, -- Voidstorm - has a tooltip, so might be shown
	["spell:1261902"] = true, -- Voidstorm Pockets
	["spell:1272997"] = true, -- Voidstorm Victuals - likely related to the item, most likely not shown ingame, but just in case
}
local CHANNELMATCH = "|Hchannel:channel:(%d+)|h%[(.+) %- ([^%]]+)%]"
local CHANNELREPLACE = "|Hchannel:channel:%d|h[%s - %s]"
local LINKMATCH = "|H%s([^|]*)|h%[([^%]]+)%]"
local LINKREPLACE = "|H%s%s|h[%s]"
local qFormat = function(str)
	-- some strings have the player name first, we need to ignore that
	local firstMatch, middle, secondMatch = str:match( "(%%s)(.*)(%%s)" )
	str = secondMatch and str:gsub( "(%%s)(.*)(%%s)", ".+%2(.+)" ) or str:gsub( "%%s", "(.+)" )
	return str
end
local QUESTMATCH = { -- at least the Sparks of War quests is shareable, so...
	qFormat(ERR_QUEST_ACCEPTED_S),
	qFormat(ERR_QUEST_COMPLETE_S),
	qFormat(ERR_QUEST_FAILED_BAG_FULL_S),
	qFormat(ERR_QUEST_FAILED_S),
	qFormat(ERR_QUEST_FORCE_REMOVED_S),
	qFormat(ERR_QUEST_PUSH_ALREADY_DONE_TO_RECIPIENT_S),
	qFormat(ERR_QUEST_PUSH_DEAD_TO_RECIPIENT_S),
	qFormat(ERR_QUEST_PUSH_INVALID_TO_RECIPIENT_S),
	qFormat(ERR_QUEST_PUSH_LOG_FULL_TO_RECIPIENT_S),
	qFormat(ERR_QUEST_PUSH_LOW_LEVEL_TO_RECIPIENT_S),
	qFormat(ERR_QUEST_PUSH_ONQUEST_TO_RECIPIENT_S),
	qFormat(ERR_QUEST_PUSH_PREREQUISITE_TO_RECIPIENT_S),
}
local chatFunc = function( self, element )
	if not element or issecretvalue(element.message) or not element.message or not element.message:find(OLD) then return end
	
	if element.message:match( "^Dump%:" ) then return end
	
	-- General/LocalDefense channels
	local channelID, channelName, channelZone = element.message:match( CHANNELMATCH )
	if channelZone and channelZone == OLD then
		element.message = element.message:gsub( CHANNELMATCH, CHANNELREPLACE:format( channelID, channelName, NEW ) )
	end
	
	-- various quest status messages
	for i, qMatch in pairs(QUESTMATCH) do
		local quest = element.message:match( qMatch )
		if quest and quest:find(OLD) then
			element.message = predacify(element.message)
		end
	end
	
	
	-- hyperlinks
	for link in pairs(fixLinks) do
		local linkMatch = LINKMATCH:gsub( "%%s", link ) -- :format has issues with the other % characters
		local rest, name = element.message:match( linkMatch )
		if name then
			element.message = element.message:gsub( linkMatch, LINKREPLACE:format( link, rest, predacify(name) ) )
		end
	end
end
for i = 1, NUM_CHAT_WINDOWS do
	hooksecurefunc( _G["ChatFrame"..i].historyBuffer, "PushFront", chatFunc )
end
-- un-predacify outgoing messages
EventRegistry:RegisterCallback("ChatFrame.OnEditBoxPreSendText", function( s, editbox )
	local message = editbox:GetText()
	for link, originalName in pairs(fixLinks) do
		if type(originalName) == "string" then 
			local link2 = link:gsub( "(%a+)", "(%1)" )
			local linkMatch = LINKMATCH:gsub( "%%s", link2 ) -- :format has issues with the other % characters
			local linkType, rest, name = message:match( linkMatch )
			if name then 
				editbox:SetText( editbox:GetText():gsub( linkMatch, LINKREPLACE:format( link, rest, originalName ) ) )
			end
		end
	end
end)
-- chat in Guild & Communities frame - does not work (not even in guild chat)
-- hooksecurefunc( CommunitiesFrame.Chat.MessageFrame.historyBuffer, "PushFront", spellFunc )



-- HOOK OTHER ADDONS
local addonHooks = {
	["Immersion"] = function()
		-- Quest Title
		hookTextFrame( ImmersionFrame.TalkBox.NameFrame.Name )
		-- Quest Text
		hookTextFrame( ImmersionFrame.TalkBox.TextFrame.Text )
		-- Quest Objectives
		hookTextFrame( ImmersionContentFrame.ObjectivesText )
	end,
	["Storyline"] = function()
		-- Quest Title
		hookTextFrame( Storyline_NPCFrame.Banner.Title )
		-- Quest Text (also Completion)
		hookTextFrame( Storyline_NPCFrameChatText )
		-- Quest Objectives
		hookTextFrame( Storyline_NPCFrameObjectivesContent.Objectives )
		-- available/accepted quests
		hooksecurefunc( Storyline_API.dialogs.buttons, "getButton", function() -- fires multiple times per GOSSIP_SHOW, but seems the only way to hook reliably
			-- we don't get a proper handle of the buttons, but:
			for i, button in ipairs( { Storyline_API.dialogs.scrollFrame.container:GetChildren() } ) do
				if not button.NulHook then
					button.NulHook = true
					hookTextFrame( button.text )
				end
			end
		end)
	end,
	["Classic Quest Log"] = function()
		-- Quest Title
		hookTextFrame( ClassicQuestLog.Detail.ScrollFrame.Content.TitleHeader )
		-- Quest Text
		hookTextFrame( ClassicQuestLog.Detail.ScrollFrame.Content.DescriptionText )
		-- Quest Objectives
		hookTextFrame( ClassicQuestLog.Detail.ScrollFrame.Content.ObjectivesText )
		-- Quest Log/List
		hooksecurefunc( ClassicQuestLog.Log, "Update", function(self)
			for i, button in ipairs(self.ScrollFrame.Content.Buttons) do
				if not button.NulHooked then -- more reliable than updating after 
					button.NulHooked = true
					hookTextFrame( button )
				end
			end
		end)
		-- Abandon Quest dialog
		hooksecurefunc( ClassicQuestLog.Dialog, "Confirm", function( self, text,callback )
			if text:find(OLD) then
				self.Text:SetText( predacify(text) )
			end
		end)
	end,
	["DialogueUI"] = function()
		local func = function()
			local questID = GetQuestID()
			if not quests[questID] then return end
			-- Quest Title
			local text = DUIQuestFrame.FrontFrame.Header.Title:GetText()
			if text:find(OLD) then
				DUIQuestFrame.FrontFrame.Header.Title:SetText( predacify(text) )
			end
			-- Quest Text
			for _, fs in pairs(DUIQuestFrame.fontStringPool:GetActiveObjects()) do
				local text = fs:GetText()
				if text:find(OLD) then
					fs:SetText( predacify(text) )
				end
			end
		end
		hooksecurefunc( DUIQuestFrame, "HandleQuestDetail", func )
		hooksecurefunc( DUIQuestFrame, "HandleQuestComplete", func )
		hooksecurefunc( DUIDialogOptionButtonMixin, "SetQuest", function( self, questInfo, hotkey )
			-- available/active/complete quests
			if not quests[self.questID] then return end
			if questInfo.title:find(OLD) then
				self:SetButtonText( predacify(questInfo.title), true )
			end
		end)
	end,
	["SexyMap"] = function()
		-- not immediately available
		C_Timer.NewTicker( 10, function(handle)
			if SexyMapZoneTextButton then
				-- find zoneTextFont
				for i, region in pairs( { SexyMapZoneTextButton:GetRegions() } ) do
					if region.SetText then
						--debug("found zoneTextFont")
						hookTextFrame( region )
						-- and just in case you logged into an area where it said "Voidstorm":
						if region:GetText() == OLD then region:SetText(NEW) end
					end
				end
				-- tooltip
				SexyMapZoneTextButton:HookScript( "OnEnter", function()
					local text = SexyMapZoneTextTooltipTextLeft1:GetText()
					if text == OLD then
						SexyMapZoneTextTooltipTextLeft1:SetText(NEW)
					end
				end)
				handle:Cancel()
			end
		end)
	end,
	["WorldQuestTracker"] = function() -- zone summary label on Quel'Thalas map
		local WQTHook = function(questData)
			if not questData or not zones[questData.mapID] then return end
			local anchor = WorldQuestTrackerAddon.WorldSummary.GetAnchor(questData.filterType, questData.worldQuestType, questData.questName, questData.mapID)
			if not anchor or not anchor.Title or not anchor.Title.label then return end
			if not anchor.NulHook then
				anchor.NulHook = true
				hookTextFrame( anchor.Title.label )
				-- this anchor will be reused for other maps, but will be used for "Voidstorm" again when going back to Quel'Thalas, so it's not like we'll touch multiple anchors
			end
		end
		if WorldQuestTrackerAddon.WorldSummary.AddQuest then -- don't think this is likely to exist yet, but could theoretically happen if this function is called with delay
			hooksecurefunc( WorldQuestTrackerAddon.WorldSummary, "AddQuest", WQTHook )
		else
			hooksecurefunc( WorldQuestTrackerAddon, "InitializeFactions", function()
				-- by now AddQuest should exist
				hooksecurefunc( WorldQuestTrackerAddon.WorldSummary, "AddQuest", WQTHook )
			end)
		end
	end,
	["InteractiveWormholes"] = function() -- zone name in tooltip of pin
		InteractiveWormholesTooltip:HookScript( "OnShow", function()
			local text = InteractiveWormholesTooltipTextLeft1:GetText()
			if text == OLD then
				InteractiveWormholesTooltipTextLeft1:SetText(NEW)
			end
		end)
	end,
	-- todo: HN_Midnight
	
	-- not loaded initially, might as well use our system for these stock 'addons', too
	["Blizzard_PlayerSpells"] = function()
		hooksecurefunc( SpellBookItemMixin, "UpdateSpellData", function( self )
			if self.spellBookItemInfo.name:find(OLD) then
				self.spellBookItemInfo.name = predacify( self.spellBookItemInfo.name )
				self:UpdateVisuals()
			end
		end)
	end,
	["Blizzard_AchievementUI"] = function()
		-- main achievement body in the achievement frame
		hooksecurefunc( AchievementTemplateMixin, "Init", function( self, elementData )
			if not elementData or not achievements[elementData.id] then return end
			self.Label:SetText( predacify( self.Label:GetText() ) )
			self.Description:SetText( predacify( self.Description:GetText(), true ) ) -- second parameter to not remove "the" in the description
			self.HiddenDescription:SetText( predacify( self.HiddenDescription:GetText(), true ) ) -- second one that's used when the achievement is focused
		end)
		-- criteria in the achievement frame
		hooksecurefunc( "AchievementObjectives_DisplayCriteria", function( objectivesFrame, id )
			if not id or not achievements[id] then return end
			local textStrings, progressBars, metas = 0, 0, 0
			local numCriteria = GetAchievementNumCriteria(id)
			for i = 1, numCriteria do
				--debug("check crit",i,numCriteria)
				local criteriaString, criteriaType, completed, quantity, reqQuantity, charName, flags, assetID, quantityString = GetAchievementCriteriaInfo(id, i)
				if ( criteriaType == CRITERIA_TYPE_ACHIEVEMENT and assetID ) then
					metas = metas + 1
					local metaCriteria = objectivesFrame:GetMeta(metas)
					if achievements[metaCriteria.id] then
						local v, achievementName = GetAchievementInfo(assetID)
						achievementName = predacify(achievementName)
						debug("predacify subachievement",achievementName)
						metaCriteria.Label:SetText( predacify(achievementName) )
					end
				elseif ( bit.band(flags, EVALUATION_TREE_FLAG_PROGRESS_BAR) == EVALUATION_TREE_FLAG_PROGRESS_BAR ) then
					--
				else
					textStrings = textStrings + 1
					if criteriaString:find(OLD) then
						debug("predacify criterium",criteriaString)
						local criteria = objectivesFrame:GetCriteria(textStrings)
						criteria.Name:SetText( predacify(criteriaString) )
					end
				end
			end
		end)
		-- search results
		hooksecurefunc( "AchievementSearchPreviewButton_OnLoad", function(self)
			debug("AchievementSearchPreviewButton_OnLoad",self.previewIndex)
			-- no further filtering needed, all achievement names containing "Voidstorm" should be handled
			hookTextFrame( self.Name )
		end)
		for i, searchPreview in ipairs(AchievementFrame.SearchPreviewContainer.searchPreviews) do
			-- no further filtering needed, all achievement names containing "Voidstorm" should be handled
			hookTextFrame( searchPreview.Name )
		end
		hooksecurefunc( AchievementFullSearchResultsButtonMixin, "Init", function( self, elementData )
			local achievementID = GetFilteredAchievementID(elementData.index)
			if not achievements[achievementID] then return end
			self.Name:SetText( predacify(self.Name:GetText()) )
		end)
	end,
	["Blizzard_ObjectiveTracker"] = function()
		local func = function( self, quest )
			local questID = quest:GetID()
			if not quests[questID] then return end
			local blocks = self.usedBlocks and self.usedBlocks[self.blockTemplate] and self.usedBlocks[self.blockTemplate][questID]--self:GetBlock(questID)
			if not blocks then return end
			blocks:SetHeader( predacify(quest.title) )
			local complete = blocks:GetExistingLine("QuestComplete")
			if complete and complete.Text then
				complete.Text:SetText( predacify(complete.Text:GetText()) )
			end
			local questLogIndex = C_QuestLog.GetLogIndexForQuestID(questID)
			local numObjectives = GetNumQuestLeaderBoards(questLogIndex)
			for objectiveIndex = 1, numObjectives do
				local line = blocks:GetExistingLine(objectiveIndex)
				if line and line.Text then
					line.Text:SetText( predacify(line.Text:GetText()) )
				end
			end
		end
		hooksecurefunc( CampaignQuestObjectiveTracker, "UpdateSingle", func )
		hooksecurefunc( QuestObjectiveTracker, "UpdateSingle", func )
		hooksecurefunc( AchievementObjectiveTracker, "AddAchievement", function( self, achievementID, achievementName, description )
			if not achievements[achievementID] then return end
			local blocks = self.usedBlocks and self.usedBlocks[self.blockTemplate] and self.usedBlocks[self.blockTemplate][achievementID]
			if not blocks then return end
			achievementName = predacify(achievementName)
			blocks:SetHeader(achievementName)
			local numCriteria = GetAchievementNumCriteria(achievementID)
			--debug("numCriteria",numCriteria)
			for i = 1, numCriteria do
				local line = blocks and blocks:GetLine(i) and blocks:GetLine(i).Text
				if not line then debug(i,"no line",blocks) end
				--debug(i,"line text",line:GetText())
				if line and line:GetText() and line:GetText():find(OLD) then
					line:SetText( predacify( line:GetText() ) )
				end
			end
		end)
	end,
	-- Flight Map
	["Blizzard_FlightMap"] = function()
		if FlightMapFrame then
			findDataProvider(FlightMapFrame) -- SHOULD already exist
		end
		if not FlightMapFrame or not zoneLabelFound then
			hooksecurefunc( FlightMapFrame or FlightMapMixin, "AddStandardDataProviders", findDataProvider ) -- ...but if not...
		end
	end,
	-- Scouting Map in the Sanctum of Light
	["Blizzard_AdventureMap"] = function()
		-- offered quest
		hooksecurefunc( AdventureMapQuestChoiceDialog, "RefreshDetails", function(self)
			if not quests[self.questID] then return end
			local questTitle, descriptionText, objectiveText = C_AdventureMap.GetQuestInfo(self.questID)
			if not descriptionText then return end
			self.Details.Child.TitleHeader:SetText(predacify(questTitle))
			self.Details.Child.DescriptionText:SetText(predacify(descriptionText))
		end)
	end,
	---- pin on the Adventure Map
	["Blizzard_GarrisonUI"] = function()
		for dataProvider in pairs(CovenantMissionFrame.MapTab.dataProviders) do
			if dataProvider.AddQuest and dataProvider.AddChoicePin then
				hooksecurefunc( dataProvider, "AddChoicePin", function( self, questID, textureKit, name, zoneDescription )
					if not quests[questID] then return end
					local pin = self.pinsByQuestID[questID]
					pin.Text:SetText(predacify(name))
					pin.zoneDescription = predacify(zoneDescription)
				end)
			end
		end
	end,
	-- profession window 
	["Blizzard_ProfessionsTemplates"] = function()
		hooksecurefunc( ProfessionsRecipeListRecipeMixin, "Init", function( self, node )
			local elementData = node:GetData()
			if not elementData or not elementData.recipeInfo or not spells[elementData.recipeInfo.recipeID] or not elementData.recipeInfo.name:find(OLD) then return end
			self.Label:SetText( predacify(elementData.recipeInfo.name) )
		end)
		hooksecurefunc( ProfessionsRecipeSchematicFormMixin, "UpdateOutputItem", function( self )
			if not self.recipeSchematic or not spells[self.recipeSchematic.recipeID] then return end
			if self.recipeSchematic.name:find(OLD) then
				self.OutputText:SetText( predacify(self.recipeSchematic.name) )
			end
		end)
	end,
}

-- addon stuff is not guaranteed to be available on login
local frame = CreateFrame( "FRAME" )
frame:SetScript( "OnEvent", function( self, _, addon )
	if addonHooks[addon] then
		addonHooks[addon]()
	end
end)

local notFound
for addon, func in pairs(addonHooks) do
	local loaded, finished = C_AddOns.IsAddOnLoaded(addon)
	if finished then
		func()
	else
		notFound = true
	end
end
if notFound then
	frame:RegisterEvent( "ADDON_LOADED" )
end
