Quantcast
---@diagnostic disable: duplicate-set-field
-- **************************************************************************
-- * TitanBag.lua
-- *
-- * By: The Titan Panel Development Team
-- **************************************************************************

-- ******************************** Constants *******************************
local _G = getfenv(0);
local TITAN_BAG_ID = "Bag";
local TITAN_BUTTON = "TitanPanel" .. TITAN_BAG_ID .. "Button"

local TITAN_BAG_THRESHOLD_TABLE = {
	Values = { 0.5, 0.75, 0.9 },
	Colors = { HIGHLIGHT_FONT_COLOR, NORMAL_FONT_COLOR, ORANGE_FONT_COLOR, RED_FONT_COLOR },
}
local L = LibStub("AceLocale-3.0"):GetLocale(TITAN_ID, true)
--local updateTable = {TITAN_BAG_ID, TITAN_PANEL_UPDATE_BUTTON};

-- ******************************** Variables *******************************
--local AceTimer = LibStub("AceTimer-3.0")

local trace = false

local MIN_BAGS = 0
local MAX_BAGS = 0
local bag_data = {} -- to hold the user bag data

-- ******************************** Functions *******************************

-- Set so Retail and Classic can run
local GetItemNow = C_Item.GetItemInfoInstant or GetItemInfoInstant

---Determine if this is a profession bag using only instant data rather than calling server
---@param slot number
---@return boolean
local function IsProfessionBagID(slot)
	-- The info needed is available using GetItemInfoInstant; only the bag slot or item id is required.
	-- A LOT of info is available but we only need class and subclass here.
	-- itemType : warcraft.wiki.gg/wiki/itemType
	local res = false
	local info, itemId, itemType, itemSubType, itemEquipLoc, itemTexture, classID, subclassID
	local inv_id = C_Container.ContainerIDToInventoryID(slot)

	if inv_id == nil then
		-- Only works on bag and bank bags NOT backpack!
		-- However the backpack is never a profession bag.
	else
		info = GetInventoryItemLink("player", inv_id)
		if info == nil then
			-- Slot likely empty, no need to process.
		else
			itemId, itemType, itemSubType, itemEquipLoc, itemTexture, classID, subclassID = GetItemNow(info)
			if classID == 1 then -- is a container / bag
				if subclassID >= 1 then
					-- profession bag of some type [2 - 10] Jan 2024 (DragonFlight / Wrath / Classic Era)
					-- OR soul bag [1]
					res = true
				else
					-- is a arrow or bullet bag; only two options
				end
			elseif classID == 6 then -- is a 'projectile' holder
				res = true
				-- is a ammo bag or quiver; only two options
			elseif classID == 11 then -- is a 'quiver'; Wrath and CE
				res = true
				-- is a ammo pouch or quiver; only two options
				-- style = subclassID + 20 -- change to get local color for name
			else
				-- not a profession bag
			end
		end
	end

	return res
end

---Tell the UI to open / close the bags
local function ToggleBags()
	if TitanGetVar(TITAN_BAG_ID, "OpenBags") then
		ToggleAllBags()
	else
		-- User has not enabled open on click
	end
end

---Collect bag info - name, slots (total, used, free), name (if available).
--- The bag name is not always available when player entering world but the required info is.
---@param id string Plugin ID
local function GetBagData(id)
	--[[
	The bag name is not always available when player entering world.
	The user may see bag name as <unknown> until an event triggers a bag check AND the name is available.
	Grabbing the total slots is available on client to determine if a bag exists and get its free / used counts.
	--]]
	-- 2024 Jan : Moved away from named based to id based. Allows name to come later from server
	-- 2024 Aug : Removed coloring of bag name to focus on counts which is the real info.

	if trace then
		TitanPluginDebug(TITAN_BAG_ID, "T GetBagData"
			.. " '" .. tostring(id) .. "'"
		)
	end
	local total_slots = 0
	local total_free = 0
	local total_used = 0
	local is_prof_bag = false
	-- calculated but not used ATM
	local prof_slots = 0
	local prof_free = 0
	local prof_used = 0

	for bag_slot = MIN_BAGS, MAX_BAGS do -- assuming 0 (Backpack) will not be a profession bag
		-- Ensure a blank structure exists.
		-- Blanking data may seem overkill but it allows the plugin to react to events without
		-- caring when they occur and it will set the bag name when it arrives AND an event occurs.
		bag_data[bag_slot] = {
			has_bag = false,
			name = "",
			max_slots = 0,
			free_slots = 0,
			used_slots = 0,
			style = "",
			color = "",
		}

		local slots = C_Container.GetContainerNumSlots(bag_slot)

		-- Check type here to set slot style properly.
		-- Profession bags are NOT included in overall free / used counts
		local bag_type = "none"
		is_prof_bag = IsProfessionBagID(bag_slot)

		-- Blizz treats 'last' slot as a reagent only slot...
		-- For our purpose, treat it as a profession bag.
		if is_prof_bag or bag_slot == 5 then
			bag_type = "profession"
		else
			bag_type = "normal"
		end
		bag_data[bag_slot].style = bag_type

		if slots > 0 then
			bag_data[bag_slot].has_bag = true

			local bag_name = (C_Container.GetBagName(bag_slot) or UNKNOWN)
			bag_data[bag_slot].name = bag_name
			bag_data[bag_slot].max_slots = slots

			local free = C_Container.GetContainerNumFreeSlots(bag_slot)
			local used = slots - free
			bag_data[bag_slot].free_slots = free
			bag_data[bag_slot].used_slots = used


			-- add to total
			if bag_data[bag_slot].style == "profession" then
				prof_slots = prof_slots + slots
				prof_free = prof_free + free
				prof_used = prof_used + used
			else
				total_slots = total_slots + slots
				total_free = total_free + free
				total_used = total_used + used
			end
		else
			bag_data[bag_slot].has_bag = false
			bag_data[bag_slot].name = NONE
		end

		if trace then
			TitanPluginDebug(TITAN_BAG_ID, "...T GetBagData"
				.. " " .. tostring(bag_slot) .. ""
				.. " ?:" .. tostring(bag_data[bag_slot].has_bag) .. ""
				.. " max: " .. tostring(bag_data[bag_slot].max_slots) .. ""
				.. " used: " .. tostring(bag_data[bag_slot].used_slots) .. ""
				.. " free: " .. tostring(bag_data[bag_slot].free_slots) .. ""
				.. " type: " .. tostring(bag_data[bag_slot].style) .. ""
				.. " prof: " .. tostring(is_prof_bag) .. ""
				.. " '" .. tostring(bag_data[bag_slot].name) .. "'"
			)
		end
	end

	-- Normal bags
	bag_data.total_slots = total_slots
	bag_data.total_free = total_free
	bag_data.total_used = total_used

	-- Profession / reagent bags
	bag_data.prof_slots = prof_slots
	bag_data.prof_free = prof_free
	bag_data.prof_used = prof_used
end

---plugin Handle registered events
---@param self Button
---@param event string
---@param ... any
local function OnEvent(self, event, ...)
	if event == "PLAYER_ENTERING_WORLD" then
		-- Leave in case future code is needed...
	elseif event == "BAG_UPDATE" then
		-- update the plugin text
		TitanPanelButton_UpdateButton(TITAN_BAG_ID);
	elseif event == "BAG_CONTAINER_UPDATE" then
		-- 2024 Aug : Added as additional check if user swaps bags; may not be required
		-- update the plugin text
		TitanPanelButton_UpdateButton(TITAN_BAG_ID);
	end

	if trace then
		TitanPluginDebug(TITAN_BAG_ID, "_OnEvent"
			.. " " .. tostring(event) .. ""
		)
	end
end

---Opens all bags on a LeftClick
---@param self Button
---@param button string
local function OnClick(self, button)
	if (button == "LeftButton") then
		ToggleBags();
	end
end

---Generate the plugin button text
---@param id string
---@return string
---@return string
local function GetButtonText(id)
	GetBagData(id)

	local bagText = ""
	if TitanGetVar(TITAN_BAG_ID, "ShowUsedSlots") then
		bagText = format(L["TITAN_BAG_FORMAT"], bag_data.total_used, bag_data.total_slots);
	else
		bagText = format(L["TITAN_BAG_FORMAT"], bag_data.total_free, bag_data.total_slots);
	end

	local bagRichText = ""
	if (TitanGetVar(TITAN_BAG_ID, "ShowColoredText")) then
		local color = ""
		color = TitanUtils_GetThresholdColor(TITAN_BAG_THRESHOLD_TABLE, bag_data.total_used / bag_data.total_slots);
		bagRichText = TitanUtils_GetColoredText(bagText, color);
	else
		bagRichText = TitanUtils_GetHighlightText(bagText);
	end

	bagRichText = bagRichText
	--..bagRichTextProf[1]..bagRichTextProf[2]..bagRichTextProf[3]..bagRichTextProf[4]..bagRichTextProf[5];

	if trace then
		TitanPluginDebug(TITAN_BAG_ID, "T GetBagData"
			.. " '" .. tostring(bagRichText) .. "'"
		)
	end
	return L["TITAN_BAG_BUTTON_LABEL"], bagRichText
end

---Determine the color based on percentage
---@param text string
---@param show_color boolean
---@param numerator number
---@param denom number
---@return string
local function ThresholdColor(text, show_color, numerator, denom)
	local res = ""
	local color = ""
	if show_color then
		if denom == 0 then
			color = TitanUtils_GetThresholdColor(TITAN_BAG_THRESHOLD_TABLE, 1);
		else
			color = TitanUtils_GetThresholdColor(TITAN_BAG_THRESHOLD_TABLE, numerator / denom);
		end
		res = TitanUtils_GetColoredText(text, color);
	else
		-- use without color
		res = TitanUtils_GetHighlightText(text);
	end

	return res
end

---Generate tooltip text
---@return string
local function GetTooltipText()
	-- Normal shows free / used of total per user options.
	-- Detailed shows list bags with profession bag counts in gray - not counted.
	-- Hint shows if user selects open bags on left click.
	--#region-- 2024 Aug (8.1.0) : With the addition of new 'reagent' slot, we dropped coloring & counting profession bags.
	local returnstring = "";
	local show_color = TitanGetVar(TITAN_BAG_ID, "ShowColoredText")

	-- Collect names and x / y numbers, color numbers if user requested
	if TitanGetVar(TITAN_BAG_ID, "ShowDetailedInfo") then
		returnstring = "\n";
		if TitanGetVar(TITAN_BAG_ID, "ShowUsedSlots") then
			returnstring = returnstring .. TitanUtils_GetNormalText(L["TITAN_BAG_MENU_TEXT"])
				.. ":\t" .. TitanUtils_GetNormalText(L["TITAN_BAG_USED_SLOTS"]) .. ":\n";
		else
			returnstring = returnstring .. TitanUtils_GetNormalText(L["TITAN_BAG_MENU_TEXT"])
				.. ":\t" .. TitanUtils_GetNormalText(L["TITAN_BAG_FREE_SLOTS"]) .. ":\n";
		end

		for bag = MIN_BAGS, MAX_BAGS do
			local bagText = ""
			local bagRichText

			if bag_data[bag] then
				if bag_data[bag].has_bag then
					-- Format the x / y slots per user options
					if (TitanGetVar(TITAN_BAG_ID, "ShowUsedSlots")) then
						bagText = format(L["TITAN_BAG_FORMAT"], bag_data[bag].used_slots, bag_data[bag].max_slots);
					else
						bagText = format(L["TITAN_BAG_FORMAT"], bag_data[bag].free_slots, bag_data[bag].max_slots);
					end
					-- Format x / y per user options
					if bag_data[bag].style == "profession" then
						bagRichText = TitanUtils_GetGrayText(bagText)
					else
						bagRichText = ThresholdColor(bagText, show_color, bag_data[bag].used_slots, bag_data[bag].max_slots)
					end
				else
					bagRichText = ""
				end
				-- Format bag name as 'normal' 2024 Aug
				local name_text = TitanUtils_GetNormalText(bag_data[bag].name)
				returnstring = returnstring .. name_text .. "\t" .. bagRichText .. "\n";
			else
				--Silent error - should never get here...
			end
		end
		returnstring = returnstring .. "------\t" .. "---\n";
	end

	-- Always show free / used of max slots to user
	local xofy = ""
	local slots = ""
	if TitanGetVar(TITAN_BAG_ID, "ShowUsedSlots") then
		xofy = "" .. tostring(bag_data.total_used) .. "/" .. tostring(bag_data.total_slots)
		xofy = ThresholdColor(xofy, show_color, bag_data.total_used, bag_data.total_slots)
		slots = L["TITAN_BAG_USED_SLOTS"]
	else
		xofy = "" .. tostring(bag_data.total_free) .. "/" .. tostring(bag_data.total_slots)
		xofy = ThresholdColor(xofy, show_color, bag_data.total_free, bag_data.total_slots)
		slots = L["TITAN_BAG_FREE_SLOTS"]
	end
	returnstring = returnstring .. TitanUtils_GetNormalText(slots) .. ":\t" .. xofy .. "\n"

	-- Add Hint if user wants to open bags on left click.
	if TitanGetVar(TITAN_BAG_ID, "OpenBags") then
		returnstring = returnstring .. "\n" .. TitanUtils_GetGreenText(L["TITAN_BAG_TOOLTIP_HINTS"])
	else
		-- nop
	end
	return returnstring
end

---Generate and display rightclick menu options for user.
local function PrepareBagMenu()
	local info
	-- level 1
	TitanPanelRightClickMenu_AddTitle(TitanPlugins[TITAN_BAG_ID].menuText);

	info = {};
	info.text = L["TITAN_BAG_MENU_SHOW_USED_SLOTS"];
	info.func = function()
		TitanSetVar(TITAN_BAG_ID, "ShowUsedSlots", 1);
		TitanPanelButton_UpdateButton(TITAN_BAG_ID);
		end
	info.checked = TitanGetVar(TITAN_BAG_ID, "ShowUsedSlots");
	TitanPanelRightClickMenu_AddButton(info, TitanPanelRightClickMenu_GetDropdownLevel());

	info = {};
	info.text = L["TITAN_BAG_MENU_SHOW_AVAILABLE_SLOTS"];
	info.func = function()
		TitanSetVar(TITAN_BAG_ID, "ShowUsedSlots", nil);
		TitanPanelButton_UpdateButton(TITAN_BAG_ID);
		end
	info.checked = TitanUtils_Toggle(TitanGetVar(TITAN_BAG_ID, "ShowUsedSlots"));
	TitanPanelRightClickMenu_AddButton(info, TitanPanelRightClickMenu_GetDropdownLevel());

	info = {};
	info.text = L["TITAN_BAG_MENU_SHOW_DETAILED"];
	info.func = function()
		TitanToggleVar(TITAN_BAG_ID, "ShowDetailedInfo");
	end
	info.checked = TitanGetVar(TITAN_BAG_ID, "ShowDetailedInfo");
	TitanPanelRightClickMenu_AddButton(info, TitanPanelRightClickMenu_GetDropdownLevel());

	info = {};
	info.text = L["TITAN_BAG_MENU_OPEN_BAGS"]
	info.func = function()
		TitanToggleVar(TITAN_BAG_ID, "OpenBags")
	end
	info.checked = TitanGetVar(TITAN_BAG_ID, "OpenBags");
	TitanPanelRightClickMenu_AddButton(info, TitanPanelRightClickMenu_GetDropdownLevel());

	TitanPanelRightClickMenu_AddSpacer();

	TitanPanelRightClickMenu_AddControlVars(TITAN_BAG_ID)
end

---plugin Registers the plugin and simple init
---@param self Button
local function OnLoad(self)
	local notes = ""
		.. "Adds bag and free slot information to Titan Panel.\n"
		.. "- Open bags should work... Retail taint fixed Apr 2024 (10.2.7).\n"
		.. "- Professions counts moved to tooltip only : Aug 2024 (Titan 8.1.0).\n"
	self.registry = {
		id = TITAN_BAG_ID,
		category = "Built-ins",
		version = TITAN_VERSION,
		menuText = L["TITAN_BAG_MENU_TEXT"],
		menuTextFunction = PrepareBagMenu,
		buttonTextFunction = GetButtonText,
		tooltipTitle = L["TITAN_BAG_TOOLTIP"],
		tooltipTextFunction = GetTooltipText,
		icon = "Interface\\AddOns\\TitanBag\\TitanBag",
		iconWidth = 16,
		notes = notes,
		controlVariables = {
			ShowIcon = true,
			ShowLabelText = true,
			ShowColoredText = true,
			DisplayOnRightSide = true,
		},
		savedVariables = {
			ShowUsedSlots = 1,
			ShowDetailedInfo = false,
			ShowIcon = 1,
			ShowLabelText = 1,
			ShowColoredText = 1,
			DisplayOnRightSide = false,
			OpenBags = true,
		}
	};

	-- As of Apr 2024 (10.2.7) the taint on opening bags in Retail is fixed.

	-- Reagent bag slot added end of DragonFlight for War Within expansion.
	if NUM_TOTAL_EQUIPPED_BAG_SLOTS == nil then -- NOT Retail as of DragonFlight WHY BLIZZ!?
		MAX_BAGS = Constants.InventoryConstants.NumBagSlots
	else                                     -- Classic API
		MAX_BAGS = NUM_TOTAL_EQUIPPED_BAG_SLOTS
	end

	self:RegisterEvent("PLAYER_ENTERING_WORLD");
end

---Prep and update plugin button here to minimize resources.
---@param self Button
local function OnShow(self)
	-- Register for bag updates and update the plugin text
	self:RegisterEvent("BAG_UPDATE")
	self:RegisterEvent("BAG_CONTAINER_UPDATE")
	TitanPanelButton_UpdateButton(TITAN_BAG_ID);
end

---Shutdown plugin button here to minimize resources.
---@param self Button
local function OnHide(self)
	self:UnregisterEvent("BAG_UPDATE")
	self:UnregisterEvent("BAG_CONTAINER_UPDATE")
end

---Create needed plugin frames
local function Create_Frames()
	if _G[TITAN_BUTTON] then
		return -- if already created
	end

	-- general container frame
	local f = CreateFrame("Frame", nil, UIParent)
	--	f:Hide()

	-- Titan plugin button
	local window = CreateFrame("Button", TITAN_BUTTON, f, "TitanPanelComboTemplate")
	window:SetFrameStrata("FULLSCREEN")
	-- Using SetScript("OnLoad",   does not work
	OnLoad(window);
	--	TitanPanelButton_OnLoad(window); -- Titan XML template calls this...

	window:SetScript("OnShow", function(self)
		OnShow(self);
		TitanPanelButton_OnShow(self);
	end)
	window:SetScript("OnHide", function(self)
		OnHide(self)
	end)
	window:SetScript("OnEvent", function(self, event, ...)
		OnEvent(self, event, ...)
	end)
	window:SetScript("OnClick", function(self, button)
		OnClick(self, button);
		TitanPanelButton_OnClick(self, button);
	end)
end

Create_Frames() -- do the work

--[[
TitanDebug("T isP 0:"
	.." "..tostring(slot)..""
	.." "..tostring(itemId)..""
	.." '"..tostring(itemType).."'"
	.." '"..tostring(itemSubType).."'"
	.." "..tostring(itemEquipLoc)..""
	.." '"..tostring(itemTexture).."'"
	.." "..tostring(classID)..""
	.." "..tostring(subclassID)..""
	)
--]]