--## Author: Nevcairiel ## Version: 11.2.0.2
local Quester = LibStub("AceAddon-3.0"):NewAddon("Quester", "AceHook-3.0", "AceConsole-3.0", "LibSink-2.0")
local QuesterL = {}

if GetLocale() == "zhCN" then
QuesterL["Allows easy identification of daily/weekly quests, as well as raid and dungeon quests."] = "可轻松识别每日/每周任务，以及突袭和地下城任务。"
QuesterL["Always show objective percentage values on progress bars"] = "始终在进度条上显示客观百分比值"
QuesterL["Behavior Configuration"] = "性能配置"
QuesterL["Configure the sounds you want to hear with the toggles below."] = "配置你想听到下面的切换的声音"
QuesterL["Difficulty Coloring"] = "难度着色"
QuesterL["Enable the coloring of quests according to their difficulty in NPC and Item tooltips."] = "允许在NPC及物品提示处对任务难度进行着色"
QuesterL["Enable the coloring of quests according to their difficulty in the quest tracker."] = "允许在任务追踪器中对任务难度进行着色"
QuesterL["Enable the coloring of quests according to their difficulty on NPC Gossip frames."] = "允许在NPC闲聊对话框中对任务难度进行着色"
QuesterL["Gossip frames"] = "闲聊框体"
QuesterL["Highlight most valuable reward"] = "高亮最有价值的奖励"
QuesterL["Highlight the reward with the highest vendor value when completing a quest."] = "当完成任务时，将卖店价最高的物品高亮显示。"
QuesterL["Instead of 2/8, show 6"] = "而不是2/8，显示6"
QuesterL["Job's Done!"] = "工作完成"
QuesterL["Lock the Objective Tracker in place"] = "将目标跟踪器锁定到位"
QuesterL["More Work?!"] = "更多工作？"
QuesterL["Note: Changing this option may require your UI to be reloaded to take full effect."] = "注意：更改此项设定可能需要重载用户界面才能生效"
QuesterL["Only show number of objective items remaining"] = "仅显示剩余的目标项数"
QuesterL["Peasant"] = "农夫"
QuesterL["Peon"] = "苦工"
QuesterL["Progress Output"] = "进程输出"
QuesterL["Quest Tracker"] = "任务追踪器"
QuesterL["Remove numbers from single task objectives"] = "从单个任务目标中删除数字"
QuesterL["Reset Position"] = "重置位置"
QuesterL["Reset the position of the Objective Tracker to the default."] = "将“目标跟踪器”的位置重置为默认值。"
QuesterL["Select the set of sounds to use."] = "选择使用的语音组"
QuesterL["Show Quest Level"] = "显示任务等级"
QuesterL["Show Quest Tag Icons in the Objective Tracker"] = "在目标跟踪器中显示任务标记图标"
QuesterL["Sound Configuration"] = "声音配置"
QuesterL["Sound Set"] = "语音组"
QuesterL["Toggle if completing a quest should automatically remove it from the tracker."] = "切换当完成某项任务时是否自动从跟踪列表中移除"
QuesterL["Toggle if quest levels are shown in various parts of the UI."] = "切换是否在多个界面位置显示任务等级"
QuesterL["Toggle playing the 'Job's Done!' sound after completing a quest."] = "切换当完成某项任务时是否播放“工作完成”语音"
QuesterL["Toggle playing the 'More Work?!' sound after completing an objective."] = "切换当完成某项任务目标时是否播放“更多工作”语音"
QuesterL["Toggling this option may require a UI reload to fully take effect."] = "切换此选项可能需要UI重新加载才能完全生效。"
QuesterL["Tooltips"] = "提示"
QuesterL["Unlock Quest Tracker position"] = "解锁Quest Tracker位置"
QuesterL["Unlock the position of the Objective Tracker, allowing it to be moved by clicking and dragging its header."] = "解锁目标跟踪器的位置，允许通过单击并拖动其标题来移动它。"
QuesterL["Un-track complete quests"] = "不追踪已完成任务"
QuesterL["You can select where you want progress messages displayed using the options below."] = "你可以在下面的列表中选择任务进程报告的输出位置。"
QuesterL["You can unlock it again in the options"] = "您可以在选项中再次解锁"
QuesterL["QuesterTitle"] = "|cffFF6A00[任务]|r功能增强";
elseif GetLocale() == "zhTW" then
QuesterL["Allows easy identification of daily/weekly quests, as well as raid and dungeon quests."] = "可以輕鬆識別每日/每週任務，以及團隊和地下城任務。"
QuesterL["Always show objective percentage values on progress bars"] = "始終顯示目標進度條的百分比"
QuesterL["Behavior Configuration"] = "行為設定"
QuesterL["Configure the sounds you want to hear with the toggles below."] = "在下面切換你想聽到的聲音。"
QuesterL["Difficulty Coloring"] = "難度著色"
QuesterL["Enable the coloring of quests according to their difficulty in NPC and Item tooltips."] = "根據任務難度對NPC與物品提示啟用著色。"
QuesterL["Enable the coloring of quests according to their difficulty in the quest tracker."] = "根據任務難度對任務追蹤器啟用著色。"
QuesterL["Enable the coloring of quests according to their difficulty on NPC Gossip frames."] = "根據任務難度對NPC對話框架啟用著色。"
QuesterL["Gossip frames"] = "對話框架"
QuesterL["Highlight most valuable reward"] = "高亮最有價值的獎賞"
QuesterL["Highlight the reward with the highest vendor value when completing a quest."] = "當完成一個任務時，高亮顯示販售價值最高的獎賞。"
QuesterL["Instead of 2/8, show 6"] = "顯示6來取代2/8"
QuesterL["Job's Done!"] = "工作完成！"
QuesterL["Lock the Objective Tracker in place"] = "鎖定任務追蹤器位置"
QuesterL["More Work?!"] = "更多工作？"
QuesterL["Note: Changing this option may require your UI to be reloaded to take full effect."] = "注意：更改此選項可能需要重新加載您的插件才能完全發揮效用。"
QuesterL["Only show number of objective items remaining"] = "僅顯示剩餘的目標物品數"
QuesterL["Peasant"] = "農夫"
QuesterL["Peon"] = "苦工"
QuesterL["Progress Output"] = "進度輸出"
QuesterL["Quest Tracker"] = "任務追蹤器"
QuesterL["Remove numbers from single task objectives"] = "從單個任務目標中刪除數字"
QuesterL["Reset Position"] = "重置位置"
QuesterL["Reset the position of the Objective Tracker to the default."] = "重置任務追蹤器為預設位置。"
QuesterL["Select the set of sounds to use."] = "選擇要使用的音效設置。"
QuesterL["Show Quest Level"] = "顯示任務等級"
QuesterL["Show Quest Tag Icons in the Objective Tracker"] = "在目標追蹤器中顯示任務標籤"
QuesterL["Sound Configuration"] = "聲音設定"
QuesterL["Sound Set"] = "音效設置"
QuesterL["Toggle if completing a quest should automatically remove it from the tracker."] = "切換是否在任務完成時自動自追蹤器移除。"
QuesterL["Toggle if quest levels are shown in various parts of the UI."] = "切換是否在用戶介面各處示任務等級。"
QuesterL["Toggle playing the 'Job's Done!' sound after completing a quest."] = "切換是否在完成任務後播放「工作完成！」音效。"
QuesterL["Toggle playing the 'More Work?!' sound after completing an objective."] = "切換是否在完成任務目標後播放「更多工作？」音效。"
QuesterL["Toggling this option may require a UI reload to fully take effect."] = "切換此選項可能需要重新載入UI方能完全生效。"
QuesterL["Tooltips"] = "工具提示"
QuesterL["Unlock Quest Tracker position"] = "解鎖任務追蹤器位置"
QuesterL["Unlock the position of the Objective Tracker, allowing it to be moved by clicking and dragging its header."] = "解鎖任務追蹤器的位置，使其能夠透過拖曳標題而被移動。"
QuesterL["Un-track complete quests"] = "不追蹤已完成任務"
QuesterL["You can select where you want progress messages displayed using the options below."] = "您可以使用以下的選項選擇您要顯示進度訊息的地方。"
QuesterL["You can unlock it again in the options"] = "您可以在選項內再次解鎖"
QuesterL["QuesterTitle"] = "|cffFF6A00[任务]|r功能增强";
else
QuesterL["Allows easy identification of daily/weekly quests, as well as raid and dungeon quests."] = true
QuesterL["Always show objective percentage values on progress bars"] = true
QuesterL["Behavior Configuration"] = true
QuesterL["Configure the sounds you want to hear with the toggles below."] = true
QuesterL["Difficulty Coloring"] = true
QuesterL["Enable the coloring of quests according to their difficulty in NPC and Item tooltips."] = true
QuesterL["Enable the coloring of quests according to their difficulty in the quest tracker."] = true
QuesterL["Enable the coloring of quests according to their difficulty on NPC Gossip frames."] = true
QuesterL["Gossip frames"] = true
QuesterL["Highlight most valuable reward"] = true
QuesterL["Highlight the reward with the highest vendor value when completing a quest."] = true
QuesterL["Instead of 2/8, show 6"] = true
QuesterL["Job's Done!"] = true
QuesterL["Lock the Objective Tracker in place"] = true
QuesterL["More Work?!"] = true
QuesterL["Note: Changing this option may require your UI to be reloaded to take full effect."] = true
QuesterL["Only show number of objective items remaining"] = true
QuesterL["Peasant"] = true
QuesterL["Peon"] = true
QuesterL["Progress Output"] = true
QuesterL["Quest Tracker"] = true
QuesterL["Remove numbers from single task objectives"] = true
QuesterL["Reset Position"] = true
QuesterL["Reset the position of the Objective Tracker to the default."] = true
QuesterL["Select the set of sounds to use."] = true
QuesterL["Show Quest Level"] = true
QuesterL["Show Quest Tag Icons in the Objective Tracker"] = true
QuesterL["Sound Configuration"] = true
QuesterL["Sound Set"] = true
QuesterL["Toggle if completing a quest should automatically remove it from the tracker."] = true
QuesterL["Toggle if quest levels are shown in various parts of the UI."] = true
QuesterL["Toggle playing the 'Job's Done!' sound after completing a quest."] = true
QuesterL["Toggle playing the 'More Work?!' sound after completing an objective."] = true
QuesterL["Toggling this option may require a UI reload to fully take effect."] = true
QuesterL["Tooltips"] = true
QuesterL["Unlock Quest Tracker position"] = true
QuesterL["Unlock the position of the Objective Tracker, allowing it to be moved by clicking and dragging its header."] = true
QuesterL["Un-track complete quests"] = true
QuesterL["You can select where you want progress messages displayed using the options below."] = true
QuesterL["You can unlock it again in the options"] = true
QuesterL["QuesterTitle"] = "|cffFF6A00[Quest]|rQuester";
end

local db
local defaults = {
	profile = {
		-- options
		questLevels = true,
		removeComplete = false,
		highlightReward = true,
		showObjectivePercentages = false,
		hide01 = false,
		shortenNumbers = false,
		showTagIcons = true,

		-- coloring
		gossipColor = true,
		questTrackerColor = true,
		tooltipColor = true,

		-- position
		pos = {
			x = nil,
			y = nil,
		},

		-- sounds
		soundSet = 1,
		morework = true,
		jobsdone = true,

		-- sink
		sinkOptions = {
			sink20OutputSink = "UIErrorsFrame",
		},
	}
}

-- "Deformat" the pattern to find their argument order
local MatchObject, MatchMonster, MatchPlayer, MatchFaction, MatchProgressBar, MatchErrObject, MatchErrFound, MatchErrKill, MatchErrCompleted
do
	local function GetPermute3(pattern)
		local one, two, three = pattern:match("%%(%d)%$.+%%(%d)%$.+%%(%d)%$")
		if one and two and three then
			return ("return function(r%d, r%d, r%d) return r1, r2, r3 end"):format(one, two, three)
		end
		return "return function(...) return ... end"
	end

	local function GetMatcher(pattern)
		local permuteFn = loadstring(GetPermute3(pattern))()
		local pattern_opt = OPTIONAL_QUEST_OBJECTIVE_DESCRIPTION:format(pattern)
		local match_pattern = "^" .. pattern:gsub("%(","%%("):gsub("%)", "%%)"):gsub("(%%%d?$?d)", "(.-)"):gsub("(%%%d?$?[^()])", "(.+)") .. "$"
		local match_pattern_opt = "^" .. pattern_opt:gsub("%(","%%("):gsub("%)", "%%)"):gsub("(%%%d?$?d)", "(.-)"):gsub("(%%%d?$?[^()])", "(.+)") .. "$"
		return function(text) local a,b,c = permuteFn(text:match(match_pattern_opt)) if not a then a,b,c = permuteFn(text:match(match_pattern)) end return a,b,c end
	end

	local function GetMatcherNonGreedy(pattern, greedyComponent)
		local permuteFn = loadstring(GetPermute3(pattern))()
		local pattern_opt = OPTIONAL_QUEST_OBJECTIVE_DESCRIPTION:format(pattern)
		local match_pattern = "^" .. pattern:gsub("%(","%%("):gsub("%)", "%%)"):gsub("(%%%d?$?d)", "(.-)"):gsub(("(%%%%%d$[^()])"):format(greedyComponent), "(.+)"):gsub("(%%%d?$?[^()])", "(.-)") .. "$"
		local match_pattern_opt = "^" .. pattern_opt:gsub("%(","%%("):gsub("%)", "%%)"):gsub("(%%%d?$?d)", "(.-)"):gsub(("(%%%%%d$[^()])"):format(greedyComponent), "(.+)"):gsub("(%%%d?$?[^()])", "(.-)") .. "$"
		return function(text) local a,b,c = permuteFn(text:match(match_pattern_opt)) if not a then a,b,c = permuteFn(text:match(match_pattern)) end return a,b,c end
	end

	MatchObject = GetMatcher(QUEST_OBJECTS_FOUND)
	MatchMonster = GetMatcher(QUEST_MONSTERS_KILLED)
	MatchPlayer = GetMatcher(QUEST_PLAYERS_KILLED)
	MatchFaction = GetMatcherNonGreedy(QUEST_FACTION_NEEDED, 1)
	MatchProgressBar = GetMatcher(QUEST_PROGRESS_TOOLTIP_OBJECTIVE_PROGRESS)

	MatchErrObject = GetMatcher(ERR_QUEST_ADD_ITEM_SII)
	MatchErrFound = GetMatcher(ERR_QUEST_ADD_FOUND_SII)
	MatchErrKill = GetMatcher(ERR_QUEST_ADD_KILL_SII)
	MatchErrCompleted = GetMatcher(ERR_QUEST_OBJECTIVE_COMPLETE_S)
end

-- utility functions
local function ColorGradient(perc, ...)
	if perc >= 1 then
		local r, g, b = select(select("#", ...) - 2, ...)
		return r, g, b
	elseif perc <= 0 then
		local r, g, b = ...
		return r, g, b
	end

	local num = select("#", ...) / 3

	local segment, relperc = math.modf(perc*(num-1))
	local r1, g1, b1, r2, g2, b2 = select((segment*3)+1, ...)

	return r1 + (r2-r1)*relperc, g1 + (g2-g1)*relperc, b1 + (b2-b1)*relperc
end

local function rgb2hex(r, g, b)
	if type(r) == "table" then
		g = r.g
		b = r.b
		r = r.r
	end
	return format("%02x%02x%02x", r*255, g*255, b*255)
end

local tags = {
	DAILY = "\226\128\162",
	GROUP = "-队伍",  --+
	SCENARIO = "-战役",  --s
	DUNGEON = "-副本",  --d
	HEROIC_DUNGEON = "-英雄本",  --d+
	RAID = "-团队"   --r
}

local function GetQuestTag(groupSize, frequency, tagInfo)
	local tag = ""
	if frequency == Enum.QuestFrequency.Daily or frequency == Enum.QuestFrequency.Weekly then
		tag = tags.DAILY
	end
	if tagInfo then
		if tagInfo.tagID == Enum.QuestTag.Group then
			tag = tag .. tags.GROUP
		elseif tagInfo.tagID == Enum.QuestTag.Scenario then
			tag = tag .. tags.SCENARIO
		elseif tagInfo.tagID == Enum.QuestTag.Dungeon then
			tag = tag .. tags.DUNGEON
		elseif tagInfo.tagID == Enum.QuestTag.Heroic then
			tag = tag .. tags.HEROIC_DUNGEON
		elseif QUEST_TAG_DUNGEON_TYPES[tagInfo.tagID] then
			tag = tag .. tags.RAID
		end
	end
	return tag
end

local function GetQuestColorString(level, scaling, questID)
	return rgb2hex(GetQuestDifficultyColor(level, scaling, questID))
end

local function GetTaggedTitle(i, color, tag)
	if not i or i == 0 then return nil end
	local info = C_QuestLog.GetInfo(i)
	if not info then return end

	local title = info.title
	if not info.isHeader and title and info.questID then
		local tagString = tag and GetQuestTag(info.suggestedGroup, info.frequency, C_QuestLog.GetQuestTagInfo(info.questID)) or ""
		if color then
			if db.questLevels then
				title = format("|cff%s[%s%s] %s|r", GetQuestColorString(info.level, info.isScaling, info.questID), info.level, tagString, title)
			else
				title = format("|cff%s%s|r", GetQuestColorString(info.level, info.isScaling, info.questID), title)
			end
		elseif db.questLevels then
			title = format("[%s%s] %s", info.level, tagString, title)
		end
	end
	return title
end

local function GetChatTaggedTitle(i)
	if not i or i == 0 then return nil end
	local info = C_QuestLog.GetInfo(i)
	if not info or info.isHeader or not info.title then return end
	return format("(%s%s) %s", info.level, GetQuestTag(info.suggestedGroup, info.frequency), info.title)
end

local function GetQuestTagAtlas(i)
	if not i or i == 0 then return nil end
	local info = C_QuestLog.GetInfo(i)
	if not info or not info.questID then return end

	local tagID
	local tagInfo = C_QuestLog.GetQuestTagInfo(info.questID)
	local isComplete = C_QuestLog.IsComplete(info.questID)
	if tagInfo and tagInfo.tagID == Enum.QuestTag.Account then
		local factionGroup = GetQuestFactionGroup(info.questID)
		if factionGroup then
			tagID = "ALLIANCE"
			if factionGroup == LE_QUEST_FACTION_HORDE then
				tagID = "HORDE"
			end
		else
			tagID = tagInfo.tagID
		end
	elseif info.frequency == Enum.QuestFrequency.Daily and not isComplete then
		tagID = "DAILY"
	elseif info.frequency == Enum.QuestFrequency.Weekly and not isComplete then
		tagID = "WEEKLY"
	elseif tagInfo and tagInfo.tagID then
		tagID = tagInfo.tagID
	elseif C_CampaignInfo.IsCampaignQuest(info.questID) then
		local faction = UnitFactionGroup("player")
		tagID = faction == "Horde" and "HORDE" or "ALLIANCE"
	end

	if tagID and QUEST_TAG_ATLAS[tagID] then
		return QUEST_TAG_ATLAS[tagID]
	end

	return nil
end

-- faction data for reputation quests
local factionLabels = {}
do
	local gender = UnitSex("player")
	for i=1, #FACTION_BAR_COLORS do
		local faction = GetText("FACTION_STANDING_LABEL"..i, gender)
		factionLabels[faction] = i
	end
end

-- data cache
local items, mobs, progress = {}, {}, {}
local table_cache = {}
local complete, oldcomplete = {}, {}
local quests, oldquests = {}, {}

local function getOptionsTable()
	local options = {
		type = "group",
		name = QuesterL["QuesterTitle"],
		get = function(k) return db[k.arg] end,
		set = function(k, v) db[k.arg] = v end,
		args = {
			behaviorheader = {
				type = "header",
				name = QuesterL["Behavior Configuration"],
				order = 1,
			},
			questLevel = {
				name = QuesterL["Show Quest Level"],
				desc = QuesterL["Toggle if quest levels are shown in various parts of the UI."] .. "\n" .. QuesterL["Note: Changing this option may require your UI to be reloaded to take full effect."],
				type = "toggle",
				arg = "questLevels",
				order = 2,
				width = "full",
			},
			removeComplete = {
				name = QuesterL["Un-track complete quests"],
				desc = QuesterL["Toggle if completing a quest should automatically remove it from the tracker."],
				type = "toggle",
				arg = "removeComplete",
				order = 3,
				width = "full",
			},
			highlightReward = {
				name = QuesterL["Highlight most valuable reward"],
				desc = QuesterL["Highlight the reward with the highest vendor value when completing a quest."],
				type = "toggle",
				arg = "highlightReward",
				order = 4,
				width = "full",
			},
			showObjectivePercentages = {
				name = QuesterL["Always show objective percentage values on progress bars"],
				desc = QuesterL["Toggling this option may require a UI reload to fully take effect."],
				type = "toggle",
				arg = "showObjectivePercentages",
				order = 4.5,
				width = "full",
			},
			hide01 = {
				name = QuesterL["Remove numbers from single task objectives"],
				type = "toggle",
				arg = "hide01",
				order = 4.6,
				width = "full",
			},
			shortenNumbers = {
				name = QuesterL["Only show number of objective items remaining"],
				desc = QuesterL["Instead of 2/8, show 6"],
				type = "toggle",
				arg = "shortenNumbers",
				order = 4.6,
				width = "full",
			},
			showTagIcons = {
				name = QuesterL["Show Quest Tag Icons in the Objective Tracker"],
				desc = QuesterL["Allows easy identification of daily/weekly quests, as well as raid and dungeon quests."],
				type = "toggle",
				arg = "showTagIcons",
				order = 4.7,
				width = "full",
			},
			colorheader = {
				type = "header",
				name = QuesterL["Difficulty Coloring"],
				order = 5,
			},
			gossipColor = {
				name = QuesterL["Gossip frames"],
				desc = QuesterL["Enable the coloring of quests according to their difficulty on NPC Gossip frames."],
				type = "toggle",
				arg = "gossipColor",
				order = 6,
				width = "double",
			},
			questTrackerColor = {
				name = QuesterL["Quest Tracker"],
				desc = QuesterL["Enable the coloring of quests according to their difficulty in the quest tracker."],
				type = "toggle",
				arg = "questTrackerColor",
				order = 7,
				width = "double",
			},
			tooltipColor = {
				name = QuesterL["Tooltips"],
				desc = QuesterL["Enable the coloring of quests according to their difficulty in NPC and Item tooltips."],
				type = "toggle",
				arg = "tooltipColor",
				order = 8,
				width = "double",
			},
			soundheader = {
				type = "header",
				name = QuesterL["Sound Configuration"],
				order = 10,
			},
			sounddesc = {
				type = "description",
				name = QuesterL["Configure the sounds you want to hear with the toggles below."],
				order = 11,
			},
			soundSet = {
				type = "select",
				name = QuesterL["Sound Set"],
				desc = QuesterL["Select the set of sounds to use."],
				arg = "soundSet",
				values = { QuesterL["Peasant"], QuesterL["Peon"] },
				order = 12,
			},
			sound_nl = {
				type = "description",
				name = "",
				order = 13,
			},
			morework = {
				name = QuesterL["More Work?!"],
				desc = QuesterL["Toggle playing the 'More Work?!' sound after completing an objective."],
				type = "toggle",
				arg = "morework",
				order = 15,
			},
			jobsdone = {
				name = QuesterL["Job's Done!"],
				desc = QuesterL["Toggle playing the 'Job's Done!' sound after completing a quest."],
				type = "toggle",
				arg = "jobsdone",
				order = 16,
			},
			header = {
				type = "header",
				name = QuesterL["Progress Output"],
				order = 20,
			},
			desc = {
				type = "description",
				name = QuesterL["You can select where you want progress messages displayed using the options below."],
				order = 21,
			},
			sink = Quester:GetSinkAce3OptionsDataTable(),
		}
	}

	-- hack sink options into submission
	options.args.sink.order = 22
	options.args.sink.inline = true
	options.args.sink.name = ""
	return options
end

local QUESTER_SOUND_MORE_WORK = 1
local QUESTER_SOUND_JOBS_DONE = 2

-- Sounds used. SoundKit IDs where available, file paths otherwise
local sounds = {
	[1] = {
		6288, -- "Sound\\Creature\\Peasant\\PeasantWhat3.ogg"
		"Interface\\AddOns\\OrzUI\\Media\\Sounds\\jobsdone.ogg"
	},
	[2] = {
		6197, -- "Sound\\Creature\\Peon\\PeonYes3.ogg"
		6199, -- "Sound\\Creature\\Peon\\PeonBuildingComplete1.ogg"
	}
}
local function PlayQuestSound(index)
	local soundSet = db.soundSet
	if not soundSet or not sounds[soundSet] then soundSet = 1 end
	local sound = sounds[soundSet][index]
	if type(sound) == "string" then
		PlaySoundFile(sound)
	elseif type(sound) == "number" then
		PlaySound(sound)
	end
end

local first, blockQuestUpdate = true, true
function Quester:OnInitialize()
	self.db = LibStub("AceDB-3.0"):New("QuesterDB", defaults, true)
	db = self.db.profile

	self:SetSinkStorage(self.db.profile.sinkOptions)

	LibStub("AceConfigRegistry-3.0"):RegisterOptionsTable(QuesterL["QuesterTitle"], getOptionsTable)
	local _, optionsCategory = LibStub("AceConfigDialog-3.0"):AddToBlizOptions(QuesterL["QuesterTitle"], QuesterL["QuesterTitle"])

	self:RegisterChatCommand("quester", function() Settings.OpenToCategory(optionsCategory) end)

	self.eventFrame = CreateFrame("Frame", "QuesterEventFrame")
	self.eventFrame:SetScript("OnEvent", function(frame, event, ...) Quester:HandleEvent(event, ...) end)

	TooltipDataProcessor.AddLinePostCall(Enum.TooltipDataLineType.QuestTitle, self.TooltipLineProcessorQuestTitle)
	TooltipDataProcessor.AddTooltipPostCall(Enum.TooltipDataType.Item, self.TooltipProcessorItem)
end

function Quester:RegisterEvent(event)
	assert(self[event], "Event Handler missing for event " .. event)
	self.eventFrame:RegisterEvent(event)
end

function Quester:HandleEvent(event, ...)
	if event and self[event] then
		self[event](self, ...)
	end
end

function Quester:OnEnable()
	self:RegisterEvent("QUEST_LOG_UPDATE")
	self:RegisterEvent("UNIT_QUEST_LOG_CHANGED")
	self:RegisterEvent("QUEST_WATCH_LIST_CHANGED")

	self:RegisterEvent("PLAYER_LEAVING_WORLD")
	self:RegisterEvent("PLAYER_ENTERING_WORLD")

	self:RegisterEvent("GOSSIP_SHOW")
	self:RegisterEvent("QUEST_GREETING")
	self:RegisterEvent("QUEST_COMPLETE")

	self:RawHookScript(UIErrorsFrame, "OnEvent", "UIErrorsFrame_OnEvent", true)

	self:SecureHook(QuestObjectiveTracker, "GetBlock", "QuestTrackerGetBlock")
	self:SecureHook(QuestObjectiveTracker, "OnFreeBlock", "QuestTrackerOnFreeBlock")
	self:SecureHook(BonusObjectiveTracker, "GetBlock", "QuestTrackerGetBlock")
	self:SecureHook(BonusObjectiveTracker, "OnFreeBlock", "QuestTrackerOnFreeBlock")
	self:SecureHook(CampaignQuestObjectiveTracker, "GetBlock", "QuestTrackerGetBlock")
	self:SecureHook(CampaignQuestObjectiveTracker, "OnFreeBlock", "QuestTrackerOnFreeBlock")
	self:SecureHook(WorldQuestObjectiveTracker, "GetBlock", "QuestTrackerGetBlock")
	self:SecureHook(WorldQuestObjectiveTracker, "OnFreeBlock", "QuestTrackerOnFreeBlock")
	self:SecureHook("QuestLogQuests_Update")

	self:SetupChatFilter()

	if QuestFrameRewardPanel:IsVisible() then
		self:QUEST_COMPLETE()
	end
end

function Quester:OnDisable()
	self.eventFrame:UnregisterAllEvents()
end

function Quester:UIErrorsFrame_OnEvent(frame, event, ...)
	if event == "UI_INFO_MESSAGE" then
		local category, message = ...
		local name, numItems, numNeeded = MatchErrObject(message)
		if not name then
			name, numItems, numNeeded = MatchErrKill(message)
		end
		if not name then
			name, numItems, numNeeded = MatchErrFound(message)
		end
		if not name then
			name = MatchErrCompleted(message)
			if name then
				numItems = 1
				numNeeded = 1
			end
		end
		if not name then
			if message == ERR_QUEST_UNKNOWN_COMPLETE then
				name = message
				numItems = 1
				numNeeded = 1
			end
		end
		if name then
			numItems, numNeeded = tonumber(numItems), tonumber(numNeeded)
			if numItems and numNeeded then
				local perc = numItems / numNeeded
				self:Pour(message, ColorGradient(perc, 1,0,0, 1,1,0, 0,1,0))
				return
			else
				--[==[@debug@
				self:Print("Unable to parse objectives from message: " .. message)
				--@end-debug@]==]
			end
		end
	end
	return self.hooks[frame].OnEvent(frame, event, ...)
end

local function getTable()
	local t = next(table_cache)
	if t then
		table_cache[t] = nil
	else t = {} end
	return t
end

local function emptyAll()
	wipe(items)
	wipe(oldcomplete)
	wipe(oldquests)
	complete, oldcomplete = oldcomplete, complete
	quests, oldquests = oldquests, quests

	for k, v in pairs(progress) do
		if type(v) == "table" then
			wipe(v)
			table_cache[v] = true
		end
		progress[k] = nil
	end
	for k, v in pairs(mobs) do
		if type(v) == "table" then
			wipe(v)
			table_cache[v] = true
		end
		mobs[k] = nil
	end
end

function Quester:UNIT_QUEST_LOG_CHANGED(unit, ...)
	if unit == "player" then
		self:QUEST_LOG_UPDATE()
	end
end

function Quester:QUEST_WATCH_LIST_CHANGED()
	self:QUEST_LOG_UPDATE()
end

function Quester:PLAYER_LEAVING_WORLD()
	blockQuestUpdate = true
end

function Quester:PLAYER_ENTERING_WORLD()
	blockQuestUpdate = nil
	self:QUEST_LOG_UPDATE()
end

local function processObjective(questID, questTitle, isTask, objIndex, info)
	local itemDesc, numItems, numNeeded, objKey
	if info and info.text then
		if info.type == "item" or info.type == "object" then
			itemDesc, numItems, numNeeded = MatchObject(info.text)
			if itemDesc then
				if tonumber(numNeeded) and tonumber(numItems) and tonumber(numItems) > tonumber(numNeeded) then
					objKey = info.text:gsub(numItems, numNeeded)
				end
				items[itemDesc] = info.text -- used for tooltips
			else
				numItems, numNeeded = (info.finished and 1 or 0), 1
			end
		elseif info.type == "monster" then
			itemDesc, numItems, numNeeded = MatchMonster(info.text)
			if itemDesc == nil or numItems == nil or numNeeded == nil then
				--Sometimes we get objectives like "Find Mankrik's Wife: 0/1", which are listed as "monster".
				itemDesc, numItems, numNeeded = MatchObject(info.text)
			end
			if itemDesc then
				if mobs[itemDesc] then
					if type(mobs[itemDesc]) == "string" then
						local s = mobs[itemDesc]
						mobs[itemDesc] = getTable()
						tinsert(mobs[itemDesc], s)
					end
					tinsert(mobs[itemDesc], info.text)
				else
					mobs[itemDesc] = info.text
				end
			end
		elseif info.type == "player" then
			numItems, numNeeded, itemDesc = MatchPlayer(info.text)

			-- it is unknown if some quests marked as "player" use the Monster syntax,
			-- but attempt to parse if it failed above
			if itemDesc == nil or numItems == nil or numNeeded == nil then
				itemDesc, numItems, numNeeded = MatchMonster(info.text)
			end
		elseif info.type == "reputation" then
			itemDesc, numItems, numNeeded = MatchFaction(info.text)
			numItems, numNeeded = factionLabels[numItems], factionLabels[numNeeded]
		elseif info.type == "progressbar" then
			itemDesc, numNeeded, numItems = MatchProgressBar(info.text)
			if numItems then numItems = numItems:match("%d+") end
			numNeeded = 100
			--[==[@debug@
				if not (itemDesc and numItems) then
					print("Quester: unable to parse progress bar: " .. info.text)
				end
			--@end-debug@]==]
		elseif info.type == "event" or info.type == "log" or info.type == "spell" or info.type == "progressbar" then
			itemDesc, numNeeded, numItems = info.text, 1, (info.finished and 1 or 0)
		else
			--[==[@debug@
			if info.type and info.type ~= "" then
				print("Unknown quest objective type: " .. info.type .. ", on quest: " .. questTitle .. ", objective: " .. info.text)
			end
			--@end-debug@]==]
			return
		end
		numNeeded, numItems = tonumber(numNeeded), tonumber(numItems)
		--[==[@debug@
		if (numItems ~= info.numFulfilled or numNeeded ~= info.numRequired) and not (info.type == "object" and info.numFulfilled == 1 and info.numRequired == 1 and not info.finished) and (info.type ~= "progressbar") then
			print("Quester: mismatching parsed and provided data on quest: " .. questTitle .. " (ID: " .. questID .. "), Objective: " .. info.text .. ", Type: " .. info.type .. ", Parsed: " .. tostring(numItems) .. "/" .. tostring(numNeeded) .. ", provided: " .. info.numFulfilled .. "/" .. info.numRequired)
		end
		--@end-debug@]==]
		if numNeeded and numNeeded > 0 and numItems then
			local key = info.text
			if not progress[key] then
				progress[key] = getTable()
			end
			progress[key].q = questTitle
			progress[key].qid = questID
			progress[key].lid = objIndex
			progress[key].i = numItems
			progress[key].n = numNeeded
			progress[key].perc = numItems / numNeeded
			progress[key].done = info.finished

			-- alias for progressbar handling
			if info.type == "progressbar" and itemDesc then progress[itemDesc] = progress[key] end

			local c = objKey or (questTitle .. info.text)
			if info.finished then
				complete[c] = true
			end
			if not first and not complete[questTitle] and info.finished and not oldcomplete[c] and (not isTask or oldquests[questTitle]) then
				if db.morework then
					PlayQuestSound(QUESTER_SOUND_MORE_WORK)
				end
			end
		end
	end
end

function Quester:QUEST_LOG_UPDATE()
	-- check if updates are disabled (ie. during loading screens)
	if blockQuestUpdate then return end

	-- no re-entry
	blockQuestUpdate = true

	-- clear previous data cache
	emptyAll()

	-- store previous selection, so we can restore it
	local startingQuestLogSelection = C_QuestLog.GetSelectedQuest()

	-- enumerate all quests
	local numEntries, numQuests = C_QuestLog.GetNumQuestLogEntries()
	for index = 1, numEntries do
		-- the quest log is stateful, and some functions require an active entry
		local info = C_QuestLog.GetInfo(index)
		if info and not info.isHeader and not info.isBounty and info.questID and info.questID ~= 0 then
			C_QuestLog.SetSelectedQuest(info.questID)

			local title = info.title
			-- Some other quest addons hook GetQuestLogTitle to add levels to the names.  This is annoying, so strip out the common format for it.
			if title:match("^%[") then title = title:match("^%[[^%]]+%]%s?(.*)") end

			-- store the quest in our lookup table
			quests[title] = info.questID

			local isComplete, isFailed = C_QuestLog.IsComplete(info.questID), C_QuestLog.IsFailed(info.questID)

			-- process objectives
			local numObjectives = C_QuestLog.GetNumQuestObjectives(info.questID)
			if isFailed or isComplete or numObjectives == 0 then
				if not first and not oldcomplete[title] and numObjectives > 0 then
					-- completed the quest
					if isFailed then
						self:Pour(ERR_QUEST_FAILED_S:format(title), 1, 0, 0)
					else
						self:Pour(ERR_QUEST_COMPLETE_S:format(title), 0, 1, 0)
						if db.jobsdone then
							PlayQuestSound(QUESTER_SOUND_JOBS_DONE)
						end
						if db.removeComplete then
							C_QuestLog.RemoveQuestWatch(info.questID)
						end
					end
				end
				complete[title] = true
			end

			-- enumerate all objectives and store them
			local objectives = C_QuestLog.GetQuestObjectives(info.questID)
			for o = 1, numObjectives do
				processObjective(info.questID, title, info.isTask, o, objectives[o])
			end
		end
	end
	if numEntries > 0 then first = nil end

	-- restore previous questlog selection
	C_QuestLog.SetSelectedQuest(startingQuestLogSelection)

	-- process watched world quests
	for i = 1, C_QuestLog.GetNumWorldQuestWatches() do
		local watchedWorldQuestID = C_QuestLog.GetQuestIDForWorldQuestWatchIndex(i)
		local taskName = C_TaskQuest.GetQuestInfoByQuestID(watchedWorldQuestID)

		if taskName then
			quests[taskName] = watchedWorldQuestID
			local numObjectives = C_QuestLog.GetNumQuestObjectives(watchedWorldQuestID)
			local objectives = C_QuestLog.GetQuestObjectives(watchedWorldQuestID)
			for o = 1, numObjectives do
				processObjective(watchedWorldQuestID, taskName, true, o, objectives[o])
			end
		end
	end

	-- update the objective tracker
	self:UpdateObjectiveTracker(QuestObjectiveTracker)
	self:UpdateObjectiveTracker(BonusObjectiveTracker)
	self:UpdateObjectiveTracker(CampaignQuestObjectiveTracker)
	self:UpdateObjectiveTracker(WorldQuestObjectiveTracker)

	-- update any open dialogs
	self:QUEST_GREETING()
	self:GOSSIP_SHOW()

	blockQuestUpdate = nil
end

local function ProcessGossip(button, data)
	local text = data.info.title
	local level = data.info.questLevel or -1
	local tagString = GetQuestTag(data.info.suggestedGroup, data.info.frequency, C_QuestLog.GetQuestTagInfo(data.info.questID)) or ""
	if level <= 0 then
		-- keep the text untouched
	elseif db.gossipColor then
		button:SetText(format("|cff%s[%d%s]|r %s", GetQuestColorString(level), level, tagString, text))
	else
		button:SetText(format("[%d%s] %s", level, tagString, text))
	end
	button:Resize()
end

function Quester:GOSSIP_SHOW()
	if not GossipFrame:IsVisible() or not db.questLevels then return end
	for _, frame in GossipFrame.GreetingPanel.ScrollBox:GetView():EnumerateFrames() do
		local data = frame:GetElementData()
		if data.buttonType == GOSSIP_BUTTON_TYPE_ACTIVE_QUEST or data.buttonType == GOSSIP_BUTTON_TYPE_AVAILABLE_QUEST then
			ProcessGossip(frame, data)
		end
	end
end

function Quester:QUEST_GREETING()
	if not QuestFrameGreetingPanel:IsVisible() or not db.questLevels then return end

	-- Enumerate over all available buttons, and modify them
	for button in QuestFrameGreetingPanel.titleButtonPool:EnumerateActive() do
		local title, level
		if button.isActive == 1 then
			title, level = GetActiveTitle(button:GetID()), GetActiveLevel(button:GetID())
		else
			title, level = GetAvailableTitle(button:GetID()), GetAvailableLevel(button:GetID())
		end
		if level == -1 then
			-- keep the text untouched
		elseif db.gossipColor then
			button:SetText(format("|cff%s[%d]|r %s", GetQuestColorString(level), level, title))
		else
			button:SetText(format("[%d] %s", level, title))
		end
		button:SetHeight(button:GetTextHeight() + 2)
	end
end

function Quester.TooltipLineProcessorQuestTitle(tooltip, lineData)
	local self = Quester
	if tooltip ~= GameTooltip then return end
	if lineData.id then
		local index = C_QuestLog.GetLogIndexForQuestID(lineData.id)
		if index and index > 0 then
			_G["GameTooltipTextLeft" .. lineData.lineIndex]:SetText(GetTaggedTitle(index, db.tooltipColor, true))
		end
	end
end

function Quester.TooltipProcessorItem(tooltip, data)
	local self = Quester
	if tooltip ~= GameTooltip then return end

	local name, link, id = TooltipUtil.GetDisplayedItem(tooltip)
	if name and items[name] then
		local it = items[name]
		if progress[it] then
			local index = C_QuestLog.GetLogIndexForQuestID(progress[it].qid)
			if index and index > 0 then
				tooltip:AddLine(" ")
				tooltip:AddLine(GetTaggedTitle(index, db.tooltipColor, true))
				local text = GetQuestLogLeaderBoard(progress[it].lid, index)
				if text then
					tooltip:AddLine(format(" - |cff%s%s|r", rgb2hex(ColorGradient(progress[it].perc, 1,0,0, 1,1,0, 0,1,0)), text))
				end
			end
		end
	end
end

function Quester:QuestLogQuests_Update()
	for button in QuestScrollFrame.titleFramePool:EnumerateActive() do
		if button and button:IsShown() then
			local text = GetTaggedTitle(button.questLogIndex, false, false)

			local partyMembersOnQuest = 0
			for j=1, GetNumSubgroupMembers() do
				if C_QuestLog.IsUnitOnQuest("party"..j, button.questID) then
					partyMembersOnQuest = partyMembersOnQuest + 1
				end
			end

			if partyMembersOnQuest > 0 then
				text = "["..partyMembersOnQuest.."] "..text
			end

			-- store previous text height, so we can compute the new total height
			local prevTextHeight = button.Text:GetHeight()

			-- update text
			button.Text:SetText(text)

			-- compute new button height, in case text wrapping changed
			local totalHeight = button:GetHeight()
			totalHeight = totalHeight - prevTextHeight + button.Text:GetHeight()
			button:SetHeight(totalHeight)
		end
	end
	QuestScrollFrame.Contents:Layout()
end

function Quester:UpdateObjectiveTracker(tracker)
	for _id, block in pairs(tracker.usedBlocks) do
		if block.used then
			for key, line in pairs(block.lines) do
				self:ObjectiveTracker_AddObjective(block, key, line.Text:GetText(), line.template)
			end
		end
	end
end

function Quester:QuestTrackerHeaderSetText(HeaderText, text)
	local block = HeaderText:GetParent()
	if HeaderText.__QuesterTagIcon then
		HeaderText.__QuesterTagIcon:Hide()
	end
	if block.__QuesterQuestTracker and block.id then
		local questLogIndex = C_QuestLog.GetLogIndexForQuestID(block.id)
		if questLogIndex then
			text = GetTaggedTitle(questLogIndex, db.questTrackerColor, true)
			HeaderText:__QuesterSetText(text)

			if db.showTagIcons then
				local tagAtlas = GetQuestTagAtlas(questLogIndex)
				if tagAtlas then
					if not HeaderText.__QuesterTagIcon then
						HeaderText.__QuesterTagIcon = block:CreateTexture(nil, "ARTWORK")
						HeaderText.__QuesterTagIcon:SetSize(18, 18)
						HeaderText.__QuesterTagIcon:SetPoint("TOP", HeaderText, "TOP", 0, 3)
						HeaderText.__QuesterTagIcon:SetPoint("LEFT", HeaderText, "RIGHT", -16, 0)
					end
					HeaderText.__QuesterTagIcon:SetAtlas(tagAtlas)
					HeaderText.__QuesterTagIcon:Show()
				end
			end
		end
	end
end

function Quester:QuestTrackerGetBlock(mod, questID, optTemplate)
	local blockTemplate = optTemplate or mod.blockTemplate
	if not mod.usedBlocks[blockTemplate] then return end
	local block = mod.usedBlocks[blockTemplate][questID]
	if block and block.HeaderText then
		if not block.__QuesterHooked then
			block.HeaderText.__QuesterSetText = block.HeaderText.SetText
			self:SecureHook(block.HeaderText, "SetText", "QuestTrackerHeaderSetText")
			self:SecureHook(block, "AddObjective", "ObjectiveTracker_AddObjective")
			--self:SecureHook(block, "AddProgressBar", "ObjectiveTracker_AddProgressBar")
			block.__QuesterHooked = true
		end
		block.__QuesterQuestTracker = true
	end
end

function Quester:QuestTrackerOnFreeBlock(mod, block)
	block.__QuesterQuestTracker = nil
end

local function shorten_numbers(cur, total)
	if db.hide01 and total == "1" then
		return ""
	end
	if db.shortenNumbers then
		return tostring(total-cur).." "
	end
end

local function shorten_numbers_opt(opt, cur, total)
	local s = shorten_numbers(cur, total)
	if s then
		return ("%s %s"):format(opt, s)
	end
end

local objective_count = "^(%d+)/(%d+) "
local objective_count_opt = "^" .. OPTIONAL_QUEST_OBJECTIVE_DESCRIPTION:format("QuesterPattern"):gsub("%(", "%(%%("):gsub("%)", "%%)%)"):gsub("QuesterPattern", "(%%d+)/(%%d+) ")

function Quester:ObjectiveTracker_AddObjective(block, objectiveKey, text, template, useFullHeight, dashStyle, colorStyle, adjustForNoText, overrideHeight)
	local line = block:GetExistingLine(objectiveKey)
	if not line then return end
	if colorStyle == OBJECTIVE_TRACKER_COLOR["Header"] then
		if db.questTrackerColor then
			text = select(4, GetTaskInfo(block.id))
			if text then
				line.Text:SetText(format("|cff%s%s|r", rgb2hex(QuestDifficultyColors["difficult"]), text))
			end
		end
	else
		if progress[text] then
			local newText
			if db.shortenNumbers or db.hide01 then
				newText = text:gsub(objective_count, shorten_numbers):gsub(objective_count_opt, shorten_numbers_opt)
			end
			line.Text:SetText(format("|cff%s%s|r", rgb2hex(ColorGradient(progress[text].perc, 0.8,0.8,0.8, 1,1,0, 0,1,0)), newText or text))
		end
	end
end

function Quester:SetupChatFilter()
	local function process(full, level, partial)
		return full:gsub(partial, quests[partial] and GetChatTaggedTitle(C_QuestLog.GetLogIndexForQuestID(quests[partial])) or "("..level..") "..partial)
	end
	local function filter(frame, event, msg, ...)
		if msg then
			if db.questLevels then
				msg = msg:gsub("(|c%x+|Hquest:%d+:(%d+)|h%[([^|]*)%]|h|r)", process)
			end
			return false, msg, ...
		end
	end
	for _,event in pairs{"SAY", "YELL", "GUILD", "GUILD_OFFICER", "WHISPER", "WHISPER_INFORM", "PARTY", "PARTY_LEADER", "RAID", "RAID_LEADER", "INSTANCE_CHAT", "INSTANCE_CHAT_LEADER", "BATTLEGROUND", "BATTLEGROUND_LEADER", "CHANNEL"} do
		ChatFrame_AddMessageEventFilter("CHAT_MSG_"..event, filter)
	end
end

function Quester:SetRewardHighlight(reward)
	if not self.rewardHighlightFrame then
		self.rewardHighlightFrame = CreateFrame("Frame", "QuesterRewardHighlight", QuestInfoRewardsFrame, "AutoCastOverlayTemplate")
		self.rewardHighlightFrame:SetScript("OnHide", function(frame) frame:ShowAutoCastEnabled(false) end)
	end
	self.rewardHighlightFrame:SetSize(42, 42)
	self.rewardHighlightFrame:ClearAllPoints()
	self.rewardHighlightFrame:SetPoint("CENTER", reward, "CENTER")
	self.rewardHighlightFrame:Show()
	self.rewardHighlightFrame:ShowAutoCastEnabled(true)
end

function Quester:QUEST_COMPLETE()
	if self.rewardHighlightFrame then
		self.rewardHighlightFrame:Hide()
	end

	if not db.highlightReward then return end

	local bestprice, bestitem = 0, 0
	for i = 1, GetNumQuestChoices() do
		local link, name, _, qty = GetQuestItemLink("choice", i), GetQuestItemInfo("choice", i)
		local price = link and select(11, C_Item.GetItemInfo(link))
		if not price then return end
		price = price * (qty or 1)
		if price > bestprice then
			bestprice = price
			bestitem = i
		end
	end
	if bestitem > 0 then
		self:SetRewardHighlight(_G[("QuestInfoRewardsFrameQuestInfoItem%dIconTexture"):format(bestitem)])
	end
end
