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

local Bags = {
	Buttons = {},
	BagParents = {},
	SectionLabels = {},
	PendingRefresh = false,
	Sorting = false,
	SortGeneration = 0,
}
SlickUI.Bags = Bags

local BAG_FIRST = 0
local BAG_LAST = 5
local REAGENT_BAG = 5
local CATEGORY_ORDER = {
	"New Items", "Quest", "Consumables", "Equipment", "Trade Goods",
	"Gems", "Recipes", "Containers", "Battle Pets", "Junk", "Miscellaneous", "Empty Slots",
}

local CATEGORY_BY_CLASS = {
	[0] = "Consumables",
	[1] = "Containers",
	[2] = "Equipment",
	[3] = "Gems",
	[4] = "Equipment",
	[7] = "Trade Goods",
	[9] = "Recipes",
	[12] = "Quest",
	[17] = "Battle Pets",
	[19] = "Trade Goods",
}

local function GetItemInfo(itemID)
	if C_Item and C_Item.GetItemInfo then
		return C_Item.GetItemInfo(itemID)
	end
	return _G.GetItemInfo(itemID)
end

local function IsNewItem(bagID, slotID)
	return C_NewItems and C_NewItems.IsNewItem and C_NewItems.IsNewItem(bagID, slotID) == true
end

local function HideBlizzardBags()
	if ContainerFrameCombinedBags then ContainerFrameCombinedBags:Hide() end
	for index = 1, 13 do
		local frame = _G["ContainerFrame" .. index]
		if frame then frame:Hide() end
	end
end

local function CallBlizzardSecurely(callback, ...)
	if type(callback) ~= "function" then return end
	if type(securecallfunction) == "function" then
		pcall(securecallfunction, callback, ...)
	else
		pcall(callback, ...)
	end
end

local function GetCategory(bagID, slotID, itemID, quality)
	if not itemID then return "Empty Slots" end
	if IsNewItem(bagID, slotID) then return "New Items" end
	if quality == 0 then return "Junk" end
	local _, _, _, _, _, _, _, _, _, _, _, classID = GetItemInfo(itemID)
	return CATEGORY_BY_CLASS[classID] or "Miscellaneous"
end

local function ConfigureNativeButton(button, bagID, slotID)
	if type(bagID) ~= "number" or type(slotID) ~= "number" then return false end
	button.BagID = bagID
	button.SlotID = slotID
	local parent = button:GetParent()
	if parent and type(parent.SetID) == "function" then parent:SetID(bagID) end
	button:SetID(slotID)
	if type(button.SetBagID) == "function" then
		pcall(button.SetBagID, button, bagID)
	end
	if type(button.Update) == "function" then
		CallBlizzardSecurely(button.Update, button)
	elseif type(ContainerFrameItemButton_Update) == "function" then
		CallBlizzardSecurely(ContainerFrameItemButton_Update, button)
	end
	return true
end

local function GetButtonRegion(button, fieldName)
	local region = button[fieldName]
	if not region and button:GetName() then
		region = _G[button:GetName() .. fieldName]
		button[fieldName] = region
	end
	return region
end

local function RefreshButtonVisuals(button, bagID, slotID)
	local info = C_Container.GetContainerItemInfo(bagID, slotID)
	local icon = GetButtonRegion(button, "IconTexture") or button.icon
	local count = GetButtonRegion(button, "Count") or button.Count
	local cooldown = GetButtonRegion(button, "Cooldown") or button.Cooldown
	local newItemTexture = GetButtonRegion(button, "NewItemTexture") or button.NewItemTexture
	local battlepayTexture = GetButtonRegion(button, "BattlepayItemTexture") or button.BattlepayItemTexture
	local flashTexture = GetButtonRegion(button, "flash") or button.flash

	if icon then
		icon:SetTexture(info and info.iconFileID or nil)
		icon:SetTexCoord(0, 1, 0, 1)
		icon:SetShown(info ~= nil)
	end
	if count then
		local stackCount = info and info.stackCount or 0
		count:SetText(stackCount > 1 and stackCount or "")
		count:SetShown(stackCount > 1)
	end
	local isNew = info ~= nil and IsNewItem(bagID, slotID)
	if isNew and type(button.UpdateNewItem) == "function" then
		pcall(button.UpdateNewItem, button, info.quality)
	else
		if button.flashAnim and button.flashAnim:IsPlaying() then button.flashAnim:Stop() end
		if button.newitemglowAnim and button.newitemglowAnim:IsPlaying() then button.newitemglowAnim:Stop() end
		if flashTexture then flashTexture:SetAlpha(0); flashTexture:Hide() end
		if newItemTexture then newItemTexture:SetAlpha(0); newItemTexture:Hide() end
		if battlepayTexture then battlepayTexture:Hide() end
	end
	if type(SetItemButtonDesaturated) == "function" then
		pcall(SetItemButtonDesaturated, button, info and info.isLocked or false)
	end
	if cooldown and type(CooldownFrame_Set) == "function" then
		local start, duration, enable, modRate = C_Container.GetContainerItemCooldown(bagID, slotID)
		pcall(CooldownFrame_Set, cooldown, start or 0, duration or 0, enable or 0, false, modRate or 1)
	end
end

function Bags:GetButton(bagID, slotID)
	local key = bagID .. ":" .. slotID
	local button = self.Buttons[key]
	if button then
		button.BagID = bagID
		button.SlotID = slotID
		return button
	end

	local parent = self.BagParents[bagID]
	if not parent then
		parent = CreateFrame("Frame", nil, self.Content)
		parent:SetAllPoints(self.Content)
		parent:SetID(bagID)
		self.BagParents[bagID] = parent
	end

	local name = "SlickUIBagItem" .. bagID .. "Slot" .. slotID
	button = CreateFrame("ItemButton", name, parent, "ContainerFrameItemButtonTemplate")
	button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
	button:RegisterForDrag("LeftButton")
	button.BagID = bagID
	button.SlotID = slotID
	self.Buttons[key] = button
	return button
end

function Bags:GetSectionLabel(index)
	local label = self.SectionLabels[index]
	if label then return label end
	label = Theme:CreateText(self.Content, 12)
	label:SetTextColor(unpack(Theme.Colors.goldText))
	label:SetJustifyH("LEFT")
	self.SectionLabels[index] = label
	return label
end

function Bags:CollectSlots()
	local slots = {}
	local query = self.SearchBox and self.SearchBox:GetText():lower() or ""
	for bagID = BAG_FIRST, BAG_LAST do
		local count = C_Container.GetContainerNumSlots(bagID) or 0
		for slotID = 1, count do
			local info = C_Container.GetContainerItemInfo(bagID, slotID)
			local itemID = info and info.itemID or C_Container.GetContainerItemID(bagID, slotID)
			local quality = info and info.quality
			local category = GetCategory(bagID, slotID, itemID, quality)
			local itemName = itemID and select(1, GetItemInfo(itemID)) or ""
			local matches = query == "" or (itemName and itemName:lower():find(query, 1, true)) or category:lower():find(query, 1, true)
			if matches then
				table.insert(slots, {
					bagID = bagID,
					slotID = slotID,
					itemID = itemID,
					quality = quality,
					category = category,
				})
			end
		end
	end
	return slots
end

function Bags:HideLayoutObjects()
	for _, button in pairs(self.Buttons) do button:Hide() end
	for _, label in ipairs(self.SectionLabels) do label:Hide() end
end

function Bags:PlaceButton(button, column, row, buttonSize, spacing, topOffset)
	button:ClearAllPoints()
	button:SetSize(buttonSize, buttonSize)
	button:SetPoint("TOPLEFT", self.Content, "TOPLEFT", column * (buttonSize + spacing), -(topOffset + row * (buttonSize + spacing)))
	ConfigureNativeButton(button, button.BagID, button.SlotID)
	RefreshButtonVisuals(button, button.BagID, button.SlotID)
	button:Show()
end

function Bags:LayoutUnified(slots, settings)
	local columns = settings.columns
	local size, spacing = settings.buttonSize, settings.spacing
	local mainSlots, reagentSlots = {}, {}
	for _, slot in ipairs(slots) do
		table.insert(slot.bagID == REAGENT_BAG and reagentSlots or mainSlots, slot)
	end
	for index, slot in ipairs(mainSlots) do
		local button = self:GetButton(slot.bagID, slot.slotID)
		local zero = index - 1
		self:PlaceButton(button, zero % columns, math.floor(zero / columns), size, spacing, 4)
	end
	local mainRows = math.ceil(#mainSlots / columns)
	local topOffset = 8 + mainRows * (size + spacing)
	if #reagentSlots > 0 then
		local label = self:GetSectionLabel(1)
		label:ClearAllPoints()
		label:SetPoint("TOPLEFT", self.Content, "TOPLEFT", 0, -topOffset)
		label:SetText("Reagent Bag")
		label:Show()
		topOffset = topOffset + 22
		for index, slot in ipairs(reagentSlots) do
			local button = self:GetButton(slot.bagID, slot.slotID)
			local zero = index - 1
			self:PlaceButton(button, zero % columns, math.floor(zero / columns), size, spacing, topOffset)
		end
		topOffset = topOffset + math.ceil(#reagentSlots / columns) * (size + spacing)
	end
	return topOffset
end

local function CapacityColor(used, total)
	if total <= 0 then return "8f98a8" end
	local fullness = used / total
	if fullness >= 0.90 then return "ef5350" end
	if fullness >= 0.75 then return "ffd54f" end
	return "4fd17b"
end

local function GetCapacity(firstBag, lastBag)
	local used, total = 0, 0
	for bagID = firstBag, lastBag do
		local slots = C_Container.GetContainerNumSlots(bagID) or 0
		local free = C_Container.GetContainerNumFreeSlots(bagID) or 0
		total = total + slots
		used = used + math.max(0, slots - free)
	end
	return used, total
end

function Bags:UpdateCapacityText()
	local bagUsed, bagTotal = GetCapacity(BAG_FIRST, REAGENT_BAG - 1)
	local reagentUsed, reagentTotal = GetCapacity(REAGENT_BAG, REAGENT_BAG)
	self.BagSpaceText:SetFormattedText("Bag Space: |cff%s%d/%d|r", CapacityColor(bagUsed, bagTotal), bagUsed, bagTotal)
	self.ReagentSpaceText:SetFormattedText("Reagent Bag: |cff%s%d/%d|r", CapacityColor(reagentUsed, reagentTotal), reagentUsed, reagentTotal)
end

function Bags:LayoutCategories(slots, settings)
	local grouped = {}
	for _, category in ipairs(CATEGORY_ORDER) do grouped[category] = {} end
	for _, slot in ipairs(slots) do table.insert(grouped[slot.category], slot) end

	local columns = settings.columns
	local size, spacing = settings.buttonSize, settings.spacing
	local topOffset, labelIndex = 0, 0
	for _, category in ipairs(CATEGORY_ORDER) do
		local categorySlots = grouped[category]
		if #categorySlots > 0 then
			labelIndex = labelIndex + 1
			local label = self:GetSectionLabel(labelIndex)
			label:ClearAllPoints()
			label:SetPoint("TOPLEFT", self.Content, "TOPLEFT", 0, -topOffset)
			label:SetText(category .. "  |cff8f98a8" .. #categorySlots .. "|r")
			label:Show()
			topOffset = topOffset + 22
			for index, slot in ipairs(categorySlots) do
				local button = self:GetButton(slot.bagID, slot.slotID)
				local zero = index - 1
				self:PlaceButton(button, zero % columns, math.floor(zero / columns), size, spacing, topOffset)
			end
			topOffset = topOffset + math.ceil(#categorySlots / columns) * (size + spacing) + 12
		end
	end
	return math.max(topOffset, 60)
end

function Bags:Refresh()
	if not self.Frame then return end
	if self.Sorting then
		self.PendingRefresh = true
		return
	end
	self.PendingRefresh = false
	self:HideLayoutObjects()
	local settings = SlickUI.DB.bags
	local slots = self:CollectSlots()
	local height
	if settings.mode == "categories" then
		height = self:LayoutCategories(slots, settings)
	else
		height = self:LayoutUnified(slots, settings)
	end
	local contentWidth = settings.columns * (settings.buttonSize + settings.spacing)
	self.Content:SetSize(math.max(contentWidth, 100), math.max(height, 100))
	self.Frame:SetWidth(math.max(460, math.min(contentWidth + 82, 1180)))
	self.ModeText:SetText(settings.mode == "categories" and "Category View" or "Single Bag")
	self:UpdateCapacityText()
	self.EmptyText:SetShown(#slots == 0)
end

function Bags:FinishSorting()
	if not self.Sorting then return end
	self.Sorting = false
	self.PendingRefresh = false
	if self.SortButton then
		self.SortButton:Enable()
		self.SortButton:SetText("Sort Bags")
	end
	if self.Content then self.Content:Show() end
	if self.SortOverlay then
		self.SortOverlay:SetScript("OnUpdate", nil)
		self.SortOverlay:Hide()
	end
	self:Refresh()
end

function Bags:StartSorting()
	if self.Sorting or not SlickUI:IsAvailableOutOfCombat("Bag sorting") then return end
	if not C_Container or type(C_Container.SortBags) ~= "function" then return end

	self.Sorting = true
	self.SortGeneration = self.SortGeneration + 1
	local generation = self.SortGeneration
	self.PendingRefresh = false
	if self.SortButton then
		self.SortButton:Disable()
		self.SortButton:SetText("Sorting...")
	end
	if self.Content then self.Content:Hide() end
	if self.SortOverlay then
		self.SortOverlay.Elapsed = 0
		self.SortOverlay.Progress:SetValue(0)
		self.SortOverlay:SetScript("OnUpdate", function(overlay, elapsed)
			overlay.Elapsed = (overlay.Elapsed + elapsed) % 1.5
			overlay.Progress:SetValue((overlay.Elapsed / 1.5) * 100)
		end)
		self.SortOverlay:Show()
	end

	local ok = pcall(C_Container.SortBags)
	if not ok then
		self:FinishSorting()
		SlickUI:Print("Blizzard could not start bag sorting.")
		return
	end

	C_Timer.After(20, function()
		if Bags.Sorting and Bags.SortGeneration == generation then Bags:FinishSorting() end
	end)
end

function Bags:UpdateCombatControls()
	local inCombat = InCombatLockdown() == true
	if self.SortButton and not self.Sorting then
		self.SortButton:SetEnabled(not inCombat)
		self.SortButton:SetText(inCombat and "Sort Locked" or "Sort Bags")
	end
end

function Bags:ApplySettings()
	if not self.Frame then return end
	local settings = SlickUI.DB.bags
	local position = settings.position or {}
	self.Frame:SetScale(settings.scale)
	self.Frame:ClearAllPoints()
	self.Frame:SetPoint(
		position.point or "CENTER",
		UIParent,
		position.relativePoint or "CENTER",
		tonumber(position.x) or 180,
		tonumber(position.y) or 20
	)
	self:Refresh()
end

function Bags:SetMode(mode)
	if mode ~= "unified" and mode ~= "categories" then return end
	SlickUI.DB.bags.mode = mode
	self:Refresh()
	if SlickUI.Options and SlickUI.Options.RefreshBagControls then SlickUI.Options:RefreshBagControls() end
end

function Bags:UpdateBagRail()
	if not self.BagSlotButtons then return end
	for bagID = 1, REAGENT_BAG do
		local button = self.BagSlotButtons[bagID]
		local inventoryID = C_Container.ContainerIDToInventoryID(bagID)
		button.InventoryID = inventoryID
		button.Icon:SetTexture(inventoryID and GetInventoryItemTexture("player", inventoryID) or nil)
		button.Icon:SetShown(inventoryID and GetInventoryItemTexture("player", inventoryID) ~= nil)
		button.Empty:SetShown(not button.Icon:IsShown())
		local slots = C_Container.GetContainerNumSlots(bagID) or 0
		button.Count:SetText(slots > 0 and slots or "")
	end
end

function Bags:SetBagRailCollapsed(collapsed)
	SlickUI.DB.bags.bagRailCollapsed = collapsed == true
	self.BagRail:SetShown(not collapsed)
	self.BagRailToggle:SetText(collapsed and ">" or "<")
end

function Bags:Show()
	if not SlickUI.DB.bags.enabled then
		SlickUI:Print("SlickUI Bags are disabled. Enable them in Settings > SlickUI > Bags.")
		return
	end
	HideBlizzardBags()
	self.Frame:Show()
	self:UpdateBagRail()
	self:Refresh()
end

function Bags:HideBlizzardBags()
	HideBlizzardBags()
end

function Bags:Hide()
	if self.Frame then self.Frame:Hide() end
end

function Bags:Toggle()
	if self.Frame:IsShown() then self:Hide() else self:Show() end
end

function Bags:OpenForAuctionHouse()
	if not SlickUI.DB.bags.enabled then return end
	if not self.AuctionHouseActive then
		self.AuctionBagsWereShown = self.Frame and self.Frame:IsShown() or false
		self.OpenedBagsForAuction = not self.AuctionBagsWereShown
	end
	self.AuctionHouseActive = true
	self:Show()
	-- Blizzard may open its containers immediately after AUCTION_HOUSE_SHOW.
	-- Suppress those follow-up frames without toggling the SlickUI window.
	for _, delay in ipairs({ 0, 0.1, 0.5 }) do
		C_Timer.After(delay, function()
			if Bags.AuctionHouseActive and SlickUI.DB.bags.enabled then
				HideBlizzardBags()
			end
		end)
	end
end

function Bags:CloseForAuctionHouse()
	if not self.AuctionHouseActive then return end
	self.AuctionHouseActive = false
	if self.OpenedBagsForAuction then self:Hide() end
	self.OpenedBagsForAuction = false
	self.AuctionBagsWereShown = false
end

function Bags:OpenForMerchant()
	if not SlickUI.DB.bags.enabled then return end
	if not self.MerchantActive then
		self.MerchantBagsWereShown = self.Frame and self.Frame:IsShown() or false
		self.OpenedBagsForMerchant = not self.MerchantBagsWereShown
	end
	self.MerchantActive = true
	self:Show()
	for _, delay in ipairs({ 0, 0.1, 0.5 }) do
		C_Timer.After(delay, function()
			if Bags.MerchantActive and SlickUI.DB.bags.enabled then HideBlizzardBags() end
		end)
	end
end

function Bags:CloseForMerchant()
	if not self.MerchantActive then return end
	self.MerchantActive = false
	if self.OpenedBagsForMerchant then self:Hide() end
	self.OpenedBagsForMerchant = false
	self.MerchantBagsWereShown = false
end

function Bags:CreateFrame()
	local frame = CreateFrame("Frame", "SlickUIBagFrame", UIParent, "BackdropTemplate")
	frame:SetSize(650, 560)
	frame:SetPoint("CENTER", UIParent, "CENTER", 180, 20)
	frame:SetFrameStrata("HIGH")
	frame:SetClampedToScreen(true)
	frame:SetMovable(true)
	frame:EnableMouse(true)
	frame:RegisterForDrag("LeftButton")
	frame:SetScript("OnDragStart", function(self) if not InCombatLockdown() then self:StartMoving() end end)
	frame:SetScript("OnDragStop", function(self)
		self:StopMovingOrSizing()
		local point, _, relativePoint, x, y = self:GetPoint(1)
		SlickUI.DB.bags.position = {
			point = point or "CENTER",
			relativePoint = relativePoint or "CENTER",
			x = math.floor((x or 0) + 0.5),
			y = math.floor((y or 0) + 0.5),
		}
	end)
	frame:SetBackdrop({ bgFile = Theme.Texture, edgeFile = Theme.Texture, edgeSize = 1 })
	frame:SetBackdropColor(0.025, 0.03, 0.045, 0.97)
	frame:SetBackdropBorderColor(unpack(Theme.Colors.border))
	frame:Hide()

	local title = Theme:CreateText(frame, 18)
	title:SetPoint("TOPLEFT", 18, -16)
	title:SetText("SlickUI Bags")
	title:SetTextColor(unpack(Theme.Colors.goldText))

	local modeText = Theme:CreateText(frame, 11)
	modeText:SetPoint("LEFT", title, "RIGHT", 12, 0)
	modeText:SetTextColor(unpack(Theme.Colors.mutedText))
	self.ModeText = modeText

	local bagSpaceText = Theme:CreateText(frame, 11)
	bagSpaceText:SetSize(125, 16)
	bagSpaceText:SetPoint("TOP", frame, "TOP", -18, -18)
	bagSpaceText:SetJustifyH("RIGHT")
	bagSpaceText:SetTextColor(unpack(Theme.Colors.mutedText))
	self.BagSpaceText = bagSpaceText

	local reagentSpaceText = Theme:CreateText(frame, 11)
	reagentSpaceText:SetSize(135, 16)
	reagentSpaceText:SetPoint("LEFT", bagSpaceText, "RIGHT", 24, 0)
	reagentSpaceText:SetJustifyH("LEFT")
	reagentSpaceText:SetTextColor(unpack(Theme.Colors.mutedText))
	self.ReagentSpaceText = reagentSpaceText

	local close = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
	close:SetPoint("TOPRIGHT", -5, -5)

	local unified = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
	unified:SetSize(100, 22)
	unified:SetPoint("TOPLEFT", 18, -48)
	unified:SetText("Single Bag")
	unified:SetScript("OnClick", function() Bags:SetMode("unified") end)
	self.UnifiedButton = unified

	local categories = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
	categories:SetSize(100, 22)
	categories:SetPoint("LEFT", unified, "RIGHT", 8, 0)
	categories:SetText("Categories")
	categories:SetScript("OnClick", function() Bags:SetMode("categories") end)
	self.CategoriesButton = categories

	local sort = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
	sort:SetSize(84, 22)
	sort:SetPoint("TOPRIGHT", -46, -48)
	sort:SetText("Sort Bags")
	sort:SetScript("OnClick", function() Bags:StartSorting() end)
	self.SortButton = sort

	local search = CreateFrame("EditBox", nil, frame, "SearchBoxTemplate")
	search:SetSize(180, 22)
	search:SetPoint("RIGHT", sort, "LEFT", -10, 0)
	search:SetAutoFocus(false)
	search:HookScript("OnTextChanged", function() Bags:Refresh() end)
	self.SearchBox = search

	local scroll = CreateFrame("ScrollFrame", "SlickUIBagScrollFrame", frame, "UIPanelScrollFrameTemplate")
	scroll:SetPoint("TOPLEFT", 18, -82)
	scroll:SetPoint("BOTTOMRIGHT", -36, 18)
	local content = CreateFrame("Frame", nil, scroll)
	content:SetSize(580, 460)
	scroll:SetScrollChild(content)
	self.Content = content

	local emptyText = Theme:CreateText(content, 12)
	emptyText:SetPoint("TOPLEFT", 4, -8)
	emptyText:SetText("No bag slots match this search.")
	emptyText:SetTextColor(unpack(Theme.Colors.mutedText))
	emptyText:Hide()
	self.EmptyText = emptyText

	local bagRail = CreateFrame("Frame", nil, frame, "BackdropTemplate")
	bagRail:SetSize(58, 274)
	bagRail:SetPoint("TOPRIGHT", frame, "TOPLEFT", -4, -82)
	bagRail:SetBackdrop({ bgFile = Theme.Texture, edgeFile = Theme.Texture, edgeSize = 1 })
	bagRail:SetBackdropColor(0.025, 0.03, 0.045, 0.97)
	bagRail:SetBackdropBorderColor(unpack(Theme.Colors.border))
	self.BagRail = bagRail
	self.BagSlotButtons = {}
	for bagID = 1, REAGENT_BAG do
		local button = CreateFrame("Button", nil, bagRail, "BackdropTemplate")
		button:SetSize(42, 42)
		button:SetPoint("TOP", bagRail, "TOP", 0, -10 - (bagID - 1) * 50)
		button:SetBackdrop({ bgFile = Theme.Texture, edgeFile = Theme.Texture, edgeSize = 1 })
		button:SetBackdropColor(0.04, 0.05, 0.07, 0.96)
		button:SetBackdropBorderColor(unpack(Theme.Colors.borderMuted))
		button:RegisterForClicks("LeftButtonUp")
		button:RegisterForDrag("LeftButton")
		button.Icon = button:CreateTexture(nil, "ARTWORK")
		button.Icon:SetPoint("TOPLEFT", 4, -4)
		button.Icon:SetPoint("BOTTOMRIGHT", -4, 4)
		button.Empty = button:CreateTexture(nil, "BACKGROUND")
		button.Empty:SetPoint("TOPLEFT", 4, -4)
		button.Empty:SetPoint("BOTTOMRIGHT", -4, 4)
		button.Empty:SetColorTexture(0.09, 0.10, 0.13, 0.85)
		button.Count = Theme:CreateText(button, 9)
		button.Count:SetPoint("BOTTOMRIGHT", -3, 3)
		button.Count:SetTextColor(unpack(Theme.Colors.text))
		button.BagID = bagID
		local function HandleBagSlot(self)
			if not self.InventoryID or InCombatLockdown() then return end
			PickupInventoryItem(self.InventoryID)
		end
		button:SetScript("OnClick", HandleBagSlot)
		button:SetScript("OnReceiveDrag", HandleBagSlot)
		button:SetScript("OnDragStart", HandleBagSlot)
		button:SetScript("OnEnter", function(self)
			GameTooltip:SetOwner(self, "ANCHOR_LEFT")
			if not self.InventoryID or not GameTooltip:SetInventoryItem("player", self.InventoryID) then
				GameTooltip:SetText(self.BagID == REAGENT_BAG and "Reagent Bag Slot" or "Bag Slot " .. self.BagID)
			end
			GameTooltip:AddLine("Click or drag to equip, swap, or remove this bag.", 0.68, 0.71, 0.78, true)
			GameTooltip:Show()
		end)
		button:SetScript("OnLeave", GameTooltip_Hide)
		self.BagSlotButtons[bagID] = button
	end
	local bagRailToggle = CreateFrame("Button", nil, frame, "BackdropTemplate")
	bagRailToggle:SetSize(26, 52)
	bagRailToggle:SetPoint("BOTTOMRIGHT", bagRail, "TOPRIGHT", 0, 2)
	bagRailToggle:SetBackdrop({ bgFile = Theme.Texture, edgeFile = Theme.Texture, edgeSize = 1 })
	bagRailToggle:SetBackdropColor(0.025, 0.03, 0.045, 0.97)
	bagRailToggle:SetBackdropBorderColor(unpack(Theme.Colors.border))
	local bagRailText = Theme:CreateText(bagRailToggle, 16)
	bagRailText:SetPoint("CENTER")
	bagRailToggle:SetFontString(bagRailText)
	bagRailToggle:SetScript("OnClick", function() Bags:SetBagRailCollapsed(not SlickUI.DB.bags.bagRailCollapsed) end)
	self.BagRailToggle = bagRailToggle
	self:SetBagRailCollapsed(SlickUI.DB.bags.bagRailCollapsed)

	local sortOverlay = CreateFrame("Frame", nil, frame, "BackdropTemplate")
	sortOverlay:SetPoint("TOPLEFT", scroll, "TOPLEFT", 0, 0)
	sortOverlay:SetPoint("BOTTOMRIGHT", scroll, "BOTTOMRIGHT", 0, 0)
	sortOverlay:SetFrameLevel(scroll:GetFrameLevel() + 10)
	sortOverlay:SetBackdrop({ bgFile = Theme.Texture })
	sortOverlay:SetBackdropColor(0.025, 0.03, 0.045, 0.92)
	sortOverlay:Hide()

	local sortingText = Theme:CreateText(sortOverlay, 16)
	sortingText:SetPoint("CENTER", sortOverlay, "CENTER", 0, 20)
	sortingText:SetText("Sorting bags...")
	sortingText:SetTextColor(unpack(Theme.Colors.goldText))

	local progress = CreateFrame("StatusBar", nil, sortOverlay, "BackdropTemplate")
	progress:SetSize(280, 16)
	progress:SetPoint("TOP", sortingText, "BOTTOM", 0, -12)
	progress:SetMinMaxValues(0, 100)
	progress:SetValue(0)
	progress:SetStatusBarTexture(Theme.Texture)
	progress:SetStatusBarColor(unpack(Theme.Colors.cast))
	progress:SetBackdrop({ bgFile = Theme.Texture, edgeFile = Theme.Texture, edgeSize = 1 })
	progress:SetBackdropColor(0.08, 0.09, 0.12, 1)
	progress:SetBackdropBorderColor(unpack(Theme.Colors.border))
	sortOverlay.Progress = progress
	self.SortOverlay = sortOverlay

	self.Frame = frame
	self:ApplySettings()
	table.insert(UISpecialFrames, frame:GetName())
end

function Bags:InstallBagToggleHook()
	if self.ToggleHookInstalled then return true end
	if type(hooksecurefunc) ~= "function" or type(ToggleAllBags) ~= "function" then return false end
	hooksecurefunc("ToggleAllBags", function()
		if not SlickUI.DB.bags.enabled or Bags.InternalToggle then return end
		Bags.InternalToggle = true
		HideBlizzardBags()
		if Bags.AuctionHouseActive or Bags.MerchantActive then Bags:Show() else Bags:Toggle() end
		Bags.InternalToggle = false
	end)
	self.ToggleHookInstalled = true
	return true
end

function Bags:Initialize()
	self:CreateFrame()
	local events = CreateFrame("Frame")
	events:RegisterEvent("BAG_UPDATE_DELAYED")
	events:RegisterEvent("BAG_UPDATE_COOLDOWN")
	events:RegisterEvent("ITEM_LOCK_CHANGED")
	events:RegisterEvent("GET_ITEM_INFO_RECEIVED")
	events:RegisterEvent("BAG_NEW_ITEMS_UPDATED")
	events:RegisterEvent("PLAYER_REGEN_ENABLED")
	events:RegisterEvent("PLAYER_REGEN_DISABLED")
	events:RegisterEvent("AUCTION_HOUSE_SHOW")
	events:RegisterEvent("AUCTION_HOUSE_CLOSED")
	events:RegisterEvent("MERCHANT_SHOW")
	events:RegisterEvent("MERCHANT_CLOSED")
	events:RegisterEvent("ADDON_LOADED")
	events:RegisterEvent("PLAYER_ENTERING_WORLD")
	events:SetScript("OnEvent", function(_, event, addon)
		if event == "ADDON_LOADED" or event == "PLAYER_ENTERING_WORLD" then
			Bags:InstallBagToggleHook()
			if SlickUI.DB.bags.enabled and not Bags.ToggleHookInstalled then
				-- Blizzard's container functions can appear a frame after its addon
				-- reports loaded. Retry briefly so a first-time character receives
				-- the same replacement hook as an established character.
				for _, delay in ipairs({ 0, 0.1, 0.5, 1.0 }) do
					C_Timer.After(delay, function() Bags:InstallBagToggleHook() end)
				end
			end
			if event == "ADDON_LOADED" then return end
		end
		if event == "AUCTION_HOUSE_SHOW" then Bags:OpenForAuctionHouse(); return end
		if event == "AUCTION_HOUSE_CLOSED" then Bags:CloseForAuctionHouse(); return end
		if event == "MERCHANT_SHOW" then Bags:OpenForMerchant(); return end
		if event == "MERCHANT_CLOSED" then Bags:CloseForMerchant(); return end
		if event == "BAG_UPDATE_DELAYED" and Bags.Sorting then
			C_Timer.After(0, function() Bags:FinishSorting() end)
			return
		end
		if event == "PLAYER_REGEN_DISABLED" then Bags:UpdateCombatControls(); Bags:Refresh(); return end
		if event == "PLAYER_REGEN_ENABLED" then
			Bags:UpdateCombatControls()
			if Bags.PendingRefresh then Bags:Refresh() end
			return
		end
		Bags:UpdateBagRail()
		Bags:Refresh()
	end)

	self:InstallBagToggleHook()
	self:UpdateCombatControls()

	SLASH_SLICKUIBAGS1 = "/slickbags"
	SlashCmdList.SLICKUIBAGS = function() Bags:Toggle() end
end

SlickUI:RegisterModule("Bags", Bags)
