
local _, app = ...
local L = app.L

-- Globals
local setmetatable, rawget, select, tostring, ipairs, pairs, tinsert, tonumber
	= setmetatable, rawget, select, tostring, ipairs, pairs, tinsert, tonumber

-- WoW API Cache
local GetAchievementNumCriteria,GetAchievementInfo,GetAchievementLink,GetAchievementCriteriaInfo,GetAchievementCategory,GetAchievementCriteriaInfoByID
	= GetAchievementNumCriteria,GetAchievementInfo,GetAchievementLink,GetAchievementCriteriaInfo,GetAchievementCategory,GetAchievementCriteriaInfoByID
local GetItemInfo = app.WOWAPI.GetItemInfo

-- Module
local DelayedCallback = app.CallbackHandlers.DelayedCallback;
local IsRetrieving = app.Modules.RetrievingData.IsRetrieving;
local SearchForObject = app.SearchForObject

-- App

local CollectionCache
local CollectionCacheFunctions = {
	MaxAchievementID = function()
		local maxid, achID = 0, 0;
		for id,_ in pairs(app.GetRawFieldContainer("achievementID")) do
			achID = tonumber(id) or id
			if achID > maxid then maxid = achID; end
		end
		 -- go beyond ATT max known just in case we haven't sourced the highest achievementID
		return maxid + 100
	end,
	RealAchievementIDs = function()
		local achs = {}
		local maxid = CollectionCache.MaxAchievementID
		local exists
		for id=1,maxid do
			exists = GetAchievementInfo(id)
			if exists then
				achs[#achs + 1] = id
			end
		end
		return achs
	end
}
CollectionCache = setmetatable({}, { __index = function(t, key)
	local func = CollectionCacheFunctions[key]
	if func then
		-- app.PrintDebug("Achievement.CollectionCache",key)
		local val = func()
		-- app.PrintDebugPrior("Achievement.CollectionCache.Done",val,val and type(val) == "table" and #val)
		t[key] = val
		return val
	end
end})
local QuickAchievementCache = setmetatable({}, { __index = function(t,key)
	if not key then return end
	local achObj = SearchForObject("achievementID", key, "key")
	t[key] = achObj
	return achObj
end})

-- Achievement Lib
do
	local KEY, CACHE = "achievementID", "Achievements"
	local CLASSNAME = "Achievement"
	local GetStatistic
		= GetStatistic

	local cache = app.CreateCache(KEY);
	local FLAG_AccountWide = ACHIEVEMENT_FLAGS_ACCOUNT
	local FlagsUtil_IsSet,string_len,string_sub
		= FlagsUtil.IsSet,string.len,string.sub
	local Colorize = app.Modules.Color.Colorize
	local function CacheInfo(t, field)
		local _t, id = cache.GetCached(t);
		--local IDNumber, Name, Points, Completed, Month, Day, Year, Description, Flags, Image, RewardText, isGuildAch = GetAchievementInfo(t[KEY]);
		local _, name, _, _, _, _, _, _, flags, icon = GetAchievementInfo(id);
		local silentLink = GetAchievementLink(id)
		if not silentLink then
			app.PrintDebug(Colorize("Achievement with no Link",app.Colors.ChatLinkError),id)
			silentLink = name or "achievementID:"..id
		end
		_t.silentLink = silentLink
		local accountWide = FlagsUtil_IsSet(tonumber(flags) or 0, FLAG_AccountWide)
		_t.accountWide = accountWide
		if accountWide then
			local len = string_len(silentLink)
			_t.text = Colorize(string_sub(silentLink,11,len - 2),app.Colors.Account)
		else
			_t.text = silentLink
		end
		_t.name = name or ("Achievement #"..id);
		_t.icon = icon or QUESTION_MARK_ICON;
		if field then return _t[field]; end
	end
	local InvalidStatistics = setmetatable({
		["0"] = 1,
		["1"] = 1,
		["2"] = 1,
		["3"] = 1,
		["4"] = 1,
		["5"] = 1,
		["6"] = 1,
		["7"] = 1,
		["8"] = 1,
		["9"] = 1,
		[""] = 1,
	}, { __index=function(t,key)
		if not key or key:match("%W") or not key:match(" %/ ") then return 1 end
	end})
	local onTooltipForAchievement = function(t, tooltipInfo)
		local achievementID = t.achievementID;
		if achievementID and IsShiftKeyDown() then
			local criteriaDatas,criteriaDatasByUID = {}, {};
			for criteriaID=1,99999,1 do
				local criteriaString, criteriaType, completed, _, _, _, _, assetID, quantityString, criteriaUID = GetAchievementCriteriaInfoByID(achievementID, criteriaID);
				if criteriaString and criteriaUID then
					criteriaDatasByUID[criteriaUID] = true;
					tinsert(criteriaDatas, {
						" [" .. criteriaUID .. "]: " .. tostring(criteriaString),
						"(" .. tostring(assetID) .. " @ " .. tostring(criteriaType) .. ") " .. tostring(quantityString) .. " " .. app.GetCompletionIcon(completed)
					});
				end
			end
			local totalCriteria = GetAchievementNumCriteria(achievementID) or 0;
			if totalCriteria > 0 then
				for criteriaIndex=1,totalCriteria,1 do
					---@diagnostic disable-next-line: redundant-parameter
					local criteriaString, criteriaType, completed, _, _, _, _, assetID, quantityString, criteriaUID = GetAchievementCriteriaInfo(achievementID, criteriaIndex, true);
					if criteriaString and (not criteriaDatasByUID[criteriaUID] or criteriaUID == 0) then
						tinsert(criteriaDatas, {
							" [" .. criteriaUID .. " @ Index: " .. criteriaIndex .. "]: " .. tostring(criteriaString),
							"(" .. tostring(assetID) .. " @ " .. tostring(criteriaType) .. ") " .. tostring(quantityString) .. " " .. app.GetCompletionIcon(completed)
						});
					end
				end
			end
			if #criteriaDatas > 0 then
				tinsert(tooltipInfo, { left = " " });
				tinsert(tooltipInfo, {
					left = "Total Criteria",
					right = tostring(#criteriaDatas),
					r = 0.8, g = 0.8, b = 1
				});
				for i,criteriaData in ipairs(criteriaDatas) do
					tinsert(tooltipInfo, {
						left = criteriaData[1],
						right = criteriaData[2],
						r = 1, g = 1, b = 1
					});
				end
			end
		end
	end
	-- This was used to update information about achievement progress following Pet Battles
	-- This unfortunately triggers all the time and rarely actually represents useful Achievement changes
	-- TODO: Think of another way to represent Achievement changes post Pet Battles
	-- local function OnUpdateWindows()
	-- 	app.HandleEvent("OnUpdateWindows")
	-- end
	-- local function DelayedOnUpdateWindows()
	-- 	AfterCombatOrDelayedCallback(OnUpdateWindows, 1)
	-- end
	-- app.AddEventRegistration("RECEIVED_ACHIEVEMENT_LIST", DelayedOnUpdateWindows);
	app.CreateAchievement = app.CreateClass(CLASSNAME, KEY, {
		CACHE = function() return CACHE end,
		silentLink = function(t)
			return cache.GetCachedField(t, "silentLink", CacheInfo);
		end,
		text = function(t)
			return cache.GetCachedField(t, "text", CacheInfo);
		end,
		name = function(t)
			return cache.GetCachedField(t, "name", CacheInfo);
		end,
		icon = function(t)
			return cache.GetCachedField(t, "icon", CacheInfo);
		end,
		accountWide = function(t)
			return cache.GetCachedField(t, "accountWide", CacheInfo);
		end,
		collectible = function(t) return app.Settings.Collectibles[CACHE] end,
		collected = function(t)
			return app.TypicalCharacterCollected(CACHE, t[KEY])
		end,
		saved = function(t)
			local id = t[KEY];
			-- character collected
			if app.IsCached(CACHE, id) then return 1; end
		end,
		parentCategoryID = function(t)
			return GetAchievementCategory(t[KEY]) or -1;
		end,
		statistic = function(t)
			if GetAchievementNumCriteria(t[KEY]) == 1 then
				local quantity, reqQuantity = select(4, GetAchievementCriteriaInfo(t[KEY], 1));
				if quantity and reqQuantity and reqQuantity > 1 then
					return tostring(quantity) .. " / " .. tostring(reqQuantity);
				end
			end
			---@diagnostic disable-next-line: missing-parameter
			local statistic = GetStatistic(t[KEY]);
			if InvalidStatistics[statistic] then return end
			return statistic
		end,
		sortProgress = function(t)
			if t.collected then
				return 1;
			end
			-- only calculate achievement progress using achievements where the single criteria is the 'progress bar'
			if GetAchievementNumCriteria(t[KEY]) == 1 then
				local quantity, reqQuantity = select(4, GetAchievementCriteriaInfo(t[KEY], 1));
				if quantity and reqQuantity and reqQuantity > 1 then
					-- print("ach-prog",t.achievementID,quantity,reqQuantity);
					return (quantity / reqQuantity);
				end
			end
			return 0;
		end,
		back = function(t)
			return t.sourceIgnored and 0.5 or 0;
		end,
		OnTooltip = function(t)
			return onTooltipForAchievement;
		end,
	})

	app.AddEventHandler("OnRefreshCollections", function()
		local me, completed
		-- app.PrintDebug("OnRefreshCollections.Achievement")
		local mine, acct, none = {}, {}, {}
		for _,id in ipairs(CollectionCache.RealAchievementIDs) do
			completed, _, _, _, _, _, _, _, _, me = select(4, GetAchievementInfo(id))
			if me then
				mine[id] = true
			elseif completed then	-- any character has completed it, we can cache for account directly
				acct[id] = true
			else
				none[id] = true
			end
		end
		-- Character Cache
		app.SetBatchCached(CACHE, mine, 1)
		app.SetBatchCached(CACHE, none)
		-- Account Cache (removals handled by Sync)
		app.SetBatchAccountCached(CACHE, mine, 1)
		app.SetBatchAccountCached(CACHE, acct, 1)
		-- app.PrintDebugPrior("OnRefreshCollections.Achievement")
	end);
	app.AddEventHandler("OnSavedVariablesAvailable", function(currentCharacter, accountWideData)
		if not currentCharacter[CACHE] then currentCharacter[CACHE] = {} end
		if not accountWideData[CACHE] then accountWideData[CACHE] = {} end
	end);
	app.AddEventRegistration("ACHIEVEMENT_EARNED", function(id)
		local state = select(13, GetAchievementInfo(tonumber(id)))
		if state then
			app.SetCached(CACHE, id, 1)
			app.SetAccountCached(CACHE, id, 1)
			app.UpdateRawID(KEY, id);
		end
	end);
	app.AddSimpleCollectibleSwap(CLASSNAME, CACHE)

	-- Adds ATT information about the list of Achievements into the provided tooltip
	local function AddAchievementInfoToTooltip(info, achievements, reference)
		if achievements then
			local text
			for _,ach in ipairs(achievements) do
				text = ach.text;
				if not text then
					text = RETRIEVING_DATA;
					reference.working = true;
				end
				text = app.GetCompletionIcon(ach.saved) .. " [" .. ach.achievementID .. "] " .. text;
				if ach.isGuild then text = text .. " (" .. GUILD .. ")"; end
				info[#info + 1] = {
					left = text
				}
			end
		end
	end
	-- Information Types
	app.AddEventHandler("OnLoad", function()
		app.Settings.CreateInformationType("Achievement_CriteriaFor", {
			text = "Achievement_CriteriaFor",
			priority = 1.5, HideCheckBox = true, ForceActive = true,
			Process = function(t, reference, tooltipInfo)
				if reference.criteriaID and reference.achievementID then
					local achievement = QuickAchievementCache[reference.achievementID]
					tinsert(tooltipInfo, {
						left = L.CRITERIA_FOR,
						right = achievement.text or GetAchievementLink(reference.achievementID),
					});
				end
			end
		})
		app.Settings.CreateInformationType("Achievement_Statistic", {
			text = "Achievement_Statistic",
			HideCheckBox = true, ForceActive = true,
			Process = function(t, reference, tooltipInfo)
				-- achievement progress. If it has a measurable statistic, show it under the achievement description
				if reference.achievementID then
					if reference.statistic then
						tinsert(tooltipInfo, {
							left = L.PROGRESS,
							right = reference.statistic,
						});
					end
				end
			end
		})
		app.Settings.CreateInformationType("sourceAchievements", {
			text = "Achievement_Requirements",
			HideCheckBox = true, ForceActive = true, priority = 9500,
			Process = function(t, reference, tooltipInfo)
				if not reference.sourceAchievements then return end
				local isDebugMode = app.MODE_DEBUG
				if not isDebugMode and reference.collected then return end

				local bestMatch, sas
				local prereqs = {}
				for i,sourceAchievementID in ipairs(reference.sourceAchievements) do
					if sourceAchievementID > 0 and (isDebugMode or not app.IsAccountCached("Achievements", sourceAchievementID)) then
						sas = SearchForObject("achievementID", sourceAchievementID, "field", true)
						if #sas > 0 then
							bestMatch = nil;
							for j,sa in ipairs(sas) do
								if sa.achievementID == sourceAchievementID then
									if isDebugMode or (not sa.saved and app.GroupFilter(sa)) then
										bestMatch = sa;
									end
								end
							end
							if bestMatch then
								prereqs[#prereqs + 1] = bestMatch
							end
						else
							prereqs[#prereqs + 1] = app.CreateAchievement(sourceAchievementID)
						end
					end
				end
				if prereqs and #prereqs > 0 then
					tooltipInfo[#tooltipInfo + 1] = {
						left = L.REQUIRED_ACHIEVEMENTS
					}
					AddAchievementInfoToTooltip(tooltipInfo, prereqs, reference);
				end
			end
		})
	end)
end

local function BuildSourceAchievements(group)
	if not group.sourceAchievements then return end

	local sas = {}
	local sourceGroup = app.CreateRawText(L.REQUIRED_ACHIEVEMENTS, {
		description = L.REQUIRED_ACHIEVEMENTS_DESC,
		icon = 135950,
		OnUpdate = app.AlwaysShowUpdate,
		OnClick = app.UI.OnClick.IgnoreRightClick,
		sourceIgnored = true,
		skipFull = true,
		SortPriority = -2.9,
		g = sas,
	})
	for i,achID in ipairs(group.sourceAchievements) do
		app.NestObject(sourceGroup, QuickAchievementCache[achID] or app.CreateAchievement(achID), true)
	end
	app.NestObject(group, sourceGroup, nil, 1)
end
app.AddEventHandler("OnNewPopoutGroup", BuildSourceAchievements)

-- Achievement Criteria Lib
do
	-- Returns expected criteria data for either criteriaIndex or criteriaID
	local function GetCriteriaInfo(t, achievementID)
		-- prioritize the correct id
		local critUID = t.uid or t.criteriaID
		local critID = t.id or critUID
		achievementID = achievementID or t.achievementID
		if not achievementID or not critID then return end

		local criteriaString, criteriaType, completed, quantity, reqQuantity, charName, flags, assetID, quantityString, criteriaID, eligible
			= GetAchievementCriteriaInfoByID(achievementID, critUID)
		-- criteriaType will exist even when criteriaString is empty, so don't need to check retrieving and stuff
		if criteriaType then
			return criteriaString, criteriaType, completed, quantity, reqQuantity, charName, flags, assetID, quantityString, criteriaID, eligible
		end
		if critID <= GetAchievementNumCriteria(achievementID) then
			---@diagnostic disable-next-line: redundant-parameter
			return GetAchievementCriteriaInfo(achievementID, critID, true)
		end
	end

	-- Criteria field values which will use the value of the respective Achievement instead
	local UseParentAchievementValueKeys = {
		"c", "classID", "races", "r", "u", "e", "sr", "pb", "pvp", "requireSkill", "icon"
	}
	local function GetParentAchievementInfo(t, key, _t)
		-- if the Achievement data was already cached, but the criteria is still getting here
		-- then the Achievement's data field was nil
		if t._cached then return end
		local id = t.achievementID
		local achievement = QuickAchievementCache[id]
		if achievement then
			-- copy parent Achievement field re-mappings
			for _,key in ipairs(UseParentAchievementValueKeys) do
				_t[key] = achievement[key]
			end
			t._cached = true;
			return rawget(_t, key);
		end
		DelayedCallback(app.report, 1, "Missing Referenced Achievement!",id);
	end
	local function default_name(t)
		if t.link then return t.link; end
		local name
		local achievementID = t.achievementID
		if achievementID then
			local criteriaID = t.criteriaID;
			if criteriaID then
				-- typical criteria name lookup
				name = GetCriteriaInfo(t, achievementID)
				if not IsRetrieving(name) then return name; end

				-- app.PrintDebug("fallback crit name",achievementID,criteriaID,t.uid,t.id)
				-- criteria nested under a parent of a known Thing
				local parent = t.parent
				if parent then
					local parentKey = parent.key
					if parentKey and app.ThingKeys[parentKey] and (parentKey ~= "achievementID" or parent[parentKey] ~= achievementID) then
						name = parent.name
						if not IsRetrieving(name) and not name:find("Quest #") then return name; end
					end
				end

				-- criteria with provider data
				name = app.GetNameFromProviders(t)
				if not IsRetrieving(name) then return name end

				-- criteria with sourceQuests data
				local sourceQuests = t.sourceQuests;
				if sourceQuests then
					for k,id in ipairs(sourceQuests) do
						name = app.GetQuestName(id);
						t.__questname = name
						if not IsRetrieving(name) and not name:find("Quest #") then return name; end
					end
					-- app.PrintDebug("criteria sq no name",achievementID,t.criteriaID,rawget(t,"name"))
					return
				end

				-- criteria with spellID (TODO)

				-- criteria fallback to base achievement name
				name = "Criteria: "..(select(2, GetAchievementInfo(achievementID)) or "#"..criteriaID)
			end
		end
		app.PrintDebug("failed to retrieve criteria name",achievementID,t.criteriaID,name)
		if not t.CanRetry then
			return name or UNKNOWN
		end
	end
	local cache = app.CreateCache("hash", "Criteria")
	cache.DefaultFunctions.saved = function(t)
		local saved = select(3, GetCriteriaInfo(t))
		-- only cache true values
		if saved then return saved end
	end
	local criteriaFields = {
		achievementID = function(t)
			local achievementID = t.achID
			t.achievementID = achievementID;
			return achievementID;
		end,
		name = function(t)
			return cache.GetCachedField(t, "name", default_name) or t.__questname
		end,
		link = function(t)
			if t.itemID then
				local _, link, _, _, _, _, _, _, _, icon = GetItemInfo(t.itemID);
				if link then
					t.text = link;
					t.link = link;
					t.icon = icon;
					return link;
				end
			end
		end,
		RefreshCollectionOnly = true,
		collectible = function(t) return app.Settings.Collectibles.Achievements end,
		collected = function(t)
			-- completion based on achievement is faster check
			return app.TypicalCharacterCollected("Achievements", t.achievementID)
			-- otherwise lookup character saved criteria
				or (t.saved and 1)
		end,
		trackable = app.ReturnTrue,
		saved = function(t)
			return cache.GetCachedField(t, "saved")
		end,
	};
	-- apply parent Achievement field re-mappings
	for _,key in ipairs(UseParentAchievementValueKeys) do
		criteriaFields[key] = function(t)
			return cache.GetCachedField(t, key, GetParentAchievementInfo)
		end
	end
	app.CreateAchievementCriteria = app.CreateClass("AchievementCriteria", "criteriaID", criteriaFields)
	app.AddSimpleCollectibleSwap("AchievementCriteria", "Achievements")
end