PTUnitFrame = {}
PTUtil.SetEnvironment(PTUnitFrame)
PTUnitFrame.__index = PTUnitFrame
local _G = getfenv(0)
local PT = Puppeteer
local util = PTUtil
local compost = AceLibrary("Compost-2.0")

PTUnitFrame.owningGroup = nil

PTUnitFrame.unit = nil
PTUnitFrame.isCustomUnit = false
PTUnitFrame.guidUnit = nil -- Used for custom units

PTUnitFrame.rootContainer = nil -- Contains the main container and the overlay
PTUnitFrame.overlayContainer = nil -- Contains elements that should not be affected by opacity
PTUnitFrame.container = nil -- Most elements are contained in this
PTUnitFrame.nameText = nil
PTUnitFrame.healthBar = nil
PTUnitFrame.incomingHealthBar = nil
PTUnitFrame.incomingDirectHealthBar = nil
PTUnitFrame.healthText = nil
PTUnitFrame.missingHealthText = nil
PTUnitFrame.incomingHealText = nil
PTUnitFrame.powerBar = nil
PTUnitFrame.powerText = nil
PTUnitFrame.roleIcon = nil
PTUnitFrame.button = nil
PTUnitFrame.auraPanel = nil
PTUnitFrame.scrollingDamageFrame = nil -- Unimplemented
PTUnitFrame.scrollingHealFrame = nil -- Unimplemented
PTUnitFrame.auraIconPool = {} -- map: {"frame", "icon", "stackText"}
PTUnitFrame.auraIcons = {} -- map: {"frame", "icon", "stackText"}

PTUnitFrame.targetOutline = nil

PTUnitFrame.targeted = false

PTUnitFrame.flashTexture = nil -- {"frame", "texture"}
PTUnitFrame.flashTime = 0
PTUnitFrame.lastHealthPercent = 0

PTUnitFrame.incomingHealing = 0
PTUnitFrame.incomingDirectHealing = 0

PTUnitFrame.hovered = false
PTUnitFrame.pressed = false

PTUnitFrame.distanceText = nil
PTUnitFrame.lineOfSightIcon = nil -- map: {"frame", "icon"}

PTUnitFrame.inRange = true
PTUnitFrame.distance = 0
PTUnitFrame.inSight = true

PTUnitFrame.fakeStats = {} -- Used for displaying a fake party/raid

function PTUnitFrame:New(unit, isCustomUnit)
    local obj = setmetatable({unit = unit, isCustomUnit = isCustomUnit, auraIconPool = {}, 
        auraIcons = {}, fakeStats = PTUnitFrame.GenerateFakeStats()}, self)
    return obj
end

local fakeNames = {"Leeroyjenkins", "Realsigred", "Appledog", "Exdraclespy", "Dieghostt", "Olascoli", "Yaijin", 
    "Geroya", "Artemyz", "Nomeon", "Orinnberry", "Hoppetosse", "Deathell", "Jackbob", "Luscita", "Healpiggies", 
    "Pamara", "Merauder", "Onetwofree", "Biggly", "Drexx", "Grassguzzler", "Thebackup", "Steaktank", "Fshoo", 
    "Bovinebill", "Rawtee", "Aylin", "Sneeziesnorf", "Dreak", "Jordin", "Evilkillers", "Xathas", "Linkado", 
    "Smiteknight", "Rollnmbqs", "Viniss", "Rinnegon", "Elfdefense", "Foxtau", "Tombdeath", "Myhawk", "Numnumcat", 
    "Laudead", "Esatto", "Boffin", "Tikomo", "Huddletree", "Butterboy", "Bolgrand", "Ginius", "Exulthiuss", 
    "Xplol", "Wheeliebear", "Pimenton", "Meditating", "Qyroth", "Lazhar", "Rookon", "Eiris", "Padren", 
    "Erazergus", "Scarlatina", "Holdrim", "Soulbane", "Debilitated", "Doorooid", "Palefire", "Tellarna", 
    "Breathofwing", "Chillaf", "Hulena", "Hyperiann", "Bluebeam", "Daevana", "Adriena", "Aeywynn", "Bluaa", 
    "Chadd", "Leutry", "Mouzer", "Qiner"}
function PTUnitFrame.GenerateFakeStats()

    local name = fakeNames[math.random(table.getn(fakeNames))]

    local class = util.GetRandomClass()

    local currentHealth
    local maxHealth = math.random(100, 5000)
    if math.random(10) > 3 then
        currentHealth = math.random(1, maxHealth)
    elseif math.random(8) == 1 then
        currentHealth = 0
    else
        currentHealth = maxHealth
    end

    local maxPower = math.random(100, 5000)
    if util.ClassPowerTypes[class] ~= "mana" then
        maxPower = 100
    end
    local currentPower = math.random(1, maxPower)

    local debuffType
    local trackedDebuffCount = table.getn(PuppeteerSettings.TrackedDebuffTypes)
    if trackedDebuffCount > 0 then
        if math.random(1, 10) == 1 then
            debuffType = PuppeteerSettings.TrackedDebuffTypes[math.random(trackedDebuffCount)]
        end
    end

    local raidMark
    if math.random(10) == 1 then
        raidMark = math.random(8)
    end

    local online = not (math.random(12) == 1)

    local fakeStats = {
        name = name,
        class = class, 
        currentHealth = currentHealth, 
        maxHealth = maxHealth,
        currentPower = currentPower,
        maxPower = maxPower,
        debuffType = debuffType,
        raidMark = raidMark,
        online = online}
    return fakeStats
end

function PTUnitFrame:GetUnit()
    return self.unit
end

function PTUnitFrame:GetResolvedUnit()
    return not self.isCustomUnit and self.unit or self.guidUnit
end

function PTUnitFrame:GetRootContainer()
    return self.rootContainer
end

function PTUnitFrame:GetContainer()
    return self.container
end

function PTUnitFrame:Show()
    self.container:Show()
    self.rootContainer:Show()
    self:UpdateAll()
end

function PTUnitFrame:Hide()
    if not self:IsFake() then
        self.container:Hide()
        self.rootContainer:Hide()
    end
end

function PTUnitFrame:IsShown()
    return self.rootContainer:IsShown()
end

function PTUnitFrame:SetOwningGroup(group)
    self.owningGroup = group
    self:Initialize()
    self:GetRootContainer():SetParent(group:GetContainer())
end

function PTUnitFrame:RegisterClicks()
    local buttons = PTOptions.CastWhen == "Mouse Up" and util.GetUpButtons() or util.GetDownButtons()
    self.button:RegisterForClicks(unpack(buttons))
    for _, aura in ipairs(self.auraIcons) do
        aura.frame:RegisterForClicks(unpack(buttons))
    end
    for _, aura in ipairs(self.auraIconPool) do
        aura.frame:RegisterForClicks(unpack(buttons))
    end
end

function PTUnitFrame:UpdateAll()
    self:UpdateAuras()
    self:UpdateHealth()
    self:UpdatePower()
    self:UpdateRange()
    self:UpdateSight()
    self:EvaluateTarget()
    self:UpdateOutline()
    self:UpdateRaidMark()
    self:UpdatePVP()
end

function PTUnitFrame:GetShowDistanceThreshold()
    local threshold = self:GetProfile().ShowDistanceThreshold
    return self:IsEnemy() and threshold.Hostile or threshold.Friendly
end

function PTUnitFrame:GetOutOfRangeThreshold()
    local threshold = self:GetProfile().OutOfRangeThreshold
    return self:IsEnemy() and threshold.Hostile or threshold.Friendly
end

function PTUnitFrame:UpdateRange()
    local wasInRange = self.inRange
    self.distance = self:GetCache():GetDistance()
    self.inRange = math.ceil(self.distance) < self:GetOutOfRangeThreshold()
    if wasInRange ~= self.inRange then
        self:UpdateOpacity()
    end

    self:UpdateRangeText()
end

function PTUnitFrame:UpdateSight()
    self.inSight = self:GetCache():IsInSight()
    local frame = self.lineOfSightIcon.frame
    if frame:IsShown() ~= self.inSight then
        local dist = math.ceil(self.distance)
        if not self.inSight and (dist < 80 or UnitIsUnit(self.unit, "target")) then
            frame:Show()
        else
            frame:Hide()
        end
    end
end

local preciseDistance = util.CanClientGetPreciseDistance()
function PTUnitFrame:UpdateRangeText()
    local dist = math.ceil(self.distance)
    local distanceText = self.distanceText
    local text = ""
    if dist >= (preciseDistance and self:GetShowDistanceThreshold() or 28) and dist < 9999 then
        local r, g, b
        if dist > 80 then
            r, g, b = 0.75, 0.75, 0.75
        elseif dist >= self:GetOutOfRangeThreshold() then
            r, g, b = 1, 0.3, 0.3
        else
            r, g, b = 1, 0.6, 0
        end

        if preciseDistance then
            text = text..util.Colorize(dist.." yd", r, g, b)
        else
            if dist < 28 then
                text = text..util.Colorize("<"..dist.." yd", r, g, b)
            else
                text = text..util.Colorize("28+ yd", r, g, b)
            end
        end
    end
    distanceText:SetText(text)
end

function PTUnitFrame:UpdateOpacity()
    local profile = self:GetProfile()
    
    local alpha = 1
    if not self.inRange then
        alpha = alpha * (profile.OutOfRangeOpacity / 100)
    end
    if profile.AlertPercent < math.ceil((self:GetCurrentHealth() / self:GetMaxHealth()) * 100) and 
            table.getn(self:GetAfflictedDebuffTypes()) == 0 then
        alpha = alpha * (profile.NotAlertedOpacity / 100)
    end
    self.container:SetAlpha(alpha)
end

-- Evaluate if the unit of this frame is the target and update the target outline if the state has changed
function PTUnitFrame:EvaluateTarget()
    if self.unit == "target" then -- "target" frames should not show a border since it's obvious they're the target
        return
    end
    local wasTargeted = self.targeted
    self.targeted = UnitIsUnit(self.unit, "target")
    if self.targeted ~= wasTargeted then
        self:UpdateOutline()
    end
end

function PTUnitFrame:UpdateOutline()
    local aggro = self:HasAggro()
    local targeted = self.targeted

    local r, g, b
    if aggro and targeted then
        r, g, b  = 1, 0.6, 0.5
    elseif aggro then
        r, g, b = 1, 0, 0
    elseif targeted then
        r, g, b = 1, 1, 0.85
    end

    self:SetOutlineColor(r, g, b)
end

function PTUnitFrame:SetOutlineColor(r, g, b)
    if r then
        self.targetOutline:Show()
        self.targetOutline:SetBackdropBorderColor(r, g, b, 0.75)
    else
        self.targetOutline:Hide()
    end
end

function PTUnitFrame:UpdateRaidMark()
    local unit = self:GetResolvedUnit()
    local fake = self:IsFake()
    if not unit and not fake then
        self.raidMarkIcon.frame:Hide()
        return
    end

    if unit == "target" and not UnitExists("target") then
        self.raidMarkIcon.frame:Hide()
        return
    end

    local markIndex
    if fake then
        markIndex = self.fakeStats.raidMark
    else
        markIndex = GetRaidTargetIndex(unit)
    end
    if not markIndex then
        self.raidMarkIcon.frame:Hide()
        return
    end
    SetRaidTargetIconTexture(self.raidMarkIcon.icon, markIndex)
    self.raidMarkIcon.frame:Show()
end

function PTUnitFrame:UpdatePVP()
    if UnitIsPVP(self.unit) and (not IsInInstance() or not UnitIsVisible(self.unit)) then
        local faction = UnitFactionGroup(self.unit)
        if faction == "Alliance" then
            self.pvpIcon.icon:SetTexture("Interface\\TargetingFrame\\UI-PVP-Alliance")
        elseif faction == "Horde" then
            self.pvpIcon.icon:SetTexture("Interface\\TargetingFrame\\UI-PVP-Horde")
        else
            self.pvpIcon.icon:SetTexture("Interface\\TargetingFrame\\UI-PVP-FFA")
        end
        self.pvpIcon.frame:Show()
    else
        self.pvpIcon.frame:Hide()
    end
end

function PTUnitFrame:Flash()
    local FLASH_TIME = 0.15
    local START_OPACITY = self:GetProfile().FlashOpacity / 100

    self.flashTime = FLASH_TIME
    local frame = self.flashTexture.frame
    frame:Show()
    frame:SetAlpha(START_OPACITY)

    frame.flashTime = FLASH_TIME
    frame.startOpacity = START_OPACITY

    if not frame:GetScript("OnUpdate") then
        frame:SetScript("OnUpdate", PTUnitFrame.Flash_OnUpdate)
    end
end

function PTUnitFrame.Flash_OnUpdate()
    local frame = this
    local self = frame.unitFrame
    local FLASH_TIME = frame.flashTime
    local START_OPACITY = frame.startOpacity
    self.flashTime = self.flashTime - arg1
    frame:SetAlpha(START_OPACITY - (((FLASH_TIME - self.flashTime) / FLASH_TIME) * START_OPACITY))

    if self.flashTime <= 0 then
        frame:Hide()
        frame:SetScript("OnUpdate", nil)
    end
end

-- If direct healing is nil, it will be assumed that all the incoming healing is direct healing
function PTUnitFrame:SetIncomingHealing(incomingHealing, incomingDirectHealing)
    self.incomingHealing = incomingHealing
    self.incomingDirectHealing = incomingDirectHealing or incomingHealing
    self:UpdateHealth()
end

function PTUnitFrame:UpdateIncomingHealing()
    if PTHealPredict then
        local _, guid = UnitExists(self:GetUnit())
        self:SetIncomingHealing(PTHealPredict.GetIncomingHealing(guid))
    else
        local name = UnitName(self:GetUnit())
        self:SetIncomingHealing(Puppeteer.HealComm:getHeal(name))
    end
end

function PTUnitFrame:GetCurrentHealth()
    if self:IsFake() then
        if not self.fakeStats.online then
            return 0
        end
        return self.fakeStats.currentHealth
    end
    return UnitHealth(self.unit)
end

function PTUnitFrame:GetMaxHealth()
    if self:IsFake() then
        return self.fakeStats.maxHealth
    end
    return UnitHealthMax(self.unit)
end

function PTUnitFrame:GetCurrentPower()
    if self:IsFake() then
        return self.fakeStats.currentPower
    end
    return UnitMana(self.unit)
end

function PTUnitFrame:GetMaxPower()
    if self:IsFake() then
        return self.fakeStats.maxPower
    end
    return UnitManaMax(self.unit)
end

function PTUnitFrame:ShouldShowMissingHealth()
    local profile = self:GetProfile()
    local currentHealth = self:GetCurrentHealth()
    if currentHealth == 0 then
        return false
    end
    local missingHealth = self:GetMaxHealth() - currentHealth
    return (missingHealth > 0 or profile.AlwaysShowMissingHealth) and profile.MissingHealthDisplay ~= "Hidden" 
                and (profile.ShowEnemyMissingHealth or not self:IsEnemy()) 
                and not UnitIsGhost(self.unit) and (UnitIsConnected(self.unit) or self:IsFake())
end

-- Used for custom colors in profiles.
-- Valid Colors:
-- "Class" - Uses the unit's class color
-- "Default" - Does not color the text
-- Array RGB - Colors the text as the RGB values
function PTUnitFrame:ColorizeText(inputText, color)
    if color == "Class" then
        local r, g, b = util.GetClassColor(self:GetClass())
        return util.Colorize(inputText, r, g, b)
    elseif type(color) == "table" then
        return util.Colorize(inputText, color)
    end
    return inputText
end

function PTUnitFrame:UpdateHealth()
    local fake = self:IsFake()
    if not UnitExists(self.unit) and not fake then
        if self.isCustomUnit or self.unit == "target" then
            self.healthText:SetText(util.Colorize(self.unit ~= "target" and "Too Far" or "", 0.7, 0.7, 0.7))
            self.missingHealthText:SetText("")
            self:SetHealthBarValue(0)
            self.powerBar:SetValue(0)
            self:UpdateOpacity()
            self:AdjustHealthPosition()
        end
        if self.unit == "target" then
            self.nameText:SetText("")
        end
        return
    end
    local profile = self:GetProfile()
    local unit = self.unit

    local fakeOnline = self.fakeStats.online
    
    local currentHealth = self:GetCurrentHealth()
    local maxHealth = self:GetMaxHealth()
    
    local unitName = self:GetName()

    self:UpdateOpacity() -- May be changed further down

    if not UnitIsConnected(unit) and (not fake or not fakeOnline) then
        self.nameText:SetText(self:ColorizeText(unitName, profile.NameText.Color))
        self.healthText:SetText(util.Colorize("Offline", 0.7, 0.7, 0.7))
        self.missingHealthText:SetText("")
        self:SetHealthBarValue(0)
        self.powerBar:SetValue(0)
        self:UpdateOpacity()
        self:AdjustHealthPosition()
        return
    end

    local enemy = self:IsEnemy()

    -- Set Name and its colors
    local nameText
    if self:IsPlayer() or fake then
        local r, g, b
        if profile.ShowDebuffColorsOn == "Name" and not enemy then
            r, g, b = self:GetDebuffColor()
        end
        if r then
            nameText = util.Colorize(unitName, r, g, b)
        else
            nameText = self:ColorizeText(unitName, profile.NameText.Color)
        end
    else -- Unit is not a player
        if enemy then
            nameText = util.Colorize(unitName, 1, 0.3, 0.3)
        else -- Unit is not an enemy
            local r, g, b
            if profile.ShowDebuffColorsOn == "Name" then
                r, g, b = self:GetDebuffColor()
            end
            if r then
                nameText = util.Colorize(unitName, r, g, b)
            else
                nameText = unitName
            end
        end
    end
    self.nameText:SetText(nameText)

    local healthText = self.healthText
    local missingHealthText = self.missingHealthText

    -- Set Health Status
    if currentHealth <= 0 then -- Unit Dead
        local cache = self:GetCache()
        local text
        if cache:IsBeingResurrected() then
            if cache:GetResurrectionCasts() > 1 then
                text = util.Colorize("DEAD", 0.8, 1, 0.8)
            else
                text = util.Colorize("DEAD", 0.3, 1, 0.3)
            end
        else
            text = util.Colorize("DEAD", 1, 0.3, 0.3)
        end

        -- Check for Feign Death so the healer doesn't get alarmed
        local feign = self:GetCache():HasBuffIDOrName(5384, "Feign Death")
        if feign then
            text = "Feign"
        end

        healthText:SetText(text)
        missingHealthText:SetText("")
        self:SetHealthBarValue(0)
        self.powerBar:SetValue(0)
        if self.lastHealthPercent > 0 and not self:IsEnemy() then
            if not feign then
                self:Flash()
            end
            self.lastHealthPercent = 0
        end
    elseif UnitIsGhost(unit) then
        healthText:SetText(util.Colorize("Ghost", 1, 0.3, 0.3))
        missingHealthText:SetText("")
        self:SetHealthBarValue(0)
        self.powerBar:SetValue(0)
    else -- Unit Not Dead
        local text = ""
        local missingText = ""
        if profile.HealthDisplay == "Health/Max Health" then
            text = currentHealth.."/"..maxHealth
        elseif profile.HealthDisplay == "Health" then
            text = currentHealth
        elseif profile.HealthDisplay == "% Health" then
            text = math.floor((currentHealth / maxHealth) * 100).."%"
        end
        
        if profile.ShowDebuffColorsOn == "Health" then
            local r, g, b = self:GetDebuffColor()
            if r then
                text = util.Colorize(text, r, g, b)
            end
        end
        if self.hovered then
            text = util.Colorize(text, 1, 1, 1)
        end

        local missingHealth = math.floor(maxHealth - currentHealth)

        if self:ShouldShowMissingHealth() then
            local missingHealthStr
            if profile.MissingHealthDisplay == "-Health" then
                missingHealthStr = "-"..missingHealth
            elseif profile.MissingHealthDisplay == "-% Health" then
                missingHealthStr = "-"..math.ceil((missingHealth / maxHealth) * 100).."%"
            end

            if profile.MissingHealthInline then
                if text ~= "" then
                    text = text..self:ColorizeText(" ("..missingHealthStr..")", profile.HealthTexts.Missing.Color)
                end
            else
                missingText = self:ColorizeText(missingHealthStr, profile.HealthTexts.Missing.Color)
            end
        end

        healthText:SetText(text)
        missingHealthText:SetText(missingText)

        self:SetHealthBarValue(currentHealth / maxHealth)

        local healthPercent = (currentHealth / maxHealth) * 100
        if healthPercent < self.lastHealthPercent - profile.FlashThreshold and not self:IsEnemy() then
            self:Flash()
        end
        self.lastHealthPercent = healthPercent

        if self:GetCache():HasBuffIDOrName(27827, "Spirit of Redemption") then
            healthText:SetText(util.Colorize("Spirit", 1, 0.3, 0.3))
        end
    end

    self:UpdateOpacity()
    self:AdjustHealthPosition()
end

local greenToRedColors = {{1, 0, 0}, {1, 0.3, 0}, {1, 1, 0}, {0.6, 0.92, 0}, {0, 0.8, 0}}
function PTUnitFrame:SetHealthBarValue(value)
    local unit = self.unit
    local healthBar = self.healthBar
    local incomingHealthBar = self.incomingHealthBar
    local incomingDirectHealthBar = self.incomingDirectHealthBar
    local incomingHealText = self.incomingHealText
    local incomingHealing = self.incomingHealing
    local incomingDirectHealing = self.incomingDirectHealing
    local profile = self:GetProfile()
    local enemy = self:IsEnemy()

    healthBar:SetValue(value)

    local healthIncMaxRatio = 0
    local healthIncDirectMaxRatio = 0

    if incomingHealing > 0 then
        healthIncMaxRatio = value + (incomingHealing / self:GetMaxHealth())
        healthIncDirectMaxRatio = value + (incomingDirectHealing / self:GetMaxHealth())
        incomingHealthBar:SetValue(healthIncMaxRatio)
        incomingDirectHealthBar:SetValue(healthIncDirectMaxRatio)
        if profile.IncomingHealDisplay == "Overheal" then
            if healthIncMaxRatio > 1 then
                incomingHealText:SetText("+"..math.ceil(self:GetCurrentHealth() + incomingHealing - self:GetMaxHealth()))
                local rgb = incomingDirectHealing > 0 and profile.IncomingHealText.Color or 
                    profile.IncomingHealText.IndirectColor
                if incomingDirectHealing > 0 then
                    incomingHealText:SetTextColor(rgb[1], rgb[2], rgb[3])
                else
                    incomingHealText:SetTextColor(rgb[1], rgb[2], rgb[3])
                end
            else
                incomingHealText:SetText("")
            end
        elseif profile.IncomingHealDisplay == "Heal" then
            incomingHealText:SetText("+"..math.ceil(incomingHealing))
            local rgb = incomingDirectHealing > 0 and profile.IncomingHealText.Color or 
                    profile.IncomingHealText.IndirectColor
            if incomingDirectHealing > 0 then
                incomingHealText:SetTextColor(rgb[1], rgb[2], rgb[3])
            else
                incomingHealText:SetTextColor(rgb[1], rgb[2], rgb[3])
            end
        else
            incomingHealText:SetText("")
        end
    else -- No incoming healing
        incomingHealthBar:SetValue(0)
        incomingDirectHealthBar:SetValue(0)
        incomingHealText:SetText("")
    end
    incomingHealthBar:SetAlpha(0.35)
    incomingDirectHealthBar:SetAlpha(0.4)

    local r, g, b
    
    if profile.ShowDebuffColorsOn == "Health Bar" then
        r, g, b = self:GetDebuffColor()
    end

    if UnitIsCharmed(unit) and enemy then
        r, g, b = 0.25, 0.25, 0.25
    end
    
    if r == nil then -- If there's no debuff color, proceed to normal colors
        local hbc = enemy and profile.EnemyHealthBarColor or profile.HealthBarColor
        if hbc == "Class" then
            local class = util.GetClass(unit)
            if class == nil then
                class = self.fakeStats.class
            end
            r, g, b = util.GetClassColor(class)
        elseif hbc == "Green" then
            r, g, b = 0, 0.8, 0
        elseif hbc == "Green To Red" then
            r, g, b = util.InterpolateColorsNoTable(greenToRedColors, value)
        end

        if healthIncMaxRatio > 1 then
            local brightenFactor = math.min(((healthIncMaxRatio - 1) / 4) + 1, 1.25)
            r = math.min(r * brightenFactor, 1)
            g = math.min(g * brightenFactor, 1)
            b = math.min(b * brightenFactor, 1)
        end
    end
    healthBar:SetStatusBarColor(r, g, b)
    incomingHealthBar:SetStatusBarColor(0, 0.8, 0)
    incomingDirectHealthBar:SetStatusBarColor(0, 0.8, 0)

    local feign = self:GetCache():HasBuffIDOrName(5384, "Feign Death")
    local bg = healthBar.background
    if value == 0 and not feign then
        bg:SetTexture(0.5, 0.5, 0.5, 0.5)
    elseif value < 0.3 and not enemy and not feign then
        bg:SetTexture(1, 0.4, 0.4, 0.25)
    else
        bg:SetTexture(0.5, 0.5, 0.5, 0.25)
    end
end

-- Returns the r, g, b of the current dispellable debuff color, or nil if none
function PTUnitFrame:GetDebuffColor()
    local enemy = self:IsEnemy()
    if not enemy then -- Do not display debuff colors for enemies
        for _, trackedDebuffType in ipairs(PuppeteerSettings.TrackedDebuffTypes) do
            if self:GetAfflictedDebuffTypes()[trackedDebuffType] then
                local debuffTypeColor = PuppeteerSettings.DebuffTypeColors[trackedDebuffType]
                return debuffTypeColor[1], debuffTypeColor[2], debuffTypeColor[3]
            end
        end
    end

    local fake = self:IsFake()
    if fake and self.fakeStats.debuffType then
        local debuffTypeColor = PuppeteerSettings.DebuffTypeColors[self.fakeStats.debuffType]
        return debuffTypeColor[1], debuffTypeColor[2], debuffTypeColor[3]
    end
end

function PTUnitFrame:UpdatePower()
    local profile = self:GetProfile()
    local unit = self.unit
    local powerBar = self.powerBar
    local fake = self:IsFake()
    local class = fake and self.fakeStats.class or util.GetClass(unit)
    local currentPower = self:GetCurrentPower()
    local maxPower = self:GetMaxPower()

    if not UnitExists(self.unit) and not fake then
        powerBar:SetValue(0)
        self.powerText:SetText("")
        return
    end
    
    local powerColor = fake and util.PowerColors[util.ClassPowerTypes[class or "WARRIOR"]] or util.GetPowerColor(unit)

    powerBar:SetValue(currentPower / maxPower)
    powerBar:SetStatusBarColor(powerColor[1], powerColor[2], powerColor[3])
    local text = ""
    if profile.PowerDisplay == "Power" then
        text = currentPower
    elseif profile.PowerDisplay == "Power/Max Power" then
        text = currentPower.."/"..maxPower
    elseif profile.PowerDisplay == "% Power" then
        if maxPower == 0 then
            maxPower = 1
        end
        text = math.floor((currentPower / maxPower) * 100).."%"
    end
    self.powerText:SetText(text)
end

local AURA_DURATION_TEXT_FLASH_THRESHOLD = 5
local AURA_DURATION_TEXT_LOW_THRESHOLD = 30
-- A map of all seconds below the flash threshold to an array of colors to interpolate
local durationTextFlashColorsRange
if util.IsSuperWowPresent() then
    local flashColorsReset = {{1, 1, 0.75}, {1, 0.7, 0.55}, {1, 0.6, 0.6}}
    local flashColors = {{1, 1, 0.25}, {1, 0.6, 0.35}, {1, 0.4, 0.4}}

    local textFlashColors = {}
    for i = 0, AURA_DURATION_TEXT_FLASH_THRESHOLD + 1 do
        textFlashColors[i] = {}
        textFlashColors[i][1] = util.InterpolateColors(flashColors, 
            (AURA_DURATION_TEXT_FLASH_THRESHOLD - i) / AURA_DURATION_TEXT_FLASH_THRESHOLD)
        textFlashColors[i][2] = util.InterpolateColors(flashColorsReset, 
        (AURA_DURATION_TEXT_FLASH_THRESHOLD - i) / AURA_DURATION_TEXT_FLASH_THRESHOLD)
    end
    durationTextFlashColorsRange = {}
    for seconds, colors in pairs(textFlashColors) do
        durationTextFlashColorsRange[seconds] = {colors[2], colors[1]}
    end
end
function PTUnitFrame:AllocateAura()
    local frame = CreateFrame("Button", nil, self.auraPanel, "UIPanelButtonTemplate")
    frame.unitFrame = self
    frame:SetNormalTexture(nil)
    frame:SetHighlightTexture(nil)
    frame:SetPushedTexture(nil)
    local buttons = PTOptions.CastWhen == "Mouse Up" and util.GetUpButtons() or util.GetDownButtons()
    frame:RegisterForClicks(unpack(buttons))
    frame:EnableMouse(true)

    frame:SetScript("OnEnter", PTUnitFrame.Aura_OnEnter)
    frame:SetScript("OnLeave", PTUnitFrame.Aura_OnLeave)
    frame:SetScript("OnClick", PTUnitFrame.Aura_OnClick)
    frame:SetScript("OnMouseUp", PTUnitFrame.Aura_OnMouseUp)
    frame:SetScript("OnMouseDown", PTUnitFrame.Aura_OnMouseDown)
    
    local icon = frame:CreateTexture(nil, "ARTWORK")
    local border = frame:CreateTexture(nil, "OVERLAY")
    border:SetTexture("Interface\\Buttons\\UI-Debuff-Overlays")
    border:SetTexCoord(0.296875, 0.5703125, 0, 0.515625)
    border:SetPoint("TOPLEFT", icon, "TOPLEFT", -0.5, 0.5)
    border:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", 0.5, -0.5)
    local stackText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    stackText:SetTextColor(1, 1, 1)

    -- Duration display, only used when SuperWoW is present
    if util.IsSuperWowPresent() then
        local duration = CreateFrame("Model", nil, frame, "CooldownFrameTemplate")
        duration.noCooldownCount = true
        duration:SetAlpha(0.8)
        local durationOverlayFrame = CreateFrame("Frame", nil, frame)
        durationOverlayFrame:SetFrameLevel(durationOverlayFrame:GetFrameLevel() + 1)
        local durationText = durationOverlayFrame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
        durationText:SetPoint("BOTTOMLEFT", durationOverlayFrame, "BOTTOMLEFT", 0, 0)
        durationText.SetSeconds = function(self, seconds)
            self.seconds = seconds
            if seconds == nil then
                self:SetText("")
                return
            end
            self:SetText(seconds <= 60 and seconds or math.ceil(seconds / 60).."m")
            self:SetFont("Fonts\\FRIZQT__.TTF", math.ceil(frame:GetHeight() * 
                (seconds < 540 and (seconds < 10 and 0.6 or 0.45) or 0.35)), "OUTLINE")
        end
        duration.UpdateText = function()
            local seconds = duration.seconds
            local secondsPrecise = duration.secondsPrecise
            durationText:SetSeconds(seconds)
            if seconds <= AURA_DURATION_TEXT_FLASH_THRESHOLD then
                durationText:SetTextColor(
                    util.InterpolateColorsNoTable(durationTextFlashColorsRange[seconds], 
                    secondsPrecise - seconds))
            elseif seconds <= AURA_DURATION_TEXT_LOW_THRESHOLD then
                durationText:SetTextColor(1, 1, 0.25)
            else
                durationText:SetTextColor(1, 1, 1)
            end
            duration:SetScript("OnUpdate", nil)
        end
        local SetSequenceTime = duration.SetSequenceTime
        local GetTime = GetTime
        duration:SetScript("OnUpdateModel", function()
            if duration.stopping == 0 then
                duration:SetAlpha(0.8)
                local time = GetTime()
                local progress = (time - duration.start) / duration.duration
                if progress < 1.0 then
                    SetSequenceTime(duration, 0, 1000 - (progress * 1000))
                    local secondsPrecise = duration.start - time + duration.duration
                    local seconds = math.floor(secondsPrecise)
                    if seconds <= (duration.displayAt or AURA_DURATION_TEXT_FLASH_THRESHOLD) then
                        if durationText.seconds ~= seconds or seconds <= AURA_DURATION_TEXT_FLASH_THRESHOLD then
                            -- You don't want to know why it's gotta be done like this..
                            -- (If you're insane and you do, it's because otherwise the text will disappear for one frame otherwise)
                            duration.seconds = seconds
                            duration.secondsPrecise = secondsPrecise
                            duration:SetScript("OnUpdate", duration.UpdateText)
                        end
                    elseif durationText.seconds ~= nil then
                        durationText:SetSeconds(nil)
                    end
                    return
                end
                durationText:SetSeconds(nil)
                SetSequenceTime(duration, 0, 0)
            end
        end)
        return {["frame"] = frame, ["icon"] = icon, ["border"] = border, ["stackText"] = stackText, ["overlay"] = durationOverlayFrame, 
            ["durationText"] = durationText, ["duration"] = duration, ["durationEnabled"] = true}
    end
    return {["frame"] = frame, ["icon"] = icon, ["border"] = border, ["stackText"] = stackText}
end

-- Get an icon from the available pool. Automatically inserts into the used pool.
function PTUnitFrame:GetUnusedAura()
    local aura
    if table.getn(self.auraIconPool) > 0 then
        aura = table.remove(self.auraIconPool, table.getn(self.auraIconPool))
    else
        aura = self:AllocateAura()
    end
    aura.frame:SetAlpha(aura.frame:GetParent():GetAlpha())
    table.insert(self.auraIcons, aura)
    return aura
end

function PTUnitFrame:ReleaseAuras()
    if table.getn(self.auraIcons) == 0 then
        return
    end
    -- Release all icons back to the icon pool
    for _, aura in ipairs(self.auraIcons) do
        local frame = aura.frame
        frame:Hide()
        frame:ClearAllPoints()

        local icon = aura.icon
        icon:ClearAllPoints()

        local stackText = aura.stackText
        stackText:ClearAllPoints()
        stackText:SetText("")

        if aura.durationEnabled then
            aura.durationText:SetSeconds(nil)
            CooldownFrame_SetTimer(aura.duration, 0, 0, 0)
        end

        table.insert(self.auraIconPool, aura)
    end
    self.auraIcons = compost:Erase(self.auraIcons)
end

do
    local trackedBuffs = PuppeteerSettings.TrackedBuffs
    function PTUnitFrame.BuffSorter(a, b)
        return trackedBuffs[a.name] < trackedBuffs[b.name]
    end
end

do
    local trackedDebuffs = PuppeteerSettings.TrackedDebuffs
    function PTUnitFrame.DebuffSorter(a, b)
        return trackedDebuffs[a.name] < trackedDebuffs[b.name]
    end
end

function PTUnitFrame:UpdateAuras()
    local profile = self:GetProfile()
    local unit = self.unit
    local enemy = self:IsEnemy()

    self:ReleaseAuras()

    local cache = self:GetCache()
    
    local trackedBuffs = PuppeteerSettings.TrackedBuffs

    local buffs = compost:GetTable() -- Buffs that are tracked because of matching name
    for name, array in pairs(cache.BuffsMap) do
        if trackedBuffs[name] or enemy then
            util.AppendArrayElements(buffs, array)
        end
    end

    if not enemy then
        table.sort(buffs, self.BuffSorter)
    end
    

    local trackedDebuffs = PuppeteerSettings.TrackedDebuffs
    local trackedDebuffTypes = PuppeteerSettings.TrackedDebuffTypesSet

    local debuffs = compost:GetTable() -- Debuffs that are tracked because of matching name, later combined with typed debuffs
    local typedDebuffs = compost:GetTable() -- Debuffs that are tracked because it's a tracked type (like "Magic" or "Disease")
    for name, array in pairs(cache.DebuffsMap) do
        if trackedDebuffs[name] or enemy then
            util.AppendArrayElements(debuffs, array)
        else
            -- Check if debuff is a tracked type
            for _, debuff in ipairs(array) do
                if trackedDebuffTypes[debuff.type] then
                    table.insert(typedDebuffs, debuff)
                end
            end
        end
    end

    if not enemy then
        table.sort(debuffs, self.DebuffSorter)
        util.AppendArrayElements(debuffs, typedDebuffs)
    end

    local auraTrackerProps = profile.AuraTracker
    local width = auraTrackerProps.Width == "Anchor" and auraTrackerProps:GetAnchorComponent(self):GetWidth() or auraTrackerProps.Width
    local auraSize = auraTrackerProps.Height
    local origSize = auraSize
    local spacing = profile.TrackedAurasSpacing
    local origSpacing = spacing
    local auraCount = table.getn(buffs) + table.getn(debuffs)

    -- If there's not enough space, shrink until all auras fit
    while ((auraSize * auraCount) + (spacing * (auraCount - 1)) > width) and auraSize >= 1 do
        auraSize = auraSize - 1
        spacing = (auraSize / origSize) * origSpacing
    end

    local xOffset = 0
    local yOffset = profile.TrackedAurasAlignment == "TOP" and 0 or origSize - auraSize
    for _, buff in ipairs(buffs) do
        local aura = self:GetUnusedAura()
        self:CreateAura(aura, buff.name, buff.index, buff.texture, buff.stacks, buff.type, xOffset, -yOffset, "Buff", auraSize)
        xOffset = xOffset + auraSize + spacing
    end
    xOffset = 0
    for _, debuff in ipairs(debuffs) do
        local aura = self:GetUnusedAura()
        self:CreateAura(aura, debuff.name, debuff.index, debuff.texture, debuff.stacks, debuff.type, xOffset, -yOffset, "Debuff", auraSize)
        xOffset = xOffset - auraSize - spacing
    end
    compost:Reclaim(buffs)
    compost:Reclaim(debuffs)
    compost:Reclaim(typedDebuffs)

    -- Prevent lingering tooltips when the icon is removed or is changed to a different aura
    if not PT.GameTooltip.OwningFrame or not PT.GameTooltip.OwningFrame:IsShown() or 
            PT.GameTooltip.IconTexture ~= PT.GameTooltip.OwningIcon:GetTexture() then
        PT.GameTooltip:Hide()
    end
end


function PTUnitFrame.Aura_OnEnter()
    local self = this.unitFrame
    local aura = this.aura
    local index = this.auraIndex
    local type = this.auraType

    local tooltip = PT.GameTooltip
    local cache = self:GetCache()
    tooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT")
    tooltip.OwningFrame = this
    tooltip.OwningIcon = aura.icon
    local unit = self:GetResolvedUnit()
    if type == "Buff" then
        tooltip.IconTexture = cache.Buffs[index].texture
        tooltip:SetUnitBuff(unit, index)
    else
        tooltip.IconTexture = cache.Debuffs[index].texture
        tooltip:SetUnitDebuff(unit, index)
    end
    local auraTime = cache.AuraTimes[(type == "Buff" and cache.Buffs[index] or cache.Debuffs[index]).name]
    if auraTime then
        local seconds = math.floor(auraTime.startTime - GetTime() + auraTime.duration)
        if seconds < 60 then
            tooltip:AddLine(seconds.." second"..(seconds ~= 1 and "s" or "").." remaining")
        else
            tooltip:AddLine(math.ceil(seconds / 60).." minutes remaining")
        end
    end
    tooltip:Show()

    if MouseIsOver(self.button) then
        self.button:GetScript("OnEnter")()
    end
end

function PTUnitFrame.Aura_OnLeave()
    PT.GameTooltip:Hide()
    PT.GameTooltip.OwningFrame = nil
    PT.GameTooltip.OwningIcon = nil
    PT.GameTooltip.IconTexture = nil
    -- Don't check mouse position for leaving, because it could cause the tooltip to stay if the icon is on the edge
    this.unitFrame.button:GetScript("OnLeave")()
end

do
    local wrapButtonScript = function(scriptName)
        return function()
            local self = this.unitFrame
            if MouseIsOver(self.button) then
                self.button:GetScript(scriptName)()
            end
        end
    end

    PTUnitFrame.Aura_OnClick = wrapButtonScript("OnClick")

    PTUnitFrame.Aura_OnMouseUp = wrapButtonScript("OnMouseUp")

    PTUnitFrame.Aura_OnMouseDown = wrapButtonScript("OnMouseDown")
end

local debuffTypeBorderColors = {
    ["Magic"] = {0.2, 0.6, 1.0},
    ["Curse"] = {0.6, 0.0, 1.0},
    ["Disease"] = {0.6, 0.4, 0},
    ["Poison"] = {0.0, 0.6, 0},
    ["Other"] = {1, 0, 0}
}
function PTUnitFrame:CreateAura(aura, name, index, texturePath, stacks, auraType, xOffset, yOffset, type, size)
    local frame = aura.frame
    frame:SetWidth(size)
    frame:SetHeight(size)
    frame:SetPoint(type == "Buff" and "TOPLEFT" or "TOPRIGHT", xOffset, yOffset)
    frame:Show()
    frame.unitFrame = self
    frame.aura = aura
    frame.auraIndex = index
    frame.auraType = type

    local icon = aura.icon
    icon:SetAllPoints(frame)
    icon:SetTexture(texturePath)

    if aura.durationEnabled then
        local overlay = aura.overlay
        overlay:SetAllPoints()

        local duration = aura.duration
        duration:SetAllPoints()
        duration:SetScale(size * 0.0275)
    end
    
    if stacks > 1 then
        local stackText = aura.stackText
        stackText:SetPoint("CENTER", frame, "CENTER", 0, 0)
        stackText:SetFont("Fonts\\FRIZQT__.TTF", math.ceil(size * (stacks < 10 and 0.75 or 0.6)))
        stackText:SetText(stacks)
    end

    if type == "Buff" then
        aura.border:Hide()
    else
        local border = aura.border
        border:Show()
        local color = debuffTypeBorderColors[auraType] or debuffTypeBorderColors["Other"]
        border:SetVertexColor(color[1], color[2], color[3])
    end

    if aura.durationEnabled then
        local cache = self:GetCache()
        if cache.AuraTimes[name] then
            local debuffTime = cache.AuraTimes[name]
            local start = debuffTime["startTime"]
            local duration = debuffTime["duration"]
            local durationUI = aura.duration

            CooldownFrame_SetTimer(durationUI, start, duration, 1)

            if duration < 60 then
                durationUI.displayAt = PTOptions.ShowAuraTimesAt.Short
            elseif duration <= 60 * 2 then
                durationUI.displayAt = PTOptions.ShowAuraTimesAt.Medium
            else
                durationUI.displayAt = PTOptions.ShowAuraTimesAt.Long
            end

            -- To prevent having a frame where the duration is not updated
            aura.durationText:SetSeconds(nil)
            util.CallWithThis(durationUI, durationUI:GetScript("OnUpdateModel"))
        end
    end
end

function PTUnitFrame:Initialize()
    local unit = self.unit

    local profile = self:GetProfile()

    -- Container Elements

    local rootContainer = CreateFrame("Frame", "PTUnitFrame_"..unit, UIParent)
    self.rootContainer = rootContainer
    rootContainer:SetPoint("CENTER", 0, 0)

    local container = CreateFrame("Frame", "$parentContainer", rootContainer)
    self.container = container
    container:SetAllPoints(rootContainer)

    local overlayContainer = CreateFrame("Frame", "$parentOverlayContainer", rootContainer)
    self.overlayContainer = overlayContainer
    overlayContainer:SetFrameLevel(container:GetFrameLevel() + 5)
    overlayContainer:SetAllPoints(rootContainer)

    -- Distance Text

    local distanceText = overlayContainer:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    self.distanceText = distanceText
    distanceText:SetAlpha(profile.RangeText:GetAlpha())

    local losFrame = CreateFrame("Frame", nil, container)
    losFrame:SetFrameLevel(container:GetFrameLevel() + 3)
    local losIcon = losFrame:CreateTexture(nil, "OVERLAY")
    self.lineOfSightIcon = {frame = losFrame, icon = losIcon}
    losIcon:SetTexture("Interface\\Icons\\Spell_nature_sleep")
    losIcon:SetAlpha(profile.LineOfSightIcon:GetAlpha())
    losFrame:Hide()

    -- Role Icon

    local roleFrame = CreateFrame("Frame", nil, container)
    roleFrame:SetFrameLevel(container:GetFrameLevel() + 3)
    local roleIcon = roleFrame:CreateTexture(nil, "OVERLAY")
    self.roleIcon = {frame = roleFrame, icon = roleIcon}
    roleIcon:SetAlpha(profile.RoleIcon:GetAlpha())
    roleFrame:Hide()

    -- Raid Mark Icon

    local raidMarkFrame = CreateFrame("Frame", nil, container)
    raidMarkFrame:SetFrameLevel(container:GetFrameLevel() + 3)
    local raidMarkIcon = raidMarkFrame:CreateTexture(nil, "OVERLAY")
    self.raidMarkIcon = {frame = raidMarkFrame, icon = raidMarkIcon}
    raidMarkIcon:SetAlpha(profile.RaidMarkIcon:GetAlpha())
    raidMarkIcon:SetTexture("Interface\\TARGETINGFRAME\\UI-RaidTargetingIcons")
    raidMarkFrame:Hide()

    -- PVP Icon

    local pvpFrame = CreateFrame("Frame", nil, container)
    pvpFrame:SetFrameLevel(container:GetFrameLevel() + 4)
    local pvpIcon = pvpFrame:CreateTexture(nil, "OVERLAY")
    self.pvpIcon = {frame = pvpFrame, icon = pvpIcon}
    pvpIcon:SetAlpha(profile.PVPIcon:GetAlpha())
    pvpIcon:SetTexture("Interface\\TargetingFrame\\UI-PVP-Alliance")
    pvpIcon:SetTexCoord(3 / 64, 39 / 64, 2 / 64, 38 / 64)
    pvpFrame:Hide()

    -- Health Bar Element

    local healthBar = CreateFrame("StatusBar", "$parentHealthBar", container)
    self.healthBar = healthBar
    healthBar:SetStatusBarTexture(PT.BarStyles[profile.HealthBarStyle])
    healthBar:SetMinMaxValues(0, 1)

    local incomingHealthBar = CreateFrame("StatusBar", "$parentIncomingHealthBar", container)
    self.incomingHealthBar = incomingHealthBar
    incomingHealthBar:SetStatusBarTexture(PT.BarStyles[profile.HealthBarStyle])
    incomingHealthBar:SetMinMaxValues(0, 1)
    incomingHealthBar:SetFrameLevel(healthBar:GetFrameLevel() - 1)

    local incomingDirectHealthBar = CreateFrame("StatusBar", "$parentIncomingDirectHealthBar", container)
    self.incomingDirectHealthBar = incomingDirectHealthBar
    incomingDirectHealthBar:SetStatusBarTexture(PT.BarStyles[profile.HealthBarStyle])
    incomingDirectHealthBar:SetMinMaxValues(0, 1)
    incomingDirectHealthBar:SetFrameLevel(healthBar:GetFrameLevel() - 1)

    -- Name Element

    local name = healthBar:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    self.nameText = name
    name:SetAlpha(profile.NameText:GetAlpha())

    local bg = container:CreateTexture(nil, "BACKGROUND")
    healthBar.background = bg
    bg:SetAllPoints(healthBar)
    bg:SetTexture(0.5, 0.5, 0.5, 0.25)

    -- Incoming Text
    local incomingHealText = overlayContainer:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    self.incomingHealText = incomingHealText

    -- Missing Health Text

    local missingHealthText = healthBar:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    self.missingHealthText = missingHealthText
    missingHealthText:SetAlpha(profile.HealthTexts.Missing:GetAlpha())

    -- Power Bar Element

    local powerBar = CreateFrame("StatusBar", "$parentPowerStatusBar", container)
    self.powerBar = powerBar
    powerBar:SetStatusBarTexture(PT.BarStyles[profile.PowerBarStyle])
    powerBar:SetMinMaxValues(0, 1)
    powerBar:SetValue(1)
    powerBar:SetStatusBarColor(0, 0, 1)
    powerBar:SetBackdrop({bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background"})
    local powerText = powerBar:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    self.powerText = powerText
    powerText:SetAlpha(profile.PowerText:GetAlpha())

    -- Button Element

    local button = CreateFrame("Button", "$parentButton", healthBar, "UIPanelButtonTemplate")
    self.button = button
    local healthText = button:GetFontString()
    self.healthText = healthText
    healthText:ClearAllPoints()


    healthText = healthBar:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    self.healthText = healthText

    self:RegisterClicks()
    button:SetScript("OnClick", function()
        local buttonType = arg1
        PT.UnitFrame_OnClick(buttonType, unit, self)
    end)
    button:SetScript("OnMouseDown", function()
        local buttonType = arg1
        PT.CurrentlyHeldButton = buttonType
        PT.ReapplySpellsTooltip()
        self.pressed = true
        self:AdjustHealthPosition()
    end)
    button:SetScript("OnMouseUp", function()
        PT.CurrentlyHeldButton = nil
        PT.ReapplySpellsTooltip()
        self.pressed = false
        self:AdjustHealthPosition()
    end)
    button:SetScript("OnEnter", function()
        local attachTooltipTo
        if PTOptions.SpellsTooltip.AttachTo == "Frame" then
            attachTooltipTo = self.rootContainer
        elseif PTOptions.SpellsTooltip.AttachTo == "Group" then
            attachTooltipTo = self.owningGroup:GetContainer()
        elseif PTOptions.SpellsTooltip.AttachTo == "Screen" then
            attachTooltipTo = UIParent
        else
            attachTooltipTo = self.button
        end
        PT.ApplySpellsTooltip(attachTooltipTo, unit, self.button)
        self.hovered = true
        self:UpdateHealth()
        if PTOptions.SetMouseover and util.IsSuperWowPresent() then
            SetMouseoverUnit(self:GetResolvedUnit())
        end
        PT.Mouseover = self:GetUnit()
        PT.MouseoverFrame = self
        PT.ApplyOverrideBindings()
    end)
    button:SetScript("OnLeave", function()
        PT.HideSpellsTooltip()
        self.hovered = false
        self:UpdateHealth()
        if PTOptions.SetMouseover and util.IsSuperWowPresent() then
            SetMouseoverUnit(nil)
        end
        PT.Mouseover = nil
        PT.MouseoverFrame = nil
        if PT.CurrentlyHeldButton and not util.GetAllButtonsSet()[PT.CurrentlyHeldButton] then
            self.pressed = false
            self:AdjustHealthPosition()
            PT.CurrentlyHeldButton = nil
            PT.ReapplySpellsTooltip()
        end
        PT.RemoveOverrideBindings()
    end)
    button:EnableMouse(true)

    button:SetNormalTexture(nil)
    button:SetHighlightTexture(nil)
    button:SetPushedTexture(nil)

    -- Buff Panel Element

    local buffPanel = CreateFrame("Frame", "$parentBuffPanel", container)
    self.auraPanel = buffPanel
    buffPanel:SetFrameLevel(container:GetFrameLevel() + 2)

    local targetOutline = CreateFrame("Frame", "$parentTargetOutline", rootContainer)
    self.targetOutline = targetOutline
    targetOutline:SetBackdrop({edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = profile.TargetOutline.Thickness})
    targetOutline:SetFrameLevel(container:GetFrameLevel() + 10)
    targetOutline:Hide()

    local flashFrame = CreateFrame("Frame", "$parentFlash", container)
    flashFrame:SetFrameLevel(container:GetFrameLevel() + 9)
    flashFrame.unitFrame = self
    local flashTexture = flashFrame:CreateTexture(nil, "OVERLAY")
    self.flashTexture = {frame = flashFrame, texture = flashTexture}
    flashTexture:SetTexture(1, 1, 1)
    flashFrame:Hide()

    self:SetHealthBarValue(0)
    self:SizeElements()
end

function PTUnitFrame:SizeElements()
    local profile = self:GetProfile()
    local width = profile.Width
    local healthBarHeight = profile.HealthBarHeight
    local powerBarHeight = profile.PowerBarHeight

    local rootContainer = self.rootContainer
    rootContainer:SetWidth(width)

    local overlayContainer = self.overlayContainer
    overlayContainer:SetWidth(width)

    local container = self.container
    container:SetWidth(width)

    local healthBar = self.healthBar
    healthBar:SetWidth(width)
    healthBar:SetHeight(healthBarHeight)
    healthBar:SetPoint("TOPLEFT", container, "TOPLEFT", 0, -profile.PaddingTop)

    local incomingHealthBar = self.incomingHealthBar
    incomingHealthBar:SetWidth(width)
    incomingHealthBar:SetHeight(healthBarHeight)
    incomingHealthBar:SetPoint("TOPLEFT", container, "TOPLEFT", 0, -profile.PaddingTop)

    local directIncomingHealthBar = self.incomingDirectHealthBar
    directIncomingHealthBar:SetWidth(width)
    directIncomingHealthBar:SetHeight(healthBarHeight)
    directIncomingHealthBar:SetPoint("TOPLEFT", container, "TOPLEFT", 0, -profile.PaddingTop)

    local powerBar = self.powerBar
    powerBar:SetWidth(width)
    powerBar:SetHeight(powerBarHeight)
    powerBar:SetPoint("TOPLEFT", healthBar, "BOTTOMLEFT", 0, 0)

    local button = self.button
    button:SetWidth(healthBar:GetWidth())
    button:SetHeight(healthBar:GetHeight() + powerBar:GetHeight())
    button:SetPoint("TOP", 0, 0)

    local name = self.nameText
    self:UpdateComponent(name, profile.NameText)

    self:AdjustHealthPosition()

    local powerText = self.powerText
    self:UpdateComponent(powerText, profile.PowerText)

    local incomingHealText = self.incomingHealText
    self:UpdateComponent(incomingHealText, profile.IncomingHealText)

    local distanceText = self.distanceText
    self:UpdateComponent(distanceText, profile.RangeText)

    local losFrame = self.lineOfSightIcon.frame
    self:UpdateComponent(self.lineOfSightIcon.frame, profile.LineOfSightIcon)

    local losIcon = self.lineOfSightIcon.icon
    losIcon:SetAllPoints(losFrame)

    local roleFrame = self.roleIcon.frame
    self:UpdateComponent(roleFrame, profile.RoleIcon)

    local roleIcon = self.roleIcon.icon
    roleIcon:SetAllPoints(roleFrame)

    local raidMarkFrame = self.raidMarkIcon.frame
    self:UpdateComponent(raidMarkFrame, profile.RaidMarkIcon)

    local raidMarkIcon = self.raidMarkIcon.icon
    raidMarkIcon:SetAllPoints(raidMarkFrame)

    local pvpFrame = self.pvpIcon.frame
    self:UpdateComponent(pvpFrame, profile.PVPIcon)

    local pvpIcon = self.pvpIcon.icon
    pvpIcon:SetAllPoints(pvpFrame)

    local auraPanel = self.auraPanel
    self:UpdateComponent(auraPanel, profile.AuraTracker)

    self:UpdateComponent(self.targetOutline, profile.TargetOutline)

    self:UpdateComponent(self.flashTexture.frame, profile.Flash)
    self.flashTexture.texture:SetAllPoints(self.flashTexture.frame)

    rootContainer:SetHeight(self:GetHeight())
    overlayContainer:SetHeight(self:GetHeight())
    container:SetHeight(self:GetHeight())
end

function PTUnitFrame:AdjustHealthPosition()
    local profile = self:GetProfile()

    local healthTexts = profile.HealthTexts
    local healthTextProps = (self:ShouldShowMissingHealth() and not profile.MissingHealthInline) and 
        healthTexts.WithMissing or healthTexts.Normal
    local missingHealthTextProps = healthTexts.Missing

    local xOffset, yOffset
    if self.pressed then
        xOffset, yOffset = 1, -1
    end
    self:UpdateComponent(self.healthText, healthTextProps, xOffset, yOffset)
    self:UpdateComponent(self.missingHealthText, missingHealthTextProps, xOffset, yOffset)
end

local alignmentAnchorMap = {
    ["LEFT"] = {
        ["TOP"] = "TOPLEFT",
        ["CENTER"] = "LEFT",
        ["BOTTOM"] = "BOTTOMLEFT",
    },
    ["CENTER"] = {
        ["TOP"] = "TOP",
        ["CENTER"] = "CENTER",
        ["BOTTOM"] = "BOTTOM",
    },
    ["RIGHT"] = {
        ["TOP"] = "TOPRIGHT",
        ["CENTER"] = "RIGHT",
        ["BOTTOM"] = "BOTTOMRIGHT",
    }
}
function PTUnitFrame:UpdateComponent(component, props, xOffset, yOffset)
    xOffset = xOffset or 0
    yOffset = yOffset or 0

    local anchor = props:GetAnchorComponent(self)

    component:ClearAllPoints()
    if component.SetFont then -- Must be a FontString
        component:SetWidth(math.min(props.MaxWidth, anchor:GetWidth()))
        component:SetHeight(props.FontSize * 1.25)
        component:SetFont("Fonts\\FRIZQT__.TTF", props.FontSize, props.Outline and "OUTLINE" or nil)
        if props.Outline then
            component:SetShadowOffset(0, 0)
        end
        component:SetJustifyH(props.AlignmentH)
        component:SetJustifyV(props.AlignmentV)
    else
        component:SetWidth(props:GetWidth(self))
        component:SetHeight(props:GetHeight(self))
    end
    local alignment = alignmentAnchorMap[props.AlignmentH][props.AlignmentV]
    component:SetPoint(alignment, anchor, alignment, props:GetOffsetX() + xOffset, props:GetOffsetY() + yOffset)
end

function PTUnitFrame:GetCache()
    return PTUnit.Get(self.unit)
end

function PTUnitFrame:GetAfflictedDebuffTypes()
    return self:GetCache().AfflictedDebuffTypes
end

function PTUnitFrame:GetWidth()
    return self:GetProfile().Width
end

function PTUnitFrame:GetHeight()
    return self:GetProfile():GetHeight()
end

function PTUnitFrame:GetName()
    if self:IsFake() then
        return self.fakeStats.name
    end
    return UnitName(self.unit)
end

function PTUnitFrame:GetClass()
    if self:IsFake() then
        return self.fakeStats.class
    end
    return util.GetClass(self.unit)
end

function PTUnitFrame:IsPlayer()
    return UnitIsPlayer(self.unit)
end

function PTUnitFrame:IsEnemy()
    return UnitCanAttack("player", self.unit)
end

function PTUnitFrame:IsFake()
    return Puppeteer.TestUI and not UnitExists(self.unit)
end

function PTUnitFrame:GetRole()
    return Puppeteer.GetUnitAssignedRole(self:GetUnit())
end

function PTUnitFrame:HasAggro()
    local unit = self:GetUnit()
    if self.isCustomUnit then
        if not self.guidUnit then
            return false
        end
        unit = PTUnitProxy.ResolveCustomUnit(self.guidUnit)
        if not unit then
            return false
        end
    end
    return Puppeteer.Banzai:GetUnitAggroByUnitId(unit)
end

local roleTexturesPath = PTUtil.GetAssetsPath().."textures\\roles\\"
local roleTextures = {
    ["Tank"] = roleTexturesPath.."Tank",
    ["Healer"] = roleTexturesPath.."Healer",
    ["Damage"] = roleTexturesPath.."Damage"
}
function PTUnitFrame:UpdateRole()
    local role = self:GetRole()
    self.roleIcon.icon:SetTexture(roleTextures[role])
    if role then
        self.roleIcon.frame:Show()
    else
        self.roleIcon.frame:Hide()
    end
end

function PTUnitFrame:GetProfile()
    return self.owningGroup:GetProfile()
end