local _, ns = ...
local SlickUI = ns.SlickUI
local Theme = SlickUI.Theme
local oUF = ns.oUF

local Labels = {
	player = "Player",
	target = "Target",
	focus = "Focus",
	pet = "Pet",
	targettarget = "Target of Target",
	boss = "Boss Frames",
}

SlickUI.BossFrames = SlickUI.BossFrames or {}

-- SecureUnitButtonTemplate handles right-click through the "togglemenu"
-- attribute, but it still expects the layout to provide this callback.
-- Sharing it keeps personal and group frames on Blizzard's current menu API
-- without installing an insecure OnClick handler.
function SlickUI.OpenUnitMenu(frame, menuType)
	local unit = frame.unit
	if not unit or type(UnitPopup_OpenMenu) ~= "function" then
		return
	end

	UnitPopup_OpenMenu(menuType or frame.SlickMenuType or "TARGET", {
		unit = unit,
		name = UnitName(unit),
	})
end

local function GetConfigKey(unit)
	if type(unit) == "string" and unit:match("^boss%d+$") then
		return "boss"
	end
	return unit
end

local PlayerDisplayChoices = {
	{ key = "none", label = "None" },
	{ key = "name", label = "Name", tag = "[name]" },
	{ key = "level", label = "Level", tag = "[slickui:unitlevel]" },
	{ key = "classification", label = "Classification", tag = "[slickui:classification]" },
	{ key = "status", label = "Status", tag = "[slickui:playerstatus]" },
	{ key = "healthCurrent", label = "Current Health", tag = "[slickui:shorthealth]" },
	{ key = "healthValues", label = "Current / Maximum Health", tag = "[slickui:shorthealth]/[slickui:shortmaxhealth]" },
	{ key = "healthPercent", label = "Health Percent", tag = "[perhp<$%]" },
	{ key = "healthMissing", label = "Missing Health", tag = "[slickui:shortmissinghealth]" },
	{ key = "powerCurrent", label = "Current Power", tag = "[curpp]" },
	{ key = "powerValues", label = "Current / Maximum Power", tag = "[curpp]/[maxpp]" },
	{ key = "powerPercent", label = "Power Percent", tag = "[perpp<$%]" },
}

local PlayerDisplayChoiceByKey = {}
for _, choice in ipairs(PlayerDisplayChoices) do
	PlayerDisplayChoiceByKey[choice.key] = choice
end

local PlayerDisplaySlots = {
	{ key = "topLeft", point = "TOPLEFT", relativePoint = "TOPLEFT", x = 22, y = -3, justify = "LEFT" },
	{ key = "topCenter", point = "TOP", relativePoint = "TOP", x = 0, y = -3, justify = "CENTER" },
	{ key = "topRight", point = "TOPRIGHT", relativePoint = "TOPRIGHT", x = -5, y = -3, justify = "RIGHT" },
	{ key = "middleLeft", point = "LEFT", relativePoint = "LEFT", x = 5, y = 0, justify = "LEFT" },
	{ key = "middleCenter", point = "CENTER", relativePoint = "CENTER", x = 0, y = 0, justify = "CENTER" },
	{ key = "middleRight", point = "RIGHT", relativePoint = "RIGHT", x = -5, y = 0, justify = "RIGHT" },
	{ key = "bottomLeft", point = "BOTTOMLEFT", relativePoint = "BOTTOMLEFT", x = 5, y = 3, justify = "LEFT" },
	{ key = "bottomCenter", point = "BOTTOM", relativePoint = "BOTTOM", x = 0, y = 3, justify = "CENTER" },
	{ key = "bottomRight", point = "BOTTOMRIGHT", relativePoint = "BOTTOMRIGHT", x = -5, y = 3, justify = "RIGHT" },
}

SlickUI.PlayerDisplayChoices = PlayerDisplayChoices
SlickUI.PlayerDisplayChoiceByKey = PlayerDisplayChoiceByKey
SlickUI.PlayerDisplaySlots = PlayerDisplaySlots

oUF.Tags.Methods["slickui:unitlevel"] = function(unit)
	local level = UnitLevel(unit)
	if issecretvalue and issecretvalue(level) then
		return level
	elseif type(level) == "number" and level > 0 then
		return level
	end
	return "??"
end
oUF.Tags.Events["slickui:unitlevel"] = "UNIT_LEVEL PLAYER_LEVEL_UP"

local function IsSafeBoolean(value, expected)
	if issecretvalue and issecretvalue(value) then
		return false
	end
	return value == expected
end

local function IsAttackableUnit(unit)
	if type(unit) ~= "string" or type(UnitCanAttack) ~= "function" then return false end
	local ok, attackable = pcall(UnitCanAttack, "player", unit)
	return ok and IsSafeBoolean(attackable, true)
end

oUF.Tags.Methods["slickui:playerstatus"] = function(unit)
	if IsSafeBoolean(UnitIsDead(unit), true) then
		return "Dead"
	elseif IsSafeBoolean(UnitIsGhost(unit), true) then
		return "Ghost"
	elseif IsSafeBoolean(UnitIsConnected(unit), false) then
		return "Offline"
	elseif IsSafeBoolean(UnitIsAFK(unit), true) then
		return "AFK"
	elseif IsSafeBoolean(UnitIsDND(unit), true) then
		return "DND"
	elseif unit == "player" and IsSafeBoolean(IsResting(), true) then
		return "zzz"
	end
end
oUF.Tags.Events["slickui:playerstatus"] = "UNIT_HEALTH UNIT_CONNECTION PLAYER_FLAGS_CHANGED PLAYER_UPDATE_RESTING"

local ClassificationLabels = {
	elite = "|cffffa52cElite|r",
	rare = "|cff48b8ffRare|r",
	rareelite = "|ffc76cffRare Elite|r",
	worldboss = "|cffff4040World Boss|r",
	minus = "|cff9a9a9aMinor|r",
	trivial = "|cff808080Trivial|r",
}

oUF.Tags.Methods["slickui:classification"] = function(unit)
	local classification = UnitClassification(unit)
	if canaccessvalue and not canaccessvalue(classification) then return end
	if issecretvalue and issecretvalue(classification) then return end
	if type(classification) ~= "string" then return end
	return ClassificationLabels[classification]
end
oUF.Tags.Events["slickui:classification"] = "UNIT_CLASSIFICATION_CHANGED"

local CompactNumberData = {
	breakpointData = {
		{ breakpoint = 1e9, abbreviation = "B", significandDivisor = 1e8, fractionDivisor = 10, abbreviationIsGlobal = false },
		{ breakpoint = 1e6, abbreviation = "M", significandDivisor = 1e5, fractionDivisor = 10, abbreviationIsGlobal = false },
		{ breakpoint = 1e3, abbreviation = "K", significandDivisor = 100, fractionDivisor = 10, abbreviationIsGlobal = false },
	},
}

local function CompactNumber(value)
	return AbbreviateNumbers(value, CompactNumberData)
end

oUF.Tags.Methods["slickui:shorthealth"] = function(unit)
	return CompactNumber(UnitHealth(unit))
end
oUF.Tags.Events["slickui:shorthealth"] = "UNIT_HEALTH UNIT_MAXHEALTH"

oUF.Tags.Methods["slickui:shortmaxhealth"] = function(unit)
	return CompactNumber(UnitHealthMax(unit))
end
oUF.Tags.Events["slickui:shortmaxhealth"] = "UNIT_MAXHEALTH"

oUF.Tags.Methods["slickui:shortmissinghealth"] = function(unit)
	local missing = UnitHealthMissing(unit)
	if issecretvalue and issecretvalue(missing) then return CompactNumber(missing) end
	if type(missing) ~= "number" or missing == 0 then return end
	return CompactNumber(missing)
end
oUF.Tags.Events["slickui:shortmissinghealth"] = "UNIT_HEALTH UNIT_MAXHEALTH"

local function GetSettings(unit)
	return SlickUI.DB.unitFrames.frames[GetConfigKey(unit)]
end

local function UpdateUnitFrameHealthColor(health, unit)
	local reaction = UnitReaction(unit, "player")
	if canaccessvalue and not canaccessvalue(reaction) then return end
	if issecretvalue and issecretvalue(reaction) then return end
	if type(reaction) == "number" and reaction <= 3 then
		health:SetStatusBarColor(unpack(Theme.Colors.hostile))
	elseif reaction == 4 then
		health:SetStatusBarColor(unpack(Theme.Colors.neutral))
	end
end

local function UpdateFrameDisplayWidths(frame)
	if not frame.SlickDisplaySlots then
		return
	end

	local width = math.max(30, frame:GetWidth() / 3 - 8)
	for _, text in pairs(frame.SlickDisplaySlots) do
		text:SetWidth(width)
	end
end

local function CreateFrameDisplay(self, health, unit)
	unit = GetConfigKey(unit)
	self.SlickDisplaySlots = {}
	for _, slot in ipairs(PlayerDisplaySlots) do
		local fontSizes = SlickUI.DB.unitFrames.frameDisplayFontSizes[unit]
		local display = SlickUI.DB.unitFrames.frameDisplays[unit]
		local fontSize = fontSizes[slot.key] or 9
		local text = Theme:CreateText(health, fontSize)
		local x = slot.key == "topLeft" and unit ~= "player" and 5 or slot.x
		text:SetPoint(slot.point, health, slot.relativePoint, x, slot.y)
		text:SetJustifyH(slot.justify)
		text:SetWordWrap(false)
		self.SlickDisplaySlots[slot.key] = text

		local selected = display[slot.key] or "none"
		local choice = PlayerDisplayChoiceByKey[selected]
		if choice and choice.tag then
			self:Tag(text, choice.tag)
		else
			text:Hide()
		end
	end
	UpdateFrameDisplayWidths(self)
end

local function CreateCastbar(self, width)
	local castbar = CreateFrame("StatusBar", nil, self)
	castbar:SetStatusBarTexture(Theme.Texture)
	castbar:SetStatusBarColor(unpack(Theme.Colors.cast))
	castbar:SetSize(width, 18)
	castbar:SetPoint("TOP", self, "BOTTOM", 0, -5)
	castbar:SetFrameLevel(self:GetFrameLevel() + 5)
	Theme:CreatePanel(castbar)
	Theme:CreateBarBackground(castbar)

	local icon = castbar:CreateTexture(nil, "ARTWORK")
	icon:SetSize(18, 18)
	icon:SetPoint("RIGHT", castbar, "LEFT", -4, 0)
	icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
	castbar.Icon = icon

	local text = Theme:CreateText(castbar, 11)
	text:SetPoint("LEFT", castbar, "LEFT", 5, 0)
	text:SetPoint("RIGHT", castbar, "RIGHT", -42, 0)
	text:SetJustifyH("LEFT")
	castbar.Text = text

	local time = Theme:CreateText(castbar, 11)
	time:SetPoint("RIGHT", castbar, "RIGHT", -5, 0)
	castbar.Time = time

	local spark = castbar:CreateTexture(nil, "OVERLAY")
	spark:SetTexture("Interface\\CastingBar\\UI-CastingBar-Spark")
	spark:SetSize(10, 24)
	spark:SetBlendMode("ADD")
	castbar.Spark = spark

	local shield = castbar:CreateTexture(nil, "OVERLAY")
	shield:SetTexture("Interface\\CastingBar\\UI-CastingBar-Small-Shield")
	shield:SetSize(22, 28)
	shield:SetPoint("CENTER", icon)
	castbar.Shield = shield

	castbar.PostCastStart = function(element)
		element:SetStatusBarColor(unpack(Theme.Colors.cast))
	end
	castbar.PostCastInterruptible = function(element)
		local owner = element.__owner or element:GetParent()
		local unit = owner and (owner.unit or owner.SlickUnit)
		if not IsAttackableUnit(unit) then
			element:SetStatusBarColor(unpack(Theme.Colors.cast))
		elseif element.notInterruptible == true then
			element:SetStatusBarColor(unpack(Theme.Colors.castProtected))
		else
			element:SetStatusBarColor(unpack(Theme.Colors.castInterruptible))
		end
	end

	self.Castbar = castbar
end

local function AnchorPlayerCastbar(frame)
	local castbar = frame and frame.Castbar
	if not castbar then return end
	if InCombatLockdown() then
		frame.SlickResourceLayoutPending = true
		return
	end
	castbar:ClearAllPoints()
	local summons = frame.SlickTotems
	local stagger = frame.Stagger
	local resource = frame.SlickActiveResource
	if summons then
		castbar:SetPoint("TOP", summons, "BOTTOM", 0, -5)
	elseif stagger and stagger:IsShown() then
		castbar:SetPoint("TOP", stagger, "BOTTOM", 0, -5)
	elseif resource and resource:IsShown() then
		castbar:SetPoint("TOP", resource, "BOTTOM", 0, -5)
	else
		castbar:SetPoint("TOP", frame, "BOTTOM", 0, -5)
	end
end

local function AnchorPlayerSecondaryResources(frame)
	if not frame then return end
	if InCombatLockdown() then
		frame.SlickResourceLayoutPending = true
		return
	end
	frame.SlickResourceLayoutPending = nil
	local resource = frame.SlickActiveResource
	local stagger = frame.Stagger
	local summons = frame.SlickTotems
	if stagger then
		stagger:ClearAllPoints()
		if resource and resource:IsShown() then
			stagger:SetPoint("TOPLEFT", resource, "BOTTOMLEFT", 0, -3)
		else
			stagger:SetPoint("TOPLEFT", frame, "BOTTOMLEFT", 0, -3)
		end
	end
	if summons then
		summons:ClearAllPoints()
		if stagger and stagger:IsShown() then
			summons:SetPoint("TOPLEFT", stagger, "BOTTOMLEFT", 0, -3)
		elseif resource and resource:IsShown() then
			summons:SetPoint("TOPLEFT", resource, "BOTTOMLEFT", 0, -3)
		else
			summons:SetPoint("TOPLEFT", frame, "BOTTOMLEFT", 0, -3)
		end
	end
	AnchorPlayerCastbar(frame)
end

local function CreatePlayerTotems(frame)
	local count, size, spacing = 4, 25, 3
	local row = CreateFrame("Frame", nil, frame)
	row:SetSize((count * size) + ((count - 1) * spacing), size)
	row:SetPoint("TOPLEFT", frame, "BOTTOMLEFT", 0, -3)
	local totems = {}
	for index = 1, count do
		local button = CreateFrame("Button", nil, row, "BackdropTemplate,SecureActionButtonTemplate")
		button:SetSize(size, size)
		button:SetPoint("LEFT", row, "LEFT", (index - 1) * (size + spacing), 0)
		button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
		button:SetAttribute("type", "destroytotem")
		button:SetAttribute("totem-slot", index)
		button:SetAttribute("useOnKeyDown", false)
		button:SetAlpha(0)
		Theme:CreatePanel(button)
		local icon = button:CreateTexture(nil, "ARTWORK")
		icon:SetPoint("TOPLEFT", 2, -2)
		icon:SetPoint("BOTTOMRIGHT", -2, 2)
		icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
		button.Icon = icon
		local cooldown = CreateFrame("Cooldown", nil, button, "CooldownFrameTemplate")
		cooldown:SetAllPoints(icon)
		cooldown:SetDrawEdge(false)
		cooldown:SetHideCountdownNumbers(false)
		button.Cooldown = cooldown
		totems[index] = button
	end
	frame.Totems = totems
	frame.SlickTotems = row
end

local function StyleAuraButton(_, button)
	button.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
	button.Icon:SetDrawLayer("ARTWORK")
	button.Cooldown:SetDrawEdge(false)
	button.Cooldown:SetHideCountdownNumbers(false)
	Theme:CreatePanel(button)
	Theme:SetFont(button.Count, 10)
	button.Count:ClearAllPoints()
	button.Count:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", -1, 1)
	local mine = button:CreateTexture(nil, "OVERLAY")
	mine:SetColorTexture(unpack(Theme.Colors.border))
	mine:SetPoint("TOPLEFT", button, "TOPLEFT", 2, -2)
	mine:SetSize(5, 5)
	mine:Hide()
	button.SlickMine = mine
end

local function UpdateAuraButton(_, button, _, data)
	button.SlickMine:SetShown(button.isHarmfulAura == true and data.isPlayerAura == true)
end

local function IsReadable(value)
	if canaccessvalue and not canaccessvalue(value) then return false end
	if issecretvalue and issecretvalue(value) then return false end
	return value ~= nil
end

local function IsKnownActiveSpell(spellID)
	if not IsReadable(spellID) or type(IsPlayerSpell) ~= "function" then
		return false
	end

	local ok, known = pcall(IsPlayerSpell, spellID)
	if not ok or not IsReadable(known) or known ~= true then
		return false
	end

	local passiveFunction = C_Spell and C_Spell.IsSpellPassive or IsPassiveSpell
	if type(passiveFunction) == "function" then
		local passiveOK, passive = pcall(passiveFunction, spellID)
		if passiveOK and IsReadable(passive) and passive == true then
			return false
		end
	end

	return true
end

local ExactConsumableBuffNames = {
	["flask of the shattered sun"] = true,
	["flask of the blood knights"] = true,
	["flask of the magisters"] = true,
	["flask of thalassian resistance"] = true,
	["thalassian phoenix oil"] = true,
	["void-touched augment rune"] = true,
}

local function IsRecognizedConsumableBuff(name)
	if not IsReadable(name) or type(name) ~= "string" then
		return false
	end

	local normalized = string.lower(name)
	return ExactConsumableBuffNames[normalized] == true
		or string.find(normalized, "well fed", 1, true) ~= nil
		or string.find(normalized, "flask of ", 1, true) == 1
		or string.find(normalized, "phial of ", 1, true) == 1
		or string.find(normalized, " augment rune", 1, true) ~= nil
		or string.find(normalized, " oil", 1, true) ~= nil
end

local function CreateAuraRow(self, auraType)
	local row = CreateFrame("Frame", nil, self)
	row.__owner = self
	row.initialAnchor = "BOTTOMLEFT"
	row.growthX = "RIGHT"
	row.growthY = "UP"
	row.reanchorIfVisibleChanged = true
	row.showStealableBuffs = auraType == "buffs"
	row.showDebuffType = auraType == "debuffs"
	row.PostCreateButton = StyleAuraButton
	row.PostUpdateButton = UpdateAuraButton
	if auraType == "buffs" then
		self.Buffs = row
	else
		self.Debuffs = row
	end
	return row
end

local function FilterKnownSpellAndConsumableBuffs(element, unit, data)
	-- Requiring a duration removes permanent account, equipment, profession, and
	-- world effects before the spellbook/consumable tests are considered.
	if C_UnitAuras.GetAuraDuration(unit, data.auraInstanceID) == nil then
		return false
	end

	if IsKnownActiveSpell(data.spellId) then
		return true
	end

	return element.includeRecognizedConsumables == true
		and IsRecognizedConsumableBuff(data.name)
end

local function GetHorizontalAuraLayout(frameWidth, settings)
	local iconSize = settings.iconSize
	local spacing = settings.spacing
	local count = settings.maxIcons
	local columns = math.max(1, math.min(count, math.floor((frameWidth + spacing) / (iconSize + spacing))))
	local rows = math.ceil(count / columns)
	local height = rows * iconSize + math.max(0, rows - 1) * spacing
	return columns, height
end

local function ConfigureAuraRow(frame, auraType, settings, sibling)
	local row = auraType == "buffs" and frame.Buffs or frame.Debuffs
	local iconSize = settings.iconSize
	local spacing = settings.spacing
	local count = settings.maxIcons
	local sameSideOffset = 0
	local frameWidth = frame:GetWidth()
	local horizontalColumns, horizontalHeight = GetHorizontalAuraLayout(frameWidth, settings)
	if auraType == "debuffs" and sibling.enabled and sibling.position == settings.position then
		if settings.position == "ABOVE" or settings.position == "BELOW" then
			local _, siblingHeight = GetHorizontalAuraLayout(frameWidth, sibling)
			sameSideOffset = siblingHeight + 2
		else
			sameSideOffset = sibling.iconSize + sibling.spacing + 2
		end
	end

	row.size = iconSize
	row.spacing = spacing
	row.num = count
	row.onlyShowPlayer = false
	row.includeRecognizedConsumables = settings.includeConsumables == true
	row.FilterAura = auraType == "buffs" and settings.knownSpellsOnly == true and FilterKnownSpellAndConsumableBuffs or nil
	row.maxCols = count
	row.anchoredButtons = 0
	row:ClearAllPoints()
	if settings.position == "BELOW" then
		row.initialAnchor = "TOPLEFT"
		row.growthX = "RIGHT"
		row.growthY = "DOWN"
		row:SetPoint("TOPLEFT", frame, "BOTTOMLEFT", 0, -4 - sameSideOffset)
		row.maxCols = horizontalColumns
		row:SetSize(frameWidth, horizontalHeight)
	elseif settings.position == "LEFT" then
		row.initialAnchor = "BOTTOMRIGHT"
		row.growthX = "LEFT"
		row.growthY = "UP"
		row.maxCols = 1
		row:SetPoint("BOTTOMRIGHT", frame, "BOTTOMLEFT", -4 - sameSideOffset, 0)
		row:SetSize(iconSize, count * iconSize + math.max(0, count - 1) * spacing)
	elseif settings.position == "RIGHT" then
		row.initialAnchor = "BOTTOMLEFT"
		row.growthX = "RIGHT"
		row.growthY = "UP"
		row.maxCols = 1
		row:SetPoint("BOTTOMLEFT", frame, "BOTTOMRIGHT", 4 + sameSideOffset, 0)
		row:SetSize(iconSize, count * iconSize + math.max(0, count - 1) * spacing)
	else
		row.initialAnchor = "BOTTOMLEFT"
		row.growthX = "RIGHT"
		row.growthY = "UP"
		row:SetPoint("BOTTOMLEFT", frame, "TOPLEFT", 0, 4 + sameSideOffset)
		row.maxCols = horizontalColumns
		row:SetSize(frameWidth, horizontalHeight)
	end
end

local function LayoutResourceSegments(element, count, width)
	count = math.max(1, count or #element)
	local spacing = 2
	local segmentWidth = math.max(1, (width - spacing * (count - 1)) / count)
	for index, bar in ipairs(element) do
		bar:ClearAllPoints()
		bar:SetWidth(segmentWidth)
		if index == 1 then
			bar:SetPoint("LEFT", element, "LEFT", 0, 0)
		else
			bar:SetPoint("LEFT", element[index - 1], "RIGHT", spacing, 0)
		end
	end
end

local function CreateSegmentedResource(frame, count)
	local element = CreateFrame("Frame", nil, frame)
	element:SetPoint("TOPLEFT", frame, "BOTTOMLEFT", 0, -3)
	element:SetSize(frame:GetWidth(), 7)
	for index = 1, count do
		local bar = CreateFrame("StatusBar", nil, element)
		bar:SetHeight(7)
		bar:SetStatusBarTexture(Theme.Texture)
		bar:SetMinMaxValues(0, 1)
		bar:SetValue(0)
		bar:Hide()
		Theme:CreateBarBackground(bar)
		element[index] = bar
	end
	LayoutResourceSegments(element, count, frame:GetWidth())
	return element
end

local function CreatePlayerClassResources(frame)
	local classPower = CreateSegmentedResource(frame, 10)
	classPower:Hide()
	classPower.PostUpdate = function(element, _, maximum, maximumChanged)
		if maximumChanged and maximum and maximum > 0 then
			LayoutResourceSegments(element, maximum, frame:GetWidth())
		end
	end
	classPower.PostVisibility = function(element, visible)
		if visible == true then
			frame.SlickActiveResource = element
		elseif frame.SlickActiveResource == element then
			frame.SlickActiveResource = nil
		end
		AnchorPlayerSecondaryResources(frame)
	end
	frame.ClassPower = classPower

	local runes = CreateSegmentedResource(frame, 6)
	runes.colorSpec = true
	local _, classToken = UnitClass("player")
	if classToken == "DEATHKNIGHT" then
		runes:Show()
		frame.SlickActiveResource = runes
	else
		runes:Hide()
	end
	frame.Runes = runes

	frame.SlickClassPower = classPower
	frame.SlickRunes = runes

	local stagger = CreateFrame("StatusBar", nil, frame)
	stagger:SetSize(frame:GetWidth(), 9)
	stagger:SetStatusBarTexture(Theme.Texture)
	stagger:SetMinMaxValues(0, 1)
	stagger:SetValue(0)
	Theme:CreateBarBackground(stagger)
	local staggerText = Theme:CreateText(stagger, 9)
	staggerText:SetPoint("CENTER")
	staggerText:SetText("Stagger")
	stagger.Text = staggerText
	stagger.PostVisibility = function(_, visible)
		AnchorPlayerSecondaryResources(frame)
	end
	frame.Stagger = stagger
	frame.SlickStagger = stagger
	CreatePlayerTotems(frame)
	AnchorPlayerSecondaryResources(frame)
end

-- Resource visibility can change while mounting a combat vehicle. The player
-- frame is protected then, so replay only the cosmetic anchor work afterward.
local resourceLayoutEvents = CreateFrame("Frame")
resourceLayoutEvents:RegisterEvent("PLAYER_REGEN_ENABLED")
resourceLayoutEvents:SetScript("OnEvent", function()
	local frame = SlickUI.UnitFrames and SlickUI.UnitFrames.player
	if frame and frame.SlickResourceLayoutPending then
		AnchorPlayerSecondaryResources(frame)
	end
end)

local function Style(self, unit)
	local configKey = GetConfigKey(unit)
	local settings = GetSettings(unit)
	self.SlickUnit = unit
	self.SlickConfigKey = configKey
	self:SetSize(settings.width, settings.height)
	self:SetScale(settings.scale)
	self:RegisterForClicks("AnyUp")
	self:SetAttribute("*type1", "target")
	self:SetAttribute("*type2", "togglemenu")
	self.SlickMenuType = unit == "player" and "SELF" or (unit == "pet" and "PET" or "TARGET")
	self.menu = SlickUI.OpenUnitMenu
	self:SetFrameStrata("LOW")
	self:SetScript("OnEnter", UnitFrame_OnEnter)
	self:SetScript("OnLeave", UnitFrame_OnLeave)

	Theme:CreatePanel(self)

	local power = CreateFrame("StatusBar", nil, self)
	power:SetStatusBarTexture(Theme.Texture)
	power:SetStatusBarColor(unpack(Theme.Colors.power))
	power:SetPoint("BOTTOMLEFT", self, "BOTTOMLEFT", 2, 2)
	power:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", -2, 2)
	power:SetHeight(settings.powerHeight)
	Theme:CreateBarBackground(power)
	power.colorPower = true
	self.Power = power

	local health = CreateFrame("StatusBar", nil, self)
	health:SetStatusBarTexture(Theme.Texture)
	health:SetStatusBarColor(unpack(Theme.Colors.health))
	health:SetPoint("TOPLEFT", self, "TOPLEFT", 2, -2)
	health:SetPoint("TOPRIGHT", self, "TOPRIGHT", -2, -2)
	health:SetPoint("BOTTOM", power, "TOP", 0, 2)
	Theme:CreateBarBackground(health)
	health.colorClass = SlickUI.DB.unitFrames.classColors
	health.colorReaction = true
	health.colorHealth = not SlickUI.DB.unitFrames.classColors
	health.smoothing = Enum.StatusBarInterpolation.Immediate
	health.PostUpdateColor = UpdateUnitFrameHealthColor
	self.Health = health

	local name = Theme:CreateText(health, settings.width > 150 and 13 or 11)
	name:SetPoint("TOPLEFT", health, "TOPLEFT", 5, -4)
	name:SetPoint("RIGHT", health, "RIGHT", -48, 0)
	name:SetJustifyH("LEFT")
	self:Tag(name, "[name]")
	self.SlickNameText = name

	local healthPercent = Theme:CreateText(health, 11)
	healthPercent:SetPoint("TOPRIGHT", health, "TOPRIGHT", -5, -4)
	healthPercent:SetJustifyH("RIGHT")
	self:Tag(healthPercent, "[perhp<$%]")
	self.SlickHealthText = healthPercent

	local level = Theme:CreateText(health, 10)
	level:SetPoint("BOTTOMLEFT", health, "BOTTOMLEFT", 5, 3)
	level:SetTextColor(unpack(Theme.Colors.mutedText))
	self:Tag(level, "[smartlevel]")
	self.SlickLevelText = level

	local status = Theme:CreateText(health, 10)
	status:SetPoint("BOTTOMRIGHT", health, "BOTTOMRIGHT", -5, 3)
	status:SetTextColor(unpack(Theme.Colors.mutedText))
	self:Tag(status, "[status]")
	self.SlickStatusText = status

	name:Hide()
	healthPercent:Hide()
	level:Hide()
	status:Hide()
	CreateFrameDisplay(self, health, configKey)

	local raidTarget = health:CreateTexture(nil, "OVERLAY")
	raidTarget:SetSize(20, 20)
	raidTarget:SetPoint("CENTER", self, "TOP", 0, 2)
	self.RaidTargetIndicator = raidTarget

	if unit ~= "player" and unit ~= "pet" and configKey ~= "target" then
		local classification = Theme:CreateText(health, 9)
		classification:SetPoint("RIGHT", health, "LEFT", -6, 0)
		classification:SetJustifyH("RIGHT")
		classification:SetTextColor(unpack(Theme.Colors.goldText))
		self:Tag(classification, "[slickui:classification]")
		self.SlickClassificationBadge = classification
	end

	if unit == "player" then
		local combat = self:CreateTexture(nil, "OVERLAY", nil, 7)
		combat:SetSize(24, 24)
		combat:SetPoint("CENTER", self, "TOPLEFT", 1, -1)
		self.CombatIndicator = combat

		local pvp = self:CreateTexture(nil, "OVERLAY")
		pvp:SetSize(24, 24)
		pvp:SetPoint("LEFT", self, "RIGHT", 5, 0)
		self.PvPIndicator = pvp

		CreatePlayerClassResources(self)
	end

	if unit == "player" or unit == "target" or unit == "focus" then
		CreateCastbar(self, settings.width)
		if unit == "player" then AnchorPlayerCastbar(self) end
	end

	CreateAuraRow(self, "buffs")
	CreateAuraRow(self, "debuffs")
end

oUF:RegisterStyle("SlickUI", Style)

local function RegisterFrame(unit, globalName)
	local frame = oUF:Spawn(unit, globalName)
	SlickUI.UnitFrames[unit] = frame
	SlickUI.Movers:Register(frame, unit, Labels[unit])
	return frame
end

function SlickUI:ArrangeBossFrames()
	local anchor = self.UnitFrames.boss
	local settings = self.DB.unitFrames.frames.boss
	if not anchor or not settings then return false end
	local frameStep = settings.height * settings.scale + settings.spacing
	anchor:SetSize(settings.width * settings.scale, settings.height * settings.scale * 5 + settings.spacing * 4)
	for index, frame in ipairs(self.BossFrames) do
		frame:ClearAllPoints()
		frame:SetPoint("TOPLEFT", anchor, "TOPLEFT", 0, -(index - 1) * frameStep)
	end
	return true
end

function SlickUI:ApplyUnitFrameSettings(unit)
	if not self:IsAvailableOutOfCombat("Unit-frame changes") then
		return false
	end

	local frames = unit == "boss" and self.BossFrames or { self.UnitFrames[unit] }
	local settings = self.DB.unitFrames.frames[unit]
	if not settings or #frames == 0 then
		return false
	end

	for _, frame in ipairs(frames) do
		if frame then
			frame:SetSize(settings.width, settings.height)
			frame:SetScale(settings.scale)
			UpdateFrameDisplayWidths(frame)
			frame.Power:SetHeight(math.min(settings.powerHeight, settings.height - 8))
			if frame.SlickClassPower then
				frame.SlickClassPower:SetWidth(settings.width)
				LayoutResourceSegments(frame.SlickClassPower, frame.SlickClassPower.__max or #frame.SlickClassPower, settings.width)
			end
			if frame.SlickStagger then frame.SlickStagger:SetWidth(settings.width) end
			if frame.SlickRunes then
				frame.SlickRunes:SetWidth(settings.width)
				LayoutResourceSegments(frame.SlickRunes, #frame.SlickRunes, settings.width)
			end
			frame.Health.colorClass = self.DB.unitFrames.classColors
			frame.Health.colorReaction = true
			frame.Health.colorHealth = not self.DB.unitFrames.classColors
			frame.Health:ForceUpdate()

			frame.SlickNameText:Hide()
			frame.SlickHealthText:Hide()
			frame.SlickLevelText:Hide()
			frame.SlickStatusText:Hide()

			if frame.Castbar then
				frame.Castbar:SetWidth(settings.width)
				if unit == "player" then AnchorPlayerCastbar(frame) end
			end
			if settings.enabled then
				frame:Enable()
			else
				frame:Disable()
				frame:Hide()
			end
		end
	end

	self:ApplyAuraSettings(unit)
	if unit == "boss" then self:ArrangeBossFrames() end

	return true
end

function SlickUI:ApplyAuraSettings(unit)
	local frames = unit == "boss" and self.BossFrames or { self.UnitFrames[unit] }
	local settings = self.DB.unitFrames.auras[unit]
	if not settings or #frames == 0 then
		return false
	end

	for _, frame in ipairs(frames) do
		if frame then
			ConfigureAuraRow(frame, "buffs", settings.buffs, settings.debuffs)
			ConfigureAuraRow(frame, "debuffs", settings.debuffs, settings.buffs)
			frame:EnableElement("Auras")
			for auraType, auraSettings in pairs(settings) do
				local row = auraType == "buffs" and frame.Buffs or frame.Debuffs
				if auraSettings.enabled then
					row:Show()
					row.needFullUpdate = true
					row:ForceUpdate()
				else
					row:Hide()
				end
			end
		end
	end
	return true
end

function SlickUI:ApplyFrameDisplaySettings(unit, changedSlot)
	local frames = unit == "boss" and self.BossFrames or { self.UnitFrames[unit] }
	if #frames == 0 then
		return false
	end

	local display = self.DB.unitFrames.frameDisplays[unit]
	local fontSizes = self.DB.unitFrames.frameDisplayFontSizes[unit]
	for _, frame in ipairs(frames) do
		if frame and frame.SlickDisplaySlots then
			for slotKey, text in pairs(frame.SlickDisplaySlots) do
				if not changedSlot or changedSlot == slotKey then
					Theme:SetFont(text, fontSizes[slotKey] or 9)
					frame:Untag(text)
					text:SetText("")
					local selected = display[slotKey] or "none"
					local choice = PlayerDisplayChoiceByKey[selected]
					if choice and choice.tag then
						text:Show()
						frame:Tag(text, choice.tag)
						if text.UpdateTag then text:UpdateTag() end
					else
						text:Hide()
					end
				end
			end
			UpdateFrameDisplayWidths(frame)
		end
	end
	return true
end

function SlickUI:ApplyAllUnitFrameSettings()
	if not self:IsAvailableOutOfCombat("Unit-frame changes") then
		return false
	end

	for unit in pairs(self.DB.unitFrames.frames) do
		self:ApplyUnitFrameSettings(unit)
	end
	return true
end

function SlickUI:ArrangePersonalFrames()
	if not self:IsAvailableOutOfCombat("Frame arrangement") then
		return false
	end

	local config = self.DB.unitFrames
	local frames = config.frames
	local mainDistance = (frames.player.width * frames.player.scale + frames.target.width * frames.target.scale) / 2 + config.personalSpacing
	local smallDistance = (frames.pet.width * frames.pet.scale + frames.targettarget.width * frames.targettarget.scale) / 2 + config.personalSpacing
	local mainY = -205
	local secondY = mainY - math.max(frames.player.height * frames.player.scale, frames.target.height * frames.target.scale) / 2 - config.rowSpacing

	self.DB.positions.player = { x = -mainDistance / 2, y = mainY }
	self.DB.positions.target = { x = mainDistance / 2, y = mainY }
	self.DB.positions.pet = { x = -smallDistance / 2, y = secondY }
	self.DB.positions.targettarget = { x = smallDistance / 2, y = secondY }
	self.DB.positions.focus = { x = 0, y = secondY - frames.focus.height * frames.focus.scale - config.rowSpacing }

	for unit, frame in pairs(self.UnitFrames) do
		local position = self.DB.positions[unit]
		if position then
			frame:ClearAllPoints()
			frame:SetPoint("CENTER", UIParent, "CENTER", position.x, position.y)
		end
	end

	self:Print("Personal unit frames arranged with even spacing.")
	return true
end

function SlickUI:SetCastbarsEnabled(enabled)
	if not self:IsAvailableOutOfCombat("Cast-bar changes") then
		return
	end

	self.DB.showCastbars = enabled and true or false
	for _, frame in pairs(self.UnitFrames) do
		if frame.Castbar then
			if self.DB.showCastbars then
				frame:EnableElement("Castbar")
				frame.Castbar:ForceUpdate()
			else
				frame:DisableElement("Castbar")
				frame.Castbar:Hide()
			end
		end
	end
end

local function SafeDiagnosticValue(value, fallback)
	if canaccessvalue and not canaccessvalue(value) then return fallback end
	if issecretvalue and issecretvalue(value) then return fallback end
	if value == nil or value == "" then return fallback end
	return tostring(value)
end

SLASH_SLICKUIBUFFS1 = "/slickbuffs"
SlashCmdList.SLICKUIBUFFS = function()
	if select(4, GetBuildInfo()) >= 120100 then
		SlickUI:Print("Buff diagnostics are unavailable on WoW 12.1 because aura enumeration is protected.")
		return
	end
	if InCombatLockdown() then
		SlickUI:Print("Buff diagnostics are unavailable during combat because aura slots may be protected.")
		return
	end
	local settings = SlickUI.DB.unitFrames.auras.player.buffs
	local diagnosticElement = { includeRecognizedConsumables = settings.includeConsumables == true }
	local slots = { C_UnitAuras.GetAuraSlots("player", "HELPFUL") }
	local shown = 0
	SlickUI:Print("Buffs currently accepted by the known-spell filter:")
	for index = 2, #slots do
		local data = C_UnitAuras.GetAuraDataBySlot("player", slots[index])
		if data and FilterKnownSpellAndConsumableBuffs(diagnosticElement, "player", data) then
			shown = shown + 1
			local name = SafeDiagnosticValue(data.name, "protected name")
			local spellID = SafeDiagnosticValue(data.spellId, "protected ID")
			local source = SafeDiagnosticValue(data.sourceUnit, "no source")
			SlickUI:Print(string.format("%d. %s — spell %s — source %s", shown, name, spellID, source))
		end
	end
	if shown == 0 then
		SlickUI:Print("None found.")
	end
end

oUF:Factory(function(self)
	self:SetActiveStyle("SlickUI")
	RegisterFrame("player", "SlickUI_Player")
	RegisterFrame("target", "SlickUI_Target")
	RegisterFrame("focus", "SlickUI_Focus")
	RegisterFrame("pet", "SlickUI_Pet")
	RegisterFrame("targettarget", "SlickUI_TargetTarget")

	local bossAnchor = CreateFrame("Frame", "SlickUI_BossDock", UIParent)
	bossAnchor.SlickMoverOnly = true
	SlickUI.UnitFrames.boss = bossAnchor
	SlickUI.Movers:Register(bossAnchor, "boss", Labels.boss)
	for index = 1, 5 do
		local frame = oUF:Spawn("boss" .. index, "SlickUI_Boss" .. index)
		SlickUI.BossFrames[index] = frame
	end
	SlickUI:ArrangeBossFrames()

	SlickUI:ApplyAllUnitFrameSettings()
	SlickUI:SetCastbarsEnabled(SlickUI.DB.showCastbars)
	SlickUI.Movers:SetLocked(SlickUI.DB.locked)
end)
