local _, addonTable = ...
local WipedQoL = addonTable.WipedQoL
local TRANSLATE = addonTable.TRANSLATE or function(msg) return msg end

local CLOCK_UPDATE_INTERVAL = 1
local FPS_UPDATE_INTERVAL = 1
local ANVIL_ICON = "Interface\\AddOns\\WipedQoL\\Icons\\anvil"
local CHEST_ICON = "Interface\\AddOns\\WipedQoL\\Icons\\chest"
local DEFAULT_COMBAT_POTION_ICON = 134400
local CENTER_SNAP_THRESHOLD = 32
local EQUIPMENT_SLOTS = {
    1, 3, 5, 6, 7, 8, 9, 10, 16, 17, 18,
}
local LOOT_SPEC_MENU_ROW_HEIGHT = 24
local LOOT_SPEC_MENU_PADDING = 6
local REPAIR_MENU_ROW_HEIGHT = 26
local REPAIR_MENU_PADDING = 6
local REPAIR_MENU_WIDTH = 230
local REPAIR_MOUNT_IDS = { 280, 460, 1039, 2237 }
local REPAIR_ITEM_IDS = { 49040, 144341, 132514 }
local AUTO_HAMMER_ITEM_ID = 132514
local DATAPANEL_HEIGHT = 24
local DATAPANEL_PADDING = 10
local DATAPANEL_GAP = 16
local DATAPANEL_LOOT_MIN_WIDTH = 42
local DATAPANEL_LOOT_MAX_WIDTH = 180
local DATAPANEL_LOOT_ICON_SIZE = 18
local DATAPANEL_LOOT_TEXT_GAP = 4
local DATAPANEL_LOOT_HIT_PADDING = 6
local DATAPANEL_AUTO_LOOT_ROLL_MIN_WIDTH = 104
local DATAPANEL_AUTO_LOOT_ROLL_MAX_WIDTH = 184
local DATAPANEL_AUTO_LOOT_ROLL_TOGGLE_WIDTH = 38
local DATAPANEL_AUTO_LOOT_ROLL_TOGGLE_GAP = 8
local DATAPANEL_CLOCK_WIDTH = 96
local DATAPANEL_DURABILITY_WIDTH = 58
local DATAPANEL_COMBAT_POTION_WIDTH = 106
local DATAPANEL_COMBAT_POTION_ICON_SIZE = 18
local DATAPANEL_COMBAT_POTION_ICON_GAP = 2
local DATAPANEL_COMBAT_POTION_TOGGLE_GAP = 8
local DATAPANEL_COMBAT_POTION_TOGGLE_WIDTH = 38
local DATAPANEL_FPS_WIDTH = 92
local DATAPANEL_FPS_FIXED_OFFSET_Y = 0
local DATAPANEL_LAYOUT_ANIMATION_DURATION = 0.14
local COMBAT_POTION_MENU_MAX_COLUMNS = 4
local COMBAT_POTION_MENU_CARD_WIDTH = 56
local COMBAT_POTION_MENU_CARD_HEIGHT = 62
local COMBAT_POTION_MENU_ICON_SIZE = 44
local COMBAT_POTION_MENU_GAP = 8
local COMBAT_POTION_MENU_PADDING = 8
local AUTO_LOOT_ROLL_OPTIONS = {
    { value = "need", label = "Need" },
    { value = "transmog", label = "Transmog" },
    { value = "greed", label = "Greed" },
    { value = "pass", label = "Pass" },
}
local AUTO_LOOT_ROLL_ICON = "Interface\\Buttons\\UI-GroupLoot-Dice-Up"

local function GetAutoLootRollModeLabel(mode)
    for _, option in ipairs(AUTO_LOOT_ROLL_OPTIONS) do
        if option.value == mode then
            return TRANSLATE(option.label)
        end
    end

    return TRANSLATE("Pass")
end

local function IsDatapanelInteractionBlocked()
    return WipedQoL.IsInCombat and WipedQoL:IsInCombat()
end

local function IsProtectedInteractionUpdateBlocked(frame)
    return frame
        and frame.GetObjectType
        and frame:GetObjectType() == "Button"
        and IsDatapanelInteractionBlocked()
end

local function SetFrameShown(frame, shown)
    if not frame or IsProtectedInteractionUpdateBlocked(frame) then
        return
    end

    frame:SetShown(shown == true)
end

local function SetFrameMouseEnabled(frame, enabled)
    if not frame or IsProtectedInteractionUpdateBlocked(frame) then
        return
    end

    frame:EnableMouse(enabled == true)
end

local function HideDatapanelMenuFrame(frame)
    if not frame or IsDatapanelInteractionBlocked() then
        return
    end

    frame:Hide()
end

local function EnsureSettingsTable(settings, key)
    if type(settings[key]) ~= "table" then
        settings[key] = {}
    end

    return settings[key]
end

local function FormatClock(format)
    local hour, minute = GetGameTime()
    hour = tonumber(hour) or 0
    minute = tonumber(minute) or 0

    if format == "12" then
        local suffix = hour >= 12 and "PM" or "AM"
        local displayHour = hour % 12
        if displayHour == 0 then
            displayHour = 12
        end

        return string.format("%d:%02d %s", displayHour, minute, suffix)
    end

    return string.format("%02d:%02d", hour, minute)
end

local function FormatFpsStats()
    local fps = GetFramerate and math.floor((GetFramerate() or 0) + 0.5) or 0
    local homeLatency = 0
    if GetNetStats then
        local _, _, home = GetNetStats()
        homeLatency = home
    end

    homeLatency = tonumber(homeLatency) or 0

    return string.format("%d fps  %d ms", fps, homeLatency)
end

local function GetDurabilityPercent()
    local totalCurrent = 0
    local totalMax = 0

    for _, slotID in ipairs(EQUIPMENT_SLOTS) do
        local current, maximum = GetInventoryItemDurability(slotID)
        current = tonumber(current)
        maximum = tonumber(maximum)

        if current and maximum and maximum > 0 then
            totalCurrent = totalCurrent + current
            totalMax = totalMax + maximum
        end
    end

    if totalMax <= 0 then
        return 100
    end

    return math.floor(((totalCurrent / totalMax) * 100) + 0.5)
end

local function SetDurabilityColor(fontString, percent)
    if not fontString then
        return
    end

    if percent < 20 then
        fontString:SetTextColor(1, 0.25, 0.25, 1)
    elseif percent < 70 then
        fontString:SetTextColor(1, 0.82, 0, 1)
    else
        fontString:SetTextColor(0.55, 1, 0.55, 1)
    end
end

local function GetItemCountInBags(itemID)
    itemID = tonumber(itemID)
    if not itemID then
        return 0
    end

    if C_Item and C_Item.GetItemCount then
        return tonumber(C_Item.GetItemCount(itemID, false, false, false)) or 0
    end

    if GetItemCount then
        return tonumber(GetItemCount(itemID, false, false)) or 0
    end

    return 0
end

local function GetCombatPotionFamiliesInMacros()
    local catalog = WipedQoL.GetCombatPotionCatalog
        and WipedQoL:GetCombatPotionCatalog()
        or {}
    local families = {}
    local familyOrder = {}
    local familyByItemID = {}

    for _, item in ipairs(catalog) do
        local itemID = tonumber(item.id)
        local familyKey = item.family or (itemID and ("item:" .. tostring(itemID)))
        if itemID and familyKey then
            local family = families[familyKey]
            if not family then
                family = {
                    key = familyKey,
                    ids = {},
                    icon = item.icon or DEFAULT_COMBAT_POTION_ICON,
                    count = 0,
                }
                families[familyKey] = family
                familyOrder[#familyOrder + 1] = family
            end

            family.ids[#family.ids + 1] = itemID
            family.count = family.count + GetItemCountInBags(itemID)
            familyByItemID[itemID] = family
            if item.form == "normal" and not family.hasNormalIcon then
                family.icon = item.icon or family.icon
                family.hasNormalIcon = true
            end
        end
    end

    local foundFamilies = {}
    local previewItems = WipedQoL.GetCombatPotionMacroPreviewItems
        and WipedQoL:GetCombatPotionMacroPreviewItems()
        or {}
    for _, macro in ipairs(previewItems) do
        local potionMatches = macro.combatPotions
            or (macro.combatPotion and { macro.combatPotion })
            or {}
        for _, potion in ipairs(potionMatches) do
            local itemID = tonumber(potion.id)
            local family = itemID and familyByItemID[itemID]
            if family then
                foundFamilies[family.key] = true
            end
            for _, candidateItemID in ipairs(potion.candidateIDs or {}) do
                family = familyByItemID[tonumber(candidateItemID)]
                if family then
                    foundFamilies[family.key] = true
                end
            end
        end
    end

    local visibleFamilies = {}
    for _, family in ipairs(familyOrder) do
        if foundFamilies[family.key] then
            visibleFamilies[#visibleFamilies + 1] = family
        end
    end
    return visibleFamilies
end

local function GetCombatPotionDatapanelWidth()
    local familyCount = #(WipedQoL._datapanelCombatPotionFamilies or {})
    local additionalFamilies = math.max(familyCount - 2, 0)
    return DATAPANEL_COMBAT_POTION_WIDTH
        + (additionalFamilies
            * (DATAPANEL_COMBAT_POTION_ICON_SIZE + DATAPANEL_COMBAT_POTION_ICON_GAP))
end

local function GetPlayerClassColor()
    if addonTable.GetClassColor then
        return addonTable.GetClassColor()
    end

    local classFile
    if UnitClass then
        _, classFile = UnitClass("player")
    end

    if classFile == "PRIEST" then
        return 1, 0.49, 0.04
    end

    local colors = CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS
    local color = colors and classFile and colors[classFile]
    if color then
        return color.r or 1, color.g or 0.82, color.b or 0
    end

    return 1, 0.82, 0
end

local function IsDatapanelDisplayEnabled(settings)
    return settings
        and settings.enabled == true
        and ((settings.clock and settings.clock.enabled == true)
            or (settings.durability and settings.durability.enabled == true)
            or (settings.lootSpec and settings.lootSpec.enabled == true)
            or (settings.autoLootRoll and settings.autoLootRoll.enabled == true)
            or (settings.combatPotion and settings.combatPotion.enabled == true))
end

local function IsAnyDatapanelDisplayEnabled(settings)
    return IsDatapanelDisplayEnabled(settings)
        or (settings and settings.fps and settings.fps.enabled == true)
end

local function GetSpecNameByIndex(index)
    local specID, name = GetSpecializationInfo(index)
    return specID, name
end

local function GetCurrentSpecID()
    local currentSpec = GetSpecialization and GetSpecialization()
    if not currentSpec then
        return 0
    end

    local specID = GetSpecializationInfo(currentSpec)
    return specID or 0
end

local function GetCurrentSpecName()
    local currentSpec = GetSpecialization and GetSpecialization()
    if currentSpec then
        local _, name = GetSpecNameByIndex(currentSpec)
        if name then
            return name
        end
    end

    return TRANSLATE("Current specialization")
end

local function GetLootSpecName()
    local lootSpecID = GetLootSpecialization and (GetLootSpecialization() or 0) or 0
    if lootSpecID == 0 then
        return GetCurrentSpecName()
    end

    local name
    if GetSpecializationInfoByID then
        local _, specName = GetSpecializationInfoByID(lootSpecID)
        name = specName
    end
    return name or GetCurrentSpecName()
end

local function GetLootSpecOptions()
    local options = {
        { isHeader = true, label = TRANSLATE("Loot specialization") },
        { value = "current", label = TRANSLATE("Current specialization") },
    }

    local numSpecs = GetNumSpecializations and GetNumSpecializations() or 0
    for index = 1, numSpecs do
        local specID, name = GetSpecNameByIndex(index)
        if specID and name then
            options[#options + 1] = {
                value = specID,
                label = name,
            }
        end
    end

    return options
end

local IsItemReadyToUse

local function GetRepairOptions()
    local options = {
        { isHeader = true, label = TRANSLATE("Mounts") },
    }

    for _, mountID in ipairs(REPAIR_MOUNT_IDS) do
        local name, icon, isUsable, shouldHideOnChar, isCollected
        if C_MountJournal and C_MountJournal.GetMountInfoByID then
            local mountName, spellID, mountIcon, isActive, mountIsUsable, sourceType, isFavorite, isFactionSpecific, faction, mountShouldHideOnChar, mountIsCollected = C_MountJournal.GetMountInfoByID(mountID)
            name = mountName
            icon = mountIcon
            isUsable = mountIsUsable
            shouldHideOnChar = mountShouldHideOnChar
            isCollected = mountIsCollected
            if C_MountJournal.GetMountUsabilityByID then
                local mountUsableByID = C_MountJournal.GetMountUsabilityByID(mountID, true)
                if mountUsableByID ~= nil then
                    isUsable = mountUsableByID
                end
            end
        end

        if isCollected == true and shouldHideOnChar ~= true then
            options[#options + 1] = {
                mountID = mountID,
                label = name or string.format("Mount %d", mountID),
                icon = icon,
                disabled = isUsable ~= true,
            }
        end
    end

    options[#options + 1] = { isHeader = true, label = TRANSLATE("Items") }
    local itemCount = 0
    for _, itemID in ipairs(REPAIR_ITEM_IDS) do
        local count = 0
        if C_Item and C_Item.GetItemCount then
            count = C_Item.GetItemCount(itemID, false, false) or 0
        elseif GetItemCount then
            count = GetItemCount(itemID, false, false) or 0
        end

        if count > 0 then
            itemCount = itemCount + 1
            local name, icon
            if C_Item and C_Item.GetItemNameByID then
                name = C_Item.GetItemNameByID(itemID)
            end
            if (not name or name == "") and C_Item and C_Item.GetItemInfo then
                name = C_Item.GetItemInfo(itemID)
            end
            if C_Item and C_Item.GetItemIconByID then
                icon = C_Item.GetItemIconByID(itemID)
            elseif C_Item and C_Item.GetItemInfoInstant then
                icon = select(5, C_Item.GetItemInfoInstant(itemID))
            elseif GetItemIcon then
                icon = GetItemIcon(itemID)
            end

            local isUsable = IsItemReadyToUse(itemID)

            options[#options + 1] = {
                itemID = itemID,
                label = name or string.format("Item %d", itemID),
                icon = icon,
                count = itemID == AUTO_HAMMER_ITEM_ID and count or nil,
                disabled = isUsable ~= true,
            }
        end
    end

    if itemCount == 0 then
        options[#options + 1] = {
            isEmpty = true,
            label = TRANSLATE("No item found"),
            disabled = true,
        }
    end

    return options
end

IsItemReadyToUse = function(itemID)
    local isUsable = false
    if C_Item and C_Item.IsUsableItem then
        isUsable = C_Item.IsUsableItem(itemID) == true
    elseif IsUsableItem then
        isUsable = IsUsableItem(itemID) == true
    end

    if not isUsable then
        return false
    end

    local startTime, duration, enabled
    if C_Item and C_Item.GetItemCooldown then
        startTime, duration, enabled = C_Item.GetItemCooldown(itemID)
    elseif GetItemCooldown then
        startTime, duration, enabled = GetItemCooldown(itemID)
    end

    if enabled == 0 then
        return false
    end

    startTime = tonumber(startTime) or 0
    duration = tonumber(duration) or 0
    if startTime <= 0 or duration <= 0 then
        return true
    end

    local now = GetTime and GetTime() or 0
    return (startTime + duration) <= now
end

local function AddDatapanelItem(items, key, width)
    items[#items + 1] = {
        key = key,
        width = width,
    }
end

local function GetDatapanelItems(settings)
    local items = {}

    if settings.lootSpec and settings.lootSpec.enabled == true then
        local frame = WipedQoL.datapanelFrame
        local textWidth = frame and frame.lootSpec and frame.lootSpec:GetStringWidth() or 0
        if textWidth <= 0 then
            local name = GetLootSpecName()
            textWidth = string.len(name or "") * 6
        end

        local width = DATAPANEL_LOOT_ICON_SIZE + DATAPANEL_LOOT_TEXT_GAP + math.ceil(textWidth or 0) + DATAPANEL_LOOT_HIT_PADDING
        AddDatapanelItem(items, "lootSpec", math.max(DATAPANEL_LOOT_MIN_WIDTH, math.min(width, DATAPANEL_LOOT_MAX_WIDTH)))
    end

    if settings.autoLootRoll and settings.autoLootRoll.enabled == true then
        local frame = WipedQoL.datapanelFrame
        local textWidth = frame and frame.autoLootRoll and frame.autoLootRoll:GetStringWidth() or 0
        if textWidth <= 0 then
            local rollSettings = WipedQoL.GetAutoLootRollSettings and WipedQoL:GetAutoLootRollSettings() or {}
            local label = GetAutoLootRollModeLabel(rollSettings.mode or "pass")
            textWidth = string.len(label or "") * 6
        end

        local width = DATAPANEL_LOOT_ICON_SIZE + DATAPANEL_LOOT_TEXT_GAP + math.ceil(textWidth or 0) + DATAPANEL_AUTO_LOOT_ROLL_TOGGLE_GAP + DATAPANEL_AUTO_LOOT_ROLL_TOGGLE_WIDTH
        AddDatapanelItem(items, "autoLootRoll", math.max(DATAPANEL_AUTO_LOOT_ROLL_MIN_WIDTH, math.min(width, DATAPANEL_AUTO_LOOT_ROLL_MAX_WIDTH)))
    end

    if settings.clock and settings.clock.enabled == true then
        AddDatapanelItem(items, "clock", DATAPANEL_CLOCK_WIDTH)
    end

    if settings.durability and settings.durability.enabled == true then
        AddDatapanelItem(items, "durability", DATAPANEL_DURABILITY_WIDTH)
    end

    if settings.combatPotion and settings.combatPotion.enabled == true then
        AddDatapanelItem(items, "combatPotion", GetCombatPotionDatapanelWidth())
    end

    local widestItem = 0
    for _, item in ipairs(items) do
        widestItem = math.max(widestItem, item.width)
    end

    for _, item in ipairs(items) do
        item.width = widestItem
    end

    return items
end

local function GetDatapanelWidth(items)
    local count = #items
    if count == 0 then
        return 0
    end

    local width = DATAPANEL_PADDING * 2
    for index, item in ipairs(items) do
        width = width + item.width
        if index > 1 then
            width = width + DATAPANEL_GAP
        end
    end

    return width
end

local function Lerp(startValue, endValue, progress)
    return startValue + ((endValue - startValue) * progress)
end

local function EaseOutCubic(progress)
    progress = math.max(0, math.min(progress or 0, 1))
    local inverse = 1 - progress
    return 1 - (inverse * inverse * inverse)
end

local function GetDatapanelItemCenterX(item, index, count, panelWidth)
    if count == 1 then
        return panelWidth / 2
    end

    local x = DATAPANEL_PADDING
    local items = WipedQoL._datapanelLayoutItems or {}
    for itemIndex = 1, index - 1 do
        local previous = items[itemIndex]
        if previous then
            x = x + previous.width + DATAPANEL_GAP
        end
    end

    return x + (item.width / 2)
end

local function UpdateLootSpecHitbox(frame, maxWidth)
    if not frame or not frame.lootSpecFrame then
        return
    end

    maxWidth = tonumber(maxWidth) or DATAPANEL_LOOT_MAX_WIDTH
    local textWidth = frame.lootSpec and frame.lootSpec:GetStringWidth() or 0
    local width = DATAPANEL_LOOT_ICON_SIZE + DATAPANEL_LOOT_TEXT_GAP + math.ceil(textWidth or 0) + DATAPANEL_LOOT_HIT_PADDING
    frame.lootSpecFrame:SetWidth(math.max(DATAPANEL_LOOT_MIN_WIDTH, math.min(width, maxWidth)))
end

local function DatapanelOnUpdate(datapanel, elapsed)
    if datapanel._datapanelClockEnabled then
        datapanel.elapsedClock = (datapanel.elapsedClock or 0) + elapsed

        if datapanel.elapsedClock >= CLOCK_UPDATE_INTERVAL then
            datapanel.elapsedClock = 0
            WipedQoL:RefreshDatapanelClock()
        end
    end

    if datapanel._datapanelLayoutAnimation then
        if IsDatapanelInteractionBlocked() then
            datapanel._datapanelLayoutAnimation = nil
            WipedQoL.datapanelLayoutPending = true
            WipedQoL:UpdateDatapanelOnUpdate()
            return
        end

        WipedQoL:UpdateDatapanelLayoutAnimation(elapsed)
    end
end

local function FpsDatapanelOnUpdate(frame, elapsed)
    frame.elapsedFps = (frame.elapsedFps or 0) + (elapsed or 0)
    if frame.elapsedFps < FPS_UPDATE_INTERVAL then
        return
    end

    frame.elapsedFps = 0
    WipedQoL:RefreshDatapanelFpsStats()
end

local function EnsureCenterGuide()
    if WipedQoL.datapanelCenterGuide then
        return WipedQoL.datapanelCenterGuide
    end

    local guide = CreateFrame("Frame", nil, UIParent)
    guide:SetFrameStrata("TOOLTIP")
    guide:SetPoint("TOP", UIParent, "TOP", 0, 0)
    guide:SetPoint("BOTTOM", UIParent, "BOTTOM", 0, 0)
    guide:SetWidth(1)
    guide:Hide()

    guide.line = guide:CreateTexture(nil, "OVERLAY")
    guide.line:SetAllPoints(guide)
    guide.line:SetTexture("Interface\\Buttons\\WHITE8X8")
    guide.line:SetVertexColor(1, 1, 1, 0.45)

    WipedQoL.datapanelCenterGuide = guide
    return guide
end

local function SetCenterGuideShown(shown)
    local guide = EnsureCenterGuide()
    guide:SetShown(shown == true)
end

function WipedQoL:GetDatapanelSettings()
    local modules = self.db and self.db.profile and self.db.profile.modules
    if not modules then
        return {}
    end

    local settings = EnsureSettingsTable(modules, "datapanel")
    local clock = EnsureSettingsTable(settings, "clock")
    local durability = EnsureSettingsTable(settings, "durability")
    local lootSpec = EnsureSettingsTable(settings, "lootSpec")
    local autoLootRoll = EnsureSettingsTable(settings, "autoLootRoll")
    local combatPotion = EnsureSettingsTable(settings, "combatPotion")
    local fps = EnsureSettingsTable(settings, "fps")

    settings.enabled = settings.enabled == true
    settings.locked = settings.locked ~= false
    settings.point = settings.point or "TOP"
    settings.relativePoint = settings.relativePoint or "TOP"
    settings.x = tonumber(settings.x) or 0
    settings.y = tonumber(settings.y) or 0
    clock.enabled = clock.enabled == true
    if clock.format ~= "12" and clock.format ~= "24" then
        clock.format = "24"
    end

    durability.enabled = durability.enabled == true
    lootSpec.enabled = lootSpec.enabled ~= false
    autoLootRoll.enabled = autoLootRoll.enabled == true
    combatPotion.enabled = combatPotion.enabled == true
    fps.enabled = fps.enabled == true
    fps.fixed = fps.fixed ~= false
    fps.point = fps.point or "TOP"
    fps.relativePoint = fps.relativePoint or "TOP"
    fps.x = tonumber(fps.x) or 0
    fps.y = tonumber(fps.y) or -28
    return settings
end

function WipedQoL:SetDatapanelEnabled(enabled)
    local settings = self:GetDatapanelSettings()
    settings.enabled = enabled == true
    self:RefreshDatapanel()
end

function WipedQoL:SetDatapanelLocked(locked)
    local settings = self:GetDatapanelSettings()
    settings.locked = locked ~= false
    self:RefreshDatapanelLockState()
end

function WipedQoL:SetDatapanelClockEnabled(enabled)
    local settings = self:GetDatapanelSettings()
    settings.clock.enabled = enabled == true
    self:RefreshDatapanel()
end

function WipedQoL:SetDatapanelClockFormat(format)
    local settings = self:GetDatapanelSettings()
    settings.clock.format = format == "12" and "12" or "24"
    self:RefreshDatapanel()
end

function WipedQoL:SetDatapanelDurabilityEnabled(enabled)
    local settings = self:GetDatapanelSettings()
    settings.durability.enabled = enabled == true
    self:RefreshDatapanel()
end

function WipedQoL:SetDatapanelLootSpecEnabled(enabled)
    local settings = self:GetDatapanelSettings()
    settings.lootSpec.enabled = enabled == true
    self:RefreshDatapanel()
end

function WipedQoL:SetDatapanelAutoLootRollEnabled(enabled)
    local settings = self:GetDatapanelSettings()
    settings.autoLootRoll.enabled = enabled == true
    self:RefreshDatapanel()
end

function WipedQoL:SetDatapanelCombatPotionEnabled(enabled)
    local settings = self:GetDatapanelSettings()
    settings.combatPotion.enabled = enabled == true
    self:RefreshDatapanel()
end

function WipedQoL:SetDatapanelFpsEnabled(enabled)
    local settings = self:GetDatapanelSettings()
    settings.fps.enabled = enabled == true
    self:RefreshDatapanel()
end

function WipedQoL:SetDatapanelFpsFixed(fixed)
    local settings = self:GetDatapanelSettings()
    settings.fps.fixed = fixed == true and IsDatapanelDisplayEnabled(settings)
    self:RefreshDatapanelFpsLockState()
    self:ApplyDatapanelFpsPosition()
end

function WipedQoL:SetDatapanelLootSpecialization(specID)
    if not SetLootSpecialization or self:IsInCombat() then
        return
    end

    if specID == "current" then
        specID = 0
    end

    SetLootSpecialization(tonumber(specID) or 0)
    self:RefreshDatapanelLootSpec()
end

function WipedQoL:SetupDatapanel()
    self:RefreshDatapanel()
end

function WipedQoL:UpdateDatapanelEventRegistration(settings)
    local displayEnabled = IsAnyDatapanelDisplayEnabled(settings)
    local mainDisplayEnabled = IsDatapanelDisplayEnabled(settings)
    local durabilityEnabled = displayEnabled and settings.durability.enabled == true
    local lootSpecEnabled = displayEnabled and settings.lootSpec.enabled == true
    local combatPotionEnabled = displayEnabled and settings.combatPotion.enabled == true

    if displayEnabled and not self.datapanelWorldEventRegistered then
        self:RegisterEvent("PLAYER_ENTERING_WORLD", "RefreshDatapanel")
        self.datapanelWorldEventRegistered = true
    elseif not displayEnabled and self.datapanelWorldEventRegistered then
        self:UnregisterEvent("PLAYER_ENTERING_WORLD")
        self.datapanelWorldEventRegistered = nil
    end

    if mainDisplayEnabled and durabilityEnabled and not self.datapanelDurabilityEventsRegistered then
        self:RegisterEvent("UPDATE_INVENTORY_DURABILITY", "RefreshDatapanelDurability")
        self:RegisterEvent("PLAYER_EQUIPMENT_CHANGED", "RefreshDatapanelDurability")
        self.datapanelDurabilityEventsRegistered = true
    elseif (not mainDisplayEnabled or not durabilityEnabled) and self.datapanelDurabilityEventsRegistered then
        self:UnregisterEvent("UPDATE_INVENTORY_DURABILITY")
        self:UnregisterEvent("PLAYER_EQUIPMENT_CHANGED")
        self.datapanelDurabilityEventsRegistered = nil
    end

    if mainDisplayEnabled and lootSpecEnabled and not self.datapanelLootSpecEventsRegistered then
        self:RegisterEvent("PLAYER_LOOT_SPEC_UPDATED", "RefreshDatapanelLootSpec")
        self:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED", "RefreshDatapanelLootSpec")
        self.datapanelLootSpecEventsRegistered = true
    elseif (not mainDisplayEnabled or not lootSpecEnabled) and self.datapanelLootSpecEventsRegistered then
        self:UnregisterEvent("PLAYER_LOOT_SPEC_UPDATED")
        self:UnregisterEvent("PLAYER_SPECIALIZATION_CHANGED")
        self.datapanelLootSpecEventsRegistered = nil
    end

    if mainDisplayEnabled and combatPotionEnabled and not self.datapanelCombatPotionEventsRegistered then
        self:RegisterEvent("BAG_UPDATE_DELAYED", "RefreshDatapanelCombatPotion")
        self.datapanelCombatPotionEventsRegistered = true
    elseif (not mainDisplayEnabled or not combatPotionEnabled) and self.datapanelCombatPotionEventsRegistered then
        self:UnregisterEvent("BAG_UPDATE_DELAYED")
        self.datapanelCombatPotionEventsRegistered = nil
    end
end

function WipedQoL:CloseDatapanelLootSpecMenu()
    if self.datapanelLootSpecMenu then
        HideDatapanelMenuFrame(self.datapanelLootSpecMenu)
    end

    if self.datapanelLootSpecClickMonitor then
        HideDatapanelMenuFrame(self.datapanelLootSpecClickMonitor)
    end
end

function WipedQoL:CloseDatapanelAutoLootRollMenu()
    if self.datapanelAutoLootRollMenu then
        HideDatapanelMenuFrame(self.datapanelAutoLootRollMenu)
    end

    if self.datapanelAutoLootRollClickMonitor then
        HideDatapanelMenuFrame(self.datapanelAutoLootRollClickMonitor)
    end
end

function WipedQoL:CloseDatapanelRepairMenu()
    if self.datapanelRepairMenu then
        HideDatapanelMenuFrame(self.datapanelRepairMenu)
    end

    if self.datapanelRepairClickMonitor then
        HideDatapanelMenuFrame(self.datapanelRepairClickMonitor)
    end
end

function WipedQoL:CloseDatapanelCombatPotionMenu()
    if self.datapanelCombatPotionMenu then
        HideDatapanelMenuFrame(self.datapanelCombatPotionMenu)
    end

    if self.datapanelCombatPotionClickMonitor then
        HideDatapanelMenuFrame(self.datapanelCombatPotionClickMonitor)
    end
end

function WipedQoL:CloseDatapanelMenus()
    self:CloseDatapanelLootSpecMenu()
    self:CloseDatapanelAutoLootRollMenu()
    self:CloseDatapanelRepairMenu()
    self:CloseDatapanelCombatPotionMenu()
end

local DATAPANEL_MENU_LEAVE_CLOSE_DELAY = 0.16

local function IsMouseOverDatapanelMenu(menu, anchor)
    return MouseIsOver and ((menu and MouseIsOver(menu)) or (anchor and MouseIsOver(anchor)))
end

local function ConfigureDatapanelMenuMonitor(monitor, menu, anchorGetter, closeFunc)
    monitor:Hide()
    monitor.leftWasDown = false
    monitor.mouseLeftAt = nil
    monitor:SetScript("OnHide", function(self)
        self.leftWasDown = false
        self.mouseLeftAt = nil
    end)
    monitor:SetScript("OnUpdate", function(self)
        local anchor = anchorGetter and anchorGetter() or nil
        local mouseOver = IsMouseOverDatapanelMenu(menu, anchor)
        local leftDown = IsMouseButtonDown and IsMouseButtonDown("LeftButton")
        local clicked = leftDown and not self.leftWasDown
        self.leftWasDown = leftDown

        if mouseOver then
            self.mouseLeftAt = nil
            return
        end

        if clicked then
            closeFunc()
            return
        end

        local now = GetTime and GetTime() or 0
        self.mouseLeftAt = self.mouseLeftAt or now
        if (now - self.mouseLeftAt) >= DATAPANEL_MENU_LEAVE_CLOSE_DELAY then
            closeFunc()
        end
    end)
end

function WipedQoL:CreateDatapanelLootSpecMenu()
    if self.datapanelLootSpecMenu then
        return self.datapanelLootSpecMenu
    end

    local menu = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
    menu:SetFrameStrata("DIALOG")
    menu:SetFrameLevel(950)
    menu.rows = {}
    menu:EnableMouse(true)
    menu:Hide()

    if addonTable.SkinFrame then
        addonTable.SkinFrame(menu, { light = true })
    end

    local clickMonitor = CreateFrame("Frame")
    ConfigureDatapanelMenuMonitor(clickMonitor, menu, function()
        return WipedQoL.datapanelFrame and WipedQoL.datapanelFrame.lootSpecFrame or nil
    end, function()
        WipedQoL:CloseDatapanelLootSpecMenu()
    end)

    self.datapanelLootSpecMenu = menu
    self.datapanelLootSpecClickMonitor = clickMonitor
    return menu
end

function WipedQoL:CreateDatapanelAutoLootRollMenu()
    if self.datapanelAutoLootRollMenu then
        return self.datapanelAutoLootRollMenu
    end

    local menu = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
    menu:SetFrameStrata("DIALOG")
    menu:SetFrameLevel(950)
    menu.rows = {}
    menu:EnableMouse(true)
    menu:Hide()

    if addonTable.SkinFrame then
        addonTable.SkinFrame(menu, { light = true })
    end

    local clickMonitor = CreateFrame("Frame")
    ConfigureDatapanelMenuMonitor(clickMonitor, menu, function()
        return WipedQoL.datapanelFrame and WipedQoL.datapanelFrame.autoLootRollFrame or nil
    end, function()
        WipedQoL:CloseDatapanelAutoLootRollMenu()
    end)

    self.datapanelAutoLootRollMenu = menu
    self.datapanelAutoLootRollClickMonitor = clickMonitor
    return menu
end

function WipedQoL:CreateDatapanelRepairMenu()
    if self.datapanelRepairMenu then
        return self.datapanelRepairMenu
    end

    local menu = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
    menu:SetFrameStrata("DIALOG")
    menu:SetFrameLevel(950)
    menu.rows = {}
    menu:EnableMouse(true)
    menu:Hide()

    if addonTable.SkinFrame then
        addonTable.SkinFrame(menu, { light = true })
    end

    local clickMonitor = CreateFrame("Frame")
    ConfigureDatapanelMenuMonitor(clickMonitor, menu, function()
        return WipedQoL.datapanelFrame and WipedQoL.datapanelFrame.durabilityFrame or nil
    end, function()
        WipedQoL:CloseDatapanelRepairMenu()
    end)

    self.datapanelRepairMenu = menu
    self.datapanelRepairClickMonitor = clickMonitor
    return menu
end

function WipedQoL:CreateDatapanelCombatPotionMenu()
    if self.datapanelCombatPotionMenu then
        return self.datapanelCombatPotionMenu
    end

    local menu = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
    menu:SetFrameStrata("DIALOG")
    menu:SetFrameLevel(950)
    menu.cards = {}
    menu:EnableMouse(true)
    menu:Hide()

    if addonTable.SkinFrame then
        addonTable.SkinFrame(menu, { light = true })
    end

    local title = menu:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    title:SetPoint("TOPLEFT", menu, "TOPLEFT", 8, -7)
    title:SetText(TRANSLATE("Combat potion macros"))
    title:SetTextColor(1, 1, 1, 0.95)
    if addonTable.ApplyFont then
        addonTable.ApplyFont(title, 12, "")
    end
    menu.title = title

    local empty = menu:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    empty:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -8)
    empty:SetPoint("RIGHT", menu, "RIGHT", -8, 0)
    empty:SetJustifyH("LEFT")
    empty:SetTextColor(0.78, 0.78, 0.78, 1)
    if addonTable.ApplyFont then
        addonTable.ApplyFont(empty, 12, "")
    end
    menu.empty = empty

    local clickMonitor = CreateFrame("Frame")
    ConfigureDatapanelMenuMonitor(clickMonitor, menu, function()
        return WipedQoL.datapanelFrame and WipedQoL.datapanelFrame.combatPotionFrame or nil
    end, function()
        WipedQoL:CloseDatapanelCombatPotionMenu()
    end)

    self.datapanelCombatPotionMenu = menu
    self.datapanelCombatPotionClickMonitor = clickMonitor
    return menu
end

function WipedQoL:OpenDatapanelLootSpecMenu(anchor)
    if not anchor then
        return
    end
    if IsDatapanelInteractionBlocked() then
        return
    end

    self:CloseDatapanelRepairMenu()
    self:CloseDatapanelAutoLootRollMenu()
    self:CloseDatapanelCombatPotionMenu()
    local menu = self:CreateDatapanelLootSpecMenu()
    local options = GetLootSpecOptions()
    local rowWidth = math.max(anchor:GetWidth() or 180, 220)
    local menuHeight = (LOOT_SPEC_MENU_PADDING * 2) + (#options * LOOT_SPEC_MENU_ROW_HEIGHT)
    local currentLootSpecID = GetLootSpecialization and (GetLootSpecialization() or 0) or 0

    menu:SetSize(rowWidth, menuHeight)
    for index, option in ipairs(options) do
        local row = menu.rows[index]
        if not row then
            row = CreateFrame("Button", nil, menu)
            row:SetHeight(LOOT_SPEC_MENU_ROW_HEIGHT)
            row.bg = row:CreateTexture(nil, "BACKGROUND")
            row.bg:SetAllPoints(row)
            row.bg:SetTexture("Interface\\Buttons\\WHITE8X8")
            row.bg:SetVertexColor(0, 0, 0, 0.34)
            row.text = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            row.text:SetPoint("LEFT", row, "LEFT", 8, 0)
            row.text:SetPoint("RIGHT", row, "RIGHT", -8, 0)
            row.text:SetJustifyH("LEFT")
            if addonTable.ApplyFont then
                addonTable.ApplyFont(row.text, 12, "")
            end
            row:SetScript("OnEnter", function(self)
                if self.isHeader then
                    self.bg:SetVertexColor(0, 0, 0, 0)
                    return
                end

                local r, g, b = GetPlayerClassColor()
                self.bg:SetVertexColor(r * 0.22, g * 0.22, b * 0.22, 0.72)
            end)
            row:SetScript("OnLeave", function(self)
                if self.isHeader then
                    self.bg:SetVertexColor(0, 0, 0, 0)
                    return
                end

                self.bg:SetVertexColor(0, 0, 0, 0.34)
            end)
            row:SetScript("OnClick", function(self)
                if self.isHeader then
                    return
                end
                if IsDatapanelInteractionBlocked() then
                    WipedQoL:CloseDatapanelMenus()
                    return
                end

                WipedQoL:SetDatapanelLootSpecialization(self.value)
                WipedQoL:CloseDatapanelLootSpecMenu()
            end)
            menu.rows[index] = row
        end

        row.isHeader = option.isHeader == true
        row.value = option.value
        row.text:SetText(option.label)
        if row.isHeader then
            row.bg:SetVertexColor(0, 0, 0, 0)
            row.text:SetTextColor(1, 0.82, 0, 1)
        elseif option.value == currentLootSpecID or (option.value == "current" and currentLootSpecID == 0) then
            row.text:SetTextColor(GetPlayerClassColor())
        else
            row.text:SetTextColor(1, 1, 1, 0.95)
        end
        row:ClearAllPoints()
        row:SetPoint("TOPLEFT", menu, "TOPLEFT", LOOT_SPEC_MENU_PADDING, -(LOOT_SPEC_MENU_PADDING + ((index - 1) * LOOT_SPEC_MENU_ROW_HEIGHT)))
        row:SetPoint("RIGHT", menu, "RIGHT", -LOOT_SPEC_MENU_PADDING, 0)
        row:Show()
    end

    for index = #options + 1, #menu.rows do
        menu.rows[index]:Hide()
    end

    local positionAnchor = anchor
    local parentTop = UIParent and UIParent:GetTop() or 0
    local parentBottom = UIParent and UIParent:GetBottom() or 0
    local anchorTop = positionAnchor:GetTop() or 0
    local anchorBottom = positionAnchor:GetBottom() or 0
    local spaceAbove = parentTop - anchorTop
    local spaceBelow = anchorBottom - parentBottom

    menu:ClearAllPoints()
    if spaceBelow < menuHeight and spaceAbove >= menuHeight then
        menu:SetPoint("BOTTOMLEFT", positionAnchor, "TOPLEFT", 0, 0)
    else
        menu:SetPoint("TOPLEFT", positionAnchor, "BOTTOMLEFT", 0, 0)
    end

    menu:Show()
    if self.datapanelLootSpecClickMonitor then
        self.datapanelLootSpecClickMonitor:Show()
    end
end

function WipedQoL:OpenDatapanelAutoLootRollMenu(anchor)
    if not anchor then
        return
    end
    if IsDatapanelInteractionBlocked() then
        return
    end

    self:CloseDatapanelLootSpecMenu()
    self:CloseDatapanelRepairMenu()
    self:CloseDatapanelCombatPotionMenu()
    local menu = self:CreateDatapanelAutoLootRollMenu()
    local descriptionHeight = 24
    local rowWidth = math.max(anchor:GetWidth() or 140, 190)
    local menuHeight = (LOOT_SPEC_MENU_PADDING * 2) + descriptionHeight + (#AUTO_LOOT_ROLL_OPTIONS * LOOT_SPEC_MENU_ROW_HEIGHT)
    local rollSettings = self.GetAutoLootRollSettings and self:GetAutoLootRollSettings() or {}
    local currentMode = rollSettings.mode or "pass"
    local rollEnabled = rollSettings.enabled == true

    menu:SetSize(rowWidth, menuHeight)

    if not menu.descriptionPrefix then
        menu.descriptionPrefix = menu:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        menu.descriptionWord = menu:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        menu.descriptionSuffix = menu:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        menu.descriptionUnderline = menu:CreateTexture(nil, "OVERLAY")
        menu.descriptionUnderline:SetHeight(1)
        menu.descriptionUnderline:SetTexture("Interface\\Buttons\\WHITE8X8")
        if addonTable.ApplyFont then
            addonTable.ApplyFont(menu.descriptionPrefix, 12, "")
            addonTable.ApplyFont(menu.descriptionWord, 12, "")
            addonTable.ApplyFont(menu.descriptionSuffix, 12, "")
        end
    end

    menu.descriptionPrefix:SetText(TRANSLATE("Auto roll in"))
    menu.descriptionWord:SetText(TRANSLATE("legacy"))
    menu.descriptionSuffix:SetText(TRANSLATE("content"))
    menu.descriptionPrefix:SetTextColor(1, 1, 1, 0.95)
    menu.descriptionWord:SetTextColor(1, 1, 1, 0.95)
    menu.descriptionSuffix:SetTextColor(1, 1, 1, 0.95)
    menu.descriptionUnderline:SetVertexColor(1, 1, 1, 0.9)
    menu.descriptionPrefix:ClearAllPoints()
    menu.descriptionWord:ClearAllPoints()
    menu.descriptionSuffix:ClearAllPoints()
    menu.descriptionUnderline:ClearAllPoints()
    menu.descriptionPrefix:SetPoint("TOPLEFT", menu, "TOPLEFT", LOOT_SPEC_MENU_PADDING + 2, -8)
    menu.descriptionWord:SetPoint("LEFT", menu.descriptionPrefix, "RIGHT", 4, 0)
    menu.descriptionSuffix:SetPoint("LEFT", menu.descriptionWord, "RIGHT", 4, 0)
    menu.descriptionUnderline:SetPoint("TOPLEFT", menu.descriptionWord, "BOTTOMLEFT", 0, 1)
    menu.descriptionUnderline:SetPoint("TOPRIGHT", menu.descriptionWord, "BOTTOMRIGHT", 0, 1)

    for index, option in ipairs(AUTO_LOOT_ROLL_OPTIONS) do
        local row = menu.rows[index]
        if not row then
            row = CreateFrame("Button", nil, menu)
            row:SetHeight(LOOT_SPEC_MENU_ROW_HEIGHT)
            row.bg = row:CreateTexture(nil, "BACKGROUND")
            row.bg:SetAllPoints(row)
            row.bg:SetTexture("Interface\\Buttons\\WHITE8X8")
            row.bg:SetVertexColor(0, 0, 0, 0.34)
            row.text = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            row.text:SetPoint("LEFT", row, "LEFT", 8, 0)
            row.text:SetPoint("RIGHT", row, "RIGHT", -8, 0)
            row.text:SetJustifyH("LEFT")
            if addonTable.ApplyFont then
                addonTable.ApplyFont(row.text, 12, "")
            end
            row:SetScript("OnEnter", function(self)
                if self.disabled then
                    return
                end

                local r, g, b = GetPlayerClassColor()
                self.bg:SetVertexColor(r * 0.22, g * 0.22, b * 0.22, 0.72)
            end)
            row:SetScript("OnLeave", function(self)
                self.bg:SetVertexColor(0, 0, 0, 0.34)
            end)
            row:SetScript("OnClick", function(self)
                if self.disabled then
                    return
                end

                if IsDatapanelInteractionBlocked() then
                    WipedQoL:CloseDatapanelMenus()
                    return
                end

                if WipedQoL.SetAutoLootRollMode then
                    WipedQoL:SetAutoLootRollMode(self.value)
                end
                WipedQoL:CloseDatapanelAutoLootRollMenu()
            end)
            menu.rows[index] = row
        end

        row.value = option.value
        row.disabled = not rollEnabled
        row.text:SetText(TRANSLATE(option.label))
        row:EnableMouse(rollEnabled)
        if not rollEnabled then
            row.text:SetTextColor(0.45, 0.45, 0.45, 0.9)
        elseif option.value == currentMode then
            row.text:SetTextColor(GetPlayerClassColor())
        else
            row.text:SetTextColor(1, 1, 1, 0.95)
        end
        row:ClearAllPoints()
        row:SetPoint("TOPLEFT", menu, "TOPLEFT", LOOT_SPEC_MENU_PADDING, -(LOOT_SPEC_MENU_PADDING + descriptionHeight + ((index - 1) * LOOT_SPEC_MENU_ROW_HEIGHT)))
        row:SetPoint("RIGHT", menu, "RIGHT", -LOOT_SPEC_MENU_PADDING, 0)
        row:Show()
    end

    for index = #AUTO_LOOT_ROLL_OPTIONS + 1, #menu.rows do
        menu.rows[index]:Hide()
    end

    local positionAnchor = anchor
    local parentTop = UIParent and UIParent:GetTop() or 0
    local parentBottom = UIParent and UIParent:GetBottom() or 0
    local anchorTop = positionAnchor:GetTop() or 0
    local anchorBottom = positionAnchor:GetBottom() or 0
    local spaceAbove = parentTop - anchorTop
    local spaceBelow = anchorBottom - parentBottom

    menu:ClearAllPoints()
    if spaceBelow < menuHeight and spaceAbove >= menuHeight then
        menu:SetPoint("BOTTOMLEFT", positionAnchor, "TOPLEFT", 0, 0)
    else
        menu:SetPoint("TOPLEFT", positionAnchor, "BOTTOMLEFT", 0, 0)
    end

    menu:Show()
    if self.datapanelAutoLootRollClickMonitor then
        self.datapanelAutoLootRollClickMonitor:Show()
    end
end

function WipedQoL:OpenDatapanelRepairMenu(anchor)
    if not anchor then
        return
    end
    if IsDatapanelInteractionBlocked() then
        return
    end

    self:CloseDatapanelLootSpecMenu()
    self:CloseDatapanelAutoLootRollMenu()
    self:CloseDatapanelCombatPotionMenu()
    local menu = self:CreateDatapanelRepairMenu()
    local options = GetRepairOptions()
    local menuHeight = (REPAIR_MENU_PADDING * 2) + (#options * REPAIR_MENU_ROW_HEIGHT)

    menu:SetSize(REPAIR_MENU_WIDTH, menuHeight)
    for index, option in ipairs(options) do
        local row = menu.rows[index]
        if not row then
            row = CreateFrame("Button", "WipedQoLRepairMenuRow" .. index, menu, "SecureActionButtonTemplate")
            row:SetHeight(REPAIR_MENU_ROW_HEIGHT)
            row:RegisterForClicks("LeftButtonUp", "LeftButtonDown")
            row:SetAttribute("pressAndHoldAction", false)
            row:SetAttribute("*type*", nil)
            row.bg = row:CreateTexture(nil, "BACKGROUND")
            row.bg:SetAllPoints(row)
            row.bg:SetTexture("Interface\\Buttons\\WHITE8X8")
            row.bg:SetVertexColor(0, 0, 0, 0.34)
            row.icon = row:CreateTexture(nil, "OVERLAY")
            row.icon:SetPoint("LEFT", row, "LEFT", 8, 0)
            row.icon:SetSize(18, 18)
            row.countText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            row.countText:SetPoint("BOTTOMRIGHT", row.icon, "BOTTOMRIGHT", 2, -2)
            row.countText:SetTextColor(1, 1, 1, 1)
            row.countText:SetJustifyH("RIGHT")
            row.countText:SetShadowOffset(1, -1)
            row.countText:SetShadowColor(0, 0, 0, 1)
            row.countText:Hide()
            row.text = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            row.text:SetPoint("LEFT", row.icon, "RIGHT", 6, 0)
            row.text:SetPoint("RIGHT", row, "RIGHT", -8, 0)
            row.text:SetJustifyH("LEFT")
            if addonTable.ApplyFont then
                addonTable.ApplyFont(row.text, 12, "")
                addonTable.ApplyFont(row.countText, 10, "OUTLINE")
            end
            row:SetScript("OnEnter", function(self)
                if self.isHeader or self.disabled then
                    self.bg:SetVertexColor(0, 0, 0, self.isHeader and 0 or 0.34)
                    return
                end

                local r, g, b = GetPlayerClassColor()
                self.bg:SetVertexColor(r * 0.22, g * 0.22, b * 0.22, 0.72)
            end)
            row:SetScript("OnLeave", function(self)
                self.bg:SetVertexColor(0, 0, 0, self.isHeader and 0 or 0.34)
            end)
            row:SetScript("PostClick", function(self)
                if self.isHeader or self.disabled then
                    return
                end
                if IsDatapanelInteractionBlocked() then
                    WipedQoL:CloseDatapanelMenus()
                    return
                end

                if self.mountID and C_MountJournal and C_MountJournal.SummonByID then
                    C_MountJournal.SummonByID(self.mountID)
                    WipedQoL:CloseDatapanelRepairMenu()
                elseif self.itemID then
                    WipedQoL:CloseDatapanelRepairMenu()
                end
            end)
            menu.rows[index] = row
        end

        if not row.cooldownText then
            row.cooldownText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            row.cooldownText:SetPoint("RIGHT", row, "RIGHT", -8, 0)
            row.cooldownText:SetTextColor(1, 0.15, 0.15, 1)
            row.cooldownText:SetJustifyH("RIGHT")
            row.cooldownText:Hide()
            if addonTable.ApplyFont then
                addonTable.ApplyFont(row.cooldownText, 12, "")
            end
        end
        row.cooldownText:SetText(option.mountID and TRANSLATE("Unavailable") or TRANSLATE("Cooldown"))

        row.isHeader = option.isHeader == true
        row.isEmpty = option.isEmpty == true
        row.disabled = option.disabled == true
        row.mountID = option.mountID
        row.itemID = option.itemID
        row.itemCount = option.count
        if row.itemID and not row.disabled then
            local macroText = string.format("/use item:%d", row.itemID)
            row:SetAttribute("*type*", "macro")
            row:SetAttribute("type", "macro")
            row:SetAttribute("type1", "macro")
            row:SetAttribute("item", nil)
            row:SetAttribute("macrotext", macroText)
            row:SetAttribute("macrotext1", macroText)
        else
            row:SetAttribute("*type*", nil)
            row:SetAttribute("type", nil)
            row:SetAttribute("type1", nil)
            row:SetAttribute("item", nil)
            row:SetAttribute("macrotext", nil)
            row:SetAttribute("macrotext1", nil)
        end
        row.text:SetText(option.label)
        row.countText:Hide()
        if row.isHeader then
            row.cooldownText:Hide()
            row.bg:SetVertexColor(0, 0, 0, 0)
            row.icon:Hide()
            row.text:ClearAllPoints()
            row.text:SetPoint("LEFT", row, "LEFT", 8, 0)
            row.text:SetPoint("RIGHT", row, "RIGHT", -8, 0)
            row.text:SetTextColor(1, 0.82, 0, 1)
        elseif row.isEmpty then
            row.cooldownText:Hide()
            row.bg:SetVertexColor(0, 0, 0, 0.18)
            row.icon:Hide()
            row.text:ClearAllPoints()
            row.text:SetPoint("LEFT", row, "LEFT", 14, 0)
            row.text:SetPoint("RIGHT", row, "RIGHT", -8, 0)
            row.text:SetTextColor(0.55, 0.55, 0.55, 1)
        else
            row.icon:Show()
            row.icon:SetTexture(option.icon or ANVIL_ICON)
            row.icon:SetDesaturated(row.disabled == true)
            row.icon:SetAlpha(row.disabled and 0.55 or 1)
            if row.itemID and (tonumber(row.itemCount) or 0) > 0 then
                row.countText:SetText(row.itemCount)
                row.countText:SetAlpha(row.disabled and 0.55 or 1)
                row.countText:Show()
            end
            row.text:ClearAllPoints()
            row.text:SetPoint("LEFT", row.icon, "RIGHT", 6, 0)
            local statusTextOffset = row.mountID and -104 or -76
            row.text:SetPoint("RIGHT", row, "RIGHT", row.disabled and statusTextOffset or -8, 0)
            if row.disabled then
                row.text:SetTextColor(0.48, 0.48, 0.48, 1)
                row.cooldownText:Show()
            else
                row.text:SetTextColor(1, 1, 1, 0.95)
                row.cooldownText:Hide()
            end
        end

        row:ClearAllPoints()
        row:SetPoint("TOPLEFT", menu, "TOPLEFT", REPAIR_MENU_PADDING, -(REPAIR_MENU_PADDING + ((index - 1) * REPAIR_MENU_ROW_HEIGHT)))
        row:SetPoint("RIGHT", menu, "RIGHT", -REPAIR_MENU_PADDING, 0)
        row:Show()
    end

    for index = #options + 1, #menu.rows do
        menu.rows[index]:Hide()
    end

    local positionAnchor = anchor
    local parentTop = UIParent and UIParent:GetTop() or 0
    local parentBottom = UIParent and UIParent:GetBottom() or 0
    local anchorTop = positionAnchor:GetTop() or 0
    local anchorBottom = positionAnchor:GetBottom() or 0
    local spaceAbove = parentTop - anchorTop
    local spaceBelow = anchorBottom - parentBottom

    menu:ClearAllPoints()
    if spaceBelow < menuHeight and spaceAbove >= menuHeight then
        menu:SetPoint("BOTTOMRIGHT", positionAnchor, "TOPRIGHT", 0, 0)
    else
        menu:SetPoint("TOPRIGHT", positionAnchor, "BOTTOMRIGHT", 0, 0)
    end

    menu:Show()
    if self.datapanelRepairClickMonitor then
        self.datapanelRepairClickMonitor:Show()
    end
end

function WipedQoL:OpenDatapanelCombatPotionMenu(anchor, skipScan)
    if not anchor then
        return
    end
    if IsDatapanelInteractionBlocked() then
        return
    end

    self:CloseDatapanelLootSpecMenu()
    self:CloseDatapanelAutoLootRollMenu()
    self:CloseDatapanelRepairMenu()
    self.datapanelCombatPotionMenuAnchor = anchor

    if not skipScan and self.RefreshCombatPotionMacroPreview then
        self:RefreshCombatPotionMacroPreview()
    end

    local menu = self:CreateDatapanelCombatPotionMenu()
    local items = self.GetCombatPotionMacroPreviewItems and self:GetCombatPotionMacroPreviewItems() or {}
    local columns = math.min(COMBAT_POTION_MENU_MAX_COLUMNS, math.max(#items, 1))
    local cardWidth = COMBAT_POTION_MENU_CARD_WIDTH
    local cardHeight = COMBAT_POTION_MENU_CARD_HEIGHT
    local gap = COMBAT_POTION_MENU_GAP
    local rows = math.max(math.ceil(#items / columns), 1)
    local titleWidth = math.ceil(menu.title:GetStringWidth() or 0) + (COMBAT_POTION_MENU_PADDING * 2)
    local emptyWidth = #items == 0 and 220 or 0
    local gridWidth = (columns * cardWidth) + ((columns - 1) * gap) + (COMBAT_POTION_MENU_PADDING * 2)
    local menuWidth = math.max(titleWidth, emptyWidth, gridWidth)
    local menuHeight
    if #items == 0 then
        menuHeight = 58
    else
        menuHeight = 30 + (rows * cardHeight) + ((rows - 1) * gap) + COMBAT_POTION_MENU_PADDING
    end

    menu:SetSize(menuWidth, menuHeight)
    menu.empty:SetShown(#items == 0)
    if #items == 0 then
        menu.empty:SetText(TRANSLATE("No matching macros found."))
    end

    local function GetMacroIcon(icon)
        if not icon or icon == 0 or icon == "" then
            return "Interface\\Icons\\INV_Misc_QuestionMark"
        end
        return icon
    end

    for index, item in ipairs(items) do
        local card = menu.cards[index]
        if not card then
            card = CreateFrame("Frame", nil, menu, "BackdropTemplate")
            card:SetSize(cardWidth, cardHeight)
            if addonTable.SkinFrame then
                addonTable.SkinFrame(card, { light = true })
            end

            card.iconBox = CreateFrame("Frame", nil, card)
            card.iconBox:SetPoint("TOP", card, "TOP", 0, -4)
            card.iconBox:SetSize(COMBAT_POTION_MENU_ICON_SIZE, COMBAT_POTION_MENU_ICON_SIZE)

            card.icon = card:CreateTexture(nil, "ARTWORK")
            card.icon:SetAllPoints(card.iconBox)
            card.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

            card.dim = card:CreateTexture(nil, "OVERLAY", nil, 1)
            card.dim:SetAllPoints(card.iconBox)
            card.dim:SetTexture("Interface\\Buttons\\WHITE8X8")

            card.nameBg = card:CreateTexture(nil, "OVERLAY", nil, 2)
            card.nameBg:SetPoint("BOTTOMLEFT", card.iconBox, "BOTTOMLEFT", 0, 0)
            card.nameBg:SetPoint("BOTTOMRIGHT", card.iconBox, "BOTTOMRIGHT", 0, 0)
            card.nameBg:SetHeight(16)
            card.nameBg:SetTexture("Interface\\Buttons\\WHITE8X8")
            card.nameBg:SetVertexColor(0, 0, 0, 0.82)

            card.name = card:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            card.name:SetPoint("LEFT", card.nameBg, "LEFT", 3, 0)
            card.name:SetPoint("RIGHT", card.nameBg, "RIGHT", -3, 0)
            card.name:SetJustifyH("CENTER")
            card.name:SetWordWrap(false)
            card.name:SetTextColor(1, 1, 1, 0.96)
            if addonTable.ApplyFont then
                addonTable.ApplyFont(card.name, 9, "OUTLINE")
            end

            card.state = card:CreateTexture(nil, "OVERLAY", nil, 3)
            card.state:SetPoint("TOPRIGHT", card.iconBox, "TOPRIGHT", -3, -3)
            card.state:SetSize(7, 7)
            card.state:SetTexture("Interface\\Buttons\\WHITE8X8")

            menu.cards[index] = card
        end

        card:SetSize(cardWidth, cardHeight)
        if card.iconBox then
            card.iconBox:ClearAllPoints()
            card.iconBox:SetPoint("TOP", card, "TOP", 0, -4)
            card.iconBox:SetSize(COMBAT_POTION_MENU_ICON_SIZE, COMBAT_POTION_MENU_ICON_SIZE)
            card.icon:ClearAllPoints()
            card.icon:SetAllPoints(card.iconBox)
        end

        local column = (index - 1) % columns
        local row = math.floor((index - 1) / columns)
        card:ClearAllPoints()
        card:SetPoint("TOPLEFT", menu, "TOPLEFT", COMBAT_POTION_MENU_PADDING + (column * (cardWidth + gap)), -30 - (row * (cardHeight + gap)))
        card.icon:SetTexture(GetMacroIcon(item.icon))
        card.icon:SetDesaturated(item.paused == true)
        card.icon:SetAlpha(item.paused and 0.58 or 1)
        card.dim:SetVertexColor(0, 0, 0, item.paused and 0.38 or 0)
        card.name:SetText(item.name or TRANSLATE("Unnamed macro"))
        if item.paused then
            card.state:SetVertexColor(0.9, 0.22, 0.18, 1)
        else
            card.state:SetVertexColor(0.2, 0.85, 0.36, 1)
        end
        card:Show()
    end

    for index = #items + 1, #menu.cards do
        menu.cards[index]:Hide()
    end

    local positionAnchor = anchor
    local parentTop = UIParent and UIParent:GetTop() or 0
    local parentBottom = UIParent and UIParent:GetBottom() or 0
    local anchorTop = positionAnchor:GetTop() or 0
    local anchorBottom = positionAnchor:GetBottom() or 0
    local spaceAbove = parentTop - anchorTop
    local spaceBelow = anchorBottom - parentBottom

    menu:ClearAllPoints()
    if spaceBelow < menuHeight and spaceAbove >= menuHeight then
        menu:SetPoint("BOTTOMLEFT", positionAnchor, "TOPLEFT", 0, 0)
    else
        menu:SetPoint("TOPLEFT", positionAnchor, "BOTTOMLEFT", 0, 0)
    end

    menu:Show()
    if self.datapanelCombatPotionClickMonitor then
        self.datapanelCombatPotionClickMonitor:Show()
    end
end

local function EnsureDatapanelCombatPotionIndicator(frame, index)
    frame.combatPotionIndicators = frame.combatPotionIndicators or {}
    local indicator = frame.combatPotionIndicators[index]
    if indicator then
        return indicator
    end

    indicator = CreateFrame("Frame", nil, frame.combatPotionFrame)
    indicator:SetSize(
        DATAPANEL_COMBAT_POTION_ICON_SIZE,
        DATAPANEL_COMBAT_POTION_ICON_SIZE
    )

    indicator.icon = indicator:CreateTexture(nil, "ARTWORK")
    indicator.icon:SetAllPoints(indicator)
    indicator.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    indicator.count = indicator:CreateFontString(
        nil,
        "OVERLAY",
        "GameFontHighlightSmall"
    )
    indicator.count:SetPoint("BOTTOMRIGHT", indicator, "BOTTOMRIGHT", 2, -2)
    indicator.count:SetJustifyH("RIGHT")
    indicator.count:SetTextColor(1, 1, 1, 1)
    indicator.count:SetShadowOffset(1, -1)
    indicator.count:SetShadowColor(0, 0, 0, 1)
    if addonTable.ApplyFont then
        addonTable.ApplyFont(indicator.count, 10, "OUTLINE")
    end

    frame.combatPotionIndicators[index] = indicator
    return indicator
end

local function UpdateDatapanelCombatPotionIndicators(frame, families, shown)
    families = families or {}
    frame.combatPotionIndicators = frame.combatPotionIndicators or {}

    for index, family in ipairs(families) do
        local indicator = EnsureDatapanelCombatPotionIndicator(frame, index)
        indicator.icon:SetTexture(family.icon or DEFAULT_COMBAT_POTION_ICON)
        indicator.count:SetText(tonumber(family.count) or 0)
        indicator:SetShown(shown == true)
    end

    for index = #families + 1, #frame.combatPotionIndicators do
        frame.combatPotionIndicators[index]:Hide()
    end
end

function WipedQoL:CreateDatapanel()
    if self.datapanelFrame then
        return self.datapanelFrame
    end

    local frame = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
    frame:SetSize(420, DATAPANEL_HEIGHT)
    frame:SetPoint("TOP", UIParent, "TOP", 0, 0)
    frame:SetFrameStrata("LOW")
    frame:SetMovable(true)
    frame:EnableMouse(false)
    frame:RegisterForDrag("LeftButton")
    frame:SetClampedToScreen(true)
    frame:Hide()

    if addonTable.SkinFrame then
        addonTable.SkinFrame(frame, { light = true })
    end
    if frame._rhhBG then
        frame._rhhBG:SetVertexColor(0.02, 0.02, 0.02, 0.62)
    end
    if frame._rhhInner then
        frame._rhhInner:SetVertexColor(1, 1, 1, 0.02)
    end

    frame.clock = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    frame.clock:SetPoint("CENTER", frame, "CENTER", 0, 0)
    frame.clock:SetJustifyH("CENTER")
    frame.clock:SetTextColor(GetPlayerClassColor())

    frame.lootSpecFrame = CreateFrame("Button", nil, frame)
    frame.lootSpecFrame:SetPoint("LEFT", frame, "LEFT", 10, 0)
    frame.lootSpecFrame:SetSize(180, 22)
    frame.lootSpecFrame:RegisterForClicks("AnyUp")
    frame.lootSpecFrame:SetScript("OnEnter", function()
        frame.lootSpec:SetTextColor(GetPlayerClassColor())
        local settings = WipedQoL:GetDatapanelSettings()
        if settings.enabled == true and settings.lootSpec.enabled == true then
            WipedQoL:OpenDatapanelLootSpecMenu(frame.lootSpecFrame)
        end
    end)
    frame.lootSpecFrame:SetScript("OnLeave", function()
        frame.lootSpec:SetTextColor(1, 1, 1, 0.95)
    end)
    frame.lootSpecFrame:SetScript("OnClick", function()
        local settings = WipedQoL:GetDatapanelSettings()
        if settings.enabled == true and settings.lootSpec.enabled == true then
            WipedQoL:OpenDatapanelLootSpecMenu(frame.lootSpecFrame)
        end
    end)

    frame.lootSpecIcon = frame.lootSpecFrame:CreateTexture(nil, "OVERLAY")
    frame.lootSpecIcon:SetPoint("LEFT", frame.lootSpecFrame, "LEFT", 0, 0)
    frame.lootSpecIcon:SetSize(DATAPANEL_LOOT_ICON_SIZE, DATAPANEL_LOOT_ICON_SIZE)
    frame.lootSpecIcon:SetTexture(CHEST_ICON)

    frame.lootSpec = frame.lootSpecFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    frame.lootSpec:SetPoint("LEFT", frame.lootSpecIcon, "RIGHT", DATAPANEL_LOOT_TEXT_GAP, 0)
    frame.lootSpec:SetPoint("RIGHT", frame.lootSpecFrame, "RIGHT", 0, 0)
    frame.lootSpec:SetJustifyH("LEFT")
    frame.lootSpec:SetTextColor(1, 1, 1, 0.95)

    frame.autoLootRollFrame = CreateFrame("Button", nil, frame)
    frame.autoLootRollFrame:SetSize(DATAPANEL_AUTO_LOOT_ROLL_MIN_WIDTH, 22)
    frame.autoLootRollFrame:RegisterForClicks("AnyUp")
    frame.autoLootRollFrame:SetScript("OnEnter", function()
        frame.autoLootRoll:SetTextColor(GetPlayerClassColor())
        local settings = WipedQoL:GetDatapanelSettings()
        if settings.enabled == true and settings.autoLootRoll.enabled == true then
            WipedQoL:OpenDatapanelAutoLootRollMenu(frame.autoLootRollFrame)
        end
    end)
    frame.autoLootRollFrame:SetScript("OnLeave", function()
        frame.autoLootRoll:SetTextColor(1, 1, 1, 0.95)
    end)
    frame.autoLootRollFrame:SetScript("OnClick", function()
        local settings = WipedQoL:GetDatapanelSettings()
        if settings.enabled == true and settings.autoLootRoll.enabled == true then
            WipedQoL:OpenDatapanelAutoLootRollMenu(frame.autoLootRollFrame)
        end
    end)

    frame.autoLootRollIcon = frame.autoLootRollFrame:CreateTexture(nil, "OVERLAY")
    frame.autoLootRollIcon:SetPoint("LEFT", frame.autoLootRollFrame, "LEFT", 0, 0)
    frame.autoLootRollIcon:SetSize(18, 18)
    frame.autoLootRollIcon:SetTexture(AUTO_LOOT_ROLL_ICON)
    frame.autoLootRollIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    frame.autoLootRoll = frame.autoLootRollFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    frame.autoLootRoll:SetPoint("LEFT", frame.autoLootRollIcon, "RIGHT", 4, 0)
    frame.autoLootRoll:SetPoint("RIGHT", frame.autoLootRollFrame, "RIGHT", 0, 0)
    frame.autoLootRoll:SetJustifyH("LEFT")
    frame.autoLootRoll:SetTextColor(1, 1, 1, 0.95)

    local AutoRollUIHelpers = addonTable.UI
    if AutoRollUIHelpers and AutoRollUIHelpers.CreateToggleSwitch then
        frame.autoLootRollToggle = AutoRollUIHelpers.CreateToggleSwitch(frame.autoLootRollFrame, DATAPANEL_AUTO_LOOT_ROLL_TOGGLE_WIDTH, 18)
        frame.autoLootRollToggle:SetPoint("RIGHT", frame.autoLootRollFrame, "RIGHT", 0, 0)
        frame.autoLootRoll:ClearAllPoints()
        frame.autoLootRoll:SetPoint("LEFT", frame.autoLootRollIcon, "RIGHT", DATAPANEL_LOOT_TEXT_GAP, 0)
        frame.autoLootRoll:SetPoint("RIGHT", frame.autoLootRollToggle, "LEFT", -8, 0)
        frame.autoLootRollToggle:SetScript("OnClick", function(self)
            if IsDatapanelInteractionBlocked() then
                WipedQoL:CloseDatapanelAutoLootRollMenu()
                WipedQoL:RefreshDatapanelAutoLootRoll()
                return
            end

            local targetEnabled = not self:GetChecked()
            self:SetChecked(targetEnabled, true)
            if WipedQoL.SetAutoLootRollEnabled then
                WipedQoL:SetAutoLootRollEnabled(targetEnabled)
            end
        end)
    end

    frame.durabilityFrame = CreateFrame("Button", nil, frame)
    frame.durabilityFrame:SetSize(DATAPANEL_DURABILITY_WIDTH, 22)
    frame.durabilityFrame:RegisterForClicks("AnyUp")
    frame.durabilityFrame:SetScript("OnEnter", function()
        local settings = WipedQoL:GetDatapanelSettings()
        if settings.enabled == true and settings.durability.enabled == true then
            WipedQoL:OpenDatapanelRepairMenu(frame.durabilityFrame)
        end
    end)
    frame.durabilityFrame:SetScript("OnClick", function()
        local settings = WipedQoL:GetDatapanelSettings()
        if settings.enabled == true and settings.durability.enabled == true then
            WipedQoL:OpenDatapanelRepairMenu(frame.durabilityFrame)
        end
    end)

    frame.durabilityIcon = frame.durabilityFrame:CreateTexture(nil, "OVERLAY")
    frame.durabilityIcon:SetPoint("LEFT", frame.durabilityFrame, "LEFT", 0, 0)
    frame.durabilityIcon:SetSize(18, 18)
    frame.durabilityIcon:SetTexture(ANVIL_ICON)

    frame.durability = frame.durabilityFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    frame.durability:SetPoint("LEFT", frame.durabilityIcon, "RIGHT", 1, 0)
    frame.durability:SetPoint("RIGHT", frame.durabilityFrame, "RIGHT", 0, 0)
    frame.durability:SetJustifyH("RIGHT")

    frame.combatPotionFrame = CreateFrame("Button", nil, frame)
    frame.combatPotionFrame:SetSize(DATAPANEL_COMBAT_POTION_WIDTH, 22)
    frame.combatPotionFrame:RegisterForClicks("AnyUp")
    frame.combatPotionFrame:SetScript("OnEnter", function()
        if IsDatapanelInteractionBlocked() then
            return
        end

        local settings = WipedQoL:GetDatapanelSettings()
        if settings.enabled == true and settings.combatPotion.enabled == true then
            WipedQoL:OpenDatapanelCombatPotionMenu(frame.combatPotionFrame)
        end
    end)
    frame.combatPotionFrame:SetScript("OnClick", function()
        if IsDatapanelInteractionBlocked() then
            WipedQoL:CloseDatapanelCombatPotionMenu()
            return
        end

        local settings = WipedQoL:GetDatapanelSettings()
        if settings.enabled == true and settings.combatPotion.enabled == true then
            WipedQoL:OpenDatapanelCombatPotionMenu(frame.combatPotionFrame)
        end
    end)

    frame.combatPotionIndicators = {}

    local UIHelpers = addonTable.UI
    if UIHelpers and UIHelpers.CreateToggleSwitch then
        frame.combatPotionToggle = UIHelpers.CreateToggleSwitch(
            frame.combatPotionFrame,
            DATAPANEL_COMBAT_POTION_TOGGLE_WIDTH,
            18
        )
        frame.combatPotionToggle:SetPoint("CENTER", frame.combatPotionFrame, "CENTER")
        frame.combatPotionToggle:SetScript("OnClick", function(self)
            if IsDatapanelInteractionBlocked() then
                WipedQoL:CloseDatapanelCombatPotionMenu()
                WipedQoL:RefreshDatapanelCombatPotion(true)
                return
            end

            local targetActive = not self:GetChecked()
            self:SetChecked(targetActive, true)

            if targetActive then
                if WipedQoL.UnpauseCombatPotionInMacros then
                    WipedQoL:UnpauseCombatPotionInMacros()
                end
            elseif WipedQoL.PauseCombatPotionInMacros then
                WipedQoL:PauseCombatPotionInMacros()
            end

            WipedQoL:RefreshDatapanelCombatPotion(true)
        end)
    end

    if addonTable.ApplyFont then
        addonTable.ApplyFont(frame.clock, 18, "")
        addonTable.ApplyFont(frame.lootSpec, 12, "")
        addonTable.ApplyFont(frame.autoLootRoll, 12, "")
        addonTable.ApplyFont(frame.durability, 12, "")
    end

    frame.elapsedClock = 0
    frame:SetScript("OnDragStart", function(datapanel)
        local settings = WipedQoL:GetDatapanelSettings()
        if settings.locked then
            return
        end

        datapanel:StartMoving()
        SetCenterGuideShown(true)
    end)
    frame:SetScript("OnDragStop", function(datapanel)
        datapanel:StopMovingOrSizing()
        SetCenterGuideShown(false)
        WipedQoL:SaveDatapanelPosition()
    end)
    frame:SetScript("OnHide", function()
        SetCenterGuideShown(false)
        WipedQoL:CloseDatapanelLootSpecMenu()
        WipedQoL:CloseDatapanelRepairMenu()
        WipedQoL:CloseDatapanelCombatPotionMenu()
    end)

    self.datapanelFrame = frame
    return frame
end

function WipedQoL:CreateDatapanelFps()
    if self.datapanelFpsFrame then
        return self.datapanelFpsFrame
    end

    local frame = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
    frame:SetSize(DATAPANEL_FPS_WIDTH, DATAPANEL_HEIGHT)
    frame:SetFrameStrata("LOW")
    frame:SetMovable(true)
    frame:RegisterForDrag("LeftButton")
    frame:SetClampedToScreen(true)
    frame:Hide()

    if addonTable.SkinFrame then
        addonTable.SkinFrame(frame, { light = true })
    end
    if frame._rhhBG then
        frame._rhhBG:SetVertexColor(0.02, 0.02, 0.02, 0.62)
    end
    if frame._rhhInner then
        frame._rhhInner:SetVertexColor(1, 1, 1, 0.02)
    end

    frame.stats = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    frame.stats:SetPoint("CENTER", frame, "CENTER", 0, 0)
    frame.stats:SetJustifyH("CENTER")
    frame.stats:SetTextColor(1, 1, 1, 0.95)

    if addonTable.ApplyFont then
        addonTable.ApplyFont(frame.stats, 12, "")
    end

    frame:SetScript("OnDragStart", function(fpsFrame)
        local settings = WipedQoL:GetDatapanelSettings()
        if settings.fps.fixed then
            return
        end

        fpsFrame:StartMoving()
        SetCenterGuideShown(true)
    end)
    frame:SetScript("OnDragStop", function(fpsFrame)
        fpsFrame:StopMovingOrSizing()
        SetCenterGuideShown(false)
        WipedQoL:SaveDatapanelFpsPosition()
    end)
    frame:SetScript("OnHide", function()
        SetCenterGuideShown(false)
    end)

    self.datapanelFpsFrame = frame
    return frame
end

function WipedQoL:SetDatapanelItemPosition(item, centerX)
    local frame = self.datapanelFrame
    if not frame or not item then
        return
    end

    local key = item.key

    if key == "lootSpec" and frame.lootSpecFrame then
        frame.lootSpecFrame:ClearAllPoints()
        frame.lootSpecFrame:SetSize(item.width, 22)
        frame.lootSpecFrame:SetPoint("CENTER", frame, "LEFT", centerX, 0)

        local textWidth = math.min(
            math.ceil(frame.lootSpec:GetStringWidth() or 0),
            math.max(0, item.width - DATAPANEL_LOOT_ICON_SIZE - DATAPANEL_LOOT_TEXT_GAP)
        )
        local contentWidth = DATAPANEL_LOOT_ICON_SIZE + DATAPANEL_LOOT_TEXT_GAP + textWidth
        frame.lootSpecIcon:ClearAllPoints()
        frame.lootSpecIcon:SetPoint("LEFT", frame.lootSpecFrame, "LEFT", (item.width - contentWidth) / 2, 0)
        if frame.lootSpec then
            frame.lootSpec:ClearAllPoints()
            frame.lootSpec:SetPoint("LEFT", frame.lootSpecIcon, "RIGHT", DATAPANEL_LOOT_TEXT_GAP, 0)
            frame.lootSpec:SetWidth(textWidth)
            frame.lootSpec:SetJustifyH("CENTER")
        end
    elseif key == "autoLootRoll" and frame.autoLootRollFrame then
        frame.autoLootRollFrame:ClearAllPoints()
        frame.autoLootRollFrame:SetSize(item.width, 22)
        frame.autoLootRollFrame:SetPoint("CENTER", frame, "LEFT", centerX, 0)

        local toggleWidth = frame.autoLootRollToggle and DATAPANEL_AUTO_LOOT_ROLL_TOGGLE_WIDTH or 0
        local toggleGap = frame.autoLootRollToggle and DATAPANEL_AUTO_LOOT_ROLL_TOGGLE_GAP or 0
        local textWidth = math.min(
            math.ceil(frame.autoLootRoll:GetStringWidth() or 0),
            math.max(0, item.width - DATAPANEL_LOOT_ICON_SIZE - DATAPANEL_LOOT_TEXT_GAP - toggleGap - toggleWidth)
        )
        local contentWidth = DATAPANEL_LOOT_ICON_SIZE + DATAPANEL_LOOT_TEXT_GAP + textWidth + toggleGap + toggleWidth
        frame.autoLootRollIcon:ClearAllPoints()
        frame.autoLootRollIcon:SetPoint("LEFT", frame.autoLootRollFrame, "LEFT", (item.width - contentWidth) / 2, 0)
        frame.autoLootRoll:ClearAllPoints()
        frame.autoLootRoll:SetPoint("LEFT", frame.autoLootRollIcon, "RIGHT", DATAPANEL_LOOT_TEXT_GAP, 0)
        frame.autoLootRoll:SetWidth(textWidth)
        frame.autoLootRoll:SetJustifyH("CENTER")
        if frame.autoLootRollToggle then
            frame.autoLootRollToggle:ClearAllPoints()
            frame.autoLootRollToggle:SetPoint("LEFT", frame.autoLootRoll, "RIGHT", toggleGap, 0)
        end
    elseif key == "clock" and frame.clock then
        frame.clock:ClearAllPoints()
        frame.clock:SetWidth(item.width)
        frame.clock:SetPoint("CENTER", frame, "LEFT", centerX, 0)
        frame.clock:SetJustifyH("CENTER")
    elseif key == "durability" and frame.durabilityFrame then
        frame.durabilityFrame:ClearAllPoints()
        frame.durabilityFrame:SetSize(item.width, 22)
        frame.durabilityFrame:SetPoint("CENTER", frame, "LEFT", centerX, 0)

        local iconTextGap = 1
        local textWidth = math.min(
            math.ceil(frame.durability:GetStringWidth() or 0),
            math.max(0, item.width - DATAPANEL_LOOT_ICON_SIZE - iconTextGap)
        )
        local contentWidth = DATAPANEL_LOOT_ICON_SIZE + iconTextGap + textWidth
        frame.durabilityIcon:ClearAllPoints()
        frame.durabilityIcon:SetPoint("LEFT", frame.durabilityFrame, "LEFT", (item.width - contentWidth) / 2, 0)
        frame.durability:ClearAllPoints()
        frame.durability:SetPoint("LEFT", frame.durabilityIcon, "RIGHT", iconTextGap, 0)
        frame.durability:SetWidth(textWidth)
        frame.durability:SetJustifyH("CENTER")
    elseif key == "combatPotion" and frame.combatPotionFrame then
        frame.combatPotionFrame:ClearAllPoints()
        frame.combatPotionFrame:SetSize(item.width, 22)
        frame.combatPotionFrame:SetPoint("CENTER", frame, "LEFT", centerX, 0)

        local indicators = frame.combatPotionIndicators or {}
        local familyCount = #(self._datapanelCombatPotionFamilies or {})
        local iconWidth = (familyCount * DATAPANEL_COMBAT_POTION_ICON_SIZE)
            + (math.max(familyCount - 1, 0) * DATAPANEL_COMBAT_POTION_ICON_GAP)
        local toggleGap = frame.combatPotionToggle
            and familyCount > 0
            and DATAPANEL_COMBAT_POTION_TOGGLE_GAP
            or 0
        local toggleWidth = frame.combatPotionToggle
            and DATAPANEL_COMBAT_POTION_TOGGLE_WIDTH
            or 0
        local contentWidth = iconWidth + toggleGap + toggleWidth
        local previousIndicator
        for index = 1, familyCount do
            local indicator = indicators[index]
            if indicator then
                indicator:ClearAllPoints()
                if previousIndicator then
                    indicator:SetPoint(
                        "LEFT",
                        previousIndicator,
                        "RIGHT",
                        DATAPANEL_COMBAT_POTION_ICON_GAP,
                        0
                    )
                else
                    indicator:SetPoint(
                        "LEFT",
                        frame.combatPotionFrame,
                        "LEFT",
                        (item.width - contentWidth) / 2,
                        0
                    )
                end
                previousIndicator = indicator
            end
        end
        if frame.combatPotionToggle then
            frame.combatPotionToggle:ClearAllPoints()
            if previousIndicator then
                frame.combatPotionToggle:SetPoint(
                    "LEFT",
                    previousIndicator,
                    "RIGHT",
                    toggleGap,
                    0
                )
            else
                frame.combatPotionToggle:SetPoint(
                    "LEFT",
                    frame.combatPotionFrame,
                    "LEFT",
                    (item.width - contentWidth) / 2,
                    0
                )
            end
        end
    end
end

function WipedQoL:GetDatapanelLayoutTargets(settings)
    settings = settings or self:GetDatapanelSettings()
    local items = GetDatapanelItems(settings)
    self._datapanelLayoutItems = items
    local width = GetDatapanelWidth(items)
    local targets = {}

    for index, item in ipairs(items) do
        targets[item.key] = {
            key = item.key,
            width = item.width,
            x = GetDatapanelItemCenterX(item, index, #items, width),
        }
    end

    return width, targets
end

function WipedQoL:UpdateDatapanelOnUpdate()
    local frame = self.datapanelFrame
    if not frame then
        return
    end

    if frame._datapanelClockEnabled or frame._datapanelLayoutAnimation then
        frame:SetScript("OnUpdate", DatapanelOnUpdate)
    else
        frame:SetScript("OnUpdate", nil)
    end
end

function WipedQoL:SnapDatapanelLayout(width, targets)
    local frame = self.datapanelFrame
    if not frame or width <= 0 then
        return
    end

    if IsDatapanelInteractionBlocked() then
        self.datapanelLayoutPending = true
        return
    end

    frame:SetSize(width, DATAPANEL_HEIGHT)
    frame._datapanelItemPositions = frame._datapanelItemPositions or {}
    for _, target in pairs(targets) do
        self:SetDatapanelItemPosition(target, target.x)
        frame._datapanelItemPositions[target.key] = target.x
    end
    frame._datapanelLayoutWidth = width
    self:ApplyDatapanelFpsPosition()
end

function WipedQoL:StartDatapanelLayoutAnimation(width, targets)
    local frame = self.datapanelFrame
    if not frame or width <= 0 then
        return
    end

    if IsDatapanelInteractionBlocked() then
        self.datapanelLayoutPending = true
        return
    end

    local currentPositions = frame._datapanelItemPositions or {}
    local animation = {
        elapsed = 0,
        duration = DATAPANEL_LAYOUT_ANIMATION_DURATION,
        fromWidth = frame:GetWidth() or frame._datapanelLayoutWidth or width,
        toWidth = width,
        items = {},
    }

    for key, target in pairs(targets) do
        animation.items[key] = {
            key = key,
            width = target.width,
            fromX = currentPositions[key] or target.x,
            toX = target.x,
        }
    end

    frame._datapanelLayoutAnimation = animation
    self:UpdateDatapanelOnUpdate()
end

function WipedQoL:UpdateDatapanelLayoutAnimation(elapsed)
    local frame = self.datapanelFrame
    local animation = frame and frame._datapanelLayoutAnimation
    if not animation then
        return
    end

    if IsDatapanelInteractionBlocked() then
        frame._datapanelLayoutAnimation = nil
        self.datapanelLayoutPending = true
        self:UpdateDatapanelOnUpdate()
        return
    end

    animation.elapsed = (animation.elapsed or 0) + (elapsed or 0)
    local progress = EaseOutCubic(animation.elapsed / animation.duration)
    local width = Lerp(animation.fromWidth, animation.toWidth, progress)

    frame:SetSize(width, DATAPANEL_HEIGHT)
    self:ApplyDatapanelFpsPosition()
    frame._datapanelItemPositions = frame._datapanelItemPositions or {}
    for _, item in pairs(animation.items) do
        local x = Lerp(item.fromX, item.toX, progress)
        self:SetDatapanelItemPosition(item, x)
        frame._datapanelItemPositions[item.key] = x
    end

    if progress >= 1 then
        frame:SetSize(animation.toWidth, DATAPANEL_HEIGHT)
        self:ApplyDatapanelFpsPosition()
        for _, item in pairs(animation.items) do
            self:SetDatapanelItemPosition(item, item.toX)
            frame._datapanelItemPositions[item.key] = item.toX
        end
        frame._datapanelLayoutWidth = animation.toWidth
        frame._datapanelLayoutAnimation = nil
        self:UpdateDatapanelOnUpdate()
    end
end

function WipedQoL:ApplyDatapanelLayout(settings)
    local frame = self.datapanelFrame
    if not frame then
        return
    end

    if IsDatapanelInteractionBlocked() then
        self.datapanelLayoutPending = true
        return
    end

    local width, targets = self:GetDatapanelLayoutTargets(settings)
    if width <= 0 then
        return
    end

    if frame:IsShown() and frame._datapanelLayoutWidth then
        self:StartDatapanelLayoutAnimation(width, targets)
    else
        self:SnapDatapanelLayout(width, targets)
    end
end

function WipedQoL:RefreshPendingDatapanelLayout()
    if not self.datapanelLayoutPending then
        return
    end

    self.datapanelLayoutPending = nil
    self:ApplyDatapanelLayout(self:GetDatapanelSettings())
end

function WipedQoL:RefreshDatapanelLootSpec()
    local frame = self.datapanelFrame
    if not frame or not frame.lootSpec then
        return
    end

    local settings = self:GetDatapanelSettings()
    local enabled = settings.enabled == true and settings.lootSpec.enabled == true
    SetFrameShown(frame.lootSpecFrame, enabled)
    if enabled then
        frame.lootSpec:SetText(GetLootSpecName())
        UpdateLootSpecHitbox(frame, DATAPANEL_LOOT_MAX_WIDTH)
        if not self._refreshingDatapanel and frame:IsShown() then
            self:ApplyDatapanelLayout(settings)
        end
    else
        self:CloseDatapanelLootSpecMenu()
    end
end

function WipedQoL:RefreshDatapanelAutoLootRoll()
    local frame = self.datapanelFrame
    if not frame or not frame.autoLootRoll then
        return
    end

    local settings = self:GetDatapanelSettings()
    local enabled = settings.enabled == true and settings.autoLootRoll.enabled == true
    SetFrameShown(frame.autoLootRollFrame, enabled)
    if frame.autoLootRollIcon then
        frame.autoLootRollIcon:SetShown(enabled)
    end
    frame.autoLootRoll:SetShown(enabled)
    if frame.autoLootRollToggle then
        SetFrameShown(frame.autoLootRollToggle, enabled)
    end

    if enabled then
        local rollSettings = self.GetAutoLootRollSettings and self:GetAutoLootRollSettings() or {}
        local mode = rollSettings.mode or "pass"
        frame.autoLootRoll:SetText(GetAutoLootRollModeLabel(mode))
        if frame.autoLootRollToggle then
            local blocked = IsDatapanelInteractionBlocked() == true
            frame.autoLootRollToggle:SetChecked(rollSettings.enabled == true, not blocked)
            frame.autoLootRollToggle:SetAlpha(blocked and 0.45 or 1)
            SetFrameMouseEnabled(frame.autoLootRollToggle, not blocked)
        end

        if not self._refreshingDatapanel and frame:IsShown() then
            self:ApplyDatapanelLayout(settings)
        end
    else
        self:CloseDatapanelAutoLootRollMenu()
    end
end

function WipedQoL:ApplyDatapanelPosition()
    local frame = self.datapanelFrame
    if not frame then
        return
    end

    local settings = self:GetDatapanelSettings()
    frame:ClearAllPoints()
    frame:SetPoint(settings.point or "TOP", UIParent, settings.relativePoint or "TOP", settings.x or 0, settings.y or 0)
    self:ApplyDatapanelFpsPosition()
end

function WipedQoL:SaveDatapanelPosition()
    local frame = self.datapanelFrame
    if not frame then
        return
    end

    local settings = self:GetDatapanelSettings()
    local centerX = frame:GetCenter()
    local top = frame:GetTop()
    local parentCenterX = UIParent and UIParent:GetCenter()
    local parentTop = UIParent and UIParent:GetTop()
    local x = (centerX and parentCenterX) and (centerX - parentCenterX) or 0
    local y = (top and parentTop) and (top - parentTop) or 0

    if centerX and parentCenterX and math.abs(centerX - parentCenterX) <= CENTER_SNAP_THRESHOLD then
        x = 0
    end

    settings.point = "TOP"
    settings.relativePoint = "TOP"
    settings.x = x
    settings.y = y
    self:ApplyDatapanelPosition()
end

function WipedQoL:ApplyDatapanelFpsPosition()
    local frame = self.datapanelFpsFrame
    if not frame then
        return
    end

    local settings = self:GetDatapanelSettings()
    frame:ClearAllPoints()
    if settings.fps.fixed and self.datapanelFrame and self.datapanelFrame:IsShown() then
        frame:SetWidth(DATAPANEL_FPS_WIDTH)
        local panelBottom = self.datapanelFrame:GetBottom()
        if panelBottom and panelBottom < (DATAPANEL_HEIGHT - DATAPANEL_FPS_FIXED_OFFSET_Y) then
            frame:SetPoint("BOTTOM", self.datapanelFrame, "TOP", 0, -DATAPANEL_FPS_FIXED_OFFSET_Y)
        else
            frame:SetPoint("TOP", self.datapanelFrame, "BOTTOM", 0, DATAPANEL_FPS_FIXED_OFFSET_Y)
        end
        return
    end

    frame:SetWidth(DATAPANEL_FPS_WIDTH)
    frame:SetPoint(settings.fps.point or "TOP", UIParent, settings.fps.relativePoint or "TOP", settings.fps.x or 0, settings.fps.y or -28)
end

function WipedQoL:SaveDatapanelFpsPosition()
    local frame = self.datapanelFpsFrame
    if not frame then
        return
    end

    local settings = self:GetDatapanelSettings()
    if settings.fps.fixed then
        self:ApplyDatapanelFpsPosition()
        return
    end

    local centerX = frame:GetCenter()
    local top = frame:GetTop()
    local parentCenterX = UIParent and UIParent:GetCenter()
    local parentTop = UIParent and UIParent:GetTop()
    local x = (centerX and parentCenterX) and (centerX - parentCenterX) or 0
    local y = (top and parentTop) and (top - parentTop) or -28

    if centerX and parentCenterX and math.abs(centerX - parentCenterX) <= CENTER_SNAP_THRESHOLD then
        x = 0
    end

    settings.fps.point = "TOP"
    settings.fps.relativePoint = "TOP"
    settings.fps.x = x
    settings.fps.y = y
    self:ApplyDatapanelFpsPosition()
end

function WipedQoL:SaveCurrentDatapanelFpsPositionAsFree(settings)
    local frame = self.datapanelFpsFrame
    if not frame then
        return
    end

    settings = settings or self:GetDatapanelSettings()
    local centerX = frame:GetCenter()
    local top = frame:GetTop()
    local parentCenterX = UIParent and UIParent:GetCenter()
    local parentTop = UIParent and UIParent:GetTop()
    local x = (centerX and parentCenterX) and (centerX - parentCenterX) or (settings.fps.x or 0)
    local y = (top and parentTop) and (top - parentTop) or (settings.fps.y or -28)

    if centerX and parentCenterX and math.abs(centerX - parentCenterX) <= CENTER_SNAP_THRESHOLD then
        x = 0
    end

    settings.fps.point = "TOP"
    settings.fps.relativePoint = "TOP"
    settings.fps.x = x
    settings.fps.y = y
end

function WipedQoL:RefreshDatapanelFpsLockState()
    local frame = self.datapanelFpsFrame
    if not frame then
        return
    end

    local settings = self:GetDatapanelSettings()
    SetFrameMouseEnabled(frame, settings.fps.fixed ~= true)
end

function WipedQoL:RefreshDatapanelLockState()
    local frame = self.datapanelFrame
    if not frame then
        return
    end

    local settings = self:GetDatapanelSettings()
    SetFrameMouseEnabled(frame, settings.locked ~= true)
end

function WipedQoL:RefreshDatapanelClock()
    local frame = self.datapanelFrame
    if not frame or not frame.clock then
        return
    end

    local settings = self:GetDatapanelSettings()
    local enabled = settings.enabled == true and settings.clock.enabled == true
    frame.clock:SetShown(enabled)
    if enabled then
        frame.clock:SetTextColor(GetPlayerClassColor())
        frame.clock:SetText(FormatClock(settings.clock.format))
    end
end

function WipedQoL:RefreshDatapanelFpsStats()
    local frame = self.datapanelFpsFrame
    if not frame or not frame.stats then
        return
    end

    local settings = self:GetDatapanelSettings()
    local enabled = settings.fps.enabled == true
    frame:SetShown(enabled)
    if enabled then
        frame.stats:SetTextColor(GetPlayerClassColor())
        frame.stats:SetText(FormatFpsStats())
    end
end

function WipedQoL:RefreshDatapanelDurability()
    local frame = self.datapanelFrame
    if not frame or not frame.durability then
        return
    end

    local settings = self:GetDatapanelSettings()
    local enabled = settings.enabled == true and settings.durability.enabled == true
    frame.durability:SetShown(enabled)
    if frame.durabilityFrame then
        SetFrameShown(frame.durabilityFrame, enabled)
    end
    if frame.durabilityIcon then
        frame.durabilityIcon:SetShown(enabled)
    end

    if enabled then
        local percent = GetDurabilityPercent()
        frame.durability:SetText(string.format("%d%%", percent))
        SetDurabilityColor(frame.durability, percent)
        if not self._refreshingDatapanel and frame:IsShown() then
            self:ApplyDatapanelLayout(settings)
        end
    else
        self:CloseDatapanelRepairMenu()
    end
end

function WipedQoL:RefreshDatapanelCombatPotion(skipScan)
    local frame = self.datapanelFrame
    if not frame or not frame.combatPotionFrame then
        return
    end

    local settings = self:GetDatapanelSettings()
    local enabled = settings.enabled == true and settings.combatPotion.enabled == true
    local blocked = IsDatapanelInteractionBlocked() == true
    SetFrameShown(frame.combatPotionFrame, enabled)
    SetFrameMouseEnabled(frame.combatPotionFrame, enabled and not blocked)
    if frame.combatPotionToggle then
        SetFrameShown(frame.combatPotionToggle, enabled)
    end

    if enabled and blocked then
        self:CloseDatapanelCombatPotionMenu()
    end

    if enabled then
        if not skipScan and self.RefreshCombatPotionMacroPreview then
            self:RefreshCombatPotionMacroPreview()
        end

        local previousFamilyCount = #(self._datapanelCombatPotionFamilies or {})
        local families = GetCombatPotionFamiliesInMacros()
        self._datapanelCombatPotionFamilies = families
        UpdateDatapanelCombatPotionIndicators(frame, families, true)

        if previousFamilyCount ~= #families
            and not self._refreshingDatapanel
            and frame:IsShown()
        then
            self:ApplyDatapanelLayout(settings)
        end

        local allActive, hasMacros, mixedActive = false, false, false
        if self.GetCombatPotionMacroActiveState then
            allActive, hasMacros, mixedActive = self:GetCombatPotionMacroActiveState()
        end

        if frame.combatPotionToggle then
            frame.combatPotionToggle:SetAlpha((hasMacros and not blocked) and 1 or 0.45)
            SetFrameMouseEnabled(frame.combatPotionToggle, hasMacros and not blocked)
            if mixedActive and frame.combatPotionToggle.SetMixed then
                frame.combatPotionToggle:SetMixed(true, true)
            else
                frame.combatPotionToggle:SetChecked(allActive, true)
            end
        end

        if self.datapanelCombatPotionMenu and self.datapanelCombatPotionMenu:IsShown() and self.datapanelCombatPotionMenuAnchor then
            self:OpenDatapanelCombatPotionMenu(self.datapanelCombatPotionMenuAnchor, true)
        end
    else
        UpdateDatapanelCombatPotionIndicators(
            frame,
            self._datapanelCombatPotionFamilies,
            false
        )
        self:CloseDatapanelCombatPotionMenu()
    end
end

function WipedQoL:RefreshDatapanel()
    local settings = self:GetDatapanelSettings()
    local enabled = IsAnyDatapanelDisplayEnabled(settings)
    local mainEnabled = IsDatapanelDisplayEnabled(settings)
    self:UpdateDatapanelEventRegistration(settings)

    if not enabled then
        self:CloseDatapanelMenus()
        if self.datapanelFrame then
            self.datapanelFrame._datapanelClockEnabled = nil
            self.datapanelFrame._datapanelLayoutAnimation = nil
            self.datapanelFrame:SetScript("OnUpdate", nil)
            self.datapanelFrame:Hide()
        end
        if self.datapanelFpsFrame then
            self.datapanelFpsFrame:SetScript("OnUpdate", nil)
            self.datapanelFpsFrame:Hide()
        end
        return
    end

    if mainEnabled then
        local frame = self:CreateDatapanel()
        self._refreshingDatapanel = true
        self:ApplyDatapanelPosition()
        self:RefreshDatapanelLockState()
        self:RefreshDatapanelLootSpec()
        self:RefreshDatapanelAutoLootRoll()
        self:RefreshDatapanelClock()
        self:RefreshDatapanelDurability()
        self:RefreshDatapanelCombatPotion()
        self:ApplyDatapanelLayout(settings)
        frame:SetShown(true)
        if settings.clock.enabled then
            frame.elapsedClock = CLOCK_UPDATE_INTERVAL
        end
        frame._datapanelClockEnabled = settings.clock.enabled == true
        self:UpdateDatapanelOnUpdate()
        self._refreshingDatapanel = nil
    else
        if settings.fps.enabled and settings.fps.fixed then
            if self.datapanelFpsFrame then
                self:ApplyDatapanelFpsPosition()
                self:SaveCurrentDatapanelFpsPositionAsFree(settings)
            end
            settings.fps.fixed = false
        end

        if self.datapanelFrame then
            self.datapanelFrame._datapanelClockEnabled = nil
            self.datapanelFrame._datapanelLayoutAnimation = nil
            self.datapanelFrame:SetScript("OnUpdate", nil)
            self.datapanelFrame:Hide()
        end
    end

    if settings.fps.enabled then
        local fpsFrame = self:CreateDatapanelFps()
        fpsFrame.elapsedFps = FPS_UPDATE_INTERVAL
        fpsFrame:SetScript("OnUpdate", FpsDatapanelOnUpdate)
        self:RefreshDatapanelFpsLockState()
        self:ApplyDatapanelFpsPosition()
        self:RefreshDatapanelFpsStats()
    elseif self.datapanelFpsFrame then
        self.datapanelFpsFrame:SetScript("OnUpdate", nil)
        self.datapanelFpsFrame:Hide()
    end
end
