--- SeamoreSpheresUnitFrame
--
local SeamoreSpheresUnitFrame = {}
local AddOnName, nsVars = ...
local ss = nsVars.SS

local tonumber, string, strlower = tonumber, string, strlower
local pairs, table = pairs, table
local InCombatLockdown = InCombatLockdown
local CreateFrame = ss.CreateFrame
local GetScreenWidth, GetScreenHeight = GetScreenWidth, GetScreenHeight
local type, select, tostring = type, select, tostring
local ErrorInvalidArgs = ss.Utils.Errors.ErrorInvalidArgs
local throw = ss.Utils.throw

local sphereIndex = 0

local _frameIndex = {
  active = 0,
  preview = 0,
}

local function onDrag(seamoreSpheresUnitFrame)
  local configName = (seamoreSpheresUnitFrame.configName == "vehicle") and "health"
      or ((seamoreSpheresUnitFrame.configName == "vehicleResource") and "mainResource" or seamoreSpheresUnitFrame.configName)

  ss.db[configName].general.anchor, ss.db[configName].general.posX, ss.db[configName].general.posY = select(3, seamoreSpheresUnitFrame:GetPoint())

  if configName == "health" then
    ss.db["vehicle"].general.anchor, ss.db["vehicle"].general.posX, ss.db["vehicle"].general.posY = select(3, seamoreSpheresUnitFrame:GetPoint())
  elseif configName == "mainResource" then
    ss.db["vehicleResource"].general.anchor, ss.db["vehicleResource"].general.posX, ss.db["vehicleResource"].general.posY = select(3, seamoreSpheresUnitFrame:GetPoint())
  end

  if ss.Layout.spheres[configName] then
    -- Display the change using the actual sphere.
    ss.Layout.spheres[configName]:RefreshPos()
  end
end

local function initOnce(seamoreSpheresUnitFrame, generalInfo)
  if not seamoreSpheresUnitFrame._initialized then
    seamoreSpheresUnitFrame:SetHeight(ss.global.general.SphereSize / generalInfo.scale)
    seamoreSpheresUnitFrame:SetWidth(ss.global.general.SphereSize / generalInfo.scale)

    if seamoreSpheresUnitFrame.displayMode == "active" then
      seamoreSpheresUnitFrame:RegisterForClicks("AnyUp")
      seamoreSpheresUnitFrame:SetAttribute("type1", "target")
      seamoreSpheresUnitFrame:SetAttribute("target", "unit")
      seamoreSpheresUnitFrame:SetAttribute("type2", "togglemenu")
      seamoreSpheresUnitFrame:SetAttribute("togglemenu", generalInfo.unit)
      seamoreSpheresUnitFrame:SetAttribute("unit", generalInfo.unit)

      ss.Extensions:MakeMovable(seamoreSpheresUnitFrame, nil, onDrag)

      -- Add the tooltip.
      ss.Utils:AddUnitTooltip(seamoreSpheresUnitFrame, generalInfo.unit)
    else
      seamoreSpheresUnitFrame:ClearAllPoints()
      seamoreSpheresUnitFrame:EnableMouse(false)
      seamoreSpheresUnitFrame:SetFrameStrata("HIGH")
      seamoreSpheresUnitFrame:SetPoint("CENTER", 0, 40)
    end

    seamoreSpheresUnitFrame:SetScale((seamoreSpheresUnitFrame.displayMode == "active") and (generalInfo.globalScale / 100) or 1)

    seamoreSpheresUnitFrame._initialized = true
  end
end

--- Converts a region-relative (ie: "CENTER", "TOPRIGHT"...) point to a "LEFT" or "BOTTOM" one.
-- @param val The position value.
-- @param scale The sphere scale.
-- @param pad The padding (region dimension) to use.
-- @return The adjusted value.
--
local function adjustMidForRaw(val, scale, pad)
  local adjust = ((ss.global.general.SphereSize * scale) / 2) - (ss.global.general.SphereSize / 2)
  local _cur = (val * scale) - (pad * scale)
  local _max = ((pad * scale) * 3) - ss.global.general.SphereSize - (pad * scale)
  local _per = _cur / _max
  _cur = _cur + ((adjust * 2) * _per) - adjust
  _per = _cur / _max
  adjust = ((adjust * 2) * _per) - adjust

  return (val + (adjust * (1 / scale)))
end

--- Converts a "LEFT" or "BOTTOM" point to a region-relative one (ie: "CENTER", "TOPRIGHT"...).
-- @param val The raw value.
-- @param scale The sphere scale.
-- @param pad The padding (region dimension) to use.
-- @return The adjusted value.
--
local function adjustMidFromRaw(val, scale, pad)
  local adjust = ((ss.global.general.SphereSize * scale) / 2) - (ss.global.general.SphereSize / 2)
  local _max = (pad * 3) - ss.global.general.SphereSize - pad
  local _per = (val - pad) / _max
  adjust = ((adjust * 2) * _per) - adjust

  return (val + (ss.global.general.SphereSize / 2) - (pad * 2) - adjust)
end

function SeamoreSpheresUnitFrame:Init(configName, displayMode)
  if type(configName) == "string" and type(displayMode) == "string" then
    -- Initialize the properties.
    self.configName = configName
    self.displayMode = displayMode
    local generalInfo = ss.db[configName].general

    if displayMode == "active" then
      if InCombatLockdown() then
        ss.PendingTask.add("SeamoreSpheresUnitFrame" .. tostring(self._sphereIndex) .. "_Init", function()
          self:ClearAllPoints()
          self:SetHeight(ss.global.general.SphereSize / generalInfo.scale)
          self:SetWidth(ss.global.general.SphereSize / generalInfo.scale)
          self:SetPoint(generalInfo.anchor, generalInfo.posX, generalInfo.posY)
          self:SetScale(generalInfo.globalScale / 100)
        end)
      else
        self:ClearAllPoints()
        self:SetHeight(ss.global.general.SphereSize / generalInfo.scale)
        self:SetWidth(ss.global.general.SphereSize / generalInfo.scale)
        self:SetPoint(generalInfo.anchor, generalInfo.posX, generalInfo.posY)
        self:SetScale(generalInfo.globalScale / 100)
      end
    end

    initOnce(self, generalInfo)

    if displayMode == "active" then
      -- TODO: Find a way to set unit while in combat. (maybe need to use multiple frames?)
      ss.PendingTask.add("SeamoreSpheresUnitFrame" .. tostring(self._sphereIndex) .. "_UpdateUnit", function()
        self:SetAttribute("togglemenu", generalInfo.unit)
        self:SetAttribute("unit", generalInfo.unit)

        -- Update the tooltip.
        ss.Utils:AddUnitTooltip(self, generalInfo.unit)
      end)
    end

    -- Initialize the container.
    self.sphere:Init()
  else
    throw(ErrorInvalidArgs, "SeamoreSpheresUnitFrame", "Init",
        { "configName", "string", configName },
        { "displayMode", "string", displayMode }
    )
  end

  return self.sphere.config
end

function SeamoreSpheresUnitFrame:ShowSphere(show)
  show = (show ~= false) and true

  if InCombatLockdown() then
    ss.PendingTask.add("SeamoreSpheresUnitFrame" .. tostring(self._sphereIndex) .. (show and "_ShowSphere" or "_HideSphere"), function()
      -- Run outside of combat.
      self:EnableMouse(show)
    end)
  else
    self:EnableMouse(show)
  end

  if not show then
    self.sphere.ResourceUpdate = nil
    self:Fade(0, function(display)
      display.sphere:SetAlpha(0)
    end)
  else
    self.sphere:SetAlpha(1)
    self.sphere.ResourceUpdate = (self.displayMode == "active") and self.sphere.resourceMonitor:Watch() or nil
    self:Fade(1)
  end
end

function SeamoreSpheresUnitFrame:HideSphere()
  self:ShowSphere(false)
end

function SeamoreSpheresUnitFrame:SetResource(configName)
  if type(configName) ~= "string" then
    throw(ErrorInvalidArgs, "SeamoreSpheresUnitFrame", "SetResource",
        { "configName", "string", configName }
    )
  elseif not ss.db[configName] or not ss.db[configName].modes[ss.profile.modeInfo[ss.db[configName].general.configName].modeSpec] then
    throw("Invalid configName passed to SeamoreSpheresUnitFrame" .. tostring(self._sphereIndex) .. ":SetResource()")
  else
    self.configName = configName

    if self.displayMode == "preview" then
      -- Initialize properties.
      self.sphere.textFormat = ss.db[configName].textFormat
      self.sphere.configName = configName
      return self.sphere:SetMode(ss.db[configName].modes[ss.profile.modeInfo[configName].modeSpec][ss.profile.modeInfo[configName].mode])
    else
      return self:Init(self.configName, self.displayMode)
    end
  end

  return self.sphere.config
end

--- Creates a resource group.
-- @param resource The resource data.
-- @param region The sphere region.
--
function SeamoreSpheresUnitFrame:AddResourceGroup(resource, region)
  self.sphere.resourceGroup = self.sphere.resourceGroup or {}
  resource = resource or {}

  if resource.buffGroup then
    self:AddMicroGroup(resource, "buff", 1, table.getn(resource.buffGroup), 1, nil, region)
  elseif table.getn(resource) > 0 then
    if not resource.getMinMax then
      self:AddResourceGroup(resource[1], region)
      self:AddSecondaryResourceGroup(resource[2], region)
    else
      resource.getMinMax(resource[1])
      local max = tonumber(resource[1]) and select(3, resource.getMinMax(resource[1])) or 1
      self:AddMicroGroup(resource, resource[1], 1, max, 1, nil, region)
    end
  elseif self.displayMode == "active" then
    for _, v in pairs(self.sphere.resourceGroup or {}) do
      v:GetParent():SetMicroResource(nil)
    end

    -- Add power tracking
    self.sphere.resourceGroupUnit = nil
    self.sphere.resourceGroupUnitId = nil
    self:AddSecondaryResourceGroup()
  end
end

--- Adds a group of micro spheres to the display.
-- @param resource The resource data.
-- @param resourceId The resource identifier.
-- @param start The start position.
-- @param limit The itterator limit.
-- @param step The itterator value.
-- @param group The group identifier.
-- @param region The sphere region.
--
function SeamoreSpheresUnitFrame:AddMicroGroup(resource, resourceId, start, limit, step, group, region)
  group = group or "resource"

  for i = start, limit, step do
    self.sphere[group .. "Group"][i] = self.sphere[group .. "Group"][i] or self.sphere:InitMicroSphere(i, (resourceId == "buff") and resource.buffGroup[i] or resource, region, group)

    if resourceId ~= "buff" and ss.ClassResourceData.resourceAnimation(resource.name) then
      -- Add or update resource animation.
      self.sphere[group .. "Group"][i].resourceAnimation:UpdateAnimation(ss.ClassResourceData.resourceAnimation(resource.name))
      self.sphere[group .. "Group"][i].resourceAnimation:SetAlpha(0)
    elseif self.sphere[group .. "Group"][i].resourceAnimation then
      self.sphere[group .. "Group"][i].resourceAnimation:Hide()
    end
  end

  if self.displayMode == "active" then

    -- Add power tracking
    self.sphere[group .. "GroupUnit"] = resource
    self.sphere[group .. "GroupUnitId"] = resourceId

    for _, v in pairs(self.sphere[group .. "Group"]) do
      v:GetParent():SetMicroResource(resourceId, resource)
    end
  end
end

--- Creates a secondary resource group.
-- @param resource The resource data.
-- @param region The sphere region.
--
function SeamoreSpheresUnitFrame:AddSecondaryResourceGroup(resource, region)
  self.sphere.secondaryResourceGroup = self.sphere.secondaryResourceGroup or {}

  if resource and resource.buffGroup then
    self:AddMicroGroup(resource, "buff", 10, 11 - table.getn(resource.buffGroup), -1, "secondaryResource", region)
  elseif resource and (table.getn(resource) > 0) then
    local max = resource[1] and select(3, resource.getMinMax(resource[1])) or 1
    self:AddMicroGroup(resource, resource[1], 10, 11 - max, -1, "secondaryResource", region)
  else
    for _, v in pairs(self.sphere.secondaryResourceGroup or {}) do
      v:GetParent():SetMicroResource(nil)
    end

    self.sphere.secondaryResourceGroupUnit = nil
    self.sphere.secondaryResourceGroupUnitId = nil
  end
end

--- Updates the storage values.
--
function SeamoreSpheresUnitFrame:UpdateDB()
  if self.displayMode == "preview" then
    local configName = self.configName

    -- Update the animations.
    ss.db[configName].animation.posX = self.animPosX
    ss.db[configName].animation.posY = self.animPosY
    ss.db[configName].animation.groupId = self.animGroup
    ss.db[configName].animation.id = self.animId

    ss.db[configName].animation.camDistance = self.animCamDist
    ss.db[configName].animation.alpha = self.animAlpha
    ss.db[configName].animation.frameLevel = self.sphere.animation:GetFrameLevel() - ss.private.layout.config.frameLevelAdjust

    -- Update the swirl.
    ss.db[configName].modes[ss.profile.modeInfo[configName].modeSpec][self.modeId].swirl.speed = self.sphere.swirl1:GetSpeed()
    ss.db[configName].modes[ss.profile.modeInfo[configName].modeSpec][self.modeId].swirl.id = self.sphere.swirlId

    -- Update the modes.
    ss.db[configName].modes[ss.profile.modeInfo[configName].modeSpec][self.modeId].fillTexture = self.fillTexture
    ss.db[configName].modes[ss.profile.modeInfo[configName].modeSpec][self.modeId].fillInfo = {
      fillGroup = self.fillGroup,
      fillId = self.fillId,
    }

    -- Update the fil colors.
    local color = self:GetFillColor()
    local r, g, b, a = ss.Utils:GetRGBAValues(color[1])
    ss.db[configName].modes[ss.profile.modeInfo[configName].modeSpec][self.modeId].fillColor[1][2] = { r = r, g = g, b = b, a = a }

    if color[2] then
      r, g, b, a = ss.Utils:GetRGBAValues(color[2])
      ss.db[configName].modes[ss.profile.modeInfo[configName].modeSpec][self.modeId].fillColor[2][2] = { r = r, g = g, b = b, a = a }
    end

    -- Update the swirl colors.
    r, g, b, a = self:GetSwirlColor(1)
    ss.db[configName].modes[ss.profile.modeInfo[configName].modeSpec][self.modeId].swirlColor[1] = { r = r, g = g, b = b, a = a }
    r, g, b, a = self:GetSwirlColor(2)
    ss.db[configName].modes[ss.profile.modeInfo[configName].modeSpec][self.modeId].swirlColor[2] = { r = r, g = g, b = b, a = a }
    r, g, b, a = self:GetSwirlColor(3)
    ss.db[configName].modes[ss.profile.modeInfo[configName].modeSpec][self.modeId].swirlColor[3] = { r = r, g = g, b = b, a = a }

    -- Update the fonts.
    r, g, b, a = self.sphere.font1:GetTextColor()
    ss.db[configName].modes[ss.profile.modeInfo[configName].modeSpec][self.modeId].fontColor[1] = { r = r, g = g, b = b, a = a }
    r, g, b, a = self.sphere.font2:GetTextColor()
    ss.db[configName].modes[ss.profile.modeInfo[configName].modeSpec][self.modeId].fontColor[2] = { r = r, g = g, b = b, a = a }

    ss.db[configName].textFormat = self.textFormat
    ss.db[configName].font[1].show = self.sphere.font1:IsVisible()
    ss.db[configName].font[2].show = self.sphere.font2:IsVisible()

    -- Update the values ignoring vehicle resources..
    configName = (configName == "vehicle") and "health"
        or ((configName == "vehicleResource") and "mainResource" or configName)

    if configName == "health" then
      ss.db["vehicle"].general.globalScale = self:GetGlobalScale()
      ss.db["vehicle"].general.anchor, ss.db["vehicle"].general.posX, ss.db["vehicle"].general.posY = self:GetPos()
    elseif configName == "mainResource" then
      ss.db["vehicleResource"].general.globalScale = self:GetGlobalScale()
      ss.db["vehicleResource"].general.anchor, ss.db["vehicleResource"].general.posX, ss.db["vehicleResource"].general.posY = self:GetPos()
    end

    ss.db[configName].general.globalScale = self:GetGlobalScale()
    ss.db[configName].general.anchor, ss.db[configName].general.posX, ss.db[configName].general.posY = self:GetPos()
  end
end

--- Retrieves the fill texture color.
-- @return The color value.
--
function SeamoreSpheresUnitFrame:GetFillColor()
  if self.mode.fillColor[2] and self.sphere.fillingLeft and self.sphere.fillingRight then
    local r, g, b, a = self.sphere.fillingLeft:GetVertexColor()
    local r2, g2, b2, a2 = self.sphere.fillingRight:GetVertexColor()

    return {
      [1] = { r = r, g = g, b = b, a = a },
      [2] = { r = r2, g = g2, b = b2, a = a2 },
    };
  elseif self.sphere.filling then
    local r, g, b, a = self.sphere.filling:GetVertexColor()

    return {
      [1] = { r = r, g = g, b = b, a = a },
    };
  else
    return nil
  end
end

--- Returnrs the sphere position.
-- @param raw Flag to return raw values.
-- @return the position values.
-- @see adjustMidForRaw().
--
function SeamoreSpheresUnitFrame:GetPos(raw)
  if raw then
    local scale, bScale = self:GetRawScales()
    local sphereSize = bScale * ss.global.general.SphereSize
    local xPad = (bScale * GetScreenWidth()) / 4
    local yPad = (bScale * GetScreenHeight()) / 4

    local x = self._posX
    local y = self._posY

    if string.match(strlower(self._anchor), "right") then
      -- Right adjust.
      x = x + (xPad * 4) - sphereSize
    elseif not string.match(strlower(self._anchor), "left") then
      -- Middle adjust.
      x = x - (sphereSize / 2) + (xPad * 2)
      x = adjustMidForRaw(x, scale, xPad)
    end

    if string.match(strlower(self._anchor), "top") then
      -- Top adjust.
      y = y + (yPad * 4) - sphereSize
    elseif not string.match(strlower(self._anchor), "bottom") then
      -- Middle adjust.
      y = y - (sphereSize / 2) + (yPad * 2)
      y = adjustMidForRaw(y, scale, yPad)
    end

    return self._anchor, x * scale, y * scale
  else
    return self._anchor, self._posX, self._posY
  end
end

--- Retrieves the actual sphere scales.
-- These are used as conversion values.
-- @return the scale values.
--
function SeamoreSpheresUnitFrame:GetRawScales()
  local scale = self:GetGlobalScale() / 100
  return scale, (1 / scale)
end

--- Retrieves the sphere scale.
-- @return the scale value.
--
function SeamoreSpheresUnitFrame:GetGlobalScale()
  return self._globalScale
end

--- Retrieves the swirl color.
-- @param the swirl index.
-- @return The color value.
--
function SeamoreSpheresUnitFrame:GetSwirlColor(index)
  index = index or 1
  return self.sphere["swirl" .. index].texture:GetVertexColor()
end

--- Hides the sphere display.
--
function SeamoreSpheresUnitFrame:Hide()
  self:ShowSphere(false)
end

--- Hides the gradients.
--
function SeamoreSpheresUnitFrame:HideGradients()
  if self.sphere.fillingLeft and self.sphere.fillingLeft.gradient then
    self.sphere.fillingLeft.gradient:SetAlpha(0)
    self.sphere.fillingLeft.gradient:Hide()
  end

  if self.sphere.fillingRight and self.sphere.fillingRight.gradient then
    self.sphere.fillingRight.gradient:SetAlpha(0)
    self.sphere.fillingRight.gradient:Hide()
  end

  if self.sphere.filling and self.sphere.filling.gradient then
    self.sphere.filling.gradient:SetAlpha(0)
    self.sphere.filling.gradient:Hide()
  end

  if self.sphere.resourceGroup then
    for _, v in pairs(self.sphere.resourceGroup) do
      if v.filling.gradient then
        v.filling.gradient:SetAlpha(0)
        v.filling.gradient:Hide()
      end
    end
  end

  if self.sphere.secondaryResourceGroup then
    for _, v in pairs(self.sphere.secondaryResourceGroup) do
      if v.filling.gradient then
        v.filling.gradient:SetAlpha(0)
        v.filling.gradient:Hide()
      end
    end
  end
end

--- Refreshes sphere position from storage.
--
function SeamoreSpheresUnitFrame:RefreshPos()
  local configName = (self.configName == "vehicle") and "health"
      or ((self.configName == "vehicleResource") and "mainResource" or self.configName)

  -- Set the pos using the proper config.
  if self._anchor ~= ss.db[configName].general.anchor or self._posX ~= ss.db[configName].general.posX or self._posY ~= ss.db[configName].general.posY then
    self:SetPos(ss.db[configName].general.anchor, ss.db[configName].general.posX, ss.db[configName].general.posY)
  end
end

--- Sets the sphere animation.
-- @param anim The animation details.
--
function SeamoreSpheresUnitFrame:SetAnimation(anim)
  if not self.sphere.animation then
    self.sphere.animation = ss.AnimationCreator:New(self, anim)
  end

  self:SetAnimationAlpha(anim.alpha)
  self:SetAnimationOffset(anim.posX, anim.posY)
  self:SetAnimationScale(anim.camDistance)
  self:SetAnimationDisplayInfo(anim.groupId, anim.id)
  self.sphere.animation:SetFrameLevel(anim.frameLevel or 3)
end

--- Sets the animation alpha.
-- @param alpha The alpha value.
--
function SeamoreSpheresUnitFrame:SetAnimationAlpha(alpha)
  self.sphere.animation:SetAlpha(alpha)
  self.animAlpha = alpha
end

--- Sets the animation positions.
-- To set one value, pass nil to the other.
-- @param x The x value. (optional)
-- @param y The y value. (optional)
--
function SeamoreSpheresUnitFrame:SetAnimationOffset(x, y)
  x, y = x or select(2, self.sphere.animation:GetPosition()), y or select(3, self.sphere.animation:GetPosition())
  self.sphere.animation:SetPosition(0, x, y)
  self.animPosX = x
  self.animPosY = y
end

--- Sets the animation scale.
-- @param val The scale value.
--
function SeamoreSpheresUnitFrame:SetAnimationScale(val)
  self.sphere.animation:SetCamDistanceScale(val)
  self.animCamDist = val
end

--- Sets the animation display info.
-- @param groupId The animation group identifier.
-- @param id The animation identifier.
-- @see Animation:SetDisplayInfo().
--
function SeamoreSpheresUnitFrame:SetAnimationDisplayInfo(groupId, id)
  self.sphere.animation:SetDisplayInfo(ss.Animations.get(groupId, id))
  self.animGroup = groupId
  self.animId = id
end

--- Sets the fill texture color.
-- @param color The color value.
--
function SeamoreSpheresUnitFrame:SetFillColor(color)
  if color[2] and color[1] then
    self.sphere.fillingLeft:SetVertColor(color[1])
    self.sphere.fillingRight:SetVertColor(color[2])

    if ss.global.show.gradient then
      local r, g, b, a = ss.Utils:GetRGBAValues(color[1])
      self.sphere.fillingLeft.gradient:SetVertexColor(r, g, b, a)
      self.sphere.fillingLeft.gradient:SetGradientAlpha("VERTICAL", r, g, b, a, r, g, b, 0)

      r, g, b, a = ss.Utils:GetRGBAValues(color[2])
      self.sphere.fillingRight.gradient:SetVertexColor(r, g, b, a)
      self.sphere.fillingRight.gradient:SetGradientAlpha("VERTICAL", r, g, b, a, r, g, b, 0)
    end
  elseif color[2] then
    self.sphere.fillingRight:SetVertColor(color[2])

    if ss.global.show.gradient then
      local r, g, b, a = ss.Utils:GetRGBAValues(color[2])
      self.sphere.fillingRight.gradient:SetVertexColor(r, g, b, a)
      self.sphere.fillingRight.gradient:SetGradientAlpha("VERTICAL", r, g, b, a, r, g, b, 0)
    end
  else
    self.sphere.filling:SetVertColor(color[1])

    if ss.global.show.gradient then
      local r, g, b, a = ss.Utils:GetRGBAValues(color[1])
      self.sphere.filling.gradient:SetVertexColor(r, g, b, a)
      self.sphere.filling.gradient:SetGradientAlpha("VERTICAL", r, g, b, a, r, g, b, 0)
    end
  end
end

--- Sets the fill texture.
-- @param group The texture group.
-- @param id The texture identifier.
--
function SeamoreSpheresUnitFrame:SetFillTexture(group, id)
  local texture = ss.FillTextures.get(group, id)
  self.fillGroup = group
  self.fillId = id

  if texture[2] then
    self.sphere.fillingLeft:SetTexture(texture[1])
    self.sphere.fillingRight:SetTexture(texture[2])

    if ss.global.show.gradient then
      self.sphere.fillingLeft.gradient:SetTexture(texture[1])
      self.sphere.fillingRight.gradient:SetTexture(texture[2])
    end
    self.fillTexture = texture
  else
    self.sphere.filling:SetTexture(texture)

    if ss.global.show.gradient then
      self.sphere.filling.gradient:SetTexture(texture)
    end
    self.fillTexture = { [1] = texture, }
  end
end

--- Sets the resource mode.
-- @param mode The target mode.
-- @param modeName The mode name.
--
function SeamoreSpheresUnitFrame:SetMode(mode, modeName)
  if not mode then
    return ;
  end

  if self.displayMode == "active" then
    ss.profile.modeInfo[self.configName].mode = modeName or ss.profile.modeInfo[self.configName].mode
  end

  self.prevMode = self.mode
  self.mode = mode
  self.sphere.mode = mode
  self.fillId = mode.fillInfo.fillId
  self.fillGroup = mode.fillInfo.fillGroup

  self.monitorConfig = self.sphere:SetMode(mode, modeName)

  self.fillTexture = self.mode.fillTexture
end

--- Updates the sphere position.
-- @param raw Flag to use raw values.
-- @see adjustMidFromRaw().
--
function SeamoreSpheresUnitFrame:SetPos(anchor, x, y, raw)
  if raw then
    local scale, bScale = self:GetRawScales()
    local xPad = GetScreenWidth() / 4
    local yPad = GetScreenHeight() / 4
    local anchorAdjust = ""

    if not x then
      x = self._posX * scale
      if string.match(strlower(self._anchor), "left") then
        anchorAdjust = "LEFT"
      elseif string.match(strlower(self._anchor), "right") then
        anchorAdjust = "RIGHT"
      end
    elseif x + ss.global.general.SphereSize > (xPad * 3) then
      anchorAdjust = "RIGHT"
      x = x + ss.global.general.SphereSize - (xPad * 4)
    elseif x <= xPad then
      anchorAdjust = "LEFT"
    elseif x > xPad then
      x = adjustMidFromRaw(x, scale, xPad)
    end

    if not y then
      y = self._posY * scale
      if string.match(strlower(self._anchor), "top") then
        anchorAdjust = "TOP" .. anchorAdjust
      elseif string.match(strlower(self._anchor), "bottom") then
        anchorAdjust = "BOTTOM" .. anchorAdjust
      end
    elseif y + ss.global.general.SphereSize > (yPad * 3) then
      anchorAdjust = "TOP" .. anchorAdjust
      y = y + ss.global.general.SphereSize - (yPad * 4)
    elseif y <= yPad then
      anchorAdjust = "BOTTOM" .. anchorAdjust
    elseif y > yPad then
      y = adjustMidFromRaw(y, scale, yPad)
    end

    anchor = (anchorAdjust == "") and "CENTER" or anchorAdjust
    x = x * bScale
    y = y * bScale
  end

  self._anchor = anchor or self._anchor
  self._posX = x or self._posX
  self._posY = y or self._posY

  if self.displayMode ~= "preview" then
    ss.PendingTask.remove("setPos" .. tostring(self._sphereIndex))
    ss.PendingTask.add("setPos" .. tostring(self._sphereIndex), function()
      -- Update the pos and all contained animations.
      self:ClearAllPoints()
      self:SetPoint(self._anchor, self._posX, self._posY)
    end)
  else
    local configName = (self.configName == "vehicle") and "health"
        or ((self.configName == "vehicleResource") and "mainResource" or self.configName)

    if ss.Layout.spheres[configName] then
      -- Display the change using the actual sphere.
      ss.Layout.spheres[configName]:SetPos(anchor, x, y)
    end
  end
end

--- Sets the sphere resource.
-- @param resource The target resource.
--
function SeamoreSpheresUnitFrame:setResource(resource)
  if not resource and not resource.modes[ss.profile.modeInfo[resource.general.configName].modeSpec] then
    return ;
  end

  self.sphere.textFormat = resource.textFormat
  self.dbConfig = resource
  self.textFormat = resource.textFormat
  self.modeId = ss.profile.modeInfo[resource.general.configName].mode
  self.configName = resource.general.configName
  self.animPosX = resource.animation.posX
  self.animPosY = resource.animation.posY
  self.animGroup = resource.animation.groupId
  self.animId = resource.animation.id
  self.animCamDist = resource.animation.camDistance
  self.animAlpha = resource.animation.alpha
  local mode = resource.modes[ss.profile.modeInfo[resource.general.configName].modeSpec][ss.profile.modeInfo[resource.general.configName].mode]
  self.speed = mode.swirl.speed or 5

  self.monitorConfig = self:SetResource(resource.general.configName)
end

--- Sets the sphere scale.
-- @param scale The scale value.
--
function SeamoreSpheresUnitFrame:SetGlobalScale(scale)
  self._globalScale = scale

  if self.displayMode ~= "preview" then
    ss.PendingTask.remove("setScale" .. tostring(self._sphereIndex))
    ss.PendingTask.add("setScale" .. tostring(self._sphereIndex), function()
      -- Update the scale and refresh all contained animations.
      self:SetScale(scale / 100)
      self.sphere.animation:RefreshCamera()

      if self.sphere.resourceGroup then
        for _, v in pairs(self.sphere.resourceGroup) do
          v.animation:RefreshCamera()

          if v.resourceAnimation then
            v.resourceAnimation:RefreshCamera()
          end
        end
      end

      if self.sphere.secondaryResourceGroup then
        for _, v in pairs(self.sphere.secondaryResourceGroup) do
          v.animation:RefreshCamera()

          if v.resourceAnimation then
            v.resourceAnimation:RefreshCamera()
          end
        end
      end
    end)
  else
    local configName = (self.configName == "vehicle") and "health"
        or ((self.configName == "vehicleResource") and "mainResource" or self.configName)

    if ss.Layout.spheres[configName] then
      -- Display the change using the actual sphere.
      ss.Layout.spheres[configName]:SetGlobalScale(scale)
    end
  end
end

--- Sets the swirl color.
-- @param color The color value.
-- @param index The swirl index. (optional)
--
function SeamoreSpheresUnitFrame:setSwirlColor(color, index)
  if not index then
    self.sphere.swirl1.texture:SetVertColor(color)
    self.sphere.swirl2.texture:SetVertColor(color)
    self.sphere.swirl3.texture:SetVertColor(color)
  else
    self.sphere["swirl" .. index].texture:SetVertColor(color)
  end
end

--- Sets the swirl speed.
-- @param speed The speed value.
--
function SeamoreSpheresUnitFrame:setSwirlSpeed(speed)
  self.sphere.swirl1:SetSpeed(speed, 2, 4)
  self.sphere.swirl2:SetSpeed(speed, 3, 5)
  self.sphere.swirl3:SetSpeed(speed, 2, 3)

  self.speed = speed or 5
end

--- Sets the swirl texture.
-- @param id The texture identifier.
--
function SeamoreSpheresUnitFrame:setSwirlTexture(id)
  self.sphere.swirlId = id
  local textures = ss.FillTextures.get("swirl", id)

  self.sphere.swirl1.texture:SetTexture(textures[1])
  self.sphere.swirl2.texture:SetTexture(textures[2])
  self.sphere.swirl3.texture:SetTexture(textures[3])

  local size = (self.displayMode == "active") and ss.db[self.configName].general.size or (ss.db[self.configName].general.size * ss.db[self.configName].general.scale)
  size = ss.FillTextures.getSwirlSize(id, size)

  self.sphere.swirl1:SetWidth(size * 0.9)
  self.sphere.swirl1:SetHeight(size * 0.9)
  self.sphere.swirl2:SetWidth(size * 0.9)
  self.sphere.swirl2:SetHeight(size * 0.9)
  self.sphere.swirl3:SetWidth(size * 0.9)
  self.sphere.swirl3:SetHeight(size * 0.9)
end

--- Sets the text value for font 1.
-- @param text The string to use.
--
function SeamoreSpheresUnitFrame:setText(text)
  self.sphere.font1:SetText(text)
end

--- Sets the text value for font 2.
-- @param text The string to use.
--
function SeamoreSpheresUnitFrame:setText2(text)
  self.sphere.font2:SetText(text)
end

--- Sets the color for font 1.
-- @param color The color value.
--
function SeamoreSpheresUnitFrame:setTextColor(color)
  self.sphere.font1:SetTextColor(ss.Utils:GetRGBAValues(color))
end

--- Sets the color for font 2.
-- @param color The color value.
--
function SeamoreSpheresUnitFrame:setTextColor2(color)
  self.sphere.font2:SetTextColor(ss.Utils:GetRGBAValues(color))
end

--- Sets the text format.
-- @param textFormat The format value.
--
function SeamoreSpheresUnitFrame:setTextFormat(textFormat)
  self.textFormat = textFormat
  self.sphere.textFormat = textFormat
end

--- Toggles sphere visibility. (safe)
-- @param show Flag to display the frames.
--
function SeamoreSpheresUnitFrame:show(show)
  self:ShowSphere(show)
end

--- Shows the sphere display.
--
function SeamoreSpheresUnitFrame:Show()
  self:ShowSphere(true)
end

--- Toggles font visibility.
-- @param font The target font.
-- @param show Flag to display the frame.
--
function SeamoreSpheresUnitFrame:showFont(font, show)
  if font == 1 and show == false then
    self.sphere.font1:Hide()
  elseif font == 2 and show == false then
    self.sphere.font2:Hide()
  elseif not font and show == false then
    self.sphere.font1:Hide()
    self.sphere.font2:Hide()
  elseif font == 1 then
    self.sphere.font1:Show()
  elseif font == 2 then
    self.sphere.font2:Show()
  else
    self.sphere.font1:Show()
    self.sphere.font2:Show()
  end
end

--- Displays the gradients.
--
function SeamoreSpheresUnitFrame:showGradients()
  if not InCombatLockdown() then
    self.sphere.fillingLeft.gradient:SetAlpha(1)
    self.sphere.fillingLeft.gradient:Show()
    self.sphere.fillingRight.gradient:SetAlpha(1)
    self.sphere.fillingRight.gradient:Show()

    self.sphere.filling.gradient:SetAlpha(1)
    self.sphere.filling.gradient:Show()

    if self.configName == "health" and self.displayMode == "active" then
      self.sphere.fillingAbsorb.gradient:SetAlpha(1)
      self.sphere.fillingAbsorb.gradient:Show()
    end
  end
end

--- Updates resource group textures.
-- @param index The group index.
-- @param textureGroup The texture group identifier.
-- @param texture The texture identifier.
--
function SeamoreSpheresUnitFrame:updateResourceGroupTextures(index, textureGroup, texture)
  local group = (index and index == 1) and "resourceGroup" or "secondaryResourceGroup"

  if self.sphere[group] then
    for _, v in pairs(self.sphere[group]) do
      v.filling:SetTexture(ss.FillTextures.get(textureGroup, texture))

      if ss.global.show.gradient then
        v.filling.gradient:SetTexture(ss.FillTextures.get(textureGroup, texture))
      end
    end
  end
end

--- Refreshes the sphere scale from storage.
--
function SeamoreSpheresUnitFrame:UpdateScale()
  local configName = (self.configName == "vehicle") and "health"
      or ((self.configName == "vehicleResource") and "mainResource" or self.configName)

  -- Set the scale using the proper config.
  if not self._globalScale or self._globalScale ~= ss.db[configName].general.globalScale then
    self:SetGlobalScale(ss.db[configName].general.globalScale)
  end
end

rawset(_G, "SeamoreSpheresUnitFrame_OnLoad", function(self)
  sphereIndex = sphereIndex + 1
  self._sphereIndex = sphereIndex

  for k, v in pairs(SeamoreSpheresUnitFrame) do
    self[k] = v
  end

  ss.Extensions:MakeFadable(self)
end)

rawset(_G, "SeamoreSpheresUnitFrame", {
  DISPLAY_TYPE_ACTIVE = "active",
  DISPLAY_TYPE_PREVIEW = "preview",
  New = function(self, sphereConfig, parent, displayMode)
    displayMode = displayMode or self.DISPLAY_TYPE_ACTIVE
    _frameIndex[displayMode] = _frameIndex[displayMode] + 1

    local template = (displayMode == self.DISPLAY_TYPE_PREVIEW) and "SeamoreSpheresPreviewFrameTemplate" or "SeamoreSpheresUnitFrameTemplate"
    local name = ss._f .. sphereConfig.general.configName .. "_" .. string.gsub(template, "Template", tostring(_frameIndex[displayMode]))
    local base = CreateFrame("Button", name, parent, template)

    base.displayMode = displayMode
    base.dbConfig = sphereConfig
    base.configName = sphereConfig.general.configName
    base.name = sphereConfig.general.name
    base.mode = sphereConfig.modes[ss.profile.modeInfo[base.configName].modeSpec][ss.profile.modeInfo[base.configName].mode]
    base.textFormat = sphereConfig.textFormat
    base.animPosX = sphereConfig.animation.posX
    base.animPosY = sphereConfig.animation.posY
    base.animGroup = sphereConfig.animation.groupId
    base.animId = sphereConfig.animation.id
    base.animCamDist = sphereConfig.animation.camDistance
    base.animAlpha = sphereConfig.animation.alpha
    base.fillTexture = base.mode.fillTexture
    base.fillId = base.mode.fillInfo.fillId
    base.fillGroup = base.mode.fillInfo.fillGroup
    base.modeId = ss.profile.modeInfo[base.configName].mode

    base.monitorConfig = base:Init(base.dbConfig.general.configName, base.displayMode)

    base:SetPos(sphereConfig.general.anchor, sphereConfig.general.posX, sphereConfig.general.posY)
    base:setSwirlSpeed(base.mode.swirl.speed)
    base:showFont(1, sphereConfig.font[1].show)
    base:showFont(2, sphereConfig.font[2].show)

    return base
  end,
})
