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

local InformationBars = {
	Bars = {},
	Blocks = {},
	Definitions = {},
	BarOrder = { "top1", "top2", "bottom1", "bottom2" },
	PendingInsets = false,
	ExternalKeys = {},
	DropPreview = nil,
	CinematicActive = false,
	CinematicGeneration = 0,
}
SlickUI.InformationBars = InformationBars

local function IsReadable(value)
	if canaccessvalue and not canaccessvalue(value) then return false end
	if issecretvalue and issecretvalue(value) then return false end
	return value ~= nil
end

local function SafeNumber(value)
	return IsReadable(value) and type(value) == "number" and value or nil
end

local function Compact(value)
	if not SafeNumber(value) then return "?" end
	return AbbreviateLargeNumbers and AbbreviateLargeNumbers(value) or tostring(math.floor(value))
end

local function GoldText()
	if SlickUI.GoldLedger then return SlickUI.GoldLedger:GetBarText() end
	local money = SafeNumber(GetMoney())
	if not money then return "Gold: ?" end
	local gold = math.floor(money / 10000)
	local silver = math.floor((money % 10000) / 100)
	return string.format("Gold: |cffffd24a%dg|r |cffd5d8e0%02ds|r", gold, silver)
end

local function GoldTooltip(tooltip)
	if SlickUI.GoldLedger then SlickUI.GoldLedger:BuildTooltip(tooltip) end
end

local function OpenGoldLedger(_, button)
	if not SlickUI.GoldLedger then return end
	if button == "RightButton" then SlickUI.GoldLedger:ToggleWindow()
	else SlickUI.GoldLedger:ToggleBalanceWindow() end
end

local function FishingText()
	return SlickUI.Fishing and SlickUI.Fishing:GetBarText() or "Fishing: 0"
end

local function FishingTooltip(tooltip)
	if SlickUI.Fishing then SlickUI.Fishing:BuildTooltip(tooltip) end
end

local function OpenFishingLedger()
	if SlickUI.Fishing then SlickUI.Fishing:OpenLedger() end
end

local function GroupControlsText()
	return SlickUI.GroupControls and SlickUI.GroupControls:GetWidgetText() or "Group Controls"
end

local function ToggleGroupControls()
	if SlickUI.GroupControls then
		SlickUI.GroupControls:Toggle()
	else
		SlickUI:Print("Group Controls are not available.")
	end
end

local function FriendlyNameplatesText()
	local mode = SlickUI.Nameplates and SlickUI.Nameplates:GetFriendlyMode() or "names"
	local labels = { off = "Off", names = "Names", bars = "Bars" }
	return "Friendly: " .. (labels[mode] or labels.names)
end

local function FriendlyNameplatesTooltip(tooltip)
	tooltip:AddLine("Friendly Nameplates", unpack(Theme.Colors.goldText))
	tooltip:AddLine("Quickly change Blizzard friendly-player nameplates without opening settings.", 1, 1, 1, true)
	tooltip:AddLine(" ")
	tooltip:AddDoubleLine("Current mode", FriendlyNameplatesText():gsub("^Friendly: ", ""), 1, 1, 1, 0.40, 0.82, 1.00)
end

local function FriendlyNameplatesClick(_, button)
	if button == "RightButton" then
		if SlickUI.Options then
			SlickUI.Options:Open()
			C_Timer.After(0, function() SlickUI.Options:SelectPage("nameplates") end)
		end
	elseif SlickUI.Nameplates then
		SlickUI.Nameplates:CycleFriendlyMode()
	end
end

local function BagText()
	local free, total = 0, 0
	for bag = 0, (NUM_TOTAL_EQUIPPED_BAG_SLOTS or NUM_BAG_SLOTS or 4) do
		local slots = SafeNumber(C_Container.GetContainerNumSlots(bag))
		local available = SafeNumber(C_Container.GetContainerNumFreeSlots(bag))
		if slots and available then
			total = total + slots
			free = free + available
		end
	end
	return string.format("Bags: %d/%d free", free, total)
end

local function ClassColor(classFileName)
	local color = classFileName and RAID_CLASS_COLORS and RAID_CLASS_COLORS[classFileName]
	return color and color.r or 1, color and color.g or 1, color and color.b or 1
end

local function ShortName(name)
	if type(name) ~= "string" then return "Unknown" end
	return Ambiguate and Ambiguate(name, "short") or name
end

local function GetOnlineGuildMembers()
	local members = {}
	if not IsInGuild() then return members end
	if GuildRoster then GuildRoster() end
	local total = GetNumGuildMembers and GetNumGuildMembers() or 0
	for index = 1, total do
		local name, _, _, level, _, zone, _, _, online, status, classFileName, _, _, isMobile = GetGuildRosterInfo(index)
		if online and name then
			members[#members + 1] = {
				name = ShortName(name),
				level = SafeNumber(level),
				zone = isMobile and "Mobile" or (IsReadable(zone) and zone or ""),
				status = IsReadable(status) and status or 0,
				classFileName = IsReadable(classFileName) and classFileName or nil,
			}
		end
	end
	table.sort(members, function(left, right) return left.name < right.name end)
	return members
end

local function GuildText()
	if not IsInGuild() then return "Guild: None" end
	return string.format("Guild: |cff42df72%d|r online", #GetOnlineGuildMembers())
end

local function GuildTooltip(tooltip)
	tooltip:AddLine(IsInGuild() and (GetGuildInfo("player") or "Guild") or "Guild", unpack(Theme.Colors.goldText))
	if not IsInGuild() then
		tooltip:AddLine("This character is not in a guild.", 1, 1, 1, true)
		return
	end
	local members = GetOnlineGuildMembers()
	tooltip:AddLine(string.format("%d online", #members), 0.40, 0.82, 1.00)
	tooltip:AddLine(" ")
	for _, member in ipairs(members) do
		local r, g, b = ClassColor(member.classFileName)
		local status = member.status == 1 and "AFK" or member.status == 2 and "DND" or ""
		local detail = member.zone
		if member.level then detail = string.format("%d  %s", member.level, detail) end
		if status ~= "" then detail = detail .. "  " .. status end
		tooltip:AddDoubleLine(member.name, detail, r, g, b, 0.78, 0.82, 0.90)
	end
end

local function OpenGuild()
	if C_AddOns and C_AddOns.LoadAddOn then pcall(C_AddOns.LoadAddOn, "Blizzard_GuildUI") end
	if ToggleGuildFrame then ToggleGuildFrame() end
end

local FRIEND_GAME_ATLASES = {
	WoW = "Battlenet-WoWicon",
	Fen = "Battlenet-D4icon",
	D3 = "Battlenet-D3icon",
	S2 = "Battlenet-SC2icon",
	WTCG = "Battlenet-WTCGicon",
	Hero = "Battlenet-HotSicon",
	Pro = "Battlenet-Overwatchicon",
	OSI = "Battlenet-DIicon",
}

local function GetFriendGameIcon(clientProgram)
	if not clientProgram then return "" end
	local atlas = FRIEND_GAME_ATLASES[clientProgram]
	if atlas and type(CreateAtlasMarkup) == "function" then
		local valid = true
		if C_Texture and C_Texture.GetAtlasInfo then
			local ok, info = pcall(C_Texture.GetAtlasInfo, atlas)
			valid = ok and info ~= nil
		end
		if valid then
			local ok, markup = pcall(CreateAtlasMarkup, atlas, 14, 14)
			if ok and type(markup) == "string" then return markup end
		end
	end
	if clientProgram == "WoW" then
		return "|TInterface\\FriendsFrame\\Battlenet-WoWicon:14:14:0:0|t"
	end
	return "|TInterface\\FriendsFrame\\Battlenet-Battleneticon:14:14:0:0|t"
end

local function IsPlayingGame(game)
	if not game or not game.isOnline then return false end
	local client = game.clientProgram
	return client ~= nil and client ~= "" and client ~= "App" and client ~= "BSAp"
end

local function GetOnlineFriends()
	local friends = {}
	local wowFriendCount = C_FriendList and C_FriendList.GetNumFriends and C_FriendList.GetNumFriends() or 0
	for index = 1, wowFriendCount do
		local info = C_FriendList.GetFriendInfoByIndex(index)
		if info and info.connected and info.name then
			friends[#friends + 1] = {
				name = ShortName(info.name),
				detail = string.format("%s  %s", info.className or "WoW", info.area or "") .. " " .. GetFriendGameIcon(BNET_CLIENT_WOW or "WoW"),
				kind = "WoW",
				playing = true,
			}
		end
	end
	local bnetCount = BNGetNumFriends and select(1, BNGetNumFriends()) or 0
	for index = 1, bnetCount do
		local accountInfo = C_BattleNet and C_BattleNet.GetFriendAccountInfo and C_BattleNet.GetFriendAccountInfo(index)
		if not accountInfo and BNGetFriendInfo then
			local legacyInfo = BNGetFriendInfo(index)
			if type(legacyInfo) == "table" then accountInfo = legacyInfo end
		end
		local game = accountInfo and accountInfo.gameAccountInfo
		if C_BattleNet and C_BattleNet.GetFriendNumGameAccounts and C_BattleNet.GetFriendGameAccountInfo then
			local gameCount = C_BattleNet.GetFriendNumGameAccounts(index) or 0
			local onlineFallback
			for gameIndex = 1, gameCount do
				local candidate = C_BattleNet.GetFriendGameAccountInfo(index, gameIndex)
				if candidate and candidate.isOnline then
					onlineFallback = onlineFallback or candidate
					if IsPlayingGame(candidate) then game = candidate; break end
				end
			end
			if not IsPlayingGame(game) then game = onlineFallback or game end
		end
		if accountInfo and (accountInfo.isOnline or (game and game.isOnline)) then
			local detail = "Battle.net"
			local playing = IsPlayingGame(game)
			if playing then
				local character = game.characterName and ShortName(game.characterName) or nil
				local location = game.areaName or game.realmName or game.clientProgram
				if character and location then detail = character .. "  " .. location
				elseif character then detail = character
				elseif location then detail = location end
				local icon = GetFriendGameIcon(game.clientProgram)
				if icon ~= "" then detail = detail .. " " .. icon end
			end
			friends[#friends + 1] = {
				name = accountInfo.accountName or accountInfo.battleTag or "Battle.net Friend",
				detail = detail,
				kind = "Battle.net",
				playing = playing,
			}
		end
	end
	table.sort(friends, function(left, right)
		if left.playing ~= right.playing then return left.playing end
		if left.kind ~= right.kind then return left.kind < right.kind end
		return left.name < right.name
	end)
	return friends
end

local function GetOnlineFriendCount()
	local wowOnline = 0
	local wowFriendCount = C_FriendList and C_FriendList.GetNumFriends and C_FriendList.GetNumFriends() or 0
	for index = 1, wowFriendCount do
		local info = C_FriendList.GetFriendInfoByIndex(index)
		if info and info.connected then wowOnline = wowOnline + 1 end
	end
	local bnetOnline = BNGetNumFriends and select(2, BNGetNumFriends()) or 0
	return wowOnline + (SafeNumber(bnetOnline) or 0)
end

local function FriendsText()
	return string.format("Friends: |cff48b8ff%d|r online", GetOnlineFriendCount())
end

local function FriendsTooltip(tooltip)
	local friends = GetOnlineFriends()
	tooltip:AddLine("Friends", unpack(Theme.Colors.goldText))
	local onlineCount = GetOnlineFriendCount()
	tooltip:AddLine(string.format("%d online", onlineCount), 0.40, 0.82, 1.00)
	if onlineCount == 0 then
		tooltip:AddLine("No friends are currently online.", 1, 1, 1, true)
		return
	end
	if #friends == 0 then
		tooltip:AddLine("Online friend details are still loading.", 1, 1, 1, true)
		return
	end
	tooltip:AddLine(" ")
	for _, friend in ipairs(friends) do
		local r, g, b = friend.kind == "Battle.net" and 0.40 or 0.78, friend.kind == "Battle.net" and 0.82 or 0.82, 1.00
		tooltip:AddDoubleLine(friend.name, friend.detail, r, g, b, 0.78, 0.82, 0.90)
	end
end

local function OpenFriends()
	if ToggleFriendsFrame then ToggleFriendsFrame(1) end
end

local function CraftingDashboardText()
	return "Crafting Dashboard"
end

local function CraftingDashboardTooltip(tooltip)
	tooltip:AddLine("Crafting Dashboard", unpack(Theme.Colors.goldText))
	tooltip:AddLine("Open the Recipe Profit Dashboard for the currently loaded profession.", 1, 1, 1, true)
	if not (SlickUI.DB and SlickUI.DB.crafting and SlickUI.DB.crafting.enabled ~= false) then
		tooltip:AddLine("Crafting help is currently disabled.", 1, 0.35, 0.25, true)
	elseif not (C_TradeSkillUI and SafeCall(C_TradeSkillUI.IsTradeSkillReady)) then
		tooltip:AddLine("Open a profession to load its recipes.", 1, 0.82, 0.29, true)
	end
end

local function OpenCraftingDashboard()
	if SlickUI.DB and SlickUI.DB.crafting and SlickUI.DB.crafting.enabled == false then
		SlickUI:Print("Enable Crafting Help in SlickUI settings first.")
		return
	end
	local loader = SlickUI.CraftingLoader
	if loader and loader.Load then loader:Load() end
	local planner = SlickUI.CraftingPlanner
	if planner and planner.ToggleDashboard then
		planner:ToggleDashboard()
	else
		SlickUI:Print("The Crafting Dashboard is not available.")
	end
end

local function DurabilityText()
	local lowest
	for slot = 1, 18 do
		local current, maximum = GetInventoryItemDurability(slot)
		current, maximum = SafeNumber(current), SafeNumber(maximum)
		if current and maximum and maximum > 0 then
			local percent = current / maximum * 100
			if not lowest or percent < lowest then lowest = percent end
		end
	end
	if not lowest then return "Durability: --" end
	local percent = math.floor(lowest + 0.5)
	local color = percent >= 70 and "ff42df72" or percent >= 35 and "ffffd24a" or "ffff4a45"
	return string.format("Durability: |c%s%d%%|r", color, percent)
end

local DurabilitySlots = {
	{ slot = INVSLOT_HEAD, label = "Head" },
	{ slot = INVSLOT_SHOULDER, label = "Shoulders" },
	{ slot = INVSLOT_CHEST, label = "Chest" },
	{ slot = INVSLOT_WAIST, label = "Waist" },
	{ slot = INVSLOT_LEGS, label = "Legs" },
	{ slot = INVSLOT_FEET, label = "Feet" },
	{ slot = INVSLOT_WRIST, label = "Wrists" },
	{ slot = INVSLOT_HAND, label = "Hands" },
	{ slot = INVSLOT_MAINHAND, label = "Main Hand" },
	{ slot = INVSLOT_OFFHAND, label = "Off Hand" },
}

local function DurabilityColor(percent)
	if percent >= 70 then return 0.26, 0.87, 0.45 end
	if percent >= 35 then return 1.00, 0.82, 0.29 end
	return 1.00, 0.29, 0.27
end

local function DurabilityTooltip(tooltip)
	tooltip:AddLine("Equipment Durability", unpack(Theme.Colors.goldText))
	local found
	for _, info in ipairs(DurabilitySlots) do
		local current, maximum = GetInventoryItemDurability(info.slot)
		current, maximum = SafeNumber(current), SafeNumber(maximum)
		if current and maximum and maximum > 0 then
			found = true
			local percent = math.floor((current / maximum * 100) + 0.5)
			local itemName = GetInventoryItemLink("player", info.slot) or info.label
			local r, g, b = DurabilityColor(percent)
			tooltip:AddDoubleLine(itemName, percent .. "%", 1, 1, 1, r, g, b)
		end
	end
	if not found then
		tooltip:AddLine("No equipped items have durability.", 1, 1, 1, true)
	end
end

local function LocationText()
	local zone = GetMinimapZoneText() or GetZoneText() or "Unknown"
	local coordinates = ""
	local mapID = C_Map.GetBestMapForUnit("player")
	if IsReadable(mapID) then
		local position = C_Map.GetPlayerMapPosition(mapID, "player")
		if position then
			local x, y = position:GetXY()
			x, y = SafeNumber(x), SafeNumber(y)
			if x and y then coordinates = string.format(" %.1f, %.1f", x * 100, y * 100) end
		end
	end
	return zone .. coordinates
end

local function ClockText()
	local settings = SlickUI.DB.informationBars.clock or {}
	local hour, minute
	if settings.useLocalTime then
		local localTime = date("*t")
		hour, minute = localTime.hour, localTime.min
	else
		hour, minute = GetGameTime()
	end
	hour, minute = SafeNumber(hour), SafeNumber(minute)
	if not hour or not minute then return "Time: --:--" end
	if settings.use24Hour ~= false then return string.format("Time: %02d:%02d", hour, minute) end
	local suffix = hour >= 12 and "PM" or "AM"
	local displayHour = hour % 12
	if displayHour == 0 then displayHour = 12 end
	return string.format("Time: %d:%02d %s", displayHour, minute, suffix)
end

local function AlarmWarning(message)
	if RaidNotice_AddMessage and RaidWarningFrame then
		RaidNotice_AddMessage(RaidWarningFrame, message, ChatTypeInfo.RAID_WARNING)
	else
		UIErrorsFrame:AddMessage(message, 1, 0.25, 0.1, 1)
	end
end

local function PlayAlarmSound()
	local sound = SOUNDKIT and (SOUNDKIT.READY_CHECK or SOUNDKIT.RAID_WARNING)
	if sound and PlaySound then PlaySound(sound, "Master") end
end

local function CanBroadcastAlarm()
	if not IsInGroup() then return false end
	return UnitIsGroupLeader("player") or UnitIsGroupAssistant("player")
end

local function BroadcastAlarm(message)
	if not InformationBars.ClockAlarmBroadcast or not CanBroadcastAlarm() then return end
	local channel
	if IsInGroup(LE_PARTY_CATEGORY_INSTANCE) then
		channel = "INSTANCE_CHAT"
	elseif IsInRaid() then
		channel = "RAID_WARNING"
	else
		channel = "PARTY"
	end
	if SendChatMessage then SendChatMessage(message, channel) end
end

function InformationBars:StopClockAlarm(silent)
	if self.ClockAlarmTicker then self.ClockAlarmTicker:Cancel(); self.ClockAlarmTicker = nil end
	self.ClockAlarmEnds = nil
	if self.ClockAlarmStatus then self.ClockAlarmStatus:SetText("No alarm running") end
	if not silent then SlickUI:Print("Alarm cancelled.") end
end

function InformationBars:StartClockAlarm(minutes)
	minutes = tonumber(minutes)
	if not minutes or minutes <= 0 then SlickUI:Print("Enter an alarm duration greater than zero."); return end
	self:StopClockAlarm(true)
	self.ClockAlarmEnds = GetTime() + (minutes * 60)
	self.ClockAlarmMessage = SlickUI.DB.informationBars.clock.alarmMessage or "Break Time Over"
	self.ClockAlarmBroadcast = SlickUI.DB.informationBars.clock.broadcastToGroup == true
	local lastSecond
	self.ClockAlarmTicker = C_Timer.NewTicker(0.2, function()
		local remaining = math.max(0, math.ceil((InformationBars.ClockAlarmEnds or 0) - GetTime()))
		if InformationBars.ClockAlarmStatus then
			InformationBars.ClockAlarmStatus:SetText(string.format("Alarm: %d:%02d remaining", math.floor(remaining / 60), remaining % 60))
		end
		if remaining <= 5 and remaining > 0 and remaining ~= lastSecond then
			lastSecond = remaining
			AlarmWarning("Alarm: " .. remaining)
			PlayAlarmSound()
			BroadcastAlarm("Alarm: " .. remaining)
		end
		if remaining <= 0 then
			local message = InformationBars.ClockAlarmMessage or "Break Time Over"
			InformationBars:StopClockAlarm(true)
			AlarmWarning(message)
			PlayAlarmSound()
			BroadcastAlarm(message)
			SlickUI:Print(message)
		end
	end)
	SlickUI:Print(string.format("Alarm started for %g minute(s).", minutes))
end

function InformationBars:CreateClockAlarmPanel(owner)
	if self.ClockAlarmPanel then return self.ClockAlarmPanel end
	local panel = CreateFrame("Frame", "SlickUIClockAlarmPanel", UIParent, "BackdropTemplate")
	panel:SetSize(330, 202); panel:SetFrameStrata("DIALOG"); panel:SetClampedToScreen(true)
	Theme:CreatePanel(panel)
	local title = Theme:CreateText(panel, 15); title:SetPoint("TOPLEFT", 12, -10); title:SetText("Alarm Timer")
	local help = Theme:CreateText(panel, 10); help:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -5); help:SetText("Set three reusable countdown presets (minutes).")
	local settings = SlickUI.DB.informationBars.clock
	local messageLabel = Theme:CreateText(panel, 10); messageLabel:SetPoint("TOPLEFT", 13, -55); messageLabel:SetText("Completion message")
	local messageEdit = CreateFrame("EditBox", nil, panel, "InputBoxTemplate")
	messageEdit:SetSize(190, 24); messageEdit:SetAutoFocus(false); messageEdit:SetMaxLetters(80)
	messageEdit:SetPoint("LEFT", messageLabel, "RIGHT", 9, 0); messageEdit:SetText(settings.alarmMessage or "Break Time Over")
	messageEdit:SetScript("OnEnterPressed", function(self) self:ClearFocus() end)
	messageEdit:SetScript("OnEditFocusLost", function(self)
		local message = self:GetText():match("^%s*(.-)%s*$")
		if message == "" then message = "Break Time Over"; self:SetText(message) end
		settings.alarmMessage = message
	end)
	local broadcast = CreateFrame("CheckButton", nil, panel, "UICheckButtonTemplate")
	broadcast:SetSize(24, 24); broadcast:SetPoint("TOPLEFT", 10, -82)
	broadcast.Text:SetText("Broadcast countdown and message to group")
	broadcast.Text:SetTextColor(unpack(Theme.Colors.text))
	broadcast:SetChecked(settings.broadcastToGroup == true)
	broadcast:SetScript("OnClick", function(self) settings.broadcastToGroup = self:GetChecked() == true end)
	for index = 1, 3 do
		local edit = CreateFrame("EditBox", nil, panel, "InputBoxTemplate")
		edit:SetSize(54, 24); edit:SetAutoFocus(false); edit:SetNumeric(false); edit:SetText(tostring(settings.presets[index] or 5))
		edit:SetPoint("TOPLEFT", 13 + ((index - 1) * 104), -118)
		local start = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate")
		start:SetSize(45, 24); start:SetText("Start"); start:SetPoint("LEFT", edit, "RIGHT", 3, 0)
		start:SetScript("OnClick", function()
			messageEdit:ClearFocus()
			local value = tonumber(edit:GetText())
			if value and value > 0 then settings.presets[index] = value end
			InformationBars:StartClockAlarm(value)
		end)
	end
	local status = Theme:CreateText(panel, 11); status:SetPoint("BOTTOMLEFT", 13, 14); status:SetText("No alarm running")
	local cancel = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate")
	cancel:SetSize(62, 24); cancel:SetPoint("BOTTOMRIGHT", -10, 8); cancel:SetText("Cancel")
	cancel:SetScript("OnClick", function() InformationBars:StopClockAlarm(false) end)
	panel:Hide(); self.ClockAlarmPanel, self.ClockAlarmStatus = panel, status
	return panel
end

local function OpenClockControls(owner, button)
	local settings = SlickUI.DB.informationBars.clock
	if button == "RightButton" then
		if not MenuUtil or type(MenuUtil.CreateContextMenu) ~= "function" then return end
		MenuUtil.CreateContextMenu(owner, function(_, root)
			root:SetTag("SLICKUI_CLOCK")
			root:CreateTitle("Clock")
			root:CreateRadio("Server time", function() return not settings.useLocalTime end, function() settings.useLocalTime = false; InformationBars:UpdateBlocks() end)
			root:CreateRadio("Local time", function() return settings.useLocalTime == true end, function() settings.useLocalTime = true; InformationBars:UpdateBlocks() end)
			root:CreateDivider()
			root:CreateRadio("24-hour format", function() return settings.use24Hour ~= false end, function() settings.use24Hour = true; InformationBars:UpdateBlocks() end)
			root:CreateRadio("12-hour format", function() return settings.use24Hour == false end, function() settings.use24Hour = false; InformationBars:UpdateBlocks() end)
		end)
		return
	end
	local panel = InformationBars:CreateClockAlarmPanel(owner)
	panel:ClearAllPoints()
	if (owner:GetCenter() or 0) > (UIParent:GetWidth() / 2) then
		panel:SetPoint("TOPRIGHT", owner, "BOTTOMRIGHT", 0, -6)
	else
		panel:SetPoint("TOPLEFT", owner, "BOTTOMLEFT", 0, -6)
	end
	if (owner:GetTop() or 0) < (UIParent:GetHeight() / 2) then
		panel:ClearAllPoints()
		panel:SetPoint("BOTTOM", owner, "TOP", 0, 6)
	end
	panel:SetShown(not panel:IsShown())
end

local function PerformanceText()
	local fps = SafeNumber(GetFramerate())
	local _, _, home, world = GetNetStats()
	home, world = SafeNumber(home), SafeNumber(world)
	if not fps then return "Performance: ?" end
	local roundedFPS = math.floor(fps + 0.5)
	local fpsColor = roundedFPS >= 60 and "ff42df72" or roundedFPS >= 30 and "ffffd24a" or "ffff4a45"
	local latency = world or home
	if not latency then return string.format("FPS: |c%s%d|r  Latency: ?", fpsColor, roundedFPS) end
	local latencyColor = latency <= 100 and "ff42df72" or latency <= 200 and "ffffd24a" or "ffff4a45"
	return string.format("FPS: |c%s%d|r  Latency: |c%s%dms|r", fpsColor, roundedFPS, latencyColor, latency)
end

local MemorySnapshot = { total = 0, addons = {}, updated = 0 }

local function RefreshMemorySnapshot(force)
	local now = SafeNumber(GetTime()) or 0
	if not force and MemorySnapshot.updated > 0 and now - MemorySnapshot.updated < 10 then return end
	MemorySnapshot.updated = now
	MemorySnapshot.total = 0
	wipe(MemorySnapshot.addons)
	if type(UpdateAddOnMemoryUsage) == "function" then pcall(UpdateAddOnMemoryUsage) end
	local count = C_AddOns and C_AddOns.GetNumAddOns and C_AddOns.GetNumAddOns() or 0
	for index = 1, count do
		local name = C_AddOns.GetAddOnName(index)
		local ok, memory = pcall(GetAddOnMemoryUsage, index)
		memory = ok and SafeNumber(memory) or nil
		if memory and memory > 0 and IsReadable(name) and type(name) == "string" then
			local title = name
			if C_AddOns.GetAddOnTitle then
				local titleOK, addonTitle = pcall(C_AddOns.GetAddOnTitle, name)
				if titleOK then title = addonTitle end
			end
			if not IsReadable(title) or type(title) ~= "string" or title == "" then title = name end
			MemorySnapshot.total = MemorySnapshot.total + memory
			MemorySnapshot.addons[#MemorySnapshot.addons + 1] = { name = title, memory = memory }
		end
	end
	table.sort(MemorySnapshot.addons, function(left, right) return left.memory > right.memory end)
end

local function FormatMemory(kilobytes)
	if kilobytes >= 1024 then return string.format("%.1f MB", kilobytes / 1024) end
	return string.format("%d KB", math.floor(kilobytes + 0.5))
end

local function MemoryText()
	RefreshMemorySnapshot(false)
	return "Memory: " .. FormatMemory(MemorySnapshot.total)
end

local function MemoryTooltip(tooltip)
	RefreshMemorySnapshot(false)
	tooltip:AddLine("Addon Memory", unpack(Theme.Colors.goldText))
	tooltip:AddDoubleLine("Total loaded addon memory", FormatMemory(MemorySnapshot.total), 1, 1, 1, 0.40, 0.82, 1.00)
	tooltip:AddLine(" ")
	local shown = math.min(15, #MemorySnapshot.addons)
	for index = 1, shown do
		local addon = MemorySnapshot.addons[index]
		tooltip:AddDoubleLine(addon.name, FormatMemory(addon.memory), 0.92, 0.94, 1.00, 0.72, 0.82, 1.00)
	end
	if #MemorySnapshot.addons > shown then
		tooltip:AddLine(string.format("...and %d more addons", #MemorySnapshot.addons - shown), unpack(Theme.Colors.mutedText))
	end
	tooltip:AddLine("Right-click to toggle the live memory monitor.", 0.40, 0.82, 1.00, true)
end

local function GetMemoryByName(name)
	for _, addon in ipairs(MemorySnapshot.addons) do
		if addon.name == name then return addon.memory end
	end
	return 0
end

function InformationBars:RefreshMemoryMonitor()
	local monitor = self.MemoryMonitor
	if not monitor or not monitor:IsShown() then return end
	RefreshMemorySnapshot(true)
	local slick = GetMemoryByName("SlickUI")
	if not monitor.Baseline then monitor.Baseline = slick end
	local delta = slick - monitor.Baseline
	local deltaColor = delta > 1024 and "ffff4a45" or delta > 256 and "ffffd24a" or "ff42df72"
	monitor.Slick:SetText(string.format("SlickUI: |cff42dfff%s|r   Change: |c%s%s%s|r",
		FormatMemory(slick), deltaColor, delta >= 0 and "+" or "", FormatMemory(delta)))
	monitor.Total:SetText("All addons: " .. FormatMemory(MemorySnapshot.total) .. "   Updated every 2 seconds")
	for index, row in ipairs(monitor.Rows) do
		local addon = MemorySnapshot.addons[index]
		if addon then
			row.Name:SetText(addon.name)
			row.Value:SetText(FormatMemory(addon.memory))
			row:Show()
		else
			row:Hide()
		end
	end
	self:UpdateBlocks()
end

function InformationBars:ApplyMemoryMonitorFonts()
	local monitor = self.MemoryMonitor
	if not monitor then return end
	local settings = SlickUI.DB and SlickUI.DB.informationBars
	local size = math.max(8, math.min(18, tonumber(settings and settings.memoryFontSize) or 10))
	Theme:SetFont(monitor.Title, size + 8)
	Theme:SetFont(monitor.Slick, size + 3)
	Theme:SetFont(monitor.Total, size + 1)
	Theme:SetFont(monitor.Note, size)
	local rowHeight = math.max(19, size + 4)
	for index, row in ipairs(monitor.Rows) do
		Theme:SetFont(row.Name, size + 1)
		Theme:SetFont(row.Value, size + 1)
		row:SetHeight(rowHeight)
		row:ClearAllPoints()
		if index == 1 then
			row:SetPoint("TOPLEFT", monitor.Total, "BOTTOMLEFT", 1, -12)
		else
			row:SetPoint("TOPLEFT", monitor.Rows[index - 1], "BOTTOMLEFT", 0, -1)
		end
	end
	monitor:SetHeight(310 + (math.max(0, size - 10) * 7))
end

function InformationBars:OpenMemoryFontMenu(owner)
	if not MenuUtil or type(MenuUtil.CreateContextMenu) ~= "function" then
		SlickUI:Print("WoW's context-menu system is unavailable.")
		return
	end
	MenuUtil.CreateContextMenu(owner, function(_, root)
		root:SetTag("SLICKUI_MEMORY_MONITOR_FONT")
		root:CreateTitle("Live Memory Font Size")
		for size = 8, 18 do
			local selectedSize = size
			root:CreateRadio(selectedSize .. " pt",
				function(value)
					local settings = SlickUI.DB and SlickUI.DB.informationBars
					return math.max(8, math.min(18, tonumber(settings and settings.memoryFontSize) or 10)) == value
				end,
				function(value)
					SlickUI.DB.informationBars.memoryFontSize = value
					InformationBars:ApplyMemoryMonitorFonts()
				end,
				selectedSize)
		end
	end)
end

function InformationBars:CreateMemoryMonitor()
	if self.MemoryMonitor then return self.MemoryMonitor end
	local frame = CreateFrame("Frame", "SlickUIMemoryMonitor", UIParent, "BackdropTemplate")
	frame:SetSize(390, 310)
	frame:SetPoint("CENTER", UIParent, "CENTER", 0, 80)
	frame:SetFrameStrata("DIALOG")
	frame:SetMovable(true)
	frame:EnableMouse(true)
	frame:RegisterForDrag("LeftButton")
	frame:SetClampedToScreen(true)
	Theme:CreatePanel(frame)
	frame:SetScript("OnDragStart", function(self) self:StartMoving() end)
	frame:SetScript("OnDragStop", function(self) self:StopMovingOrSizing() end)
	frame:SetScript("OnMouseUp", function(self, button)
		if button == "RightButton" then InformationBars:OpenMemoryFontMenu(self) end
	end)

	local title = Theme:CreateText(frame, 18)
	title:SetPoint("TOPLEFT", 16, -14)
	title:SetText("SlickUI Live Memory")
	title:SetTextColor(unpack(Theme.Colors.goldText))
	frame.Title = title
	local close = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
	close:SetPoint("TOPRIGHT", -4, -4)
	local slick = Theme:CreateText(frame, 13)
	slick:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -12)
	frame.Slick = slick
	local total = Theme:CreateText(frame, 11)
	total:SetPoint("TOPLEFT", slick, "BOTTOMLEFT", 0, -5)
	total:SetTextColor(unpack(Theme.Colors.mutedText))
	frame.Total = total

	frame.Rows = {}
	for index = 1, 10 do
		local row = CreateFrame("Frame", nil, frame)
		row:SetSize(356, 19)
		row:SetPoint("TOPLEFT", 17, -91 - ((index - 1) * 20))
		local name = Theme:CreateText(row, 11)
		name:SetPoint("LEFT", 0, 0)
		local value = Theme:CreateText(row, 11)
		value:SetPoint("RIGHT", 0, 0)
		value:SetTextColor(0.40, 0.82, 1.00)
		row.Name, row.Value = name, value
		frame.Rows[index] = row
	end
	local note = Theme:CreateText(frame, 10)
	note:SetPoint("BOTTOMLEFT", 16, 12)
	note:SetText("Close and reopen to reset the SlickUI change measurement.")
	note:SetTextColor(unpack(Theme.Colors.mutedText))
	frame.Note = note
	frame:SetScript("OnShow", function(self)
		self.Baseline = nil
		self.Elapsed = 2
	end)
	frame:SetScript("OnUpdate", function(self, elapsed)
		self.Elapsed = (self.Elapsed or 0) + elapsed
		if self.Elapsed >= 2 then
			self.Elapsed = 0
			InformationBars:RefreshMemoryMonitor()
		end
	end)
	frame:Hide()
	self.MemoryMonitor = frame
	self:ApplyMemoryMonitorFonts()
	return frame
end

local function RefreshMemory(_, button)
	if button == "RightButton" then
		local monitor = InformationBars:CreateMemoryMonitor()
		if monitor:IsShown() then monitor:Hide() else monitor:Show() end
		return
	end
	RefreshMemorySnapshot(true)
	InformationBars:UpdateBlocks()
end

local function GetSpeedValues()
	local current, run, flight, swim = GetUnitSpeed("player")
	return SafeNumber(current), SafeNumber(run), SafeNumber(flight), SafeNumber(swim)
end

-- WoW's GetUnitSpeed return values for running, swimming, and flying already
-- include active forms and movement modifiers. Using the returned run value as
-- the denominator therefore made Ghost Wolf and similar effects read 100%.
-- Normal unmodified player running speed is 7 yards per second.
local NORMAL_RUN_SPEED = 7

local function SpeedText()
	local current = GetSpeedValues()
	if not current then return "Speed: ?" end
	return string.format("Speed: %d%%", math.floor(current / NORMAL_RUN_SPEED * 100 + 0.5))
end

local function SpeedTooltip(tooltip)
	local current, run, flight, swim = GetSpeedValues()
	tooltip:AddLine("Movement Speed", unpack(Theme.Colors.goldText))
	if not current then
		tooltip:AddLine("Current movement speed is unavailable.", 1, 1, 1, true)
		return
	end
	tooltip:AddDoubleLine("Current", string.format("%.1f yd/s (%d%%)", current, math.floor(current / NORMAL_RUN_SPEED * 100 + 0.5)), 1, 1, 1, 0.40, 0.82, 1.00)
	tooltip:AddDoubleLine("Normal run baseline", "7.0 yd/s (100%)", 0.80, 0.84, 0.92, 1, 1, 1)
	if run then tooltip:AddDoubleLine("Run speed", string.format("%.1f yd/s", run), 0.80, 0.84, 0.92, 1, 1, 1) end
	if swim and swim > 0 then tooltip:AddDoubleLine("Swim speed", string.format("%.1f yd/s", swim), 0.80, 0.84, 0.92, 1, 1, 1) end
	if flight and flight > 0 then tooltip:AddDoubleLine("Flight speed", string.format("%.1f yd/s", flight), 0.80, 0.84, 0.92, 1, 1, 1) end
end

local function XPText()
	if SlickUI.XPTracker then return SlickUI.XPTracker:GetBarText() end
	local level = SafeNumber(UnitLevel("player"))
	local current, maximum = SafeNumber(UnitXP("player")), SafeNumber(UnitXPMax("player"))
	if current and maximum and maximum > 0 then
		return string.format("Level %d: %s/%s XP", level or 0, Compact(current), Compact(maximum))
	end
	return string.format("Level: %s", level or "?")
end

local function XPTooltip(tooltip)
	if SlickUI.XPTracker then SlickUI.XPTracker:BuildTooltip(tooltip) end
end

local function OpenXPTracker()
	if SlickUI.XPTracker then SlickUI.XPTracker:ToggleWindow() end
end

local function OpenCharacter()
	if ToggleCharacter then ToggleCharacter("PaperDollFrame") end
end

local function OpenMap()
	if ToggleWorldMap then ToggleWorldMap() end
end

local function OpenBags()
	if OpenAllBags then OpenAllBags() end
end

local function WindowMoveText()
	local enabled = SlickUI.DB and not SlickUI.DB.locked
	return enabled and "Windows: |cff42df72Move|r" or "Windows: |cffffd24aLocked|r"
end

local function WindowMoveTooltip(tooltip)
	local enabled = SlickUI.DB and not SlickUI.DB.locked
	tooltip:AddLine("SlickUI Window Move", unpack(Theme.Colors.goldText))
	if enabled then
		tooltip:AddLine("Window Move is enabled. Drag any visible SlickUI mover, including player, target, party, raid, action-bar, tracker, and utility frames.", 1, 1, 1, true)
		tooltip:AddLine("Left-click to lock all SlickUI windows again.", 0.40, 0.82, 1.00, true)
	else
		tooltip:AddLine("Left-click to unlock all registered SlickUI windows and display their mover overlays.", 1, 1, 1, true)
	end
	tooltip:AddLine("Right-click also opens SlickUI Edit Mode directly.", 0.55, 0.88, 1.00, true)
	tooltip:AddLine("The minimap button bar retains its own lock and movement control.", 0.75, 0.75, 0.75, true)
	tooltip:AddLine("Protected windows remain locked during combat.", 1.00, 0.52, 0.30, true)
end

local function ToggleWindowMove(_, button)
	if button == "RightButton" then
		if SlickUI.Movers then SlickUI.Movers:SetLocked(false) end
	elseif SlickUI.Movers then
		SlickUI.Movers:SetLocked(not SlickUI.DB.locked)
	end
end

local function GroupItemLevelText()
	return SlickUI.GroupItemLevels and SlickUI.GroupItemLevels:GetBarText() or "Group iLvl: ?"
end

local function GroupItemLevelTooltip(tooltip)
	if SlickUI.GroupItemLevels then SlickUI.GroupItemLevels:BuildTooltip(tooltip) end
end

local function GearHighsText()
	return SlickUI.CharacterEquipment and SlickUI.CharacterEquipment:GetHighsSummary() or "Gear Highs"
end

local function GearHighsTooltip(tooltip)
	if SlickUI.CharacterEquipment then SlickUI.CharacterEquipment:BuildHighsTooltip(tooltip) end
end

local function OpenGearHighs()
	if SlickUI.CharacterEquipment then SlickUI.CharacterEquipment:ToggleHighsWindow() end
end

local function OpenGroupItemLevels()
	if SlickUI.GroupItemLevels then SlickUI.GroupItemLevels:ToggleWindow() end
end

local function GetCurrentSpecialization()
	local index = SafeNumber(GetSpecialization and GetSpecialization())
	if not index then return nil end
	local specializationID, name, description, icon, role = GetSpecializationInfo(index)
	if not IsReadable(name) or type(name) ~= "string" or name == "" then return nil end
	return specializationID, name, description, icon, role
end

local function SpecializationText()
	local _, name = GetCurrentSpecialization()
	return "Spec: " .. (name or "Unavailable")
end

local function SpecializationTooltip(tooltip)
	local specializationID, name, description, _, role = GetCurrentSpecialization()
	tooltip:AddLine("Current Specialization", unpack(Theme.Colors.goldText))
	if not specializationID then
		tooltip:AddLine("Specialization information is currently unavailable.", 1, 1, 1, true)
		return
	end
	tooltip:AddDoubleLine("Specialization", name, 1, 1, 1, 0.40, 0.82, 1.00)
	if IsReadable(role) and type(role) == "string" and role ~= "" then
		tooltip:AddDoubleLine("Role", role, 0.80, 0.84, 0.92, 1, 1, 1)
	end
	if IsReadable(description) and type(description) == "string" and description ~= "" then
		tooltip:AddLine(" ")
		tooltip:AddLine(description, 0.82, 0.86, 0.94, true)
	end
end

local function OpenSpecialization()
	if InCombatLockdown() then
		SlickUI:Print("Specialization controls cannot be opened during combat.")
		return
	end

	if not PlayerSpellsFrame and C_AddOns and C_AddOns.LoadAddOn then
		pcall(C_AddOns.LoadAddOn, "Blizzard_PlayerSpells")
	end
	if not PlayerSpellsFrame then
		SlickUI:Print("The Blizzard specialization window is not available yet.")
		return
	end

	if PlayerSpellsFrame.TrySetTab and PlayerSpellsUtil and PlayerSpellsUtil.FrameTabs then
		pcall(PlayerSpellsFrame.TrySetTab, PlayerSpellsFrame, PlayerSpellsUtil.FrameTabs.ClassSpecializations)
	end
	if ShowUIPanel then
		ShowUIPanel(PlayerSpellsFrame)
	else
		PlayerSpellsFrame:Show()
	end
end

local function DifficultyName(id)
	id = SafeNumber(id)
	if not id or not GetDifficultyInfo then return "Unavailable" end
	local name = GetDifficultyInfo(id)
	return IsReadable(name) and type(name) == "string" and name ~= "" and name or "Unavailable"
end

local DifficultyColors = {
	[1] = { hex = "55FF55", rgb = { 0.33, 1.00, 0.33 } },   -- Dungeon Normal
	[2] = { hex = "4DA6FF", rgb = { 0.30, 0.65, 1.00 } },   -- Dungeon Heroic
	[23] = { hex = "B366FF", rgb = { 0.70, 0.40, 1.00 } },  -- Dungeon Mythic
	[14] = { hex = "55FF55", rgb = { 0.33, 1.00, 0.33 } },  -- Raid Normal
	[15] = { hex = "4DA6FF", rgb = { 0.30, 0.65, 1.00 } },  -- Raid Heroic
	[16] = { hex = "B366FF", rgb = { 0.70, 0.40, 1.00 } },  -- Raid Mythic
}

local function DifficultyColor(id)
	id = SafeNumber(id)
	return DifficultyColors[id] or { hex = "B8BEC9", rgb = { 0.72, 0.75, 0.79 } }
end

local function ColoredDifficultyName(id)
	local color = DifficultyColor(id)
	return "|cFF" .. color.hex .. DifficultyName(id) .. "|r"
end

local function GetCurrentDifficulties()
	local dungeonID = GetDungeonDifficultyID and GetDungeonDifficultyID() or nil
	local raidID = GetRaidDifficultyID and GetRaidDifficultyID() or nil
	return DifficultyName(dungeonID), DifficultyName(raidID), dungeonID, raidID
end

local function DifficultyText()
	local _, _, dungeonID, raidID = GetCurrentDifficulties()
	return string.format("Difficulty: D %s / R %s", ColoredDifficultyName(dungeonID), ColoredDifficultyName(raidID))
end

local function DifficultyTooltip(tooltip)
	local dungeon, raid, dungeonID, raidID = GetCurrentDifficulties()
	local dungeonColor = DifficultyColor(dungeonID).rgb
	local raidColor = DifficultyColor(raidID).rgb
	tooltip:AddLine("Dungeon / Raid Difficulty", unpack(Theme.Colors.goldText))
	tooltip:AddDoubleLine("Dungeon", dungeon, 1, 1, 1, unpack(dungeonColor))
	tooltip:AddDoubleLine("Raid", raid, 1, 1, 1, unpack(raidColor))
	tooltip:AddLine(" ")
	tooltip:AddLine("Click to choose Normal, Heroic, or Mythic difficulty separately for dungeons and raids.", 0.82, 0.86, 0.94, true)
end

local DungeonDifficulties = {
	{ id = 1, fallback = "Normal" },
	{ id = 2, fallback = "Heroic" },
	{ id = 23, fallback = "Mythic" },
}

local RaidDifficulties = {
	{ id = 14, fallback = "Normal" },
	{ id = 15, fallback = "Heroic" },
	{ id = 16, fallback = "Mythic" },
}

local function CanChangeGroupDifficulty()
	return not IsInGroup() or UnitIsGroupLeader("player")
end

local function SetSelectedDifficulty(kind, id)
	if InCombatLockdown() then
		SlickUI:Print("Difficulty cannot be changed during combat.")
		return
	end
	if not CanChangeGroupDifficulty() then
		SlickUI:Print("Only the group leader can change dungeon or raid difficulty.")
		return
	end

	local setter = kind == "dungeon" and SetDungeonDifficultyID or SetRaidDifficultyID
	if type(setter) ~= "function" then
		SlickUI:Print("WoW's " .. kind .. " difficulty control is unavailable.")
		return
	end
	local ok, message = pcall(setter, id)
	if not ok then SlickUI:Print("Difficulty change failed: " .. tostring(message)) end
	C_Timer.After(0, function() InformationBars:UpdateBlocks() end)
	C_Timer.After(0.25, function() InformationBars:UpdateBlocks() end)
end

local function CreateDifficultyChoices(parent, kind, choices, currentGetter)
	for _, choice in ipairs(choices) do
		local label = DifficultyName(choice.id)
		if label == "Unavailable" then label = choice.fallback end
		label = "|cFF" .. DifficultyColor(choice.id).hex .. label .. "|r"
		parent:CreateRadio(label,
			function(id) return currentGetter and currentGetter() == id end,
			function(id) SetSelectedDifficulty(kind, id) end,
			choice.id)
	end

end

local function OpenDifficultyControls(owner)
	if InCombatLockdown() then
		SlickUI:Print("Difficulty controls cannot be opened during combat.")
		return
	end
	if not MenuUtil or type(MenuUtil.CreateContextMenu) ~= "function" then
		SlickUI:Print("WoW's difficulty menu system is unavailable.")
		return
	end

	MenuUtil.CreateContextMenu(owner or UIParent, function(_, root)
		root:SetTag("SLICKUI_DIFFICULTY")
		root:CreateTitle("Dungeon / Raid Difficulty")
		local dungeonID = GetDungeonDifficultyID and GetDungeonDifficultyID()
		local raidID = GetRaidDifficultyID and GetRaidDifficultyID()
		local dungeon = root:CreateButton("Dungeon: " .. ColoredDifficultyName(dungeonID))
		CreateDifficultyChoices(dungeon, "dungeon", DungeonDifficulties, GetDungeonDifficultyID)
		local raid = root:CreateButton("Raid: " .. ColoredDifficultyName(raidID))
		CreateDifficultyChoices(raid, "raid", RaidDifficulties, GetRaidDifficultyID)
		if not CanChangeGroupDifficulty() then
			root:QueueDivider()
			root:CreateTitle("Only the group leader can change difficulty.")
		end
	end)
end

InformationBars.Definitions = {
	location = { label = "Location", value = LocationText, tooltip = "Current zone and map coordinates", click = OpenMap },
	xp = { label = "Experience / Level", value = XPText, tooltip = "Current level, XP rate, and estimated time to level", tooltipBuilder = XPTooltip, click = OpenXPTracker, clickHint = "Click to open the XP Tracker." },
	gold = { label = "Gold", value = GoldText, tooltip = "Current balance, session change, and today's change", tooltipBuilder = GoldTooltip, click = OpenGoldLedger, clickHint = "Left-click: account balances. Right-click: Gold Ledger." },
	fishing = { label = "Fishing Ledger", value = FishingText, tooltip = "Session and lifetime fishing catches", tooltipBuilder = FishingTooltip, click = OpenFishingLedger, clickHint = "Click to open the Fishing Ledger." },
	bags = { label = "Bag Space", value = BagText, tooltip = "Available space in equipped bags", click = OpenBags },
	guild = { label = "Guild", value = GuildText, tooltip = "Online guild members", tooltipBuilder = GuildTooltip, click = OpenGuild, clickHint = "Click to open the Guild page." },
	friends = { label = "Friends", value = FriendsText, tooltip = "Online friends", tooltipBuilder = FriendsTooltip, click = OpenFriends, clickHint = "Click to open the Friends page." },
	craftingDashboard = { label = "Crafting Dashboard", value = CraftingDashboardText, tooltip = "Open the Recipe Profit Dashboard", tooltipBuilder = CraftingDashboardTooltip, click = OpenCraftingDashboard, clickHint = "Click to open the Recipe Profit Dashboard." },
	durability = { label = "Durability", value = DurabilityText, tooltip = "Durability of equipped items", tooltipBuilder = DurabilityTooltip, click = OpenCharacter, clickHint = "Click to open the Character window." },
	performance = { label = "Performance", value = PerformanceText, tooltip = "Current framerate and world latency" },
	clock = { label = "Clock", value = ClockText, tooltip = "Current local or realm time", click = OpenClockControls, clickHint = "Left-click: alarm presets. Right-click: clock settings." },
	memory = { label = "Addon Memory", value = MemoryText, tooltip = "Total memory used by loaded addons", tooltipBuilder = MemoryTooltip, click = RefreshMemory, clickHint = "Left-click to refresh. Right-click for the live monitor." },
	speed = { label = "Movement Speed", value = SpeedText, tooltip = "Current movement speed", tooltipBuilder = SpeedTooltip },
	windowMove = { label = "Window Move", value = WindowMoveText, tooltip = "Unlock supported Blizzard windows for direct dragging", tooltipBuilder = WindowMoveTooltip, click = ToggleWindowMove, clickHint = "Left-click: direct window movement. Right-click: SlickUI mover overlays." },
	groupItemLevel = { label = "Group Item Levels", value = GroupItemLevelText, tooltip = "Cached equipped item levels for the current group", tooltipBuilder = GroupItemLevelTooltip, click = OpenGroupItemLevels, clickHint = "Click to open the full group item-level roster." },
	gearHighs = { label = "Gear Highs", value = GearHighsText, tooltip = "Highest appropriate item levels recorded for this character", tooltipBuilder = GearHighsTooltip, click = OpenGearHighs, clickHint = "Click to open the Gear Highs window." },
	interruptCooldowns = {
		label = "Group Interrupts",
		value = function() return SlickUI.InterruptCooldowns and SlickUI.InterruptCooldowns:GetWidgetText() or "Interrupts" end,
		tooltip = "Estimated interrupt readiness for party and raid members",
		click = function(owner, button)
			if SlickUI.InterruptCooldowns then SlickUI.InterruptCooldowns:HandleWidgetClick(owner, button) end
		end,
		clickHint = "Left-click to show or hide the floating roster. Right-click to configure.",
	},
	specialization = { label = "Current Specialization", value = SpecializationText, tooltip = "Current class specialization", tooltipBuilder = SpecializationTooltip, click = OpenSpecialization, clickHint = "Click to open the specialization screen." },
	difficulty = { label = "Dungeon / Raid Difficulty", value = DifficultyText, tooltip = "Current dungeon and raid difficulty settings", tooltipBuilder = DifficultyTooltip, click = OpenDifficultyControls, clickHint = "Click to choose dungeon and raid difficulty." },
	rangeCheck = {
		label = "Range Check",
		value = function()
			return SlickUI.RangeDisplay and SlickUI.RangeDisplay:GetWidgetText() or "Range: Off"
		end,
		tooltip = "Target range display",
		tooltipBuilder = function(tooltip)
			if SlickUI.RangeDisplay then SlickUI.RangeDisplay:BuildTooltip(tooltip) end
		end,
		click = function(owner, button)
			if SlickUI.RangeDisplay then SlickUI.RangeDisplay:HandleWidgetClick(owner, button) end
		end,
		clickHint = "Left-click to show or hide. Right-click to choose font size.",
	},
	mythicKeys = {
		label = "Mythic + Keys",
		value = function()
			return SlickUI.MythicKeys and SlickUI.MythicKeys:GetBarText() or "Mythic + Keys"
		end,
		tooltip = "Known Mythic+ keystones",
		tooltipBuilder = function(tooltip)
			if SlickUI.MythicKeys then SlickUI.MythicKeys:BuildTooltip(tooltip) end
		end,
		click = function()
			if SlickUI.MythicKeys then SlickUI.MythicKeys:ToggleWindow() end
		end,
		clickHint = "Click to open the searchable Mythic + Keys roster.",
	},
	groupControls = {
		label = "Group Controls",
		value = GroupControlsText,
		tooltip = "Open or close SlickUI's raid and group command panel",
		click = ToggleGroupControls,
		clickHint = "Click to open or close Group Controls.",
	},
	groupMarkers = {
		label = "Apply Group Marks",
		value = function() return SlickUI.GroupMarkers and SlickUI.GroupMarkers:GetWidgetText() or "Marks: Apply" end,
		tooltip = "Open secure tank and healer marker buttons",
		tooltipBuilder = function(tooltip)
			if SlickUI.GroupMarkers then SlickUI.GroupMarkers:BuildTooltip(tooltip) end
		end,
		click = function()
			if SlickUI.GroupMarkers then SlickUI.GroupMarkers:Apply(true) end
		end,
		clickHint = "Click to open the secure group marker panel.",
	},
	friendlyNameplates = {
		label = "Friendly Nameplates",
		value = FriendlyNameplatesText,
		tooltip = "Blizzard friendly-player nameplate mode",
		tooltipBuilder = FriendlyNameplatesTooltip,
		click = FriendlyNameplatesClick,
		clickHint = "Left-click to cycle Off, Names, and Bars. Right-click to configure.",
	},
}

local function ExternalKey(name)
	return "ldb:" .. name
end

local function GetExternalText(object, name)
	local text = object.text
	if IsReadable(text) and type(text) == "string" and text ~= "" then return text end
	local value, suffix = object.value, object.suffix
	if IsReadable(value) and (type(value) == "string" or type(value) == "number") then
		local result = tostring(value)
		if IsReadable(suffix) and type(suffix) == "string" then result = result .. suffix end
		return result
	end
	local label = object.label
	return IsReadable(label) and type(label) == "string" and label or name
end

local function GetConfig()
	return SlickUI.DB.informationBars
end

local function GetBarSide(id)
	return id:match("^top") and "top" or "bottom"
end

function InformationBars:GetInsets()
	local top, bottom = 0, 0
	for _, id in ipairs(self.BarOrder) do
		local config = GetConfig().bars[id]
		if config.enabled then
			if GetBarSide(id) == "top" then top = top + config.height else bottom = bottom + config.height end
		end
	end
	return top, bottom
end

function InformationBars:ApplyScreenInsets()
	-- Never modify UIParent anchors. Even resetting UIParent to WorldFrame creates
	-- an addon-owned dependency in the root anchor family and can make Blizzard
	-- Edit Mode reject its BuffFrame-to-MinimapCluster layout as cyclic.
	self.PendingInsets = false
	return true
end

function InformationBars:ScreenInsetsMatch()
	return true
end

-- Blizzard's Edit Mode aura layout can re-anchor BuffFrame to the full-screen
-- top edge after UIParent has already been inset for SlickUI's information
-- bars. Clamp only an overlapping frame, preserving its horizontal placement
-- and any lower position selected by the player.
function InformationBars:ApplyAuraFrameClearance()
	-- BuffFrame is owned by Blizzard Edit Mode and may be anchored relative to
	-- MinimapCluster. The UIParent inset already supplies bar clearance; changing
	-- BuffFrame's points here can create a cyclic Edit Mode anchor dependency.
	return true
end

function InformationBars:HookAuraFrame()
	-- Intentionally do not hook or reposition Blizzard's Edit Mode aura frame.
end

function InformationBars:QueueAuraFrameClearance()
	self.AuraClearanceGeneration = (self.AuraClearanceGeneration or 0) + 1
	local generation = self.AuraClearanceGeneration
	for _, delay in ipairs({ 0, 0.25, 1, 3 }) do
		C_Timer.After(delay, function()
			if generation ~= InformationBars.AuraClearanceGeneration then return end
			InformationBars:HookAuraFrame()
			InformationBars:ApplyAuraFrameClearance()
		end)
	end
end

function InformationBars:CreateBar(id)
	local frame = CreateFrame("Frame", "SlickUIInformationBar_" .. id, WorldFrame, "BackdropTemplate")
	frame:SetFrameStrata("HIGH")
	frame:EnableMouse(true)
	frame:SetBackdrop({ bgFile = Theme.Texture, edgeFile = Theme.Texture, edgeSize = 1 })
	frame:SetBackdropColor(0.025, 0.03, 0.045, 1)
	frame:SetBackdropBorderColor(unpack(Theme.Colors.borderMuted))
	frame.SlickID = id

	local function ToggleLock()
		if InCombatLockdown() then return end
		local config = GetConfig().bars[id]
		config.locked = not config.locked
		InformationBars:RefreshLocks()
	end

	-- Widgets are mouse-enabled children and receive their own clicks first.
	-- A left-click that reaches the bar therefore came from otherwise-empty
	-- background space and can safely toggle arrangement mode.
	frame:SetScript("OnMouseUp", function(self, button)
		if button == "LeftButton" then
			ToggleLock()
		elseif button == "RightButton" then
			if not MenuUtil or type(MenuUtil.CreateContextMenu) ~= "function" then
				SlickUI:Print("WoW's context-menu system is unavailable.")
				return
			end
			MenuUtil.CreateContextMenu(self, function(_, root)
				root:SetTag("SLICKUI_INFORMATION_BAR_FONT")
				root:CreateTitle("Information Bar Font Size")
				for size = 8, 18 do
					local selectedSize = size
					root:CreateRadio(selectedSize .. " pt",
						function(value) return InformationBars:GetFontSize() == value end,
						function(value) InformationBars:SetFontSize(value) end,
						selectedSize)
				end
			end)
		end
	end)

	local lock = CreateFrame("Button", nil, frame)
	lock:SetSize(22, 22)
	lock:SetPoint("RIGHT", frame, "RIGHT", -4, 0)
	local icon = lock:CreateTexture(nil, "ARTWORK")
	icon:SetAllPoints()
	lock.Icon = icon
	lock:SetScript("OnClick", ToggleLock)
	lock:SetScript("OnEnter", function(self)
		GameTooltip:SetOwner(self, "ANCHOR_BOTTOMRIGHT")
		GameTooltip:AddLine(GetConfig().bars[id].locked and "Unlock this information bar" or "Lock this information bar")
		GameTooltip:AddLine(GetConfig().bars[id].locked and "Unlock to arrange data blocks." or "Lock to enable data-block clicks.", 1, 1, 1, true)
		GameTooltip:Show()
	end)
	lock:SetScript("OnLeave", GameTooltip_Hide)
	frame.LockButton = lock
	self.Bars[id] = frame
	return frame
end

function InformationBars:ApplyTheme()
	local pink = SlickUI.DB and SlickUI.DB.cute and SlickUI.DB.cute.pinkPonyClub == true
	for _, id in ipairs(self.BarOrder) do
		local bar, config = self.Bars[id], GetConfig().bars[id]
		if bar and config then
			if pink then
				bar:SetBackdropColor(0.24, 0.035, 0.13, config.opacity)
				bar:SetBackdropBorderColor(1.00, 0.24, 0.68, math.min(1, config.opacity + 0.08))
			else
				bar:SetBackdropColor(0.025, 0.03, 0.045, config.opacity)
				bar:SetBackdropBorderColor(Theme.Colors.borderMuted[1], Theme.Colors.borderMuted[2], Theme.Colors.borderMuted[3], math.min(1, config.opacity + 0.08))
			end
		end
	end
end

function InformationBars:ArrangeBars()
	local topOffset, bottomOffset = 0, 0
	for _, id in ipairs(self.BarOrder) do
		local bar, config = self.Bars[id], GetConfig().bars[id]
		bar:ClearAllPoints()
		bar:SetHeight(config.height)
		bar:SetAlpha(1)
		local pink = SlickUI.DB and SlickUI.DB.cute and SlickUI.DB.cute.pinkPonyClub == true
		if pink then
			bar:SetBackdropColor(0.24, 0.035, 0.13, config.opacity)
			bar:SetBackdropBorderColor(1.00, 0.24, 0.68, math.min(1, config.opacity + 0.08))
		else
			bar:SetBackdropColor(0.025, 0.03, 0.045, config.opacity)
			bar:SetBackdropBorderColor(Theme.Colors.borderMuted[1], Theme.Colors.borderMuted[2], Theme.Colors.borderMuted[3], math.min(1, config.opacity + 0.08))
		end
		if GetBarSide(id) == "top" then
			bar:SetPoint("TOPLEFT", WorldFrame, "TOPLEFT", 0, -topOffset)
			bar:SetPoint("TOPRIGHT", WorldFrame, "TOPRIGHT", 0, -topOffset)
			topOffset = topOffset + (config.enabled and config.height or 0)
		else
			bar:SetPoint("BOTTOMLEFT", WorldFrame, "BOTTOMLEFT", 0, bottomOffset)
			bar:SetPoint("BOTTOMRIGHT", WorldFrame, "BOTTOMRIGHT", 0, bottomOffset)
			bottomOffset = bottomOffset + (config.enabled and config.height or 0)
		end
		bar:SetShown(config.enabled and not self.CinematicActive)
	end
	self:ApplyScreenInsets()
end

function InformationBars:SetCinematicActive(active)
	self.CinematicGeneration = self.CinematicGeneration + 1
	local generation = self.CinematicGeneration

	if active then
		self.CinematicActive = true
		self:HideDropPreview()
		for _, bar in pairs(self.Bars) do bar:Hide() end
		self:ApplyScreenInsets()
		return
	end

	-- The cinematic viewport can take a frame or two to return to its normal
	-- dimensions. Keep the bars suspended until that transition is complete so
	-- their anchors and data blocks are never arranged against the squeezed view.
	C_Timer.After(0.25, function()
		if generation ~= InformationBars.CinematicGeneration then return end
		InformationBars.CinematicActive = false
		InformationBars:Refresh()
	end)
end

local function ShowBlockTooltip(frame)
	local definition = InformationBars.Definitions[frame.SlickKey]
	if definition.ldbObject then
		local object = definition.ldbObject
		if type(object.OnEnter) == "function" then
			local ok = pcall(object.OnEnter, frame)
			if ok then return end
		end
		if type(object.OnTooltipShow) == "function" then
			GameTooltip:SetOwner(frame, "ANCHOR_BOTTOM")
			GameTooltip:ClearLines()
			local ok = pcall(object.OnTooltipShow, GameTooltip)
			if ok then GameTooltip:Show() return end
		end
	end
	GameTooltip:SetOwner(frame, "ANCHOR_BOTTOM")
	GameTooltip:ClearLines()
	if definition.tooltipBuilder then
		local ok = pcall(definition.tooltipBuilder, GameTooltip)
		if not ok then
			GameTooltip:AddLine(definition.label, unpack(Theme.Colors.goldText))
			GameTooltip:AddLine(definition.tooltip, 1, 1, 1, true)
		end
		GameTooltip:AddLine("Drag to another snap point or information bar.", 0.65, 0.72, 0.82, true)
		local config = GetConfig().blocks[frame.SlickKey]
		if definition.click and GetConfig().bars[config.bar].locked then
			GameTooltip:AddLine(definition.clickHint or "Click to open.", 0.40, 0.82, 1.00, true)
		end
		GameTooltip:Show()
		return
	end
	GameTooltip:AddLine(definition.label, unpack(Theme.Colors.goldText))
	GameTooltip:AddLine(definition.tooltip, 1, 1, 1, true)
	GameTooltip:AddLine("Drag to another snap point or information bar.", 0.65, 0.72, 0.82, true)
	local config = GetConfig().blocks[frame.SlickKey]
	if definition.click and GetConfig().bars[config.bar].locked then
		GameTooltip:AddLine(definition.clickHint or "Click to open.", 0.40, 0.82, 1.00, true)
	end
	GameTooltip:Show()
end

function InformationBars:CreateBlock(key)
	local frame = CreateFrame("Button", nil, WorldFrame)
	frame:SetHeight(20)
	frame:SetMovable(true)
	frame:EnableMouse(true)
	frame:RegisterForDrag("LeftButton")
	frame:RegisterForClicks("AnyUp")
	frame.SlickKey = key

	local dragHighlight = CreateFrame("Frame", nil, frame, "BackdropTemplate")
	dragHighlight:SetAllPoints(frame)
	dragHighlight:SetFrameLevel(frame:GetFrameLevel() + 8)
	dragHighlight:SetBackdrop({ bgFile = Theme.Texture, edgeFile = Theme.Texture, edgeSize = 2 })
	dragHighlight:SetBackdropColor(0.15, 0.65, 1.00, 0.14)
	dragHighlight:SetBackdropBorderColor(0.30, 0.78, 1.00, 0.95)
	dragHighlight:EnableMouse(false)
	dragHighlight:Hide()
	frame.DragHighlight = dragHighlight

	local icon = frame:CreateTexture(nil, "ARTWORK")
	icon:SetSize(16, 16)
	icon:SetPoint("LEFT", 3, 0)
	icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
	icon:Hide()
	frame.Icon = icon

	local text = Theme:CreateText(frame, 11)
	text:SetPoint("LEFT", 5, 0)
	text:SetTextColor(unpack(Theme.Colors.text))
	frame.Text = text

	frame:SetScript("OnEnter", ShowBlockTooltip)
	frame:SetScript("OnLeave", function(self)
		local definition = InformationBars.Definitions[self.SlickKey]
		if definition and definition.ldbObject and type(definition.ldbObject.OnLeave) == "function" then
			pcall(definition.ldbObject.OnLeave, self)
		end
		GameTooltip:Hide()
	end)
	frame:SetScript("OnDragStart", function(self)
		if InCombatLockdown() then return end
		if GetConfig().bars[GetConfig().blocks[self.SlickKey].bar].locked then return end
		GameTooltip:Hide()
		local config = GetConfig().blocks[self.SlickKey]
		self.OldBar, self.OldX = config.bar, config.x
		self:ClearAllPoints()
		self:SetFrameStrata("DIALOG")
		self:SetAlpha(0.62)
		self:StartMoving()
		InformationBars:ShowDragTargets(self)
		InformationBars:UpdateDropPreview(self)
		self:SetScript("OnUpdate", function(dragged)
			InformationBars:UpdateDropPreview(dragged)
		end)
	end)
	frame:SetScript("OnDragStop", function(self)
		if not self.OldBar then return end
		self:SetScript("OnUpdate", nil)
		self:StopMovingOrSizing()
		self:SetFrameStrata("HIGH")
		self:SetAlpha(1)
		InformationBars:HideDropPreview()
		InformationBars:HideDragTargets()
		InformationBars:FinishBlockDrag(self)
		self.OldBar, self.OldX = nil, nil
	end)
	frame:SetScript("OnClick", function(self, button)
		local config = GetConfig().blocks[self.SlickKey]
		if not GetConfig().bars[config.bar].locked then return end
		local definition = InformationBars.Definitions[self.SlickKey]
		if definition.ldbObject and type(definition.ldbObject.OnClick) == "function" then
			local ok, message = pcall(definition.ldbObject.OnClick, self, button)
			if not ok then SlickUI:Print("External data-plugin click failed: " .. tostring(message)) end
		elseif definition.click then
			definition.click(self, button)
		end
	end)

	self.Blocks[key] = frame
	return frame
end

function InformationBars:ShowDragTargets(draggedFrame)
	for key, block in pairs(self.Blocks) do
		local config = GetConfig().blocks[key]
		if block ~= draggedFrame and config and config.enabled and block:IsShown() then
			block.DragHighlight:SetBackdropColor(0.15, 0.65, 1.00, 0.14)
			block.DragHighlight:SetBackdropBorderColor(0.30, 0.78, 1.00, 0.95)
			block.DragHighlight:Show()
		else
			block.DragHighlight:Hide()
		end
	end
end

function InformationBars:HideDragTargets()
	for _, block in pairs(self.Blocks) do
		if block.DragHighlight then block.DragHighlight:Hide() end
	end
end

function InformationBars:GetSwapTarget(frame, id, x)
	if not id then return nil end
	for otherKey, otherFrame in pairs(self.Blocks) do
		local other = GetConfig().blocks[otherKey]
		if otherKey ~= frame.SlickKey and other.enabled and other.bar == id then
			local overlap = math.abs((other.x + otherFrame:GetWidth() / 2) - (x + frame:GetWidth() / 2))
			if overlap < (otherFrame:GetWidth() + frame:GetWidth()) / 2 then return otherFrame end
		end
	end
end

function InformationBars:CreateDropPreview()
	if self.DropPreview then return self.DropPreview end
	local preview = CreateFrame("Frame", "SlickUIInformationBarDropPreview", WorldFrame, "BackdropTemplate")
	preview:SetFrameStrata("DIALOG")
	preview:SetBackdrop({ bgFile = Theme.Texture, edgeFile = Theme.Texture, edgeSize = 2 })
	preview:SetBackdropColor(1.00, 0.62, 0.08, 0.24)
	preview:SetBackdropBorderColor(1.00, 0.77, 0.24, 1.00)
	preview:EnableMouse(false)
	local text = Theme:CreateText(preview, 9)
	text:SetPoint("CENTER")
	text:SetText("DROP HERE")
	text:SetTextColor(unpack(Theme.Colors.goldText))
	preview.Label = text
	preview:Hide()
	self.DropPreview = preview
	return preview
end

function InformationBars:GetDropPosition(frame)
	local id, cursorX = self:FindBarAtCursor()
	if not id then return nil end
	local bar = self.Bars[id]
	local snap = GetConfig().snap
	local x = math.floor((cursorX - frame:GetWidth() / 2) / snap + 0.5) * snap
	x = math.max(0, math.min(x, bar:GetWidth() - frame:GetWidth() - 30))
	return id, x
end

function InformationBars:UpdateDropPreview(frame)
	local preview = self:CreateDropPreview()
	local id, x = self:GetDropPosition(frame)
	self:ShowDragTargets(frame)
	if not id then
		preview:Hide()
		return
	end
	local swapTarget = self:GetSwapTarget(frame, id, x)
	if swapTarget and swapTarget.DragHighlight then
		swapTarget.DragHighlight:SetBackdropColor(1.00, 0.62, 0.08, 0.28)
		swapTarget.DragHighlight:SetBackdropBorderColor(1.00, 0.82, 0.22, 1.00)
		preview.Label:SetText("SWAP WIDGET")
	else
		preview.Label:SetText("DROP HERE")
	end

	local bar = self.Bars[id]
	preview:SetParent(bar)
	preview:ClearAllPoints()
	preview:SetSize(frame:GetWidth(), frame:GetHeight())
	preview:SetPoint("LEFT", bar, "LEFT", x, 0)
	preview:Show()
	preview.SlickBar = id
	preview.SlickX = x
end

function InformationBars:HideDropPreview()
	if not self.DropPreview then return end
	self.DropPreview:Hide()
	self.DropPreview.SlickBar = nil
	self.DropPreview.SlickX = nil
	self:HideDragTargets()
end

function InformationBars:FindBarAtCursor()
	local cursorX, cursorY = GetCursorPosition()
	for _, id in ipairs(self.BarOrder) do
		local bar = self.Bars[id]
		if bar:IsShown() then
			local scale = bar:GetEffectiveScale()
			local x, y = cursorX / scale, cursorY / scale
			local left, right, bottom, top = bar:GetLeft(), bar:GetRight(), bar:GetBottom(), bar:GetTop()
			if left and right and bottom and top and x >= left and x <= right and y >= bottom and y <= top then
				return id, x - left
			end
		end
	end
end

function InformationBars:FinishBlockDrag(frame)
	local id, x = self:GetDropPosition(frame)
	local config = GetConfig().blocks[frame.SlickKey]
	if not id then
		config.bar, config.x = frame.OldBar, frame.OldX
		self:LayoutBlocks()
		return
	end

	local swapped
	for otherKey, otherFrame in pairs(self.Blocks) do
		local other = GetConfig().blocks[otherKey]
		if otherKey ~= frame.SlickKey and other.enabled and other.bar == id then
			local overlap = math.abs((other.x + otherFrame:GetWidth() / 2) - (x + frame:GetWidth() / 2))
			if overlap < (otherFrame:GetWidth() + frame:GetWidth()) / 2 then
				other.bar, other.x = frame.OldBar, frame.OldX
				swapped = true
				break
			end
		end
	end
	config.bar, config.x = id, x
	self:LayoutBlocks()
	if swapped then SlickUI:Print("Information blocks exchanged positions.") end
end

function InformationBars:UpdateBlocks()
	for key, frame in pairs(self.Blocks) do
		local definition = self.Definitions[key]
		Theme:SetFont(frame.Text, self:GetFontSize())
		local ok, value = pcall(definition.value)
		frame.Text:SetText(ok and value or (definition.label .. ": ?"))
		local icon = definition.ldbObject and definition.ldbObject.icon
		if IsReadable(icon) and (type(icon) == "string" or type(icon) == "number") then
			frame.Icon:SetTexture(icon)
			local coordinates = definition.ldbObject.iconCoords
			if type(coordinates) == "table" and #coordinates >= 4 then
				frame.Icon:SetTexCoord(coordinates[1], coordinates[2], coordinates[3], coordinates[4])
			else
				frame.Icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
			end
			frame.Icon:Show()
			frame.Text:ClearAllPoints()
			frame.Text:SetPoint("LEFT", 23, 0)
			frame:SetWidth(math.max(70, frame.Text:GetStringWidth() + 30))
		else
			frame.Icon:Hide()
			frame.Text:ClearAllPoints()
			frame.Text:SetPoint("LEFT", 5, 0)
			frame:SetWidth(math.max(70, frame.Text:GetStringWidth() + 10))
		end
	end
	self:LayoutBlocks()
end

function InformationBars:RegisterExternalObject(name, object)
	if type(name) ~= "string" or name == "SlickUI" or type(object) ~= "table" then return end
	local objectType = object.type
	if objectType ~= nil and objectType ~= "data source" and objectType ~= "launcher" then return end

	local key = ExternalKey(name)
	local label = object.label
	if not IsReadable(label) or type(label) ~= "string" or label == "" then label = name end
	self.Definitions[key] = {
		label = label,
		tooltip = "External addon data supplied through LibDataBroker.",
		isExternal = true,
		ldbName = name,
		ldbObject = object,
		value = function() return GetExternalText(object, name) end,
	}
	self.ExternalKeys[key] = true

	local config = GetConfig().blocks
	if type(config[key]) ~= "table" then
		config[key] = { enabled = false, bar = "top1", x = 20 }
	end
	if not self.Blocks[key] then self:CreateBlock(key) end
	self:UpdateBlocks()
	if SlickUI.Options and SlickUI.Options.RebuildInformationBlockChecks then
		SlickUI.Options:RebuildInformationBlockChecks()
		SlickUI.Options:RefreshInformationBarControls()
	end
end

function InformationBars:OnLDBCreated(_, name, object)
	self:RegisterExternalObject(name, object)
end

function InformationBars:OnLDBAttributeChanged(_, name)
	local frame = self.Blocks[ExternalKey(name)]
	if frame then self:UpdateBlocks() end
end

function InformationBars:DiscoverExternalObjects()
	self.LDB = LibStub("LibDataBroker-1.1", true)
	if not self.LDB then return end
	for name, object in self.LDB:DataObjectIterator() do self:RegisterExternalObject(name, object) end
	self.LDB.RegisterCallback(self, "LibDataBroker_DataObjectCreated", "OnLDBCreated")
	self.LDB.RegisterCallback(self, "LibDataBroker_AttributeChanged", "OnLDBAttributeChanged")
end

function InformationBars:LayoutBlocks()
	for key, frame in pairs(self.Blocks) do
		local blocks = GetConfig().blocks
		local config = blocks[key]
		if type(config) ~= "table" then
			config = { enabled = false, bar = "top1", x = 20 }
			blocks[key] = config
		end
		local bar = self.Bars[config.bar]
		if not frame.OldBar then
			frame:ClearAllPoints()
		end
		if not frame.OldBar and config.enabled and bar and bar:IsShown() then
			local x = math.max(0, math.min(config.x, math.max(0, bar:GetWidth() - frame:GetWidth() - 30)))
			config.x = x
			frame:SetParent(bar)
			frame:SetPoint("LEFT", bar, "LEFT", x, 0)
			frame:Show()
		elseif not frame.OldBar then
			frame:Hide()
		end
	end
end

function InformationBars:RefreshSnapGrid(bar, id)
	bar.SnapGrid = bar.SnapGrid or {}
	local config = GetConfig().bars[id]
	local show = config.enabled and not config.locked and bar:IsShown()
	local snap = math.max(5, tonumber(GetConfig().snap) or 20)
	local width = bar:GetWidth() or 0
	local needed = show and math.max(0, math.floor(width / snap)) or 0
	for index = 1, needed do
		local marker = bar.SnapGrid[index]
		if not marker then
			marker = bar:CreateTexture(nil, "OVERLAY")
			marker:SetColorTexture(0.30, 0.78, 1.00, 0.24)
			marker:SetWidth(1)
			bar.SnapGrid[index] = marker
		end
		marker:ClearAllPoints()
		marker:SetPoint("TOP", bar, "TOPLEFT", index * snap, -3)
		marker:SetPoint("BOTTOM", bar, "BOTTOMLEFT", index * snap, 3)
		marker:Show()
	end
	for index = needed + 1, #bar.SnapGrid do bar.SnapGrid[index]:Hide() end
end

function InformationBars:RefreshLocks()
	for id, bar in pairs(self.Bars) do
		local locked = GetConfig().bars[id].locked
		bar.LockButton.Icon:SetTexture(locked and "Interface\\Buttons\\LockButton-Locked-Up" or "Interface\\Buttons\\LockButton-Unlocked-Up")
		self:RefreshSnapGrid(bar, id)
	end
end

function InformationBars:Refresh()
	self:ArrangeBars()
	self:RefreshLocks()
	self:UpdateBlocks()
end

function InformationBars:SetBarField(id, field, value)
	GetConfig().bars[id][field] = value
	self:Refresh()
end

function InformationBars:GetFontSize()
	local config = GetConfig()
	local legacySize = config.bars.top1 and config.bars.top1.fontSize
	return math.max(8, math.min(18, tonumber(config.fontSize) or tonumber(legacySize) or 11))
end

function InformationBars:SetFontSize(value)
	GetConfig().fontSize = math.max(8, math.min(18, tonumber(value) or 11))
	self:Refresh()
end

function InformationBars:FindOpenPosition(id, key)
	local bar = self.Bars[id]
	local frame = self.Blocks[key]
	if not bar or not frame then return nil end

	local snap = GetConfig().snap
	local width = frame:GetWidth()
	local rightLimit = math.max(0, math.floor((bar:GetWidth() - width - 30) / snap) * snap)
	for candidate = rightLimit, 0, -snap do
		local available = true
		for otherKey, otherFrame in pairs(self.Blocks) do
			local other = GetConfig().blocks[otherKey]
			if otherKey ~= key and other and other.enabled and other.bar == id then
				local gap = 6
				local candidateRight = candidate + width
				local otherRight = other.x + otherFrame:GetWidth()
				if candidate < otherRight + gap and candidateRight + gap > other.x then
					available = false
					break
				end
			end
		end
		if available then return candidate end
	end
	return nil
end

function InformationBars:SetBlockOnBar(key, id, enabled)
	local config = GetConfig().blocks[key]
	config.enabled = enabled == true
	if config.enabled then
		config.bar = id
		local openPosition = self:FindOpenPosition(id, key)
		if openPosition == nil then
			config.enabled = false
			SlickUI:Print("No open snap position is available on " .. id .. ".")
		else
			config.x = openPosition
		end
	end
	self:LayoutBlocks()
end

function InformationBars:ResetLayout()
	local defaults = SlickUI.Defaults.informationBars
	for key, value in pairs(defaults.blocks) do
		local config = GetConfig().blocks[key]
		config.enabled, config.bar, config.x = value.enabled, value.bar, value.x
	end
	self:Refresh()
end

function InformationBars:Initialize()
	for _, id in ipairs(self.BarOrder) do self:CreateBar(id) end
	local blockConfig = GetConfig().blocks
	for key in pairs(self.Definitions) do
		if type(blockConfig[key]) ~= "table" then
			local defaults = SlickUI.Defaults.informationBars.blocks[key]
			blockConfig[key] = defaults and {
				enabled = defaults.enabled == true,
				bar = defaults.bar or "top1",
				x = tonumber(defaults.x) or 20,
			} or { enabled = false, bar = "top1", x = 20 }
		end
		self:CreateBlock(key)
	end

	local driver = CreateFrame("Frame")
	driver:RegisterEvent("ADDON_LOADED")
	driver:RegisterEvent("PLAYER_ENTERING_WORLD")
	driver:RegisterEvent("PLAYER_REGEN_ENABLED")
	driver:RegisterEvent("CINEMATIC_START")
	driver:RegisterEvent("CINEMATIC_STOP")
	driver:RegisterEvent("GUILD_ROSTER_UPDATE")
	driver:RegisterEvent("PLAYER_GUILD_UPDATE")
	driver:RegisterEvent("FRIENDLIST_UPDATE")
	driver:RegisterEvent("BN_FRIEND_ACCOUNT_ONLINE")
	driver:RegisterEvent("BN_FRIEND_ACCOUNT_OFFLINE")
	driver:RegisterEvent("BN_FRIEND_INFO_CHANGED")
	driver:SetScript("OnEvent", function(_, event, loadedAddon)
		if event == "ADDON_LOADED" then
			if loadedAddon == "Blizzard_BuffFrame" then InformationBars:QueueAuraFrameClearance() end
			return
		end
		if event == "CINEMATIC_START" then
			InformationBars:SetCinematicActive(true)
			return
		elseif event == "CINEMATIC_STOP" then
			InformationBars:SetCinematicActive(false)
			return
		end
		if event == "PLAYER_ENTERING_WORLD" then
			InformationBars.ReadyForInsets = true
			InformationBars:QueueAuraFrameClearance()
		end
		if InformationBars.PendingInsets or event == "PLAYER_ENTERING_WORLD" then InformationBars:ApplyScreenInsets() end
	end)
	driver:SetScript("OnUpdate", function(_, elapsed)
		if InformationBars.CinematicActive then return end
		InformationBars.Elapsed = (InformationBars.Elapsed or 0) + elapsed
		if InformationBars.Elapsed >= 1 then
			InformationBars.Elapsed = 0
			-- Blizzard can restore UIParent to the full-screen anchors late in the
			-- login sequence. The settings checkbox fixes that by applying the
			-- inset again; keep the same invariant automatically without writing
			-- anchors every frame when they are already correct.
			if not InformationBars:ScreenInsetsMatch() then
				InformationBars:ApplyScreenInsets()
			end
			InformationBars:UpdateBlocks()
		end
	end)
	self.Driver = driver
	self:Refresh()
	self:DiscoverExternalObjects()
end

SlickUI:RegisterModule("InformationBars", InformationBars)
