--[[-----------------------------------------------------------------------------
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,

	["SetTabs"] = function(self, tabs, containerFrame)
		for _, tab in ipairs(self.tabCache) do
			tab:Hide()
		end

		self.frame.minTabWidth = containerFrame:GetWidth() / #tabs - 2.5
		for i, achievement in ipairs(tabs) do
			local tab
			if self.tabCache[i] then
				tab = self.tabCache[i]
				tab:Show()
			else
				tab = CreateFrame("Button", nil, self.frame, "PanelTabButtonTemplate")
				tab:SetPoint("TOPLEFT", containerFrame, "BOTTOMLEFT", 0, 4)
				tab:SetScript("OnClick", function(selTab)
					PanelTemplates_SetTab(self.frame, selTab:GetID())
					self:Fire("OnTabSelected", selTab:GetID())
				end)
				tinsert(self.tabCache, tab)
			end

			tab:SetText(achievement)
			tab:SetID(i)
		end
		PanelTemplates_SetNumTabs(self.frame, #tabs)
		PanelTemplates_SetTab(self.frame, 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
			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 = {}

	local progressText = frame:CreateFontString()
	progressText:SetFontObject("GameFontHighlight")
	progressText:SetFont(progressText:GetFont(), 14, "OUTLINE")
	progressText:SetPoint("TOPLEFT", 10, -7)
	progressText:SetPoint("TOPRIGHT", -10, -7)
	progressText:SetHeight(20)
	progressText:SetText("Loading Progress...")

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

	local searchBox = CreateFrame("EditBox", nil, frame, "SearchBoxTemplate")
	searchBox:SetPoint("TOPLEFT", 10, -31)
	searchBox:SetPoint("TOPRIGHT", radioDropdown, "TOPLEFT", -10, 0)
	searchBox:SetHeight(20)
	searchBox:HookScript("OnTextChanged", Search_OnTextChanged)

	local treeframe = CreateFrame("Frame", nil, frame, "BackdropTemplate")
	treeframe:SetPoint("TOPLEFT", searchBox, "BOTTOMLEFT", -5, 0)
	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 }
	)
	radioDropdown:SetMenuAnchor(AnchorUtil.CreateAnchor("TOPLEFT", radioDropdown, "TOPRIGHT", 0, 5))

	return AceGUI:RegisterAsWidget(widget)
end

AceGUI:RegisterWidgetType(Type, Constructor, Version)