--[[-----------------------------------------------------------------------------
TreeGroup Container
Container that uses a tree control to switch between groups.
-------------------------------------------------------------------------------]]
local Type, Version = "Meta_ListGroup", 1
local AceGUI = LibStub and LibStub("AceGUI-3.0", true)
if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end

-- Lua APIs
local next, pairs, ipairs, assert, type = next, pairs, ipairs, assert, type
local math_min, math_max, floor = math.min, math.max, math.floor
local select, tremove, unpack, tconcat = select, table.remove, unpack, table.concat

-- WoW APIs
local CreateFrame, UIParent = CreateFrame, UIParent

-- Recycling functions
local new, del
do
	local pool = setmetatable({}, { __mode = 'k' })
	function new()
		local t = next(pool)
		if t then
			pool[t] = nil
			return t
		else
			return {}
		end
	end

	function del(t)
		for k in pairs(t) do
			t[k] = nil
		end
		pool[t] = true
	end
end

local DEFAULT_TREE_WIDTH = 400
local DEFAULT_TREE_SIZABLE = true

--[[-----------------------------------------------------------------------------
Support functions
-------------------------------------------------------------------------------]]


--[[-----------------------------------------------------------------------------
Scripts
-------------------------------------------------------------------------------]]
local function Search_OnTextChanged(frame)
	if frame.reset then
		frame.reset = false
		return
	end
	local self = frame.obj
	self:RefreshTree()
end

local function Filter_SetSelection(dd, value)
	local self = dd.obj
	self.filterSelect = value
	self:RefreshTree()
end

local function Filter_IsSelection(dd, value)
	local self = dd.obj
	return value == self.filterSelect
end

--[[-----------------------------------------------------------------------------
Methods
-------------------------------------------------------------------------------]]
local methods = {
	["OnAcquire"] = function(self)
		self:SetTreeWidth(DEFAULT_TREE_WIDTH, DEFAULT_TREE_SIZABLE)
		self:EnableButtonTooltips(true)
	end,

	["OnRelease"] = function(self)
		self.status = nil
		self.frame:SetScript("OnUpdate", nil)
		for k, v in pairs(self.localstatus) do
			if k == "groups" then
				for k2 in pairs(v) do
					v[k2] = nil
				end
			else
				self.localstatus[k] = nil
			end
		end
		self.localstatus.scrollvalue = 0
		self.localstatus.treewidth = DEFAULT_TREE_WIDTH
		self.localstatus.treesizable = DEFAULT_TREE_SIZABLE
	end,

	["EnableButtonTooltips"] = function(self, enable)
		self.enabletooltips = enable
	end,

	["SetStatusTable"] = function(self, status)
		assert(type(status) == "table")
		self.status = status
		if not status.groups then
			status.groups = {}
		end
		if not status.scrollvalue then
			status.scrollvalue = 0
		end
		if not status.treewidth then
			status.treewidth = DEFAULT_TREE_WIDTH
		end
		if status.treesizable == nil then
			status.treesizable = DEFAULT_TREE_SIZABLE
		end
		self:SetTreeWidth(status.treewidth, status.treesizable)
		self:RefreshTree()
	end,

	["RefreshTree"] = function(self)
		local tree = self.data
		if self.filterfunc then
			tree = self:filterfunc(self.searchbox:GetText(), tree, self.filterSelect)
		end
		self.scrollview:UpdateContentData(tree)
	end,

	["OnWidthSet"] = function(self, width)
		local treeframe = self.treeframe
		local status = self.status or self.localstatus
		status.fullwidth = width

		if width > 100 and status.treewidth ~= width then
			self:SetTreeWidth(width, status.treesizable)
		end
		if treeframe.SetResizeBounds then
			treeframe:SetResizeBounds(100, 1, width, 1600)
		else
			treeframe:SetMaxResize(width, 1600)
		end
	end,

	["SetTreeWidth"] = function(self, treewidth, resizable)
		if not resizable then
			if type(treewidth) == 'number' then
				resizable = false
			elseif type(treewidth) == 'boolean' then
				resizable = treewidth
				treewidth = DEFAULT_TREE_WIDTH
			else
				resizable = false
				treewidth = DEFAULT_TREE_WIDTH
			end
		end
		self.treeframe:SetWidth(treewidth)

		local status = self.status or self.localstatus
		status.treewidth = treewidth
		status.treesizable = resizable

		-- recalculate the content width
		if status.fullwidth then
			self:OnWidthSet(status.fullwidth)
		end
	end,

	["GetTreeWidth"] = function(self)
		local status = self.status or self.localstatus
		return status.treewidth or DEFAULT_TREE_WIDTH
	end,

	["LayoutFinished"] = function(self, width, height)
		if self.noAutoHeight then return end
		self:SetHeight((height or 0) + 20)
	end,

	["SetProgress"] = function(self, progressText)
		self.progresstext:SetText(progressText)
	end,

	["SetElementInitializer"] = function(self, elementInitializer)
		self.scrollview:SetElementInitializer("BackdropTemplate", elementInitializer)
	end,

	["SetElementExtentCalculator"] = function(self, elementExtentCalculator)
		self.scrollview:SetElementExtentCalculator(elementExtentCalculator)
	end,

	["SetFilterFunction"] = function(self, filterfunc)
		self.filterfunc = filterfunc
	end,

	["SetData"] = function(self, data)
		self.data = data
		self:RefreshTree()
	end,

	-- Build one modern tab: dark gold-bordered pill with centered gold label, a hover
	-- glow, and a gold "active" underline. Fully custom (no PanelTabButtonTemplate) so
	-- its width is exactly what we set -- it can never overflow the window edge.
	["MakeTab"] = function(self, bar)
		local tab = CreateFrame("Button", nil, bar, "BackdropTemplate")
		tab:SetBackdrop({
			bgFile = "Interface\\Buttons\\WHITE8X8",
			edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1,
		})
		tab:SetBackdropColor(0.10, 0.09, 0.08, 0.95)
		tab:SetBackdropBorderColor(0.52, 0.42, 0.20, 1)
		tab.label = tab:CreateFontString(nil, "OVERLAY")
		tab.label:SetFontObject(GameFontNormalSmall)
		tab.label:SetPoint("LEFT", 5, 0)
		tab.label:SetPoint("RIGHT", -5, 0)
		tab.label:SetJustifyH("CENTER")
		tab.label:SetWordWrap(false)
		local hl = tab:CreateTexture(nil, "HIGHLIGHT")
		hl:SetAllPoints()
		hl:SetColorTexture(1, 0.82, 0.28, 0.13)
		hl:SetBlendMode("ADD")
		tab.underline = tab:CreateTexture(nil, "OVERLAY")
		tab.underline:SetHeight(2)
		tab.underline:SetPoint("BOTTOMLEFT", 4, 1)
		tab.underline:SetPoint("BOTTOMRIGHT", -4, 1)
		tab.underline:SetColorTexture(1, 0.82, 0.30, 0.9)
		tab.underline:Hide()
		tab:SetScript("OnClick", function(selTab)
			self:SelectTab(selTab:GetID())
			self:Fire("OnTabSelected", selTab:GetID())
		end)
		return tab
	end,

	["SelectTab"] = function(self, id)
		self.selectedTab = id
		for _, tab in ipairs(self.tabCache) do
			if tab:IsShown() then
				if tab:GetID() == id then
					tab:SetBackdropColor(0.24, 0.17, 0.05, 0.98)
					tab:SetBackdropBorderColor(1, 0.82, 0.32, 1)
					tab.label:SetTextColor(1, 0.93, 0.62)
					tab.underline:Show()
				else
					tab:SetBackdropColor(0.10, 0.09, 0.08, 0.95)
					tab:SetBackdropBorderColor(0.52, 0.42, 0.20, 1)
					tab.label:SetTextColor(0.82, 0.75, 0.52)
					tab.underline:Hide()
				end
			end
		end
	end,

	["SetTabs"] = function(self, tabs, containerFrame)
		-- Tab bar pinned to both bottom corners of the window: it always spans exactly
		-- the window's inner width. Tabs are custom buttons laid out in equal columns
		-- INSIDE the bar, each SetWidth exactly -- so nothing can spill past the border.
		local leftInset, rightInset, gap, rowHeight = 6, 6, 5, 26
		local bar = self.tabBar
		if not bar then
			bar = CreateFrame("Frame", nil, containerFrame)
			self.tabBar = bar
		end
		bar:ClearAllPoints()
		bar:SetPoint("TOPLEFT", containerFrame, "BOTTOMLEFT", leftInset, 3)
		bar:SetPoint("TOPRIGHT", containerFrame, "BOTTOMRIGHT", -rightInset, 3)
		bar:Show()

		for _, tab in ipairs(self.tabCache) do tab:Hide() end
		local n = #tabs
		if n == 0 then return end

		local barWidth = containerFrame:GetWidth() - leftInset - rightInset
		if barWidth < 100 then barWidth = 490 end
		-- Fit ALL expansion tabs on ONE row whenever they'd stay readable -- a wrapped
		-- second row renders below the window and gets clipped, which is why a 5th
		-- expansion "disappeared". Tabs shrink to share the width (labels truncate,
		-- no word-wrap); only spill to a 2nd row if each would be narrower than this.
		local minTabWidth = 58
		local perRow = math.max(1, math.floor((barWidth + gap) / (minTabWidth + gap)))
		perRow = math.min(perRow, n)
		local rows = math.ceil(n / perRow)
		local cols = math.ceil(n / rows)
		bar:SetHeight(rows * rowHeight)

		local shown = {}
		for i, name in ipairs(tabs) do
			local tab = self.tabCache[i]
			if not tab then tab = self:MakeTab(bar); tinsert(self.tabCache, tab) end
			tab:SetParent(bar)
			tab:Show()
			tab.label:SetText(name)
			tab:SetID(i)
			shown[i] = tab
		end

		for i, tab in ipairs(shown) do
			local rowIndex = math.floor((i - 1) / cols)
			local firstInRow = rowIndex * cols + 1
			local countInRow = math.min(cols, n - firstInRow + 1)
			local colIndex = (i - 1) - rowIndex * cols
			local colWidth = (barWidth - gap * (countInRow - 1)) / countInRow
			tab:ClearAllPoints()
			tab:SetPoint("TOPLEFT", bar, "TOPLEFT", colIndex * (colWidth + gap), -rowIndex * rowHeight)
			tab:SetSize(colWidth, rowHeight - 4)
		end

		self:SelectTab(self.selectedTab or 1)
	end,

	["ResetSearch"] = function(self)
		self.searchbox:SetText("")
		self.searchbox.reset = true
	end,

	["JumpToIndex"] = function(self, index, data)
		self.scrollbox:ScrollToElementDataIndex(index, ScrollBoxConstants.ScrollBegin)
		self:ExpandByData(data, true)
	end,

	["ExpandByData"] = function(self, data, noCollapse)
		local isChild = tostring(data.value):match("(.*)\001")
		if not isChild then
			local frame = self.scrollview:FindFrame(data)
			data.expanded = not data.expanded
			if frame then
				if noCollapse then
					if not data.expanded then
						data.expanded = true
						return
					end
				end
				frame:Expand(data.expanded)
				self.scrollbox:FullUpdate(true)
			end
		end
	end,

	["Expand"] = function(self, frame, btnType)
		if IsShiftKeyDown() then
			if not AchievementFrame then
				AchievementFrame_LoadUI()
			end
			AchievementFrame:Show()
			AchievementFrame_SelectAchievement(frame.data.value)
			return
		end
		local isChild = tostring(frame.data.value):match("(.*)\001")
		if not isChild then
			frame.data.expanded = not frame.data.expanded
			frame:Expand(frame.data.expanded)
			self.scrollbox:FullUpdate(true)
		else
			if btnType == "RightButton" then
				self:Fire("OnExpandRightClick", frame.data)
			elseif frame.data.jump then
				local index, data = self.scrollview:GetDataProvider():FindByPredicate(function(searchData)
					return searchData.value == frame.data.jump
				end)
				if not index then return end
				self:JumpToIndex(index, data)
			end
		end
	end,

	["ShowTooltip"] = function(self, frame)
		self:Fire("OnButtonEnter", frame)

		local rTime = nil
		local function refreshTooltip()
			if rTime and rTime.Cancel then
				rTime:Cancel()
			end
			rTime = C_Timer.NewTimer(0.1, function()
				if GameTooltip:GetOwner() ~= frame then
					return
				end
				self:ShowTooltip(frame)
			end)
		end

		if frame.data.tooltip then
			GameTooltip:SetOwner(frame, "ANCHOR_NONE")
			GameTooltip:ClearAllPoints()
			GameTooltip:SetPoint("LEFT", frame, "RIGHT")
			GameTooltip:ClearLines()

			for rType, id in frame.data.tooltip:gmatch("%[(%a+):(%d+)%]") do
				if tonumber(id) then
					if rType == "item" then
						local item = Item:CreateFromItemID(tonumber(id))
						item:ContinueOnItemLoad(function()
							frame.data.tooltip = frame.data.tooltip:gsub("%[item:" .. id .. "%]", item:GetItemLink())
							refreshTooltip()
						end)
					elseif rType == "spell" then
						local spell = Spell:CreateFromSpellID(tonumber(id))
						spell:ContinueOnSpellLoad(function()
							frame.data.tooltip = frame.data.tooltip:gsub("%[spell:" .. id .. "%]",
								C_Spell.GetSpellLink(tonumber(id)))
							refreshTooltip()
						end)
					end
				end
			end

			GameTooltip:AddLine(frame.data.tooltip or "", 1, 1, 1, true)

			local id = tonumber(tostring(frame.data.value):match("^(%d+)"))
			local achievementInfo = { GetAchievementInfo(id) }
			local achReward = achievementInfo[11] or ""

			if achReward and achReward ~= "" then
				GameTooltip:AddLine(" ")
				GameTooltip:AddLine(achReward, 1, 1, 1, true)
			end

			local prereqs = frame.data.prerequisites
			if prereqs and (prereqs.quests or prereqs.achievements or prereqs.startQuest) then
				local QUEST_ICON = "|TInterface\\GossipFrame\\AvailableQuestIcon:14:14:0:0|t"
				local ACH_ICON = "|TInterface\\Icons\\Ability_Warrior_ShieldWall:14:14:0:0:64:64:5:59:5:59|t"
				GameTooltip:AddLine(" ")
				GameTooltip:AddLine("|cffffd700Prerequisites:|r")
				local allDone = true

				for _, q in ipairs(prereqs.quests or {}) do
					local qid = type(q) == "table" and q.id or q
					local qname = (type(q) == "table" and q.name) or (qid and C_QuestLog.GetTitleForQuestID(qid)) or ("Quest " .. tostring(qid))
					local done = qid and C_QuestLog.IsQuestFlaggedCompleted(qid)
					if not done then allDone = false end
					GameTooltip:AddLine(QUEST_ICON .. " " ..
						(done and "|cff00ff00" or "|cffff0000") .. "Quest: " .. qname .. (done and " (done)" or "") .. "|r")
				end

				for _, a in ipairs(prereqs.achievements or {}) do
					local aid = type(a) == "table" and a.id or a
					local _, aname, _, aDone = GetAchievementInfo(aid)
					aname = (type(a) == "table" and a.name) or aname or ("Achievement " .. tostring(aid))
					if not aDone then allDone = false end
					GameTooltip:AddLine(ACH_ICON .. " " ..
						(aDone and "|cff00ff00" or "|cffff0000") .. "Achievement: " .. aname .. (aDone and " (done)" or "") .. "|r")
				end

				if not allDone and prereqs.startQuest then
					local sq = prereqs.startQuest
					local loc = (sq.x and sq.y) and string.format(" (%.1f, %.1f)", sq.x, sq.y) or ""
					GameTooltip:AddLine("|cffffa500To start: pick up \"" .. (sq.name or "the starting quest") .. "\"" ..
						(sq.npc and (" from " .. sq.npc) or "") .. loc .. "|r", nil, nil, nil, true)
				end
			end

			GameTooltip:Show()
		end
	end,

	["HideTooltip"] = function(self, frame)
		self:Fire("OnButtonLeave", frame)
		GameTooltip:Hide()
	end,
}

--[[-----------------------------------------------------------------------------
Constructor
-------------------------------------------------------------------------------]]

local function Constructor()
	local frame = CreateFrame("Frame", nil, UIParent)
	frame.Tabs = {}

	-- (progress + search row sit lower now, clearing the centered Do-it-now button)
	local progressText = frame:CreateFontString()
	progressText:SetFontObject("GameFontHighlight")
	progressText:SetFont(progressText:GetFont(), 14, "OUTLINE")
	progressText:SetPoint("TOPLEFT", 10, -34)
	progressText:SetPoint("TOPRIGHT", -10, -34)
	progressText:SetHeight(20)
	progressText:SetTextColor(1, 0.86, 0.4)
	progressText:SetText("Loading Progress...")

	local radioDropdown = CreateFrame("DropdownButton", nil, frame, "WowStyle1FilterDropdownTemplate")
	radioDropdown:SetPoint("TOPRIGHT", -10, -60)
	radioDropdown:SetSize(150, 22)

	-- Custom modern search field: dark input, thin gold border, magnifier, placeholder.
	-- (Replaces SearchBoxTemplate so the gold outline hugs the field exactly.)
	local searchWrap = CreateFrame("Frame", nil, frame, "BackdropTemplate")
	searchWrap:SetPoint("TOPLEFT", 10, -60)
	searchWrap:SetPoint("TOPRIGHT", radioDropdown, "TOPLEFT", -10, 0)
	searchWrap:SetHeight(22)
	searchWrap:SetBackdrop({
		bgFile = "Interface\\Buttons\\WHITE8X8",
		edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1,
	})
	searchWrap:SetBackdropColor(0.05, 0.05, 0.06, 0.9)
	searchWrap:SetBackdropBorderColor(0.72, 0.58, 0.26, 0.95)

	local searchIcon = searchWrap:CreateTexture(nil, "OVERLAY")
	searchIcon:SetSize(13, 13)
	searchIcon:SetPoint("LEFT", 7, 0)
	searchIcon:SetTexture("Interface\\Common\\UI-Searchbox-Icon")
	searchIcon:SetVertexColor(0.85, 0.72, 0.4)

	local searchBox = CreateFrame("EditBox", nil, searchWrap)
	searchBox:SetPoint("LEFT", searchIcon, "RIGHT", 5, 0)
	searchBox:SetPoint("RIGHT", -6, 0)
	searchBox:SetHeight(20)
	searchBox:SetAutoFocus(false)
	searchBox:SetFontObject("GameFontHighlightSmall")
	searchBox:SetScript("OnEscapePressed", searchBox.ClearFocus)

	local placeholder = searchBox:CreateFontString(nil, "ARTWORK", "GameFontDisableSmall")
	placeholder:SetPoint("LEFT", 2, 0)
	placeholder:SetText("Search")
	searchBox:HookScript("OnTextChanged", function(box)
		placeholder:SetShown(box:GetText() == "")
		Search_OnTextChanged(box)
	end)
	searchBox:HookScript("OnEditFocusGained", function() placeholder:Hide() end)
	searchBox:HookScript("OnEditFocusLost", function(box) placeholder:SetShown(box:GetText() == "") end)

	local treeframe = CreateFrame("Frame", nil, frame, "BackdropTemplate")
	treeframe:SetPoint("TOPLEFT", searchWrap, "BOTTOMLEFT", 0, -4)
	treeframe:SetPoint("BOTTOMLEFT")
	treeframe:SetWidth(DEFAULT_TREE_WIDTH)
	treeframe:SetResizable(true)
	treeframe:SetClipsChildren(true)
	if treeframe.SetResizeBounds then -- WoW 10.0
		treeframe:SetResizeBounds(100, 1, 400, 1600)
	else
		treeframe:SetMinResize(100, 1)
		treeframe:SetMaxResize(400, 1600)
	end

	local scrollBox = CreateFrame("Frame", nil, treeframe, "WowScrollBoxList")
	scrollBox:SetSize(250, 400)

	local scrollBar = CreateFrame("EventFrame", nil, treeframe, "WowTrimScrollBar")
	scrollBar:SetPoint("TOPLEFT", scrollBox, "TOPRIGHT", 5, 0)
	scrollBar:SetPoint("BOTTOMLEFT", scrollBox, "BOTTOMRIGHT")
	scrollBar:SetHideIfUnscrollable(true)

	local scrollView = CreateScrollBoxListLinearView()
	ScrollUtil.InitScrollBoxListWithScrollBar(scrollBox, scrollBar, scrollView)

	local function setAnchors(withScrollBar)
		scrollBox:ClearAllPoints()
		for _, anchor in ipairs(withScrollBar and {
			CreateAnchor("TOPLEFT", 0, -10),
			CreateAnchor("BOTTOMRIGHT", -45, 0)
		} or {
			CreateAnchor("TOPLEFT", 0, -10),
			CreateAnchor("BOTTOMRIGHT", -15, 0)
		}) do
			scrollBox:SetPoint(anchor:Get())
		end
	end
	scrollBar:HookScript("OnShow", function()
		setAnchors(true)
	end)
	scrollBar:HookScript("OnHide", function()
		setAnchors()
	end)
	setAnchors()

	function scrollView:UpdateContentData(data, keepOldData, insertAt, customInserts)
		if not data then return end
		local scrollPercent = scrollBox:GetScrollPercentage()
		local dataProvider = scrollView:GetDataProvider()
		if not dataProvider then
			dataProvider = CreateDataProvider()
			self:SetDataProvider(dataProvider)
		end
		if not keepOldData then
			dataProvider:Flush()
		end
		if not customInserts then
			dataProvider:Insert(unpack(data))
		else
			for index, part in ipairs(data) do
				if not insertAt then
					dataProvider:Insert(part)
				else
					dataProvider:InsertAtIndex(part, insertAt + index)
				end
			end
		end
		scrollBox:SetScrollPercentage(scrollPercent or 1)
	end

	---@class Meta_ListGroup : table, AceGUIWidget, AceGUITreeGroup
	local widget = {
		frame          = frame,
		localstatus    = { groups = {}, scrollvalue = 0 },
		treeframe      = treeframe,
		scrollview     = scrollView,
		scrollbox      = scrollBox,
		searchbox      = searchBox,
		progresstext   = progressText,
		type           = Type,
		filterfunc     = false,
		data           = {},
		tabCache       = {},
		filterDropdown = radioDropdown,
		filterSelect   = 1
	}
	for method, func in pairs(methods) do
		widget[method] = func
	end
	treeframe.obj, searchBox.obj, scrollView.obj, radioDropdown.obj = widget, widget, widget, widget

	MenuUtil.CreateRadioMenu(radioDropdown,
		function(value)
			return Filter_IsSelection(radioDropdown, value)
		end,
		function(value)
			Filter_SetSelection(radioDropdown, value)
		end,
		{ "All", 1 },
		{ "Earned", 2 },
		{ "Incomplete", 3 },
		{ "Almost done (1 left)", 4 },
		{ "Almost done (\226\137\1642 left)", 5 },
		{ "Almost done (\226\137\1643 left)", 6 }
	)
	radioDropdown:SetMenuAnchor(AnchorUtil.CreateAnchor("TOPLEFT", radioDropdown, "TOPRIGHT", 0, 5))

	return AceGUI:RegisterAsWidget(widget)
end

AceGUI:RegisterWidgetType(Type, Constructor, Version)