local _, private = ...

-- Lua Globals --
-- luacheck: globals next ipairs error assert min type
-- luacheck: globals tinsert tremove wipe sort unpack

-- Libs --
local LDB = _G.LibStub("LibDataBroker-1.1")
local qTipLib = _G.LibStub("LibQTip-2.0")
local qTipKeyMap = setmetatable({}, { __mode = "k" })

local function ToTooltipKey(key)
    if type(key) == "string" then
        return key
    end

    if key == nil then
        error("attempt to use a nil key", 3)
    end

    local mappedKey = qTipKeyMap[key]
    if not mappedKey then
        mappedKey = tostring(key)
        qTipKeyMap[key] = mappedKey
    end

    return mappedKey
end

local qTip = {}

function qTip:Release(tooltip)
    qTipLib:ReleaseTooltip(tooltip)
end

function qTip:IsAcquired(key)
    return qTipLib:IsAcquiredTooltip(ToTooltipKey(key))
end

local Aurora = _G.Aurora
local Base = Aurora.Base

-- RealUI --
local RealUI = private.RealUI
local Scale = RealUI.Scale
local db, ndb

local MODNAME = "Infobar"
local Infobar = RealUI:NewModule(MODNAME, "AceEvent-3.0", "AceTimer-3.0")
local MenuFrame = RealUI:GetModule("MenuFrame")

Infobar.LDB = LDB
Infobar.locked = true

local MOVING_BLOCK
local blocksByData = {}
local orderedBlocks = {}
local BAR_HEIGHT = 16
local blockFont

local function IsCombatBlocked()
    return _G.InCombatLockdown() and not db.combatTips
end

local infobarTooltip = _G.CreateFrame("GameTooltip", "RealUI_InfobarTooltip", _G.UIParent, "GameTooltipTemplate")
Aurora.Skin.GameTooltipTemplate(infobarTooltip)

----------------------
-- Block Management --
----------------------
local function PrepareTooltip(tooltip, block)
    Infobar:debug("PrepareTooltip", tooltip, block and block.name)
    if tooltip and block then
        --RealUI.RegisterModdedFrame(tooltip)
        tooltip:ClearAllPoints()
        if tooltip.SetOwner then
            tooltip:SetOwner(block, ("ANCHOR_NONE"))
        end
        local anchor = block.side:upper()
        Infobar:debug("SetPoint", anchor)
        Scale.Point(tooltip, ("BOTTOM"..anchor), block, ("TOP"..anchor))
    end
end

local BlockMixin = {}
function BlockMixin:OnEnter()
    --Infobar:debug("OnEnter", self.name)
    --self.highlight:Show()

    if IsCombatBlocked() then return end
    local dataObj  = self.dataObj

    MenuFrame:CloseAll()
    if dataObj.tooltip then
        PrepareTooltip(dataObj.tooltip, self)
        if dataObj.tooltiptext then
            dataObj.tooltip:SetText(dataObj.tooltiptext)
        end
        dataObj.tooltip:Show()

    elseif dataObj.OnEnter then
        dataObj.OnEnter(self)

    elseif dataObj.OnTooltipShow then
        PrepareTooltip(infobarTooltip, self)
        dataObj.OnTooltipShow(infobarTooltip)
        infobarTooltip:Show()

    elseif dataObj.tooltiptext then
        PrepareTooltip(infobarTooltip, self)
        infobarTooltip:SetText(dataObj.tooltiptext)
        infobarTooltip:Show()
    end
end

function BlockMixin:OnLeave()
    Infobar:debug("OnLeave", self.name)
    --self.highlight:Hide()

    if IsCombatBlocked() then return end
    local dataObj  = self.dataObj

    if dataObj.OnTooltipShow then
        infobarTooltip:Hide()
    end

    if dataObj.OnLeave then
        dataObj.OnLeave(self)
    elseif dataObj.tooltip then
        dataObj.tooltip:Hide()
    else
        infobarTooltip:Hide()
    end
end

function BlockMixin:OnClick(...)
    Infobar:debug("OnClick", self.name, ...)
    if IsCombatBlocked() then return end
    if self.dataObj.OnClick then
        Infobar:debug("Send OnClick")
        self.dataObj.OnClick(self, ...)
    end
end

function BlockMixin:OnDragStart(button)
    Infobar:debug("OnDragStart", self.name, button)
    local dock = self:GetNearestDock()
    dock:DetatchBlock(self)

    local x, y = self:GetCenter()
    x = x - (self:GetWidth()/Scale.Value(2))
    y = y - (self:GetHeight()/Scale.Value(2))
    self:ClearAllPoints()
    Scale.RawSetPoint(self, "TOPLEFT", "UIParent", "BOTTOMLEFT", x, y)
    self:SetMovable(true)
    self:StartMoving()
    MOVING_BLOCK = self
end

function BlockMixin:OnDragStop(button)
    Infobar:debug("OnDragStop", self.name, button)
    self:StopMovingOrSizing()

    local dock = self:GetNearestDock()
    dock:AtatchBlock(self)
    self:SavePosition()

    MOVING_BLOCK = nil
end

function BlockMixin:OnEvent(event, ...)
    Infobar:debug("OnEvent", self.name, event, ...)
    self.dataObj.OnEvent(self, event, ...)

    -- Update the tooltip
    if qTip:IsAcquired(self) then
        qTip:Release(self.tooltip)
        self:OnEnter()
    end
end

function BlockMixin:OnUpdate(elapsed)
    --Infobar:debug("OnUpdate", self.name, elapsed)
    if self.dataObj.OnUpdate then
        self.dataObj.OnUpdate(self, elapsed)
    end

    if self.checkWidth and self.icon.isFont then
        local width = self.icon:GetStringWidth()
        Infobar:debug(self.name, "OnUpdate", width)
        if width > 1 then
            self:SetWidth(self:GetWidth() + width)
            self.checkWidth = nil
        end
    end

    if self == MOVING_BLOCK then
        local dock = self:GetNearestDock()
        if dock:IsMouseOver(BAR_HEIGHT * 4, 0, 0, 0) then
            local insert = dock:GetInsertIndex()
            if self.index ~= insert then
                dock:MoveBlock(self, insert)
            end
        end

        if not _G.IsMouseButtonDown(self.dragButton) then
            self:OnDragStop(self.dragButton)
            self.dragButton = nil
            MOVING_BLOCK = nil
        end
    end
end

function BlockMixin:GetNearestDock()
    local xOfs = self:GetCenter()
    if not xOfs then return Infobar.frame[self.side or "left"] end

    local uiCenter = Infobar.frame:GetWidth() / 2
    if uiCenter == 0 then return Infobar.frame[self.side or "left"] end

    local oldSide, newSide = self.side, "left"
    if xOfs > uiCenter then
        newSide = "right"
    end

    if oldSide ~= newSide then
        Infobar.frame[oldSide]:RemoveBlock(self)
        Infobar.frame[newSide]:AddBlock(self)
    end

    return Infobar.frame[newSide]
end

function BlockMixin:SavePosition()
    if not self.dataObj then return end
    local blockInfo = Infobar:GetBlockInfo(self.name, self.dataObj)

    blockInfo.side = self.side
    blockInfo.index = self.index
end

function BlockMixin:RestorePosition()
    local blockInfo = Infobar:GetBlockInfo(self.name, self.dataObj)

    local dock = self:GetNearestDock()
    dock:AddBlock(self, blockInfo.index)
end

local function AdjustElementsDeferred(block, info)
    block:AdjustElements(info)
end

function BlockMixin:AdjustElements(blockInfo)
    local font, size, outline = blockFont.font, Scale.Value(blockFont.size), blockFont.outline
    local space = 2
    local width = space

    --[[ B99: some data sources put their own NAME in `text`.

         SimulationCraft reports `text = "SimulationCraft"` with the same
         label, so the block drew the name twice — and turning "Show label"
         off changed nothing, because the surviving copy was the VALUE, which
         that setting does not govern. The toggle looked broken on exactly the
         blocks where it mattered.

         When the value is nothing but the block's own name, it IS the label
         as far as anyone looking at the bar is concerned. So: never draw both,
         and let "Show label" decide whether it appears at all. A block with a
         real value is untouched — this only fires when the two strings match.

         `LibDataBroker_AttributeChanged` calls back into here after every
         value change, so a block that later reports something meaningful gets
         its text back on the next update. ]]
    local labelText = self.label and self.label:GetText()
    local textIsName = labelText and labelText ~= ""
        and labelText == self.text:GetText()
    local showText = not (textIsName and not blockInfo.showLabel)

    self.text:SetShown(showText)
    Scale.Point(self.text, "RIGHT", -space, 0)
    self.text:SetFont(font, size, outline)
    if showText then
        width = Scale.Value(width + space) + self.text:GetStringWidth()
    end

    if self.icon then
        if blockInfo.showIcon then
            Scale.Point(self.icon, "LEFT", space, 0)
            self.icon:Show()
            local iconWidth
            if self.icon.isFont then
                local iconFont = self.dataObj.iconFont
                self.icon:SetFont(iconFont.font, size, outline)
                iconWidth = self.icon:GetStringWidth() -- Scale.Value(space)
            else
                self.icon:SetSize(size, size)
                iconWidth = size
            end

            self.checkWidth = iconWidth < 1
            width = width + Scale.Value(space) + iconWidth
            Infobar:debug("icon", width)
        else
            self.icon:Hide()
        end
    end

    -- The name-as-value case is carried by `self.text` above, so the label
    -- itself never draws for those blocks (B99).
    if blockInfo.showLabel and not textIsName then
        if self.icon and blockInfo.showIcon then
            self.label:SetPoint("LEFT", self.icon, "RIGHT", 0, 0)
        else
            Scale.Point(self.label, "LEFT", space, 0)
        end

        self.label:SetFont(font, size, outline)
        self.label:Show()
        width = width + Scale.Value(space) + self.label:GetStringWidth()
        Infobar:debug("label", self.dataObj.label, width)
    else
        self.label:Hide()
    end

    if _G.InCombatLockdown and _G.InCombatLockdown() then
        RealUI.TryInCombat(AdjustElementsDeferred, false, self, blockInfo)
        return
    end

    self:SetWidth(width)
    Scale.Height(self, BAR_HEIGHT)
end

local function SortBlocks(block1, block2)
    local name1 = block1.dataObj.name or block1.name
    local name2 = block2.dataObj.name or block2.name
    return name1 < name2
end
local function CreateNewBlock(name, dataObj, blockInfo)
    Infobar:debug("CreateNewBlock", name, dataObj)
    local block = _G.Mixin(_G.CreateFrame("Button", nil, Infobar.frame), BlockMixin)

    --[[ B91: drop the placeholder this block is replacing.
         `LibDataBroker_DataObjectCreated` parks a FAKE block for every
         disabled data source — a plain table with `isFake`, no frame and no
         BlockMixin — so it still shows up in the config list. Enabling that
         block used to append the real one and leave the fake behind, so
         `orderedBlocks` held both. The fake then had `blockInfo.enabled ==
         true` alongside it and any consumer that called a mixin method on it
         got "attempt to call a nil value" — `AdjustElements` from the Infobar
         config toggles. `blocksByData` was always replaced correctly; only the
         ordered list leaked. ]]
    local stale = blocksByData[dataObj]
    if stale and stale.isFake then
        _G.tDeleteItem(orderedBlocks, stale)
    end

    blocksByData[dataObj] = block
    block.dataObj = dataObj
    block.name = name
    tinsert(orderedBlocks, block)
    sort(orderedBlocks, SortBlocks)

    -- Create secure frame overlay for blocks that need secure actions
    if dataObj.useSecureActions then
        local secureFrame = _G.CreateFrame("Button", nil, block, "SecureActionButtonTemplate")
        secureFrame:SetAllPoints(block)
        secureFrame:EnableMouse(true)
        secureFrame:RegisterForClicks(_G.GetCVarBool("ActionButtonUseKeyDown") and "AnyDown" or "AnyUp")
        block.secureFrame = secureFrame

        -- Hook the secure frame events to the block's handlers
        secureFrame:SetScript("OnEnter", function(self)
            -- Show highlight on block
            if block.highlight then
                block.highlight:Show()
            end
            BlockMixin.OnEnter(block)
        end)
        secureFrame:SetScript("OnLeave", function(self)
            -- Hide highlight on block
            if block.highlight then
                block.highlight:Hide()
            end
            BlockMixin.OnLeave(block)
        end)

        -- Keep block mouse enabled for fallback, but secure frame takes priority
        block:EnableMouse(true)
    end

    local bg = block:CreateTexture(nil, "BACKGROUND")
    bg:SetColorTexture(1, 1, 1, 0.25)
    bg:SetAllPoints(block)
    bg:Hide()
    block.bg = bg

    local font, size, outline = blockFont.font, blockFont.size, blockFont.outline
    local text = block:CreateFontString(nil, "ARTWORK")
    text:SetFont(font, size, outline)
    text:SetTextColor(1, 1, 1)
    if dataObj.suffix and dataObj.suffix ~= "" then
        text:SetText(dataObj.value .. " " .. dataObj.suffix)
    else
        text:SetText(dataObj.value or dataObj.text)
    end
    block.text = text

    if dataObj.icon then
        local icon
        if dataObj.iconFont then
            icon = block:CreateFontString(nil, "ARTWORK")
            icon:SetFont(dataObj.iconFont.font, size, dataObj.iconFont.outline)
            icon:SetText(dataObj.icon)
            if dataObj.iconR then
                icon:SetTextColor(dataObj.iconR, dataObj.iconG, dataObj.iconB)
            end
            icon.isFont = true
        else
            icon = block:CreateTexture(nil, "ARTWORK")
            icon:SetTexture(dataObj.icon)
            Scale.Size(icon, size, size)
            if dataObj.iconR then
                icon:SetVertexColor(dataObj.iconR, dataObj.iconG, dataObj.iconB)
            end
            if dataObj.iconCoords then
                icon:SetTexCoord(unpack(dataObj.iconCoords))
            end
        end
        block.icon = icon
    end

    local label = block:CreateFontString(nil, "ARTWORK")
    label:SetFont(font, size, outline)
    label:SetTextColor(1, 1, 1)
    label:SetText(dataObj.label or dataObj.name)
    block.label = label

    local highlight = block:CreateTexture(nil, "ARTWORK")
    highlight:SetColorTexture(RealUI.charInfo.class.color:GetRGB())
    Scale.Height(highlight, 1)
    Scale.Point(highlight, "BOTTOMLEFT")
    Scale.Point(highlight, "BOTTOMRIGHT")
    highlight:Hide()
    block:SetHighlightTexture(highlight)
    block.highlight = highlight

    do -- pulseAnim
        local pulse = block:CreateTexture(nil, "ARTWORK")
        pulse:SetTexture([[Interface\PaperDollInfoFrame\UI-Character-Tab-Highlight-yellow]])
        pulse:SetBlendMode("ADD")
        pulse:SetAlpha(0)
        Scale.Point(pulse, "TOPLEFT")
        Scale.Point(pulse, "BOTTOMRIGHT")

        local pulseAnim = block:CreateAnimationGroup()
        pulseAnim:SetToFinalAlpha(true)
        pulseAnim:SetLooping("REPEAT")
        block.pulseAnim = pulseAnim

        local fadeIn = pulseAnim:CreateAnimation("Alpha")
        fadeIn:SetTarget(pulse)
        fadeIn:SetFromAlpha(0)
        fadeIn:SetToAlpha(1)
        fadeIn:SetDuration(1)
        fadeIn:SetOrder(1)

        local fadeOut = pulseAnim:CreateAnimation("Alpha")
        fadeOut:SetTarget(pulse)
        fadeOut:SetFromAlpha(1)
        fadeOut:SetToAlpha(0)
        fadeOut:SetDuration(1)
        fadeOut:SetOrder(2)
    end

    block:SetScript("OnEnter", block.OnEnter)
    block:SetScript("OnLeave", block.OnLeave)

    block:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    block:SetScript("OnClick", block.OnClick)
    block:SetScript("OnDragStart", block.OnDragStart)

    block:SetScript("OnUpdate", block.OnUpdate)
    block:AdjustElements(blockInfo)
    if block.secureFrame then
        block.secureFrame:SetAllPoints(block)
    end
    block:SetClampedToScreen(true)
    block:SetFrameLevel(Infobar.frame:GetFrameLevel() + 2)
    if block.secureFrame then
        block.secureFrame:SetFrameLevel(block:GetFrameLevel() + 5)
        block.secureFrame:SetFrameStrata(block:GetFrameStrata())
    end
    return block
end

function Infobar:AddBlock(name, dataObj, blockInfo)
    local block = blocksByData[dataObj]
    if not block or block.isFake then
        block = CreateNewBlock(name, dataObj, blockInfo)
    end

    if dataObj.events then
        block:SetScript("OnEvent", block.OnEvent)
        block:RegisterEvent("PLAYER_ENTERING_WORLD")
        for i = 1, #dataObj.events do
            block:RegisterEvent(dataObj.events[i])
        end
    end

    if blockInfo.side then
        block.side = blockInfo.side
        local dock = self.frame[blockInfo.side]
        if blockInfo.index == 1 then
            dock:SetPrimary(block)
        else
            dock:AddBlock(block, blockInfo.index)
        end
    end

    if dataObj.OnEnable then
        dataObj.OnEnable(block)
    end

    return block
end

function Infobar:RemoveBlock(name, dataObj, blockInfo)
    self:debug("Infobar:RemoveBlock", name, blockInfo.side, blockInfo.index)
    local block = blocksByData[dataObj]
    -- `AddBlock` already knows a block may not exist yet (it creates one when
    -- the lookup misses or returns a fake). Removal had no such awareness, so
    -- disabling a data object that never got a real block passed nil straight
    -- into `DockMixin:RemoveBlock` → "indexed assignment on local 'block'".
    -- Hit by the "all blocks" tristate toggle in `/realadv`, which walks every
    -- registered block including ones that were never built.
    if not block or block.isFake then return end

    if blockInfo.side then
        local dock = Infobar.frame[blockInfo.side]
        dock:RemoveBlock(block)
    end

    block:Hide()
    if dataObj.OnDisable then
        dataObj.OnDisable(block)
    end
end

function Infobar:ShowBlock(name, dataObj, blockInfo)
    self:debug("Infobar:HideBlock", name, blockInfo.side, blockInfo.index)
    local block, dock = blocksByData[dataObj], Infobar.frame[blockInfo.side]
    for i = 1, #dock.ADJUSTED_BLOCKS do
        if block == dock.ADJUSTED_BLOCKS[i].block then
            dock.ADJUSTED_BLOCKS[i].isHidden = false
            break
        end
    end

    self:AddBlock(name, dataObj, blockInfo)
end
function Infobar:HideBlock(name, dataObj, blockInfo)
    local block, position = blocksByData[dataObj], blockInfo.index
    local dock, found = Infobar.frame[blockInfo.side]
    for i = 1, #dock.ADJUSTED_BLOCKS do
        if block == dock.ADJUSTED_BLOCKS[i].block then
            dock.ADJUSTED_BLOCKS[i].isHidden = true
            found = true
            break
        end
    end

    if not found then
        local i = 1
        while i <= #dock.ADJUSTED_BLOCKS do
            if position < dock.ADJUSTED_BLOCKS[i].position then
                break
            end
            i = i + 1
        end
        tinsert(dock.ADJUSTED_BLOCKS, i, {
            position = position, -- where the block should be
            index = #dock.DOCKED_BLOCKS, -- where the block is
            isHidden = true,
            block = block
        })
    end
    self:RemoveBlock(name, dataObj, blockInfo)
end

function Infobar:LibDataBroker_DataObjectCreated(event, name, dataObj, noupdate)
    if dataObj.type == "data source" or dataObj.type == "RealUI" then
        local blockInfo = self:GetBlockInfo(name, dataObj)
        if blockInfo and blockInfo.enabled then
            self:AddBlock(name, dataObj, blockInfo)
        else
            local block = {
                dataObj = dataObj,
                name = name,
                isFake = true
            }
            blocksByData[dataObj] = block
            tinsert(orderedBlocks, block)
            sort(orderedBlocks, SortBlocks)
        end
    end
end
function Infobar:LibDataBroker_AttributeChanged(event, name, attr, value, dataObj)
    --self:debug("AttributeChanged:", event, name, attr, value, dataObj.type)
    if not db then return end
    local block = blocksByData[dataObj]
    if block and not block.isFake then
        local blockInfo = self:GetBlockInfo(name, dataObj)
        if not blockInfo then
            return
        end
        if attr == "value" or attr == "suffix" or attr == "text" then
            if dataObj.suffix and dataObj.suffix ~= "" then
                block.text:SetText(dataObj.value .. " " .. dataObj.suffix)
            else
                block.text:SetText(dataObj.value or dataObj.text)
            end
        end
        if blockInfo.showLabel and attr:find("label") then
            block.label:SetText(dataObj.label)
            if dataObj.labelR then
                block.label:SetTextColor(dataObj.labelR, dataObj.labelG, dataObj.labelB)
            end
        end
        if blockInfo.showIcon and attr:find("icon") then
            local icon = block.icon
            if icon.isFont then
                icon:SetText(dataObj.icon)
                if dataObj.iconR then
                    icon:SetTextColor(dataObj.iconR, dataObj.iconG, dataObj.iconB)
                end
            else
                block.icon:SetTexture(dataObj.icon)
                if dataObj.iconR then
                    block.icon:SetVertexColor(dataObj.iconR, dataObj.iconG, dataObj.iconB)
                end
                if dataObj.iconCoords then
                    block.icon:SetTexCoord(unpack(dataObj.iconCoords))
                end
            end
        end
        block:AdjustElements(blockInfo)
    end
end

--- Iterate the blocks in display order.
--
-- Yields **(index, block)** — `orderedBlocks` is an array. It does NOT yield
-- the data object; that lives on `block.dataObj`. Three call sites in
-- RealUI_Config bound the index as `dataObj` and fed a number into
-- `AddBlock`/`GetBlockInfo` (B91), which is why this now says so out loud.
function Infobar:IterateBlocks()
    return next, orderedBlocks
end

---------------------
-- Dock Management --
---------------------
local DockMixin = {}
function DockMixin:OnLoad()
    Scale.Height(self, BAR_HEIGHT)
    self.anchor = "BOTTOM" .. self.side:upper()
    self.anchorAlt = "BOTTOM" .. self.alt:upper()
    Scale.Point(self, self.anchor)
    Scale.Point(self, self.anchorAlt, Infobar.frame, "BOTTOM")

    self.insertHighlight = self:CreateTexture(nil, "ARTWORK")
    Scale.Size(self.insertHighlight, 1, BAR_HEIGHT)
    self.insertHighlight:SetColorTexture(1, 1, 1)

    local spacerBlock = _G.Mixin(_G.CreateFrame("Button", nil, self), BlockMixin)
    spacerBlock.name = "spacer"
    Scale.Height(spacerBlock, BAR_HEIGHT)
    self.spacerBlock = spacerBlock

    local bg = spacerBlock:CreateTexture(nil, "BACKGROUND")
    bg:SetColorTexture(1, 1, 0, 0.5)
    bg:SetAllPoints()
    spacerBlock.bg = bg

    self.DOCKED_BLOCKS = {}
    self.ADJUSTED_BLOCKS = {} -- blocks that are not in thier saved position
    self.isDirty = true    --You dirty, dirty frame
end

function DockMixin:SetPrimary(block)
    self.primary = block
    self:AddBlock(block, 1)
end

function DockMixin:AddBlock(block, position)
    if ( not self.primary ) then
        error("Need a primary block before another can be added.")
    end

    if ( self:HasDockedBlock(block) ) then
        return --We're already docked...
    end

    self.isDirty = true
    block.isDocked = true
    block.side = self.side

    position = position or (#self.DOCKED_BLOCKS + 1)
    local adjustedPosition = position
    for i = 1, #self.ADJUSTED_BLOCKS do
        if adjustedPosition < self.ADJUSTED_BLOCKS[i].position then
            adjustedPosition = self.ADJUSTED_BLOCKS[i].index
            break
        end
    end

    if ( adjustedPosition and adjustedPosition <= #self.DOCKED_BLOCKS + 1 ) then
        assert(adjustedPosition ~= 1 or block == self.primary, adjustedPosition)
        tinsert(self.DOCKED_BLOCKS, adjustedPosition, block)
    else
        tinsert(self.DOCKED_BLOCKS, block)
    end

    if position > #self.DOCKED_BLOCKS or adjustedPosition ~= position then
        -- the block is not where is should be, save both for future reference
        local i = 1
        while i <= #self.ADJUSTED_BLOCKS do
            if position < self.ADJUSTED_BLOCKS[i].position then
                break
            end
            i = i + 1
        end
        tinsert(self.ADJUSTED_BLOCKS, i, {
            position = position, -- where the block should be
            index = #self.DOCKED_BLOCKS, -- where the block is
            block = block
        })
    end

    -- Never touch the block being dragged: it is mid-StartMoving, and
    -- UpdateBlocks lays out the spacer in its place rather than re-anchoring
    -- it. Clearing its points here leaves it unanchored for the rest of the
    -- drag, after which GetCenter() returns nil.
    if ( self.primary ~= block and block ~= MOVING_BLOCK ) then
        block:ClearAllPoints()
        block:SetMovable(false)
        block:SetResizable(false)
    end

    if block == MOVING_BLOCK then
        self.spacerBlock:Show()
    end

    self:UpdateBlocks()
end

function DockMixin:RemoveBlock(block)
    -- Second line of defence for the same fault: a nil block is "nothing to
    -- undock", never an error.
    if not block or block == self.primary or #self.DOCKED_BLOCKS == 1 then return end

    self.isDirty = true
    _G.tDeleteItem(self.DOCKED_BLOCKS, block)
    block.isDocked = false

    if block == MOVING_BLOCK then
        self.spacerBlock:Hide()
    end

    block:Show()
    self:UpdateBlocks()
end

function DockMixin:HasDockedBlock(block)
    return _G.tContains(self.DOCKED_BLOCKS, block)
end

function DockMixin:GetBlockAtIndex(index)
    local block = self.DOCKED_BLOCKS[index]
    if block.isDetatched then
        return self.spacerBlock, true
    else
        return block
    end
end

function DockMixin:DetatchBlock(block)
    block.isDetatched = true
    self.spacerBlock:Show()
    self:UpdateBlocks(true)
end

function DockMixin:AtatchBlock(block)
    block.isDetatched = nil
    self.spacerBlock:Hide()
    self:UpdateBlocks(true)
end

function DockMixin:MoveBlock(block, newIndex)
    self:RemoveBlock(block)
    self:AddBlock(block, newIndex)
end

local toBeRemoved = {}
function DockMixin:UpdateBlocks(forceUpdate)
    if ( not self.isDirty and not forceUpdate ) then
        --No changes have been made since the last update.
        return
    end

    if _G.InCombatLockdown() then
        -- Defer until combat ends to avoid taint
        Infobar:RegisterEvent("PLAYER_REGEN_ENABLED", function()
            Infobar:UnregisterEvent("PLAYER_REGEN_ENABLED")
            self:UpdateBlocks(true)
        end)
        return
    end

    local lastBlock
    for index, block in ipairs(self.DOCKED_BLOCKS) do
        if forceUpdate then
            block:AdjustElements(Infobar:GetBlockInfo(block.name, block.dataObj))
        end

        wipe(toBeRemoved)
        local indexAdjust = 0
        for i = 1, #self.ADJUSTED_BLOCKS do
            if self.ADJUSTED_BLOCKS[i].isHidden and index >= self.ADJUSTED_BLOCKS[i].position then
                indexAdjust = indexAdjust + 1
            end

            if block == self.ADJUSTED_BLOCKS[i].block then
                if index == self.ADJUSTED_BLOCKS[i].position then
                    -- the block is now where is should be, remove it
                    tinsert(toBeRemoved, i)
                else
                    -- the block is *still* not where is should be, update it's index
                    self.ADJUSTED_BLOCKS[i].index = index
                end
            end
        end
        for i = 1, #toBeRemoved do
            tremove(self.ADJUSTED_BLOCKS, toBeRemoved[i])
        end
        block.index = index + indexAdjust
        block:SavePosition()
        block:Show()

        if block.isDetatched then
            self.spacerBlock:SetWidth(block:GetWidth())
            block = self.spacerBlock
        end

        block:ClearAllPoints()
        if lastBlock then
            local xOfs = self.side == "left" and db.blockGap or -db.blockGap
            Scale.Point(block, self.anchor, lastBlock, self.anchorAlt, xOfs, 0)
        else
            Scale.Point(block, self.anchor)
        end
        lastBlock = block
    end

    self.isDirty = false

    return true
end

function DockMixin:GetInsertIndex()
    local moveX = MOVING_BLOCK:GetCenter()
    if not moveX then
        -- The dragged block has no resolved position this frame; leave it
        -- where it is rather than guessing an index.
        return MOVING_BLOCK.index, false
    end

    local insertIndex, detachedIndex
    for index = 2, #self.DOCKED_BLOCKS do
        local block, isDetatched = self:GetBlockAtIndex(index)
        local center = block:GetCenter()
        if isDetatched then
            detachedIndex = index
        end

        if center then
            local width = min(block:GetWidth(), MOVING_BLOCK:GetWidth()) / 2
            local left, right = center - width, center + width
            if moveX > left and moveX < right then
                insertIndex = index
            end
        end
    end
    insertIndex = (insertIndex or detachedIndex) or #self.DOCKED_BLOCKS

    --We aren't to the left of anything, so we're going into the far-right position.
    return insertIndex, insertIndex == self.DOCKED_BLOCKS
end

--------------------
-- Bar Management --
--------------------
function Infobar:CreateBar()
    local frame = _G.CreateFrame("Frame", "RealUI_Infobar", _G.UIParent)
    frame:SetPoint("BOTTOMLEFT", _G.UIParent)
    frame:SetPoint("BOTTOMRIGHT", _G.UIParent)
    Scale.Size(frame, RealUI.GetInterfaceSize(), BAR_HEIGHT)
    frame:SetFrameStrata("LOW")
    frame:SetFrameLevel(0)
    RealUI.RegisterModdedFrame(frame, function(this)
        Scale.Size(this, RealUI.GetInterfaceSize(), BAR_HEIGHT)
        for index, block in Infobar:IterateBlocks() do
            local blockInfo = Infobar:GetBlockInfo(block.name)
            if block.AdjustElements then block:AdjustElements(blockInfo) end
        end

        -- Write Infobar's reserved-space height into BOTH profiles'
        -- position tables. ndb.positions only points at the ACTIVE
        -- profile, so we have to reach through the raw AceDB storage to
        -- update the inactive profile too. Otherwise RealUI-Healing
        -- (or whichever profile wasn't active at Infobar init) keeps the
        -- raw default of 16 and ActionBars computes bar positions 29px
        -- too low on HiDPI displays.
        local barBotY = Scale.Value(BAR_HEIGHT)
        if ndb and ndb.positions then
            if ndb.positions[1] then ndb.positions[1]["ActionBarsBotY"] = barBotY end
            if ndb.positions[2] then ndb.positions[2]["ActionBarsBotY"] = barBotY end
        end
        -- Reach into the saved-variable profile table so inactive
        -- profiles pick up the same value on their next activation.
        local coreDB = RealUI.db
        local sv = coreDB and coreDB.sv
        if sv and sv.profiles then
            for _, profileName in ipairs({"RealUI", "RealUI-Healing"}) do
                local prof = sv.profiles[profileName]
                if prof and prof.positions then
                    if prof.positions[1] then prof.positions[1]["ActionBarsBotY"] = barBotY end
                    if prof.positions[2] then prof.positions[2]["ActionBarsBotY"] = barBotY end
                end
            end
        end
    end)

    -- Stripes
    local bgAlpha = db.bgAlpha or 0.5
    Base.SetBackdrop(frame, Aurora.Color.frame, bgAlpha)
    frame:SetBackdropBorderColor(Aurora.Color.frame, bgAlpha)
    RealUI:AddFrameStripes(frame)
    frame._stripes:SetAlpha(bgAlpha)

    -- Watch bars
    local watch = {}
    watch.main = _G.CreateFrame("StatusBar", nil, frame)
    watch.main:SetStatusBarTexture(RealUI.textures.plain)
    watch.main:SetAllPoints()
    watch.main:Hide()

    local mainBar = watch.main:GetStatusBarTexture()
    watch.main.rested = watch.main:CreateTexture(nil, "ARTWORK")
    Scale.Point(watch.main.rested, "TOPLEFT", mainBar, "TOPRIGHT")
    watch.main.rested:Hide()

    watch.main.restedTick = watch.main:CreateTexture(nil, "ARTWORK")
    watch.main.restedTick:SetColorTexture(1, 1, 1, 0.5)
    Scale.Point(watch.main.restedTick, "TOPLEFT", watch.main.rested, "TOPRIGHT")
    Scale.Point(watch.main.restedTick, "BOTTOMRIGHT", watch.main.rested, "BOTTOMRIGHT", 1, 0)
    watch.main.restedTick:Hide()

    for i = 1, 2 do
        local bar = _G.CreateFrame("StatusBar", nil, frame)
        bar:SetStatusBarTexture(RealUI.textures.plain)
        Scale.Height(bar, 1)
        bar:SetFrameLevel(watch.main:GetFrameLevel() + 1)
        bar:Hide()

        local bg = bar:CreateTexture(nil, "BACKGROUND")
        bg:SetColorTexture(0, 0, 0)
        Scale.Point(bg, "TOPLEFT", bar, -1, 1)
        Scale.Point(bg, "BOTTOMRIGHT", bar, 1, -1)
        bar.bg = bg

        watch[i] = bar
    end
    Scale.Point(watch[1], "BOTTOMLEFT", watch.main, "TOPLEFT", 0, -1)
    Scale.Point(watch[1], "BOTTOMRIGHT", watch.main, "TOPRIGHT", 0, -1)

    Scale.Point(watch[2], "BOTTOMLEFT", watch.main, "TOPLEFT", 0, 1)
    Scale.Point(watch[2], "BOTTOMRIGHT", watch.main, "TOPRIGHT", 0, 1)

    frame.watch = watch

    -- Docks
    frame.left = _G.Mixin(_G.CreateFrame("Frame", nil, frame), DockMixin)
    frame.left.side = "left"
    frame.left.alt = "right"
    frame.left:OnLoad()

    frame.right = _G.Mixin(_G.CreateFrame("Frame", nil, frame), DockMixin)
    frame.right.side = "right"
    frame.right.alt = "left"
    frame.right:OnLoad()

    self.frame = frame
end

-- Blocks with a secure action overlay (e.g. hearthstone) never receive drag
-- events while the overlay owns the mouse, so toggle it with the lock state.
-- The overlay is a protected frame — leave it alone during combat lockdown.
local function SetSecureFrameMouse(block, enable)
    if block.secureFrame and not _G.InCombatLockdown() then
        block.secureFrame:EnableMouse(enable)
    end
end

function Infobar:Unlock()
    local left = self.frame.left
    for i, block in next, left.DOCKED_BLOCKS do
        if i > 1 then
            block:RegisterForDrag("LeftButton")
            block.bg:Show()
            SetSecureFrameMouse(block, false)
        end
    end

    local right = self.frame.right
    for i, block in next, right.DOCKED_BLOCKS do
        if i > 1 then
            block:RegisterForDrag("LeftButton")
            block.bg:Show()
            SetSecureFrameMouse(block, false)
        end
    end

    self.locked = false
end
function Infobar:Lock()
    local left = self.frame.left
    for i, block in next, left.DOCKED_BLOCKS do
        block:RegisterForDrag()
        block.bg:Hide()
        SetSecureFrameMouse(block, true)
    end

    local right = self.frame.right
    for i, block in next, right.DOCKED_BLOCKS do
        block:RegisterForDrag()
        block.bg:Hide()
        SetSecureFrameMouse(block, true)
    end

    self.locked = true
end

function Infobar:SettingsUpdate(setting, block)
    -- Safety check: ensure frame is initialized
    if not self.frame then
        return
    end

    -- print("setting:", setting, " block:", block)
    local showBars = db.showBars and not (db.HideStatusBarMaxLevel and _G.GameRulesUtil.IsPlayerAtEffectiveMaxLevel())
    if setting == "statusBar" or setting == "HideStatusBarMaxLevel" then
        local watch = self.frame.watch
        watch.main:SetShown(showBars)
        for i = 1, 2 do
            watch[i]:SetShown(showBars)
        end
        if block then
            block:OnEvent("SettingsUpdate")
        end
    elseif setting == "font" then
        -- B124: family/size live in the profile now, so a change has to
        -- re-resolve rather than just repaint.
        self:RefreshBlockFont()
    elseif setting == "bgAlpha" then
        self.frame:SetBackdropColor(Aurora.Color.frame, db.bgAlpha)
        self.frame:SetBackdropBorderColor(Aurora.Color.frame, db.bgAlpha)
        self.frame._stripes:SetAlpha(db.bgAlpha)
        self.frame.watch:UpdateColors()

        local outline = self:GetFontOutline()
        if blockFont.outline ~= outline then
            blockFont.outline = outline
            self.frame.left:UpdateBlocks(true)
            self.frame.right:UpdateBlocks(true)
        end
    else
        -- No specific setting (e.g. a profile switch via RefreshMod):
        -- refresh everything the profile can affect.
        local watch = self.frame.watch
        watch.main:SetShown(showBars)
        for i = 1, 2 do
            watch[i]:SetShown(showBars)
        end
        self.frame:SetBackdropColor(Aurora.Color.frame, db.bgAlpha)
        self.frame:SetBackdropBorderColor(Aurora.Color.frame, db.bgAlpha)
        self.frame._stripes:SetAlpha(db.bgAlpha)
        watch:UpdateColors()
        -- B124: a profile switch can bring a different font with it, so the
        -- catch-all branch re-resolves rather than only refreshing the outline.
        self:RefreshBlockFont()
    end
end

--[[ B124: resolve the infobar's font from settings.

     Two axes, both able to fall back to "whatever RealUI is already using", so
     the shipped look is unchanged unless someone opts out.

     FAMILY — `face = "default"` reads RealUI_Skins' chat font, which is what
     this always did. Any other value is an LSM font name.

     SIZE — `size = 0` means "match the chat font", read from ChatFrame1 rather
     than from a stored number, so it tracks the user's own chat settings and
     the Display -> Font Scale multiplier that rewrites them. It is clamped to
     the bar: a large chat font would otherwise be cropped by a 16px bar with
     no indication of why. The old `BAR_HEIGHT * 0.6` remains the last-resort
     fallback for when the chat frame cannot be read.
]]
function Infobar:ResolveBlockFont()
    local settings = (self.db and self.db.profile and self.db.profile.font) or {}
    local LSM = _G.LibStub("LibSharedMedia-3.0", true)

    -- Family
    local fontPath = "Fonts\\FRIZQT__.TTF" -- last resort: default WoW font
    local face = settings.face
    if face and face ~= "default" and LSM then
        fontPath = LSM:Fetch("font", face) or fontPath
    else
        -- RealUI_Skins is a separate addon, so this is a guarded read.
        if _G.C_AddOns.IsAddOnLoaded("RealUI_Skins") then
            local skinsDB = _G.RealUI_SkinsDB
            if skinsDB and skinsDB.profile and skinsDB.profile.fonts and skinsDB.profile.fonts.chat then
                fontPath = skinsDB.profile.fonts.chat.path or fontPath
            end
        end
    end

    -- Size
    local size = settings.size
    if not size or size <= 0 then
        -- B124 follow-up: this was `local _, chatSize = chatFrame and
        -- chatFrame:GetFont()`, and `and` truncates a multiple return to its
        -- first value — so chatSize was ALWAYS nil and "size 0 follows the
        -- chat font" silently used the fixed fallback instead. Luacheck's
        -- "variable 'chatSize' is never set" was a real bug, not noise.
        local chatFrame = _G.ChatFrame1
        local chatSize
        if chatFrame then
            local _
            _, chatSize = chatFrame:GetFont()
        end
        if chatSize and chatSize > 0 then
            -- Clamp to the bar so an oversized chat font cannot crop the text.
            size = _G.math.min(chatSize, BAR_HEIGHT - 2)
        else
            size = RealUI.Round(BAR_HEIGHT * 0.6)
        end
    end

    return {
        font = fontPath,
        size = RealUI.Round(size),
        outline = self:GetFontOutline(),
    }
end

--- Re-resolve the font and repaint every block. Called from SettingsUpdate.
function Infobar:RefreshBlockFont()
    if not blockFont then return end

    local resolved = self:ResolveBlockFont()
    blockFont.font = resolved.font
    blockFont.size = resolved.size
    blockFont.outline = resolved.outline

    if self.frame then
        self.frame.left:UpdateBlocks(true)
        self.frame.right:UpdateBlocks(true)
    end
end

function Infobar:GetBlockInfo(dataobjectname)
    local objType = type(dataobjectname)
    assert(objType == "string" or objType == "table", "\"dataobjectname\" must be a string or a table, got "..objType)

    local name, dataObj
    if objType == "table" then
        dataObj = dataobjectname
        name = LDB:GetNameByDataObject(dataObj)
        assert(dataObj.type and name, "table must be an LDB data object.")
    elseif objType == "string" then
        name = dataobjectname
        dataObj = LDB:GetDataObjectByName(name)
        assert(dataObj and dataObj.type, "string must be the name of an LDB data object.")
    end

    if not db or not db.blocks then return nil end

    if dataObj.type == "RealUI" then
        self:debug("RealUI object", name)
        return db.blocks.realui and db.blocks.realui[name]
    elseif dataObj.type == "data source" then
        self:debug("Other object", name)
        for k, v in LDB:pairs(dataObj) do
            self:debug(k, v)
        end
        return db.blocks.others and db.blocks.others[name]
    end
end
--------------------
-- Initialization --
--------------------
function Infobar:RefreshMod()
    db = self.db.profile
    ndb = RealUI.db.profile

    -- Only update settings if frame is initialized
    if self.frame then
        self:SettingsUpdate()
    end
end

function Infobar:OnInitialize()
    local specgear = {}
    for specIndex = 1, #RealUI.charInfo.specs do
        specgear[specIndex] = -1
    end
    self.db = RealUI.db:RegisterNamespace(MODNAME)
    self.db:RegisterDefaults({
        char = {
            progressState = "xp",
            currencyState = "money",
            specgear = specgear,
        },
        profile = {
            bgAlpha = 0.5,
            showBars = true,
            HideStatusBarMaxLevel = false,
            combatTips = false,
            blockGap = 3,
            --[[ B124: the infobar font had no settings at all.

                 The FAMILY already followed RealUI's chat font, but only as a
                 hardcoded read with no way to override it. The SIZE was
                 `BAR_HEIGHT * 0.6` — a derivation, not a setting, which also
                 meant changing the bar height silently changed the font size.

                 `face = "default"` keeps following RealUI's chat font, so
                 existing profiles are unchanged on that axis. `size = 0` means
                 "match the chat font", which is what the reporter asked for
                 and is resolved live in ResolveBlockFont below. ]]
            font = {
                face = "default",
                size = 0,
            },
            blocks = {
                others = {
                    ["*"] = {
                        enabled = false,
                        showLabel = false,
                        showIcon = true,
                        side = "left",
                        index = 10,
                    },
                },
                realui = {
                    ["**"] = {
                        enabled = true,
                        showLabel = false,
                        showIcon = true,
                        side = "left",
                        index = 10,
                    },
                    -- Left
                    start = {
                        side = "left",
                        index = 1,
                        enabled = -1
                    },
                    guild = {
                        side = "left",
                        index = 2,
                    },
                    friends = {
                        side = "left",
                        index = 3,
                    },
                    durability = {
                        side = "left",
                        index = 4,
                    },
                    progress = {
                        side = "left",
                        index = 5,
                    },

                    -- Right
                    clock = {
                        side = "right",
                        index = 1,
                        enabled = -1
                    },
                    mail = {
                        side = "right",
                        index = 2,
                    },
                    bags = {
                        side = "right",
                        index = 3,
                    },
                    spec = {
                        side = "right",
                        index = 4,
                    },
                    currency = {
                        side = "right",
                        index = 5,
                    },
                    netstats = {
                        showIcon = false,
                        side = "right",
                        index = 6,
                    },
                    chromie = {
                        side = "right",
                        index = 7,
                    },
                },
            },
        },
    })
    db = self.db.profile

    function self:GetFontOutline(alpha)
        alpha = alpha or db.bgAlpha or 0.5
        if alpha > 0.2 then
            return ""
        else
            return "OUTLINE"
        end
    end

    self:SetEnabledState(RealUI:GetModuleEnabled(MODNAME))
end

local function UpdateSecureClickRegistration()
    local clicks = _G.GetCVarBool("ActionButtonUseKeyDown") and "AnyDown" or "AnyUp"
    for _, block in ipairs(orderedBlocks) do
        if block.secureFrame then
            block.secureFrame:RegisterForClicks(clicks)
        end
    end
end

function Infobar:OnEnable()
    LDB.RegisterCallback(self, "LibDataBroker_DataObjectCreated")
    LDB.RegisterCallback(self, "LibDataBroker_AttributeChanged")

    -- Secure block overlays capture ActionButtonUseKeyDown at creation;
    -- re-register clicks when it changes so they stay in sync with the
    -- action bars instead of holding the stale edge until a reload.
    self:RegisterEvent("CVAR_UPDATE", function(_, cvarName)
        if cvarName == "ActionButtonUseKeyDown" or cvarName == "ACTION_BUTTON_USE_KEY_DOWN" then
            RealUI.TryInCombat(UpdateSecureClickRegistration, false)
        end
    end)

    blockFont = self:ResolveBlockFont()

    self:CreateBar()
    self:CreateBlocks()

    for name, dataObj in LDB:DataObjectIterator() do
        if dataObj.type == "data source" then
            self:LibDataBroker_DataObjectCreated("OnEnable", name, dataObj, true)
        end
    end

    -- Adjust ActionBar positions — write to BOTH profiles so either one
    -- can load into ApplyABSettings with the correct Infobar-aware value
    -- (see the matching logic in Infobar:CreateBar for rationale).
    ndb = RealUI.db.profile
    local barBotY = Scale.Value(BAR_HEIGHT)
    if ndb.positions[1] then ndb.positions[1]["ActionBarsBotY"] = barBotY end
    if ndb.positions[2] then ndb.positions[2]["ActionBarsBotY"] = barBotY end
    local sv = RealUI.db and RealUI.db.sv
    if sv and sv.profiles then
        for _, profileName in ipairs({"RealUI", "RealUI-Healing"}) do
            local prof = sv.profiles[profileName]
            if prof and prof.positions then
                if prof.positions[1] then prof.positions[1]["ActionBarsBotY"] = barBotY end
                if prof.positions[2] then prof.positions[2]["ActionBarsBotY"] = barBotY end
            end
        end
    end
end
