local _, Cell = ...
local L = Cell.L
local F = Cell.funcs
local I = Cell.iFuncs
local P = Cell.pixelPerfectFuncs
local LCG = LibStub("LibCustomGlow-1.0")
local LGI = LibStub:GetLibrary("LibGroupInfo")
local A = Cell.animations

local UnitIsConnected = UnitIsConnected
local UnitIsVisible = UnitIsVisible
local UnitIsDeadOrGhost = UnitIsDeadOrGhost
local UnitIsUnit = UnitIsUnit
local UnitIsPlayer = UnitIsPlayer
local UnitGUID = UnitGUID
local UnitClassBase = UnitClassBase
local UnitLevel = UnitLevel
local IsInGroup = IsInGroup
local IsInRaid = IsInRaid

local sort, tinsert, tconcat = table.sort, table.insert, table.concat

---------------------------------------------------------------------
-- data
---------------------------------------------------------------------
local buffs = {}
local requiredBuffs = {}
local requiredByEveryone = {}
local available = {}
local unaffected = {}

if Cell.isRetail then
    buffs = {
        stamina = {
            tag = ITEM_MOD_STAMINA_SHORT, -- Stamina
            icon = 135987,
            order = 1,
            provider = {
                PRIEST = {id = 21562, level = 6}, -- Power Word: Fortitude - 真言术：韧
            }
        },
        versatility = {
            tag = STAT_VERSATILITY, -- Versatility
            icon = 136078,
            order = 2,
            provider = {
                DRUID = {id = 1126, level = 9}, -- Mark of the Wild - 野性印记
            }
        },
        mastery = {
            tag = STAT_MASTERY, -- Mastery
            icon = 4630367,
            order = 3,
            provider = {
                SHAMAN = {id = 462854, level = 16}, -- Skyfury - 天怒
            }
        },
        intellect = {
            tag = ITEM_MOD_INTELLECT_SHORT, -- Intellect
            icon = 135932,
            order = 4,
            provider = {
                MAGE = {id = 1459, level = 8}, -- Arcane Brilliance - 奥术智慧
            }
        },
        attackPower = {
            tag = RAID_BUFF_3, -- Attack Power
            icon = 132333,
            order = 5,
            provider = {
                WARRIOR = {id = 6673, level = 10}, -- Battle Shout - 战斗怒吼
            }
        },
        movement = {
            tag = TUTORIAL_TITLE2, -- Movement
            icon = 4622448,
            order = 6,
            provider = {
                EVOKER = {id = 364342, level = 30}, -- Blessing of the Bronze - 青铜龙的祝福
            }
        }
    }

    requiredBuffs = {
        [250] = "attackPower", -- Blood
        [251] = "attackPower", -- Frost
        [252] = "attackPower", -- Unholy

        [577] = "attackPower", -- Havoc
        [581] = "attackPower", -- Vengeance

        [102] = "intellect", -- Balance
        [103] = "attackPower", -- Feral
        [104] = "attackPower", -- Guardian
        [105] = "intellect", -- Restoration

        [1467] = "intellect", -- Devastation
        [1468] = "intellect", -- Preservation

        [253] = "attackPower", -- Beast Mastery
        [254] = "attackPower", -- Marksmanship
        [255] = "attackPower", -- Survival

        [62] = "intellect", -- Arcane
        [63] = "intellect", -- Fire
        [64] = "intellect", -- Frost

        [268] = "attackPower", -- Brewmaster
        [269] = "attackPower", -- Windwalker
        [270] = "intellect", -- Mistweaver

        [65] = "intellect", -- Holy
        [66] = "attackPower", -- Protection
        [70] = "attackPower", -- Retribution

        [256] = "intellect", -- Discipline
        [257] = "intellect", -- Holy
        [258] = "intellect", -- Shadow

        [259] = "attackPower", -- Assassination
        [260] = "attackPower", -- Outlaw
        [261] = "attackPower", -- Subtlety

        [262] = "intellect", -- Elemental
        [263] = "attackPower", -- Enhancement
        [264] = "intellect", -- Restoration

        [265] = "intellect", -- Affliction
        [266] = "intellect", -- Demonology
        [267] = "intellect", -- Destruction

        [71] = "attackPower", -- Arms
        [72] = "attackPower", -- Fury
        [73] = "attackPower", -- Protection
    }

    requiredByEveryone = {
        stamina = true,
        versatility = true,
        mastery = true,
        movement = true,
    }

    available = {
        stamina = false,
        versatility = false,
        mastery = false,
        intellect = false,
        attackPower = false,
        movement = false,
    }

    unaffected = {
        stamina = {},
        versatility = {},
        mastery = {},
        intellect = {},
        attackPower = {},
        movement = {},
    }

elseif Cell.isMists then
    buffs = {
        stamina = {
            tag = RAID_BUFF_2, -- Stamina
            icon = 135987,
            order = 1,
            provider = {
                PRIEST = {id = 21562, level = 22}, -- Power Word: Fortitude
                WARLOCK = {id = 109773, level = 82}, -- Dark Intent
                WARRIOR = {id = 469, level = 68}, -- Commanding Shout
            },
        },
        stats = {
            tag = RAID_BUFF_1, -- Stats
            icon = 136078,
            order = 2,
            provider = {
                DRUID = {id = 1126, level = 62}, -- Mark of the Wild
                MONK = {id = 115921, level = 22}, -- Legacy of the Emperor
                PALADIN = {id = 20217, level = 30}, -- Blessing of Kings
            }
        },
        spellPower = {
            tag = RAID_BUFF_5, -- Spell Power
            icon = 135932,
            order = 3,
            provider = {
                MAGE = {id = {1459, 61316}, level = 58}, -- Arcane Brilliance / Dalaran Brilliance
                SHAMAN = {id = 77747, level = 40}, -- Burning Wrath
                WARLOCK = {id = 109773, level = 82}, -- Dark Intent
            }
        },
        attackPower = {
            tag = RAID_BUFF_3, -- Attack Power
            icon = 132333,
            order = 4,
            provider = {
                DEATHKNIGHT = {id = 57330, level = 65}, -- Horn of Winter
                HUNTER = {id = 19506, level = 39}, -- Trueshot Aura
                WARRIOR = {id = 6673, level = 42}, -- Battle Shout
            }
        },
        mastery = {
            tag = RAID_BUFF_7, -- Mastery
            icon = 135908,
            order = 5,
            provider = {
                PALADIN = {id = 19740, level = 81}, -- Blessing of Might
                SHAMAN = {id = 116956, level = 80}, -- Grace of Air
            }
        }
    }

    requiredBuffs = {
        [250] = "attackPower", -- Blood
        [251] = "attackPower", -- Frost
        [252] = "attackPower", -- Unholy

        [102] = "spellPower", -- Balance
        [103] = "attackPower", -- Feral
        [104] = "attackPower", -- Guardian
        [105] = "spellPower", -- Restoration

        [253] = "attackPower", -- Beast Mastery
        [254] = "attackPower", -- Marksmanship
        [255] = "attackPower", -- Survival

        [62] = "spellPower", -- Arcane
        [63] = "spellPower", -- Fire
        [64] = "spellPower", -- Frost

        [268] = "attackPower", -- Brewmaster
        [269] = "attackPower", -- Windwalker
        [270] = "spellPower", -- Mistweaver

        [65] = "spellPower", -- Holy
        [66] = "attackPower", -- Protection
        [70] = "attackPower", -- Retribution

        [256] = "spellPower", -- Discipline
        [257] = "spellPower", -- Holy
        [258] = "spellPower", -- Shadow

        [259] = "attackPower", -- Assassination
        [260] = "attackPower", -- Outlaw
        [261] = "attackPower", -- Subtlety

        [262] = "spellPower", -- Elemental
        [263] = "attackPower", -- Enhancement
        [264] = "spellPower", -- Restoration

        [265] = "spellPower", -- Affliction
        [266] = "spellPower", -- Demonology
        [267] = "spellPower", -- Destruction

        [71] = "attackPower", -- Arms
        [72] = "attackPower", -- Fury
        [73] = "attackPower", -- Protection
    }

    requiredByEveryone = {
        stamina = true,
        stats = true,
        mastery = true,
    }

    available = {
        stamina = false,
        stats = false,
        spellPower = false,
        attackPower = false,
        mastery = false,
    }

    unaffected = {
        stamina = {},
        stats = {},
        spellPower = {},
        attackPower = {},
        mastery = {},
    }
end

---------------------------------------------------------------------
-- prepare
---------------------------------------------------------------------
local classBuffs = {
    -- class = {
    --     buff = level,
    -- }
}
local buffOrder = {}
local buffsProvidedByMe = {}
local myClass = UnitClassBase("player")

do
    local myLevel = UnitLevel("player")

    local function Insert(class, buffKey, name, icon)
        tinsert(buffs[buffKey]["names"], name)
        if myClass == class and myLevel >= classBuffs[class][buffKey] then
            buffsProvidedByMe[buffKey] = {name, icon}
        end
    end

    for buffKey, buffData in pairs(buffs) do
        tinsert(buffOrder, buffKey)
        buffData.names = {}

        for class, info in pairs(buffData.provider) do
            classBuffs[class] = classBuffs[class] or {}
            classBuffs[class][buffKey] = info.level

            if type(info.id) == "table" then
                for _, spellId in ipairs(info.id) do
                    local name, icon = F.GetSpellInfo(spellId)
                    if name then
                        Insert(class, buffKey, name, icon)
                    end
                end
            else
                local name, icon = F.GetSpellInfo(info.id)
                if name then
                    Insert(class, buffKey, name, icon)
                end
            end
        end
    end

    sort(buffOrder, function(a, b)
        return buffs[a]["order"] < buffs[b]["order"]
    end)
end

-------------------------------------------------
-- vars
-------------------------------------------------
local enabled
local myUnit = ""
local hasBuffProvider

local fl function Reset(which)
    if not which or which == "available" then
        for k, v in pairs(available) do
            available[k] = false
        end
        hasBuffProvider = false
    end

    if not which or which == "unaffected" then
        for k, v in pairs(unaffected) do
            wipe(unaffected[k])
        end
    end
end

local function GetUnaffectedString(buff)
    local list = unaffected[buff]
    local name = buffs[buff]["tag"]

    local players = {}
    for unit in pairs(list) do
        local name = UnitName(unit)
        tinsert(players, name)
    end

    if #players == 0 then
        return
    elseif #players <= 10 then
        return L["Missing Buff"] .. " (" .. name .. "): " .. tconcat(players, ", ")
    else
        return L["Missing Buff"] .. " (" .. name .. "): " .. L["many"]
    end
end

-------------------------------------------------
-- frame
-------------------------------------------------
local buffTrackerFrame = CreateFrame("Frame", "CellBuffTrackerFrame", Cell.frames.mainFrame, "BackdropTemplate")
Cell.frames.buffTrackerFrame = buffTrackerFrame
P.Size(buffTrackerFrame, 102, 50)
PixelUtil.SetPoint(buffTrackerFrame, "BOTTOMLEFT", CellParent, "CENTER", 1, 1)
buffTrackerFrame:SetClampedToScreen(true)
-- buffTrackerFrame:SetClampRectInsets(0, 0, -20, 0)
buffTrackerFrame:SetMovable(true)
buffTrackerFrame:RegisterForDrag("LeftButton")
buffTrackerFrame:SetScript("OnDragStart", function()
    buffTrackerFrame:StartMoving()
    buffTrackerFrame:SetUserPlaced(false)
end)
buffTrackerFrame:SetScript("OnDragStop", function()
    buffTrackerFrame:StopMovingOrSizing()
    P.SavePosition(buffTrackerFrame, CellDB["tools"]["buffTracker"][4])
end)

-------------------------------------------------
-- mover
-------------------------------------------------
buffTrackerFrame.moverText = buffTrackerFrame:CreateFontString(nil, "OVERLAY", "CELL_FONT_WIDGET")
buffTrackerFrame.moverText:SetPoint("TOP", 0, -3)
buffTrackerFrame.moverText:SetText(L["Mover"])
buffTrackerFrame.moverText:Hide()

local fakeIconsFrame = CreateFrame("Frame", nil, buffTrackerFrame)
P.Point(fakeIconsFrame, "BOTTOMRIGHT", buffTrackerFrame)
P.Point(fakeIconsFrame, "TOPLEFT", buffTrackerFrame, "TOPLEFT", 0, -18)
fakeIconsFrame:EnableMouse(true)
fakeIconsFrame:SetFrameLevel(buffTrackerFrame:GetFrameLevel() + 10)
fakeIconsFrame:Hide()

local fakeIcons = {}
local function CreateFakeIcon(spellIcon)
    local bg = fakeIconsFrame:CreateTexture(nil, "BORDER")
    bg:SetColorTexture(0, 0, 0, 1)
    P.Size(bg, 32, 32)

    local icon = fakeIconsFrame:CreateTexture(nil, "ARTWORK")
    icon:SetTexture(spellIcon)
    icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    P.Point(icon, "TOPLEFT", bg, "TOPLEFT", 1, -1)
    P.Point(icon, "BOTTOMRIGHT", bg, "BOTTOMRIGHT", -1, 1)

    function bg:UpdatePixelPerfect()
        P.Resize(bg)
        P.Repoint(bg)
        P.Repoint(icon)
    end

    return bg
end

do
    for _, k in ipairs(buffOrder) do
        tinsert(fakeIcons, CreateFakeIcon(buffs[k]["icon"]))
    end
end

local function ShowMover(show)
    if show then
        if not CellDB["tools"]["buffTracker"][1] then return end
        buffTrackerFrame:EnableMouse(true)
        buffTrackerFrame.moverText:Show()
        Cell.StylizeFrame(buffTrackerFrame, {0, 1, 0, 0.4}, {0, 0, 0, 0})
        fakeIconsFrame:Show()
        buffTrackerFrame:SetAlpha(1)
    else
        buffTrackerFrame:EnableMouse(false)
        buffTrackerFrame.moverText:Hide()
        Cell.StylizeFrame(buffTrackerFrame, {0, 0, 0, 0}, {0, 0, 0, 0})
        fakeIconsFrame:Hide()
        buffTrackerFrame:SetAlpha(CellDB["tools"]["fadeOut"] and 0 or 1)
    end
end
Cell.RegisterCallback("ShowMover", "BuffTracker_ShowMover", ShowMover)

-------------------------------------------------
-- buttons
-------------------------------------------------
local sendChannel
local function UpdateSendChannel()
    if IsInGroup(LE_PARTY_CATEGORY_INSTANCE) then
        sendChannel = "INSTANCE_CHAT"
    elseif IsInRaid() then
        sendChannel = "RAID"
    else
        sendChannel = "PARTY"
    end
end

local function CreateBuffButton(parent, buff)
    local b = CreateFrame("Button", nil, parent, "SecureActionButtonTemplate,BackdropTemplate")
    if parent then b:SetFrameLevel(parent:GetFrameLevel() + 1) end
    P.Size(b, 32, 32)

    b:SetBackdrop({edgeFile = Cell.vars.whiteTexture, edgeSize = P.Scale(1)})
    b:SetBackdropBorderColor(0, 0, 0, 1)

    b:RegisterForClicks("LeftButtonUp", "RightButtonUp", "LeftButtonDown", "RightButtonDown") -- NOTE: ActionButtonUseKeyDown will affect this

    -- cast
    if buffsProvidedByMe[buff] then
        b:SetAttribute("type1", "macro")
        b:SetAttribute("macrotext1", "/cast [@player] " .. buffsProvidedByMe[buff][1])
    end

    -- chat
    b:HookScript("OnClick", function(self, button, down)
        if button == "RightButton" and (down == GetCVarBool("ActionButtonUseKeyDown")) then
            local msg = GetUnaffectedString(buff)
            if msg then
                UpdateSendChannel()
                SendChatMessage(msg, sendChannel)
            end
        end
    end)

    b.texture = b:CreateTexture(nil, "OVERLAY")
    P.Point(b.texture, "TOPLEFT", b, "TOPLEFT", 1, -1)
    P.Point(b.texture, "BOTTOMRIGHT", b, "BOTTOMRIGHT", -1, 1)
    b.texture:SetTexture(buffs[buff]["icon"])
    b.texture:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    b.count = b:CreateFontString(nil, "OVERLAY")
    P.Point(b.count, "TOPLEFT", b.texture, "TOPLEFT", 2, -2)
    b.count:SetFont(GameFontNormal:GetFont(), 14, "OUTLINE")
    b.count:SetShadowColor(0, 0, 0)
    b.count:SetShadowOffset(0, 0)
    b.count:SetTextColor(1, 0, 0)

    b:SetScript("OnLeave", function()
        CellTooltip:Hide()
    end)

    function b:SetTooltips(list)
        b:SetScript("OnEnter", function()
            if F.Getn(list) ~= 0 then
                CellTooltip:SetOwner(b, "ANCHOR_TOPLEFT", 0, 3)
                CellTooltip:AddLine(L["Unaffected"] .. " |cffb7b7b7" .. buffs[buff]["tag"])
                for unit in pairs(list) do
                    local class = UnitClassBase(unit)
                    local name = UnitName(unit)
                    if class and name then
                        CellTooltip:AddLine(F.GetClassColorStr(class) .. name .. "|r")
                    end
                end
                CellTooltip:Show()
            end
        end)
    end

    function b:SetDesaturated(flag)
        b.texture:SetDesaturated(flag)
    end

    function b:StartGlow(glowType, ...)
        LCG.PixelGlow_Start(b, ...)
    end

    function b:StopGlow()
        LCG.PixelGlow_Stop(b)
    end

    function b:Reset()
        b.texture:SetDesaturated(false)
        b.count:SetText("")
        b:SetAlpha(1)
        b:StopGlow()
    end

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

        P.Repoint(b.texture)
        P.Repoint(b.count)
    end

    return b
end

local buttons = {}

do
    for _, buff in ipairs(buffOrder) do
        buttons[buff] = CreateBuffButton(buffTrackerFrame, buff)
        buttons[buff]:Hide()
        buttons[buff]:SetTooltips(unaffected[buff])
    end
end

local function UpdateButtons()
    for _, buff in pairs(buffOrder) do
        if available[buff] then
            local n = F.Getn(unaffected[buff])
            if n == 0 then
                buttons[buff].count:SetText("")
                buttons[buff]:SetAlpha(0.5)
                buttons[buff]:StopGlow()
            else
                buttons[buff].count:SetText(n)
                buttons[buff]:SetAlpha(1)
                if unaffected[buff][myUnit] then
                    -- color, N, frequency, length, thickness
                    buttons[buff]:StartGlow("Pixel", {1, 0.19, 0.19, 1}, 8, 0.25, P.Scale(8), P.Scale(2))
                else
                    buttons[buff]:StopGlow()
                end
            end
        end
    end
end

local function RepointButtons()
    if InCombatLockdown() then
        buffTrackerFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
    else
        local point, relativePoint, offsetX, offsetY, firstX, firstY
        if CellDB["tools"]["buffTracker"][2] == "left-to-right" then
            point, relativePoint = "BOTTOMLEFT", "BOTTOMRIGHT"
            offsetX, offsetY = 3, 0
            firstX, firstY = 0, 0
        elseif CellDB["tools"]["buffTracker"][2] == "right-to-left" then
            point, relativePoint = "BOTTOMRIGHT", "BOTTOMLEFT"
            offsetX, offsetY = -3, 0
            firstX, firstY = 0, 0
        elseif CellDB["tools"]["buffTracker"][2] == "top-to-bottom" then
            point, relativePoint = "TOPLEFT", "BOTTOMLEFT"
            offsetX, offsetY = 0, -3
            firstX, firstY = 0, -18
        elseif CellDB["tools"]["buffTracker"][2] == "bottom-to-top" then
            point, relativePoint = "BOTTOMLEFT", "TOPLEFT"
            offsetX, offsetY = 0, 3
            firstX, firstY = 0, 0
        end

        local last
        for _, k in pairs(buffOrder) do
            P.ClearPoints(buttons[k])
            if available[k] then
                buttons[k]:Show()
                if last then
                    P.Point(buttons[k], point, last, relativePoint, offsetX, offsetY)
                else
                    P.Point(buttons[k], point, firstX, firstY)
                end
                last = buttons[k]
            else
                buttons[k]:Hide()
                buttons[k]:Reset()
            end
        end

        last = nil
        for _, icon in pairs(fakeIcons) do
            P.ClearPoints(icon)
            if last then
                P.Point(icon, point, last, relativePoint, offsetX, offsetY)
            else
                P.Point(icon, point, buffTrackerFrame, point, firstX, firstY)
            end
            last = icon
        end
    end
end

local function ResizeButtons()
    if InCombatLockdown() then
        buffTrackerFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
    else
        local size = CellDB["tools"]["buffTracker"][3]
        for _, i in pairs(fakeIcons) do
            P.Size(i, size, size)
        end
        for _, b in pairs(buttons) do
            P.Size(b, size, size)
        end

        local n = F.Getn(buttons)
        if strfind(CellDB["tools"]["buffTracker"][2], "left") then
            buffTrackerFrame:SetSize(n * P.Scale(size) + (n - 1) * P.Scale(3), P.Scale(size + 18))
        else
            buffTrackerFrame:SetSize(P.Scale(size), n * P.Scale(size) + (n - 1) * P.Scale(3) + P.Scale(18))
        end
    end
end

-------------------------------------------------
-- fade out
-------------------------------------------------
local fadeOuts = {}
for _, b in pairs(buttons) do
    tinsert(fadeOuts, b)
end
A.ApplyFadeInOutToParent(buffTrackerFrame, function()
    return CellDB["tools"]["fadeOut"] and not buffTrackerFrame.moverText:IsShown()
end, unpack(fadeOuts))

---------------------------------------------------------------------
-- find aura
---------------------------------------------------------------------
local GetAuraDataBySpellName = C_UnitAuras.GetAuraDataBySpellName

local function UnitBuffExists(unit, buff)
    local names = buffs[buff]["names"]
    local aura
    for _, name in next, names do
        aura = GetAuraDataBySpellName(unit, name, "HELPFUL")
        if aura then
            return true, aura.sourceUnit == "player"
        end
    end
end

---------------------------------------------------------------------
-- missing buffs
---------------------------------------------------------------------
local missingBuffsFromMe = {}
local hasBuffFromMe = {}

local function UpdateMissingBuffs(unit, buff)
    missingBuffsFromMe[unit] = missingBuffsFromMe[unit] or {}
    tinsert(missingBuffsFromMe[unit], buff)
end

local function ShowMissingBuffs(unit)
    I.HideMissingBuffs(unit)

    if not missingBuffsFromMe[unit] then return end

    local num = #missingBuffsFromMe[unit]
    if num == 0 then return end

    if myClass == "PALADIN" or myClass == "WARRIOR" then
        if hasBuffFromMe[unit] then return end
    end

    if num == 1 or myClass == "PRIEST" then
        for _, buff in next, missingBuffsFromMe[unit] do
            I.ShowMissingBuff(unit, buffsProvidedByMe[buff][2])
        end
    else
        I.ShowMissingBuff(unit, 254882)
    end
end

-------------------------------------------------
-- check
-------------------------------------------------
local function CheckUnit(unit, updateBtn)
    -- print("CheckUnit", unit)
    if not hasBuffProvider then return end

    if missingBuffsFromMe[unit] then wipe(missingBuffsFromMe[unit]) end
    hasBuffFromMe[unit] = nil

    if UnitIsConnected(unit) and UnitIsVisible(unit) and not UnitIsDeadOrGhost(unit) then
        local info = LGI:GetCachedInfo(UnitGUID(unit))
        local spec = info and info.specId
        local required = spec and requiredBuffs[spec]

        for buff, hasProvider in next, available do
            if hasProvider then
                if required == buff or requiredByEveryone[buff] then
                    local exists, providedByMe = UnitBuffExists(unit, buff)
                    if exists then
                        unaffected[buff][unit] = nil
                        if providedByMe then
                            hasBuffFromMe[unit] = true
                        end
                    else
                        unaffected[buff][unit] = true
                        if buffsProvidedByMe[buff] then
                            UpdateMissingBuffs(unit, buff)
                        end
                    end
                end
            else
                unaffected[buff][unit] = nil
            end
        end
    else
        for k, t in next, unaffected do
            t[unit] = nil
        end
    end

    ShowMissingBuffs(unit)

    if updateBtn then UpdateButtons() end
end

local function IterateAllUnits()
    Reset("available")
    myUnit = ""

    local class, level
    for unit in F.IterateGroupMembers() do
        if UnitIsConnected(unit) and UnitIsVisible(unit) then
            class = UnitClassBase(unit)
            level = UnitLevel(unit)
            if classBuffs[class] then
                for buff, lvl in pairs(classBuffs[class]) do
                    if not available[buff] and level >= lvl then
                        available[buff] = true
                        hasBuffProvider = true
                    end
                end
            end

            if UnitIsUnit("player", unit) then
                myUnit = unit
            end
        end
    end

    RepointButtons()
    Reset("unaffected")

    for unit in F.IterateGroupMembers() do
        CheckUnit(unit)
    end

    UpdateButtons()
end

-------------------------------------------------
-- events
-------------------------------------------------
function buffTrackerFrame:UnitUpdated(event, guid, unit, info)
    -- print(event, guid, unit, info.specId)
    if unit == "player" then
        if UnitIsUnit("player", myUnit) then CheckUnit(myUnit, true) end
    elseif UnitIsPlayer(unit) then -- ignore pets
        CheckUnit(unit, true)
    end
end

function buffTrackerFrame:PLAYER_ENTERING_WORLD()
    buffTrackerFrame:UnregisterEvent("PLAYER_ENTERING_WORLD")
    buffTrackerFrame:GROUP_ROSTER_UPDATE()
end

local timer
function buffTrackerFrame:GROUP_ROSTER_UPDATE(immediate)
    if timer then timer:Cancel() end
    if IsInGroup() then
        buffTrackerFrame:RegisterEvent("READY_CHECK")
        buffTrackerFrame:RegisterEvent("UNIT_FLAGS")
        buffTrackerFrame:RegisterEvent("PLAYER_UNGHOST")
        buffTrackerFrame:RegisterEvent("UNIT_AURA")
    else
        buffTrackerFrame:UnregisterEvent("READY_CHECK")
        buffTrackerFrame:UnregisterEvent("UNIT_FLAGS")
        buffTrackerFrame:UnregisterEvent("PLAYER_UNGHOST")
        buffTrackerFrame:UnregisterEvent("UNIT_AURA")

        Reset()
        RepointButtons()
        return
    end

    if immediate then
        IterateAllUnits()
    else
        timer = C_Timer.NewTimer(3, IterateAllUnits)
    end
end

function buffTrackerFrame:READY_CHECK()
    buffTrackerFrame:GROUP_ROSTER_UPDATE(true)
end

function buffTrackerFrame:UNIT_FLAGS()
    buffTrackerFrame:GROUP_ROSTER_UPDATE()
end

function buffTrackerFrame:PLAYER_UNGHOST()
    buffTrackerFrame:GROUP_ROSTER_UPDATE()
end

function buffTrackerFrame:UNIT_AURA(unit)
    if IsInRaid() then
        if unit:find("^raid%d+$") then
            CheckUnit(unit, true)
        end
    else
        if unit:find("^party%d$") or unit == "player" then
            CheckUnit(unit, true)
        end
    end
end

function buffTrackerFrame:PLAYER_REGEN_ENABLED()
    buffTrackerFrame:UnregisterEvent("PLAYER_REGEN_ENABLED")
    RepointButtons()
    ResizeButtons()
end

buffTrackerFrame:SetScript("OnEvent", function(self, event, ...)
    self[event](self, ...)
end)

-------------------------------------------------
-- functions
-------------------------------------------------
local function UpdateTools(which)
    if not which or which == "buffTracker" then
        if CellDB["tools"]["buffTracker"][1] then
            buffTrackerFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
            buffTrackerFrame:RegisterEvent("GROUP_ROSTER_UPDATE")
            LGI.RegisterCallback(buffTrackerFrame, "GroupInfo_Update", "UnitUpdated")

            if not enabled and which == "buffTracker" then -- already in world, manually enabled
                buffTrackerFrame:GROUP_ROSTER_UPDATE(true)
            end
            enabled = true
            if Cell.vars.showMover then
                ShowMover(true)
            end
        else
            buffTrackerFrame:UnregisterAllEvents()
            LGI.UnregisterCallback(buffTrackerFrame, "GroupInfo_Update")

            Reset()
            myUnit = ""

            enabled = false
            ShowMover(false)

            -- missingBuffs indicator
            for unit in F.IterateGroupMembers() do
                I.HideMissingBuffs(unit, true)
            end
        end

        RepointButtons()
        ResizeButtons()
    end

    if not which or which == "fadeOut" then
        if CellDB["tools"]["fadeOut"] and not buffTrackerFrame.moverText:IsShown() then
            buffTrackerFrame:SetAlpha(0)
        else
            buffTrackerFrame:SetAlpha(1)
        end
    end

    if not which then -- position
        P.LoadPosition(buffTrackerFrame, CellDB["tools"]["buffTracker"][4])
    end
end
Cell.RegisterCallback("UpdateTools", "BuffTracker_UpdateTools", UpdateTools)

local function UpdatePixelPerfect()
    -- P.Resize(buffTrackerFrame)

    for _, i in pairs(fakeIcons) do
        i:UpdatePixelPerfect()
    end

    for _, b in pairs(buttons) do
        b:UpdatePixelPerfect()
    end
end
Cell.RegisterCallback("UpdatePixelPerfect", "BuffTracker_UpdatePixelPerfect", UpdatePixelPerfect)