---@class addonTablePlatynator
local addonTable = select(2, ...)

local LSM = LibStub("LibSharedMedia-3.0")

local pandemicCurve
local pandemicPercentage = 0.3
local dispelCurve
local dispelColorMap = {
  ["Magic"] = CreateColorFromHexString("ff007ffb"),
  ["Curse"] = CreateColorFromHexString("ffb534ed"),
  ["Disease"] = CreateColorFromHexString("fffc982f"),
  ["Poison"] = CreateColorFromHexString("ffe5fb00"),
  [""] = CreateColorFromHexString("ffff0000"),
  ["Bleed"] = CreateColorFromHexString("fffc0318"),
}
if C_CurveUtil then
  pandemicCurve = C_CurveUtil.CreateCurve()
  pandemicCurve:SetType(Enum.LuaCurveType.Step)
  pandemicCurve:AddPoint(0, 1)
  pandemicCurve:AddPoint(pandemicPercentage, 0)

  dispelCurve = C_CurveUtil.CreateColorCurve()
  dispelCurve:SetType(Enum.LuaCurveType.Step)
  dispelCurve:AddPoint(0, CreateColor(0, 0, 0, 0))
  dispelCurve:AddPoint(1, dispelColorMap["Magic"])
  dispelCurve:AddPoint(2, dispelColorMap["Curse"])
  dispelCurve:AddPoint(3, dispelColorMap["Disease"])
  dispelCurve:AddPoint(4, dispelColorMap["Poison"])
  dispelCurve:AddPoint(5, CreateColor(0, 0, 0, 0))
  dispelCurve:AddPoint(9, dispelColorMap[""])
  dispelCurve:AddPoint(10, CreateColor(0, 0, 0, 0))
  dispelCurve:AddPoint(11, dispelColorMap["Bleed"])
end

local auraFormatter = addonTable.Display.Utilities.GetAuraNumericFormatter()

local function GetAurasPoolModern(self)
  local borderAsset = LSM:Fetch("nineslice", "Platy: 1px")
  local dispelAsset = LSM:Fetch("nineslice", "Platy: 4px")
  return CreateFramePool("AuraButton", self, "CustomAuraButtonTemplate", function(_, f)
    f:ClearAllPoints()
  end, false, function(frame)
    frame:SetFlattensRenderLayers(true)
    frame:SetSize(20, 20)
    frame.Icon = frame:CreateTexture(nil, "ARTWORK")
    frame.Icon:SetSize(20, 20)
    frame.Icon:SetPoint("CENTER")
    frame.Cooldown = CreateFrame("Cooldown", nil, frame, "CooldownFrameTemplate")
    frame.Cooldown:SetHideCountdownNumbers(false)
    frame.Cooldown:SetDrawEdge(true)
    frame.Cooldown:SetReverse(true)
    frame.CountFrame = CreateFrame("Frame", nil, frame)
    frame.CountFrame:SetAllPoints()
    frame.CountFrame:SetFrameLevel(500)
    frame.CountFrame.Count = frame.CountFrame:CreateFontString(nil, nil, "GameFontHighlight")
    frame.CountFrame.Count:SetPoint("BOTTOMRIGHT", 3, -2)

    frame.Border = frame:CreateTexture(nil, "OVERLAY")
    frame.Border:SetAllPoints(true)
    frame.Border:SetScale(borderAsset.scaleModifier)
    frame.Border:SetTexture(borderAsset.file)
    frame.Border:SetTextureSliceMargins(borderAsset.margins.left, borderAsset.margins.top, borderAsset.margins.right, borderAsset.margins.bottom)
    frame.Border:SetVertexColor(0, 0, 0)
    frame.Cooldown.Text = frame.Cooldown:GetCountdownFontString()
    frame.Dispel = CreateFrame("Frame", nil, frame)
    frame.Dispel:SetAllPoints()
    do
      local dispelTexture = frame.Dispel:CreateTexture()
      dispelTexture:SetAllPoints()
      dispelTexture:SetScale(dispelAsset.scaleModifier)
      dispelTexture:SetTexture(dispelAsset.file)
      dispelTexture:SetTextureSliceMargins(dispelAsset.margins.left, dispelAsset.margins.top, dispelAsset.margins.right, dispelAsset.margins.bottom)
      dispelTexture:SetVertexColor(1, 0, 0)
      frame.Dispel.Border = dispelTexture
    end

    frame:SetApplicationCount(frame.CountFrame.Count, {})
    frame:SetIcon(frame.Icon)
    frame:SetDurationCooldown(frame.Cooldown)
    frame:SetAuraBorder(frame.Dispel.Border, {showIcon = false, showWhenHarmful = true, showWhenHelpful = true, style = 1})
  end, 30)
end

local auraContainerSharedPool
if addonTable.Constants.IsMidnightNext then
  EventUtil.ContinueOnPlayerLogin(function()
    auraContainerSharedPool = CreateFramePool("AuraContainer", UIParent, "CustomAuraContainerTemplate", function(_, f) f:ClearAllPoints() f:Hide() f:SetParent(UIParent) end, false, function(frame)
      frame.pool = GetAurasPoolModern(frame)
      frame:SetSize(10, 10)
    end, 120)
  end)
end

local function GetAurasPoolLegacy(self)
  local borderAsset = LSM:Fetch("nineslice", "Platy: 1px")
  local dispelAsset = LSM:Fetch("nineslice", "Platy: 4px")
  return CreateFramePool("Frame", self, "PlatynatorNameplateBuffButtonTemplate", nil, false, function(frame)
    frame.Border = frame:CreateTexture(nil, "OVERLAY")
    frame.Border:SetAllPoints(true)
    frame.Border:SetScale(borderAsset.scaleModifier)
    frame.Border:SetTexture(borderAsset.file)
    frame.Border:SetTextureSliceMargins(borderAsset.margins.left, borderAsset.margins.top, borderAsset.margins.right, borderAsset.margins.bottom)
    frame.Border:SetVertexColor(0, 0, 0)
    frame.Cooldown.Text = frame.Cooldown:GetRegions()
    frame.Pandemic = CreateFrame("Frame", nil, frame)
    frame.Pandemic:SetAllPoints()
    frame.Pandemic.Animation = frame.Pandemic:CreateAnimationGroup()
    frame.Pandemic:SetFrameLevel(frame.Cooldown:GetFrameLevel() + 5)
    do
      frame.Pandemic.Top = frame.Pandemic:CreateTexture()
      frame.Pandemic.Top:SetPoint("TOPLEFT")
      frame.Pandemic.Top:SetPoint("TOPRIGHT")
      frame.Pandemic.Top:SetTexture("Interface/AddOns/Platynator/Assets/Special/pandemic.png")
      frame.Pandemic.Bottom = frame.Pandemic:CreateTexture()
      frame.Pandemic.Bottom:SetPoint("BOTTOMLEFT")
      frame.Pandemic.Bottom:SetPoint("BOTTOMRIGHT")
      frame.Pandemic.Bottom:SetTexture("Interface/AddOns/Platynator/Assets/Special/pandemic.png")
      frame.Pandemic.Bottom:SetRotation(math.pi)
      frame.Pandemic.Left = frame.Pandemic:CreateTexture()
      frame.Pandemic.Left:SetPoint("TOPLEFT")
      frame.Pandemic.Left:SetPoint("BOTTOMLEFT")
      frame.Pandemic.Left:SetTexture("Interface/AddOns/Platynator/Assets/Special/pandemic-90.png")
      frame.Pandemic.Right = frame.Pandemic:CreateTexture()
      frame.Pandemic.Right:SetPoint("TOPRIGHT")
      frame.Pandemic.Right:SetPoint("BOTTOMRIGHT")
      frame.Pandemic.Right:SetTexture("Interface/AddOns/Platynator/Assets/Special/pandemic-90.png")
      frame.Pandemic.Right:SetRotation(math.pi)
      local fb = frame.Pandemic.Animation:CreateAnimation("Flipbook")
      fb:SetFlipBookColumns(1)
      fb:SetFlipBookRows(11)
      fb:SetDuration(0.5)
      fb:SetTarget(frame.Pandemic.Top)
      local fb = frame.Pandemic.Animation:CreateAnimation("Flipbook")
      fb:SetFlipBookColumns(1)
      fb:SetFlipBookRows(11)
      fb:SetDuration(0.5)
      fb:SetTarget(frame.Pandemic.Bottom)
      local fb = frame.Pandemic.Animation:CreateAnimation("Flipbook")
      fb:SetFlipBookColumns(11)
      fb:SetFlipBookRows(1)
      fb:SetDuration(0.5)
      fb:SetTarget(frame.Pandemic.Left)
      local fb = frame.Pandemic.Animation:CreateAnimation("Flipbook")
      fb:SetFlipBookColumns(11)
      fb:SetFlipBookRows(1)
      fb:SetDuration(0.5)
      fb:SetTarget(frame.Pandemic.Right)
      frame.Pandemic.Animation:SetLooping("REPEAT")
      frame.Pandemic.Animation:Play()
      function frame.Pandemic:SetVertexColor(...)
        frame.Pandemic.Top:SetVertexColor(...)
        frame.Pandemic.Bottom:SetVertexColor(...)
        frame.Pandemic.Left:SetVertexColor(...)
        frame.Pandemic.Right:SetVertexColor(...)
      end
    end
    frame.Dispel = CreateFrame("Frame", nil, frame)
    frame.Dispel:SetAllPoints()
    do
      local dispelTexture = frame.Dispel:CreateTexture()
      dispelTexture:SetAllPoints()
      dispelTexture:SetScale(dispelAsset.scaleModifier)
      dispelTexture:SetTexture(dispelAsset.file)
      dispelTexture:SetTextureSliceMargins(dispelAsset.margins.left, dispelAsset.margins.top, dispelAsset.margins.right, dispelAsset.margins.bottom)
      dispelTexture:SetVertexColor(1, 0, 0)
      frame.Dispel.Border = dispelTexture
    end
    frame:SetScript("OnEnter", function()
      GameTooltip_SetDefaultAnchor(GameTooltip, frame)
      if GameTooltip.SetUnitAuraByAuraInstanceID then
        GameTooltip:SetUnitAuraByAuraInstanceID(self.unit, frame.auraInstanceID)
      elseif frame.auraIndex then
        if frame.auraIndex ~= -1 then
          GameTooltip:SetUnitAura(self.unit, frame.auraIndex, frame.auraFilter)
          GameTooltip:Show()
        end
      else
        local index = 1
        while true do
          local aura = C_UnitAuras.GetAuraDataByIndex(self.unit, index, frame.auraFilter)
          if not aura then
            break
          end
          if aura.auraInstanceID == frame.auraInstanceID then
            frame.auraIndex = index
            break
          end
          index = index + 1
        end

        if frame.auraIndex then
          GameTooltip:SetUnitAura(self.unit, frame.auraIndex, frame.auraFilter)
          GameTooltip:Show()
        else
          frame.auraIndex = -1
        end
      end
    end)
    frame:SetScript("OnLeave", function()
      GameTooltip:Hide()
    end)
  end)
end

local function StyleAura(auraFrame, details)
  auraFrame.kind = details.kind

  auraFrame:EnableMouseMotion(details.showTooltips)

  auraFrame.CountFrame.Count:SetFontObject(addonTable.CurrentFont)
  auraFrame.CountFrame.Count:ClearAllPoints()
  addonTable.Display.ApplyAnchor(auraFrame.CountFrame.Count, details.texts.stacks.anchor, addonTable.CurrentFontUsesSmoothing and 1/details.texts.stacks.scale or 1)
  if addonTable.CurrentFontUsesSmoothing then
    auraFrame.CountFrame.Count:SetTextScale(1)
    auraFrame.CountFrame.Count:SetScale(details.texts.stacks.scale)
  else
    auraFrame.CountFrame.Count:SetTextScale(details.texts.stacks.scale)
    auraFrame.CountFrame.Count:SetScale(1)
  end
  local c1 = details.texts.stacks.color
  auraFrame.CountFrame.Count:SetTextColor(c1.r, c1.g, c1.b)
  auraFrame.CountFrame.Count:SetShown(details.texts.stacks.visible);

  auraFrame.Cooldown:SetHideCountdownNumbers(not details.texts.countdown.visible)

  if details.texts.countdown.visible then
    auraFrame.Cooldown.Text:SetFontObject(addonTable.CurrentFont)
    auraFrame.Cooldown.Text:ClearAllPoints()
    addonTable.Display.ApplyAnchor(auraFrame.Cooldown.Text, details.texts.countdown.anchor, addonTable.CurrentFontUsesSmoothing and 1/details.texts.countdown.scale or 1)
    if addonTable.CurrentFontUsesSmoothing then
      auraFrame.Cooldown.Text:SetTextScale(1)
      auraFrame.Cooldown.Text:SetScale(details.texts.countdown.scale)
    else
      auraFrame.Cooldown.Text:SetTextScale(details.texts.countdown.scale)
      auraFrame.Cooldown.Text:SetScale(1)
    end
    local c2 = details.texts.countdown.color
    auraFrame.Cooldown.Text:SetTextColor(c2.r, c2.g, c2.b)
    if addonTable.Constants.IsCooldownFormattingAvailable then
      if details.texts.countdown.showFractions then
        auraFrame.Cooldown:SetCountdownFormatter(auraFormatter)
      else
        auraFrame.Cooldown:SetCountdownFormatter(nil)
        auraFrame.Cooldown:SetCountdownAbbrevThreshold(20)
      end
    end
  end

  if auraFrame.CountFrame.Count.SetSmoothScaling then
    auraFrame.CountFrame.Count:SetSmoothScaling(addonTable.CurrentFontUsesSmoothing)
    auraFrame.Cooldown.Text:SetSmoothScaling(addonTable.CurrentFontUsesSmoothing)
  end

  auraFrame.Cooldown:SetDrawEdge(details.showSwipe)
  auraFrame.Cooldown:SetDrawSwipe(details.showSwipe)

  PixelUtil.SetSize(auraFrame, 20, 20 * details.height)
  PixelUtil.SetSize(auraFrame.Border, 20, 20 * details.height)
  PixelUtil.SetSize(auraFrame.Icon, 20, 20 * details.height)
  local texBase = 0.95 * (1 - details.height) / 2
  auraFrame.Icon:SetTexCoord(0.05, 0.95, 0.05 + texBase, 0.95 - texBase)

  auraFrame.Dispel:SetShown(details.showType)
end

addonTable.Display.NameplateMixin = {}
function addonTable.Display.NameplateMixin:OnLoad()
  self:SetFlattensRenderLayers(true)
  self:SetCollapsesLayout(true)
  self:SetScript("OnEvent", self.OnEvent)

  self.widgets = {}

  self.SoftTargetIcon = self:CreateTexture(nil, "OVERLAY")
  self.SoftTargetIcon:SetSize(34, 34)
  self.SoftTargetIcon:SetPoint("BOTTOM", self, "TOP", -1, 24)
  self.SoftTargetIcon:Hide()

  self.BuffDisplay = CreateFrame("Frame", nil, self)
  self.BuffDisplay:SetSize(10, 10)
  self.BuffDisplay:SetFlattensRenderLayers(true)
  self.DebuffDisplay = CreateFrame("Frame", nil, self)
  self.DebuffDisplay:SetSize(10, 10)
  self.DebuffDisplay:SetFlattensRenderLayers(true)
  self.CrowdControlDisplay = CreateFrame("Frame", nil, self)
  self.CrowdControlDisplay:SetSize(10, 10)
  self.CrowdControlDisplay:SetFlattensRenderLayers(true)

  if not addonTable.Constants.IsMidnightNext then
    self:SetupLegacyAuras()
  end

  self:SetSize(10, 10)

  self.casting = false

  self.sizeChangeCount = -1
end

function addonTable.Display.NameplateMixin:SetupLegacyAuras()
  self.AurasPools = {
    buffs = GetAurasPoolLegacy(self),
    debuffs = GetAurasPoolLegacy(self),
    crowdControl = GetAurasPoolLegacy(self),
  }
  self.AurasManager = addonTable.Utilities.InitFrameWithMixin(self, addonTable.Display.AurasManagerMixin)

  local function GetCallback(frame)
    local pool = self.AurasPools[frame:GetParent().kind]
    return function(data, auraFilter)
      if frame.items then
        for _, item in ipairs(frame.items) do
          pool:Release(item)
        end
        frame.items = nil
      end

      if not frame:GetParent():IsShown() then
        return
      end

      local pandemicDim = PixelUtil.ConvertPixelsToUIForRegion(1, frame)

      local details = frame:GetParent().details
      local step = PixelUtil.ConvertPixelsToUIForRegion(20 * (1 + details.padding), frame)
      local currentX = 0
      local currentY = 0
      local xOffset = 0
      local yOffset = 0
      if details.direction == "LEFT" then
        xOffset = -step
      elseif details.direction == "RIGHT" then
        xOffset = step
      else -- CENTER
        xOffset = step
        currentX = -(math.min(#data, details.limit) - 1) * step / 2
      end
      local anchor = details.anchor[1]
      if type(anchor) ~= "string" then
        anchor = "CENTER"
      end

      frame.items = {}
      for index, auraInstanceID in ipairs(data) do
        if index > details.limit then
          break
        end

        local aura = self.AurasManager:GetByInstanceID(auraInstanceID)
        local auraFrame = pool:Acquire()
        table.insert(frame.items, auraFrame)
        auraFrame:SetParent(frame)

        auraFrame.auraInstanceID = auraInstanceID
        auraFrame.auraIndex = nil
        auraFrame.auraFilter = auraFilter
        if addonTable.Constants.IsSecretsActive then
          auraFrame.durationSecret = aura.durationSecret
        else
          auraFrame.duration = aura.duration
          auraFrame.expirationTime = aura.expirationTime
        end

        auraFrame.Icon:SetTexture(aura.icon);
        auraFrame.CountFrame.Count:SetText(aura.applicationsString)

        if auraFrame.styleIndex ~= self.styleIndex then
          auraFrame.styleIndex = self.styleIndex
          StyleAura(auraFrame, details)
          if details.showStealable then
            auraFrame.Pandemic:SetVertexColor(1, 171/255, 26/255)
          elseif details.showPandemic then
            auraFrame.Pandemic:SetVertexColor(1, 1, 1)
          end

          auraFrame.Pandemic:SetShown(details.showPandemic or details.showStealable)
          if auraFrame.Pandemic:IsShown() then
            auraFrame.Pandemic.Top:SetHeight(pandemicDim)
            auraFrame.Pandemic.Bottom:SetHeight(pandemicDim)
            auraFrame.Pandemic.Left:SetWidth(pandemicDim)
            auraFrame.Pandemic.Right:SetWidth(pandemicDim)
          end
        end

        if auraFrame.durationSecret then
          auraFrame.Cooldown:SetCooldownFromDurationObject(auraFrame.durationSecret)
          if details.showPandemic then
            auraFrame.Pandemic:SetAlpha(C_CurveUtil.EvaluateColorValueFromBoolean(auraFrame.durationSecret:IsZero(), 0, auraFrame.durationSecret:EvaluateRemainingPercent(pandemicCurve)))
          end
          if details.showType then
            local color = C_UnitAuras.GetAuraDispelTypeColor(self.unit, auraFrame.auraInstanceID, dispelCurve)
            auraFrame.Dispel.Border:SetVertexColor(color:GetRGBA())
          end
          if details.showStealable then
            auraFrame.Pandemic:SetAlphaFromBoolean(aura.isStealable)
          end
        elseif auraFrame.expirationTime then
          CooldownFrame_Set(auraFrame.Cooldown, aura.expirationTime - aura.duration, aura.duration, aura.duration > 0, true);
          if details.showPandemic then
            auraFrame.Pandemic:SetAlpha(aura.duration > 0 and aura.expirationTime - GetTime() <= aura.duration * pandemicPercentage and 1 or 0)
          end
          if details.showType then
            local color = dispelColorMap[aura.dispelName]
            if color then
              auraFrame.Dispel:SetAlpha(1)
              auraFrame.Dispel.Border:SetVertexColor(color.r, color.g, color.b)
            else
              auraFrame.Dispel:SetAlpha(0)
            end
          end
          if details.showStealable then
            auraFrame.Pandemic:SetShown(aura.isStealable)
          end
        else
          auraFrame.Cooldown:Clear()
          auraFrame.Pandemic:SetAlpha(0)
          auraFrame.Dispel:SetAlpha(0)
        end

        auraFrame:Show();

        PixelUtil.SetPoint(auraFrame, anchor, frame, anchor, currentX, currentY)
        currentX = currentX + xOffset
      end
    end
  end

  self.BuffDisplay.Wrapped = CreateFrame("Frame", nil, self.BuffDisplay)
  self.BuffDisplay.Wrapped:SetSize(10, 10)
  self.BuffDisplay.kind = "buffs"
  self.DebuffDisplay.Wrapped = CreateFrame("Frame", nil, self.DebuffDisplay)
  self.DebuffDisplay.Wrapped:SetSize(10, 10)
  self.DebuffDisplay.kind = "debuffs"
  self.CrowdControlDisplay.Wrapped = CreateFrame("Frame", nil, self.CrowdControlDisplay)
  self.CrowdControlDisplay.Wrapped:SetSize(10, 10)
  self.CrowdControlDisplay.kind = "crowdControl"

  self.DebuffDisplay:Hide()
  self.BuffDisplay:Hide()
  self.CrowdControlDisplay:Hide()

  self.AurasManager:SetDebuffsCallback(GetCallback(self.DebuffDisplay.Wrapped))
  self.AurasManager:SetBuffsCallback(GetCallback(self.BuffDisplay.Wrapped))
  self.AurasManager:SetCrowdControlCallback(GetCallback(self.CrowdControlDisplay.Wrapped))
end

function addonTable.Display.NameplateMixin:OnSizeChanged()
  if self:ShouldNotSize() then
    return
  end
  -- Optimisation to avoid recalculating anchors/sizes while nameplate scales up/down
  self.sizeChangeCount = 0
  self:SetScript("OnUpdate", function()
    self.sizeChangeCount = self.sizeChangeCount + 1
    if self.sizeChangeCount >= 2 then
      self:ApplyPixelPerfectSizing()
      self:SetScript("OnUpdate", nil)
    end
  end)
end

-- Avoid pixel-perfecting the alignment if the scale hasn't changed, or its
-- shrinking cause the nameplate is disappearing
function addonTable.Display.NameplateMixin:ShouldNotSize()
  -- Detect sizing down, which we should ignore
  if not self.unit then
    return true
  end
  local scale = self:GetEffectiveScale()
  return scale == self.lastScale or scale < self.offsetScale
end

function addonTable.Display.NameplateMixin:ApplyPixelPerfectSizing()
  if self:ShouldNotSize() then
    return
  end
  for _, w in ipairs(self.widgets) do
    if w:IsShown() then
      w:ApplyAnchor()
      w:ApplySize()
    else
      w.pixelPerfectRequired = true
    end
  end
  self.lastScale = self:GetEffectiveScale()
end

function addonTable.Display.NameplateMixin:InitializeWidgets(design, scaleOffset, scaleMod)
  self.offsetScale = (scaleOffset or 1) * UIParent:GetEffectiveScale() * addonTable.Config.Get(addonTable.Config.Options.GLOBAL_SCALE)
  self.scale = design.scale * scaleMod

  self.lastScale = self:GetEffectiveScale()

  self.unit = nil
  self:UpdateVisual()

  if self.widgets then
    addonTable.Display.ReleaseWidgets(self.widgets)
    self.widgets = nil
  end
  self.widgets = addonTable.Display.GetWidgets(design, self)

  local auras = design.auras
  local designInfo = {}
  for _, a in ipairs(auras) do
    designInfo[a.kind] = a
  end
  self.auraDetails = designInfo

  if not addonTable.Constants.IsMidnightNext then
    self:InitializeWidgetsLegacyAuras(designInfo)
  else
    self:InitializeWidgetsAnchorAuras(designInfo)
  end

  if self:GetScript("OnSizeChanged") == nil then
    self:SetScript("OnSizeChanged", self.OnSizeChanged)
  end
  self:SetScript("OnUpdate", nil)
end

function addonTable.Display.NameplateMixin:InitializeModernAuras(designInfo)
  self.DebuffDisplay.Wrapped = auraContainerSharedPool:Acquire()
  self.DebuffDisplay.Wrapped:SetParent(self.DebuffDisplay)
  self.DebuffDisplay.Wrapped:SetUnit(self.unit)
  self.BuffDisplay.Wrapped = auraContainerSharedPool:Acquire()
  self.BuffDisplay.Wrapped:SetParent(self.BuffDisplay)
  self.BuffDisplay.Wrapped:SetUnit(self.unit)
  self.CrowdControlDisplay.Wrapped = auraContainerSharedPool:Acquire()
  self.CrowdControlDisplay.Wrapped:SetParent(self.CrowdControlDisplay)
  self.CrowdControlDisplay.Wrapped:SetUnit(self.unit)

  self.DebuffDisplay.Wrapped:RemoveAllAuraFrames()
  self.DebuffDisplay.Wrapped.pool:ReleaseAll()
  self.BuffDisplay.Wrapped:RemoveAllAuraFrames()
  self.BuffDisplay.Wrapped.pool:ReleaseAll()
  self.CrowdControlDisplay.Wrapped:RemoveAllAuraFrames()
  self.CrowdControlDisplay.Wrapped.pool:ReleaseAll()

  self:InitializeAurasWrappedAnchors(self.auraDetails)

  local function Arrange(parent, pool, details)
    pool:ReleaseAll()
    local step = PixelUtil.ConvertPixelsToUIForRegion(20 * (1 + details.padding), parent)
    local currentX = 0
    local currentY = 0
    local xOffset = 0
    local yOffset = 0
    if details.direction == "LEFT" then
      xOffset = -step
    elseif details.direction == "RIGHT" then
      xOffset = step
    else -- CENTER
      xOffset = step
      currentX = -(details.limit - 1) * step / 2
    end
    local anchor = details.anchor[1]
    if type(anchor) ~= "string" then
      anchor = "CENTER"
    end
    parent:Show()
    for index = 1, details.limit do
      local auraFrame = pool:Acquire()
      if auraFrame.styleIndex ~= self.styleIndex then
        StyleAura(auraFrame, details)
        auraFrame.styleIndex = self.styleIndex
      end
      PixelUtil.SetPoint(auraFrame, anchor, parent, anchor, currentX, currentY)
      currentX = currentX + xOffset

      parent:AddAuraFrame(auraFrame)
    end
  end

  if designInfo.debuffs then
    self.DebuffDisplay.Wrapped:ClearAuraFilters()
    if designInfo.debuffs.filters.fromYou then
      self.DebuffDisplay.Wrapped:AddAuraFilter("HARMFUL|INCLUDE_NAME_PLATE_ONLY|PLAYER", {maxFrameCount = designInfo.debuffs.limit})
    else
      self.DebuffDisplay.Wrapped:AddAuraFilter("HARMFUL|INCLUDE_NAME_PLATE_ONLY", {maxFrameCount = designInfo.debuffs.limit})
    end
    Arrange(self.DebuffDisplay.Wrapped, self.DebuffDisplay.Wrapped.pool, designInfo.debuffs)
    self.DebuffDisplay.Wrapped:UpdateAllAuras()
  end

  if designInfo.buffs then
    self.BuffDisplay.Wrapped:ClearAuraFilters()
    self.BuffDisplay.Wrapped:AddAuraFilter("HELPFUL|INCLUDE_NAME_PLATE_ONLY", {maxFrameCount = designInfo.buffs.limit})
    Arrange(self.BuffDisplay.Wrapped, self.BuffDisplay.Wrapped.pool, designInfo.buffs)
    self.BuffDisplay.Wrapped:UpdateAllAuras()
  end

  if designInfo.crowdControl then
    self.CrowdControlDisplay.Wrapped:ClearAuraFilters()
    if designInfo.crowdControl.filters.fromYou then
      self.CrowdControlDisplay.Wrapped:AddAuraFilter("HARMFUL|CROWD_CONTROL|PLAYER", {maxFrameCount = designInfo.crowdControl.limit})
    else
      self.CrowdControlDisplay.Wrapped:AddAuraFilter("HARMFUL|CROWD_CONTROL", {maxFrameCount = designInfo.crowdControl.limit})
    end
    Arrange(self.CrowdControlDisplay.Wrapped, self.CrowdControlDisplay.Wrapped.pool, designInfo.crowdControl)
    self.CrowdControlDisplay.Wrapped:UpdateAllAuras()
  end
end

function addonTable.Display.NameplateMixin:InitializeWidgetsAnchorAuras(designInfo)
  self.DebuffDisplay.enabled = false
  self.BuffDisplay.enabled = false
  self.CrowdControlDisplay.enabled = false
  local defaultSize = 20
  if designInfo.debuffs then
    self.DebuffDisplay.enabled = true
    self.DebuffDisplay:ClearAllPoints()
    self.DebuffDisplay:SetFrameStrata("MEDIUM")
    self.DebuffDisplay:SetFrameLevel(addonTable.Constants.LayerFrameLevelStep * designInfo.debuffs.layer + 450)
    self.DebuffDisplay.details = designInfo.debuffs
    PixelUtil.SetSize(self.DebuffDisplay, defaultSize * designInfo.debuffs.scale, defaultSize * designInfo.debuffs.scale)
    addonTable.Display.ApplyAnchor(self.DebuffDisplay, designInfo.debuffs.anchor)
  end
  if designInfo.buffs then
    self.BuffDisplay.enabled = true
    self.BuffDisplay:ClearAllPoints()
    self.BuffDisplay:SetFrameStrata("MEDIUM")
    self.BuffDisplay:SetFrameLevel(addonTable.Constants.LayerFrameLevelStep * designInfo.buffs.layer + 450 + 10)
    self.BuffDisplay.details = designInfo.buffs
    PixelUtil.SetSize(self.BuffDisplay, defaultSize * designInfo.buffs.scale, defaultSize * designInfo.buffs.scale)
    addonTable.Display.ApplyAnchor(self.BuffDisplay, designInfo.buffs.anchor)
  end
  if designInfo.crowdControl then
    self.CrowdControlDisplay.enabled = true
    self.CrowdControlDisplay:ClearAllPoints()
    self.CrowdControlDisplay:SetFrameStrata("MEDIUM")
    self.CrowdControlDisplay:SetFrameLevel(addonTable.Constants.LayerFrameLevelStep * designInfo.crowdControl.layer + 450 + 20)
    self.CrowdControlDisplay.details = designInfo.crowdControl
    PixelUtil.SetSize(self.CrowdControlDisplay, defaultSize * designInfo.crowdControl.scale, defaultSize * designInfo.crowdControl.scale)
    addonTable.Display.ApplyAnchor(self.CrowdControlDisplay, designInfo.crowdControl.anchor)
  end
end

function addonTable.Display.NameplateMixin:InitializeAurasWrappedAnchors(designInfo)
  if designInfo.debuffs then
    if self.DebuffDisplay.Wrapped then
      self.DebuffDisplay.Wrapped:ClearAllPoints()
      self.DebuffDisplay.Wrapped:SetPoint(designInfo.debuffs.anchor[1] or "CENTER")
      self.DebuffDisplay.Wrapped:SetScale(designInfo.debuffs.scale)
    end
  end

  if designInfo.buffs then
    if self.BuffDisplay.Wrapped then
      self.BuffDisplay.Wrapped:ClearAllPoints()
      self.BuffDisplay.Wrapped:SetScale(designInfo.buffs.scale)
      self.BuffDisplay.Wrapped:SetPoint(designInfo.buffs.anchor[1] or "CENTER")
    end
  end

  if designInfo.crowdControl then
    if self.CrowdControlDisplay.Wrapped then
      self.CrowdControlDisplay.Wrapped:ClearAllPoints()
      self.CrowdControlDisplay.Wrapped:SetScale(designInfo.crowdControl.scale)
      self.CrowdControlDisplay.Wrapped:SetPoint(designInfo.crowdControl.anchor[1] or "CENTER")
    end
  end
end

function addonTable.Display.NameplateMixin:InitializeWidgetsLegacyAuras(designInfo)
  self:InitializeWidgetsAnchorAuras(designInfo)
  self:InitializeAurasWrappedAnchors(designInfo)
  self.AurasManager:PostInit(designInfo.buffs, designInfo.debuffs, designInfo.crowdControl)
end

function addonTable.Display.NameplateMixin:Install(nameplate, offsetY)
  self:Show()
  self:SetFrameStrata("BACKGROUND")
  self:SetPoint("CENTER", nameplate, "CENTER", 0, offsetY)
  self:SetSize(10, 10)

  -- We force a sizing immediately to avoid 0 size widgets breaking the textures from the Blizz animations
  self:ApplyPixelPerfectSizing()
  self:SetScript("OnUpdate", nil)
end

function addonTable.Display.NameplateMixin:SetUnit(unit)
  self.SoftTargetIcon:Hide()

  self.interactUnit = unit
  if unit and (not UnitNameplateShowsWidgetsOnly or not UnitNameplateShowsWidgetsOnly(unit)) and not UnitIsGameObject(unit) then
    self.unit = unit

    if UnitCanAttack("player", self.unit) and addonTable.Config.Get(addonTable.Config.Options.OUT_OF_RANGE_ALPHA) ~= 1 then
      addonTable.Cache:RegisterCallback(self.unit, "range", function(state)
        self.inRange = state
        self:UpdateVisual()
      end)
      self.inRange = addonTable.Cache:Get(self.unit, "range")
    else
      self.inRange = true
    end

    if UnitCanAttack("player", self.unit) and addonTable.Config.Get(addonTable.Config.Options.NOT_IN_PULL_ALPHA) ~= 1 then
      self:RegisterEvent("PLAYER_REGEN_ENABLED")
      self:RegisterEvent("PLAYER_REGEN_DISABLED")

      self.inCombat = addonTable.Display.Utilities.IsInCombatWith(self.unit)
      addonTable.Cache:RegisterCallback(self.unit, "combat", function(inCombat)
        self.inCombat = inCombat
        self:UpdateVisual()
      end)
    else
      self.inCombat = true
    end

    for _, w in ipairs(self.widgets) do
      w:Show()
      w:SetUnit(self.unit)
    end

    local isTarget = addonTable.Cache:Get(unit, "target")
    local isSoftTarget = addonTable.Cache:Get(unit, "softTarget")
    local isMouseover = addonTable.Cache:Get(unit, "mouseover")
    local isFocus = addonTable.Cache:Get(unit, "focus")

    if isTarget or isSoftTarget then
      for _, w in ipairs(self.widgets) do
        if w.ApplyTarget then
          w:ApplyTarget()
        end
      end
    end

    if isMouseover then
      for _, w in ipairs(self.widgets) do
        if w.ApplyMouseover then
          w:ApplyMouseover()
        end
      end
    end

    if isFocus then
      for _, w in ipairs(self.widgets) do
        if w.ApplyFocus then
          w:ApplyFocus()
        end
      end
    end

    addonTable.Cache:RegisterCallback(unit, "target", function()
      self:UpdateForTarget()
    end)

    addonTable.Cache:RegisterCallback(unit, "softTarget", function()
      self:UpdateForTarget()
    end)

    addonTable.Cache:RegisterCallback(unit, "mouseover", function()
      self:UpdateForMouseover()
    end)

    addonTable.Cache:RegisterCallback(unit, "focus", function()
      self:UpdateForFocus()
    end)

    self.BuffDisplay:SetShown(self.BuffDisplay.enabled)
    self.DebuffDisplay:SetShown(self.DebuffDisplay.enabled)
    self.CrowdControlDisplay:SetShown(self.CrowdControlDisplay.enabled)

    if not addonTable.Constants.IsMidnightNext then
      self.AurasManager:SetUnit(self.unit)
    else
      self:InitializeModernAuras(self.auraDetails)
    end

    self:UpdateCastingState(addonTable.Cache:Get(self.unit, "cast"))
    addonTable.Cache:RegisterCallback(self.unit, "cast", function(state)
      local old = self.casting
      self:UpdateCastingState(state)
      if old ~= self.casting then
        self:UpdateVisual()
      end
    end)

    addonTable.CallbackRegistry:RegisterCallback("TextOverrideUpdated", function(_, unit)
      if unit ~= self.unit then
        return
      end
      for _, w in ipairs(self.widgets) do
        if w.ApplyTextOverride then
          w:ApplyTextOverride()
        end
      end
    end, self)
  else
    self.unit = nil
    for _, w in ipairs(self.widgets) do
      w:SetUnit(nil)
      w:Hide()
    end

    self.BuffDisplay:Hide()
    self.DebuffDisplay:Hide()
    self.CrowdControlDisplay:Hide()

    if not addonTable.Constants.IsMidnightNext then
      self.AurasManager:SetUnit()
    elseif self.BuffDisplay.Wrapped then
      auraContainerSharedPool:Release(self.BuffDisplay.Wrapped)
      auraContainerSharedPool:Release(self.DebuffDisplay.Wrapped)
      auraContainerSharedPool:Release(self.CrowdControlDisplay.Wrapped)
      self.BuffDisplay.Wrapped = nil
      self.DebuffDisplay.Wrapped = nil
      self.CrowdControlDisplay.Wrapped = nil
    end

    self:UnregisterEvent("PLAYER_REGEN_ENABLED")
    self:UnregisterEvent("PLAYER_REGEN_DISABLED")

    self:UnregisterAllEvents()
    self.casting = false

    addonTable.CallbackRegistry:UnregisterCallback("TextOverrideUpdated", self)
  end

  self:UpdateVisual()
end

function addonTable.Display.NameplateMixin:UpdateCastingState(state)
  self.casting = state.cast[1] ~= nil or state.channel[1] ~= nil
end

function addonTable.Display.NameplateMixin:UpdateForTarget()
  if self.unit then
    for _, w in ipairs(self.widgets) do
      if w.ApplyTarget then
        w:ApplyTarget()
      end
    end
  end

  self:UpdateVisual()
end

function addonTable.Display.NameplateMixin:UpdateForMouseover()
  if self.unit then
    for _, w in ipairs(self.widgets) do
      if w.ApplyMouseover then
        w:ApplyMouseover()
      end
    end
  end

  self:UpdateVisual()
end

function addonTable.Display.NameplateMixin:UpdateForFocus()
  if self.unit then
    for _, w in ipairs(self.widgets) do
      if w.ApplyFocus then
        w:ApplyFocus()
      end
    end
  end

  self:UpdateVisual()
end

function addonTable.Display.NameplateMixin:OnEvent()
  self:UpdateVisual()
end

function addonTable.Display.NameplateMixin:UpdateVisual()
  local scaleMod = addonTable.Constants.IsRetail and 1 or UIParent:GetEffectiveScale()
  if not self.unit then
    self:SetAlpha(1)
    self:SetScale(self.scale * addonTable.Config.Get(addonTable.Config.Options.GLOBAL_SCALE) * scaleMod)
    return
  end

  local scale = 1
  local alpha = 0
  local isTarget = UnitIsUnit("target", self.unit) or UnitIsUnit("softenemy", self.unit) or UnitIsUnit("softfriend", self.unit)
  if isTarget then
    alpha = 1
  else
    local isMouseover = UnitIsUnit("mouseover", self.unit)
    if isMouseover then
      alpha = math.max(alpha, addonTable.Config.Get(addonTable.Config.Options.MOUSEOVER_ALPHA))
    end
    if self.casting then
      scale = addonTable.Config.Get(addonTable.Config.Options.CAST_SCALE)
      alpha = math.max(alpha, addonTable.Config.Get(addonTable.Config.Options.CAST_ALPHA))
    end
    if not isMouseover and not self.casting then
      alpha = addonTable.Config.Get(addonTable.Config.Options.NOT_TARGET_ALPHA)
    end
  end
  if not self.inRange then
    alpha = alpha * addonTable.Config.Get(addonTable.Config.Options.OUT_OF_RANGE_ALPHA)
  end
  if not self.inCombat and UnitAffectingCombat("player") then
    alpha = alpha * addonTable.Config.Get(addonTable.Config.Options.NOT_IN_PULL_ALPHA)
  end
  self:SetScale(self.scale * scale * addonTable.Config.Get(addonTable.Config.Options.GLOBAL_SCALE) * scaleMod)
  self:SetAlpha(alpha)
end

function addonTable.Display.NameplateMixin:UpdateSoftInteract()
  -- From Blizzard code, modified
  local doEnemyIcon = GetCVarBool("SoftTargetIconEnemy")
  local doFriendIcon = GetCVarBool("SoftTargetIconFriend")
  local doInteractIcon = GetCVarBool("SoftTargetIconInteract")
  local hasCursorTexture = false
  if ((doEnemyIcon and UnitIsUnit(self.interactUnit, "softenemy")) or
    (doFriendIcon and UnitIsUnit(self.interactUnit, "softfriend")) or
    (doInteractIcon and UnitIsUnit(self.interactUnit, "softinteract"))
    ) then
    hasCursorTexture = SetUnitCursorTexture(self.SoftTargetIcon, self.interactUnit)
  end
  self.SoftTargetIcon:SetShown(hasCursorTexture)
end

function addonTable.Display.NameplateMixin:UpdateAurasForPandemic()
  local time = GetTime()
  if self.DebuffDisplay.details and self.DebuffDisplay.details.showPandemic and self.DebuffDisplay.Wrapped.items then
    for _, item in ipairs(self.DebuffDisplay.Wrapped.items) do
      if item.durationSecret then
        item.Pandemic:SetAlpha(C_CurveUtil.EvaluateColorValueFromBoolean(item.durationSecret:IsZero(), 0, item.durationSecret:EvaluateRemainingPercent(pandemicCurve)))
      elseif item.expirationTime then
        item.Pandemic:SetAlpha(item.duration > 0 and item.expirationTime - time <= item.duration * pandemicPercentage and 1 or 0)
      end
    end
  end
end
