local _, Cell = ...
local L = Cell.L
---@type CellFuncs
local F = Cell.funcs
---@class CellIndicatorFuncs
local I = Cell.iFuncs
---@type CellAnimations
local A = Cell.animations
---@type PixelPerfectFuncs
local P = Cell.pixelPerfectFuncs

local LCG = LibStub("LibCustomGlow-1.0")
local LibTranslit = LibStub("LibTranslit-1.0")

local function noop() end

-------------------------------------------------
-- shared functions
-------------------------------------------------
function I.Cooldowns_SetSize(self, width, height)
    self.width = width
    self.height = height

    for i = 1, #self do
        self[i]:SetSize(width, height)
    end

    self:UpdateSize()
end

function I.Cooldowns_UpdateSize(self, iconsShown)
    if not (self.width and self.height and self.orientation) then return end -- not init

    if iconsShown then -- call from I.UnitButton_UpdateBuffs or preview
        for i = iconsShown + 1, #self do
            self[i]:Hide()
        end
        if iconsShown ~= 0 then
            if self.orientation == "horizontal" then
                self:_SetSize(self.width*iconsShown-P.Scale(iconsShown-1), self.height)
            else
                self:_SetSize(self.width, self.height*iconsShown-P.Scale(iconsShown-1))
            end
        end
    else
        for i = 1, #self do
            if self[i]:IsShown() then
                if self.orientation == "horizontal" then
                    self:_SetSize(self.width*i-P.Scale(i-1), self.height)
                else
                    self:_SetSize(self.width, self.height*i-P.Scale(i-1))
                end
            end
        end
    end
end

function I.Cooldowns_UpdateSize_WithSpacing(self, iconsShown)
    if not (self.width and self.height and self.orientation) then return end -- not init

    if iconsShown then -- call from I.UnitButton_UpdateBuffs or preview
        for i = iconsShown + 1, #self do
            self[i]:Hide()
        end
        if iconsShown ~= 0 then
            if self.orientation == "horizontal" then
                self:_SetSize(self.width * iconsShown + P.Scale(iconsShown - 1), self.height)
            else
                self:_SetSize(self.width, self.height * iconsShown + P.Scale(iconsShown - 1))
            end
        end
    else
        for i = 1, #self do
            if self[i]:IsShown() then
                if self.orientation == "horizontal" then
                    self:_SetSize(self.width * i + P.Scale(i - 1), self.height)
                else
                    self:_SetSize(self.width, self.height * i + P.Scale(i - 1))
                end
            end
        end
    end
end

function I.Cooldowns_SetBorder(self, border)
    for i = 1, #self do
        self[i]:SetBorder(border)
    end
end

function I.Cooldowns_SetFont(self, ...)
    for i = 1, #self do
        self[i]:SetFont(...)
    end
end

function I.Cooldowns_ShowDuration(self, show)
    for i = 1, #self do
        self[i]:ShowDuration(show)
    end
end

function I.Cooldowns_ShowAnimation(self, show)
    for i = 1, #self do
        self[i]:ShowAnimation(show)
    end
end

function I.Cooldowns_UpdatePixelPerfect(self)
    P.Repoint(self)
    for i = 1, #self do
        self[i]:UpdatePixelPerfect()
    end
end

function I.Cooldowns_SetOrientation(self, orientation)
    local point1, point2, x, y

    if orientation == "left-to-right" then
        point1 = "TOPLEFT"
        point2 = "TOPRIGHT"
        self.orientation = "horizontal"
        x = -1
        y = 0
    elseif orientation == "right-to-left" then
        point1 = "TOPRIGHT"
        point2 = "TOPLEFT"
        self.orientation = "horizontal"
        x = 1
        y = 0
    elseif orientation == "top-to-bottom" then
        point1 = "TOPLEFT"
        point2 = "BOTTOMLEFT"
        self.orientation = "vertical"
        x = 0
        y = 1
    elseif orientation == "bottom-to-top" then
        point1 = "BOTTOMLEFT"
        point2 = "TOPLEFT"
        self.orientation = "vertical"
        x = 0
        y = -1
    end

    for i = 1, #self do
        P.ClearPoints(self[i])
        if i == 1 then
            P.Point(self[i], point1)
        else
            P.Point(self[i], point1, self[i-1], point2, x, y)
        end
    end

    self:UpdateSize()
end

function I.Cooldowns_SetOrientation_WithSpacing(self, orientation)
    local point1, point2, x, y

    if orientation == "left-to-right" then
        point1 = "TOPLEFT"
        point2 = "TOPRIGHT"
        self.orientation = "horizontal"
        x = 1
        y = 0
    elseif orientation == "right-to-left" then
        point1 = "TOPRIGHT"
        point2 = "TOPLEFT"
        self.orientation = "horizontal"
        x = -1
        y = 0
    elseif orientation == "top-to-bottom" then
        point1 = "TOPLEFT"
        point2 = "BOTTOMLEFT"
        self.orientation = "vertical"
        x = 0
        y = -1
    elseif orientation == "bottom-to-top" then
        point1 = "BOTTOMLEFT"
        point2 = "TOPLEFT"
        self.orientation = "vertical"
        x = 0
        y = 1
    end

    for i = 1, #self do
        P.ClearPoints(self[i])
        if i == 1 then
            P.Point(self[i], point1)
        else
            P.Point(self[i], point1, self[i-1], point2, x, y)
        end
    end

    self:UpdateSize()
end

-------------------------------------------------
-- CreateDefensiveCooldowns
-------------------------------------------------
function I.CreateDefensiveCooldowns(parent)
    local defensiveCooldowns = CreateFrame("Frame", parent:GetName().."DefensiveCooldownParent", parent.widgets.indicatorFrame)
    parent.indicators.defensiveCooldowns = defensiveCooldowns
    -- defensiveCooldowns:SetSize(20, 10)
    defensiveCooldowns:Hide()

    defensiveCooldowns._SetSize = defensiveCooldowns.SetSize
    defensiveCooldowns.SetSize = I.Cooldowns_SetSize
    defensiveCooldowns.UpdateSize = I.Cooldowns_UpdateSize
    defensiveCooldowns.SetFont = I.Cooldowns_SetFont
    defensiveCooldowns.SetOrientation = I.Cooldowns_SetOrientation
    defensiveCooldowns.ShowDuration = I.Cooldowns_ShowDuration
    defensiveCooldowns.ShowAnimation = I.Cooldowns_ShowAnimation
    defensiveCooldowns.SetupGlow = I.Glow_SetupForChildren
    defensiveCooldowns.UpdatePixelPerfect = I.Cooldowns_UpdatePixelPerfect

    for i = 1, 5 do
        local name = parent:GetName().."DefensiveCooldown"..i
        local frame = I.CreateAura_BarIcon(name, defensiveCooldowns)
        tinsert(defensiveCooldowns, frame)
    end
end

-------------------------------------------------
-- CreateExternalCooldowns
-------------------------------------------------
function I.CreateExternalCooldowns(parent)
    local externalCooldowns = CreateFrame("Frame", parent:GetName().."ExternalCooldownParent", parent.widgets.indicatorFrame)
    parent.indicators.externalCooldowns = externalCooldowns
    externalCooldowns:Hide()

    externalCooldowns._SetSize = externalCooldowns.SetSize
    externalCooldowns.SetSize = I.Cooldowns_SetSize
    externalCooldowns.UpdateSize = I.Cooldowns_UpdateSize
    externalCooldowns.SetFont = I.Cooldowns_SetFont
    externalCooldowns.SetOrientation = I.Cooldowns_SetOrientation
    externalCooldowns.ShowDuration = I.Cooldowns_ShowDuration
    externalCooldowns.ShowAnimation = I.Cooldowns_ShowAnimation
    externalCooldowns.SetupGlow = I.Glow_SetupForChildren
    externalCooldowns.UpdatePixelPerfect = I.Cooldowns_UpdatePixelPerfect

    for i = 1, 5 do
        local name = parent:GetName().."ExternalCooldown"..i
        local frame = I.CreateAura_BarIcon(name, externalCooldowns)
        tinsert(externalCooldowns, frame)
    end
end

-------------------------------------------------
-- CreateAllCooldowns
-------------------------------------------------
function I.CreateAllCooldowns(parent)
    local allCooldowns = CreateFrame("Frame", parent:GetName().."AllCooldownParent", parent.widgets.indicatorFrame)
    parent.indicators.allCooldowns = allCooldowns
    allCooldowns:Hide()

    allCooldowns._SetSize = allCooldowns.SetSize
    allCooldowns.SetSize = I.Cooldowns_SetSize
    allCooldowns.UpdateSize = I.Cooldowns_UpdateSize
    allCooldowns.SetFont = I.Cooldowns_SetFont
    allCooldowns.SetOrientation = I.Cooldowns_SetOrientation
    allCooldowns.ShowDuration = I.Cooldowns_ShowDuration
    allCooldowns.ShowAnimation = I.Cooldowns_ShowAnimation
    allCooldowns.SetupGlow = I.Glow_SetupForChildren
    allCooldowns.UpdatePixelPerfect = I.Cooldowns_UpdatePixelPerfect

    for i = 1, 5 do
        local name = parent:GetName().."ExternalCooldown"..i
        local frame = I.CreateAura_BarIcon(name, allCooldowns)
        tinsert(allCooldowns, frame)
    end
end

-------------------------------------------------
-- CreateTankActiveMitigation
-------------------------------------------------
function I.CreateTankActiveMitigation(parent)
    local bar = Cell.CreateStatusBar(parent:GetName().."TanckActiveMitigation", parent.widgets.indicatorFrame, 20, 6, 100)
    parent.indicators.tankActiveMitigation = bar
    bar:Hide()

    bar:SetStatusBarTexture(Cell.vars.whiteTexture)
    bar:GetStatusBarTexture():SetAlpha(0)
    bar:SetReverseFill(true)

    local tex = bar:CreateTexture(nil, "BORDER", nil, -1)
    bar.tex = tex
    tex:SetColorTexture(F.GetClassColor(Cell.vars.playerClass))
    tex:SetPoint("TOPLEFT")
    tex:SetPoint("BOTTOMRIGHT", bar:GetStatusBarTexture(), "BOTTOMLEFT")

    local elapsedTime = 0
    bar:SetScript("OnUpdate", function(self, elapsed)
        if elapsedTime >= 0.1 then
            bar:SetValue(bar:GetValue() + elapsedTime)
            elapsedTime = 0
        end
        elapsedTime = elapsedTime + elapsed
    end)

    function bar:SetCooldown(start, duration)
        if bar.cType == "class_color" then
            if not parent.states.class then parent.states.class = UnitClassBase(parent.states.unit) end --? why sometimes parent.states.class == nil ???
            tex:SetColorTexture(F.GetClassColor(parent.states.class))
        else
            tex:SetColorTexture(bar.cTable[1], bar.cTable[2], bar.cTable[3])
        end
        bar:SetMinMaxValues(0, duration)
        bar:SetValue(GetTime()-start)
        bar:Show()
    end

    function bar:SetColor(cType, cTable)
        bar.cType = cType
        bar.cTable = cTable
    end
end

-------------------------------------------------
-- CreateDebuffs
-------------------------------------------------
local function Debuffs_SetSize(self, normalSize, bigSize)
    for i = 1, 10 do
        P.Size(self[i], normalSize[1], normalSize[2])
    end
    -- store sizes for SetCooldown
    self.normalSize = normalSize
    self.bigSize = bigSize
    -- remove wrong data from PixelPerfect
    self.width = nil
    self.height = nil

    self:UpdateSize()
end

local function Debuffs_UpdateSize(self, iconsShown)
    if not (self.normalSize and self.bigSize and self.orientation) then return end -- not init

    if iconsShown then
        for i = iconsShown + 1, 10 do
            self[i]:Hide()
        end
    end

    local size = 0
    for i = 1, 10 do
        if self[i]:IsShown() then
            size = size + self[i].width
        end
    end
    if self.orientation == "left-to-right" or self.orientation == "right-to-left"  then
        self:_SetSize(P.Scale(size), P.Scale(self.normalSize[2]))
    else
        self:_SetSize(P.Scale(self.normalSize[1]), P.Scale(size))
    end
end

local function Debuffs_SetFont(self, ...)
    for i = 1, 10 do
        self[i]:SetFont(...)
    end
end

local function Debuffs_SetPoint(self, point, relativeTo, relativePoint, x, y)
    self:_SetPoint(point, relativeTo, relativePoint, x, y)

    if string.find(point, "LEFT$") then
        self.hAlignment = "LEFT"
    elseif string.find(point, "RIGHT$") then
        self.hAlignment = "RIGHT"
    else
        self.hAlignment = ""
    end

    if string.find(point, "^TOP") then
        self.vAlignment = "TOP"
    elseif string.find(point, "^BOTTOM") then
        self.vAlignment = "BOTTOM"
    else
        self.vAlignment = ""
    end

    if self.hAlignment == "" and self.vAlignment == "" then
        self.vAlignment = "CENTER"
    end

    -- self[1]:ClearAllPoints()
    -- self[1]:SetPoint(self.vAlignment..self.hAlignment)
    -- --! update icons
    self:SetOrientation(self.orientation or "left-to-right")
end

--! NOTE: SetPoint must be invoked before SetOrientation
local function Debuffs_SetOrientation(self, orientation)
    self.orientation = orientation
    local point1, point2, v, h
    v = self.vAlignment == "CENTER" and "" or self.vAlignment
    h = self.hAlignment
    if orientation == "left-to-right" then
        point1 = v.."LEFT"
        point2 = v.."RIGHT"
    elseif orientation == "right-to-left" then
        point1 = v.."RIGHT"
        point2 = v.."LEFT"
    elseif orientation == "top-to-bottom" then
        point1 = "TOP"..h
        point2 = "BOTTOM"..h
    elseif orientation == "bottom-to-top" then
        point1 = "BOTTOM"..h
        point2 = "TOP"..h
    end

    for i = 1, 10 do
        P.ClearPoints(self[i])
        if i == 1 then
            P.Point(self[i], point1)
        else
            P.Point(self[i], point1, self[i-1], point2)
        end
    end

    self:UpdateSize()
end

local function Debuffs_ShowTooltip(debuffs, show)
    debuffs.showTooltip = show

    for i = 1, 10 do
        if show then
            debuffs[i]:SetScript("OnEnter", function(self)
                if self.index then
                    F.ShowTooltips(debuffs.parent, "spell", debuffs.parent.states.displayedUnit, self.index, "HARMFUL")
                elseif self.auraInstanceID then
                    F.ShowTooltips(debuffs.parent, "aura", debuffs.parent.states.displayedUnit, self.auraInstanceID, "HARMFUL")
                end
            end)

            debuffs[i]:SetScript("OnLeave", function()
                GameTooltip:Hide()
            end)

            -- https://warcraft.wiki.gg/wiki/API_ScriptRegion_EnableMouse
            if not debuffs.enableBlacklistShortcut then
                debuffs[i]:SetMouseClickEnabled(false)
            end
        else
            debuffs[i]:SetScript("OnEnter", nil)
            debuffs[i]:SetScript("OnLeave", nil)
            if debuffs.enableBlacklistShortcut then
                debuffs[i]:SetMouseMotionEnabled(false)
            else
                debuffs[i]:EnableMouse(false)
            end
        end
    end
end

local function Debuffs_EnableBlacklistShortcut(debuffs, enabled)
    debuffs.enableBlacklistShortcut = enabled

    for i = 1, 10 do
        if enabled then
            debuffs[i]:SetScript("OnMouseUp", function(self, button, isInside)
                if button == "RightButton" and isInside and IsLeftAltKeyDown() and IsLeftControlKeyDown()
                    and self.spellId and not F.TContains(CellDB["debuffBlacklist"], self.spellId) then
                    -- print msg
                    local name, icon = F.GetSpellInfo(self.spellId)
                    if name and icon then
                        F.Print(L["Added |T%d:0|t|cFFFF3030%s(%d)|r into debuff blacklist."]:format(icon, name, self.spellId))
                    end
                    -- update db
                    tinsert(CellDB["debuffBlacklist"], self.spellId)
                    Cell.vars.debuffBlacklist = F.ConvertTable(CellDB["debuffBlacklist"])
                    Cell.Fire("UpdateIndicators", Cell.vars.currentLayout, "", "debuffBlacklist")
                    -- refresh
                    F.ReloadIndicatorOptions(Cell.defaults.indicatorIndices.debuffs)
                end
            end)
        else
            debuffs[i]:SetScript("OnMouseUp", nil)
            if debuffs.showTooltip then
                debuffs[i]:SetMouseClickEnabled(false)
            else
                debuffs[i]:EnableMouse(false)
            end
        end
    end
end

function I.CreateDebuffs(parent)
    local debuffs = CreateFrame("Frame", parent:GetName().."DebuffParent", parent.widgets.indicatorFrame)
    parent.indicators.debuffs = debuffs
    debuffs:Hide()
    debuffs.parent = parent

    debuffs._SetSize = debuffs.SetSize
    debuffs.SetSize = Debuffs_SetSize
    debuffs.UpdateSize = Debuffs_UpdateSize
    debuffs.SetFont = Debuffs_SetFont

    debuffs.hAlignment = ""
    debuffs.vAlignment = ""
    debuffs._SetPoint = debuffs.SetPoint
    debuffs.SetPoint = Debuffs_SetPoint
    debuffs.SetOrientation = Debuffs_SetOrientation

    debuffs.ShowDuration = I.Cooldowns_ShowDuration
    debuffs.ShowAnimation = I.Cooldowns_ShowAnimation
    debuffs.UpdatePixelPerfect = I.Cooldowns_UpdatePixelPerfect

    debuffs.ShowTooltip = Debuffs_ShowTooltip
    debuffs.EnableBlacklistShortcut = Debuffs_EnableBlacklistShortcut

    for i = 1, 10 do
        local name = parent:GetName().."Debuff"..i
        local frame = I.CreateAura_BarIcon(name, debuffs)
        tinsert(debuffs, frame)

        frame._SetCooldown = frame.SetCooldown
        function frame:SetCooldown(start, duration, debuffType, texture, count, refreshing, isBigDebuff)
            frame:_SetCooldown(start, duration, debuffType, texture, count, refreshing)
            if isBigDebuff then
                P.Size(frame, debuffs.bigSize[1], debuffs.bigSize[2])
            else
                P.Size(frame, debuffs.normalSize[1], debuffs.normalSize[2])
            end
        end
    end
end

-------------------------------------------------
-- CreateDispels
-------------------------------------------------
local function Dispels_SetSize(self, width, height)
    self.width = width
    self.height = height

    self:_SetSize(width, height)
    for i = 1, 5 do
        self[i]:SetSize(width, height)
    end

    if self._orientation then
        self:SetOrientation(self._orientation)
    else
        self:UpdateSize()
    end
end

local function Dispels_UpdateSize(self, iconsShown)
    if not (self.orientation and self.width and self.height) then return end

    local width, height = self.width, self.height
    if iconsShown then -- SetDispels
        if self.orientation == "horizontal"  then
            width = self.width + (iconsShown - 1) * floor(self.width / 2)
            height = self.height
        else
            width = self.width
            height = self.height + (iconsShown - 1) * floor(self.height / 2)
        end
    else
        for i = 1, 5 do
            if self[i]:IsShown() then
                if self.orientation == "horizontal"  then
                    width = self.width + (i - 1) * floor(self.width / 2)
                    height = self.height
                else
                    width = self.width
                    height = self.height + (i - 1) * floor(self.height / 2)
                end
            else
                break
            end
        end
    end

    self:_SetSize(width, height)
end

local dispelOrder = {"Magic", "Curse", "Disease", "Poison", "Bleed"}
local function Dispels_SetDispels(self, dispelTypes)
    local r, g, b = 0, 0, 0
    local found

    self.highlight:Hide()

    local i = 0
    for _, dispelType in ipairs(dispelOrder) do
        local showHighlight = dispelTypes[dispelType]
        if type(showHighlight) == "boolean" then
            -- highlight
            if not found and self.highlightType ~= "none" and dispelType and showHighlight then
                found = true
                local r, g, b = I.GetDebuffTypeColor(dispelType)
                if self.highlightType == "entire" then
                    self.highlight:SetTexture(Cell.vars.whiteTexture)
                    self.highlight:SetVertexColor(r, g, b, 0.5)
                elseif self.highlightType == "current" or self.highlightType == "current+" then
                    self.highlight:SetTexture(Cell.vars.texture)
                    self.highlight:SetVertexColor(r, g, b, 1)
                elseif self.highlightType == "gradient" or self.highlightType == "gradient-half" then
                    self.highlight:SetTexture(Cell.vars.whiteTexture)
                    self.highlight:SetGradient("VERTICAL", CreateColor(r, g, b, 1), CreateColor(r, g, b, 0))
                end
                self.highlight:Show()
            end
            -- icons
            if self.showIcons then
                i = i + 1
                self[i]:SetDispel(dispelType)
            end
        end
    end

    self:UpdateSize(i)

    -- hide unused
    for j = i+1, 5 do
        self[j]:Hide()
    end
end

local function Dispels_SetDispel_Blizzard(self, dispelType)
    self:SetTexture("Interface\\AddOns\\Cell\\Media\\Debuffs\\"..dispelType)
    self:Show()
end

local function Dispels_SetDispel_Rhombus(self, dispelType)
    self:SetTexture("Interface\\AddOns\\Cell\\Media\\Debuffs\\Rhombus")
    self:SetVertexColor(I.GetDebuffTypeColor(dispelType))
    self:Show()
end

local function Dispels_SetIconStyle(self, style)
    self.showIcons = style ~= "none"
    for i = 1, 5 do
        if style == "rhombus" then
            self[i].SetDispel = Dispels_SetDispel_Rhombus
        else -- blizzard
            self[i].SetDispel = Dispels_SetDispel_Blizzard
            self[i]:SetVertexColor(1, 1, 1, 1)
        end
    end
end

--! SetSize must be invoked before this
local function Dispels_SetOrientation(self, orientation)
    self._orientation = orientation
    local point, x, y
    if orientation == "left-to-right" then
        point = "TOPLEFT"
        x = floor(self.width / 2)
        y = 0
        self.orientation = "horizontal"
    elseif orientation == "right-to-left" then
        point = "TOPRIGHT"
        x = -floor(self.width / 2)
        y = 0
        self.orientation = "horizontal"
    elseif orientation == "top-to-bottom" then
        point = "TOPLEFT"
        x = 0
        y = -floor(self.height / 2)
        self.orientation = "vertical"
    elseif orientation == "bottom-to-top" then
        point = "BOTTOMLEFT"
        x = 0
        y = floor(self.height / 2)
        self.orientation = "vertical"
    end

    for i = 1, 5 do
        self[i]:ClearAllPoints()
        if i == 1 then
            self[i]:SetPoint(point)
        else
            self[i]:SetPoint(point, self[i-1], point, x, y)
        end
    end

    self:UpdateSize()
end

local function Dispels_UpdateHighlight(self, highlightType)
    self.highlightType = highlightType
    self.highlight:SetBlendMode("BLEND")

    if highlightType == "none" then
        self.highlight:Hide()
    elseif highlightType == "gradient" then
        -- self.highlight:SetParent(self.parent.widgets.indicatorFrame)
        self.highlight:ClearAllPoints()
        self.highlight:SetAllPoints(self.parent.widgets.healthBar)
        self.highlight:SetTexture(Cell.vars.whiteTexture)
        self.highlight:SetDrawLayer("ARTWORK", 0)
    elseif highlightType == "gradient-half" then
        -- self.highlight:SetParent(self.parent.widgets.indicatorFrame)
        self.highlight:ClearAllPoints()
        self.highlight:SetPoint("BOTTOMLEFT", self.parent.widgets.healthBar)
        self.highlight:SetPoint("TOPRIGHT", self.parent.widgets.healthBar, "RIGHT")
        self.highlight:SetTexture(Cell.vars.whiteTexture)
        self.highlight:SetDrawLayer("ARTWORK", 0)
    elseif highlightType == "entire" then
        -- self.highlight:SetParent(self.parent.widgets.indicatorFrame)
        self.highlight:ClearAllPoints()
        self.highlight:SetAllPoints(self.parent.widgets.healthBar)
        self.highlight:SetTexture(Cell.vars.whiteTexture)
        self.highlight:SetDrawLayer("ARTWORK", 0)
    elseif highlightType == "current" then
        -- self.highlight:SetParent(self.parent.widgets.healthBar)
        self.highlight:ClearAllPoints()
        self.highlight:SetAllPoints(self.parent.widgets.healthBar:GetStatusBarTexture())
        self.highlight:SetTexture(Cell.vars.texture)
        self.highlight:SetDrawLayer("ARTWORK", -7)
    elseif highlightType == "current+" then
        -- self.highlight:SetParent(self.parent.widgets.healthBar)
        self.highlight:ClearAllPoints()
        self.highlight:SetAllPoints(self.parent.widgets.healthBar:GetStatusBarTexture())
        self.highlight:SetTexture(Cell.vars.texture)
        self.highlight:SetDrawLayer("ARTWORK", -7)
        self.highlight:SetBlendMode("ADD")
    end
end

function I.CreateDispels(parent)
    local dispels = CreateFrame("Frame", parent:GetName().."DispelParent", parent.widgets.indicatorFrame)
    parent.indicators.dispels = dispels
    dispels.parent = parent
    dispels:Hide()

    dispels:SetScript("OnHide", function()
        dispels.highlight:Hide()
    end)

    dispels.highlight = parent.widgets.midLevelFrame:CreateTexture(parent:GetName().."DispelHighlight")
    dispels.highlight:Hide()

    dispels._SetSize = dispels.SetSize
    dispels.SetSize = Dispels_SetSize
    dispels.UpdateSize = Dispels_UpdateSize
    dispels.SetDispels = Dispels_SetDispels
    dispels.UpdateHighlight = Dispels_UpdateHighlight
    dispels.SetIconStyle = Dispels_SetIconStyle
    dispels.SetOrientation = Dispels_SetOrientation

    for i = 1, 5 do
        local icon = dispels:CreateTexture(parent:GetName().."Dispel"..i, "ARTWORK")
        tinsert(dispels, icon)
        icon:Hide()

        icon:SetDrawLayer("ARTWORK", 6-i)
        icon.SetDispel = Dispels_SetDispel_Blizzard
    end
end

-------------------------------------------------
-- CreateRaidDebuffs
-------------------------------------------------
local currentAreaDebuffs = {}
local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")

local function UpdateDebuffsForCurrentZone(instanceName)
    wipe(currentAreaDebuffs)
    local iName = F.GetInstanceName()
    if iName == "" then return end

    if iName == instanceName or instanceName == nil then
        currentAreaDebuffs = F.GetDebuffList(iName)
        F.Debug("|cffff77AARaidDebuffsChanged:|r", iName)
    end
end
Cell.RegisterCallback("RaidDebuffsChanged", "UpdateDebuffsForCurrentZone", UpdateDebuffsForCurrentZone)
eventFrame:SetScript("OnEvent", function()
    UpdateDebuffsForCurrentZone()
end)

local function CheckCondition(operator, checkedValue, currentValue)
    -- Midnight 12.0.0+: applications (count) may be secret even when spellId is not;
    -- comparisons on secret values throw errors
    if issecretvalue and (issecretvalue(currentValue) or issecretvalue(checkedValue)) then return end
    if operator == "=" then
        if currentValue == checkedValue then return true end
    elseif operator == ">" then
        if currentValue > checkedValue then return true end
    elseif operator == ">=" then
        if currentValue >= checkedValue then return true end
    elseif operator == "<" then
        if currentValue < checkedValue then return true end
    elseif operator == "<=" then
        if currentValue <= checkedValue then return true end
    else -- ~=
        if currentValue ~= checkedValue then return true end
    end
end

function I.GetDebuffOrder(spellName, spellId, count)
    -- Midnight 12.0.0+: spellId/spellName may be secret; cannot use as table key
    if issecretvalue and (issecretvalue(spellId) or issecretvalue(spellName)) then return end
    local t = currentAreaDebuffs[spellId] or currentAreaDebuffs[spellName]
    if not t then return end

    -- check condition
    local show
    if t["condition"][1] == "Stack" then
        show = CheckCondition(t["condition"][2], t["condition"][3], count)
    else -- no condition
        show = true
    end

    if show then return t["order"] end
end

function I.GetDebuffGlow(spellName, spellId, count)
    -- Midnight 12.0.0+: spellId/spellName may be secret; cannot use as table key
    if issecretvalue and (issecretvalue(spellId) or issecretvalue(spellName)) then return end
    local t = currentAreaDebuffs[spellId] or currentAreaDebuffs[spellName]
    if not t then return end

    local showGlow
    if t["glowCondition"] then
        if t["glowCondition"][1] == "Stack" then
            showGlow = CheckCondition(t["glowCondition"][2], t["glowCondition"][3], count)
        end
    else
        showGlow = true
    end

    if showGlow then
        return t["glowType"], t["glowOptions"]
    else
        return "None", nil
    end
end

function I.IsDebuffUseElapsedTime(spellName, spellId)
    -- Midnight 12.0.0+: spellId/spellName may be secret; cannot use as table key
    if issecretvalue and (issecretvalue(spellId) or issecretvalue(spellName)) then return end
    local t = currentAreaDebuffs[spellId] or currentAreaDebuffs[spellName]
    if not t then return end

    return t["useElapsedTime"]
end

local function RaidDebuffs_ShowGlow(self, glowType, glowOptions, noHiding)
    if glowType == "Normal" then
        if not noHiding then
            LCG.PixelGlow_Stop(self.parent)
            LCG.AutoCastGlow_Stop(self.parent)
            LCG.ProcGlow_Stop(self.parent)
        end
        LCG.ButtonGlow_Start(self.parent, glowOptions[1])
    elseif glowType == "Pixel" then
        if not noHiding then
            LCG.ButtonGlow_Stop(self.parent)
            LCG.AutoCastGlow_Stop(self.parent)
            LCG.ProcGlow_Stop(self.parent)
        end
        -- color, N, frequency, length, thickness
        LCG.PixelGlow_Start(self.parent, glowOptions[1], glowOptions[2], glowOptions[3], glowOptions[4], glowOptions[5])
    elseif glowType == "Shine" then
        if not noHiding then
            LCG.ButtonGlow_Stop(self.parent)
            LCG.PixelGlow_Stop(self.parent)
            LCG.ProcGlow_Stop(self.parent)
        end
        -- color, N, frequency, scale
        LCG.AutoCastGlow_Start(self.parent, glowOptions[1], glowOptions[2], glowOptions[3], glowOptions[4])
    elseif glowType == "Proc" then
        if not noHiding then
            LCG.ButtonGlow_Stop(self.parent)
            LCG.PixelGlow_Stop(self.parent)
            LCG.AutoCastGlow_Stop(self.parent)
        end
        -- color, duration
        LCG.ProcGlow_Start(self.parent, {color=glowOptions[1], duration=glowOptions[2], startAnim=false})
    else
        LCG.ButtonGlow_Stop(self.parent)
        LCG.PixelGlow_Stop(self.parent)
        LCG.AutoCastGlow_Stop(self.parent)
        LCG.ProcGlow_Stop(self.parent)
    end
end

local hiders = {
    ["Normal"] = LCG.ButtonGlow_Stop,
    ["Pixel"] = LCG.PixelGlow_Stop,
    ["Shine"] = LCG.AutoCastGlow_Stop,
    ["Proc"] = LCG.ProcGlow_Stop,
}

local function RaidDebuffs_HideGlow(self, glowType)
    if not glowType then
        for _, stop in pairs(hiders) do
            stop(self.parent)
        end
    else
        hiders[glowType](self.parent)
    end
end

local function RaidDebuffs_ShowTooltip(raidDebuffs, show)
    for i = 1, 3 do
        if show then
            raidDebuffs[i]:SetScript("OnEnter", function(self)
                if self.index then
                    F.ShowTooltips(raidDebuffs.parent, "spell", raidDebuffs.parent.states.displayedUnit, self.index, "HARMFUL")
                elseif self.auraInstanceID then
                    F.ShowTooltips(raidDebuffs.parent, "aura", raidDebuffs.parent.states.displayedUnit, self.auraInstanceID, "HARMFUL")
                end
            end)
            raidDebuffs[i]:SetScript("OnLeave", function()
                GameTooltip:Hide()
            end)
        else
            raidDebuffs[i]:SetScript("OnEnter", nil)
            raidDebuffs[i]:SetScript("OnLeave", nil)
            raidDebuffs[i]:EnableMouse(false)
        end
    end
end

function I.CreateRaidDebuffs(parent)
    local raidDebuffs = CreateFrame("Frame", parent:GetName().."RaidDebuffParent", parent.widgets.indicatorFrame)
    parent.indicators.raidDebuffs = raidDebuffs
    raidDebuffs:Hide()
    raidDebuffs.parent = parent

    hooksecurefunc(raidDebuffs, "Hide", RaidDebuffs_HideGlow)
    -- raidDebuffs:SetScript("OnHide", RaidDebuffs_HideGlow)

    raidDebuffs._SetSize = raidDebuffs.SetSize
    raidDebuffs.SetSize = I.Cooldowns_SetSize
    raidDebuffs.SetBorder = I.Cooldowns_SetBorder
    raidDebuffs.UpdateSize = I.Cooldowns_UpdateSize_WithSpacing
    raidDebuffs.ShowDuration = I.Cooldowns_ShowDuration
    raidDebuffs.SetOrientation = I.Cooldowns_SetOrientation_WithSpacing
    raidDebuffs.SetFont = I.Cooldowns_SetFont
    raidDebuffs.ShowGlow = RaidDebuffs_ShowGlow
    raidDebuffs.HideGlow = RaidDebuffs_HideGlow
    raidDebuffs.UpdatePixelPerfect = I.Cooldowns_UpdatePixelPerfect

    raidDebuffs.ShowTooltip = RaidDebuffs_ShowTooltip

    for i = 1, 3 do
        local frame = I.CreateAura_BorderIcon(parent:GetName().."RaidDebuff"..i, raidDebuffs, 2)
        tinsert(raidDebuffs, frame)
        -- frame:SetScript("OnShow", raidDebuffs.UpdateSize)
        -- frame:SetScript("OnHide", raidDebuffs.UpdateSize)
    end
end

-------------------------------------------------
-- private auras
-------------------------------------------------
local function PrivateAuras_UpdatePrivateAuraAnchor(self, unit)
    -- remove old
    if self.auraAnchorID then
        C_UnitAuras.RemovePrivateAuraAnchor(self.auraAnchorID)
        self.unit = nil
        self.auraAnchorID = nil
    end

    -- add new
    if unit then
        local _showCountdownFrame, _showCountdownNumbers = true, false
        if type(self.showCountdownFrame) == "boolean" then _showCountdownFrame = self.showCountdownFrame end
        if type(self.showCountdownNumbers) == "boolean" then _showCountdownNumbers = self.showCountdownNumbers end

        self.unit = unit
        self.auraAnchorID = C_UnitAuras.AddPrivateAuraAnchor({
            unitToken = unit,
            auraIndex = 1,
            parent = self,
            showCountdownFrame = _showCountdownFrame,
            showCountdownNumbers = _showCountdownNumbers,
            iconInfo = {
                iconWidth = self:GetWidth(),
                iconHeight = self:GetHeight(),
                iconAnchor = {
                    point = "CENTER",
                    relativeTo = self,
                    relativePoint = "CENTER",
                    offsetX = 0,
                    offsetY = 0,
                },
            },
            -- durationAnchor = {
            --     point = "BOTTOMRIGHT",
            --     relativeTo = self,
            --     relativePoint = "BOTTOMRIGHT",
            --     offsetX = 0,
            --     offsetY = 0,
            -- },
        })
    end
end

function I.CreatePrivateAuras(parent)
    local privateAuras = CreateFrame("Frame", parent:GetName().."PrivateAuraParent", parent.widgets.indicatorFrame)
    parent.indicators.privateAuras = privateAuras
    privateAuras:Hide()

    privateAuras.UpdatePrivateAuraAnchor = PrivateAuras_UpdatePrivateAuraAnchor
    privateAuras._SetSize = privateAuras.SetSize

    function privateAuras:SetSize(width, height)
        privateAuras:_SetSize(width, height)
        privateAuras:UpdatePrivateAuraAnchor(privateAuras.unit)
    end

    function privateAuras:UpdateOptions(t)
        self.showCountdownFrame = t[1]
        self.showCountdownNumbers = t[2]
        privateAuras:UpdatePrivateAuraAnchor(privateAuras.unit)
    end
end

-------------------------------------------------
-- player raid icon
-------------------------------------------------
function I.CreatePlayerRaidIcon(parent)
    -- local playerRaidIcon = parent.widgets.indicatorFrame:CreateTexture(parent:GetName().."PlayerRaidIcon", "ARTWORK", nil, -7)
    -- parent.indicators.playerRaidIcon = playerRaidIcon
    -- playerRaidIcon:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcons")
    local playerRaidIcon = CreateFrame("Frame", parent:GetName().."PlayerRaidIcon", parent.widgets.indicatorFrame)
    parent.indicators.playerRaidIcon = playerRaidIcon
    playerRaidIcon.tex = playerRaidIcon:CreateTexture(nil, "ARTWORK")
    playerRaidIcon.tex:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcons")
    playerRaidIcon.tex:SetAllPoints(playerRaidIcon)
    playerRaidIcon:Hide()
end

-------------------------------------------------
-- target raid icon
-------------------------------------------------
function I.CreateTargetRaidIcon(parent)
    local targetRaidIcon = CreateFrame("Frame", parent:GetName().."TargetRaidIcon", parent.widgets.indicatorFrame)
    parent.indicators.targetRaidIcon = targetRaidIcon
    targetRaidIcon.tex = targetRaidIcon:CreateTexture(nil, "ARTWORK")
    targetRaidIcon.tex:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcons")
    targetRaidIcon.tex:SetAllPoints(targetRaidIcon)
    targetRaidIcon:Hide()
end

-------------------------------------------------
-- name text
-------------------------------------------------
local font_name = CreateFont("CELL_FONT_NAME")
font_name:SetFont(GameFontNormal:GetFont(), 13, "")
--! NOTE: VERY IMPORTANT, if not set, shadows will DISAPPER when wow window size changed
font_name:SetTextColor(1, 1, 1, 1)
font_name:SetShadowColor(0, 0, 0)
font_name:SetShadowOffset(1, -1)

local font_status = CreateFont("CELL_FONT_STATUS")
font_status:SetFont(GameFontNormal:GetFont(), 11, "")
--! NOTE: VERY IMPORTANT, if not set, shadows will DISAPPER when wow window size changed
font_status:SetTextColor(1, 1, 1, 1)
font_status:SetShadowColor(0, 0, 0)
font_status:SetShadowOffset(1, -1)

function I.CreateNameText(parent)
    local nameText = CreateFrame("Frame", parent:GetName().."NameText", parent.widgets.indicatorFrame)
    parent.indicators.nameText = nameText
    nameText:Hide()

    nameText.name = nameText:CreateFontString(parent:GetName().."NameText_Name", "OVERLAY", "CELL_FONT_NAME")

    nameText.vehicle = nameText:CreateFontString(parent:GetName().."NameText_Vehicle", "OVERLAY", "CELL_FONT_STATUS")
    nameText.vehicle:SetTextColor(0.8, 0.8, 0.8, 1)
    nameText.vehicle:Hide()

    nameText:SetScript("OnShow", function()
        if nameText.vehicleEnabled then
            nameText.vehicle:Show()
        end
    end)
    nameText:SetScript("OnHide", function()
        nameText.vehicle:Hide()
    end)

    function nameText:SetFont(font, size, outline, shadow)
        font = F.GetFont(font)

        local flags
        if outline == "None" then
            flags = ""
        elseif outline == "Outline" then
            flags = "OUTLINE"
        else
            flags = "OUTLINE,MONOCHROME"
        end

        nameText.name:SetFont(font, size, flags)
        nameText.vehicle:SetFont(font, size-2, flags)

        if shadow then
            nameText.name:SetShadowOffset(1, -1)
            nameText.name:SetShadowColor(0, 0, 0, 1)
            nameText.vehicle:SetShadowOffset(1, -1)
            nameText.vehicle:SetShadowColor(0, 0, 0, 1)
        else
            nameText.name:SetShadowOffset(0, 0)
            nameText.name:SetShadowColor(0, 0, 0, 0)
            nameText.vehicle:SetShadowOffset(0, 0)
            nameText.vehicle:SetShadowColor(0, 0, 0, 0)
        end
        nameText.shadow = shadow

        nameText:UpdateName()
        if parent.states.inVehicle or nameText.isPreview then
            nameText:UpdateVehicleName()
        end
    end

    nameText._SetPoint = nameText.SetPoint
    function nameText:SetPoint(point, relativeTo, relativePoint, x, y)
        -- override relativeTo
        nameText:_SetPoint(point, relativeTo, relativePoint, x, y)

        -- update name
        nameText.name:ClearAllPoints()
        nameText.name:SetPoint(point)

        -- update vehicle
        local vp, _, vrp, _, vy = nameText.vehicle:GetPoint(1)
        if vp and vrp and vy then
            if string.find(vp, "TOP") then
                vp, vrp = "TOP", "BOTTOM"
            else -- BOTTOM
                vp, vrp = "BOTTOM", "TOP"
            end

            nameText.vehicle:ClearAllPoints()
            if string.find(point, "LEFT") then
                nameText.vehicle:SetPoint(vp.."LEFT", nameText.name, vrp.."LEFT", 0, vy)
            elseif string.find(point, "RIGHT") then
                nameText.vehicle:SetPoint(vp.."RIGHT", nameText.name, vrp.."RIGHT", 0, vy)
            else -- "CENTER"
                nameText.vehicle:SetPoint(vp, nameText.name, vrp, 0, vy)
            end
        end
    end

    function nameText:UpdateName()
        local name

        -- supporter rainbow
        -- if nameText.name.rainbow then
        --     nameText.name.updater:SetScript("OnUpdate", nil)
        --     if nameText.name.timer then
        --         nameText.name.timer:Cancel()
        --         nameText.name.timer = nil
        --     end
        -- end

        -- only check nickname for players
        if parent.states.isPlayer then
            if CELL_NICKTAG_ENABLED and Cell.NickTag then
                name = Cell.NickTag:GetNickname(parent.states.name, nil, true)
            end
            name = name or F.GetNickname(parent.states.name, parent.states.fullName)
        else
            name = parent.states.name
        end

        if Cell.loaded and CellDB["general"]["translit"] then
            name = LibTranslit:Transliterate(name)
        end

        F.UpdateTextWidth(nameText.name, name, nameText.width, parent.widgets.healthBar)

        if CELL_SHOW_GROUP_PET_OWNER_NAME and parent.isGroupPet then
            local owner = F.GetPlayerUnit(parent.states.unit)
            owner = UnitName(owner)
            if CELL_SHOW_GROUP_PET_OWNER_NAME == "VEHICLE" then
                F.UpdateTextWidth(nameText.vehicle, owner, nameText.width, parent.widgets.healthBar)
            elseif CELL_SHOW_GROUP_PET_OWNER_NAME == "NAME" then
                F.UpdateTextWidth(nameText.name, owner, nameText.width, parent.widgets.healthBar)
            end
        end

        if nameText.name:GetText() then
            if nameText.isPreview then
                if nameText.showGroupNumber then
                    nameText.name:SetText("|cffbbbbbb7-|r"..nameText.name:GetText())
                end
            else
                if IsInRaid() and nameText.showGroupNumber then
                    local raidIndex = UnitInRaid(parent.states.unit)
                    if raidIndex then
                        local subgroup = select(3, GetRaidRosterInfo(raidIndex))
                        -- nameText.name:SetText("|TInterface\\AddOns\\Cell\\Media\\Icons\\group"..subgroup..":0:0:0:-1:64:64:6:58:6:58|t"..nameText.name:GetText())
                        nameText.name:SetText("|cffbbbbbb"..subgroup.."-|r"..nameText.name:GetText())
                    end
                end
            end
        end

        nameText:SetSize(nameText.name:GetWidth(), nameText.name:GetHeight())
    end

    function nameText:UpdateVehicleName()
        F.UpdateTextWidth(nameText.vehicle, nameText.isPreview and L["vehicle name"] or UnitName(parent.states.displayedUnit), nameText.width, parent.widgets.healthBar)
    end

    function nameText:UpdateVehicleNamePosition(pTable)
        local p = nameText:GetPoint(1) or ""
        if string.find(p, "LEFT") then
            p = "LEFT"
        elseif string.find(p, "RIGHT") then
            p = "RIGHT"
        else -- "CENTER"
            p = ""
        end

        nameText.vehicle:ClearAllPoints()
        if pTable[1] == "TOP" then
            nameText.vehicle:Show()
            nameText.vehicle:SetPoint("BOTTOM"..p, nameText.name, "TOP"..p, 0, pTable[2])
            nameText.vehicleEnabled = true
        elseif pTable[1] == "BOTTOM" then
            nameText.vehicle:Show()
            nameText.vehicle:SetPoint("TOP"..p, nameText.name, "BOTTOM"..p, 0, pTable[2])
            nameText.vehicleEnabled = true
        else -- Hide
            nameText.vehicle:Hide()
            nameText.vehicleEnabled = false
        end
    end

    function nameText:UpdateTextWidth(width)
        nameText.width = width

        nameText:UpdateName()

        if parent.states.inVehicle or nameText.isPreview then
            F.UpdateTextWidth(nameText.vehicle, nameText.isPreview and L["Vehicle Name"] or UnitName(parent.states.displayedUnit), width, parent.widgets.healthBar)
        end
    end

    function nameText:UpdatePreviewColor(color)
        if color[1] == "class_color" then
            nameText.name:SetTextColor(F.GetClassColor(Cell.vars.playerClass))
        else
            nameText.name:SetTextColor(unpack(color[2]))
        end
    end

    function nameText:SetColor(r, g, b)
        nameText.name:SetTextColor(r, g, b)
    end

    function nameText:ShowGroupNumber(show)
        nameText.showGroupNumber = show
        nameText:UpdateName()
    end

    parent.widgets.healthBar:SetScript("OnSizeChanged", function()
        if parent.states.name then
            nameText:UpdateName()

            if parent.states.inVehicle or nameText.isPreview then
                nameText:UpdateVehicleName()
            end
        end
    end)
end

-------------------------------------------------
-- status text
-------------------------------------------------
local function StatusText_SetFont(self, font, size, outline, shadow)
    font = F.GetFont(font)

    local flags
    if outline == "None" then
        flags = ""
    elseif outline == "Outline" then
        flags = "OUTLINE"
    else
        flags = "OUTLINE,MONOCHROME"
    end

    self.text:SetFont(font, size, flags)
    self.timer:SetFont(font, size, flags)

    if shadow then
        self.text:SetShadowOffset(1, -1)
        self.text:SetShadowColor(0, 0, 0, 1)
        self.timer:SetShadowOffset(1, -1)
        self.timer:SetShadowColor(0, 0, 0, 1)
    else
        self.text:SetShadowOffset(0, 0)
        self.text:SetShadowColor(0, 0, 0, 0)
        self.timer:SetShadowOffset(0, 0)
        self.timer:SetShadowColor(0, 0, 0, 0)
    end
    self.shadow = shadow

    self:SetHeight(self.text:GetHeight()+P.Scale(1)*2)
end

local function StatusText_GetStatus(self)
    return self.status
end

local function StatusText_SetStatus(self, status)
    -- print("status: " .. (status or "nil"))
    self.status = status
    if status and self.colors then
        self.text:SetText(L[status])
        self.text:SetTextColor(unpack(self.colors[status]))
        self.timer:SetTextColor(unpack(self.colors[status]))
        self:SetHeight(self.text:GetHeight()+P.Scale(1)*2)
    else
        self:Hide()
    end
end

local function StatusText_SetColors(self, colors)
    self.colors = colors
end

local function StatusText_SetShowTimer(self, show)
    self.showTimer = show
end

local function StatusText_ShowBackground(self, show)
    if show then
        self.bg:Show()
    else
        self.bg:Hide()
    end
end

local function StatusText_SetPosition(self, point, yOffset, justify)
    self:ClearAllPoints()
    self:SetPoint("LEFT", self.parent.widgets.healthBar)
    self:SetPoint("RIGHT", self.parent.widgets.healthBar)
    self:SetPoint(point, self.parent.widgets.healthBar, 0, P.Scale(yOffset))

    self.text:ClearAllPoints()
    self.timer:ClearAllPoints()
    if justify == "justify" then
        self.text:SetPoint("LEFT")
        self.text:SetJustifyH("LEFT")
        self.timer:SetPoint("RIGHT")
        self.timer:SetJustifyH("RIGHT")
        self.bg:SetGradient("HORIZONTAL", CreateColor(0, 0, 0, 0.777), CreateColor(0, 0, 0, 0))
    elseif justify == "left" then
        self.text:SetPoint("LEFT")
        self.text:SetJustifyH("LEFT")
        self.timer:SetPoint("LEFT", self.text, "RIGHT", 2, 0)
        self.timer:SetJustifyH("LEFT")
        self.bg:SetGradient("HORIZONTAL", CreateColor(0, 0, 0, 0.777), CreateColor(0, 0, 0, 0))
    else
        self.text:SetPoint("RIGHT")
        self.text:SetJustifyH("RIGHT")
        self.timer:SetPoint("RIGHT", self.text, "LEFT", -2, 0)
        self.timer:SetJustifyH("RIGHT")
        self.bg:SetGradient("HORIZONTAL", CreateColor(0, 0, 0, 0), CreateColor(0, 0, 0, 0.777))
    end

    self:SetHeight(self.text:GetHeight()+P.Scale(1)*2)
end

local startTimeCache = {}
local function StatusText_ShowTimer(self)
    if not self.showTimer then
        self:HideTimer(true)
        return
    end

    self.timer:Show()

    -- Midnight 12.0.0+: guid may be secret for NPC/boss units
    local showGuid = self.parent.states.guid
    if not (issecretvalue and issecretvalue(showGuid)) then
        if showGuid and not startTimeCache[showGuid] then startTimeCache[showGuid] = GetTime() end
    end

    self.ticker = C_Timer.NewTicker(1, function()
        if not self.parent.states.guid and self.parent.states.unit then -- ElvUI AFK mode
            self.parent.states.guid = UnitGUID(self.parent.states.unit)
        end
        local tickGuid = self.parent.states.guid
        if tickGuid and not (issecretvalue and issecretvalue(tickGuid)) and startTimeCache[tickGuid] then
            self.timer:SetFormattedText(F.FormatTime(GetTime() - startTimeCache[tickGuid]))
        else
            self.timer:SetText("")
        end
    end)
end

local function StatusText_HideTimer(self, reset)
    self.timer:Hide()
    self.timer:SetText("")
    if reset then
        if self.ticker then self.ticker:Cancel() end
        -- Midnight 12.0.0+: guid may be secret for NPC/boss units
        local guid = self.parent.states.guid
        if guid and not (issecretvalue and issecretvalue(guid)) then
            startTimeCache[guid] = nil
        end
    end
end

function I.CreateStatusText(parent)
    local statusText = CreateFrame("Frame", parent:GetName().."StatusText", parent.widgets.indicatorFrame)
    parent.indicators.statusText = statusText
    statusText:SetIgnoreParentAlpha(true)
    statusText:Hide()

    statusText.parent = parent

    statusText.bg = statusText:CreateTexture(nil, "ARTWORK")
    statusText.bg:SetTexture(Cell.vars.whiteTexture)
    -- statusText.bg:SetGradient("HORIZONTAL", CreateColor(0, 0, 0, 0.777), CreateColor(0, 0, 0, 0))
    statusText.bg:SetAllPoints(statusText)

    local text = statusText:CreateFontString(nil, "ARTWORK", "CELL_FONT_STATUS")
    statusText.text = text

    local timer = statusText:CreateFontString(nil, "ARTWORK", "CELL_FONT_STATUS")
    statusText.timer = timer

    statusText.GetStatus = StatusText_GetStatus
    statusText.SetStatus = StatusText_SetStatus
    statusText.SetColors = StatusText_SetColors
    statusText.SetPosition = StatusText_SetPosition
    statusText.SetFont = StatusText_SetFont
    statusText.SetShowTimer = StatusText_SetShowTimer
    statusText.ShowBackground = StatusText_ShowBackground
    statusText.ShowTimer = StatusText_ShowTimer
    statusText.HideTimer = StatusText_HideTimer
end

-------------------------------------------------
-- health text
-------------------------------------------------
local sub = string.sub
local gsub = string.gsub
local find = string.find
local format = string.format
local tinsert = table.insert

local formatter = {
    ["none"] = function()
        return ""
    end,

    -- health
    ["health"] = function(pattern, hideIfEmptyOrFull, health, maxHealth, absorbs, healAbsorbs)
        if hideIfEmptyOrFull and (health == 0 or health == maxHealth) then return "" end
        return pattern:format(health)
    end,
    ["health_short"] = function(pattern, hideIfEmptyOrFull, health, maxHealth, absorbs, healAbsorbs)
        if hideIfEmptyOrFull and (health == 0 or health == maxHealth) then return "" end
        return pattern:format(F.FormatNumber(health))
    end,
    ["health_percent"] = function(pattern, hideIfEmptyOrFull, health, maxHealth, absorbs, healAbsorbs)
        if hideIfEmptyOrFull and (health == 0 or health == maxHealth) then return "" end
        return pattern:format(F.Round(health / maxHealth * 100))
    end,
    ["deficit"] = function(pattern, hideIfEmptyOrFull, health, maxHealth, absorbs, healAbsorbs)
        if hideIfEmptyOrFull and (health == 0 or health == maxHealth) then return "" end
        return pattern:format(health - maxHealth)
    end,
    ["deficit_short"] = function(pattern, hideIfEmptyOrFull, health, maxHealth, absorbs, healAbsorbs)
        if hideIfEmptyOrFull and (health == 0 or health == maxHealth) then return "" end
        return pattern:format(F.FormatNumber(health - maxHealth))
    end,
    ["deficit_percent"] = function(pattern, hideIfEmptyOrFull, health, maxHealth, absorbs, healAbsorbs)
        if hideIfEmptyOrFull and (health == 0 or health == maxHealth) then return "" end
        return pattern:format(F.Round((health - maxHealth) / maxHealth * 100))
    end,

    -- effective health
    ["effective"] = function(pattern, hideIfEmptyOrFull, health, maxHealth, absorbs, healAbsorbs)
        if hideIfEmptyOrFull and (health == 0 or health == maxHealth) and absorbs == 0 and healAbsorbs == 0 then return "" end
        return pattern:format(health + absorbs - healAbsorbs)
    end,
    ["effective_short"] = function(pattern, hideIfEmptyOrFull, health, maxHealth, absorbs, healAbsorbs)
        if hideIfEmptyOrFull and (health == 0 or health == maxHealth) and absorbs == 0 and healAbsorbs == 0 then return "" end
        return pattern:format(F.FormatNumber(health + absorbs - healAbsorbs))
    end,
    ["effective_percent"] = function(pattern, hideIfEmptyOrFull, health, maxHealth, absorbs, healAbsorbs)
        if hideIfEmptyOrFull and (health == 0 or health == maxHealth) and absorbs == 0 and healAbsorbs == 0 then return "" end
        return pattern:format(F.Round((health + absorbs - healAbsorbs) / maxHealth * 100))
    end,

    -- shields
    ["shields"] = function(pattern, health, maxHealth, absorbs, healAbsorbs)
        if absorbs == 0 then return "" end
        return pattern:format(absorbs)
    end,
    ["shields_short"] = function(pattern, health, maxHealth, absorbs, healAbsorbs)
        if absorbs == 0 then return "" end
        return pattern:format(F.FormatNumber(absorbs))
    end,
    ["shields_percent"] = function(pattern, health, maxHealth, absorbs, healAbsorbs)
        if absorbs == 0 then return "" end
        return pattern:format(F.Round(absorbs / maxHealth * 100))
    end,

    -- heal absorbs
    ["healabsorbs"] = function(pattern, health, maxHealth, absorbs, healAbsorbs)
        if healAbsorbs == 0 then return "" end
        return pattern:format(healAbsorbs)
    end,
    ["healabsorbs_short"] = function(pattern, health, maxHealth, absorbs, healAbsorbs)
        if healAbsorbs == 0 then return "" end
        return pattern:format(F.FormatNumber(healAbsorbs))
    end,
    ["healabsorbs_percent"] = function(pattern, health, maxHealth, absorbs, healAbsorbs)
        if healAbsorbs == 0 then return "" end
        return pattern:format(F.Round(healAbsorbs / maxHealth * 100))
    end,
}

local function BuildPattern(config)
    if config.format == "none" then
        return ""
    end

    local prefix
    if config.delimiter == nil then
        prefix = ""
    else
        prefix = "|cffababab" .. config.delimiter .. "|r"
    end

    local suffix = config.format:find("percent$") and "%%" or ""

    if config.color[1] == "class_color" then
        return prefix .. "%s" .. suffix
    else
        return prefix .. "|cff" .. F.ConvertRGBToHEX(F.ConvertRGB_256(unpack(config.color[2]))) .. "%s" .. suffix .. "|r"
    end
end

local function HealthText_SetFormat(self, format)
    self.GetHealth1 = formatter[format.health1.format:gsub("_no_sign$", "")]
    self.GetHealth2 = formatter[format.health2.format:gsub("_no_sign$", "")]
    self.GetShields = formatter[format.shields.format:gsub("_no_sign$", "")]
    self.GetHealAbsorbs = formatter[format.healAbsorbs.format:gsub("_no_sign$", "")]

    self.health1 = BuildPattern(format.health1)
    self.health1_hideIfEmptyOrFull = format.health1.hideIfEmptyOrFull
    self.health2 = BuildPattern(format.health2)
    self.health2_hideIfEmptyOrFull = format.health2.hideIfEmptyOrFull
    self.shields = BuildPattern(format.shields)
    self.healAbsorbs = BuildPattern(format.healAbsorbs)
end

local function HealthText_SetValue(self, health, maxHealth, shields, healAbsorbs)
    -- On Midnight 12.0.0+, health/maxHealth may be secret values in restricted contexts.
    -- Arithmetic (/, *, -, comparison) on secret values causes errors.
    -- AbbreviateNumbers() and FontString:SetText() accept secret values safely.
    -- DO NOT divide, multiply, or compare secret values.
    if Cell.isMidnight and F.IsAuraRestricted and F.IsAuraRestricted() then
        local healthStr = AbbreviateNumbers and AbbreviateNumbers(health) or tostring(health)
        self.text:SetText(healthStr)
        self:SetWidth(self.text:GetStringWidth())
        return
    end

    maxHealth = maxHealth == 0 and 1 or maxHealth

    self.text:SetFormattedText("%s%s%s%s",
        self.GetHealth1(self.health1, self.health1_hideIfEmptyOrFull, health, maxHealth, shields, healAbsorbs),
        self.GetHealth2(self.health2, self.health2_hideIfEmptyOrFull, health, maxHealth, shields, healAbsorbs),
        self.GetShields(self.shields, health, maxHealth, shields, healAbsorbs),
        self.GetHealAbsorbs(self.healAbsorbs, health, maxHealth, shields, healAbsorbs))
    self:SetWidth(self.text:GetStringWidth())
end

local function HealthText_SetFont(self, font, size, outline, shadow)
    font = F.GetFont(font)

    local flags
    if outline == "None" then
        flags = ""
    elseif outline == "Outline" then
        flags = "OUTLINE"
    else
        flags = "OUTLINE,MONOCHROME"
    end

    self.text:SetFont(font, size, flags)

    if shadow then
        self.text:SetShadowOffset(1, -1)
        self.text:SetShadowColor(0, 0, 0, 1)
    else
        self.text:SetShadowOffset(0, 0)
        self.text:SetShadowColor(0, 0, 0, 0)
    end

    self:SetSize(self.text:GetStringWidth(), size)
end

local function HealthText_SetPoint(self, point, relativeTo, relativePoint, x, y)
    self.text:ClearAllPoints()
    if string.find(point, "LEFT$") then
        self.text:SetPoint("LEFT")
    elseif string.find(point, "RIGHT$") then
        self.text:SetPoint("RIGHT")
    else
        self.text:SetPoint("CENTER")
    end
    self:_SetPoint(point, relativeTo, relativePoint, x, y)
    I.JustifyText(self.text, point)
end

local function HealthText_SetColor(self, r, g, b)
    self.text:SetTextColor(r, g, b)
end

local function HealthText_UpdatePreviewColor(self, color)
    -- if color[1] == "class_color" then
        self.text:SetTextColor(F.GetClassColor(Cell.vars.playerClass))
    -- else
    --     self.text:SetTextColor(unpack(color[2]))
    -- end
end

function I.CreateHealthText(parent)
    local healthText = CreateFrame("Frame", parent:GetName().."HealthText", parent.widgets.indicatorFrame)
    parent.indicators.healthText = healthText
    healthText:Hide()

    local text = healthText:CreateFontString(nil, "OVERLAY", "CELL_FONT_STATUS")
    healthText.text = text

    healthText.GetHealth1 = formatter.none
    healthText.GetHealth2 = formatter.none
    healthText.GetShields = formatter.none
    healthText.GetHealAbsorbs = formatter.none

    healthText.SetFont = HealthText_SetFont
    healthText._SetPoint = healthText.SetPoint
    healthText.SetPoint = HealthText_SetPoint
    healthText.SetFormat = HealthText_SetFormat
    healthText.SetValue = HealthText_SetValue
    healthText.SetColor = HealthText_SetColor
    healthText.UpdatePreviewColor = HealthText_UpdatePreviewColor
end

-------------------------------------------------
-- power text
-------------------------------------------------
local function SetPower_Percentage(self, current, max)
    if self.hideIfEmptyOrFull and (current == 0 or current == max) then
        self:Hide()
    else
        self.text:SetFormattedText("%d%%", current/max*100)
        self:SetWidth(self.text:GetStringWidth())
        self:Show()
    end
end

local function SetPower_Number(self, current, max)
    if self.hideIfEmptyOrFull and (current == 0 or current == max) then
        self:Hide()
    else
        self.text:SetText(current)
        self:SetWidth(self.text:GetStringWidth())
        self:Show()
    end
end

local function SetPower_Number_Short(self, current, max)
    if self.hideIfEmptyOrFull and (current == 0 or current == max) then
        self:Hide()
    else
        self.text:SetText(F.FormatNumber(current))
        self:SetWidth(self.text:GetStringWidth())
        self:Show()
    end
end

local function PowerText_SetFont(self, font, size, outline, shadow)
    font = F.GetFont(font)

    local flags
    if outline == "None" then
        flags = ""
    elseif outline == "Outline" then
        flags = "OUTLINE"
    else
        flags = "OUTLINE,MONOCHROME"
    end

    self.text:SetFont(font, size, flags)

    if shadow then
        self.text:SetShadowOffset(1, -1)
        self.text:SetShadowColor(0, 0, 0, 1)
    else
        self.text:SetShadowOffset(0, 0)
        self.text:SetShadowColor(0, 0, 0, 0)
    end

    self:SetSize(self.text:GetStringWidth(), size)
end

local function PowerText_SetPoint(self, point, relativeTo, relativePoint, x, y)
    self.text:ClearAllPoints()
    if string.find(point, "LEFT$") then
        self.text:SetPoint("LEFT")
    elseif string.find(point, "RIGHT$") then
        self.text:SetPoint("RIGHT")
    else
        self.text:SetPoint("CENTER")
    end
    self:_SetPoint(point, relativeTo, relativePoint, x, y)
end

local function PowerText_SetFormat(self, format)
    if format == "percentage" then
        self.SetValue = SetPower_Percentage
    elseif format == "number" then
        self.SetValue = SetPower_Number
    elseif format == "number-short" then
        self.SetValue = SetPower_Number_Short
    end
end

local function PowerText_SetColor(self, r, g, b)
    self.text:SetTextColor(r, g, b)
end

local function PowerText_SetHideIfEmptyOrFull(self, hideIfEmptyOrFull)
    self.hideIfEmptyOrFull = hideIfEmptyOrFull
end

local function PowerText_UpdatePreviewColor(self, color)
    local r, g, b
    if color[1] == "power_color" then
        r, g, b = F.GetPowerColor("player")
    elseif color[1] == "class_color" then
        r, g, b = F.GetClassColor(Cell.vars.playerClass)
    else
        r, g, b = unpack(color[2])
    end
    self.text:SetTextColor(r, g, b)
end

function I.CreatePowerText(parent)
    local powerText = CreateFrame("Frame", parent:GetName().."PowerText", parent.widgets.indicatorFrame)
    parent.indicators.powerText = powerText
    powerText:Hide()

    local text = powerText:CreateFontString(nil, "OVERLAY", "CELL_FONT_STATUS")
    powerText.text = text

    powerText.SetFont = PowerText_SetFont
    powerText._SetPoint = powerText.SetPoint
    powerText.SetPoint = PowerText_SetPoint
    powerText.SetFormat = PowerText_SetFormat
    powerText.SetColor = PowerText_SetColor
    powerText.SetHideIfEmptyOrFull = PowerText_SetHideIfEmptyOrFull
    powerText.UpdatePreviewColor = PowerText_UpdatePreviewColor
    powerText.SetValue = noop
end

-------------------------------------------------
-- role icon
-------------------------------------------------
local ICON_PATH = "Interface\\AddOns\\Cell\\Media\\Roles\\"

local function GetTexCoordsForRole(role)
    if role == "TANK" then
        return 0, 67/256, 67/256, 134/256
    elseif role == "HEALER" then
        return 67/256, 134/256, 0, 67/256
    elseif role == "DAMAGER" then
        return 67/256, 134/256, 67/256, 134/256
    end
end

local function GetTexCoordsForRoleSmall(role)
    if role == "TANK" then
        return 0, 19/64, 22/64, 41/64
    elseif role == "HEALER" then
        return 20/64, 39/64, 1/64, 20/64
    elseif role == "DAMAGER" then
        return 20/64, 39/64, 22/64, 41/64
    end
end

local function RoleIcon_SetRole(self, role)
    self.tex:SetTexCoord(0, 1, 0, 1)
    self.tex:SetVertexColor(1, 1, 1)

    if role == "TANK" or role == "HEALER" or (not self.hideDamager and role == "DAMAGER") then
        if self.texture == "default" then
            self.tex:SetTexture(ICON_PATH .. "Default_" .. role)
        elseif self.texture == "default2" then
            self.tex:SetTexture(ICON_PATH .. "Default2_ROLES")
            self.tex:SetTexCoord(GetTexCoordsForRole(role))
        elseif self.texture == "blizzard" then
            self.tex:SetTexture(ICON_PATH .. "Blizzard_ROLES")
            self.tex:SetTexCoord(GetTexCoordsForRoleSmall(role))
        elseif self.texture == "blizzard2" then
            self.tex:SetTexture(ICON_PATH .. "Blizzard2_ROLES")
            self.tex:SetTexCoord(GetTexCoordsForRole(role))
        elseif self.texture == "blizzard3" then
            self.tex:SetTexture(ICON_PATH .. "Blizzard3_" .. role)
        elseif self.texture == "blizzard4" then
            self.tex:SetTexture(ICON_PATH .. "Blizzard4_" .. role)
        elseif self.texture == "ffxiv" then
            self.tex:SetTexture(ICON_PATH .. "FFXIV_" .. role)
        elseif self.texture == "miirgui" then
            self.tex:SetTexture(ICON_PATH .. "MiirGui_" .. role)
        elseif self.texture == "mattui" then
            self.tex:SetTexture(ICON_PATH .. "MattUI_ROLES")
            self.tex:SetTexCoord(GetTexCoordsForRoleSmall(role))
        elseif self.texture == "custom" then
            self.tex:SetTexture(self[role])
        end
        self:Show()
    elseif role == "VEHICLE-ROOT" then
        self.tex:SetTexture(ICON_PATH .. "VEHICLE")
        self:Show()
    elseif role == "VEHICLE" then
        self.tex:SetTexture(ICON_PATH .. "VEHICLE")
        self.tex:SetVertexColor(0.6, 0.6, 1)
        self:Show()
    else
        self:Hide()
    end
end

local function RoleIcon_SetRoleTexture(self, t)
    self.texture = t[1]
    self.TANK = t[2]
    self.HEALER = t[3]
    self.DAMAGER = t[4]
end

local function RoleIcon_HideDamager(self, hide)
    self.hideDamager = hide
end

local function RoleIcon_UpdatePixelPerfect(self)
    P.Resize(self)
    P.Repoint(self)
end

function I.CreateRoleIcon(parent)
    local roleIcon = CreateFrame("Frame", parent:GetName().."RoleIcon", parent.widgets.indicatorFrame)
    parent.indicators.roleIcon = roleIcon
    -- roleIcon:SetPoint("TOPLEFT", indicatorFrame)
    -- roleIcon:SetSize(11, 11)

    roleIcon.tex = roleIcon:CreateTexture(nil, "ARTWORK")
    roleIcon.tex:SetAllPoints()

    roleIcon.SetRole = RoleIcon_SetRole
    roleIcon.SetRoleTexture = RoleIcon_SetRoleTexture
    roleIcon.HideDamager = RoleIcon_HideDamager
    roleIcon.UpdatePixelPerfect = RoleIcon_UpdatePixelPerfect
end

-------------------------------------------------
-- party assignment icon
-------------------------------------------------
function I.CreatePartyAssignmentIcon(parent)
    local partyAssignmentIcon = parent.widgets.indicatorFrame:CreateTexture(parent:GetName().."PartyAssignmentIcon", "ARTWORK", nil, -7)
    parent.indicators.partyAssignmentIcon = partyAssignmentIcon
    partyAssignmentIcon:Hide()

    function partyAssignmentIcon:UpdateAssignment(unit)
        if GetPartyAssignment("MAINTANK", unit) then
            partyAssignmentIcon:SetTexture("Interface\\GroupFrame\\UI-Group-MainTankIcon")
            partyAssignmentIcon:Show()
        elseif GetPartyAssignment("MAINASSIST", unit) then
            partyAssignmentIcon:SetTexture("Interface\\GroupFrame\\UI-Group-MainAssistIcon")
            partyAssignmentIcon:Show()
        else
            partyAssignmentIcon:Hide()
        end
    end

    function partyAssignmentIcon:UpdatePixelPerfect()
        P.Resize(partyAssignmentIcon)
        P.Repoint(partyAssignmentIcon)
    end
end

-------------------------------------------------
-- leader icon
-------------------------------------------------
function I.CreateLeaderIcon(parent)
    local leaderIcon = parent.widgets.indicatorFrame:CreateTexture(parent:GetName().."LeaderIcon", "ARTWORK", nil, -7)
    parent.indicators.leaderIcon = leaderIcon
    -- leaderIcon:SetPoint("TOPLEFT", roleIcon, "BOTTOM")
    -- leaderIcon:SetPoint("TOPLEFT", 0, -11)
    -- leaderIcon:SetSize(11, 11)
    leaderIcon:Hide()

    function leaderIcon:SetIcon(isLeader, isAssistant)
        if isLeader then
            leaderIcon:SetTexture("Interface\\GroupFrame\\UI-Group-LeaderIcon")
            leaderIcon:Show()
        elseif isAssistant then
            leaderIcon:SetTexture("Interface\\GroupFrame\\UI-Group-AssistantIcon")
            leaderIcon:Show()
        else
            leaderIcon:Hide()
        end
    end

    function leaderIcon:UpdatePixelPerfect()
        P.Resize(leaderIcon)
        P.Repoint(leaderIcon)
    end
end

-------------------------------------------------
-- ready check icon
-------------------------------------------------
-- READY_CHECK_WAITING_TEXTURE = "Interface\\RaidFrame\\ReadyCheck-Waiting"
-- READY_CHECK_READY_TEXTURE = "Interface\\RaidFrame\\ReadyCheck-Ready"
-- READY_CHECK_NOT_READY_TEXTURE = "Interface\\RaidFrame\\ReadyCheck-NotReady"
-- READY_CHECK_AFK_TEXTURE = "Interface\\RaidFrame\\ReadyCheck-NotReady"
-- ↓↓↓ since 10.1.5
-- READY_CHECK_WAITING_TEXTURE = "UI-LFG-PendingMark"
-- READY_CHECK_READY_TEXTURE = "UI-LFG-ReadyMark"
-- READY_CHECK_NOT_READY_TEXTURE = "UI-LFG-DeclineMark"
-- READY_CHECK_AFK_TEXTURE = "UI-LFG-DeclineMark"

local READY_CHECK_STATUS = {
    ready = {t = "Interface\\AddOns\\Cell\\Media\\Icons\\readycheck-ready", c = {0, 1, 0, 1}},
    waiting = {t = "Interface\\AddOns\\Cell\\Media\\Icons\\readycheck-waiting", c = {1, 1, 0, 1}},
    notready = {t = "Interface\\AddOns\\Cell\\Media\\Icons\\readycheck-notready", c = {1, 0, 0, 1}},
}

function I.CreateReadyCheckIcon(parent)
    local readyCheckIcon = CreateFrame("Frame", parent:GetName().."ReadyCheckIcon", parent.widgets.indicatorFrame)
    parent.indicators.readyCheckIcon = readyCheckIcon
    readyCheckIcon:Hide()
    readyCheckIcon:SetIgnoreParentAlpha(true)

    readyCheckIcon.tex = readyCheckIcon:CreateTexture(nil, "ARTWORK")
    readyCheckIcon.tex:SetAllPoints(readyCheckIcon)

    function readyCheckIcon:SetStatus(status)
        readyCheckIcon.tex:SetTexture(READY_CHECK_STATUS[status].t)
        -- readyCheckIcon.tex:SetAtlas(READY_CHECK_STATUS[status].t)
        readyCheckIcon:Show()

    end
end

-------------------------------------------------
-- aggro border
-------------------------------------------------
function I.CreateAggroBorder(parent)
    local aggroBorder = CreateFrame("Frame", parent:GetName().."AggroBorder", parent, "BackdropTemplate")
    parent.indicators.aggroBorder = aggroBorder
    P.Point(aggroBorder, "TOPLEFT", parent, "TOPLEFT", 1, -1)
    P.Point(aggroBorder, "BOTTOMRIGHT", parent, "BOTTOMRIGHT", -1, 1)
    aggroBorder:Hide()

    local top = aggroBorder:CreateTexture(nil, "BORDER")
    local bottom = aggroBorder:CreateTexture(nil, "BORDER")
    local left = aggroBorder:CreateTexture(nil, "BORDER")
    local right = aggroBorder:CreateTexture(nil, "BORDER")

    top:SetTexture(Cell.vars.whiteTexture)
    top:SetPoint("TOPLEFT")
    top:SetPoint("TOPRIGHT")
    top:SetHeight(5)

    bottom:SetTexture(Cell.vars.whiteTexture)
    bottom:SetPoint("BOTTOMLEFT")
    bottom:SetPoint("BOTTOMRIGHT")
    bottom:SetHeight(5)

    left:SetTexture(Cell.vars.whiteTexture)
    left:SetPoint("TOPLEFT")
    left:SetPoint("BOTTOMLEFT")
    left:SetWidth(5)

    right:SetTexture(Cell.vars.whiteTexture)
    right:SetPoint("TOPRIGHT")
    right:SetPoint("BOTTOMRIGHT")
    right:SetWidth(5)

    top:SetGradient("VERTICAL", CreateColor(1, 0.1, 0.1, 0.2), CreateColor(1, 0.1, 0.1, 1))
    bottom:SetGradient("VERTICAL", CreateColor(1, 0.1, 0.1, 1), CreateColor(1, 0.1, 0.1, 0.2))
    left:SetGradient("HORIZONTAL", CreateColor(1, 0.1, 0.1, 1), CreateColor(1, 0.1, 0.1, 0.2))
    right:SetGradient("HORIZONTAL", CreateColor(1, 0.1, 0.1, 0.2), CreateColor(1, 0.1, 0.1, 1))

    function aggroBorder:ShowAggro(r, g, b)
        top:SetGradient("VERTICAL", CreateColor(r, g, b, 0.2), CreateColor(r, g, b, 1))
        bottom:SetGradient("VERTICAL", CreateColor(r, g, b, 1), CreateColor(r, g, b, 0.2))
        left:SetGradient("HORIZONTAL", CreateColor(r, g, b, 1), CreateColor(r, g, b, 0.2))
        right:SetGradient("HORIZONTAL", CreateColor(r, g, b, 0.2), CreateColor(r, g, b, 1))
        aggroBorder:Show()
    end

    function aggroBorder:SetThickness(n)
        top:SetHeight(n)
        bottom:SetHeight(n)
        left:SetWidth(n)
        right:SetWidth(n)
    end

    function aggroBorder:UpdatePixelPerfect()
        P.Repoint(aggroBorder)
    end
end

-------------------------------------------------
-- aggro blink
-------------------------------------------------
function I.CreateAggroBlink(parent)
    local aggroBlink = CreateFrame("Frame", parent:GetName().."AggroBlink", parent.widgets.indicatorFrame, "BackdropTemplate")
    parent.indicators.aggroBlink = aggroBlink
    -- aggroBlink:SetPoint("TOPLEFT")
    -- aggroBlink:SetSize(10, 10)
    aggroBlink:SetBackdrop({bgFile = Cell.vars.whiteTexture, edgeFile = Cell.vars.whiteTexture, edgeSize = P.Scale(1)})
    aggroBlink:SetBackdropColor(1, 0, 0, 1)
    aggroBlink:SetBackdropBorderColor(0, 0, 0, 1)
    aggroBlink:Hide()

    local blink = aggroBlink:CreateAnimationGroup()
    aggroBlink.blink = blink
    blink:SetLooping("REPEAT")

    local alpha = blink:CreateAnimation("Alpha")
    blink.alpha = alpha
    alpha:SetFromAlpha(1)
    alpha:SetToAlpha(0)
    alpha:SetDuration(0.5)

    aggroBlink:SetScript("OnShow", function(self)
        self.blink:Play()
    end)

    aggroBlink:SetScript("OnHide", function(self)
        self.blink:Stop()
    end)

    function aggroBlink:ShowAggro(r, g, b)
        aggroBlink:SetBackdropColor(r, g, b)
        aggroBlink:Show()
    end

    function aggroBlink:UpdatePixelPerfect()
        P.Resize(aggroBlink)
        P.Repoint(aggroBlink)
        aggroBlink:SetBackdrop({bgFile = Cell.vars.whiteTexture, edgeFile = Cell.vars.whiteTexture, edgeSize = P.Scale(1)})
        aggroBlink:SetBackdropColor(1, 0, 0, 1)
        aggroBlink:SetBackdropBorderColor(0, 0, 0, 1)
    end
end

-------------------------------------------------
-- shield bar
-------------------------------------------------
local function ShieldBar_SetHorizontalValue(bar, percent)
    local maxWidth = bar.parentHealthBar:GetWidth()
    local barWidth
    if percent >= 1 then
        barWidth = maxWidth
    else
        barWidth = maxWidth * percent
    end
    bar:SetWidth(max(barWidth, 3))
end

local function ShieldBar_SetVerticalValue(bar, percent)
    local maxHeight = bar.parentHealthBar:GetHeight()
    local barHeight
    if percent >= 1 then
        barHeight = maxHeight
    else
        barHeight = maxHeight * percent
    end
    bar:SetHeight(max(barHeight, 3))
end

local function ShieldBar_SetPoint(bar, point, anchorTo, anchorPoint, x, y)
    -- if point == "HEALTH_BAR_HORIZONTAL" then
    --     bar:_SetPoint("TOPLEFT", b.widgets.healthBar)
    --     bar:_SetPoint("BOTTOMLEFT", b.widgets.healthBar)
    --     bar.SetValue = ShieldBar_SetHorizontalValue
    -- elseif point == "HEALTH_BAR_VERTICAL" then
    --     bar:_SetPoint("TOPLEFT", b.widgets.healthBar)
    --     bar:_SetPoint("BOTTOMLEFT", b.widgets.healthBar)
    --     bar.SetValue = ShieldBar_SetVerticalValue
    if point == "HEALTH_BAR" then
        bar:_SetPoint("TOPLEFT", bar.parentHealthBar, P.Scale(-1), P.Scale(1))
        bar:_SetPoint("BOTTOMLEFT", bar.parentHealthBar, P.Scale(-1), P.Scale(-1))
        bar.SetValue = ShieldBar_SetHorizontalValue
    else
        bar:_SetPoint(point, anchorTo, anchorPoint, x, y)
        bar.SetValue = ShieldBar_SetHorizontalValue
    end
end

function I.CreateShieldBar(parent)
    local shieldBar = CreateFrame("Frame", parent:GetName().."ShieldBar", parent.widgets.indicatorFrame, "BackdropTemplate")
    parent.indicators.shieldBar = shieldBar
    -- shieldBar:SetSize(4, 4)
    shieldBar:Hide()
    shieldBar:SetBackdrop({edgeFile=Cell.vars.whiteTexture, edgeSize=P.Scale(1)})
    shieldBar:SetBackdropBorderColor(0, 0, 0, 1)

    local tex = shieldBar:CreateTexture(nil, "BORDER", nil, -7)
    tex:SetAllPoints()

    shieldBar._SetPoint = shieldBar.SetPoint
    shieldBar.SetPoint = ShieldBar_SetPoint
    shieldBar.SetValue = ShieldBar_SetHorizontalValue

    shieldBar.parentHealthBar = parent.widgets.healthBar

    function shieldBar:SetColor(r, g, b, a)
        tex:SetColorTexture(r, g, b, a)
    end

    function shieldBar:UpdatePixelPerfect()
        P.Resize(shieldBar)
        P.Repoint(shieldBar)
        P.Reborder(shieldBar)
    end
end

-------------------------------------------------
-- health threshold
-------------------------------------------------
function I.CreateHealthThresholds(parent)
    local healthThresholds = CreateFrame("Frame", parent:GetName().."HealthThresholds", parent.widgets.highLevelFrame)
    parent.indicators.healthThresholds = healthThresholds
    healthThresholds:SetAllPoints(parent.widgets.healthBar)

    healthThresholds.tex = healthThresholds:CreateTexture(nil, "ARTWORK")

    function healthThresholds:SetThickness(thickness)
        healthThresholds.thickness = thickness
        P.Size(healthThresholds.tex, thickness, thickness)
    end

    function healthThresholds:SetOrientation(orientation)
        healthThresholds.orientation = orientation
        healthThresholds.tex:ClearAllPoints()
        if orientation == "horizontal" then
            healthThresholds.tex:SetPoint("TOP")
            healthThresholds.tex:SetPoint("BOTTOM")
        else
            healthThresholds.tex:SetPoint("LEFT")
            healthThresholds.tex:SetPoint("RIGHT")
        end
    end

    function healthThresholds:CheckThreshold(percent)
        local found
        for i, t in ipairs(Cell.vars.healthThresholds) do
            if percent < t[1] then
                found = i
                break
            end
        end
        if found then
            if healthThresholds.orientation == "horizontal" then
                healthThresholds.tex:SetPoint("LEFT", Cell.vars.healthThresholds[found][1] * parent.widgets.healthBar:GetWidth(), 0)
            else
                healthThresholds.tex:SetPoint("BOTTOM", 0, Cell.vars.healthThresholds[found][1] * parent.widgets.healthBar:GetHeight())
            end
            healthThresholds.tex:SetColorTexture(unpack(Cell.vars.healthThresholds[found][2]))
            healthThresholds:Show()
        else
            healthThresholds:Hide()
        end
    end

    if parent == CellIndicatorsPreviewButton then
        healthThresholds.tex:Hide()

        function healthThresholds:UpdateThresholdsPreview()
            for i, t in ipairs(Cell.vars.healthThresholds) do
                healthThresholds[i] = healthThresholds[i] or healthThresholds:CreateTexture(nil, "ARTWORK")
                P.Size(healthThresholds[i], healthThresholds.thickness, healthThresholds.thickness)
                healthThresholds[i]:SetColorTexture(unpack(t[2]))
                -- healthThresholds[i]:SetBlendMode("ADD")

                healthThresholds[i]:ClearAllPoints()
                if healthThresholds.orientation == "horizontal" then
                    healthThresholds[i]:SetPoint("TOP")
                    healthThresholds[i]:SetPoint("BOTTOM")
                    healthThresholds[i]:SetPoint("LEFT", t[1] * parent.widgets.healthBar:GetWidth(), 0)
                else
                    healthThresholds[i]:SetPoint("LEFT")
                    healthThresholds[i]:SetPoint("RIGHT")
                    healthThresholds[i]:SetPoint("BOTTOM", 0, t[1] * parent.widgets.healthBar:GetHeight())
                end
                healthThresholds[i]:Show()
            end
            -- hide unused
            for i = #Cell.vars.healthThresholds+1, #healthThresholds do
                if healthThresholds[i] then
                    healthThresholds[i]:Hide()
                end
            end
        end
    end
end

-- sort and save
function I.UpdateHealthThresholds()
    Cell.vars.healthThresholds = Cell.vars.currentLayoutTable.indicators[Cell.defaults.indicatorIndices.healthThresholds].thresholds
    F.Sort(Cell.vars.healthThresholds, 1, "ascending")
end

-------------------------------------------------
-- power word : shield 怀旧服API太落后，蛋疼！
-------------------------------------------------
function I.CreatePowerWordShield(parent)
    local powerWordShield = CreateFrame("Frame", parent:GetName().."PowerWordShield", parent.widgets.indicatorFrame, "BackdropTemplate")
    parent.indicators.powerWordShield = powerWordShield
    powerWordShield:Hide()

    powerWordShield:SetBackdrop({bgFile = [[Interface\AddOns\Cell\Media\Shapes\circle_filled.tga]]})
    powerWordShield:SetBackdropColor(0, 0, 0, 0.75)

    --! shield amount
    local shieldAmount = CreateFrame("Cooldown", parent:GetName().."PowerWordShieldAmount", powerWordShield)
    -- shieldAmount:SetAllPoints(powerWordShield)
    shieldAmount:SetSwipeTexture([[Interface\AddOns\Cell\Media\Shapes\circle_filled.tga]])
    -- shieldAmount:SetSwipeTexture(Cell.vars.whiteTexture)
    shieldAmount:SetSwipeColor(1, 1, 0)
    shieldAmount.noCooldownCount = true -- disable omnicc
    shieldAmount:SetHideCountdownNumbers(true)

    --! innerBG
    local innerBG = shieldAmount:CreateTexture(nil, "OVERLAY")
    innerBG:SetPoint("CENTER")
    innerBG:SetTexture([[Interface\AddOns\Cell\Media\Shapes\circle_filled.tga]], "CLAMP", "CLAMP", "TRILINEAR")
    innerBG:SetVertexColor(0, 0, 0, 1)

    --! shield duration
    local shieldCooldown = CreateFrame("Cooldown", parent:GetName().."PowerWordShieldDuration", powerWordShield)
    shieldCooldown:SetFrameLevel(shieldAmount:GetFrameLevel() + 1)
    -- shieldCooldown:SetPoint("CENTER")
    shieldCooldown:SetPoint("TOPLEFT", P.Scale(1), P.Scale(-1))
    shieldCooldown:SetPoint("BOTTOMRIGHT", P.Scale(-1), P.Scale(1))
    shieldCooldown:SetSwipeTexture([[Interface\AddOns\Cell\Media\Shapes\circle_filled.tga]])
    shieldCooldown:SetSwipeColor(0, 1, 0)
    shieldCooldown.noCooldownCount = true -- disable omnicc
    shieldCooldown:SetHideCountdownNumbers(true)
    shieldCooldown:Hide()
    shieldCooldown:SetScript("OnCooldownDone", function()
        shieldCooldown:Hide()
    end)

    --! weakened soul duration
    local weakendedSoulCooldown = CreateFrame("Cooldown", parent:GetName().."WeakenedSoulDuration", powerWordShield)
    weakendedSoulCooldown:SetFrameLevel(shieldAmount:GetFrameLevel() + 2)
    -- weakendedSoulCooldown:SetPoint("CENTER")
    weakendedSoulCooldown:SetPoint("TOPLEFT", P.Scale(1), P.Scale(-1))
    weakendedSoulCooldown:SetPoint("BOTTOMRIGHT", P.Scale(-1), P.Scale(1))
    weakendedSoulCooldown:SetSwipeTexture([[Interface\AddOns\Cell\Media\Shapes\circle_filled.tga]])
    weakendedSoulCooldown:SetSwipeColor(1, 0, 0)
    weakendedSoulCooldown.noCooldownCount = true -- disable omnicc
    weakendedSoulCooldown:SetHideCountdownNumbers(true)
    weakendedSoulCooldown:Hide()
    weakendedSoulCooldown:SetScript("OnCooldownDone", function()
        weakendedSoulCooldown:Hide()
    end)

    powerWordShield._SetSize = powerWordShield.SetSize
    function powerWordShield:SetSize(width, height)
        powerWordShield.size = width
        powerWordShield:UpdatePixelPerfect()
    end

    function powerWordShield:UpdatePixelPerfect()
        local size = powerWordShield.size
        if not size then return end

        powerWordShield:_SetSize(P.Scale(size), P.Scale(size))
        innerBG:SetSize(P.Scale(ceil(size/2)+2), P.Scale(ceil(size/2)+2))

        shieldCooldown:SetSize(P.Scale(ceil(size/2)), P.Scale(ceil(size/2)))
        weakendedSoulCooldown:SetSize(P.Scale(ceil(size/2)), P.Scale(ceil(size/2)))

        shieldAmount:SetPoint("TOPLEFT", P.Scale(1), P.Scale(-1))
        shieldAmount:SetPoint("BOTTOMRIGHT", P.Scale(-1), P.Scale(1))
    end

    function powerWordShield:SetShape(shape)
        local tex = "Interface\\AddOns\\Cell\\Media\\Shapes\\"..shape.."_filled.tga"
        powerWordShield:SetBackdrop({bgFile = tex})
        powerWordShield:SetBackdropColor(0, 0, 0, 0.75)
        shieldAmount:SetSwipeTexture(tex)
        innerBG:SetTexture(tex, "CLAMP", "CLAMP", "TRILINEAR")
        shieldCooldown:SetSwipeTexture(tex)
        weakendedSoulCooldown:SetSwipeTexture(tex)
    end

    function powerWordShield:UpdateShield(value, max, resetMax)
        if resetMax then
            powerWordShield.max = nil
        elseif max then
            powerWordShield.max = max
        end
        -- print("remain:", value, "max:", powerWordShield.max, resetMax and "(reset)" or "")

        shieldCooldown:ClearAllPoints()
        weakendedSoulCooldown:ClearAllPoints()

        if value > 0 and powerWordShield.max then
            local progress = (powerWordShield.max - value) / powerWordShield.max
            local start = GetTime() - (progress * 100)
            shieldAmount:SetCooldown(start, 100)
            shieldAmount:Pause()
            shieldCooldown:SetPoint("CENTER")
            weakendedSoulCooldown:SetPoint("CENTER")
        else
            shieldCooldown:SetPoint("TOPLEFT", P.Scale(1), P.Scale(-1))
            shieldCooldown:SetPoint("BOTTOMRIGHT", P.Scale(-1), P.Scale(1))
            weakendedSoulCooldown:SetPoint("TOPLEFT", P.Scale(1), P.Scale(-1))
            weakendedSoulCooldown:SetPoint("BOTTOMRIGHT", P.Scale(-1), P.Scale(1))
        end
    end

    local function Update()
        if not (shieldCooldown:IsShown() or weakendedSoulCooldown:IsShown()) then
            powerWordShield:Hide()
        end
    end

    function powerWordShield:SetShieldCooldown(start, duration)
        if start and duration then
            powerWordShield:Show()
            shieldCooldown:Show()
            shieldCooldown:SetCooldown(start, duration)
        else
            shieldCooldown:Hide()
            shieldAmount:Hide()
            Update()
        end
    end

    function powerWordShield:SetWeakenedSoulCooldown(start, duration, isMine)
        if start and duration then
            powerWordShield:Show()
            weakendedSoulCooldown:Show()
            weakendedSoulCooldown:SetCooldown(start, duration)
        else
            weakendedSoulCooldown:Hide()
            Update()
        end
    end
end

-------------------------------------------------
-- crowd controls
-------------------------------------------------
function I.CreateCrowdControls(parent)
    local crowdControls = CreateFrame("Frame", parent:GetName().."CrowdControlsParent", parent.widgets.indicatorFrame)
    parent.indicators.crowdControls = crowdControls
    crowdControls:Hide()

    crowdControls._SetSize = crowdControls.SetSize
    crowdControls.SetSize = I.Cooldowns_SetSize
    crowdControls.SetBorder = I.Cooldowns_SetBorder
    crowdControls.UpdateSize = I.Cooldowns_UpdateSize_WithSpacing
    crowdControls.ShowDuration = I.Cooldowns_ShowDuration
    crowdControls.SetOrientation = I.Cooldowns_SetOrientation_WithSpacing
    crowdControls.SetFont = I.Cooldowns_SetFont
    crowdControls.UpdatePixelPerfect = I.Cooldowns_UpdatePixelPerfect

    for i = 1, 3 do
        local frame = I.CreateAura_BorderIcon(parent:GetName().."CrowdControl"..i, crowdControls, 2)
        tinsert(crowdControls, frame)
        -- frame:SetScript("OnShow", crowdControls.UpdateSize)
        -- frame:SetScript("OnHide", crowdControls.UpdateSize)
    end
end

--------------------------------------------------
-- Combat Icon
--------------------------------------------------
local function CombatIcon_UpdatePixelPerfect(self)
    P.Resize(self)
    P.Repoint(self)
end

function I.CreateCombatIcon(parent)
    local combatIcon = CreateFrame("Frame", parent:GetName() .. "CombatIcon", parent.widgets.indicatorFrame)
    parent.indicators.combatIcon = combatIcon
    combatIcon.root = parent
    combatIcon:Hide()

    combatIcon.tex = combatIcon:CreateTexture(nil, "ARTWORK", nil, 0)
    combatIcon.tex:SetAllPoints()
    combatIcon.tex:SetTexture("Interface\\AddOns\\Cell\\Media\\Icons\\combat", nil, nil, "TRILINEAR")
    -- combatIcon.tex:SetAtlas("combat_swords-dynamicIcon")

    combatIcon.flashTex = combatIcon:CreateTexture(nil, "ARTWORK", nil, -5)
    combatIcon.flashTex:SetAllPoints()
    combatIcon.flashTex:SetTexture("Interface\\AddOns\\Cell\\Media\\Icons\\combat_glow", nil, nil, "TRILINEAR")
    -- combatIcon.flashTex:SetAtlas("combat_swords-flash")
    combatIcon.flashTex:SetBlendMode("ADD")

    A.CreateBlinkAnimation(combatIcon.flashTex, nil, true)

    combatIcon:SetScript("OnEvent", CombatIcon_OnEvent)

    combatIcon.UpdatePixelPerfect = CombatIcon_UpdatePixelPerfect

    return combatIcon
end

-------------------------------------------------
-- missing buffs
-------------------------------------------------
function I.CreateMissingBuffs(parent)
    local missingBuffs = CreateFrame("Frame", parent:GetName().."MissingBuffParent", parent.widgets.indicatorFrame)
    parent.indicators.missingBuffs = missingBuffs
    missingBuffs:Hide()

    missingBuffs._SetSize = missingBuffs.SetSize
    missingBuffs.SetSize = I.Cooldowns_SetSize
    missingBuffs.UpdateSize = I.Cooldowns_UpdateSize
    missingBuffs.SetOrientation = I.Cooldowns_SetOrientation
    missingBuffs.UpdatePixelPerfect = I.Cooldowns_UpdatePixelPerfect

    for i = 1, 3 do
        local name = parent:GetName().."MissingBuff"..i
        local frame = I.CreateAura_BarIcon(name, missingBuffs)
        tinsert(missingBuffs, frame)
        frame:HookScript("OnSizeChanged", function()
            LCG.ButtonGlow_Start(frame)
        end)
    end
end

local missingBuffsEnabled = false
function I.EnableMissingBuffs(enabled)
    missingBuffsEnabled = enabled

    if enabled and CellDB["tools"]["buffTracker"][1] then
        CellBuffTrackerFrame:GROUP_ROSTER_UPDATE(true)
    end
end

function I.UpdateMissingBuffsFilters(filters, noUpdate)
    if filters then missingBuffsFilters = filters end

    if not noUpdate and missingBuffsEnabled and CellDB["tools"]["buffTracker"][1] then
        CellBuffTrackerFrame:GROUP_ROSTER_UPDATE(true)
    end
end

local function HideMissingBuffs(b)
    for i = 1, 3 do
        b.indicators.missingBuffs[i]:Hide()
    end
end

local missingBuffsCounter = {}
function I.HideMissingBuffs(unit, force)
    if not (missingBuffsEnabled or force) then return end
    missingBuffsCounter[unit] = nil
    F.HandleUnitButton("unit", unit, HideMissingBuffs)
end

local function ShowMissingBuff(b, index, icon)
    b.indicators.missingBuffs:UpdateSize(index)

    local f = b.indicators.missingBuffs[index]
    f:SetCooldown(0, 0, nil, icon, 0)
    LCG.ButtonGlow_Start(f)
end

function I.ShowMissingBuff(unit, icon)
    if not missingBuffsEnabled then return end

    missingBuffsCounter[unit] = (missingBuffsCounter[unit] or 0) + 1
    if missingBuffsCounter[unit] > 3 then return end

    F.HandleUnitButton("unit", unit, ShowMissingBuff, missingBuffsCounter[unit], icon)
end