local TXUI, F, E, I, V, P, G = unpack(select(2, ...))
local GR = TXUI:NewModule("ThemesGradients", "AceHook-3.0")

-- Globals
local _G = _G
local abs = math.abs
local ALTERNATE_POWER_INDEX = _G.Enum.PowerType.Alternate or 10
local gsub = string.gsub
local pairs = pairs
local select = select
local UnitCanAttack = UnitCanAttack
local UnitClass = UnitClass
local UnitIsCharmed = UnitIsCharmed
local UnitIsConnected = UnitIsConnected
local UnitIsDeadOrGhost = UnitIsDeadOrGhost
local UnitIsEnemy = UnitIsEnemy
local UnitIsPlayer = UnitIsPlayer
local UnitIsTapDenied = UnitIsTapDenied
local UnitPlayerControlled = UnitPlayerControlled
local UnitPowerType = UnitPowerType
local UnitReaction = UnitReaction
local UnitThreatSituation = UnitThreatSituation
local UnitTreatAsPlayerForDisplay = UnitTreatAsPlayerForDisplay

-- Vars
local UF

function GR:GetUnitFrameType(frame)
    if frame then
        if frame.unitframeType then
            return frame.unitframeType
        else
            local parent = frame:GetParent()
            if parent and parent.unitframeType then return parent.unitframeType end
        end
    end
end

function GR:CalculateBackupColor(r, g, b)
    return F.CalculateMultiplierColor(I.GradientMode.BackupMultiplier, r, g, b)
end

function GR:CalculateCachedFading(cache, useCache, fadeDirection, percent, nR, nG, nB, sR, sG, sB, cnR, cnG, cnB, csR, csG, csB)
    -- Vars
    local frR, frG, frB, bnR, bnG, bnB, bsR, bsG, bsB, bfrR, bfrG, bfrB
    local rnfR, rnfG, rnfB, rsfR, rsfG, rsfB, rnbR, rnbG, rnbB, rsbR, rsbG, rsgB

    -- Fade out
    if not useCache then
        if fadeDirection == I.Enum.GradientMode.Direction.LEFT then
            frR, frG, frB = F.FastColorGradient(percent, sR, sG, sB, nR, nG, nB)
        else
            frR, frG, frB = F.FastColorGradient(percent, nR, nG, nB, sR, sG, sB)
        end

        cache.frR, cache.frG, cache.frB = frR, frG, frB
    else
        frR, frG, frB = cache.frR, cache.frG, cache.frB
    end

    -- Set return values
    if fadeDirection == I.Enum.GradientMode.Direction.LEFT then
        rnfR, rnfG, rnfB, rsfR, rsfG, rsfB = sR, sG, sB, frR, frG, frB
    else
        rnfR, rnfG, rnfB, rsfR, rsfG, rsfB = nR, nG, nB, frR, frG, frB
    end

    -- Calculate our backdrop color
    if not useCache then
        if csB ~= nil then
            bnR, bnG, bnB = cnR, cnG, cnB
            bsR, bsG, bsB = csR, csG, csB
        else
            bnR, bnG, bnB = F.CalculateMultiplierColor(0.35, nR, nG, nB)
            bsR, bsG, bsB = F.CalculateMultiplierColor(0.35, sR, sG, sB)
        end

        if fadeDirection == I.Enum.GradientMode.Direction.LEFT then
            bfrR, bfrG, bfrB = F.FastColorGradient(1 - percent, bnR, bnG, bnB, bsR, bsG, bsB)
        else
            bfrR, bfrG, bfrB = F.FastColorGradient(1 - percent, bsR, bsG, bsB, bnR, bnG, bnB)
        end

        cache.bnR, cache.bnG, cache.bnB = bnR, bnG, bnB
        cache.bsR, cache.bsG, cache.bsB = bsR, bsG, bsB
        cache.bfrR, cache.bfrG, cache.bfrB = bfrR, bfrG, bfrB
    else
        bnR, bnG, bnB = cache.bnR, cache.bnG, cache.bnB
        bsR, bsG, bsB = cache.bsR, cache.bsG, cache.bsB
        bfrR, bfrG, bfrB = cache.bfrR, cache.bfrG, cache.bfrB
    end

    -- Set Gradient
    if fadeDirection == I.Enum.GradientMode.Direction.LEFT then
        rnbR, rnbG, rnbB, rsbR, rsbG, rsgB = bfrR, bfrG, bfrB, bnR, bnG, bnB
    else
        rnbR, rnbG, rnbB, rsbR, rsbG, rsgB = bfrR, bfrG, bfrB, bsR, bsG, bsB
    end

    return rnfR, rnfG, rnfB, rsfR, rsfG, rsfB, rnbR, rnbG, rnbB, rsbR, rsbG, rsgB
end

-- UnitFrame Health Gradient
function GR:PostUpdateHealthColor(health, unit, eR, eG, eB)
    if not self.isEnabled or not self.db or not self.db.enabled then return end

    -- Get Parent frame
    local frame = health:GetParent()

    -- Get type of unit, raid1, raid2, raid3, party, focus etc
    local unitType = self:GetUnitFrameType(frame)
    if not unitType then return self:DebugPrint("UnitFrame Type was empty for frame", unitType) end

    -- Get direction we want
    local fadeMode = I.Enum.GradientMode.Mode.HORIZONTAL
    local fadeDirection = self.leftFrames[unitType] and I.Enum.GradientMode.Direction.LEFT or (self.rightFrames[unitType] and I.Enum.GradientMode.Direction.RIGHT)

    -- Default to top-to-bottom mode when no direction is defined
    if not fadeDirection then
        fadeMode = I.Enum.GradientMode.Mode.VERTICAL
        fadeDirection = I.Enum.GradientMode.Direction.RIGHT
    end

    -- Vars
    local cache = health.colorCache or {}
    local percent = health.cur / health.max
    local percentChange = cache.percent == nil or (abs(cache.percent - percent) > 0.05)
    local elvUIColors = E.db.unitframe.colors
    local calculateBackupFG, colorMap, colorEntry
    local nR, nG, nB, sR, sG, sB, cnR, cnG, cnB, csR, csG, csB

    -- Update cache
    local useCache = cache.nR ~= nil and cache.calculateBackupFG == false and self.updateCache[health]
    if useCache then useCache = eB == nil or (cache.eB and (F.AlmostEqual(cache.eR, eR) and F.AlmostEqual(cache.eG, eG) and F.AlmostEqual(cache.eB, eB))) end
    if percentChange then cache.percent = percent end

    if eB ~= nil then
        cache.eR, cache.eG, cache.eB = eR, eG, eB
    end

    if not useCache then
        -- Get Online status
        local isPlayer = UnitIsPlayer(unit) or (TXUI.IsRetail and UnitTreatAsPlayerForDisplay(unit))
        local playerConnected = UnitIsConnected(unit)
        local playerControlled = UnitPlayerControlled(unit)

        -- Check if we can use the colormap or generate colors ourself
        if isPlayer and (not UnitIsDeadOrGhost(unit)) and playerConnected and (UnitIsCharmed(unit)) and UnitIsEnemy("player", unit) then
            calculateBackupFG = false
            colorMap = "reactionColorMap"
            colorEntry = "BAD"
        elseif health.colorDisconnected and not playerConnected then
            calculateBackupFG = false
            colorMap = "specialColorMap"
            colorEntry = "DISCONNECTED"
        elseif health.colorTapping and not playerControlled and (UnitIsTapDenied(unit)) then
            calculateBackupFG = false
            colorMap = "specialColorMap"
            colorEntry = "TAPPED"
        elseif health.colorThreat and not playerControlled and (UnitThreatSituation("player", unit)) then
            calculateBackupFG = true
        elseif
            ((health.colorClass and isPlayer) or (health.colorClassNPC and not isPlayer))
            or ((health.colorClassPet or health.colorPetByUnitClass) and playerControlled and not isPlayer)
        then
            if health.colorPetByUnitClass then unit = (unit == "pet") and "player" or gsub(unit, "pet", "") end
            calculateBackupFG = false
            colorMap = "classColorMap"
            colorEntry = select(2, UnitClass(unit))
        elseif health.colorReaction and (UnitReaction(unit, "player")) then
            calculateBackupFG = false
            colorMap = "reactionColorMap"
            local reaction = UnitReaction(unit, "player")
            if reaction > 4 then
                colorEntry = "GOOD"
            elseif reaction > 3 then
                colorEntry = "NEUTRAL"
            else
                colorEntry = "BAD"
            end
        else
            calculateBackupFG = true
        end

        -- Caclulate colors
        if not calculateBackupFG then
            -- Get maps for class
            if not colorEntry then
                calculateBackupFG = true
                self:DebugPrint("No color entry, calculating backup", "unit:", unit, "type:", unitType, "class:", UnitClass(unit))
            else
                local fgMap = self.db[colorMap]
                local bgMap = F.GetBackgroundColorMap(colorMap)

                local shiftMap = fgMap[I.Enum.GradientMode.Color.SHIFT][colorEntry]
                local normalMap = fgMap[I.Enum.GradientMode.Color.NORMAL][colorEntry]

                local bgShiftMap = bgMap[I.Enum.GradientMode.Color.SHIFT][colorEntry]
                local bgNormalMap = bgMap[I.Enum.GradientMode.Color.NORMAL][colorEntry]

                -- Sanity check
                if not shiftMap or not normalMap then
                    calculateBackupFG = true
                    self:DebugPrint("No shift or normal health color for", "unit:", unit, "entry:", colorEntry)
                else
                    nR, nG, nB = normalMap.r, normalMap.g, normalMap.b
                    sR, sG, sB = shiftMap.r, shiftMap.g, shiftMap.b

                    cnR, cnG, cnB = bgNormalMap.r, bgNormalMap.g, bgNormalMap.b
                    csR, csG, csB = bgShiftMap.r, bgShiftMap.g, bgShiftMap.b
                end
            end
        end

        -- Get backup color if needed
        if calculateBackupFG then
            if cache.eR ~= nil then
                nR, nG, nB = cache.eR, cache.eR, cache.eB
                self:DebugPrint("Backup cache color used", "unit:", unit, "type:", unitType, "class:", UnitClass(unit), "rgb:", nR, nG, nB)
            else
                nR, nG, nB = elvUIColors.health.r, elvUIColors.health.g, elvUIColors.health.b
                self:DebugPrint("Backup elvui color used", "unit:", unit, "type:", unitType, "class:", UnitClass(unit), "rgb:", nR, nG, nB)
            end

            sR, sG, sB = self:CalculateBackupColor(nR, nG, nB)
        end

        cache.calculateBackupFG = calculateBackupFG
        cache.nR, cache.nG, cache.nB = nR, nG, nB
        cache.sR, cache.sG, cache.sB = sR, sG, sB
        cache.cnR, cache.cnG, cache.cnB = cnR, cnG, cnB
        cache.csR, cache.csG, cache.csB = csR, csG, csB
    else
        nR, nG, nB = cache.nR, cache.nG, cache.nB
        sR, sG, sB = cache.sR, cache.sG, cache.sB
        cnR, cnG, cnB = cache.cnR, cache.cnG, cache.cnB
        csR, csG, csB = cache.csR, cache.csG, cache.csB
    end

    local rnfR, rnfG, rnfB, rsfR, rsfG, rsfB, rnbR, rnbG, rnbB, rsbR, rsbG, rsgB =
        self:CalculateCachedFading(cache, (useCache and not percentChange), fadeDirection, percent, nR, nG, nB, sR, sG, sB, cnR, cnG, cnB, csR, csG, csB)

    health:SetStatusBarColor(1, 1, 1, 1)
    health:GetStatusBarTexture():SetGradient(I.Enum.GradientMode.Mode[fadeMode], rnfR, rnfG, rnfB, rsfR, rsfG, rsfB)

    if health.bg then
        health.bg:SetVertexColor(1, 1, 1, 1)

        if elvUIColors.useDeadBackdrop and (unit and UnitIsDeadOrGhost(unit)) then
            local drR, drG, drB = elvUIColors.health_backdrop_dead.r, elvUIColors.health_backdrop_dead.g, elvUIColors.health_backdrop_dead.b
            drR, drG, drB = F.CalculateMultiplierColor(0.35, drR, drG, drB)
            health.bg:SetGradient(I.Enum.GradientMode.Mode[fadeMode], drR, drG, drB)
        else
            health.bg:SetGradient(I.Enum.GradientMode.Mode[fadeMode], rnbR, rnbG, rnbB, rsbR, rsbG, rsgB)
        end
    end

    self.updateCache[health] = true
    health.colorCache = cache
end

function GR:GetCastbarColor(unit, channeling, canInterrupt, castFailed, duration, maxValue, classColorFallback)
    if not self.isEnabled or not self.db or not self.db.enabled then return end
    if unit == "vehicle" then unit = "player" end

    -- Vars
    local interruptCD
    local canInterruptInTime = false
    local hasInterruptCD = false

    if unit and (self.db.interruptCDEnabled or self.db.interruptSoonEnabled) then
        hasInterruptCD = false

        if canInterrupt and (unit ~= "player") and (UnitCanAttack("player", unit)) then
            interruptCD = F.CanInterrupt()
            if interruptCD > 0 then hasInterruptCD = true end
        end
    end

    if self.db.interruptSoonEnabled and hasInterruptCD and unit then
        interruptCD = interruptCD or F.CanInterrupt()

        if interruptCD > 0 then
            if channeling then
                canInterruptInTime = (interruptCD + 0.3) < duration
            else
                canInterruptInTime = (interruptCD + 0.3) < (maxValue - duration)
            end
        end
    end

    local useClassColor, colorEntry

    -- Determine right color to use
    if castFailed then
        colorEntry = "INTERRUPTED"
    elseif hasInterruptCD and canInterruptInTime then
        colorEntry = "INTERRUPTSOON"
    elseif hasInterruptCD and not canInterruptInTime then
        colorEntry = "INTERRUPTCD"
    elseif not canInterrupt and unit and (UnitIsPlayer(unit) or (unit ~= "player" and UnitCanAttack("player", unit))) then
        colorEntry = "NOINTERRUPT"
    elseif classColorFallback and (unit and UnitIsPlayer(unit)) then
        colorEntry = select(2, UnitClass(unit))
        useClassColor = true
    else
        colorEntry = "DEFAULT"
    end

    -- Backup if no map entry (empty class etc)
    if not colorEntry then colorEntry = "DEFAULT" end

    -- Get colors or fallback if entry dosen't exists
    local colorMap = useClassColor and "classColorMap" or "castColorMap"
    if useClassColor and self.db[colorMap][I.Enum.GradientMode.Color.NORMAL][colorEntry] == nil then
        colorEntry = "DEFAULT"
        colorMap = "castColorMap"
    end

    local fgMap = self.db[colorMap]
    local bgMap = F.GetBackgroundColorMap(colorMap)

    local shiftMap = fgMap[I.Enum.GradientMode.Color.SHIFT][colorEntry]
    local normalMap = fgMap[I.Enum.GradientMode.Color.NORMAL][colorEntry]

    local bgShiftMap = bgMap[I.Enum.GradientMode.Color.SHIFT][colorEntry]
    local bgNormalMap = bgMap[I.Enum.GradientMode.Color.NORMAL][colorEntry]

    -- Return final result
    return normalMap.r, normalMap.g, normalMap.b, shiftMap.r, shiftMap.g, shiftMap.b, bgNormalMap.r, bgNormalMap.g, bgNormalMap.b, bgShiftMap.r, bgShiftMap.g, bgShiftMap.b
end

-- Castbar Gradient
function GR:PostUpdateCastColor(castbar, useCache, castFailed)
    if not self.isEnabled or not self.db or not self.db.enabled then return end

    -- Get Parent frame
    local frame = castbar:GetParent()

    -- Get type of unit, raid, party, focus etc
    local unitType = self:GetUnitFrameType(frame)
    if not unitType then return self:DebugPrint("UnitFrame Type was empty for castbar frame", unitType) end

    -- Vars
    local cache = castbar.colorCache or {}
    local duration, maxValue = (castbar.duration or 0), (castbar.max or 1)
    local percent = castFailed and 1 or (duration / maxValue)
    local percentChange = cache.percent == nil or castFailed or (abs(percent - cache.percent) > 0.05)
    local unit = frame.unit or castbar.unit
    if unit == "vehicle" then unit = "player" end

    -- Update cache
    useCache = useCache and not percentChange and self.updateCache[castbar]
    if percentChange then cache.percent = percent end

    -- Get direction we want
    local fadeMode = I.Enum.GradientMode.Mode.HORIZONTAL
    local fadeDirection = self.leftFrames[unitType] and I.Enum.GradientMode.Direction.LEFT or (self.rightFrames[unitType] and I.Enum.GradientMode.Direction.RIGHT)

    -- Default to top-to-bottom mode when no direction is defined
    if not fadeDirection then
        fadeMode = I.Enum.GradientMode.Mode.VERTICAL
        fadeDirection = I.Enum.GradientMode.Direction.RIGHT
    end

    -- Get ElvUI Settings
    local customColor = frame.db and frame.db.castbar and frame.db.castbar.customColor
    local custom = customColor and customColor.enable and customColor
    local classColorFallback = (custom and custom.useClassColor) or (not custom and UF.db.colors.castClassColor)

    -- Get new colors from maps
    local nR, nG, nB, sR, sG, sB, cnR, cnG, cnB, csR, csG, csB =
        self:GetCastbarColor(unit, castbar.channeling, not castbar.notInterruptible, castFailed, duration, maxValue, classColorFallback)

    -- Calculate percentage gradients
    local rnfR, rnfG, rnfB, rsfR, rsfG, rsfB, rnbR, rnbG, rnbB, rsbR, rsbG, rsgB =
        self:CalculateCachedFading(cache, useCache, fadeDirection, percent, nR, nG, nB, sR, sG, sB, cnR, cnG, cnB, csR, csG, csB)

    castbar:SetStatusBarColor(1, 1, 1, 1)
    castbar:GetStatusBarTexture():SetGradient(I.Enum.GradientMode.Mode[fadeMode], rnfR, rnfG, rnfB, rsfR, rsfG, rsfB)

    if castbar.bg then
        castbar.bg:SetVertexColor(1, 1, 1, 1)
        castbar.bg:SetGradient(I.Enum.GradientMode.Mode[fadeMode], rnbR, rnbG, rnbB, rsbR, rsbG, rsgB)
    end

    -- Flag as cached
    self.updateCache[castbar] = true
    castbar.colorCache = cache
end

-- Power Gradient
function GR:PostUpdatePowerColor(power, unit, eR, eG, eB)
    if not self.isEnabled or not self.db or not self.db.enabled then return end

    -- Get Parent frame
    local frame = power:GetParent()

    -- Get type of unit, raid, party, focus etc
    local unitType = self:GetUnitFrameType(frame)
    if not unitType then return self:DebugPrint("UnitFrame Type was empty for power frame", unitType) end

    -- Vars
    local cache = power.colorCache or {}
    local percent = power.cur and ((power.cur - power.min) / (power.max - power.min)) or 0
    local percentChange = cache.percent == nil or (abs(percent - cache.percent) > 0.05)
    local calculateBackupFG, colorMap, colorEntry
    local nR, nG, nB, sR, sG, sB, cnR, cnG, cnB, csR, csG, csB

    -- Update cache
    local useCache = cache.nR ~= nil and cache.calculateBackupFG == false and self.updateCache[power]
    if useCache then useCache = eB == nil or (cache.eB and (F.AlmostEqual(cache.eR, eR) and F.AlmostEqual(cache.eG, eG) and F.AlmostEqual(cache.eB, eB))) end
    if percentChange then cache.percent = percent end

    if eB ~= nil then
        cache.eR, cache.eG, cache.eB = eR, eG, eB
    end

    -- Get direction we want
    local fadeMode = I.Enum.GradientMode.Mode.HORIZONTAL
    local fadeDirection = self.leftFrames[unitType] and I.Enum.GradientMode.Direction.LEFT or (self.rightFrames[unitType] and I.Enum.GradientMode.Direction.RIGHT)

    -- Default to top-to-bottom mode when no direction is defined
    if not fadeDirection then
        fadeMode = I.Enum.GradientMode.Mode.VERTICAL
        fadeDirection = I.Enum.GradientMode.Direction.RIGHT
    end

    if not useCache then
        calculateBackupFG = true

        if power.colorPower then
            calculateBackupFG = false
            colorMap = "powerColorMap"
            if power.displayType ~= ALTERNATE_POWER_INDEX then
                local pToken, altR, altG, altB = select(2, UnitPowerType(unit or power.unit or frame.unit))
                if not self.db["powerColorMap"][I.Enum.GradientMode.Color.NORMAL][pToken] then
                    if altR then
                        calculateBackupFG = true
                        nR, nG, nB = altR, altG, altB

                        if nR > 1 or nG > 1 or nB > 1 then
                            nR, nG, nB = nR / 255, nG / 255, nB / 255
                        end
                    else
                        colorEntry = "MANA"
                    end
                else
                    colorEntry = pToken
                end
            else
                colorEntry = "ALT_POWER"
            end
        elseif
            (power.colorClass and UnitIsPlayer(unit))
            or (power.colorClassNPC and not UnitIsPlayer(unit))
            or (power.colorClassPet and UnitPlayerControlled(unit) and not UnitIsPlayer(unit))
        then
            calculateBackupFG = false
            colorMap = "classColorMap"
            colorEntry = select(2, UnitClass(unit or power.unit or frame.unit))
        end

        -- Caclulate colors
        if not calculateBackupFG then
            -- Get maps for class
            if not colorEntry then
                calculateBackupFG = true
            else
                local fgMap = self.db[colorMap]
                local bgMap = F.GetBackgroundColorMap(colorMap)

                local shiftMap = fgMap[I.Enum.GradientMode.Color.SHIFT][colorEntry]
                local normalMap = fgMap[I.Enum.GradientMode.Color.NORMAL][colorEntry]

                local bgShiftMap = bgMap[I.Enum.GradientMode.Color.SHIFT][colorEntry]
                local bgNormalMap = bgMap[I.Enum.GradientMode.Color.NORMAL][colorEntry]

                -- Sanity check
                if not shiftMap or not normalMap then
                    calculateBackupFG = true
                    self:DebugPrint("No shift or normal power color for", "unit:", unit or power.unit or frame.unit, "entry:", colorEntry)
                else
                    -- Set new colors from maps
                    nR, nG, nB = normalMap.r, normalMap.g, normalMap.b
                    sR, sG, sB = shiftMap.r, shiftMap.g, shiftMap.b

                    cnR, cnG, cnB = bgNormalMap.r, bgNormalMap.g, bgNormalMap.b
                    csR, csG, csB = bgShiftMap.r, bgShiftMap.g, bgShiftMap.b
                end
            end
        end

        -- Get backup color if needed
        if calculateBackupFG then
            if cache.eR ~= nil then
                nR, nG, nB = cache.eR, cache.eR, cache.eB
            else
                nR, nG, nB = power:GetStatusBarColor()
            end

            sR, sG, sB = self:CalculateBackupColor(nR, nG, nB)
        end

        cache.calculateBackupFG = calculateBackupFG
        cache.nR, cache.nG, cache.nB = nR, nG, nB
        cache.sR, cache.sG, cache.sB = sR, sG, sB
        cache.cnR, cache.cnG, cache.cnB = cnR, cnG, cnB
        cache.csR, cache.csG, cache.csB = csR, csG, csB
    else
        nR, nG, nB = cache.nR, cache.nG, cache.nB
        sR, sG, sB = cache.sR, cache.sG, cache.sB
        cnR, cnG, cnB = cache.cnR, cache.cnG, cache.cnB
        csR, csG, csB = cache.csR, cache.csG, cache.csB
    end

    -- Get new colors from maps
    local rnfR, rnfG, rnfB, rsfR, rsfG, rsfB, rnbR, rnbG, rnbB, rsbR, rsbG, rsgB =
        self:CalculateCachedFading(cache, (useCache and not percentChange), fadeDirection, percent, nR, nG, nB, sR, sG, sB, cnR, cnG, cnB, csR, csG, csB)

    power:SetStatusBarColor(1, 1, 1, 1)
    power:GetStatusBarTexture():SetGradient(I.Enum.GradientMode.Mode[fadeMode], rnfR, rnfG, rnfB, rsfR, rsfG, rsfB)

    if power.BG then
        power.BG:SetVertexColor(1, 1, 1, 1)
        power.BG:SetGradient(I.Enum.GradientMode.Mode[fadeMode], rnbR, rnbG, rnbB, rsbR, rsbG, rsgB)
    end

    -- Flag as cached
    self.updateCache[power] = true
    power.colorCache = cache
end

local function updateCallbackWrapper(self, func)
    self:SettingsUpdate()
    func()
end

function GR:AddFrameToSettingsUpdate(category, frame, func)
    if self.settingsEvents[frame] ~= nil then return end

    self.settingsEvents[frame] = true
    F.Event.RegisterCallback("ThemesGradients.SettingsUpdate." .. category, F.Event.GenerateClosure(updateCallbackWrapper, self, func), frame)
end

function GR:UpdateStatusBarFrame(frame)
    if not self.isEnabled or not self.db or not self.db.enabled then return end
    if not frame then return end

    -- Comfigure Health
    if frame.Health then
        frame.Health:SetStatusBarTexture(self.db.texture)
        frame.Health.bg:SetTexture(self.db.texture)

        -- Hook if needed
        if not self:IsHooked(frame.Health, "PostUpdateColor") then
            self:RawHook(frame.Health, "PostUpdateColor", F.Event.GenerateClosure(self.PostUpdateHealthColor, self))
            self:AddFrameToSettingsUpdate("Health", frame.Health, F.Event.GenerateClosure(self.PostUpdateHealthColor, self, frame.Health, frame.unit))
        end
    end

    -- Configure CastBar
    if frame.Castbar then
        frame.Castbar:SetStatusBarTexture(self.db.texture)
        frame.Castbar.bg:SetTexture(self.db.texture)

        -- Hook if needed
        if not self:IsHooked(frame.Castbar, "PostCastStart") then
            self:SecureHook(frame.Castbar, "PostCastStart", function()
                self:PostUpdateCastColor(frame.Castbar, false, false)
            end)
        end

        if not self:IsHooked(frame.Castbar, "PostCastFail") then
            self:SecureHook(frame.Castbar, "PostCastFail", function()
                self:PostUpdateCastColor(frame.Castbar, false, true)
            end)
        end

        if not self:IsHooked(frame.Castbar, "PostCastInterruptible") then
            self:SecureHook(frame.Castbar, "PostCastInterruptible", function()
                self:PostUpdateCastColor(frame.Castbar, false, false)
            end)
        end

        if not self:IsHooked(frame.Castbar, "SetValue") then
            self:SecureHook(frame.Castbar, "SetValue", function()
                self:PostUpdateCastColor(frame.Castbar, true, false)
            end)
        end

        if not self:IsHooked(frame.Castbar, "SetMinMaxValues") then
            self:SecureHook(frame.Castbar, "SetMinMaxValues", function()
                self:PostUpdateCastColor(frame.Castbar, true, false)
            end)
        end

        -- ! Personal Change
        if F.DevCastAnimationStop then
            if not self:IsHooked(frame.Castbar, "Hide") then
                local hideFun = frame.Castbar.Hide
                self:RawHook(frame.Castbar, "Hide", function()
                    return F.DevCastAnimationHideProxy(frame.Castbar, hideFun)
                end, true)
            end

            if not self:IsHooked(frame.Castbar, "Show") then
                local showFun = frame.Castbar.Show
                self:RawHook(frame.Castbar, "Show", function()
                    return F.DevCastAnimationShowProxy(frame.Castbar, showFun)
                end, true)
            end

            if not self:IsHooked(frame.Castbar, "IsShown") then
                local shownFun = frame.Castbar.IsShown
                self:RawHook(frame.Castbar, "IsShown", function()
                    return F.DevCastAnimationIsShownProxy(frame.Castbar, shownFun)
                end, true)
            end
        end
    end

    -- Configure Power Bar
    if frame.Power then
        frame.Power:SetStatusBarTexture(self.db.texture)
        frame.Power.BG:SetTexture(self.db.texture)

        -- Hook if needed
        if not self:IsHooked(frame.Power, "PostUpdateColor") then
            self:SecureHook(frame.Power, "PostUpdateColor", function(_, unit, r, g, b)
                self:PostUpdatePowerColor(frame.Power, unit, r, g, b)
            end)

            self:AddFrameToSettingsUpdate("Power", frame.Power, F.Event.GenerateClosure(self.PostUpdatePowerColor, self, frame.Power, frame.unit))
        end
    end
end

function GR:ConfigureStatusBarFrame(_, frame)
    self:UpdateStatusBarFrame(frame)
end

function GR:UpdateStatusBar(_, frame)
    if not self.isEnabled or not self.db or not self.db.enabled then return end
    if not frame then return end

    local parentFrame = frame:GetParent()
    if not parentFrame then return end

    self:UpdateStatusBarFrame(parentFrame)
end

function GR:UpdateStatusBars()
    for frame in pairs(UF.statusbars) do
        if frame then self:UpdateStatusBar(nil, frame) end
    end
end

function GR:SettingsUpdate()
    -- Clear cache
    self.updateCache = {}

    -- Regenerate Colors
    F.GenerateBackgroundColors()

    -- Set layout
    self.leftFrames = I.GradientMode.Layouts[E.db.TXUI.installer.layout].Left
    self.rightFrames = I.GradientMode.Layouts[E.db.TXUI.installer.layout].Right

    -- ! Personal change
    if F.DevWeakAuraBarRefresh then F.DevWeakAuraBarRefresh() end
    if F.DevPlaterBarsRefresh then F.DevPlaterBarsRefresh() end
end

function GR:Disable()
    if not self.Initialized then return end
    if not self:IsHooked(UF, "Configure_HealthBar") then return end

    self:UnhookAll()
    UF:Update_AllFrames()

    F.EventManagerUnregisterAll(self.interruptNamespace)
end

function GR:Enable()
    if not self.Initialized then return end
    if not self.isEnabled or not self.db or not self.db.enabled then return end
    if self:IsHooked(UF, "Configure_HealthBar") then return end

    -- Apply settings
    self:SettingsUpdate()

    -- Hook functions for configure functions
    self:SecureHook(UF, "Configure_HealthBar", "ConfigureStatusBarFrame")
    self:SecureHook(UF, "Configure_Castbar", "ConfigureStatusBarFrame")
    self:SecureHook(UF, "Configure_Power", "ConfigureStatusBarFrame")

    -- Hook functions for update functions
    self:SecureHook(UF, "Update_StatusBars", "UpdateStatusBars")
    self:SecureHook(UF, "Update_StatusBar", "UpdateStatusBar")

    -- Register Interrupt handler
    if TXUI.IsRetail then F.EventManagerRegister(self.interruptNamespace, "PLAYER_SPECIALIZATION_CHANGED", F.CheckInterruptSpells) end

    F.EventManagerRegister(self.interruptNamespace, "PLAYER_ENTERING_WORLD", F.CheckInterruptSpells)
    F.EventManagerRegister(self.interruptNamespace, "PLAYER_LEVEL_CHANGED", F.CheckInterruptSpells)
    F.EventManagerRegister(self.interruptNamespace, "LEARNED_SPELL_IN_TAB", F.CheckInterruptSpells)
    F.EventManagerRegister(self.interruptNamespace, "PLAYER_ENTERING_WORLD", F.CheckInterruptSpells)

    -- Update!
    UF:Update_AllFrames()
end

function GR:DatabaseUpdate()
    -- Set db
    self.db = F.GetDBFromPath("TXUI.themes.gradientMode")

    -- Set enable state
    local isEnabled = (self.db and self.db.enabled) and TXUI:HasRequirements(I.Requirements.GradientMode)
    if self.isEnabled == isEnabled then return end
    self.isEnabled = isEnabled

    F.Event.ContinueOutOfCombat(function()
        F.Event.ContinueAfterElvUIUpdate(function()
            -- Disable only out of combat
            self:Disable()

            -- Enable only out of combat
            if self.isEnabled then
                if UF.Initialized then
                    self:Enable()
                else
                    self:SecureHook(UF, "Initialize", F.Event.GenerateClosure(self.Enable, self))
                end
            end
        end)
    end)
end

function GR:Initialize()
    -- Don't init second time
    if self.Initialized then return end

    -- Get Frameworks
    UF = E:GetModule("UnitFrames")

    -- Force Update Cache
    self.updateCache = {}
    self.settingsEvents = {}

    -- Set namespace var
    self.interruptNamespace = "GR_INTERRUPT"

    -- Register for updates
    F.Event.RegisterOnceCallback("TXUI.InitializedSafe", F.Event.GenerateClosure(self.DatabaseUpdate, self))
    F.Event.RegisterCallback("TXUI.DatabaseUpdate", self.DatabaseUpdate, self)
    F.Event.RegisterCallback("ThemesGradients.DatabaseUpdate", self.DatabaseUpdate, self)
    F.Event.RegisterCallback("ThemesGradients.SettingsUpdate", self.SettingsUpdate, self)

    -- Get Frameworks
    UF = E:GetModule("UnitFrames")

    -- We are done, hooray!
    self.Initialized = true
end

TXUI:RegisterModule(GR:GetName())
