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

local Nameplates = {
	Frames = setmetatable({}, { __mode = "k" }),
}
SlickUI.Nameplates = Nameplates

local DisplaySlots = {
	{ key = "left", point = "LEFT", x = 4, justify = "LEFT" },
	{ key = "center", point = "CENTER", x = 0, justify = "CENTER" },
	{ key = "right", point = "RIGHT", x = -4, justify = "RIGHT" },
}

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 IsAttackableUnit(unit)
	if type(unit) ~= "string" or type(UnitCanAttack) ~= "function" then return false end
	local ok, attackable = pcall(UnitCanAttack, "player", unit)
	return ok and IsReadable(attackable) and attackable == true
end

local function ReadUnitBoolean(func, ...)
	if type(func) ~= "function" then return nil end
	local ok, value = pcall(func, ...)
	if not ok or not IsReadable(value) then return nil end
	return value == true
end

local function IsFriendlyPlayer(unit)
	return type(unit) == "string"
		and ReadUnitBoolean(UnitIsFriend, "player", unit) == true
		and ReadUnitBoolean(UnitIsPlayer, unit) == true
end

local function IsPlayerUnit(unit)
	return type(unit) == "string" and ReadUnitBoolean(UnitIsUnit, unit, "player") == true
end

local function ShouldShowUnit(config, unit)
	if not config.enabled then return false end
	if type(unit) ~= "string" then return true end
	-- WoW exposes the player "nameplate" as the Personal Resource Display,
	-- not as a normal overhead plate. SlickUI intentionally leaves it off.
	if IsPlayerUnit(unit) then return false end
	if ReadUnitBoolean(UnitCanAttack, "player", unit) == true then
		return config.showEnemies == true
	end
	if ReadUnitBoolean(UnitIsFriend, "player", unit) == true then
		local isPlayer = ReadUnitBoolean(UnitIsPlayer, unit)
		if isPlayer == true then
			return config.showFriendlyPlayers == true
		elseif isPlayer == false then
			local playerControlled = ReadUnitBoolean(UnitPlayerControlled, unit)
			if playerControlled == true then
				return config.showFriendlyPets == true
			end
			return config.showFriendlyNPCs == true
		end
		-- WoW 12.1 can temporarily protect the player/NPC classification on
		-- friendly nameplates. Do not incorrectly classify an unreadable
		-- friendly player as an NPC and hide it when NPC plates are disabled.
		return config.showFriendlyPlayers == true or config.showFriendlyNPCs == true or config.showFriendlyPets == true
	end
	-- Neutral attackable units can transiently lack a readable reaction while
	-- their plate is being initialized. Treat them as enemies rather than as a
	-- friendly unit so the friendly toggles never reveal friendly plates.
	return config.showEnemies == true
end

local function ShouldShowNameplateFrame(config, unit)
	if IsFriendlyPlayer(unit) then return config.enabled and config.blizzardFriendlyMode ~= "off" end
	return config.enabled and ShouldShowUnit(config, unit)
end

function Nameplates:ApplyBlizzardFriendlyMode()
	if not SlickUI:IsAvailableOutOfCombat("Friendly nameplate changes") then return false end
	local config = SlickUI.DB.nameplates
	if config.blizzardFriendlyModeInitialized ~= true then
		config.blizzardFriendlyMode = config.showFriendlyPlayers and "bars" or "names"
		config.blizzardFriendlyModeInitialized = true
	end
	local mode = config.blizzardFriendlyMode
	if mode ~= "off" and mode ~= "bars" and mode ~= "names" then mode = "names" end
	config.blizzardFriendlyMode = mode
	config.showFriendlyPlayers = mode == "bars"
	if not C_CVar or type(C_CVar.SetCVar) ~= "function" then return false end
	local shown = mode ~= "off" and 1 or 0
	pcall(C_CVar.SetCVar, "nameplateShowFriends", shown)
	pcall(C_CVar.SetCVar, "nameplateShowFriendlyPlayers", shown)
	pcall(C_CVar.SetCVar, "nameplateShowOnlyNameForFriendlyPlayerUnits", mode == "names" and 1 or 0)
	pcall(C_CVar.SetCVar, "UnitNameFriendlyPlayerName", shown)
	return true
end

function Nameplates:GetFriendlyMode()
	local config = SlickUI.DB and SlickUI.DB.nameplates
	local mode = config and config.blizzardFriendlyMode or "names"
	return (mode == "off" or mode == "bars" or mode == "names") and mode or "names"
end

function Nameplates:SetFriendlyMode(mode)
	if mode ~= "off" and mode ~= "names" and mode ~= "bars" then return false end
	if not SlickUI:IsAvailableOutOfCombat("Friendly nameplate changes") then return false end
	local config = SlickUI.DB.nameplates
	config.blizzardFriendlyMode = mode
	config.blizzardFriendlyModeInitialized = true
	config.showFriendlyPlayers = mode == "bars"
	local applied = self:ApplySettings()
	if SlickUI.InformationBars then SlickUI.InformationBars:UpdateBlocks() end
	if SlickUI.Options then SlickUI.Options:RefreshNameplateControls() end
	return applied
end

function Nameplates:CycleFriendlyMode()
	local current = self:GetFriendlyMode()
	return self:SetFriendlyMode(current == "off" and "names" or (current == "names" and "bars" or "off"))
end

local function UpdatePlayerNameColor(frame)
	if not frame or not frame.SlickName or type(frame.unit) ~= "string" then return end
	local config = SlickUI.DB.nameplates
	local isPlayer = ReadUnitBoolean(UnitIsPlayer, frame.unit) == true
	local isFriendly = ReadUnitBoolean(UnitIsFriend, "player", frame.unit) == true
	local useClassColor = isPlayer and config.classColorPlayerNames == true
	if useClassColor then
		local _, classToken = UnitClass(frame.unit)
		local color = IsReadable(classToken) and type(classToken) == "string"
			and RAID_CLASS_COLORS and RAID_CLASS_COLORS[classToken]
		if color then
			frame.SlickName:SetTextColor(color.r, color.g, color.b, 1)
			return
		end
	end
	if isFriendly then
		frame.SlickName:SetTextColor(0.20, 1.00, 0.20, 1)
	else
		local reaction = UnitReaction(frame.unit, "player")
		if IsReadable(reaction) and type(reaction) == "number" then
			if reaction <= 3 then
				frame.SlickName:SetTextColor(unpack(Theme.Colors.hostile))
			elseif reaction == 4 then
				local inCombat = UnitAffectingCombat(frame.unit)
				if IsReadable(inCombat) and inCombat == true then
					frame.SlickName:SetTextColor(unpack(Theme.Colors.hostile))
				else
					frame.SlickName:SetTextColor(unpack(Theme.Colors.neutral))
				end
			else
				frame.SlickName:SetTextColor(unpack(Theme.Colors.text))
			end
		elseif IsAttackableUnit(frame.unit) then
			frame.SlickName:SetTextColor(unpack(Theme.Colors.hostile))
		else
			frame.SlickName:SetTextColor(unpack(Theme.Colors.text))
		end
	end
end

local function CreateBorder(parent, thickness, color)
	local border = CreateFrame("Frame", nil, parent)
	border:SetAllPoints(parent)
	border:SetFrameLevel(parent:GetFrameLevel() + 4)
	border.Edges = {}
	local function Edge(pointA, pointB, width, height)
		local texture = border:CreateTexture(nil, "OVERLAY")
		texture:SetColorTexture(unpack(color))
		texture:SetPoint(pointA)
		texture:SetPoint(pointB)
		if width then texture:SetWidth(width) end
		if height then texture:SetHeight(height) end
		border.Edges[#border.Edges + 1] = texture
	end
	Edge("TOPLEFT", "TOPRIGHT", nil, thickness)
	Edge("BOTTOMLEFT", "BOTTOMRIGHT", nil, thickness)
	Edge("TOPLEFT", "BOTTOMLEFT", thickness, nil)
	Edge("TOPRIGHT", "BOTTOMRIGHT", thickness, nil)
	return border
end

local function SetBorderColor(border, color)
	for _, edge in ipairs(border.Edges) do
		edge:SetColorTexture(unpack(color))
	end
end

local ThreatColors = {
	[1] = { 1.00, 0.82, 0.20, 1.00 },
	[2] = { 1.00, 0.45, 0.10, 1.00 },
	[3] = { 1.00, 0.12, 0.10, 1.00 },
}

local function UpdateNameplateHealthColor(health, unit)
	local tapDenied = UnitIsTapDenied(unit)
	if IsReadable(tapDenied) and tapDenied == true then return end
	local reaction = UnitReaction(unit, "player")
	if not IsReadable(reaction) or type(reaction) ~= "number" then return end
	if reaction <= 3 then
		health:SetStatusBarColor(unpack(Theme.Colors.hostile))
	elseif reaction == 4 then
		local inCombat = UnitAffectingCombat(unit)
		if IsReadable(inCombat) and inCombat == true then
			health:SetStatusBarColor(unpack(Theme.Colors.hostile))
		else
			health:SetStatusBarColor(unpack(Theme.Colors.neutral))
		end
	end
end

local function SafeThreatUpdate(self, _, unit)
	local indicator = self.ThreatIndicator
	if unit ~= self.unit or not SlickUI.DB.nameplates.showThreat then
		indicator:Hide()
		return
	end

	local status = UnitThreatSituation("player", unit)
	if not IsReadable(status) or type(status) ~= "number" or status <= 0 then
		indicator:Hide()
		return
	end

	SetBorderColor(indicator, ThreatColors[status] or ThreatColors[1])
	indicator:Show()
end

local function SafeQuestUpdate(self, _, unit)
	local indicator = self.QuestIndicator
	if unit ~= self.unit or not SlickUI.DB.nameplates.showQuest then
		indicator:Hide()
		return
	end

	local isQuest = UnitIsQuestBoss(unit)
	indicator:SetShown(IsReadable(isQuest) and isQuest == true)
end

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

local function CreateCastbar(self, content)
	local castbar = CreateFrame("StatusBar", nil, content)
	castbar.SlickOwner = self
	castbar:SetStatusBarTexture(Theme.Texture)
	castbar:SetPoint("TOP", self.Health, "BOTTOM", 0, -4)
	Theme:CreatePanel(castbar)
	Theme:CreateBarBackground(castbar)

	local icon = castbar:CreateTexture(nil, "ARTWORK")
	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, 10)
	text:SetPoint("LEFT", 4, 0)
	text:SetPoint("RIGHT", -35, 0)
	text:SetJustifyH("LEFT")
	castbar.Text = text

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

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

	castbar.PostCastStart = function(element)
		element:SetStatusBarColor(unpack(Theme.Colors.cast))
	end
	castbar.PostCastInterruptible = UpdateCastbarColor
	self.Castbar = castbar
end

local function Style(self)
	local config = SlickUI.DB.nameplates
	self:SetFrameStrata("LOW")

	local content = CreateFrame("Frame", nil, self)
	content:SetPoint("CENTER")
	self.SlickContent = content

	local health = CreateFrame("StatusBar", nil, content)
	health:SetStatusBarTexture(Theme.Texture)
	health:SetPoint("CENTER")
	health.colorTapping = true
	health.colorDisconnected = true
	health.colorClass = true
	health.colorReaction = true
	health.colorHealth = true
	health.smoothing = Enum.StatusBarInterpolation.Immediate
	health.PostUpdateColor = UpdateNameplateHealthColor
	Theme:CreatePanel(health)
	Theme:CreateBarBackground(health)

	-- Display damage-absorb shields as an extension of current health. The oUF
	-- Health element feeds these bars through Blizzard's prediction calculator,
	-- which remains usable when combat aura details are protected/secret.
	local damageAbsorb = CreateFrame("StatusBar", nil, health)
	damageAbsorb:SetStatusBarTexture(Theme.Texture)
	damageAbsorb:SetStatusBarColor(0.18, 0.72, 1.00, 0.92)
	damageAbsorb:SetPoint("TOP")
	damageAbsorb:SetPoint("BOTTOM")
	damageAbsorb:SetPoint("LEFT", health:GetStatusBarTexture(), "RIGHT")
	damageAbsorb:SetWidth(config.width)
	damageAbsorb:SetFrameLevel(health:GetFrameLevel() + 2)
	health.DamageAbsorb = damageAbsorb

	local overAbsorb = health:CreateTexture(nil, "OVERLAY")
	overAbsorb:SetTexture("Interface\\RaidFrame\\Shield-Overshield")
	overAbsorb:SetBlendMode("ADD")
	overAbsorb:SetPoint("TOP")
	overAbsorb:SetPoint("BOTTOM")
	overAbsorb:SetPoint("RIGHT", health, "RIGHT", 3, 0)
	overAbsorb:SetWidth(12)
	health.OverDamageAbsorbIndicator = overAbsorb
	self.Health = health

	local name = Theme:CreateText(content, config.fontSize)
	name:SetPoint("BOTTOMLEFT", health, "TOPLEFT", 1, 3)
	name:SetPoint("RIGHT", health, "RIGHT", -28, 0)
	name:SetJustifyH("LEFT")
	name:SetWordWrap(false)
	self:Tag(name, "[name]")
	self.SlickName = name

	local level = Theme:CreateText(content, config.fontSize - 1)
	level:SetPoint("BOTTOMRIGHT", health, "TOPRIGHT", -1, 3)
	level:SetJustifyH("RIGHT")
	level:SetTextColor(unpack(Theme.Colors.goldText))
	self:Tag(level, "[smartlevel]")
	self.SlickLevel = level

	self.SlickDisplaySlots = {}
	for _, slot in ipairs(DisplaySlots) do
		local text = Theme:CreateText(health, math.max(8, config.fontSize - 1))
		text:SetPoint(slot.point, health, slot.point, slot.x, 0)
		text:SetJustifyH(slot.justify)
		text:SetWordWrap(false)
		self.SlickDisplaySlots[slot.key] = text
	end

	local target = CreateBorder(health, 2, Theme.Colors.border)
	target:Hide()
	self.SlickTargetIndicator = target

	local threat = CreateBorder(health, 2, ThreatColors[1])
	threat.feedbackUnit = "player"
	threat.Override = SafeThreatUpdate
	threat:Hide()
	self.ThreatIndicator = threat

	local quest = health:CreateTexture(nil, "OVERLAY")
	quest:SetTexture("Interface\\GossipFrame\\AvailableQuestIcon")
	quest:SetSize(18, 18)
	quest:SetPoint("LEFT", health, "RIGHT", 5, 0)
	quest.Override = SafeQuestUpdate
	self.QuestIndicator = quest

	local raidTarget = health:CreateTexture(nil, "OVERLAY")
	raidTarget:SetSize(36, 36)
	raidTarget:SetPoint("BOTTOM", health, "TOP", 0, 10)
	self.RaidTargetIndicator = raidTarget

	CreateCastbar(self, content)
	Nameplates.Frames[self] = true
end

oUF:RegisterStyle("SlickUINameplate", Style)

function Nameplates:UpdateTarget()
	local config = SlickUI.DB.nameplates
	for frame in pairs(self.Frames) do
		frame.SlickTargetIndicator:Hide()
		frame.SlickContent:SetScale(config.scale)
	end
	local plate = C_NamePlate.GetNamePlateForUnit("target")
	if plate and plate.unitFrame and self.Frames[plate.unitFrame] then
		local frame = plate.unitFrame
		if config.enlargeTarget then
			frame.SlickContent:SetScale(config.scale * config.targetScale)
		end
		if config.showTargetHighlight then
			frame.SlickTargetIndicator:Show()
		end
	end
end

function Nameplates:RefreshReactionColors(unit)
	for frame in pairs(self.Frames) do
		if (not unit or frame.unit == unit) and frame.Health and frame.unit then
			UpdateNameplateHealthColor(frame.Health, frame.unit)
		end
	end
end

function Nameplates:ApplyFrame(frame)
	local config = SlickUI.DB.nameplates
	local playerUnit = IsPlayerUnit(frame.unit)
	local nameOnly = IsFriendlyPlayer(frame.unit) and not playerUnit and config.showFriendlyPlayers ~= true
	frame.SlickContent:SetScale(config.scale)
	frame.SlickContent:SetSize(config.width, config.height)
	frame.Health:SetSize(config.width, config.height)
	if frame.Health.DamageAbsorb then frame.Health.DamageAbsorb:SetWidth(config.width) end
	Theme:SetFont(frame.SlickName, config.fontSize)
	Theme:SetFont(frame.SlickLevel, math.max(8, config.fontSize - 1))
	self:ApplyDisplaySettings(frame)
	frame.Health:SetAlpha(nameOnly and 0 or 1)
	if frame.Health.DamageAbsorb then frame.Health.DamageAbsorb:SetAlpha(nameOnly and 0 or 0.92) end
	if frame.Health.OverDamageAbsorbIndicator then frame.Health.OverDamageAbsorbIndicator:SetAlpha(nameOnly and 0 or 1) end
	frame.SlickName:ClearAllPoints()
	if nameOnly then
		frame.SlickName:SetPoint("CENTER", frame.SlickContent, "CENTER", 0, 0)
		frame.SlickName:SetWidth(math.max(120, config.width))
		frame.SlickName:SetJustifyH("CENTER")
		Theme:SetFont(frame.SlickName, math.max(9, config.fontSize - 1))
	else
		frame.SlickName:SetPoint("BOTTOMLEFT", frame.Health, "TOPLEFT", 1, 3)
		frame.SlickName:SetPoint("RIGHT", frame.Health, "RIGHT", -28, 0)
		frame.SlickName:SetJustifyH("LEFT")
	end
	frame.SlickName:SetShown(config.enabled)
	UpdatePlayerNameColor(frame)
	frame.SlickLevel:SetShown(not nameOnly)
	for _, text in pairs(frame.SlickDisplaySlots) do
		if nameOnly then text:Hide() end
	end

	frame.Castbar:SetSize(config.width, config.castbarHeight)
	frame.Castbar.Icon:SetSize(config.castbarHeight, config.castbarHeight)
	frame.Castbar.Shield:SetSize(config.castbarHeight + 8, config.castbarHeight + 12)
	if config.showCastbar and not nameOnly then
		frame:EnableElement("Castbar")
	else
		frame:DisableElement("Castbar")
		frame.Castbar:Hide()
	end
	if config.showThreat and not nameOnly then
		frame:EnableElement("ThreatIndicator")
		frame.ThreatIndicator:ForceUpdate()
	else
		frame:DisableElement("ThreatIndicator")
	end
	if config.showQuest and not nameOnly then
		frame:EnableElement("QuestIndicator")
		frame.QuestIndicator:ForceUpdate()
	else
		frame:DisableElement("QuestIndicator")
	end
	frame:SetAlpha(ShouldShowNameplateFrame(config, frame.unit) and 1 or 0)
end

function Nameplates:ApplyDisplaySettings(frame, changedSlot)
	if not frame or not frame.SlickDisplaySlots then return false end
	local config = SlickUI.DB.nameplates
	config.display = config.display or {
		left = "classification",
		center = "none",
		right = "healthPercent",
	}
	local width = math.max(24, config.width / 3 - 7)
	for slotKey, text in pairs(frame.SlickDisplaySlots) do
		if not changedSlot or changedSlot == slotKey then
			Theme:SetFont(text, math.max(8, config.fontSize - 1))
			text:SetWidth(width)
			frame:Untag(text)
			text:SetText("")
			local selected = config.display[slotKey] or "none"
			local choice = SlickUI.PlayerDisplayChoiceByKey[selected]
			if choice and choice.tag then
				if selected == "classification" then
					text:SetTextColor(unpack(Theme.Colors.goldText))
				else
					text:SetTextColor(unpack(Theme.Colors.text))
				end
				text:Show()
				frame:Tag(text, choice.tag)
				if text.UpdateTag then text:UpdateTag() end
			else
				text:Hide()
			end
		end
	end
	return true
end

function Nameplates:ApplySettings()
	if not SlickUI:IsAvailableOutOfCombat("Nameplate changes") then return false end
	local config = SlickUI.DB.nameplates
	if config.blizzardFriendlyModeInitialized ~= true then
		config.blizzardFriendlyMode = config.showFriendlyPlayers and "bars" or "names"
		config.blizzardFriendlyModeInitialized = true
	end
	if not config.enabled then
		-- Do not use disabled mode as another visibility preset. Blizzard owns all
		-- nameplate CVars again once SlickUI nameplates are disabled.
		if self.Driver then self.Driver.slickEnabled = false end
		for frame in pairs(self.Frames) do frame:SetAlpha(0) end
		self:HidePreview()
		self:ApplyBlizzardFriendlyMode()
		return true
	end
	local friendlyMode = config.blizzardFriendlyMode
	if friendlyMode ~= "off" and friendlyMode ~= "bars" and friendlyMode ~= "names" then friendlyMode = "names" end
	config.blizzardFriendlyMode = friendlyMode
	config.showFriendlyPlayers = friendlyMode == "bars"
	self:EnsureDriver()
	if self.Driver then
		self.Driver.slickEnabled = true
		local maximumScale = config.scale * (config.enlargeTarget and config.targetScale or 1)
		self.Driver:SetSize(config.width * maximumScale, (config.height + config.castbarHeight + 28) * maximumScale)
		self.Driver:SetCVars({
			nameplateShowAll = 1,
			nameplateShowSelf = 0,
			nameplateShowEnemies = config.showEnemies and 1 or 0,
			-- Friendly player plate objects must exist for SlickUI to retain the
			-- overhead name when their health bars are disabled. ApplyFrame turns
			-- those objects into name-only plates.
			nameplateShowFriends = friendlyMode ~= "off" and 1 or 0,
			-- WoW 12.0 split friendly players from the legacy friendly master
			-- switch. Both must be enabled for friendly plate objects to exist.
			nameplateShowFriendlyPlayers = friendlyMode ~= "off" and 1 or 0,
			nameplateShowFriendlyNPCs = config.showFriendlyNPCs and 1 or 0,
			nameplateShowFriendlyPlayerPets = config.showFriendlyPets and 1 or 0,
			-- SlickUI controls its own name-only presentation. Blizzard's new
			-- simplified/name-only modes suppress the health-bar regions before
			-- our style can display them.
			nameplateShowOnlyNameForFriendlyPlayerUnits = 0,
			nameplateSimplifiedTypes = 0,
			nameplateMotion = config.stackNameplates and 1 or 0,
			nameplateOverlapH = config.horizontalSpacing,
			nameplateOverlapV = config.verticalSpacing,
			nameplateSelectedAlpha = 1,
			nameplateMinAlpha = 0.72,
			nameplateMaxAlpha = 1,
			nameplateSelectedScale = 1,
			nameplateMinScale = 1,
			nameplateMaxScale = 1,
		})
	end
	-- Friendly player names are a separate Blizzard display from friendly
	-- health plates. Keep overhead names readable when the user disables
	-- SlickUI friendly plates instead of making friendly players anonymous.
	if config.enabled and C_CVar and C_CVar.SetCVar then
		pcall(C_CVar.SetCVar, "UnitNameFriendlyPlayerName", friendlyMode ~= "off" and 1 or 0)
	end
	for frame in pairs(self.Frames) do self:ApplyFrame(frame) end
	self:UpdateTarget()
	if self.PreviewVisible then self:ShowPreview() end
	return true
end

function Nameplates:VisibilityCVarsMatch()
	if not C_CVar or type(C_CVar.GetCVar) ~= "function" then return true end
	local config = SlickUI.DB.nameplates
	if not config.enabled then return true end
	local friendlyMode = config.blizzardFriendlyMode or "names"
	local expectedFriends = friendlyMode ~= "off" and "1" or "0"
	local expectedFriendlyPlayers = expectedFriends
	local expectedNPCs = config.showFriendlyNPCs and "1" or "0"
	local expectedPets = config.showFriendlyPets and "1" or "0"
	local expectedSelf = "0"
	local okFriends, friends = pcall(C_CVar.GetCVar, "nameplateShowFriends")
	local okFriendlyPlayers, friendlyPlayers = pcall(C_CVar.GetCVar, "nameplateShowFriendlyPlayers")
	local okNPCs, npcs = pcall(C_CVar.GetCVar, "nameplateShowFriendlyNPCs")
	local okPets, pets = pcall(C_CVar.GetCVar, "nameplateShowFriendlyPlayerPets")
	local okSelf, selfPlate = pcall(C_CVar.GetCVar, "nameplateShowSelf")
	local okNames, names = pcall(C_CVar.GetCVar, "UnitNameFriendlyPlayerName")
	local namesMatch = not config.enabled or (okNames and tostring(names) == expectedFriends)
	local okOnlyNames, onlyNames = pcall(C_CVar.GetCVar, "nameplateShowOnlyNameForFriendlyPlayerUnits")
	local okSimplified, simplified = pcall(C_CVar.GetCVar, "nameplateSimplifiedTypes")
	return okFriends and okFriendlyPlayers and okNPCs and okPets and okSelf and okOnlyNames and okSimplified and namesMatch
		and tostring(friends) == expectedFriends and tostring(friendlyPlayers) == expectedFriendlyPlayers
		and tostring(npcs) == expectedNPCs and tostring(pets) == expectedPets and tostring(selfPlate) == expectedSelf
		and tostring(onlyNames) == "0" and tostring(simplified) == "0"
end

function Nameplates:QueueVisibilityCVarSync()
	if not SlickUI.DB.nameplates.enabled then return end
	if self.CVarSyncPending then return end
	self.CVarSyncPending = true
	C_Timer.After(0.05, function()
		Nameplates.CVarSyncPending = false
		if not InCombatLockdown() and not Nameplates:VisibilityCVarsMatch() then
			Nameplates:ApplySettings()
		end
	end)
end

function Nameplates:CreatePreview()
	if self.Preview then return end
	local root = CreateFrame("Frame", "SlickUINameplatePreview", UIParent)
	root:SetPoint("CENTER", UIParent, "CENTER", 0, 105)
	root:SetSize(300, 180)
	root:SetFrameStrata("DIALOG")
	root:Hide()
	root.Plates = {}
	local samples = {
		{ name = "Hostile Target", color = Theme.Colors.hostile, health = 0.72, level = 90, classification = "", cast = "Interruptible Cast" },
		{ name = "Friendly Player", color = { 0.12, 0.62, 0.34, 1 }, health = 0.88, level = 90, classification = "" },
		{ name = "Rare Quest Enemy", color = { 0.52, 0.22, 0.72, 1 }, health = 0.54, level = 90, classification = "|cff48b8ffRare|r", cast = "Protected Cast" },
	}
	for index, sample in ipairs(samples) do
		local plate = CreateFrame("Frame", nil, root)
		local health = CreateFrame("StatusBar", nil, plate)
		health:SetPoint("TOP")
		health:SetStatusBarTexture(Theme.Texture)
		health:SetStatusBarColor(unpack(sample.color))
		health:SetMinMaxValues(0, 1)
		health:SetValue(sample.health)
		Theme:CreatePanel(health)
		Theme:CreateBarBackground(health)
		local name = Theme:CreateText(plate, 11)
		name:SetPoint("BOTTOMLEFT", health, "TOPLEFT", 1, 3)
		name:SetText(sample.name)
		local displaySlots = {}
		for _, slot in ipairs(DisplaySlots) do
			local text = Theme:CreateText(health, 10)
			text:SetPoint(slot.point, health, slot.point, slot.x, 0)
			text:SetJustifyH(slot.justify)
			displaySlots[slot.key] = text
		end
		local castbar = CreateFrame("StatusBar", nil, plate)
		castbar:SetPoint("TOP", health, "BOTTOM", 0, -4)
		castbar:SetStatusBarTexture(Theme.Texture)
		castbar:SetStatusBarColor(unpack(index == 3 and Theme.Colors.castProtected or Theme.Colors.castInterruptible))
		castbar:SetMinMaxValues(0, 1)
		castbar:SetValue(0.63)
		Theme:CreatePanel(castbar)
		local castText = Theme:CreateText(castbar, 9)
		castText:SetPoint("LEFT", 4, 0)
		castText:SetText(sample.cast or "")
		plate.Health, plate.Name, plate.DisplaySlots, plate.Castbar, plate.Sample = health, name, displaySlots, castbar, sample
		root.Plates[index] = plate
	end
	self.Preview = root
end

function Nameplates:ShowPreview()
	self:CreatePreview()
	local config = SlickUI.DB.nameplates
	for index, plate in ipairs(self.Preview.Plates) do
		plate:SetSize(config.width, config.height + config.castbarHeight + 28)
		plate:SetScale(config.scale * (index == 1 and config.enlargeTarget and config.targetScale or 1))
		plate:ClearAllPoints()
		plate:SetPoint("TOP", self.Preview, "TOP", 0, -(index - 1) * 55)
		plate.Health:SetSize(config.width, config.height)
		Theme:SetFont(plate.Name, config.fontSize)
		local slotWidth = math.max(24, config.width / 3 - 7)
		for slotKey, text in pairs(plate.DisplaySlots) do
			local selected = (config.display or {})[slotKey] or "none"
			local values = {
				name = plate.Sample.name,
				level = tostring(plate.Sample.level),
				classification = plate.Sample.classification,
				status = "",
				healthCurrent = tostring(math.floor(100000 * plate.Sample.health)),
				healthValues = tostring(math.floor(100000 * plate.Sample.health)) .. "/100000",
				healthPercent = math.floor(plate.Sample.health * 100) .. "%",
				healthMissing = "-" .. tostring(math.floor(100000 * (1 - plate.Sample.health))),
				powerCurrent = "100",
				powerValues = "100/100",
				powerPercent = "100%",
			}
			Theme:SetFont(text, math.max(8, config.fontSize - 1))
			text:SetWidth(slotWidth)
			text:SetText(values[selected] or "")
			text:SetTextColor(unpack(Theme.Colors.text))
		end
		plate.Castbar:SetSize(config.width, config.castbarHeight)
		plate.Castbar:SetShown(config.showCastbar and plate.Sample.cast ~= nil)
	end
	self.PreviewVisible = true
	self.Preview:Show()
end

function Nameplates:HidePreview()
	self.PreviewVisible = false
	if self.Preview then self.Preview:Hide() end
end

function Nameplates:TogglePreview()
	if self.PreviewVisible then self:HidePreview() else self:ShowPreview() end
end

function Nameplates:EnsureDriver()
	if self.Driver then return self.Driver end
	oUF:SetActiveStyle("SlickUINameplate")
	self.Driver = oUF:SpawnNamePlates("SlickUI_NamePlate")
	self.Driver.slickEnabled = true
	self.Driver:SetAddedCallback(function(frame)
		Nameplates:ApplyFrame(frame)
		Nameplates:UpdateTarget()
	end)
	self.Driver:SetRemovedCallback(function(frame)
		if frame.SlickTargetIndicator then frame.SlickTargetIndicator:Hide() end
	end)
	return self.Driver
end

function Nameplates:Initialize()
	-- Starting with SlickUI nameplates disabled must leave Blizzard's native
	-- nameplate frames and their events untouched. The driver is created lazily
	-- if the user enables SlickUI nameplates later.
	if SlickUI.DB.nameplates.enabled then self:EnsureDriver() end

	local events = CreateFrame("Frame")
	events:RegisterEvent("PLAYER_TARGET_CHANGED")
	events:RegisterEvent("PLAYER_ENTERING_WORLD")
	events:RegisterEvent("PLAYER_REGEN_DISABLED")
	events:RegisterEvent("PLAYER_REGEN_ENABLED")
	events:RegisterEvent("UNIT_FLAGS")
	events:RegisterEvent("UNIT_FACTION")
	events:RegisterEvent("UNIT_NAME_UPDATE")
	events:RegisterEvent("UNIT_COMBAT")
	events:RegisterEvent("UNIT_THREAT_SITUATION_UPDATE")
	events:RegisterEvent("CVAR_UPDATE")
	events:SetScript("OnEvent", function(_, event, unit)
		if event == "PLAYER_TARGET_CHANGED" then
			Nameplates:UpdateTarget()
		elseif event == "CVAR_UPDATE" then
			if SlickUI.DB.nameplates.enabled and IsReadable(unit) and type(unit) == "string" then
				local cvar = string.lower(unit)
				if cvar == "nameplateshowfriends" or cvar == "nameplateshowfriendlyplayers"
					or cvar == "nameplateshowfriendlynpcs" or cvar == "nameplateshowfriendlyplayerpets" or cvar == "nameplateshowself"
					or cvar == "nameplateshowonlynameforfriendlyplayerunits" or cvar == "nameplatesimplifiedtypes" then
					Nameplates:QueueVisibilityCVarSync()
				end
			end
		elseif event == "UNIT_FLAGS" or event == "UNIT_FACTION" or event == "UNIT_NAME_UPDATE" or event == "UNIT_COMBAT" or event == "UNIT_THREAT_SITUATION_UPDATE" then
			Nameplates:RefreshReactionColors(unit)
			for frame in pairs(Nameplates.Frames) do
				if (not unit or frame.unit == unit) and frame.unit then
					UpdatePlayerNameColor(frame)
					frame:SetAlpha(ShouldShowNameplateFrame(SlickUI.DB.nameplates, frame.unit) and 1 or 0)
				end
			end
		elseif event == "PLAYER_REGEN_DISABLED" or event == "PLAYER_REGEN_ENABLED" then
			Nameplates:RefreshReactionColors()
			for frame in pairs(Nameplates.Frames) do
				UpdatePlayerNameColor(frame)
			end
		else
			C_Timer.After(1, function()
				if not InCombatLockdown() then
					Nameplates:ApplySettings()
				end
			end)
		end
	end)
	self.Events = events
	self:ApplySettings()
end

SlickUI:RegisterModule("Nameplates", Nameplates)
