---@type Orbit
local Orbit = Orbit
local OrbitEngine = Orbit.Engine
local Constants = Orbit.Constants
local CooldownUtils = OrbitEngine.CooldownUtils

local CDM = Orbit:GetPlugin("Orbit_CooldownViewer")
if not CDM then return end

local VIEWER_MAP = CDM.viewerMap
local BUFFICON_INDEX = Constants.Cooldown.SystemIndex.BuffIcon
local INACTIVE_ALPHA_DEFAULT = 60
local FLASH_DURATION = 0.15

-- Reusable child buffer alias
local PackChildren = function(...) return CooldownUtils:PackChildren(...) end
local _activeChildBuf = {}

local DESAT_CURVE = C_CurveUtil.CreateCurve()
DESAT_CURVE:AddPoint(0.0, 0.0)
DESAT_CURVE:AddPoint(0.001, 0.0)
DESAT_CURVE:AddPoint(1.0, 0.0)

-- Forward declarations for functions used in ProcessChildren but defined later
local GetNativeTimerCurveForSystem
local ApplyTimerColor
local ApplyBuffIconDesaturation

-- [ FLASH OVERLAY ]----------------------------------------------------------------------------------
local function EnsureFlashOverlay(icon)
    if icon.orbitCDMFlash then return end
    local flash = CreateFrame("Frame", nil, icon)
    flash:SetAllPoints(icon)
    flash:SetFrameLevel(icon:GetFrameLevel() + 3)
    flash:Hide()

    local tex = flash:CreateTexture(nil, "OVERLAY", nil, 7)
    tex:SetAllPoints(flash)
    tex:SetColorTexture(1, 1, 1, 0.4)

    local fadeGroup = flash:CreateAnimationGroup()
    fadeGroup:SetToFinalAlpha(true)
    local fadeOut = fadeGroup:CreateAnimation("Alpha")
    fadeOut:SetFromAlpha(1)
    fadeOut:SetToAlpha(0)
    fadeOut:SetDuration(FLASH_DURATION)
    fadeOut:SetSmoothing("OUT")
    fadeGroup:SetScript("OnFinished", function() flash:Hide() end)

    icon.orbitCDMFlash = flash
    icon.orbitCDMFlashTex = tex
    icon.orbitCDMFlashFade = fadeGroup
end

local function FlashIcon(icon, color)
    if not icon.orbitCDMFlash then return end
    icon.orbitCDMFlashTex:SetColorTexture(color.r, color.g, color.b, color.a or 0.4)
    icon.orbitCDMFlash:SetAlpha(1)
    icon.orbitCDMFlash:Show()
    icon.orbitCDMFlashFade:Play()
end

local function FindIconBySpellID(spellID)
    for _, entry in pairs(VIEWER_MAP) do
        if entry.viewer then
            for _, child in ipairs(PackChildren(entry.viewer:GetChildren())) do
                local cached = child.orbitCachedSpellID
                if child:IsShown() and cached and not issecretvalue(cached) and cached == spellID then
                    return child
                end
            end
        end
    end
end

hooksecurefunc("ActionButtonDown", function(id)
    local button = GetActionButtonForID(id)
    if not button or not button.action then return end
    local actionType, spellID = GetActionInfo(button.action)
    if actionType ~= "spell" or not spellID then return end
    local icon = FindIconBySpellID(spellID)
    if not icon or not icon.orbitCDMSystemIndex then return end
    FlashIcon(icon, CDM:GetSetting(icon.orbitCDMSystemIndex, "KeypressColor") or { r = 1, g = 1, b = 1, a = 0 })
end)

-- [ BUFF ICON AURA DURATION ]-----------------------------------------------------------------------
local function GetBuffIconAuraDuration(icon)
    local unit = icon.GetAuraDataUnit and icon:GetAuraDataUnit()
    if not unit then return nil end
    local auraID = icon.auraInstanceID
    if auraID == nil or issecretvalue(auraID) then return nil end
    return C_UnitAuras.GetAuraDuration(unit, auraID)
end

-- [ ANCHOR LOOKUP ]----------------------------------------------------------------------------------
local function GetAnchorInfo(anchorFrame) return anchorFrame and OrbitEngine.FrameAnchor and OrbitEngine.FrameAnchor.anchors[anchorFrame] end

-- [ PROCESS CHILDREN ]-------------------------------------------------------------------------------
function CDM:ProcessChildren(anchor)
    if not anchor then return end
    local entry = VIEWER_MAP[anchor.systemIndex]
    local blizzFrame = entry and entry.viewer
    if not blizzFrame then return end

    local systemIndex = anchor.systemIndex
    wipe(_activeChildBuf)
    local activeChildren = _activeChildBuf
    local alwaysShow = (systemIndex == BUFFICON_INDEX) and self:GetSetting(systemIndex, "AlwaysShow")

    for _, child in ipairs(PackChildren(blizzFrame:GetChildren())) do
        if child.layoutIndex then
            if not child.orbitOnShowHooked then
                local plugin = self
                child:HookScript("OnShow", function(c)
                    local parent = c:GetParent()
                    local anc = parent and parent:GetParent()
                    if Orbit.Skin.Icons.frameSettings then
                        local s = Orbit.Skin.Icons.frameSettings[parent]
                        if s then
                            Orbit.Skin.Icons:ApplyCustom(c, s)
                        end
                    end
                    if anc and plugin.ProcessChildren then
                        plugin:ProcessChildren(anc)
                    end
                end)
                child.orbitOnShowHooked = true
            end

            if not child.orbitRefreshHooked and child.RefreshData then
                local a = anchor
                hooksecurefunc(child, "RefreshData", function()
                    Orbit.Async:Debounce("CDM_Refresh_" .. systemIndex, function()
                        CDM:ProcessChildren(a)
                    end, Constants.Timing.KeyboardRestoreDelay)
                end)
                child.orbitRefreshHooked = true
            end

            if alwaysShow then
                self:HookAlwaysShow(child)
                if child:GetCooldownID() then
                    child:Show()
                    table.insert(activeChildren, child)
                end
            elseif child:IsShown() then
                table.insert(activeChildren, child)
            end
        end
    end

    table.sort(activeChildren, function(a, b) return (a.layoutIndex or 0) < (b.layoutIndex or 0) end)

    if #activeChildren > 0 then
        local hGrowth = (systemIndex == BUFFICON_INDEX) and self:GetHorizontalGrowth(anchor) or nil
        local vGrowth = self:GetGrowthDirection(anchor)
        local parentIndex = CooldownUtils:GetInheritedParentIndex(anchor, VIEWER_MAP)
        local overrides = parentIndex
                and {
                    aspectRatio = self:GetSetting(parentIndex, "aspectRatio"),
                    size = self:GetSetting(parentIndex, "IconSize"),
                    padding = self:GetSetting(parentIndex, "IconPadding"),
                }
            or nil
        local skinSettings =
            CooldownUtils:BuildSkinSettings(self, systemIndex, { verticalGrowth = vGrowth, horizontalGrowth = hGrowth, inheritOverrides = overrides })

        if not Orbit.Skin.Icons.frameSettings then
            Orbit.Skin.Icons.frameSettings = setmetatable({}, { __mode = "k" })
        end
        Orbit.Skin.Icons.frameSettings[blizzFrame] = skinSettings

        local curve = (systemIndex == BUFFICON_INDEX) and GetNativeTimerCurveForSystem(BUFFICON_INDEX) or nil
        local alwaysShow = self:GetSetting(systemIndex, "AlwaysShow")
        local inactiveAlpha = alwaysShow and (self:GetSetting(systemIndex, "InactiveAlpha") or INACTIVE_ALPHA_DEFAULT) / 100 or nil
        local hideBorders = alwaysShow and self:GetSetting(systemIndex, "HideBorders")
        for _, icon in ipairs(activeChildren) do
            Orbit.Skin.Icons:ApplyCustom(icon, skinSettings)
            self:HookGCDSwipe(icon, systemIndex)
            self:ApplyTextSettings(icon, systemIndex)
            icon.orbitCDMSystemIndex = systemIndex
            if not InCombatLockdown() and icon.GetSpellID then
                local sid = icon:GetSpellID()
                if sid and not issecretvalue(sid) then
                    icon.orbitCachedSpellID = sid
                end
            end
            EnsureFlashOverlay(icon)
            if curve then ApplyTimerColor(icon, curve) end
            if inactiveAlpha then ApplyBuffIconDesaturation(icon, inactiveAlpha, hideBorders) end
        end

        Orbit.Skin.IconLayout:ApplyManualLayout(blizzFrame, activeChildren, skinSettings)

        if not InCombatLockdown() then
            local w, h = blizzFrame:GetSize()
            if w and h and w > 0 and h > 0 then
                anchor:SetSize(w, h)
            end
            anchor.orbitRowHeight = blizzFrame.orbitRowHeight
            anchor.orbitColumnWidth = blizzFrame.orbitColumnWidth
        end
    end
end

-- [ GCD SWIPE HOOK ]--------------------------------------------------------------------------------
function CDM:HookGCDSwipe(icon, systemIndex)
    if icon.orbitGCDHooked or not icon.RefreshSpellCooldownInfo then return end
    icon.orbitSystemIndex = systemIndex
    icon.orbitPlugin = self

    hooksecurefunc(icon, "RefreshSpellCooldownInfo", function(self)
        local plugin, sysIdx = self.orbitPlugin, self.orbitSystemIndex
        if not plugin or not sysIdx then return end
        if plugin:GetSetting(sysIdx, "ShowGCDSwipe") then return end
        if self.isOnGCD and not self.wasSetFromAura then
            local cooldown = self:GetCooldownFrame()
            if cooldown then cooldown:SetDrawSwipe(false) end
        end
    end)
    icon.orbitGCDHooked = true
end

-- [ ALWAYS SHOW (BUFF ICONS) ]----------------------------------------------------------------------
function CDM:HookAlwaysShow(icon)
    if icon.orbitAlwaysShowHooked then return end
    local plugin = self
    local function onStateChange(self)
        if not plugin:GetSetting(BUFFICON_INDEX, "AlwaysShow") then
            if self.Icon then
                self.Icon:SetDesaturation(0)
                self.Icon:SetAlpha(1)
            end
            return
        end
        if not self:GetCooldownID() then
            return
        end
        self:Show()
    end
    hooksecurefunc(icon, "UpdateShownState", onStateChange)
    hooksecurefunc(icon, "RefreshData", onStateChange)
    hooksecurefunc(icon, "RefreshSpellTexture", onStateChange)
    icon.orbitAlwaysShowHooked = true
end

-- [ TIMER COLOR CURVE ]-----------------------------------------------------------------------------
local SB = OrbitEngine.SchemaBuilder
local curveCache = {}

GetNativeTimerCurveForSystem = function(systemIndex)
    local positions = CDM:GetSetting(systemIndex, "ComponentPositions")
    local timerOverrides = positions and positions["Timer"] and positions["Timer"].overrides
    local curveData = timerOverrides and timerOverrides["CustomColorCurve"]
    if not curveData or not curveData.pins or #curveData.pins == 0 then return nil end
    local cached = curveCache[systemIndex]
    if cached and cached.data == curveData then return cached.curve end
    local curve = OrbitEngine.ColorCurve:ToNativeColorCurve(curveData)
    curveCache[systemIndex] = { data = curveData, curve = curve }
    return curve
end

local function FindFontString(cd)
    if not cd then return nil end
    for _, region in ipairs(PackChildren(cd:GetRegions())) do
        if region:GetObjectType() == "FontString" then return region end
    end
    return nil
end

local function GetTimerFontStrings(icon)
    if not icon.orbitTimerFS then
        icon.orbitTimerFS = FindFontString(icon.Cooldown or (icon.GetCooldownFrame and icon:GetCooldownFrame()))
    end
    if not icon.orbitActiveFS then
        icon.orbitActiveFS = FindFontString(icon.ActiveCooldown)
    end
    return icon.orbitTimerFS, icon.orbitActiveFS
end

local function EnsureTimerColorHook(fs)
    if fs._orbitColorHooked then return end
    fs._orbitColorHooked = true
    hooksecurefunc(fs, "SetTextColor", function(self)
        if self._orbitSettingColor then return end
        local c = self._orbitCurveColor
        if not c then return end
        self._orbitSettingColor = true
        self:SetTextColor(c[1], c[2], c[3], c[4])
        self._orbitSettingColor = false
    end)
end

local function SetCachedColor(fs, r, g, b, a)
    EnsureTimerColorHook(fs)
    local c = fs._orbitCurveColor
    if not c then c = { 0, 0, 0, 1 }; fs._orbitCurveColor = c end
    c[1], c[2], c[3], c[4] = r, g, b, a
    fs._orbitSettingColor = true
    fs:SetTextColor(r, g, b, a)
    fs._orbitSettingColor = false
end

ApplyTimerColor = function(icon, curve)
    local timerFS, activeFS = GetTimerFontStrings(icon)
    if not timerFS and not activeFS then return end
    local durObj = GetBuffIconAuraDuration(icon)
    if not durObj then
        if timerFS then timerFS._orbitCurveColor = nil end
        if activeFS then activeFS._orbitCurveColor = nil end
        return
    end
    local color = durObj:EvaluateRemainingPercent(curve)
    if not color then return end
    local r, g, b, a = color:GetRGBA()
    if timerFS then SetCachedColor(timerFS, r, g, b, a) end
    if activeFS then SetCachedColor(activeFS, r, g, b, a) end
end

-- [ BUFF ICON DESATURATION ]------------------------------------------------------------------------
ApplyBuffIconDesaturation = function(icon, inactiveAlpha, hideBorders)
    if not icon.Icon then return end
    local durObj = GetBuffIconAuraDuration(icon)
    local borderFrame = hideBorders and Orbit.Skin.Icons.borderCache and Orbit.Skin.Icons.borderCache[icon]
    if durObj then
        icon.Icon:SetDesaturation(durObj:EvaluateRemainingPercent(DESAT_CURVE))
        icon.Icon:SetAlpha(1)
        if borderFrame then borderFrame:SetAlpha(1) end
    else
        icon.Icon:SetDesaturation(1)
        icon.Icon:SetAlpha(inactiveAlpha)
        if borderFrame then borderFrame:SetAlpha(0) end
    end
end


-- [ GROWTH DIRECTION ]------------------------------------------------------------------------------
function CDM:GetGrowthDirection(anchorFrame) local info = GetAnchorInfo(anchorFrame); return info and info.edge == "TOP" and "UP" or "DOWN" end

function CDM:GetHorizontalGrowth(anchorFrame)
    local info = GetAnchorInfo(anchorFrame)
    if not info then return "CENTER" end
    if info.edge == "LEFT" or info.align == "LEFT" then return "RIGHT" end
    if info.edge == "RIGHT" or info.align == "RIGHT" then return "LEFT" end
    return "CENTER"
end

-- [ ASSISTED COMBAT HIGHLIGHT ]---------------------------------------------------------------------
do
    local FLIPBOOK_SCALE = 1.4
    local HIGHLIGHT_PADDING = 6
    local highlightedIcons = {}

    local function GetIconSpellID(icon)
        if icon.orbitCachedSpellID then return icon.orbitCachedSpellID end
        if icon.trackedType == "spell" then return icon.trackedId end
        if icon.GetSpellID then
            local sid = icon:GetSpellID()
            if sid and not issecretvalue(sid) then
                return sid
            end
        end
        return nil
    end

    local function SetHighlightShown(icon, shown)
        local frame = icon.AssistedCombatHighlightFrame
        if shown then
            if not frame then
                frame = CreateFrame("Frame", nil, icon, "ActionBarButtonAssistedCombatHighlightTemplate")
                icon.AssistedCombatHighlightFrame = frame
                frame:SetPoint("CENTER")
                frame.Flipbook.Anim:Play()
                frame.Flipbook.Anim:Stop()
            end
            local w, h = icon:GetSize()
            local pw, ph = w + HIGHLIGHT_PADDING, h + HIGHLIGHT_PADDING
            frame:SetSize(pw, ph)
            frame.Flipbook:SetSize(pw * FLIPBOOK_SCALE, ph * FLIPBOOK_SCALE)
            frame:SetFrameLevel(icon:GetFrameLevel() + 10)
            frame:Show()
            if UnitAffectingCombat("player") then
                frame.Flipbook.Anim:Play()
            else
                frame.Flipbook.Anim:Stop()
            end
        elseif frame then
            frame:Hide()
        end
    end

    local function ClearAll()
        for icon in pairs(highlightedIcons) do SetHighlightShown(icon, false) end
        wipe(highlightedIcons)
    end

    local function IsEnabledForSystem(systemIndex) return CDM:GetSetting(systemIndex, "AssistedHighlight") ~= false end

    local function UpdateHighlights()
        if not AssistedCombatManager or not AssistedCombatManager:IsAssistedHighlightActive() then
            ClearAll()
            return
        end
        local nextSpell = AssistedCombatManager.lastNextCastSpellID
        ClearAll()
        if not nextSpell then return end

        for systemIndex, entry in pairs(VIEWER_MAP) do
            if IsEnabledForSystem(systemIndex) then
                if entry.viewer then
                    for _, child in ipairs(PackChildren(entry.viewer:GetChildren())) do
                        if child:IsShown() and GetIconSpellID(child) == nextSpell then
                            SetHighlightShown(child, true)
                            highlightedIcons[child] = true
                        end
                    end
                end
                if entry.anchor and entry.anchor.activeIcons then
                    for _, icon in pairs(entry.anchor.activeIcons) do
                        if icon:IsShown() and GetIconSpellID(icon) == nextSpell then
                            SetHighlightShown(icon, true)
                            highlightedIcons[icon] = true
                        end
                    end
                end
            end
        end

        for _, childData in pairs(CDM.activeChildren or {}) do
            if childData.frame and childData.frame.activeIcons then
                local csi = childData.frame.systemIndex
                if not csi or IsEnabledForSystem(csi) then
                    for _, icon in pairs(childData.frame.activeIcons) do
                        if icon:IsShown() and GetIconSpellID(icon) == nextSpell then
                            SetHighlightShown(icon, true)
                            highlightedIcons[icon] = true
                        end
                    end
                end
            end
        end
    end

    local function SyncFromCVar()
        local enabled = AssistedCombatManager and AssistedCombatManager:IsAssistedHighlightActive() or false
        for systemIndex in pairs(VIEWER_MAP) do
            CDM:SetSetting(systemIndex, "AssistedHighlight", enabled)
        end
        UpdateHighlights()
    end

    CDM.UpdateAssistedHighlights = UpdateHighlights

    EventRegistry:RegisterCallback("AssistedCombatManager.OnAssistedHighlightSpellChange", UpdateHighlights, CDM)
    EventRegistry:RegisterCallback("AssistedCombatManager.OnSetUseAssistedHighlight", SyncFromCVar, CDM)
    EventRegistry:RegisterFrameEventAndCallback("PLAYER_REGEN_ENABLED", UpdateHighlights, "OrbitCDM_AssistedRegen")
    EventRegistry:RegisterFrameEventAndCallback("PLAYER_REGEN_DISABLED", UpdateHighlights, "OrbitCDM_AssistedRegen")
end
