local _, ns = ...
local SlickUI = ns.SlickUI
local Theme = SlickUI.Theme

local QuestTracker = { Rows = {}, Sections = { dungeon = true, campaign = true, quests = true } }
SlickUI.QuestTracker = QuestTracker

local ROW_GAP, PADDING = 5, 10

StaticPopupDialogs.SLICKUI_ABANDON_QUEST = {
	text = "Abandon %s?",
	button1 = YES,
	button2 = NO,
	OnAccept = function(_, data)
		if InCombatLockdown() or not data or not data.questID then return end
		C_QuestLog.SetSelectedQuest(data.questID)
		C_QuestLog.SetAbandonQuest()
		C_QuestLog.AbandonQuest()
	end,
	timeout = 0,
	whileDead = true,
	hideOnEscape = true,
	preferredIndex = 3,
}

local function IsCampaignQuest(questID, info)
	if info and info.campaignID and info.campaignID > 0 then return true end
	if C_CampaignInfo and C_CampaignInfo.GetCampaignID then
		local ok, campaignID = pcall(C_CampaignInfo.GetCampaignID, questID)
		if ok and type(campaignID) == "number" and campaignID > 0 then return true end
	end
	if C_CampaignInfo and C_CampaignInfo.IsCampaignQuest then
		local ok, result = pcall(C_CampaignInfo.IsCampaignQuest, questID)
		if ok then return result == true end
	end
	return false
end

function QuestTracker:ShowQuestMenu(owner, questID, questTitle)
	if not MenuUtil or type(MenuUtil.CreateContextMenu) ~= "function" then return end
	MenuUtil.CreateContextMenu(owner, function(_, root)
		root:SetTag("SLICKUI_QUEST_TRACKER")
		root:CreateTitle(questTitle or "Quest")
		root:CreateButton("Untrack", function()
			if C_QuestLog and C_QuestLog.RemoveQuestWatch then C_QuestLog.RemoveQuestWatch(questID) end
		end)
		root:CreateButton("Abandon", function()
			if InCombatLockdown() then
				SlickUI:Print("Quests cannot be abandoned during combat.")
				return
			end
			StaticPopup_Show("SLICKUI_ABANDON_QUEST", questTitle or "this quest", nil, { questID = questID })
		end)
	end)
end

local function GetTrackedQuests()
	local result = { campaign = {}, quests = {}, popups = {} }
	if not C_QuestLog or not C_QuestLog.GetNumQuestLogEntries then return result end
	local count = C_QuestLog.GetNumQuestLogEntries() or 0
	local zoneName = "Other"
	for index = 1, count do
		local info = C_QuestLog.GetInfo(index)
		if info and info.isHeader and type(info.title) == "string" and info.title ~= "" then
			zoneName = info.title
		end
		if info and info.questID and info.questID > 0 and not info.isHeader and not info.isHidden then
			local watched = C_QuestLog.GetQuestWatchType and C_QuestLog.GetQuestWatchType(info.questID)
			if watched and watched ~= 0 then
				local bucket = IsCampaignQuest(info.questID, info) and result.campaign or result.quests
				bucket[#bucket + 1] = { info = info, zoneName = zoneName }
			end
		end
	end
	local getPopupCount = _G.GetNumAutoQuestPopUps or (C_QuestLog and C_QuestLog.GetNumAutoQuestPopUps)
	local getPopup = _G.GetAutoQuestPopUp or (C_QuestLog and C_QuestLog.GetAutoQuestPopUp)
	if type(getPopupCount) == "function" and type(getPopup) == "function" then
		local countValues = { pcall(getPopupCount) }
		local popupCount = countValues[2]
		if countValues[1] and type(popupCount) == "number" then
			for index = 1, popupCount do
				local popupValues = { pcall(getPopup, index) }
				local questID, popupType = popupValues[2], popupValues[3]
				if popupValues[1] and type(questID) == "number" and questID > 0 then
					local title = C_QuestLog.GetTitleForQuestID and C_QuestLog.GetTitleForQuestID(questID)
					result.popups[#result.popups + 1] = {
						questID = questID,
						popupType = popupType == "COMPLETE" and "COMPLETE" or "OFFER",
						title = type(title) == "string" and title ~= "" and title or ("Quest " .. questID),
					}
				end
			end
		end
	end
	return result
end

local function GetDungeonObjectives()
	local result = { active = false, title = nil, description = nil, criteria = {}, encounters = {} }
	if not C_Scenario or type(C_Scenario.GetInfo) ~= "function" or type(C_Scenario.GetStepInfo) ~= "function" then return result end
	local infoValues = { pcall(C_Scenario.GetInfo) }
	local okInfo, scenarioName, completed = infoValues[1], infoValues[2], infoValues[8]
	if type(scenarioName) == "table" then
		local info = scenarioName
		scenarioName = info.name or info.title
		completed = info.completed
	end
	if not okInfo or type(scenarioName) ~= "string" or scenarioName == "" or completed == true then return result end
	local stepValues = { pcall(C_Scenario.GetStepInfo) }
	local okStep, stepTitle, stepDescription, numCriteria = stepValues[1], stepValues[2], stepValues[3], stepValues[4]
	if type(stepTitle) == "table" then
		local step = stepTitle
		stepTitle = step.title or step.name
		stepDescription = step.description
		numCriteria = step.numCriteria
	end
	if not okStep or type(numCriteria) ~= "number" or numCriteria < 1 then return result end
	result.active = true
	result.title = type(stepTitle) == "string" and stepTitle ~= "" and stepTitle or scenarioName
	result.description = type(stepDescription) == "string" and stepDescription ~= "" and stepDescription or nil
	local getCriteriaInfo = C_ScenarioInfo and C_ScenarioInfo.GetCriteriaInfo
	if type(getCriteriaInfo) ~= "function" then
		getCriteriaInfo = C_Scenario and C_Scenario.GetCriteriaInfo
	end
	for index = 1, numCriteria do
		local values = type(getCriteriaInfo) == "function" and { pcall(getCriteriaInfo, index) } or { false }
		if values[1] then
			local info = values[2]
			local description, quantity, totalQuantity, criterionComplete, quantityString, isWeightedProgress
			if type(info) == "table" then
				description = info.description or info.criteriaString
				quantity = info.quantity
				totalQuantity = info.totalQuantity
				criterionComplete = info.completed
				quantityString = info.quantityString
				isWeightedProgress = info.isWeightedProgress
			else
				-- C_Scenario.GetCriteriaInfo returns criteriaString, criteriaType,
				-- completed, quantity, totalQuantity, flags, assetID,
				-- quantityString, criteriaID, duration, elapsed,
				-- criteriaFailed, isWeightedProgress. Account for pcall's leading
				-- success value when reading those positions.
				description = values[2]
				criterionComplete = values[4]
				quantity = values[5]
				totalQuantity = values[6]
				quantityString = values[9]
				isWeightedProgress = values[14]
			end
			if type(description) == "string" and description ~= "" then
				local progress
				if type(quantityString) == "string" and quantityString ~= "" then
					progress = quantityString
				elseif isWeightedProgress == true and type(quantity) == "number" then
					-- Weighted Mythic+ criteria are enemy-forces percentages. Some
					-- dungeons omit quantityString, so format the numeric value.
					progress = string.format("%.1f%%", quantity)
				elseif type(quantity) == "number" and type(totalQuantity) == "number" and totalQuantity > 1 then
					progress = string.format("%d/%d", quantity, totalQuantity)
				end
				result.criteria[#result.criteria + 1] = {
					description = description,
					completed = criterionComplete == true,
					progress = progress,
				}
			end
		end
	end
	-- Scenario criteria describe only the active step in many modern dungeons.
	-- Add the LFG encounter roster so the tracker also shows every boss, like
	-- Blizzard's dungeon tracker. The tenth GetInstanceInfo return is the
	-- active LFG dungeon ID (shifted to index 11 by pcall's success value).
	if type(GetInstanceInfo) == "function" and type(GetLFGDungeonNumEncounters) == "function"
		and type(GetLFGDungeonEncounterInfo) == "function" then
		local instanceValues = { pcall(GetInstanceInfo) }
		local dungeonID = instanceValues[11]
		if instanceValues[1] and type(dungeonID) == "number" and dungeonID > 0 then
			local countValues = { pcall(GetLFGDungeonNumEncounters, dungeonID) }
			local encounterCount = countValues[2]
			if countValues[1] and type(encounterCount) == "number" then
				for encounterIndex = 1, encounterCount do
					local encounterValues = { pcall(GetLFGDungeonEncounterInfo, dungeonID, encounterIndex) }
					local name, texture, killed = encounterValues[2], encounterValues[3], encounterValues[4]
					if encounterValues[1] and type(name) == "string" and name ~= "" then
						local killedReadable = (not canaccessvalue or canaccessvalue(killed))
							and (not issecretvalue or not issecretvalue(killed))
						result.encounters[#result.encounters + 1] = {
							name = name,
							texture = texture,
							completed = killedReadable and killed == true or false,
						}
					end
				end
			end
		end
	end

	-- A live instance does not always expose an LFG dungeon ID (notably for
	-- manually entered and newly released dungeons). In that case Blizzard's
	-- tracker can still obtain the complete boss roster from Encounter Journal
	-- map data. Merge that roster here without duplicating LFG encounters.
	if type(EJ_GetInstanceForMap) == "function" and type(EJ_GetEncounterInfoByIndex) == "function"
		and C_Map and type(C_Map.GetBestMapForUnit) == "function" then
		local mapValues = { pcall(C_Map.GetBestMapForUnit, "player") }
		local mapID = mapValues[2]
		if mapValues[1] and type(mapID) == "number" and mapID > 0 then
			local journalInstanceID
			local checkedMaps = {}
			for _ = 1, 5 do
				if checkedMaps[mapID] then break end
				checkedMaps[mapID] = true
				local journalValues = { pcall(EJ_GetInstanceForMap, mapID) }
				if journalValues[1] and type(journalValues[2]) == "number" and journalValues[2] > 0 then
					journalInstanceID = journalValues[2]
					break
				end
				local mapInfoValues = { pcall(C_Map.GetMapInfo, mapID) }
				local mapInfo = mapInfoValues[2]
				mapID = mapInfoValues[1] and type(mapInfo) == "table" and mapInfo.parentMapID or nil
				if type(mapID) ~= "number" or mapID <= 0 then break end
			end
			if journalInstanceID then
				local known = {}
				for _, encounter in ipairs(result.encounters) do
					known[string.lower(encounter.name)] = true
				end
				for encounterIndex = 1, 30 do
					local encounterValues = { pcall(EJ_GetEncounterInfoByIndex, encounterIndex, journalInstanceID) }
					local name = encounterValues[2]
					if not encounterValues[1] or type(name) ~= "string" or name == "" then break end
					local key = string.lower(name)
					if not known[key] then
						known[key] = true
						result.encounters[#result.encounters + 1] = {
							name = name,
							completed = false,
						}
					end
				end
			end
		end
	end
	return result
end

function QuestTracker:AddZoneHeader(text, key, y)
	local header = self.ZoneHeaders[key]
	if not header then
		header = CreateFrame("Frame", nil, self.Content)
		header:SetHeight(22); header:SetPoint("LEFT", 12, 0); header:SetPoint("RIGHT", -8, 0)
		local label = Theme:CreateText(header, math.max(9, SlickUI.DB.questTracker.questFontSize - 1))
		label:SetPoint("LEFT", 4, 0); label:SetPoint("RIGHT", -4, 0); label:SetJustifyH("LEFT")
		label:SetTextColor(0.78, 0.67, 0.40); header.Label = label
		self.ZoneHeaders[key] = header
	end
	header:ClearAllPoints(); header:SetPoint("TOPLEFT", 12, -y); header:SetPoint("RIGHT", -8, 0)
	header.Label:SetText(text); Theme:SetFont(header.Label, math.max(9, SlickUI.DB.questTracker.questFontSize - 1))
	header:Show(); self.ActiveZoneHeaders[#self.ActiveZoneHeaders + 1] = header
	return y + 24
end

function QuestTracker:AddQuestList(list, sectionKey, y)
	local lastZone
	for _, entry in ipairs(list) do
		if SlickUI.DB.questTracker.groupByZone and entry.zoneName ~= lastZone then
			lastZone = entry.zoneName
			y = self:AddZoneHeader(lastZone or "Other", sectionKey .. ":" .. (lastZone or "Other"), y)
		end
		y = self:AddQuest(entry.info, y)
	end
	return y
end

local function HideBlizzardTracker()
	local tracker = _G.ObjectiveTrackerFrame
	if not tracker then return end
	if tracker.SlickUIOriginalAlpha == nil then tracker.SlickUIOriginalAlpha = tracker:GetAlpha() end
	tracker:SetAlpha(0)
	if tracker.EnableMouse then tracker:EnableMouse(false) end
end

local function RestoreBlizzardTracker()
	local tracker = _G.ObjectiveTrackerFrame
	if not tracker then return end
	tracker:SetAlpha(tracker.SlickUIOriginalAlpha or 1)
	if tracker.EnableMouse then tracker:EnableMouse(true) end
end

function QuestTracker:AcquireRow()
	local row = table.remove(self.RowPool)
	if row then row:Show(); return row end
	row = CreateFrame("Button", nil, self.Content)
	row:SetPoint("LEFT", PADDING, 0); row:SetPoint("RIGHT", -PADDING, 0)
	row:RegisterForClicks("LeftButtonUp", "RightButtonUp")
	row:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight", "ADD")
	local icon = row:CreateTexture(nil, "ARTWORK")
	icon:SetSize(22, 22); icon:SetPoint("TOPLEFT", 0, -2)
	icon:SetTexture("Interface\\GossipFrame\\AvailableQuestIcon")
	row.Icon = icon
	local title = Theme:CreateText(row, 12)
	title:SetPoint("TOPLEFT", icon, "TOPRIGHT", 7, 0); title:SetPoint("RIGHT", -2, 0)
	title:SetJustifyH("LEFT"); title:SetWordWrap(true); title:SetTextColor(unpack(Theme.Colors.goldText))
	row.Title = title
	local objectives = Theme:CreateText(row, 10)
	objectives:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -3); objectives:SetPoint("RIGHT", -2, 0)
	objectives:SetJustifyH("LEFT"); objectives:SetJustifyV("TOP"); objectives:SetWordWrap(true)
	row.Objectives = objectives
	local item = CreateFrame("Button", nil, row, "SecureActionButtonTemplate")
	item:SetSize(32, 32); item:SetPoint("TOPRIGHT", -1, -1); item:RegisterForClicks("AnyUp", "AnyDown")
	local itemIcon = item:CreateTexture(nil, "ARTWORK"); itemIcon:SetAllPoints(); itemIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92); item.Icon = itemIcon
	local border = item:CreateTexture(nil, "BACKGROUND"); border:SetPoint("TOPLEFT", -1, 1); border:SetPoint("BOTTOMRIGHT", 1, -1); border:SetColorTexture(1, 0.67, 0.12, 0.75); item.Border = border
	local count = Theme:CreateText(item, 10); count:SetPoint("BOTTOMRIGHT", -1, 1); count:SetJustifyH("RIGHT"); item.Count = count
	local cooldown = CreateFrame("Cooldown", nil, item, "CooldownFrameTemplate"); cooldown:SetAllPoints(); item.Cooldown = cooldown
	item:SetScript("OnEnter", function(self)
		if not self.ItemLink then return end
		GameTooltip:SetOwner(self, "ANCHOR_LEFT")
		GameTooltip:SetHyperlink(self.ItemLink)
		GameTooltip:AddLine("Click to use this quest item.", 1, 1, 1, true)
		GameTooltip:Show()
	end)
	item:SetScript("OnLeave", GameTooltip_Hide)
	item:Hide(); row.Item = item
	row:SetScript("OnClick", function(self, button)
		if button == "LeftButton" and self.QuestPopupType and self.QuestID then
			local handler = self.QuestPopupType == "COMPLETE" and _G.ShowQuestComplete or _G.ShowQuestOffer
			if type(handler) == "function" then
				pcall(handler, self.QuestID)
			elseif QuestMapFrame_OpenToQuestDetails then
				pcall(QuestMapFrame_OpenToQuestDetails, self.QuestID)
			end
			return
		end
		if button == "RightButton" and self.QuestID then
			QuestTracker:ShowQuestMenu(self, self.QuestID, self.Title:GetText())
			return
		end
		if self.QuestID and QuestMapFrame_OpenToQuestDetails then
			pcall(QuestMapFrame_OpenToQuestDetails, self.QuestID)
		end
	end)
	row:SetScript("OnEnter", function(self)
		if not self.QuestID then return end
		GameTooltip:SetOwner(self, "ANCHOR_LEFT")
		GameTooltip:SetText(self.Title:GetText() or "Quest")
		if self.QuestPopupType then
			GameTooltip:AddLine("Click to view this quest.", 1, 1, 1, true)
			GameTooltip:Show()
			return
		end
		GameTooltip:AddLine("Left-click to open the Quest Log.", 1, 1, 1, true)
		GameTooltip:AddLine("Right-click for Untrack and Abandon.", 1, 1, 1, true)
		GameTooltip:Show()
	end)
	row:SetScript("OnLeave", GameTooltip_Hide)
	return row
end

function QuestTracker:AddHeader(text, key, y)
	local header = self.Headers[key]
	if not header then
		header = CreateFrame("Button", nil, self.Content)
		header:SetHeight(30); header:SetPoint("LEFT", 4, 0); header:SetPoint("RIGHT", -4, 0)
		local bg = header:CreateTexture(nil, "BACKGROUND"); bg:SetAllPoints(); bg:SetColorTexture(0.18, 0.12, 0.02, 0.72)
		local label = Theme:CreateText(header, SlickUI.DB.questTracker.headerFontSize); label:SetPoint("LEFT", 9, 0); label:SetTextColor(unpack(Theme.Colors.goldText)); header.Label = label
		local marker = Theme:CreateText(header, SlickUI.DB.questTracker.headerFontSize); marker:SetPoint("RIGHT", -8, 0); header.Marker = marker
		header:SetScript("OnClick", function()
			QuestTracker.Sections[key] = not QuestTracker.Sections[key]
			QuestTracker:Refresh()
		end)
		self.Headers[key] = header
	end
	header:ClearAllPoints(); header:SetPoint("TOPLEFT", 4, -y); header:SetPoint("RIGHT", -4, 0)
	header.Label:SetText(text); header.Marker:SetText(self.Sections[key] and "−" or "+"); header:Show()
	Theme:SetFont(header.Label, SlickUI.DB.questTracker.headerFontSize)
	Theme:SetFont(header.Marker, SlickUI.DB.questTracker.headerFontSize)
	return y + 34
end

function QuestTracker:AddQuest(info, y)
	local row = self:AcquireRow(); self.ActiveRows[#self.ActiveRows + 1] = row
	row.QuestID = info.questID
	row.QuestLogIndex = info.questLogIndex
	row.QuestPopupType = nil
	row.Icon:SetTexture("Interface\\GossipFrame\\AvailableQuestIcon")
	row.Icon:SetTexture("Interface\\GossipFrame\\AvailableQuestIcon")
	row:ClearAllPoints(); row:SetPoint("TOPLEFT", PADDING, -y); row:SetPoint("RIGHT", -PADDING, 0)
	row.Title:SetText(info.title or C_QuestLog.GetTitleForQuestID(info.questID) or ("Quest " .. info.questID))
	Theme:SetFont(row.Title, SlickUI.DB.questTracker.questFontSize)
	Theme:SetFont(row.Objectives, SlickUI.DB.questTracker.objectiveFontSize)
	Theme:SetFont(row.Item.Count, SlickUI.DB.questTracker.objectiveFontSize)
	local lines = {}
	local objectives = C_QuestLog.GetQuestObjectives and C_QuestLog.GetQuestObjectives(info.questID)
	for _, objective in ipairs(objectives or {}) do
		local text = objective.text
		if type(text) == "string" and text ~= "" then
			lines[#lines + 1] = (objective.finished and "|cff55dd88✓ |r" or "|cffdddddd• |r") .. text
		end
	end
	if #lines == 0 and C_QuestLog.IsComplete and C_QuestLog.IsComplete(info.questID) then lines[1] = "|cff55dd88Ready for turn-in|r" end
	row.Objectives:SetText(table.concat(lines, "\n"))
	local itemLink, itemTexture, charges, showWhenComplete
	if GetQuestLogSpecialItemInfo and info.questLogIndex then
		itemLink, itemTexture, charges, showWhenComplete = GetQuestLogSpecialItemInfo(info.questLogIndex)
	end
	local complete = C_QuestLog.IsComplete and C_QuestLog.IsComplete(info.questID)
	local showItem = itemLink and (not complete or showWhenComplete)
	row.Item:SetShown(showItem == true)
	if showItem then
		row.Item.ItemLink = itemLink
		row.Item.Icon:SetTexture(itemTexture)
		row.Item.Count:SetText(type(charges) == "number" and charges > 0 and charges or "")
		row.Item:SetAttribute("type", "item")
		row.Item:SetAttribute("item", itemLink)
		if GetQuestLogSpecialItemCooldown then
			local start, duration, enabled = GetQuestLogSpecialItemCooldown(info.questLogIndex)
			CooldownFrame_Set(row.Item.Cooldown, start or 0, duration or 0, enabled or 0)
		end
	else
		row.Item.ItemLink = nil
		row.Item:SetAttribute("type", nil)
		row.Item:SetAttribute("item", nil)
		CooldownFrame_Clear(row.Item.Cooldown)
	end
	local rightInset = showItem and 40 or 2
	row.Title:ClearAllPoints(); row.Title:SetPoint("TOPLEFT", row.Icon, "TOPRIGHT", 7, 0); row.Title:SetPoint("RIGHT", -rightInset, 0)
	row.Objectives:ClearAllPoints(); row.Objectives:SetPoint("TOPLEFT", row.Title, "BOTTOMLEFT", 0, -3); row.Objectives:SetPoint("RIGHT", -rightInset, 0)
	row.Title:SetWidth(math.max(120, self.Content:GetWidth() - 58 - rightInset))
	row.Objectives:SetWidth(math.max(120, self.Content:GetWidth() - 58 - rightInset))
	local height = math.max(30, row.Title:GetStringHeight() + row.Objectives:GetStringHeight() + 10)
	row:SetHeight(height)
	return y + height + ROW_GAP
end

function QuestTracker:AddQuestPopup(info, y)
	local row = self:AcquireRow(); self.ActiveRows[#self.ActiveRows + 1] = row
	row.QuestID, row.QuestLogIndex, row.QuestPopupType = info.questID, nil, info.popupType
	row:ClearAllPoints(); row:SetPoint("TOPLEFT", PADDING, -y); row:SetPoint("RIGHT", -PADDING, 0)
	row.Icon:SetTexture("Interface\\GossipFrame\\AvailableQuestIcon")
	row.Title:SetText(info.popupType == "COMPLETE" and "Quest Complete!" or "Quest Discovered!")
	row.Title:SetTextColor(1, 0.82, 0.22, 1)
	row.Objectives:SetText((info.title or "Quest") .. "\n|cffaaaaaaClick to view quest|r")
	Theme:SetFont(row.Title, SlickUI.DB.questTracker.questFontSize)
	Theme:SetFont(row.Objectives, SlickUI.DB.questTracker.objectiveFontSize)
	row.Item:Hide(); row.Item.ItemLink = nil
	row.Item:SetAttribute("type", nil); row.Item:SetAttribute("item", nil)
	CooldownFrame_Clear(row.Item.Cooldown)
	row.Title:ClearAllPoints(); row.Title:SetPoint("TOPLEFT", row.Icon, "TOPRIGHT", 7, 0); row.Title:SetPoint("RIGHT", -2, 0)
	row.Objectives:ClearAllPoints(); row.Objectives:SetPoint("TOPLEFT", row.Title, "BOTTOMLEFT", 0, -3); row.Objectives:SetPoint("RIGHT", -2, 0)
	row.Title:SetWidth(math.max(120, self.Content:GetWidth() - 60))
	row.Objectives:SetWidth(math.max(120, self.Content:GetWidth() - 60))
	local height = math.max(44, row.Title:GetStringHeight() + row.Objectives:GetStringHeight() + 10)
	row:SetHeight(height)
	return y + height + ROW_GAP
end

function QuestTracker:AddDungeonStep(data, y)
	local row = self:AcquireRow(); self.ActiveRows[#self.ActiveRows + 1] = row
	row.QuestID, row.QuestLogIndex, row.QuestPopupType = nil, nil, nil
	row:ClearAllPoints(); row:SetPoint("TOPLEFT", PADDING, -y); row:SetPoint("RIGHT", -PADDING, 0)
	row.Icon:SetTexture("Interface\\LFGFrame\\LFGIcon-Dungeon")
	row.Title:SetText(data.title or "Dungeon Objectives")
	Theme:SetFont(row.Title, SlickUI.DB.questTracker.questFontSize)
	Theme:SetFont(row.Objectives, SlickUI.DB.questTracker.objectiveFontSize)
	local lines = {}
	if data.description then lines[#lines + 1] = "|cffbbbbbb" .. data.description .. "|r" end
	if #(data.encounters or {}) > 0 then
		lines[#lines + 1] = "|cffffcc55Bosses|r"
		for _, encounter in ipairs(data.encounters) do
			local marker = encounter.completed and "|cff55dd88\226\156\147 |r" or "|cffdddddd\226\128\162 |r"
			lines[#lines + 1] = marker .. encounter.name
		end
	end
	for _, criterion in ipairs(data.criteria or {}) do
		local marker = criterion.completed and "|cff55dd88\226\156\147 |r" or "|cffdddddd\226\128\162 |r"
		local progress = criterion.progress and (" |cff55ccff" .. criterion.progress .. "|r") or ""
		lines[#lines + 1] = marker .. criterion.description .. progress
	end
	row.Objectives:SetText(table.concat(lines, "\n"))
	row.Item:Hide(); row.Item.ItemLink = nil
	row.Item:SetAttribute("type", nil); row.Item:SetAttribute("item", nil)
	CooldownFrame_Clear(row.Item.Cooldown)
	row.Item:SetAttribute("type", nil); row.Item:SetAttribute("item", nil)
	CooldownFrame_Clear(row.Item.Cooldown)
	row.Title:ClearAllPoints(); row.Title:SetPoint("TOPLEFT", row.Icon, "TOPRIGHT", 7, 0); row.Title:SetPoint("RIGHT", -2, 0)
	row.Objectives:ClearAllPoints(); row.Objectives:SetPoint("TOPLEFT", row.Title, "BOTTOMLEFT", 0, -3); row.Objectives:SetPoint("RIGHT", -2, 0)
	row.Title:SetWidth(math.max(120, self.Content:GetWidth() - 60))
	row.Objectives:SetWidth(math.max(120, self.Content:GetWidth() - 60))
	local height = math.max(30, row.Title:GetStringHeight() + row.Objectives:GetStringHeight() + 10)
	row:SetHeight(height)
	return y + height + ROW_GAP
end

function QuestTracker:Refresh()
	if not self.Frame or not SlickUI.DB.questTracker.enabled then return end
	if InCombatLockdown() then self.RefreshPending = true; return end
	self.RefreshPending = false
	for _, row in ipairs(self.ActiveRows) do row:Hide(); self.RowPool[#self.RowPool + 1] = row end
	wipe(self.ActiveRows)
	for _, header in ipairs(self.ActiveZoneHeaders) do header:Hide() end
	wipe(self.ActiveZoneHeaders)
	local quests, dungeon, y = GetTrackedQuests(), GetDungeonObjectives(), 5
	if dungeon.active then
		y = self:AddHeader("Dungeon", "dungeon", y)
		if self.Sections.dungeon then y = self:AddDungeonStep(dungeon, y) end
	end
	y = self:AddHeader("Campaign", "campaign", y)
	if self.Sections.campaign then
		y = self:AddQuestList(quests.campaign, "campaign", y)
	end
	y = self:AddHeader("Quests", "quests", y)
	if self.Sections.quests then
		y = self:AddQuestList(quests.quests, "quests", y)
		for _, popup in ipairs(quests.popups) do y = self:AddQuestPopup(popup, y) end
	end
	if not dungeon.active and #quests.campaign + #quests.quests + #quests.popups == 0 then
		self.Empty:Show(); self.Empty:SetPoint("TOP", 0, -(y + 18)); y = y + 55
	else self.Empty:Hide() end
	self.Content:SetHeight(math.max(self.Scroll:GetHeight(), y + 8))
	HideBlizzardTracker()
end

function QuestTracker:CreateFrame()
	local frame = CreateFrame("Frame", "SlickUIQuestTracker", UIParent)
	frame:SetSize(330, 560); frame:SetFrameStrata("MEDIUM"); frame:SetClampedToScreen(true)
	Theme:CreatePanel(frame)
	local title = Theme:CreateText(frame, 17); title:SetPoint("TOPLEFT", 12, -11); title:SetText("SlickUI Quest Tracker"); title:SetTextColor(unpack(Theme.Colors.goldText))
	local collapse = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
	collapse:SetSize(24, 22); collapse:SetPoint("TOPRIGHT", -5, -5); collapse:SetText("-")
	collapse:SetFrameLevel(frame:GetFrameLevel() + 5)
	collapse:SetScript("OnClick", function() QuestTracker:SetCollapsed(not SlickUI.DB.questTracker.collapsed) end)
	collapse:SetScript("OnEnter", function(self)
		GameTooltip:SetOwner(self, "ANCHOR_LEFT")
		GameTooltip:SetText(SlickUI.DB.questTracker.collapsed and "Expand Quest Tracker" or "Collapse Quest Tracker")
		GameTooltip:Show()
	end)
	collapse:SetScript("OnLeave", GameTooltip_Hide)
	local scroll = CreateFrame("ScrollFrame", "SlickUIQuestTrackerScrollFrame", frame, "UIPanelScrollFrameTemplate")
	scroll:SetPoint("TOPLEFT", 7, -42); scroll:SetPoint("BOTTOMRIGHT", -28, 8); scroll:EnableMouseWheel(true)
	local content = CreateFrame("Frame", nil, scroll); content:SetSize(290, 1); scroll:SetScrollChild(content)
	scroll:SetScript("OnMouseWheel", function(self, delta)
		local range = math.max(0, self:GetVerticalScrollRange() or 0)
		self:SetVerticalScroll(math.max(0, math.min(range, (self:GetVerticalScroll() or 0) - delta * SlickUI.DB.questTracker.scrollStep)))
	end)
	local empty = Theme:CreateText(content, 11); empty:SetText("No watched quests."); empty:SetTextColor(unpack(Theme.Colors.mutedText)); empty:Hide()
	self.Frame, self.Title, self.CollapseButton, self.Scroll, self.Content, self.Empty = frame, title, collapse, scroll, content, empty
	self.Headers, self.ZoneHeaders, self.ActiveZoneHeaders, self.ActiveRows, self.RowPool = {}, {}, {}, {}, {}
	SlickUI.Movers:Register(frame, "questTracker", "Quest Tracker")
	return frame
end

function QuestTracker:SetCollapsed(collapsed)
	if not self.Frame then return end
	SlickUI.DB.questTracker.collapsed = collapsed == true
	self:ApplySettings()
end

function QuestTracker:ApplySettings()
	if not self.Frame then return end
	local settings = SlickUI.DB.questTracker
	self.Frame:SetSize(settings.width, settings.height)
	self.Frame:SetScale(settings.scale)
	if self.Frame.SlickBackground then
		self.Frame.SlickBackground:SetColorTexture(Theme.Colors.panel[1], Theme.Colors.panel[2], Theme.Colors.panel[3], settings.backgroundAlpha)
		self.Frame.SlickBackground:SetShown(not settings.collapsed)
	end
	for _, edge in ipairs(self.Frame.SlickEdges or {}) do edge:SetShown(settings.showBorder == true and not settings.collapsed) end
	self.Title:SetShown(settings.showHeader == true and not settings.collapsed)
	Theme:SetFont(self.Title, settings.headerFontSize + 2)
	Theme:SetFont(self.Empty, settings.objectiveFontSize + 1)
	for _, row in ipairs(self.ActiveRows) do Theme:SetFont(row.Item.Count, settings.objectiveFontSize) end
	self.CollapseButton:SetText(settings.collapsed and "+" or "-")
	self.Scroll:SetShown(not settings.collapsed)
	self.Scroll:ClearAllPoints()
	self.Scroll:SetPoint("TOPLEFT", 7, settings.showHeader == true and -42 or -8)
	self.Scroll:SetPoint("BOTTOMRIGHT", -28, 8)
	self.Content:SetWidth(math.max(180, settings.width - 40))
	self:Refresh()
end

function QuestTracker:SetEnabled(enabled)
	SlickUI.DB.questTracker.enabled = enabled == true
	if self.Frame then self.Frame:SetShown(enabled == true) end
	if enabled then HideBlizzardTracker(); self:Refresh() else RestoreBlizzardTracker() end
end

function QuestTracker:Initialize()
	self:CreateFrame()
	self:ApplySettings()
	self:SetEnabled(SlickUI.DB.questTracker.enabled)
	local events = CreateFrame("Frame")
	for _, event in ipairs({ "PLAYER_ENTERING_WORLD", "PLAYER_REGEN_ENABLED", "QUEST_LOG_UPDATE", "QUEST_WATCH_LIST_CHANGED", "QUEST_ACCEPTED", "QUEST_AUTOCOMPLETE", "QUEST_DATA_LOAD_RESULT", "SUPER_TRACKING_CHANGED", "ZONE_CHANGED_NEW_AREA", "BAG_UPDATE_COOLDOWN", "SCENARIO_UPDATE", "SCENARIO_CRITERIA_UPDATE", "SCENARIO_COMPLETED", "BOSS_KILL", "ENCOUNTER_END", "LFG_COMPLETION_REWARD" }) do events:RegisterEvent(event) end
	events:SetScript("OnEvent", function(_, event)
		if event == "PLAYER_REGEN_ENABLED" and not QuestTracker.RefreshPending then return end
		C_Timer.After(0.1, function() if SlickUI.DB.questTracker.enabled then QuestTracker:Refresh() end end)
	end)
	self.EventFrame = events
end

SlickUI:RegisterModule("QuestTracker", QuestTracker)
