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()
	if SlickUI.GoldLedger then SlickUI.GoldLedger:ToggleWindow() 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 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 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 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
end

local function RefreshMemory()
	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

local function SpeedText()
	local current, run = GetSpeedValues()
	local baseline = run and run > 0 and run or 7
	if not current then return "Speed: ?" end
	return string.format("Speed: %d%%", math.floor(current / baseline * 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
	local baseline = run and run > 0 and run or 7
	tooltip:AddDoubleLine("Current", string.format("%.1f yd/s (%d%%)", current, math.floor(current / baseline * 100 + 0.5)), 1, 1, 1, 0.40, 0.82, 1.00)
	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.ExternalFrames and SlickUI.ExternalFrames:IsQuickMoveEnabled()
	return enabled and "Windows: |cff42df72Move|r" or "Windows: |cffffd24aLocked|r"
end

local function WindowMoveTooltip(tooltip)
	local enabled = SlickUI.ExternalFrames and SlickUI.ExternalFrames:IsQuickMoveEnabled()
	tooltip:AddLine("Blizzard Window Move", unpack(Theme.Colors.goldText))
	if enabled then
		tooltip:AddLine("Window Move is enabled. Drag a supported Blizzard window directly from an unused part of that window.", 1, 1, 1, true)
		tooltip:AddLine("Left-click to lock windows again.", 0.40, 0.82, 1.00, true)
	else
		tooltip:AddLine("Left-click to temporarily unlock supported Blizzard windows for direct dragging. SlickUI does not display mover overlays in this mode.", 1, 1, 1, true)
	end
	tooltip:AddLine("Right-click to open SlickUI Edit Mode with mover overlays.", 0.55, 0.88, 1.00, 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.ExternalFrames then
		SlickUI.ExternalFrames:ToggleQuickMove()
	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 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 Ledger", value = GoldText, tooltip = "Current balance, session change, and today's change", tooltipBuilder = GoldTooltip, click = OpenGoldLedger, clickHint = "Click to open the 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 },
	durability = { label = "Durability", value = DurabilityText, tooltip = "Lowest durability among equipped items", click = OpenCharacter },
	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 = "Click to refresh addon memory measurements." },
	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." },
	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.",
	},
}

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: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 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", function()
		if InCombatLockdown() then return end
		local config = GetConfig().bars[id]
		config.locked = not config.locked
		InformationBars:RefreshLocks()
	end)
	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: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)
		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))
		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 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: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: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: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)
	if not id then
		preview:Hide()
		return
	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
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]
		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 config = GetConfig().blocks[key]
		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: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")
	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: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
	for key in pairs(self.Definitions) do 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: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)
