
-- ============================================================
-- FILE: CooldownManager.lua
-- PURPOSE: Positions, sizes, and styles Blizzard's cooldown viewer
--   panels (EssentialCooldownViewer, UtilityCooldownViewer,
--   BuffIconCooldownViewer, BuffBarCooldownViewer) using custom
--   draggable anchor frames registered with MoverRegistry.
-- HOW IT WORKS:
--   Four PANELS each have a named anchor Frame (GetAnchor, lazy).
--   On ApplyAllPanels, the Blizzard viewer is SetParented into
--   the anchor via QueueProtectedOp (deferred if in combat).
--   hooksecurefunc on viewer.SetPoint/Layout/UpdateLayout intercepts
--   Blizzard's repositioning attempts and schedules a reanchor
--   via ScheduleReanchor (C_Timer.After(0)).
--   Chrome: custom BackdropTemplate border + dark swipe overlay
--   drawn per visible icon via viewer:GetItemFrames().
--   Visibility: combat/OOC/instance modes with smooth alpha fade
--   driven by an OnUpdate easing loop (smooth cubic Hermite).
--   Drag: EnsurePanelOverlay builds a draggable HIGH-strata overlay
--   per anchor; shown when Messenger.locked=false.
--   A 0.1s safety-net ticker on login keeps reapplying until 50
--   ticks pass, handling delayed viewer initialization.
-- DEPENDS ON:
--   MidnightUI_MoverRegistry         — position management
--   MidnightUI_RegisterOverlaySettings — settings overlay
--   MidnightUI_StyleOverlay           — overlay chrome styling
--   MidnightUI_CreateOverlayBuilder   — settings panel builder
--   Blizzard CooldownViewer frames    (EssentialCooldownViewer etc.)
-- EXPORTS:
--   _G.MidnightUI_ApplyCooldownManagerSettings()
--   _G.MidnightUI_CDM_ResetPositions()
-- OWNED SAVEDVARIABLES:
--   MidnightUISettings.CooldownManager  — per-panel config + positions
-- EVENTS REGISTERED: ADDON_LOADED, PLAYER_LOGIN,
--   PLAYER_ENTERING_WORLD, PLAYER_REGEN_ENABLED, PLAYER_REGEN_DISABLED,
--   ZONE_CHANGED_NEW_AREA, EDIT_MODE_LAYOUTS_UPDATED
-- FRAMES CREATED:
--   MidnightUI_CooldownAnchor_<key> per panel (4 total)
--   Chrome border frames per visible icon (dynamic)
--   Drag overlay per panel anchor (lazy)
-- GOTCHAS:
--   SetParent on a Blizzard viewer is a protected operation —
--   always goes through QueueProtectedOp → FlushPendingOps on
--   PLAYER_REGEN_ENABLED.
--   MEMORY NOTE from project: SetParent IS REQUIRED; SetPoint alone
--   is silently ignored by the layout engine for these viewer frames.
--   CleanSize uses Core.CleanNumber or tonumber(tostring(v)) to
--   safely handle tainted GetWidth/GetHeight return values.
-- ============================================================

local ADDON_NAME = "MidnightUI"

local _G = _G
local CreateFrame = CreateFrame
local InCombatLockdown = InCombatLockdown
local C_Timer = C_Timer
local ipairs = ipairs
local pairs = pairs
local pcall = pcall
local tostring = tostring
local math_floor = math.floor

local ManagerEvent = CreateFrame("Frame")


local PANELS = {
    {
        key          = "essential",
        label        = "Essential Cooldowns",
        moverKey     = "CooldownManager_Essential",
        viewerName   = "EssentialCooldownViewer",
        anchorGlobal = "MidnightUI_CooldownAnchor_Essential",
        defaultPos   = "BOTTOM,UIParent,BOTTOM,0.00,180.00",
    },
    {
        key          = "utility",
        label        = "Utility Cooldowns",
        moverKey     = "CooldownManager_Utility",
        viewerName   = "UtilityCooldownViewer",
        anchorGlobal = "MidnightUI_CooldownAnchor_Utility",
        defaultPos   = "BOTTOM,UIParent,BOTTOM,0.00,240.00",
    },
    {
        key          = "buffIcon",
        label        = "Tracked Buffs (Icons)",
        moverKey     = "CooldownManager_BuffIcon",
        viewerName   = "BuffIconCooldownViewer",
        anchorGlobal = "MidnightUI_CooldownAnchor_BuffIcon",
        defaultPos   = "BOTTOM,UIParent,BOTTOM,0.00,300.00",
    },
    {
        key          = "buffBar",
        label        = "Tracked Buffs (Bars)",
        moverKey     = "CooldownManager_BuffBar",
        viewerName   = "BuffBarCooldownViewer",
        anchorGlobal = "MidnightUI_CooldownAnchor_BuffBar",
        defaultPos   = "RIGHT,UIParent,RIGHT,-24.00,0.00",
    },
}

local PANEL_BY_KEY      = {}
local PANEL_BY_MOVERKEY = {}
for _, p in ipairs(PANELS) do
    PANEL_BY_KEY[p.key]            = p
    PANEL_BY_MOVERKEY[p.moverKey]  = p
end


-- ============================================================
-- SECTION: Module Init + PANELS Config
-- ManagerEvent: bare event frame for the module.
-- PANELS: ordered array defining the four cooldown viewer panels.
--   Each entry: key (settings key), label (display name), moverKey
--   (MoverRegistry identifier), viewerName (Blizzard frame name),
--   anchorGlobal (named anchor frame), defaultPos (position string).
-- PANEL_BY_KEY/PANEL_BY_MOVERKEY: O(1) lookup tables built from PANELS.
-- READS:  (pure declarations)
-- WRITES: PANEL_BY_KEY, PANEL_BY_MOVERKEY
-- FUNCTIONS: (data only)
-- ============================================================
local function EnsureCooldownManagerSettings()
    if not _G.MidnightUISettings then _G.MidnightUISettings = {} end
    local s = _G.MidnightUISettings
    if not s.CooldownManager then s.CooldownManager = {} end
    local cm = s.CooldownManager
    if cm.enabled         == nil then cm.enabled         = true     end
    if cm.oocFadeEnabled  == nil then cm.oocFadeEnabled  = false    end
    if cm.oocFadeAlpha    == nil then cm.oocFadeAlpha    = 40       end

    if not cm._borderMigrated then
        cm._borderMigrated = true
        for _, p in ipairs(PANELS) do
            if cm[p.key] then cm[p.key].showBorder = false end
        end
    end
    for _, p in ipairs(PANELS) do
        if not cm[p.key] then cm[p.key] = {} end
        local sub = cm[p.key]
        if sub.enabled        == nil then sub.enabled        = true     end
        if sub.scale          == nil then sub.scale          = 100      end
        if sub.alpha          == nil then sub.alpha          = 100      end
        if sub.offsetX        == nil then sub.offsetX        = 0        end
        if sub.offsetY        == nil then sub.offsetY        = 0        end
        if sub.visibility     == nil then sub.visibility     = "Always" end
        if sub.showBorder     == nil then sub.showBorder     = false    end
        if sub.oocFadeEnabled == nil then
            sub.oocFadeEnabled = cm.oocFadeEnabled == true
        end
        if sub.oocFadeAlpha   == nil then
            sub.oocFadeAlpha = cm.oocFadeAlpha or 60
        end
        if sub.darkSwipe  == nil then
            sub.darkSwipe = (p.key ~= "buffBar" and p.key ~= "buffIcon")
        end
    end
    return cm
end

local function GetPanelSettings(panel)
    local cm = EnsureCooldownManagerSettings()
    return cm[panel.key]
end


-- ============================================================
-- SECTION: Settings Init + Combat-Safe Operation Queue
-- EnsureCooldownManagerSettings: lazily initializes
--   MidnightUISettings.CooldownManager and per-panel sub-tables
--   with all defaults. Runs _borderMigrated upgrade pass once.
-- GetPanelSettings: convenience accessor for cm[panel.key].
-- QueueProtectedOp/FlushPendingOps: combat lockdown deferral.
--   QueueProtectedOp runs fn immediately if out of combat; otherwise
--   stores it by label (dedup) in pendingOps/pendingOrder.
--   FlushPendingOps drains the queue on PLAYER_REGEN_ENABLED.
-- READS:  MidnightUISettings.CooldownManager
-- WRITES: MidnightUISettings.CooldownManager (lazy init),
--         pendingOps, pendingOrder
-- FUNCTIONS:
--   EnsureCooldownManagerSettings() → cm table
--   GetPanelSettings(panel)         → sub-table
--   QueueProtectedOp(label, fn)     — deferred-safe fn call
--   FlushPendingOps()               — drain queue on combat exit
-- ============================================================
local pendingOps = {}
local pendingOrder = {}

local function QueueProtectedOp(label, fn)
    if not InCombatLockdown() then
        local ok, err = pcall(fn)
        if not ok and _G.MidnightUI_Debug then
            _G.MidnightUI_Debug("[CooldownManager] " .. tostring(label) .. " failed: " .. tostring(err))
        end
        return
    end
    if not pendingOps[label] then
        pendingOrder[#pendingOrder + 1] = label
    end
    pendingOps[label] = fn
end

local function FlushPendingOps()
    if InCombatLockdown() then return end
    local order = pendingOrder
    local ops = pendingOps
    pendingOrder = {}
    pendingOps = {}
    for _, label in ipairs(order) do
        local fn = ops[label]
        if fn then
            local ok, err = pcall(fn)
            if not ok and _G.MidnightUI_Debug then
                _G.MidnightUI_Debug("[CooldownManager] flush " .. tostring(label) .. " failed: " .. tostring(err))
            end
        end
    end
end


-- ============================================================
-- SECTION: Viewer Access + Hook System + Anchor + Position
-- CleanSize: taint-safe GetWidth/GetHeight — uses Core.CleanNumber
--   or tonumber(tostring(v)) to strip taint from secret dimensions.
-- GetBlizzardViewer: looks up viewer frame by name from PANELS[].viewerName.
-- hookedViewers/hookedLayoutViewers: guard flags preventing double hooks.
-- reanchorScheduled/chromeReapplyScheduled: per-panel one-shot guards
--   preventing C_Timer.After spam when Blizzard calls SetPoint rapidly.
-- HookViewerReanchor: hooks viewer.SetPoint (via hooksecurefunc) to
--   detect Blizzard repositioning and schedule reanchor.
--   Also hooks Layout/UpdateLayout/RefreshLayout for chrome reapply.
-- HookAllViewers: calls HookViewerReanchor for all 4 panels.
-- GetAnchor: lazy-creates the named anchor Frame per panel, sets
--   default position from panel.defaultPos via MR.StringToPoint.
-- ApplyPanelPosition: SetParents viewer to anchor, then SetPoint
--   CENTER + offset. Goes through QueueProtectedOp. Syncs anchor size
--   to viewer dimensions via CleanSize.
-- READS:  _G[panel.viewerName], _G[panel.anchorGlobal],
--         MidnightUI_MoverRegistry
-- WRITES: hookedViewers, hookedLayoutViewers, named anchor frame (_G)
-- FUNCTIONS:
--   CleanSize(val, fallback) → number
--   GetBlizzardViewer(panel) → frame or nil
--   ScheduleReanchor(panel), ScheduleChromeReapply(panel)
--   HookViewerReanchor(panel), HookAllViewers()
--   GetAnchor(panel) → anchor frame
--   ApplyPanelPosition(panel) — deferred-safe SetPoint + SetParent
-- ============================================================
local function CleanSize(val, fallback)
    local Core = _G.MidnightUI_Core
    if Core and Core.CleanNumber then
        return Core.CleanNumber(val, fallback)
    end
    local n = tonumber(tostring(val))
    return n or fallback
end

local function GetBlizzardViewer(panel)
    return _G[panel.viewerName]
end


local hookedViewers       = {}
local hookedLayoutViewers = {}
local reanchorScheduled   = {}
local chromeReapplyScheduled = {}

local _muiInCombat = false

local function CMLog(msg)
    if _G.MidnightUI_Diagnostics and _G.MidnightUI_Diagnostics.LogDebugSource then
        _G.MidnightUI_Diagnostics.LogDebugSource("CooldownManager", tostring(msg))
    end
end

local ApplyPanelPosition
local ApplyChrome

local function ScheduleReanchor(panel)
    if reanchorScheduled[panel.key] then return end
    reanchorScheduled[panel.key] = true
    if not C_Timer or not C_Timer.After then return end
    C_Timer.After(0, function()
        reanchorScheduled[panel.key] = false
        if ApplyPanelPosition then ApplyPanelPosition(panel) end
    end)
end

local function ScheduleChromeReapply(panel)
    if chromeReapplyScheduled[panel.key] then return end
    chromeReapplyScheduled[panel.key] = true
    if not C_Timer or not C_Timer.After then return end
    C_Timer.After(0, function()
        chromeReapplyScheduled[panel.key] = false
        if ApplyChrome then ApplyChrome(panel) end
    end)
end

local function HookViewerReanchor(panel)
    local viewer = GetBlizzardViewer(panel)
    if not viewer then return end
    if type(viewer.SetPoint) ~= "function" then return end

    if not hookedViewers[viewer] then
        hookedViewers[viewer] = true
        local ok = pcall(hooksecurefunc, viewer, "SetPoint", function(self, ...)
            if self._muiSetting then return end
            ScheduleReanchor(panel)
        end)
        if not ok then
            hookedViewers[viewer] = nil
            CMLog("SetPoint hook install FAILED " .. panel.key)
        end
    end

    if not hookedLayoutViewers[viewer] then
        hookedLayoutViewers[viewer] = true
        local hooked = false
        local function onRelayout()
            ScheduleChromeReapply(panel)
        end
        if type(viewer.Layout) == "function" then
            local ok = pcall(hooksecurefunc, viewer, "Layout", onRelayout)
            if ok then hooked = true end
        end
        if type(viewer.UpdateLayout) == "function" then
            local ok = pcall(hooksecurefunc, viewer, "UpdateLayout", onRelayout)
            if ok then hooked = true end
        end
        if type(viewer.RefreshLayout) == "function" then
            local ok = pcall(hooksecurefunc, viewer, "RefreshLayout", onRelayout)
            if ok then hooked = true end
        end
        if not hooked then
            hookedLayoutViewers[viewer] = nil
        end
    end
end

local function HookAllViewers()
    for _, panel in ipairs(PANELS) do
        HookViewerReanchor(panel)
    end
end

local function GetAnchor(panel)
    local existing = _G[panel.anchorGlobal]
    if existing then return existing end

    local anchor = CreateFrame("Frame", panel.anchorGlobal, UIParent)
    anchor:SetSize(180, 50)
    anchor:SetFrameStrata("MEDIUM")
    anchor:SetFrameLevel(20)
    anchor:SetMovable(true)
    anchor:SetClampedToScreen(true)
    anchor:EnableMouse(false)
    anchor._muiPanelKey = panel.key

    local MR = _G.MidnightUI_MoverRegistry
    if MR and MR.StringToPoint then
        local point, anchorName, relPoint, x, y = MR.StringToPoint(panel.defaultPos)
        if point then
            anchor:ClearAllPoints()
            anchor:SetPoint(point, _G[anchorName] or UIParent, relPoint, x, y)
        end
    end

    _G[panel.anchorGlobal] = anchor
    return anchor
end

ApplyPanelPosition = function(panel)
    local viewer = GetBlizzardViewer(panel)
    if not viewer then return end
    local anchor = GetAnchor(panel)
    if not anchor then return end

    HookViewerReanchor(panel)

    local sub = GetPanelSettings(panel)
    local offX = (sub and sub.offsetX) or 0
    local offY = (sub and sub.offsetY) or 0

    QueueProtectedOp("anchor:" .. panel.key, function()
        if not viewer.SetPoint then return end
        viewer._muiSetting = true
        local ok, err = pcall(function()
            if viewer:GetParent() ~= anchor then
                viewer:SetParent(anchor)
            end
            viewer:ClearAllPoints()
            viewer:SetPoint("CENTER", anchor, "CENTER", offX, offY)
            viewer:SetScale(1)
        end)
        viewer._muiSetting = false
        if not ok then
            CMLog("ApplyPanelPosition " .. panel.key .. " SetPoint FAILED: " .. tostring(err))
            return
        end
        local w = math.max(180, CleanSize(viewer:GetWidth(), 180))
        local h = math.max(50,  CleanSize(viewer:GetHeight(), 50))
        anchor:SetSize(w, h)
    end)
end


-- ============================================================
-- SECTION: Chrome Border System
-- Draws custom BackdropTemplate border frames over each icon in a
-- cooldown panel. Chrome = outer border + inner stroke + dark swipe.
-- panelChromes[panel.key]: map of {icon → chrome frame}.
-- CreateChromeFrame: builds a chrome for a given icon. Parented to
--   the anchor, not the icon (icon may be secure).
-- SyncChromeFrame: positions/sizes chrome to match its icon's current
--   screen bounds. Hides chrome if icon is hidden or has no size.
-- ApplyChrome: calls viewer:GetItemFrames(), creates/updates chrome
--   for each visible icon. Hides chrome frames no longer in the list.
--   Respects sub.showBorder and sub.darkSwipe settings.
-- READS:  viewer:GetItemFrames(), panelChromes, sub.showBorder/darkSwipe
-- WRITES: panelChromes (lazy chrome frame creation)
-- FUNCTIONS:
--   CreateChromeFrame(panel, blizzIcon) → chrome frame
--   SyncChromeFrame(chrome)             — position + show/hide
--   ApplyChrome(panel)                  — full panel chrome update
-- ============================================================
local CHROME_BORDER_R, CHROME_BORDER_G, CHROME_BORDER_B, CHROME_BORDER_A = 0.10, 0.04, 0.16, 0.95
local CHROME_INNER_R,  CHROME_INNER_G,  CHROME_INNER_B,  CHROME_INNER_A  = 0.05, 0.02, 0.10, 0.90
local CHROME_SWIPE_R,  CHROME_SWIPE_G,  CHROME_SWIPE_B,  CHROME_SWIPE_A  = 0,    0,    0,    0.20

local panelChromes = {}

local function CreateChromeFrame(panel, blizzIcon)
    local anchor = GetAnchor(panel)
    if not anchor then return nil end

    local chrome = CreateFrame("Frame", nil, anchor, "BackdropTemplate")
    chrome:SetFrameStrata("MEDIUM")
    chrome:SetFrameLevel(50)
    chrome:EnableMouse(false)
    chrome._blizzIcon = blizzIcon

    chrome:SetBackdrop({
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        edgeSize = 1,
    })
    chrome:SetBackdropBorderColor(CHROME_BORDER_R, CHROME_BORDER_G, CHROME_BORDER_B, CHROME_BORDER_A)

    local inner = CreateFrame("Frame", nil, chrome, "BackdropTemplate")
    inner:SetPoint("TOPLEFT",     chrome, "TOPLEFT",      1, -1)
    inner:SetPoint("BOTTOMRIGHT", chrome, "BOTTOMRIGHT", -1,  1)
    inner:SetBackdrop({
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        edgeSize = 1,
    })
    inner:SetBackdropBorderColor(CHROME_INNER_R, CHROME_INNER_G, CHROME_INNER_B, CHROME_INNER_A)
    inner:EnableMouse(false)
    chrome._innerStroke = inner

    local swipe = chrome:CreateTexture(nil, "OVERLAY")
    swipe:SetPoint("TOPLEFT",     chrome, "TOPLEFT",      1, -1)
    swipe:SetPoint("BOTTOMRIGHT", chrome, "BOTTOMRIGHT", -1,  1)
    swipe:SetColorTexture(CHROME_SWIPE_R, CHROME_SWIPE_G, CHROME_SWIPE_B, CHROME_SWIPE_A)
    chrome._darkSwipe = swipe

    chrome:Hide()
    return chrome
end

local function SyncChromeFrame(chrome)
    local icon = chrome._blizzIcon
    if not icon then chrome:Hide(); return end
    if not icon:IsShown() then chrome:Hide(); return end

    local w = CleanSize(icon:GetWidth(),  32)
    local h = CleanSize(icon:GetHeight(), 32)
    if w <= 0 or h <= 0 then chrome:Hide(); return end

    chrome:SetSize(w, h)
    chrome:ClearAllPoints()
    chrome:SetPoint("CENTER", icon, "CENTER", 0, 0)
    chrome:Show()
end

ApplyChrome = function(panel)
    local cm = EnsureCooldownManagerSettings()
    if not cm.enabled then return end
    local sub = cm[panel.key]
    if not sub or not sub.enabled then return end

    local viewer = GetBlizzardViewer(panel)
    if not viewer or type(viewer.GetItemFrames) ~= "function" then return end

    local items
    local ok = pcall(function() items = viewer:GetItemFrames() end)
    if not ok or type(items) ~= "table" then return end

    if not panelChromes[panel.key] then panelChromes[panel.key] = {} end
    local map = panelChromes[panel.key]

    local seen = {}
    for _, icon in ipairs(items) do
        if icon then
            local chrome = map[icon]
            if not chrome then
                chrome = CreateChromeFrame(panel, icon)
                if chrome then map[icon] = chrome end
            end
            if chrome then
                seen[chrome] = true
                if sub.showBorder == false then
                    chrome:Hide()
                else
                    SyncChromeFrame(chrome)
                    if chrome._darkSwipe then
                        chrome._darkSwipe:SetShown(sub.darkSwipe ~= false)
                    end
                end
            end
        end
    end

    for _, chrome in pairs(map) do
        if not seen[chrome] then chrome:Hide() end
    end
end


-- ============================================================
-- SECTION: Visibility + OOC Fade + Smooth Alpha
-- ResolveVisibilityAlpha: converts sub.visibility mode ("Always",
--   "In Combat", "In Dungeons & Raids", "Hidden") to a target alpha.
--   Also applies oocFadeEnabled dimming when not in combat.
-- SetPanelAlpha: sets alpha on both the viewer and all its chrome frames.
-- activeFades: per-panel fade animation state. Cleared and replaced
--   on each visibility change.
-- StartPanelFade: begins a smooth fade to targetAlpha. If already at
--   target, sets immediately.
-- ManagerEvent OnUpdate: cubic Hermite easing for active fades.
--   Runs every frame (skips when activeFades is empty).
-- ApplyPanelVisibility: immediate alpha set (no fade).
-- ApplyPanelVisibilityFaded: smooth fade to target.
-- READS:  sub.visibility, sub.alpha, sub.oocFadeEnabled, _muiInCombat,
--         IsInInstance, GetTime, viewer.GetAlpha
-- WRITES: activeFades, viewer alpha
-- FUNCTIONS:
--   ResolveVisibilityAlpha(sub) → float 0-1
--   SetPanelAlpha(panel, a)
--   StartPanelFade(panel, targetAlpha)
--   ApplyPanelVisibility(panel)       — immediate
--   ApplyPanelVisibilityFaded(panel)  — smooth
-- ============================================================
local function ResolveVisibilityAlpha(sub)
    local mode = sub and sub.visibility or "Always"
    local userAlpha = ((sub and sub.alpha) or 100) / 100
    if mode == "Hidden" or mode == "hidden" then
        return 0
    elseif mode == "In Combat" or mode == "combat" then
        return _muiInCombat and userAlpha or 0
    elseif mode == "In Dungeons & Raids" or mode == "instance" then
        local inInstance = IsInInstance and IsInInstance()
        return inInstance and userAlpha or 0
    end
    if sub and sub.oocFadeEnabled and not _muiInCombat then
        local fadeAmount = (sub.oocFadeAlpha or 60) / 100
        if fadeAmount > 1 then fadeAmount = 1 end
        if fadeAmount < 0 then fadeAmount = 0 end
        return userAlpha * (1 - fadeAmount)
    end
    return userAlpha
end

local function SetPanelAlpha(panel, a)
    local viewer = GetBlizzardViewer(panel)
    if viewer then pcall(viewer.SetAlpha, viewer, a) end
    local map = panelChromes[panel.key]
    if map then
        for _, chrome in pairs(map) do
            if chrome and chrome.SetAlpha then
                chrome:SetAlpha(a)
            end
        end
    end
end

local activeFades = {}
local FADE_DURATION = 0.35

local function GetCurrentPanelAlpha(panel)
    local viewer = GetBlizzardViewer(panel)
    if viewer and viewer.GetAlpha then
        local ok, val = pcall(viewer.GetAlpha, viewer)
        if ok and type(val) == "number" then return val end
    end
    return 1
end

local function StartPanelFade(panel, targetAlpha)
    local from = GetCurrentPanelAlpha(panel)
    if math.abs(from - targetAlpha) < 0.01 then
        SetPanelAlpha(panel, targetAlpha)
        activeFades[panel.key] = nil
        return
    end
    activeFades[panel.key] = {
        panel    = panel,
        from     = from,
        to       = targetAlpha,
        start    = GetTime(),
        duration = FADE_DURATION,
    }
end

ManagerEvent:HookScript("OnUpdate", function()
    if not next(activeFades) then return end
    local now = GetTime()
    for key, fade in pairs(activeFades) do
        local elapsed = now - fade.start
        local t = elapsed / fade.duration
        if t >= 1 then
            SetPanelAlpha(fade.panel, fade.to)
            activeFades[key] = nil
        else
            local eased = t * t * (3 - 2 * t)
            local alpha = fade.from + (fade.to - fade.from) * eased
            SetPanelAlpha(fade.panel, alpha)
        end
    end
end)

local function ApplyPanelVisibility(panel)
    local viewer = GetBlizzardViewer(panel)
    if not viewer then return end
    local sub = GetPanelSettings(panel)
    if not sub then return end
    local a = ResolveVisibilityAlpha(sub)
    activeFades[panel.key] = nil
    SetPanelAlpha(panel, a)
end

local function ApplyPanelVisibilityFaded(panel)
    local viewer = GetBlizzardViewer(panel)
    if not viewer then return end
    local sub = GetPanelSettings(panel)
    if not sub then return end
    local target = ResolveVisibilityAlpha(sub)
    StartPanelFade(panel, target)
end


-- ============================================================
-- SECTION: Overlay Drag + Lock State + Reset API
-- SavePanelPosition: snapshots anchor.GetPoint into sub.position
--   and calls MR.SavePosition.
-- EnsurePanelOverlay: lazy-creates the HIGH-strata drag overlay
--   per anchor. Styled via MidnightUI_StyleOverlay. OnDragStart/
--   DragStop delegate to anchor.StartMoving/StopMovingOrSizing
--   and call SavePanelPosition + ApplyPanelPosition.
-- ApplyPanelLockState: shows/hides the drag overlay based on
--   locked flag (Messenger.locked). Disables anchor mouse when
--   locked or when panel/manager is disabled.
-- _G.MidnightUI_CDM_ResetPositions: exported reset function.
--   Clears sub.position and movers[moverKey] for all panels, then
--   calls ApplyCooldownManagerSettings.
-- READS:  MidnightUISettings.Messenger.locked, anchor.GetPoint
-- WRITES: panelOverlays (lazy), sub.position,
--         MidnightUISettings.movers[moverKey]
-- FUNCTIONS:
--   SavePanelPosition(panel)
--   EnsurePanelOverlay(panel) → overlay frame
--   ApplyPanelLockState(panel, locked)
--   _G.MidnightUI_CDM_ResetPositions()
-- ============================================================
local panelOverlays = {}

local function SavePanelPosition(panel)
    local anchor = GetAnchor(panel)
    if not anchor then return end
    local point, _, relativePoint, xOfs, yOfs = anchor:GetPoint()
    if not point or not relativePoint or not xOfs or not yOfs then return end
    local sub = GetPanelSettings(panel)
    if sub then
        sub.position = { point, relativePoint, xOfs, yOfs }
    end
    local MR = _G.MidnightUI_MoverRegistry
    if MR and MR.SavePosition then
        MR.SavePosition(panel.moverKey)
    end
end

local function EnsurePanelOverlay(panel)
    local existing = panelOverlays[panel.key]
    if existing then return existing end
    local anchor = GetAnchor(panel)
    if not anchor then return nil end

    local overlay = CreateFrame("Frame", nil, anchor, "BackdropTemplate")
    overlay:SetAllPoints(anchor)
    overlay:SetFrameStrata("HIGH")
    overlay:SetFrameLevel((anchor:GetFrameLevel() or 0) + 10)
    overlay:EnableMouse(true)
    overlay:RegisterForDrag("LeftButton")
    overlay:Hide()

    if _G.MidnightUI_StyleOverlay then
        _G.MidnightUI_StyleOverlay(overlay, panel.label, nil, "cast")
    end

    overlay:SetScript("OnDragStart", function()
        anchor:StartMoving()
        if _G.MidnightUI_BeginFrameSnap then _G.MidnightUI_BeginFrameSnap(anchor) end
    end)
    overlay:SetScript("OnDragStop", function()
        anchor:StopMovingOrSizing()
        if _G.MidnightUI_EndFrameSnap then _G.MidnightUI_EndFrameSnap(anchor) end
        SavePanelPosition(panel)
        ApplyPanelPosition(panel)
    end)

    if _G.MidnightUI_AttachOverlaySettings then
        _G.MidnightUI_AttachOverlaySettings(overlay, panel.moverKey)
    end

    anchor.dragOverlay = overlay
    panelOverlays[panel.key] = overlay
    return overlay
end

local function ApplyPanelLockState(panel, locked)
    local cm = EnsureCooldownManagerSettings()
    local sub = cm[panel.key]
    local anchor = GetAnchor(panel)
    if not anchor then
        CMLog("LockState " .. panel.key .. " — no anchor")
        return
    end

    if not cm.enabled or not sub.enabled then
        if anchor.dragOverlay then anchor.dragOverlay:Hide() end
        anchor:EnableMouse(false)
        return
    end

    if locked then
        if anchor.dragOverlay then anchor.dragOverlay:Hide() end
        anchor:EnableMouse(false)
    else
        EnsurePanelOverlay(panel)
        local ov = anchor.dragOverlay
        local aw, ah = anchor:GetWidth(), anchor:GetHeight()
        if aw < 180 or ah < 50 then
            anchor:SetSize(math.max(180, aw), math.max(50, ah))
        end
        if ov then ov:Show() end
        anchor:EnableMouse(true)
    end
end


function _G.MidnightUI_CDM_ResetPositions()
    local cm = EnsureCooldownManagerSettings()
    for _, panel in ipairs(PANELS) do
        if cm[panel.key] then
            cm[panel.key].position = nil
            cm[panel.key].offsetX  = 0
            cm[panel.key].offsetY  = 0
        end
        if _G.MidnightUISettings and _G.MidnightUISettings.movers then
            _G.MidnightUISettings.movers[panel.moverKey] = nil
        end
    end
    if _G.MidnightUI_ApplyCooldownManagerSettings then
        _G.MidnightUI_ApplyCooldownManagerSettings()
    end
    print("|cff00ffffMidnightUI:|r Cooldown Manager positions reset to defaults. /reload to lock the changes.")
end


-- ============================================================
-- SECTION: Panel Application + Overlay Settings + MoverRegistry
-- ApplySavedPositionToAnchor: restores anchor position from
--   sub.position array or from MoverRegistry StringToPoint fallback.
-- ApplyAllPanels: master apply — reads lock state, then for each
--   panel: SetScale, SetAlpha, ApplySavedPositionToAnchor,
--   ApplyPanelPosition, ApplyChrome, ApplyPanelVisibility,
--   ApplyPanelLockState.
-- _G.MidnightUI_ApplyCooldownManagerSettings: public alias for
--   ApplyAllPanels. Called externally on settings change.
-- RefreshOverlayByKey: looks up panel by moverKey and calls
--   ApplyAllPanels. Triggered by overlay settings slider/toggle.
-- BuildPanelOverlaySettings: builds the per-panel settings UI
--   (enabled toggle, scale/alpha/offset sliders, visibility dropdown,
--   OOC fade controls) using MidnightUI_CreateOverlayBuilder.
-- MoverRegistry block: DefineMover for each panel's anchorGlobal
--   frame with onLock/onUnlock/onPositionChanged callbacks.
-- READS:  sub.position, MidnightUI_MoverRegistry, cm settings
-- WRITES: anchor frame positions, panelOverlays visibility
-- FUNCTIONS:
--   ApplySavedPositionToAnchor(panel)
--   ApplyAllPanels()
--   _G.MidnightUI_ApplyCooldownManagerSettings()
--   RefreshOverlayByKey(moverKey)
--   BuildPanelOverlaySettings(content, key) → height number
-- ============================================================
local function ApplySavedPositionToAnchor(panel)
    local anchor = GetAnchor(panel)
    if not anchor then return end
    local sub = GetPanelSettings(panel)
    local pos = sub and sub.position
    if pos and #pos >= 4 then
        anchor:ClearAllPoints()
        anchor:SetPoint(pos[1], UIParent, pos[2], pos[3], pos[4])
    else
        local MR = _G.MidnightUI_MoverRegistry
        if MR and MR.StringToPoint then
            local point, anchorName, relPoint, x, y = MR.StringToPoint(panel.defaultPos)
            if point then
                anchor:ClearAllPoints()
                anchor:SetPoint(point, _G[anchorName] or UIParent, relPoint, x, y)
            end
        end
    end
end


local function ApplyAllPanels()
    local cm = EnsureCooldownManagerSettings()
    local locked = true
    if _G.MidnightUISettings and _G.MidnightUISettings.Messenger
       and _G.MidnightUISettings.Messenger.locked ~= nil then
        locked = _G.MidnightUISettings.Messenger.locked
    end

    HookAllViewers()

    for _, panel in ipairs(PANELS) do
        local anchor = GetAnchor(panel)
        if anchor then
            local sub = cm[panel.key]
            anchor:SetScale((sub.scale or 100) / 100)
            anchor:SetAlpha((sub.alpha or 100) / 100)
            ApplySavedPositionToAnchor(panel)
            ApplyPanelPosition(panel)
            ApplyChrome(panel)
            ApplyPanelVisibility(panel)
            ApplyPanelLockState(panel, locked)
        end
    end
end

function _G.MidnightUI_ApplyCooldownManagerSettings()
    ApplyAllPanels()
end


local function RefreshOverlayByKey(moverKey)
    local panel = PANEL_BY_MOVERKEY[moverKey]
    if not panel then return end
    ApplyAllPanels()
end

local function BuildPanelOverlaySettings(content, key)
    local panel = PANEL_BY_MOVERKEY[key]
    if not panel then return 0 end
    if not _G.MidnightUI_CreateOverlayBuilder then return 0 end
    local cm = EnsureCooldownManagerSettings()
    local sub = cm[panel.key]
    if not sub then return 0 end

    local b = _G.MidnightUI_CreateOverlayBuilder(content, { startY = -6 })
    b:Header(panel.label)

    b:Checkbox("Show This Bar", sub.enabled ~= false, function(v)
        sub.enabled = v
        RefreshOverlayByKey(key)
    end)

    b:Slider("Size", 50, 200, 5, sub.scale or 100, function(v)
        sub.scale = math_floor(v)
        RefreshOverlayByKey(key)
    end)

    b:Slider("Transparency", 0, 100, 5, sub.alpha or 100, function(v)
        sub.alpha = math_floor(v)
        RefreshOverlayByKey(key)
    end)

    b:Header("Nudge Into Place")
    b:Note("Drag the bar to roughly position it, then nudge it pixel-by-pixel here.")
    b:Slider("Move Left / Right", -200, 200, 1, sub.offsetX or 0, function(v)
        sub.offsetX = math_floor(v)
        RefreshOverlayByKey(key)
    end)
    b:Slider("Move Up / Down", -200, 200, 1, sub.offsetY or 0, function(v)
        sub.offsetY = math_floor(v)
        RefreshOverlayByKey(key)
    end)

    b:Header("Look and Feel")
    b:Note("MidnightUI styling drawn on top of the game's icons.")
    b:Checkbox("Show Dark Border Around Icons", sub.showBorder ~= false, function(v)
        sub.showBorder = v
        RefreshOverlayByKey(key)
    end)
    b:Checkbox("Darken Icons", sub.darkSwipe == true, function(v)
        sub.darkSwipe = v
        RefreshOverlayByKey(key)
    end)

    b:Header("When to Show This Bar")
    b:Dropdown("Show This Bar", { "Always", "In Combat", "In Dungeons & Raids", "Hidden" }, sub.visibility or "Always", function(v)
        sub.visibility = v
        RefreshOverlayByKey(key)
    end)

    b:Header("Fade When Out of Combat")
    b:Checkbox("Fade This Bar When Not Fighting", sub.oocFadeEnabled == true, function(v)
        sub.oocFadeEnabled = v
        RefreshOverlayByKey(key)
    end)
    b:Slider("How Faded (Higher = More See-Through)", 0, 100, 5, sub.oocFadeAlpha or 60, function(v)
        sub.oocFadeAlpha = math_floor(v)
        RefreshOverlayByKey(key)
    end)

    b:Note("Coming soon: glow when a spell procs, glow when a DoT is about to fall off, and a flash when a big cooldown comes back up.")

    return b:Height()
end

if _G.MidnightUI_RegisterOverlaySettings then
    for _, panel in ipairs(PANELS) do
        _G.MidnightUI_RegisterOverlaySettings(panel.moverKey, {
            title = panel.label,
            build = BuildPanelOverlaySettings,
        })
    end
end


do
    local MR = _G.MidnightUI_MoverRegistry
    if MR and MR.DefineMover then
        for _, panel in ipairs(PANELS) do
            local capturedPanel = panel
            MR.DefineMover(panel.moverKey, {
                resolveFrame    = function() return GetAnchor(capturedPanel) end,
                defaultPosition = panel.defaultPos,
                category        = "cast",
                label           = panel.label,
                externalOverlay = true,
                onLock          = function() ApplyPanelLockState(capturedPanel, true)  end,
                onUnlock        = function() ApplyPanelLockState(capturedPanel, false) end,
                onPositionChanged = function() ApplyPanelPosition(capturedPanel) end,
            })
        end
    end
end


-- ============================================================
-- SECTION: Event Handler
-- ADDON_LOADED  — initialize settings table.
-- PLAYER_LOGIN  — init settings, set _muiInCombat, hook viewers,
--   start 5s safety-net ticker (0.1s × 50 ticks) to handle delayed
--   viewer frame initialization.
-- PLAYER_ENTERING_WORLD — same as LOGIN (handles /reload).
-- EDIT_MODE_LAYOUTS_UPDATED — re-apply all panels when Blizzard
--   edit mode changes viewer layout.
-- PLAYER_REGEN_ENABLED  — set _muiInCombat=false, FlushPendingOps,
--   fade panels to OOC alpha.
-- PLAYER_REGEN_DISABLED — set _muiInCombat=true, fade panels to
--   combat alpha.
-- ZONE_CHANGED_NEW_AREA — ApplyPanelVisibility (immediate) for
--   instance-mode panels.
-- READS:  _muiInCombat, InCombatLockdown
-- WRITES: _muiInCombat
-- FUNCTIONS: (inline OnEvent handler)
-- ============================================================
ManagerEvent:RegisterEvent("ADDON_LOADED")
ManagerEvent:RegisterEvent("PLAYER_LOGIN")
ManagerEvent:RegisterEvent("PLAYER_ENTERING_WORLD")
ManagerEvent:RegisterEvent("PLAYER_REGEN_ENABLED")
ManagerEvent:RegisterEvent("PLAYER_REGEN_DISABLED")
ManagerEvent:RegisterEvent("ZONE_CHANGED_NEW_AREA")
ManagerEvent:RegisterEvent("EDIT_MODE_LAYOUTS_UPDATED")

ManagerEvent:SetScript("OnEvent", function(self, event, arg1)
    if event == "ADDON_LOADED" then
        if arg1 ~= ADDON_NAME then return end
        EnsureCooldownManagerSettings()
        return
    end

    local function StartLoginSafetyNet()
        if not C_Timer or not C_Timer.NewTicker then return end
        local count = 0
        local ticker
        ticker = C_Timer.NewTicker(0.1, function()
            count = count + 1
            if _G.MidnightUI_ApplyCooldownManagerSettings then
                _G.MidnightUI_ApplyCooldownManagerSettings()
            end
            if count >= 50 and ticker and ticker.Cancel then
                ticker:Cancel()
            end
        end)
    end

    if event == "PLAYER_LOGIN" then
        EnsureCooldownManagerSettings()
        _muiInCombat = InCombatLockdown and InCombatLockdown() or false
        HookAllViewers()
        StartLoginSafetyNet()
        return
    end

    if event == "PLAYER_ENTERING_WORLD" then
        _muiInCombat = InCombatLockdown and InCombatLockdown() or false
        HookAllViewers()
        StartLoginSafetyNet()
        return
    end

    if event == "EDIT_MODE_LAYOUTS_UPDATED" then
        if _G.MidnightUI_ApplyCooldownManagerSettings then
            _G.MidnightUI_ApplyCooldownManagerSettings()
        end
        return
    end

    if event == "PLAYER_REGEN_ENABLED" then
        _muiInCombat = false
        FlushPendingOps()
        for _, panel in ipairs(PANELS) do
            ApplyPanelVisibilityFaded(panel)
        end
        return
    end

    if event == "PLAYER_REGEN_DISABLED" then
        _muiInCombat = true
        for _, panel in ipairs(PANELS) do
            ApplyPanelVisibilityFaded(panel)
        end
        return
    end

    if event == "ZONE_CHANGED_NEW_AREA" then
        for _, panel in ipairs(PANELS) do
            ApplyPanelVisibility(panel)
        end
        return
    end
end)
