-- WarBoard_FakeDebuff.lua
-- Purely-cosmetic "fake debuff" buttons that sit alongside the player's REAL debuffs — WITHOUT
-- ever creating a real aura or touching UnitAura / AuraUtil data, and WITHOUT injecting into
-- Blizzard's aura list (that would taint).
--
-- ⚠ TAINT NOTE (why this does NOT add itself to DebuffFrame.auraFrames):
--   An earlier version appended the button to DebuffFrame.auraFrames so Blizzard's grid would
--   position it. But that puts our INSECURE frame into a list Blizzard's SECURE code iterates
--   (notably HUD Edit Mode: TryEditModeUpdateAuraButtons calls :Hide() on every auraFrame). Touching
--   our tainted frame taints that execution, which — on Edit Mode EXIT — flows into the raid-frame
--   reset and errors comparing secret CompactUnitFrame health values ("tainted by 'WarBoard'").
--   So instead: our button is a child of DebuffFrame.AuraContainer that Blizzard NEVER iterates, and
--   we position it ourselves in a hooksecurefunc post-hook on UpdateGridLayout — hooksecurefunc is
--   taint-isolated, so our insecure repositioning never spreads to Blizzard's caller. Fully cosmetic.
--
-- Public API (WB.FakeDebuff):
--   :Show(id, data)   data = { name, description, icon, duration, borderAtlas }  (duration nil/0 = permanent)
--   :Hide(id) / :IsShown(id) / :HideAll()
--   :RefreshLayout()

local addonName, WB = ...
WB.FakeDebuff = WB.FakeDebuff or {}
local FD = WB.FakeDebuff

local ICON_DEFAULT   = 2125377                          -- inv-bountyhunting
local BORDER_DEFAULT = "UI-Debuff-Border-Default-NoIcon"
local ICON_SIZE      = 30                               -- Blizzard aura icon width
local BTN_H          = 40                               -- Blizzard aura button height (icon + duration)
local STRIDE_DEF     = 8                                -- icons per row (DebuffFrame default)
local PAD_DEF        = 5                                -- icon padding
local TICK           = 0.1                              -- countdown refresh cadence

local active  = {}   -- id -> button (currently requested/shown)
local pool    = {}   -- recycled buttons
local pending = {}   -- id -> data (queued while DebuffFrame isn't ready yet)
local hooked  = false

-- ── Helpers ──────────────────────────────────────────────────────────────────────────────
local function Container()
    if DebuffFrame and DebuffFrame.AuraContainer then
        return DebuffFrame, DebuffFrame.AuraContainer
    end
    return nil
end

local function AbbrevTime(s)
    s = math.max(0, s)
    if SecondsToTimeAbbrev then
        local ok, str = pcall(function() return format(SecondsToTimeAbbrev(s)) end)
        if ok and type(str) == "string" and str ~= "" then return str end
    end
    s = math.floor(s + 0.5)
    if s >= 60 then return math.floor(s / 60) .. "m" end
    return s .. "s"
end

local function ShowTooltip(b)
    -- Debuffs read out the BOTTOM-LEFT of the icon (matches Blizzard's AuraButton). Title in the
    -- standard aura-name gold/yellow (NORMAL_FONT_COLOR = 1, 0.82, 0).
    GameTooltip:SetOwner(b, "ANCHOR_BOTTOMLEFT")
    GameTooltip:SetText(b.wbName or "War Board", 1, 0.82, 0)
    if b.wbDesc and b.wbDesc ~= "" then
        GameTooltip:AddLine(b.wbDesc, 1, 1, 1, true)
    end
    if b.wbExpiry then
        local left = b.wbExpiry - GetTime()
        if left > 0 then GameTooltip:AddLine(AbbrevTime(left) .. " remaining", 0.6, 0.8, 1) end
    end
    GameTooltip:Show()
end

-- Countdown driver; auto-remove on expiry. Permanent (no wbExpiry) buttons idle here.
local function OnButtonUpdate(self, elapsed)
    if not self.wbExpiry then return end
    self._acc = (self._acc or 0) + elapsed
    if self._acc < TICK then return end
    self._acc = 0
    local left = self.wbExpiry - GetTime()
    if left <= 0 then
        FD:Hide(self.wbId)
        return
    end
    self.Duration:SetText(AbbrevTime(left))
    if GameTooltip:IsOwned(self) then ShowTooltip(self) end
end

-- ── Button construction (manual — never an AuraButton, so zero cancel/dispel behaviour) ──
local function AcquireButton()
    local b = table.remove(pool)
    if b then return b end

    local _, container = Container()
    b = CreateFrame("Button", nil, container)
    b.WarBoardFake = true
    b:SetSize(ICON_SIZE, BTN_H)

    b.Icon = b:CreateTexture(nil, "ARTWORK")
    b.Icon:SetSize(ICON_SIZE, ICON_SIZE)
    b.Icon:SetPoint("TOP", b, "TOP")

    -- Debuff-style border. It must FRAME the icon (overhang it), so it's centered on the icon and
    -- takes the atlas's own (larger) size — NOT SetAllPoints(Icon), which shrinks the ring inside it.
    b.border = b:CreateTexture(nil, "OVERLAY")
    b.border:SetPoint("CENTER", b.Icon, "CENTER", 0, 0)

    b.Duration = b:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    b.Duration:SetPoint("TOP", b.Icon, "BOTTOM", 0, -1)
    b.Duration:SetTextColor(1, 0.9, 0.4)

    b:SetScript("OnEnter",  ShowTooltip)
    b:SetScript("OnLeave",  GameTooltip_Hide)
    b:SetScript("OnUpdate", OnButtonUpdate)
    return b
end

-- ── Positioning (taint-safe: runs inside a hooksecurefunc post-hook, so it never taints Blizzard) ──
-- Our fakes are PINNED to the first slots (slot 0 = the grow corner / rightmost for the player debuff
-- frame) and never move; the real debuffs are re-anchored to the slots AFTER them, so an incoming
-- debuff lands to the LEFT of the fake instead of shoving it. We anchor each element RELATIVE to the
-- one before it (avoids replicating GridLayoutUtil math). Reading Blizzard fields is fine; writing to
-- Blizzard buttons here is safe because the hook is taint-isolated and aura-button positions are
-- never read by secure/combat code.
local managing = false   -- true while we're overriding Blizzard's debuff positions (a fake is up)
local function RepositionFakes(frame)
    frame = frame or DebuffFrame
    local container = frame and frame.AuraContainer
    if not container then return end

    -- Our shown fakes, in a stable order.
    local fakes = {}
    for _, b in pairs(active) do fakes[#fakes + 1] = b end

    -- No fakes and we weren't managing → Blizzard owns the layout; don't touch anything.
    if #fakes == 0 and not managing then return end

    -- During HUD Edit Mode Blizzard fills every slot with example auras; hide our fakes and leave the
    -- real buttons alone (they come back when this hook runs again on exit).
    if frame.isInEditMode then
        for _, b in ipairs(fakes) do b:Hide() end
        managing = (#fakes > 0)
        return
    end

    table.sort(fakes, function(x, y) return (x.wbId or "") < (y.wbId or "") end)

    -- Real debuffs currently shown, in auraFrames (grid) order.
    local reals = {}
    for _, b in ipairs(frame.auraFrames or {}) do
        if not b.WarBoardFake and not b.isAuraAnchor and b.hasValidInfo and b:IsShown() then
            reals[#reals + 1] = b
        end
    end

    local stride    = container.iconStride  or STRIDE_DEF
    local pad       = container.iconPadding or PAD_DEF
    local growRight = container.addIconsToRight and true or false
    local growTop   = container.addIconsToTop and true or false
    local scale     = container.iconScale or 1
    local corner    = (growTop and (growRight and "BOTTOMLEFT" or "BOTTOMRIGHT"))
                   or (growRight and "TOPLEFT" or "TOPRIGHT")

    -- FAKES FIRST (slots 0..F-1, pinned), then the real debuffs shifted to the slots after them.
    -- When #fakes == 0 this is the cleanup pass: it re-packs the reals into slots 0..N-1 (restoring
    -- Blizzard's default) exactly once, after which we stop managing and hand the layout back.
    local seq = {}
    for _, b in ipairs(fakes) do seq[#seq + 1] = b end
    for _, b in ipairs(reals) do seq[#seq + 1] = b end

    for i = 1, #seq do
        local b   = seq[i]
        local col = (i - 1) % stride
        b:SetScale(scale)
        b:ClearAllPoints()
        if col == 0 then
            local above = seq[i - stride]          -- icon directly "above" (same column, prev row)
            if above then
                if growTop then b:SetPoint("BOTTOM", above, "TOP",    0,  pad)
                else            b:SetPoint("TOP",    above, "BOTTOM", 0, -pad) end
            else
                b:SetPoint(corner, container, corner, 0, 0)   -- slot 0 → pinned to the grow corner
            end
        else
            local prev = seq[i - 1]
            if growRight then b:SetPoint("LEFT",  prev, "RIGHT",  pad, 0)
            else              b:SetPoint("RIGHT", prev, "LEFT",  -pad, 0) end
        end
        if b.WarBoardFake then b:Show() end
    end

    managing = (#fakes > 0)   -- once no fakes remain, this run restored the reals; stop next time
end
FD._Reposition = RepositionFakes

-- Post-hook so our fakes re-pack whenever the real debuffs relayout. hooksecurefunc keeps our
-- insecure repositioning from tainting Blizzard's execution (the whole reason for this design).
local function EnsureHook()
    if hooked then return end
    if not (DebuffFrame and DebuffFrame.UpdateGridLayout) then return end
    hooksecurefunc(DebuffFrame, "UpdateGridLayout", RepositionFakes)
    hooked = true
end

-- ── Public API ─────────────────────────────────────────────────────────────────────────────
function FD:Show(id, data)
    if type(id) ~= "string" or type(data) ~= "table" then return end
    EnsureHook()

    local frame, container = Container()
    if not (frame and container) then
        pending[id] = data
        return
    end

    local b = active[id] or AcquireButton()
    active[id] = b
    b.wbId     = id
    b.wbName   = data.name or "War Board"
    b.wbDesc   = data.description
    b:SetParent(container)

    b.Icon:SetTexture(data.icon or ICON_DEFAULT)
    -- RED undispellable debuff frame. AuraUtil.SetAuraBorderAtlas(border) with NO dispel type is
    -- exactly what a real "None" (undispellable) debuff uses — the correct red NoIcon border atlas +
    -- colour for this client build. Fall back to the raw atlas + red tint if the helper is missing.
    local applied = AuraUtil and AuraUtil.SetAuraBorderAtlas
        and pcall(AuraUtil.SetAuraBorderAtlas, b.border)
    if not applied then
        b.border:SetAtlas(data.borderAtlas or BORDER_DEFAULT)
        b.border:SetVertexColor(0.8, 0, 0)
    end
    -- Size the border to FRAME the icon: use the atlas's native size (larger than the 30px icon so
    -- the ring overhangs it), or a sensible fallback. Centred on the icon (anchor set at creation).
    local atlas = b.border:GetAtlas()
    if atlas then
        b.border:SetAtlas(atlas, true)   -- useAtlasSize = true → native (overhanging) size
    end
    if (b.border:GetWidth() or 0) <= ICON_SIZE then
        b.border:SetSize(ICON_SIZE + 8, ICON_SIZE + 8)   -- guarantee an overhang if the atlas was tight
    end

    local dur = tonumber(data.duration)
    if dur and dur > 0 then
        b.wbStart, b.wbExpiry = GetTime(), GetTime() + dur
        b.Duration:SetText(AbbrevTime(dur))
        b.Duration:Show()
    else
        b.wbStart, b.wbExpiry = nil, nil
        b.Duration:SetText("")
        b.Duration:Hide()
    end
    b._acc = 0
    b:Show()

    RepositionFakes(frame)
end

function FD:Hide(id)
    local b = active[id]
    if not b then return end
    active[id] = nil
    b.wbExpiry = nil
    b:Hide()
    b:ClearAllPoints()
    pool[#pool + 1] = b
    RepositionFakes()
end

function FD:RefreshLayout()
    RepositionFakes()
end

function FD:IsShown(id)
    return active[id] ~= nil
end

function FD:HideAll()
    for id in pairs(active) do self:Hide(id) end
end

-- ── Wiring: flush queued shows + reposition after loading screens ────────────────────────────
local ev = CreateFrame("Frame")
ev:RegisterEvent("PLAYER_LOGIN")
ev:RegisterEvent("PLAYER_ENTERING_WORLD")
ev:SetScript("OnEvent", function()
    EnsureHook()
    if next(pending) and Container() then
        local todo = pending; pending = {}
        for id, data in pairs(todo) do FD:Show(id, data) end
    end
    RepositionFakes()
end)
