
local detailsFramework = _G["DetailsFramework"]

if (not detailsFramework or not DetailsFrameworkCanLoad) then
	return
end

local _
--lua locals
local rawset = rawset --lua local
local rawget = rawget --lua local
local setmetatable = setmetatable --lua local
local unpack = table.unpack or unpack --lua local
local type = type --lua local
local floor = math.floor --lua local
local loadstring = loadstring --lua local
local G_CreateFrame = _G.CreateFrame
local CreateFrame = function (frameType , name, parent, template, id)
	local frame = G_CreateFrame(frameType , name, parent, template, id)
	detailsFramework:Mixin(frame, detailsFramework.FrameFunctions)
	frame:SetClipsChildren(false)
	return frame
end

local UnitHealth = UnitHealth
local UnitHealthMax = UnitHealthMax
local UnitGetIncomingHeals = UnitGetIncomingHeals
local UnitGetTotalHealAbsorbs = UnitGetTotalHealAbsorbs
local UnitGetTotalAbsorbs = UnitGetTotalAbsorbs
local UnitPowerMax = UnitPowerMax
local UnitPower = UnitPower
local UnitPowerBarID = UnitPowerBarID
local GetUnitPowerBarInfoByID = GetUnitPowerBarInfoByID
local IsInGroup = IsInGroup
local UnitPowerType = UnitPowerType
local UnitIsConnected = UnitIsConnected
local UnitPlayerControlled = UnitPlayerControlled
local UnitIsTapDenied = UnitIsTapDenied
local max = math.max
local min = math.min
local abs = math.abs
local GetSpellInfo = GetSpellInfo


local CastInfo = detailsFramework.CastInfo

local PixelUtil = PixelUtil

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--health bar frame

--[=[
	DF:CreateHealthBar (parent, name, settingsOverride)
	creates a health bar to show an unit health
	@parent = frame to pass for the CreateFrame function
	@name = absolute name of the frame, if omitted it uses the parent's name .. "HealthBar"
	@settingsOverride = table with keys and values to replace the defaults from the framework

	methods:
	healthbar:SetUnit(unit)
	healthBar:GetTexture()
	healthBar:SetTexture(texture)
--]=]

---@class df_healthbarsettings : table
---@field CanTick boolean
---@field ShowHealingPrediction boolean
---@field ShowShields boolean
---@field BackgroundColor table
---@field Texture texturepath|textureid|atlasname
---@field ShieldIndicatorTexture texturepath|textureid|atlasname
---@field ShieldGlowTexture texturepath|textureid|atlasname
---@field ShieldGlowWidth number
---@field DontSetStatusBarTexture boolean
---@field Width number
---@field Height number

---@class df_healthbar : statusbar, df_scripthookmixin, df_statusbarmixin
---@field unit unit
---@field displayedUnit unit
---@field oldHealth number
---@field currentHealth number
---@field currentHealthMax number
---@field nextShieldHook number
---@field WidgetType string
---@field Settings df_healthbarsettings
---@field background texture
---@field incomingHealIndicator texture
---@field shieldAbsorbIndicator texture
---@field healAbsorbIndicator texture
---@field shieldAbsorbGlow texture
---@field barTexture texture
---@field SetUnit fun(self:df_healthbar, unit:unit?, displayedUnit:unit)
---@field GetTexture fun(self:df_healthbar) : texture
---@field SetTexture fun(self:df_healthbar, texture:texturepath|textureid|atlasname)
---@field SetColor fun(self:df_healthbar, red:number, green:number, blue:number, alpha:number)
---@field UpdateHealPrediction fun(self:df_healthbar)
---@field UpdateHealth fun(self:df_healthbar)
---@field UpdateMaxHealth fun(self:df_healthbar)

--healthBar meta prototype
	local healthBarMetaPrototype = {
		WidgetType = "healthBar",
		dversion = detailsFramework.dversion,
	}

	--check if there's a metaPrototype already existing
	if (_G[detailsFramework.GlobalWidgetControlNames["healthBar"]]) then
		--get the already existing metaPrototype
		local oldMetaPrototype = _G[detailsFramework.GlobalWidgetControlNames["healthBar"]]
		--check if is older
		if ( (not oldMetaPrototype.dversion) or (oldMetaPrototype.dversion < detailsFramework.dversion) ) then
			--the version is older them the currently loading one
			--copy the new values into the old metatable
			for funcName, _ in pairs(healthBarMetaPrototype) do
				oldMetaPrototype[funcName] = healthBarMetaPrototype[funcName]
			end
		end
	else
		--first time loading the framework
		_G[detailsFramework.GlobalWidgetControlNames["healthBar"]] = healthBarMetaPrototype
	end

	local healthBarMetaFunctions = _G[detailsFramework.GlobalWidgetControlNames["healthBar"]]
	detailsFramework:Mixin(healthBarMetaFunctions, detailsFramework.ScriptHookMixin)

--hook list
	local defaultHooksForHealthBar = {
		OnHide = {},
		OnShow = {},
		OnHealthChange = {},
		OnHealthMaxChange = {},
		OnAbsorbOverflow = {},
	}

	--use the hook already existing
	healthBarMetaFunctions.HookList = healthBarMetaFunctions.HookList or defaultHooksForHealthBar
	--copy the non existing values from a new version to the already existing hook table
	detailsFramework.table.deploy(healthBarMetaFunctions.HookList, defaultHooksForHealthBar)

--Health Bar Meta Functions

	--health bar settings
	healthBarMetaFunctions.Settings = {
		CanTick = false, --if true calls the method 'OnTick' every tick, the function needs to be overloaded, it receives self and deltaTime as parameters
		ShowHealingPrediction = true, --when casting a healing pass, show the amount of health that spell will heal
		ShowShields = true, --indicator of the amount of damage absortion the unit has
		DontSetStatusBarTexture = false,

		--appearance
		BackgroundColor = detailsFramework:CreateColorTable (.2, .2, .2, .8),
		Texture = [[Interface\RaidFrame\Raid-Bar-Hp-Fill]],
		ShieldIndicatorTexture = [[Interface\RaidFrame\Shield-Fill]],
		ShieldGlowTexture = [[Interface\RaidFrame\Shield-Overshield]],
		ShieldGlowWidth = 16,

		--default size
		Width = 100,
		Height = 20,
	}

	healthBarMetaFunctions.HealthBarEvents = {
		{"PLAYER_ENTERING_WORLD"},
		{"UNIT_HEALTH", true},
		{"UNIT_MAXHEALTH", true},
		{"UNIT_HEAL_PREDICTION", true},
		{"UNIT_ABSORB_AMOUNT_CHANGED", true},
		--{"UNIT_HEAL_ABSORB_AMOUNT_CHANGED", true},
	}

	--setup the castbar to be used by another unit
	healthBarMetaFunctions.SetUnit = function(self, unit, displayedUnit)
		if (self.unit ~= unit or self.displayedUnit ~= displayedUnit or unit == nil) then
			self.unit = unit
			self.displayedUnit = displayedUnit or unit

			--register events
			if (unit) then
				self.currentHealth = UnitHealth(unit) or 0
				self.currentHealthMax = UnitHealthMax(unit) or 0

				for _, eventTable in ipairs(self.HealthBarEvents) do
					local event = eventTable[1]
					local isUnitEvent = eventTable[2]
					if event then
						if (isUnitEvent) then
							self:RegisterUnitEvent(event, self.displayedUnit, self.unit)
						else
							self:RegisterEvent(event)
						end
					end
				end

				--check for settings and update some events
				if (not self.Settings.ShowHealingPrediction) then
					self:UnregisterEvent("UNIT_HEAL_PREDICTION")
					self:UnregisterEvent("UNIT_HEAL_ABSORB_AMOUNT_CHANGED")
					self.incomingHealIndicator:Hide()
					self.healAbsorbIndicator:Hide()
				end
				if (not self.Settings.ShowShields) then
					self:UnregisterEvent("UNIT_ABSORB_AMOUNT_CHANGED")
					self.shieldAbsorbIndicator:Hide()
					self.shieldAbsorbGlow:Hide()
				end

				--set scripts
				self:SetScript("OnEvent", self.OnEvent)

				if (self.Settings.CanTick) then
					self:SetScript("OnUpdate", self.OnTick)
				end

				self:PLAYER_ENTERING_WORLD(self.unit, self.displayedUnit)
			else
				--remove all registered events
				for _, eventTable in ipairs(self.HealthBarEvents) do
					local event = eventTable[1]
					if event then
						self:UnregisterEvent(event)
					end
				end

				--remove scripts
				self:SetScript("OnEvent", nil)
				self:SetScript("OnUpdate", nil)
				self:Hide()
			end
		end
	end

	healthBarMetaFunctions.Initialize = function(self)
		PixelUtil.SetWidth(self, self.Settings.Width, 1)
		PixelUtil.SetHeight(self, self.Settings.Height, 1)

		self:SetTexture(self.Settings.Texture)

		self.background:SetAllPoints()
		self.background:SetTexture(self.Settings.BackgroundColor:GetColor())

		--setpoint of these widgets are set inside the function that updates the incoming heal
		self.incomingHealIndicator:SetTexture(self:GetTexture())
		self.healAbsorbIndicator:SetTexture(self:GetTexture())
		self.healAbsorbIndicator:SetVertexColor(.1, .8, .8)
		self.shieldAbsorbIndicator:SetTexture(self.Settings.ShieldIndicatorTexture, true, true)

		self.shieldAbsorbGlow:SetWidth(self.Settings.ShieldGlowWidth)
		self.shieldAbsorbGlow:SetTexture(self.Settings.ShieldGlowTexture)
		self.shieldAbsorbGlow:SetBlendMode("ADD")
		self.shieldAbsorbGlow:SetPoint("topright", self, "topright", 8, 0)
		self.shieldAbsorbGlow:SetPoint("bottomright", self, "bottomright", 8, 0)
		self.shieldAbsorbGlow:Hide()

		self:SetUnit(nil)

		self.currentHealth = 1
		self.currentHealthMax = 2
	end

	--call every tick
	healthBarMetaFunctions.OnTick = function(self, deltaTime) end --if overrided, set 'CanTick' to true on the settings table

	--when an event happen for this unit, send it to the apropriate function
	healthBarMetaFunctions.OnEvent = function(self, event, ...)
		local eventFunc = self[event]
		if (eventFunc) then
			--the function doesn't receive which event was, only 'self' and the parameters
			eventFunc(self, ...)
		end
	end

	--when the unit max health is changed
	healthBarMetaFunctions.UpdateMaxHealth = function(self)
		local maxHealth = UnitHealthMax(self.displayedUnit)
		self:SetMinMaxValues(0, maxHealth)
		self.currentHealthMax = maxHealth

		if (self.OnHealthMaxChange) then --direct call
			self.OnHealthMaxChange(self, self.displayedUnit)
		else
			self:RunHooksForWidget("OnHealthMaxChange", self, self.displayedUnit)
		end
	end

	healthBarMetaFunctions.UpdateHealth = function(self)
		-- update max health regardless to avoid weird wrong values on UpdateMaxHealth sometimes
		-- local maxHealth = UnitHealthMax(self.displayedUnit)
		-- self:SetMinMaxValues(0, maxHealth)
		-- self.currentHealthMax = maxHealth

		self.oldHealth = self.currentHealth
		local health = UnitHealth(self.displayedUnit)
		self.currentHealth = health
		PixelUtil.SetStatusBarValue(self, health)

		if (self.OnHealthChange) then --direct call
			self.OnHealthChange(self, self.displayedUnit)
		else
			self:RunHooksForWidget("OnHealthChange", self, self.displayedUnit)
		end
	end

	--health and absorbs prediction
	healthBarMetaFunctions.UpdateHealPrediction = function(self)
		local currentHealth = self.currentHealth
		local currentHealthMax = self.currentHealthMax

		if (not currentHealthMax or currentHealthMax <= 0) then
			return
		end

		local healthPercent = currentHealth / currentHealthMax

		--order is: the health of the unit > damage absorb > heal absorb > incoming heal
		local width = self:GetWidth()

		if (self.Settings.ShowHealingPrediction) then
			--incoming heal on the unit from all sources
			local unitHealIncoming = 0--self.displayedUnit and UnitGetIncomingHeals(self.displayedUnit) or 0
			--heal absorbs
			local unitHealAbsorb = 0--self.displayedUnit and UnitGetTotalHealAbsorbs(self.displayedUnit) or 0

			if (unitHealIncoming > 0) then
				--calculate what is the percent of health incoming based on the max health the player has
				local incomingPercent = unitHealIncoming / currentHealthMax
				self.incomingHealIndicator:Show()
				self.incomingHealIndicator:SetWidth(max(1, min (width * incomingPercent, abs(healthPercent - 1) * width)))
				self.incomingHealIndicator:SetPoint("topleft", self, "topleft", width * healthPercent, 0)
				self.incomingHealIndicator:SetPoint("bottomleft", self, "bottomleft", width * healthPercent, 0)
			else
				self.incomingHealIndicator:Hide()
			end

			if (unitHealAbsorb > 0) then
				local healAbsorbPercent = unitHealAbsorb / currentHealthMax
				self.healAbsorbIndicator:Show()
				self.healAbsorbIndicator:SetWidth(max(1, min (width * healAbsorbPercent, abs(healthPercent - 1) * width)))
				self.healAbsorbIndicator:SetPoint("topleft", self, "topleft", width * healthPercent, 0)
				self.healAbsorbIndicator:SetPoint("bottomleft", self, "bottomleft", width * healthPercent, 0)
			else
				self.healAbsorbIndicator:Hide()
			end
		end

		if (self.Settings.ShowShields) then
			--damage absorbs
			local unitDamageAbsorb = self.displayedUnit and UnitGetTotalAbsorbs (self.displayedUnit) or 0

			if (unitDamageAbsorb > 0) then
				local damageAbsorbPercent = unitDamageAbsorb / currentHealthMax
				self.shieldAbsorbIndicator:Show()
				--set the width where the max width size is what is lower: the absorb size or the missing amount of health in the health bar
				--/dump NamePlate1PlaterUnitFrameHealthBar.shieldAbsorbIndicator:GetSize()
				self.shieldAbsorbIndicator:SetWidth(max(1, min (width * damageAbsorbPercent, abs(healthPercent - 1) * width)))
				self.shieldAbsorbIndicator:SetPoint("topleft", self, "topleft", width * healthPercent, 0)
				self.shieldAbsorbIndicator:SetPoint("bottomleft", self, "bottomleft", width * healthPercent, 0)

				--if the absorb percent pass 100%, show the glow
				if ((healthPercent + damageAbsorbPercent) > 1) then
					self.nextShieldHook = self.nextShieldHook or 0

					if (GetTime() >= self.nextShieldHook) then
						self:RunHooksForWidget("OnAbsorbOverflow", self, self.displayedUnit, healthPercent + damageAbsorbPercent - 1)
						self.nextShieldHook = GetTime() + 0.2
					end

					self.shieldAbsorbGlow:Show()
				else
					self.shieldAbsorbGlow:Hide()
					if (self.nextShieldHook) then
						self:RunHooksForWidget("OnAbsorbOverflow", self, self.displayedUnit, 0)
						self.nextShieldHook = nil
					end
				end
			else
				self.shieldAbsorbIndicator:Hide()
				self.shieldAbsorbGlow:Hide()
				if (self.nextShieldHook) then
					self:RunHooksForWidget("OnAbsorbOverflow", self, self.displayedUnit, 0)
					self.nextShieldHook = nil
				end
			end
		else
			self.shieldAbsorbIndicator:Hide()
			self.shieldAbsorbGlow:Hide()
			if (self.nextShieldHook) then
				self:RunHooksForWidget("OnAbsorbOverflow", self, self.displayedUnit, 0)
				self.nextShieldHook = nil
			end
		end
	end

	--Health Events
		healthBarMetaFunctions.PLAYER_ENTERING_WORLD = function(self, ...)
			self:UpdateMaxHealth()
			self:UpdateHealth()
			self:UpdateHealPrediction()
		end

		healthBarMetaFunctions.UNIT_HEALTH = function(self, unitId)
			self:UpdateHealth()
			self:UpdateHealPrediction()
		end

		healthBarMetaFunctions.UNIT_MAXHEALTH = function(self, unitId)
			self:UpdateMaxHealth()
			self:UpdateHealth()
			self:UpdateHealPrediction()
		end

		healthBarMetaFunctions.UNIT_HEALTH_FREQUENT = function(self, ...)
			self:UpdateHealth()
			self:UpdateHealPrediction()
		end

		healthBarMetaFunctions.UNIT_HEAL_PREDICTION = function(self, ...)
			self:UpdateMaxHealth()
			self:UpdateHealth()
			self:UpdateHealPrediction()
		end

		healthBarMetaFunctions.UNIT_ABSORB_AMOUNT_CHANGED = function(self, ...)
			self:UpdateMaxHealth()
			self:UpdateHealth()
			self:UpdateHealPrediction()
		end

		healthBarMetaFunctions.UNIT_HEAL_ABSORB_AMOUNT_CHANGED = function(self, ...)
			self:UpdateMaxHealth()
			self:UpdateHealth()
			self:UpdateHealPrediction()
		end

-- ~healthbar

---comment
---@param parent frame
---@param name string?
---@param settingsOverride table?  a table with key/value pairs to override the default settings
---@return df_healthbar
function detailsFramework:CreateHealthBar(parent, name, settingsOverride)
	assert(name or parent:GetName(), "DetailsFramework:CreateHealthBar parameter 'name' omitted and parent has no name.")

	local healthBar = CreateFrame("StatusBar", name or (parent:GetName() .. "HealthBar"), parent, "BackdropTemplate")
		do --layers
			--background
			healthBar.background = healthBar:CreateTexture(nil, "background")
			healthBar.background:SetDrawLayer("background", -6)

			--artwork
			--healing incoming
			healthBar.incomingHealIndicator = healthBar:CreateTexture(nil, "artwork", nil, 5)
			healthBar.incomingHealIndicator:SetDrawLayer("artwork", 4)
			--current shields on the unit
			healthBar.shieldAbsorbIndicator =  healthBar:CreateTexture(nil, "artwork", nil, 3)
			healthBar.shieldAbsorbIndicator:SetDrawLayer("artwork", 5)
			--debuff absorbing heal
			healthBar.healAbsorbIndicator = healthBar:CreateTexture(nil, "artwork", nil, 4)
			healthBar.healAbsorbIndicator:SetDrawLayer("artwork", 6)
			--the shield fills all the bar, show that cool glow
			healthBar.shieldAbsorbGlow = healthBar:CreateTexture(nil, "artwork", nil, 6)
			healthBar.shieldAbsorbGlow:SetDrawLayer("artwork", 7)
			--statusbar texture
			healthBar.barTexture = healthBar:CreateTexture(nil, "artwork", nil, 1)
		end

	--mixins
	detailsFramework:Mixin(healthBar, healthBarMetaFunctions)
	detailsFramework:Mixin(healthBar, detailsFramework.StatusBarFunctions)

	healthBar:CreateTextureMask()
	healthBar:SetTexture([[Interface\WorldStateFrame\WORLDSTATEFINALSCORE-HIGHLIGHT]])

	--settings and hooks
	local settings = detailsFramework.table.copy({}, healthBarMetaFunctions.Settings)
	if (settingsOverride) then
		detailsFramework.table.copy(settings, settingsOverride)
	end
	healthBar.Settings = settings

	if (healthBar.Settings.DontSetStatusBarTexture) then
		healthBar.barTexture:SetAllPoints()
	else
		healthBar:SetStatusBarTexture(healthBar.barTexture)
	end

	--hook list
	healthBar.HookList = detailsFramework.table.copy({}, healthBarMetaFunctions.HookList)

	--initialize the cast bar
	healthBar:Initialize()

	return healthBar
end

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--power bar frame

--[=[
	DF:CreatePowerBar (parent, name, settingsOverride)
	creates statusbar frame to show the unit power bar
	@parent = frame to pass for the CreateFrame function
	@name = absolute name of the frame, if omitted it uses the parent's name .. "PPowerBar"
	@settingsOverride = table with keys and values to replace the defaults from the framework
--]=]

---@class df_powerbarsettings : table
---@field ShowAlternatePower boolean
---@field ShowPercentText boolean
---@field HideIfNoPower boolean
---@field CanTick boolean
---@field BackgroundColor table
---@field Texture texturepath|textureid|atlasname
---@field Width number
---@field Height number

---@class df_powerbar : statusbar, df_scripthookmixin, df_statusbarmixin
---@field unit string
---@field displayedUnit string
---@field WidgetType string
---@field currentPower number
---@field currentPowerMax number
---@field powerType number
---@field minPower number
---@field Settings df_powerbarsettings
---@field background texture
---@field percentText fontstring
---@field SetUnit fun(self:df_healthbar, unit:unit?, displayedUnit:unit?)

detailsFramework.PowerFrameFunctions = {
	WidgetType = "powerBar",

	HookList = {
		OnHide = {},
		OnShow = {},
	},

	Settings = {
		--misc
		ShowAlternatePower = true, --if true it'll show alternate power over the regular power the unit uses
		ShowPercentText = true, --if true show a text with the current energy percent
		HideIfNoPower = true, --if true and the UnitMaxPower returns zero, it'll hide the power bar with self:Hide()
		CanTick = false, --if it calls the OnTick function every tick

		--appearance
		BackgroundColor = detailsFramework:CreateColorTable (.2, .2, .2, .8),
		Texture = [[Interface\RaidFrame\Raid-Bar-Resource-Fill]],

		--default size
		Width = 100,
		Height = 20,
	},

	PowerBarEvents = {
		{"PLAYER_ENTERING_WORLD"},
		{"UNIT_DISPLAYPOWER", true},
		{"UNIT_POWER_BAR_SHOW", true},
		{"UNIT_POWER_BAR_HIDE", true},
		{"UNIT_MAXPOWER", true},
		{"UNIT_POWER_UPDATE", true},
		{"UNIT_POWER_FREQUENT", true},
	},

	--setup the castbar to be used by another unit
	SetUnit = function(self, unit, displayedUnit)
		if (self.unit ~= unit or self.displayedUnit ~= displayedUnit or unit == nil) then
			self.unit = unit
			self.displayedUnit = displayedUnit or unit

			--register events
			if (unit) then
				for _, eventTable in ipairs(self.PowerBarEvents) do
					local event = eventTable[1]
					local isUnitEvent = eventTable[2]

					if (isUnitEvent) then
						self:RegisterUnitEvent(event, self.displayedUnit)
					else
						self:RegisterEvent(event)
					end
				end

				--set scripts
				self:SetScript("OnEvent", self.OnEvent)

				if (self.Settings.CanTick) then
					self:SetScript("OnUpdate", self.OnTick)
				end

				self:Show()
				self:UpdatePowerBar()
			else
				--remove all registered events
				for _, eventTable in ipairs(self.PowerBarEvents) do
					local event = eventTable[1]
					self:UnregisterEvent(event)
				end

				--remove scripts
				self:SetScript("OnEvent", nil)
				self:SetScript("OnUpdate", nil)
				self:Hide()
			end
		end
	end,

	Initialize = function(self)
		PixelUtil.SetWidth (self, self.Settings.Width)
		PixelUtil.SetHeight(self, self.Settings.Height)

		self:SetTexture(self.Settings.Texture)

		self.background:SetAllPoints()
		self.background:SetTexture(self.Settings.BackgroundColor:GetColor())

		if (self.Settings.ShowPercentText) then
			self.percentText:Show()
			PixelUtil.SetPoint(self.percentText, "center", self, "center", 0, 0)

			detailsFramework:SetFontSize(self.percentText, 9)
			detailsFramework:SetFontColor(self.percentText, "white")
			detailsFramework:SetFontOutline(self.percentText, "OUTLINE")
		else
			self.percentText:Hide()
		end

		self:SetUnit(nil)
	end,

	--call every tick
	OnTick = function(self, deltaTime) end, --if overrided, set 'CanTick' to true on the settings table

	--when an event happen for this unit, send it to the apropriate function
	OnEvent = function(self, event, ...)
		local eventFunc = self[event]
		if (eventFunc) then
			--the function doesn't receive which event was, only 'self' and the parameters
			eventFunc(self, ...)
		end
	end,

	UpdatePowerBar = function(self)
		self:UpdatePowerInfo()
		self:UpdateMaxPower()
		self:UpdatePower()
		self:UpdatePowerColor()
	end,

	--power update
	UpdateMaxPower = function(self)
		self.currentPowerMax = UnitPowerMax(self.displayedUnit, self.powerType)
		self:SetMinMaxValues(self.minPower, self.currentPowerMax)

		if (self.currentPowerMax == 0 and self.Settings.HideIfNoPower) then
			self:Hide()
		end
	end,

	UpdatePower = function(self)
		self.currentPower = UnitPower(self.displayedUnit, self.powerType)
		PixelUtil.SetStatusBarValue(self, self.currentPower)

		if (self.Settings.ShowPercentText) then
			self.percentText:SetText(floor(self.currentPower / self.currentPowerMax * 100) .. "%")
		end
	end,

	--when a event different from unit_power_update is triggered, update which type of power the unit should show
	UpdatePowerInfo = function(self)
		self.powerType = UnitPowerType (self.displayedUnit)
		self.minPower = 0
	end,

	--tint the bar with the color of the power, e.g. blue for a mana bar
	UpdatePowerColor = function(self)
		if (not UnitIsConnected (self.unit)) then
			self:SetStatusBarColor(.5, .5, .5)
			return
		end

		if (self.powerType == Enum.PowerType.Focus) then
			--don't change this, keep the same color as the game tints on CompactUnitFrame.lua
			self:SetStatusBarColor(0.7, 0.7, 0.6)
			return
		end

		local powerColor = PowerBarColor[self.powerType] --don't appear to be, but PowerBarColor is a global table with all power colors /run Details:Dump (PowerBarColor)
		if (powerColor) then
			self:SetStatusBarColor(powerColor.r, powerColor.g, powerColor.b)
			return
		end

		local _, _, r, g, b = UnitPowerType(self.displayedUnit)
		if (r) then
			self:SetStatusBarColor(r, g, b)
			return
		end

		--if everything else fails, tint as rogue energy
		powerColor = PowerBarColor["ENERGY"]
		self:SetStatusBarColor(powerColor.r, powerColor.g, powerColor.b)
	end,

	--events
	PLAYER_ENTERING_WORLD = function(self, ...)
		self:UpdatePowerBar()
	end,
	UNIT_DISPLAYPOWER  = function(self, ...)
		self:UpdatePowerBar()
	end,
	UNIT_POWER_BAR_SHOW = function(self, ...)
		self:UpdatePowerBar()
	end,
	UNIT_POWER_BAR_HIDE = function(self, ...)
		self:UpdatePowerBar()
	end,

	UNIT_MAXPOWER = function(self, ...)
		self:UpdateMaxPower()
		self:UpdatePower()
	end,
	UNIT_POWER_UPDATE = function(self, ...)
		self:UpdatePower()
	end,
	UNIT_POWER_FREQUENT = function(self, ...)
		self:UpdatePower()
	end,
}

detailsFramework:Mixin(detailsFramework.PowerFrameFunctions, detailsFramework.ScriptHookMixin)

-- ~powerbar

---create a power bar
---@param parent frame
---@param name string?
---@param settingsOverride table? a table with key/value pairs to override the default settings
---@return df_powerbar
function detailsFramework:CreatePowerBar(parent, name, settingsOverride)
	assert(name or parent:GetName(), "DetailsFramework:CreatePowerBar parameter 'name' omitted and parent has no name.")

	local powerBar = CreateFrame("StatusBar", name or (parent:GetName() .. "PowerBar"), parent, "BackdropTemplate")
		do --layers
			--background
			powerBar.background = powerBar:CreateTexture(nil, "background")
			powerBar.background:SetDrawLayer("background", -6)

			--artwork
			powerBar.barTexture = powerBar:CreateTexture(nil, "artwork")
			powerBar:SetStatusBarTexture(powerBar.barTexture)

			--overlay
			powerBar.percentText = powerBar:CreateFontString(nil, "overlay", "GameFontNormal")
		end

	--mixins
	detailsFramework:Mixin(powerBar, detailsFramework.PowerFrameFunctions)
	detailsFramework:Mixin(powerBar, detailsFramework.StatusBarFunctions)

	powerBar:CreateTextureMask()
	powerBar:SetTexture([[Interface\WorldStateFrame\WORLDSTATEFINALSCORE-HIGHLIGHT]])

	--settings and hooks
	local settings = detailsFramework.table.copy({}, detailsFramework.PowerFrameFunctions.Settings)
	if (settingsOverride) then
		detailsFramework.table.copy(settings, settingsOverride)
	end
	powerBar.Settings = settings

	local hookList = detailsFramework.table.copy({}, detailsFramework.PowerFrameFunctions.HookList)
	powerBar.HookList = hookList

	--initialize the cast bar
	powerBar:Initialize()

	return powerBar
end

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--cast bar frame

--[=[
	DF:CreateCastBar (parent, name, settingsOverride)
	creates a cast bar to show an unit cast
	@parent = frame to pass for the CreateFrame function
	@name = absolute name of the frame, if omitted it uses the parent's name .. "CastBar"
	@settingsOverride = table with keys and values to replace the defaults from the framework
--]=]

---@class df_castbarsettings : table
---@field NoFadeEffects  boolean if true it won't play fade effects when a cast if finished
---@field ShowTradeSkills boolean if true, it shows cast for trade skills, e.g. creating an icon with blacksmith
---@field ShowShield boolean if true, shows the shield above the spell icon for non interruptible casts
---@field CanTick boolean if true it will run its OnTick function every tick.
---@field ShowCastTime boolean if true, show the remaining time to finish the cast, lazy tick must be enabled
---@field FadeInTime number amount of time in seconds to go from zero to 100% alpha when starting to cast
---@field FadeOutTime number amount of time in seconds to go from 100% to zero alpha when the cast finishes
---@field CanLazyTick boolean if true, it'll execute the lazy tick function, it ticks in a much slower pace comparece with the regular tick
---@field LazyUpdateCooldown number amount of time to wait for the next lazy update, this updates non critical things like the cast timer
---@field ShowEmpoweredDuration boolean full hold time for empowered spells
---@field FillOnInterrupt boolean
---@field HideSparkOnInterrupt boolean
---@field Width number
---@field Height number
---@field Colors df_castcolors
---@field BackgroundColor table
---@field Texture texturepath|textureid
---@field BorderShieldWidth number
---@field BorderShieldHeight number
---@field BorderShieldCoords table
---@field BorderShieldTexture number
---@field SpellIconWidth number
---@field SpellIconHeight number
---@field ShieldIndicatorTexture texturepath|textureid
---@field ShieldGlowTexture texturepath|textureid
---@field SparkTexture texturepath|textureid
---@field SparkWidth number
---@field SparkHeight number
---@field SparkOffset number

---@alias caststage_color
---| "Casting"
---| "Channeling"
---| "Interrupted"
---| "Failed"
---| "NotInterruptable"
---| "Finished"

---@class df_castcolors : table
---@field Casting table
---@field Channeling table
---@field Interrupted table
---@field Failed table
---@field NotInterruptable table
---@field Finished table

---@class df_castbar : statusbar, df_scripthookmixin, df_statusbarmixin
---@field unit string
---@field displayedUnit string
---@field WidgetType string
---@field value number
---@field maxValue number
---@field spellStartTime number
---@field spellEndTime number
---@field empowered boolean
---@field curStage number
---@field numStages number
---@field empStages {start:number, finish:number}[]
---@field stagePips texture[]
---@field holdAtMaxTime number
---@field casting boolean
---@field channeling boolean
---@field interrupted boolean
---@field failed boolean
---@field finished boolean
---@field canInterrupt boolean
---@field spellID spellid
---@field castID number
---@field spellName spellname
---@field spellTexture textureid
---@field Colors df_castcolors
---@field Settings df_castbarsettings
---@field background texture
---@field extraBackground texture
---@field Text fontstring
---@field BorderShield texture
---@field Icon texture
---@field Spark texture
---@field percentText fontstring
---@field barTexture texture
---@field flashTexture texture
---@field fadeOutAnimation animationgroup
---@field fadeInAnimation animationgroup
---@field flashAnimation animationgroup
---@field SetUnit fun(self:df_castbar, unit:string?)
---@field SetDefaultColor fun(self:df_castbar, colorType: caststage_color, red:any, green:number?, blue:number?, alpha:number?)
---@field UpdateCastColor fun(self:df_castbar) after setting a new color, call this function to update the bar color (while casting or channeling)
---@field GetCastColor fun(self:df_castbar) return a table with the color values for the current state of the casting process

detailsFramework.CastFrameFunctions = {
	WidgetType = "castBar",

	HookList = {
		OnHide = {},
		OnShow = {},

		--can be regular cast or channel
		OnCastStart = {},
	},

	CastBarEvents = {
		{"UNIT_SPELLCAST_INTERRUPTED"},
		{"UNIT_SPELLCAST_CHANNEL_START"},
		{"UNIT_SPELLCAST_CHANNEL_UPDATE"},
		{"UNIT_SPELLCAST_CHANNEL_STOP"},
		{"PLAYER_ENTERING_WORLD"},
		{"UNIT_SPELLCAST_START", true},
		{"UNIT_SPELLCAST_STOP", true},
		{"UNIT_SPELLCAST_FAILED", true},
	},

	Settings = {
		NoFadeEffects = false, --if true it won't play fade effects when a cast if finished
		ShowTradeSkills = false, --if true, it shows cast for trade skills, e.g. creating an icon with blacksmith
		ShowShield = true, --if true, shows the shield above the spell icon for non interruptible casts
		CanTick = true, --if true it will run its OnTick function every tick.
		ShowCastTime = true, --if true, show the remaining time to finish the cast, lazy tick must be enabled
		FadeInTime = 0.1, --amount of time in seconds to go from zero to 100% alpha when starting to cast
		FadeOutTime = 0.5, --amount of time in seconds to go from 100% to zero alpha when the cast finishes
		CanLazyTick = true, --if true, it'll execute the lazy tick function, it ticks in a much slower pace comparece with the regular tick
		LazyUpdateCooldown = 0.2, --amount of time to wait for the next lazy update, this updates non critical things like the cast timer

		ShowEmpoweredDuration = true, --full hold time for empowered spells

		FillOnInterrupt = true,
		HideSparkOnInterrupt = true,

		--default size
		Width = 100,
		Height = 20,

		--colour the castbar statusbar by the type of the cast
		Colors = {
			Casting = detailsFramework:CreateColorTable (1, 0.73, .1, 1),
			Channeling = detailsFramework:CreateColorTable (1, 0.73, .1, 1),
			Finished = detailsFramework:CreateColorTable (0, 1, 0, 1),
			NonInterruptible = detailsFramework:CreateColorTable (.7, .7, .7, 1),
			Failed = detailsFramework:CreateColorTable (.4, .4, .4, 1),
			Interrupted = detailsFramework:CreateColorTable (.965, .754, .154, 1),
		},

		--appearance
		BackgroundColor = detailsFramework:CreateColorTable (.2, .2, .2, .8),
		Texture = [[Interface\TargetingFrame\UI-StatusBar]],
		BorderShieldWidth = 10,
		BorderShieldHeight = 12,
		BorderShieldCoords = {0.26171875, 0.31640625, 0.53125, 0.65625},
		BorderShieldTexture = 1300837,
		SpellIconWidth = 10,
		SpellIconHeight = 10,
		ShieldIndicatorTexture = [[Interface\RaidFrame\Shield-Fill]],
		ShieldGlowTexture = [[Interface\RaidFrame\Shield-Overshield]],
		SparkTexture = [[Interface\CastingBar\UI-CastingBar-Spark]],
		SparkWidth = 16,
		SparkHeight = 16,
		SparkOffset = 0,
	},

	Initialize = function(self)
		self.unit = "unutilized unit"
		self.lazyUpdateCooldown = self.Settings.LazyUpdateCooldown
		self.Colors = self.Settings.Colors

		self:SetUnit(nil)
		PixelUtil.SetWidth (self, self.Settings.Width)
		PixelUtil.SetHeight(self, self.Settings.Height)

		self.background:SetTexture(self.Settings.BackgroundColor:GetColor())
		self.background:SetAllPoints()
		self.extraBackground:SetTexture(0, 0, 0, 1)
		self.extraBackground:SetVertexColor(self.Settings.BackgroundColor:GetColor())
		self.extraBackground:SetAllPoints()

		self:SetTexture(self.Settings.Texture)

		self.BorderShield:SetPoint("center", self, "left", 0, 0)
		self.BorderShield:SetTexture(self.Settings.BorderShieldTexture)
		self.BorderShield:SetTexCoord(unpack(self.Settings.BorderShieldCoords))
		self.BorderShield:SetSize(self.Settings.BorderShieldWidth, self.Settings.BorderShieldHeight)

		self.Icon:SetPoint("center", self, "left", 2, 0)
		self.Icon:SetSize(self.Settings.SpellIconWidth, self.Settings.SpellIconHeight)

		self.Spark:SetTexture(self.Settings.SparkTexture)
		self.Spark:SetSize(self.Settings.SparkWidth, self.Settings.SparkHeight)

		self.percentText:SetPoint("right", self, "right", -2, 0)
		self.percentText:SetJustifyH("right")

		self.fadeOutAnimation.alpha1:SetDuration(self.Settings.FadeOutTime)
		self.fadeInAnimation.alpha1:SetDuration(self.Settings.FadeInTime)
	end,

	SetDefaultColor = function(self, colorType, r, g, b, a)
		assert(type(colorType) == "string", "DetailsFramework: CastBar:SetDefaultColor require a string in the first argument.")
		self.Colors[colorType]:SetColor(r, g, b, a)
	end,

	--this get a color suggestion based on the type of cast being shown in the cast bar
	GetCastColor = function(self)
		if (not self.canInterrupt) then
			return self.Colors.NonInterruptible

		elseif (self.channeling) then
			return self.Colors.Channeling

		elseif (self.failed) then
			return self.Colors.Failed

		elseif (self.interrupted) then
			return self.Colors.Interrupted

		elseif (self.finished) then
			return self.Colors.Finished

		else
			return self.Colors.Casting
		end
	end,

	--update all colors of the cast bar
	UpdateCastColor = function(self)
		local castColor = self:GetCastColor()
		self:SetColor(castColor) --SetColor handles with ParseColors()
	end,

	--initial checks to know if this is a valid cast and should show the cast bar, if this fails the cast bar won't show
	IsValid = function(self, unit, castName, isTradeSkill, ignoreVisibility)
		if (not ignoreVisibility and not self:IsShown()) then
			return false
		end

		if (not self.Settings.ShowTradeSkills) then
			if (isTradeSkill) then
				return false
			end
		end

		if (not castName) then
			return false
		end

		return true
	end,

	--handle the interrupt state of the cast
	--this does not change the cast bar color because this function is called inside the start cast where is already handles the cast color
	UpdateInterruptState = function(self)
		if (self.Settings.ShowShield and not self.canInterrupt) then
			self.BorderShield:Show()
		else
			self.BorderShield:Hide()
		end
	end,

	--this check if the cast did reach 100% in the statusbar, mostly called from OnTick
	CheckCastIsDone = function(self, event, isFinished)
		--check max value
		if (not isFinished and not self.finished) then
			if (self.casting) then
				if (self.value >= self.maxValue) then
					isFinished = true
				end

			elseif (self.channeling) then
				if (self.value > self.maxValue or self.value <= 0) then
					isFinished = true
				end
			end

			--check if passed an event (not begin used at the moment)
			if (event) then
				if (event == UNIT_SPELLCAST_STOP or event == UNIT_SPELLCAST_CHANNEL_STOP) then
					isFinished = true
				end
			end
		end

		--the cast is finished
		if (isFinished) then
			if (self.casting) then
				self.UNIT_SPELLCAST_STOP(self, self.unit, self.unit, self.castID, self.spellID)

			elseif (self.channeling) then
				self.UNIT_SPELLCAST_CHANNEL_STOP(self, self.unit, self.unit, self.castID, self.spellID)
			end

			return true
		end
	end,

	--setup the castbar to be used by another unit
	SetUnit = function(self, unit, displayedUnit)
		if (self.unit ~= unit or self.displayedUnit ~= displayedUnit or unit == nil) then
			self.unit = unit
			self.displayedUnit = displayedUnit or unit

			--reset the cast bar
			self.casting = nil
			self.channeling = nil
			self.caninterrupt = nil

			--register events
			if (unit) then
				for _, eventTable in ipairs(self.CastBarEvents) do
					local event = eventTable[1]
					local isUnitEvent = eventTable[2]

					if event then
						if (isUnitEvent) then
							self:RegisterUnitEvent(event, unit)
						else
							self:RegisterEvent(event)
						end
					end
				end

				--set scripts
				self:SetScript("OnEvent", self.OnEvent)
				self:SetScript("OnShow", self.OnShow)
				self:SetScript("OnHide", self.OnHide)

				if (self.Settings.CanTick) then
					self:SetScript("OnUpdate", self.OnTick)
				end

				--check is can show the cast time text
				if (self.Settings.ShowCastTime and self.Settings.CanLazyTick) then
					self.percentText:Show()
				else
					self.percentText:Hide()
				end

				--setup animtions
				self:CancelScheduleToHide()

				--self:PLAYER_ENTERING_WORLD (unit, unit)
				self:OnEvent("PLAYER_ENTERING_WORLD", unit, unit)

			else
				for _, eventTable in ipairs(self.CastBarEvents) do
					local event = eventTable[1]
					if event then
						self:UnregisterEvent(event)
					end
				end

				--register main events
				self:SetScript("OnUpdate", nil)
				self:SetScript("OnEvent", nil)
				self:SetScript("OnShow", nil)
				self:SetScript("OnHide", nil)

				self:Hide()
			end
		end
	end,

	--executed after a scheduled to hide timer is done
	DoScheduledHide = function(timerObject)
		timerObject.castBar.scheduledHideTime = nil

		--just to make sure it isn't casting
		if (not timerObject.castBar.casting and not timerObject.castBar.channeling) then
			if (not timerObject.castBar.Settings.NoFadeEffects) then
				timerObject.castBar:Animation_FadeOut()
			else
				timerObject.castBar:Hide()
			end
		end
	end,

	HasScheduledHide = function(self)
		return self.scheduledHideTime and not self.scheduledHideTime:IsCancelled()
	end,

	CancelScheduleToHide = function(self)
		if (self:HasScheduledHide()) then
			self.scheduledHideTime:Cancel()
		end
	end,

	--after an interrupt, do not immediately hide the cast bar, let it up for short amount of time to give feedback to the player
	ScheduleToHide = function(self, delay)
		if (not delay) then
			if (self.scheduledHideTime and not self.scheduledHideTime:IsCancelled()) then
				self.scheduledHideTime:Cancel()
			end

			self.scheduledHideTime = nil
			return
		end

		--already have a scheduled timer?
		if (self.scheduledHideTime and not self.scheduledHideTime:IsCancelled()) then
			self.scheduledHideTime:Cancel()
		end

		self.scheduledHideTime = C_Timer.NewTimer(delay, self.DoScheduledHide)
		self.scheduledHideTime.castBar = self
	end,

	OnHide = function(self)
		--just in case some other effects made it have a different alpha since SetUnit won't load if the unit is the same.
		self:SetAlpha(1)
		--cancel any timer to hide scheduled
		self:CancelScheduleToHide()
	end,

	--just update the current value if a spell is being cast since it wasn't running its tick function during the hide state
	--everything else should be in the correct state
	OnShow = function(self)
		self.flashTexture:Hide()

		if (self.unit) then
			if (self.casting) then
				local name, text, texture, startTime = CastInfo.UnitCastingInfo(self.unit)
				if (name) then
					--[[if not self.spellStartTime then
						self:UpdateCastingInfo(self.unit)
					end]]--
					self.value = GetTime() - self.spellStartTime
				end

				self:RunHooksForWidget("OnShow", self, self.unit)

			elseif (self.channeling) then
				local name, text, texture, endTime = CastInfo.UnitChannelInfo(self.unit)
				if (name) then
					--[[if not self.spellEndTime then
						self:UpdateChannelInfo(self.unit)
					end]]--
					self.value = self.empowered and (GetTime() - self.spellStartTime) or (self.spellEndTime - GetTime())
				end

				self:RunHooksForWidget("OnShow", self, self.unit)
			end
		end
	end,

	--it's triggering several events since it's not registered for the unit with RegisterUnitEvent
	OnEvent = function(self, event, ...)
		local arg1 = ...
		local unit = self.unit

		if (event == "PLAYER_ENTERING_WORLD") then
			local newEvent = self.PLAYER_ENTERING_WORLD (self, unit, ...)
			if (newEvent) then
				self.OnEvent (self, newEvent, unit)
				return
			end

		elseif (arg1 ~= unit) then
			return
		end

		local eventFunc = self [event]
		if (eventFunc) then
			eventFunc (self, unit, ...)
		end
	end,

	OnTick_LazyTick = function(self)
		--run the lazy tick if allowed
		if (self.Settings.CanLazyTick) then
			--update the cast time
			if (self.Settings.ShowCastTime) then
				if (self.casting) then
					self.percentText:SetText(format("%.1f", abs(self.value - self.maxValue)))

				elseif (self.channeling) then
					local remainingTime = self.empowered and abs(self.value - self.maxValue) or abs(self.value)
					if (remainingTime > 999) then
						self.percentText:SetText("")
					else
						self.percentText:SetText(format("%.1f", remainingTime))
					end
				else
					self.percentText:SetText("")
				end
			end

			return true
		else
			return false
		end
	end,

	--tick function for regular casts
	OnTick_Casting = function(self, deltaTime)
		self.value = self.value + deltaTime

		if (self:CheckCastIsDone()) then
			return
		else
			self:SetValue(self.value)
		end

		--update spark position
		local sparkPosition = self.value / self.maxValue * self:GetWidth()
		self.Spark:SetPoint("center", self, "left", sparkPosition + self.Settings.SparkOffset, 0)


		--in order to allow the lazy tick run, it must return true, it tell that the cast didn't finished
		return true
	end,

	--tick function for channeling casts
	OnTick_Channeling = function(self, deltaTime)
		self.value = self.empowered and self.value + deltaTime or self.value - deltaTime

		if (self:CheckCastIsDone()) then
			return
		else
			self:SetValue(self.value)
		end

		--update spark position
		local sparkPosition = self.value / self.maxValue * self:GetWidth()
		self.Spark:SetPoint("center", self, "left", sparkPosition + self.Settings.SparkOffset, 0)

		self:CreateOrUpdateEmpoweredPips()

		return true
	end,

	OnTick = function(self, deltaTime)
		if (self.casting) then
			if (not self:OnTick_Casting(deltaTime)) then
				return
			end

			--lazy tick
			self.lazyUpdateCooldown = self.lazyUpdateCooldown - deltaTime
			if (self.lazyUpdateCooldown < 0) then
				self:OnTick_LazyTick()
				self.lazyUpdateCooldown = self.Settings.LazyUpdateCooldown
			end

		elseif (self.channeling) then
			if (not self:OnTick_Channeling(deltaTime)) then
				return
			end

			--lazy tick
			self.lazyUpdateCooldown = self.lazyUpdateCooldown - deltaTime
			if (self.lazyUpdateCooldown < 0) then
				self:OnTick_LazyTick()
				self.lazyUpdateCooldown = self.Settings.LazyUpdateCooldown
			end
		end
	end,

	--animation start script
	Animation_FadeOutStarted = function(self)

	end,

	--animation finished script
	Animation_FadeOutFinished = function(self)
		local castBar = self:GetParent()
		castBar:SetAlpha(1)
		castBar:Hide()
	end,

	--animation start script
	Animation_FadeInStarted = function(self)

	end,

	--animation finished script
	Animation_FadeInFinished = function(self)
		local castBar = self:GetParent()
		castBar:Show()
		castBar:SetAlpha(1)
	end,

	--animation calls
	Animation_FadeOut = function(self)
		self:ScheduleToHide(false)

		if (self.fadeInAnimation:IsPlaying()) then
			self.fadeInAnimation:Stop()
		end

		if (not self.fadeOutAnimation:IsPlaying()) then
			self.fadeOutAnimation:Play()
		end
	end,

	Animation_FadeIn = function(self)
		self:ScheduleToHide (false)

		if (self.fadeOutAnimation:IsPlaying()) then
			self.fadeOutAnimation:Stop()
		end

		if (not self.fadeInAnimation:IsPlaying()) then
			self.fadeInAnimation:Play()
		end
	end,

	Animation_Flash = function(self)
		if (not self.flashAnimation:IsPlaying()) then
			self.flashAnimation:Play()
		end
	end,

	Animation_StopAllAnimations = function(self)
		if (self.flashAnimation:IsPlaying()) then
			self.flashAnimation:Stop()
		end

		if (self.fadeOutAnimation:IsPlaying()) then
			self.fadeOutAnimation:Stop()
		end

		if (self.fadeInAnimation:IsPlaying()) then
			self.fadeInAnimation:Stop()
		end
	end,

	PLAYER_ENTERING_WORLD = function(self, unit, arg1)
		local isChannel = CastInfo.UnitChannelInfo(unit)
		local isRegularCast = CastInfo.UnitCastingInfo(unit)

		if (isChannel) then
			self.channeling = true
			self:UpdateChannelInfo(unit)
			return self.unit == arg1 and "UNIT_SPELLCAST_CHANNEL_START"

		elseif (isRegularCast) then
			self.casting = true
			self:UpdateCastingInfo(unit)
			return self.unit == arg1 and "UNIT_SPELLCAST_START"

		else
			self.casting = nil
			self.channeling = nil
			self.failed = nil
			self.finished = nil
			self.interrupted = nil
			self.Spark:Hide()
			self:Hide()
		end
	end,

	UpdateCastingInfo = function(self, unit, ...)
		local unitID, castID, spellID = ...
		local name, text, texture, startTime, endTime, isTradeSkill, uciCastID, notInterruptible, uciSpellID = CastInfo.UnitCastingInfo(unit)
		spellID = uciSpellID or spellID
		castID = uciCastID or castID
		
		if spellID and (not name or not texture or not text) then
			local siName, _, siIcon, siCastTime = GetSpellInfo(spellID)
			texture = texture or siIcon
			name = name or siName
			text = text or siName
			if not startTime then
				startTime = GetTime()
				endTime = startTime + siCastTime
			end
		end

		--is valid?
		if (not self:IsValid(unit, name, isTradeSkill, true)) then
			return
		end

		--empowered? no!
			self.holdAtMaxTime = nil
			self.empowered = false
			self.curStage = nil
			self.numStages = nil
			self.empStages = nil
			self:CreateOrUpdateEmpoweredPips()

		--setup cast
			self.casting = true
			self.channeling = nil
			self.interrupted = nil
			self.failed = nil
			self.finished = nil
			self.canInterrupt = not notInterruptible
			self.spellID = spellID
			self.castID = castID
			self.spellName = name
			self.spellTexture = texture
			self.spellStartTime = startTime / 1000
			self.spellEndTime = endTime / 1000
			self.value = GetTime() - self.spellStartTime
			self.maxValue = self.spellEndTime - self.spellStartTime

			self:SetMinMaxValues(0, self.maxValue)
			self:SetValue(self.value)
			self:SetAlpha(1)
			self.Icon:SetTexture(texture)
			self.Icon:Show()
			self.Text:SetText(text or name)

			if (self.Settings.ShowCastTime and self.Settings.CanLazyTick) then
				self.percentText:Show()
			end

			self.flashTexture:Hide()
			self:Animation_StopAllAnimations()

			self:SetAlpha(1)

			--set the statusbar color
			self:UpdateCastColor()

			if (not self:IsShown() and not self.Settings.NoFadeEffects) then
				self:Animation_FadeIn()
			end

			self.Spark:Show()
			self:Show()

		--update the interrupt cast border
		self:UpdateInterruptState()
	end,

	UNIT_SPELLCAST_START = function(self, unit, ...)
		self:UpdateCastingInfo(unit, ...)
		self:RunHooksForWidget("OnCastStart", self, self.unit, "UNIT_SPELLCAST_START")
	end,

	CreateOrUpdateEmpoweredPips = function(self, unit, numStages, startTime, endTime)
		unit = unit or self.unit
		numStages = numStages or self.numStages
		startTime = startTime or ((self.spellStartTime or 0) * 1000)
		endTime = endTime or ((self.spellEndTime or 0) * 1000)

		if not self.empStages or not numStages or numStages <= 0 then
			self.stagePips = self.stagePips or {}
			for i, stagePip in pairs(self.stagePips) do
				stagePip:Hide()
			end
			return
		end

		local width = self:GetWidth()
		local height = self:GetHeight()
		for i = 1, numStages, 1 do
			local curStartTime = self.empStages[i] and self.empStages[i].start
			local curEndTime = self.empStages[i] and self.empStages[i].finish
			local curDuration = curEndTime - curStartTime
			local offset = width * curEndTime / (endTime - startTime) * 1000
			if curDuration > -1 then
				local stagePip = self.stagePips[i]
				if not stagePip then
					stagePip = self:CreateTexture(nil, "overlay", nil, 2)
					stagePip:SetBlendMode("ADD")
					stagePip:SetTexture([[Interface\CastingBar\UI-CastingBar-Spark]])
					stagePip:SetTexCoord(11/32,18/32,9/32,23/32)
					stagePip:SetSize(2, height)
					--stagePip = CreateFrame("FRAME", nil, self, "CastingBarFrameStagePipTemplate")
					self.stagePips[i] = stagePip
				end

				stagePip:ClearAllPoints()
				--stagePip:SetPoint("TOP", self, "TOPLEFT", offset, -1)
				--stagePip:SetPoint("BOTTOM", self, "BOTTOMLEFT", offset, 1)
				--stagePip.BasePip:SetVertexColor(1, 1, 1, 1)
				stagePip:SetPoint("CENTER", self, "LEFT", offset, 0)
				stagePip:SetVertexColor(1, 1, 1, 1)
				stagePip:Show()
			end
		end
	end,

	UpdateChannelInfo = function(self, unit, ...)
		local unitID, castID, spellID = ...
		local name, text, texture, startTime, endTime, isTradeSkill, notInterruptible, uciSpellID, _, numStages = CastInfo.UnitChannelInfo (unit)
		spellID = uciSpellID or spellID
		castID = uciCastID or castID
		
		if spellID and (not name or not texture or not text) then
			local siName, _, siIcon, siCastTime = GetSpellInfo(spellID)
			texture = texture or siIcon
			name = name or siName
			text = text or siName
			if not startTime then
				startTime = GetTime()
				endTime = startTime + siCastTime
			end
		end

		--is valid?
		if (not self:IsValid (unit, name, isTradeSkill, true)) then
			return
		end

		--empowered?
			self.empStages = {}
			self.stagePips = self.stagePips or {}
			for i, stagePip in pairs(self.stagePips) do
				stagePip:Hide()
			end

			if numStages and numStages > 0 then
				self.holdAtMaxTime = GetUnitEmpowerHoldAtMaxTime(self.unit)
				self.empowered = true
				self.numStages = numStages

				local lastStageEndTime = 0
				for i = 1, numStages do
					self.empStages[i] = {
						start = lastStageEndTime,
						finish = lastStageEndTime + GetUnitEmpowerStageDuration(unit, i - 1) / 1000,
					}
					lastStageEndTime = self.empStages[i].finish

					if startTime / 1000 + lastStageEndTime <= GetTime() then
						self.curStage = i
					end
				end

				if (self.Settings.ShowEmpoweredDuration) then
					endTime = endTime + self.holdAtMaxTime
				end

				--create/update pips
				self:CreateOrUpdateEmpoweredPips(unit, numStages, startTime, endTime)
			else
				self.holdAtMaxTime = nil
				self.empowered = false
				self.curStage = nil
				self.numStages = nil
			end

		--setup cast
			self.casting = nil
			self.channeling = true
			self.interrupted = nil
			self.failed = nil
			self.finished = nil
			self.canInterrupt = not notInterruptible
			self.spellID = spellID
			self.castID = castID
			self.spellName = name
			self.spellTexture = texture
			self.spellStartTime = startTime / 1000
			self.spellEndTime = endTime / 1000
			self.value = self.empowered and (GetTime() - self.spellStartTime) or (self.spellEndTime - GetTime())
			self.maxValue = self.spellEndTime - self.spellStartTime
			self.reverseChanneling = self.empowered

			self:SetMinMaxValues(0, self.maxValue)
			self:SetValue(self.value)

			self:SetAlpha(1)
			self.Icon:SetTexture(texture)
			self.Icon:Show()
			self.Text:SetText(text)

			if (self.Settings.ShowCastTime and self.Settings.CanLazyTick) then
				self.percentText:Show()
			end

			self.flashTexture:Hide()
			self:Animation_StopAllAnimations()

			self:SetAlpha(1)

			--set the statusbar color
			self:UpdateCastColor()

			if (not self:IsShown() and not self.Settings.NoFadeEffects) then
				self:Animation_FadeIn()
			end

			self.Spark:Show()
			self:Show()

		--update the interrupt cast border
		self:UpdateInterruptState()

	end,

	UNIT_SPELLCAST_CHANNEL_START = function(self, unit, ...)
		self:UpdateChannelInfo(unit, ...)
		self:RunHooksForWidget("OnCastStart", self, self.unit, "UNIT_SPELLCAST_CHANNEL_START")
	end,

	UNIT_SPELLCAST_STOP = function(self, unit, ...)
		local unitID, castID, spellID = ...
		if (self.castID == castID) then
			if (self.interrupted) then
				if (self.Settings.HideSparkOnInterrupt) then
					self.Spark:Hide()
				end
			else
				self.Spark:Hide()
			end

			self.percentText:Hide()

			local value = self:GetValue()
			local _, maxValue = self:GetMinMaxValues()

			if (self.interrupted) then
				if (self.Settings.FillOnInterrupt) then
					self:SetValue(self.maxValue or maxValue or 1)
				end
			else
				self:SetValue(self.maxValue or maxValue or 1)
			end

			self.casting = nil
			self.channeling = nil
			self.finished = true
			self.castID = nil

			if (not self:HasScheduledHide()) then
				--check if settings has no fade option or if its parents are not visible
				if (not self:IsVisible()) then
					self:Hide()

				elseif (self.Settings.NoFadeEffects) then
					self:ScheduleToHide (0.3)

				else
					self:Animation_Flash()
					self:Animation_FadeOut()
				end
			end

			self:UpdateCastColor()
		end
	end,

	UNIT_SPELLCAST_CHANNEL_STOP = function(self, unit, ...)
		local unitID, castID, spellID = ...

		if (self.channeling and castID == self.castID) then
			self.Spark:Hide()
			self.percentText:Hide()

			local value = self:GetValue()
			local _, maxValue = self:GetMinMaxValues()
			self:SetValue(self.maxValue or maxValue or 1)

			self.casting = nil
			self.channeling = nil
			self.finished = true
			self.castID = nil

			if (not self:HasScheduledHide()) then
				--check if settings has no fade option or if its parents are not visible
				if (not self:IsVisible()) then
					self:Hide()

				elseif (self.Settings.NoFadeEffects) then
					self:ScheduleToHide (0.3)

				else
					self:Animation_Flash()
					self:Animation_FadeOut()
				end
			end

			self:UpdateCastColor()
		end
	end,

	UNIT_SPELLCAST_EMPOWER_START = function(self, unit, ...)
		self:UNIT_SPELLCAST_CHANNEL_START(unit, ...)
	end,

	UNIT_SPELLCAST_EMPOWER_UPDATE = function(self, unit, ...)
		self:UNIT_SPELLCAST_CHANNEL_UPDATE(unit, ...)
	end,

	UNIT_SPELLCAST_EMPOWER_STOP = function(self, unit, ...)
		self:UNIT_SPELLCAST_CHANNEL_STOP(unit, ...)
	end,

	UNIT_SPELLCAST_FAILED = function(self, unit, ...)
		local unitID, castID, spellID = ...

		if ((self.casting or self.channeling) and castID == self.castID and not self.fadeOut) then
			self.casting = nil
			self.channeling = nil
			self.failed = true
			self.finished = true
			self.castID = nil
			self:SetValue(self.maxValue or select(2, self:GetMinMaxValues()) or 1)

			--set the statusbar color
			self:UpdateCastColor()

			self.Spark:Hide()
			self.percentText:Hide()
			self.Text:SetText(FAILED) --auto locale within the global namespace

			self:ScheduleToHide (1)
		end
	end,

	UNIT_SPELLCAST_INTERRUPTED = function(self, unit, ...)
		local unitID, castID, spellID = ...

		if ((self.casting or self.channeling) and castID == self.castID and not self.fadeOut) then
			self.casting = nil
			self.channeling = nil
			self.interrupted = true
			self.finished = true
			self.castID = nil

			if (self.Settings.FillOnInterrupt) then
				self:SetValue(self.maxValue or select(2, self:GetMinMaxValues()) or 1)
			end

			if (self.Settings.HideSparkOnInterrupt) then
				self.Spark:Hide()
			end

			local castColor = self:GetCastColor()
			self:SetColor(castColor) --SetColor handles with ParseColors()

			self.percentText:Hide()
			self.Text:SetText(INTERRUPTED) --auto locale within the global namespace

			self:ScheduleToHide(1)
		end
	end,

	UNIT_SPELLCAST_DELAYED = function(self, unit, ...)
		local name, text, texture, startTime, endTime, isTradeSkill, castID, notInterruptible = CastInfo.UnitCastingInfo (unit)

		if (not self:IsValid (unit, name, isTradeSkill)) then
			return
		end

		--update the cast time
		self.spellStartTime = startTime / 1000
		self.spellEndTime = endTime / 1000
		self.value = GetTime() - self.spellStartTime
		self.maxValue = self.spellEndTime - self.spellStartTime
		self:SetMinMaxValues(0, self.maxValue)
	end,

	UNIT_SPELLCAST_CHANNEL_UPDATE = function(self, unit, ...)
		local name, text, texture, startTime, endTime, isTradeSkill, notInterruptible, spellID, _, numStages = CastInfo.UnitChannelInfo (unit)

		if (not self:IsValid(unit, name, isTradeSkill)) then
			return
		end

		--update the cast time
		self.spellStartTime = startTime / 1000
		self.spellEndTime = endTime / 1000
		self.value = self.empowered and (GetTime() - self.spellStartTime) or (self.spellEndTime - GetTime())
		self.maxValue = self.spellEndTime - self.spellStartTime

		if (self.value < 0 or self.value > self.maxValue) then
			self.value = 0
		end

		self:SetMinMaxValues(0, self.maxValue)
		self:SetValue(self.value)
	end,

	--cast changed its state to interruptable
	UNIT_SPELLCAST_INTERRUPTIBLE = function(self, unit, ...)
		self.canInterrupt = true
		self:UpdateCastColor()
		self:UpdateInterruptState()
	end,

	--cast changed its state to non interruptable
	UNIT_SPELLCAST_NOT_INTERRUPTIBLE = function(self, unit, ...)
		self.canInterrupt = false
		self:UpdateCastColor()
		self:UpdateInterruptState()
	end,
}

detailsFramework:Mixin(detailsFramework.CastFrameFunctions, detailsFramework.ScriptHookMixin)

-- ~castbar

---create a castbar widget
---@param parent frame
---@param name string?
---@param settingsOverride table? a table with key/value pairs to override the default settings
---@return df_castbar
function detailsFramework:CreateCastBar(parent, name, settingsOverride)
	assert(name or parent:GetName(), "DetailsFramework:CreateCastBar parameter 'name' omitted and parent has no name.")

	local castBar = CreateFrame("StatusBar", name or (parent:GetName() .. "CastBar"), parent, "BackdropTemplate")

		do --layers

			--these widgets was been made with back compatibility in mind
			--they are using the same names as the retail game uses on the nameplate castbar
			--this should make Plater core and Plater scripts made by users compatible with the new unit frame made on the framework

			--background
			castBar.background = castBar:CreateTexture(nil, "background", nil, -6)
			castBar.extraBackground = castBar:CreateTexture(nil, "background", nil, -5)

			--overlay
			castBar.Text = castBar:CreateFontString(nil, "overlay", "SystemFont_Shadow_Small")
			castBar.Text:SetDrawLayer("overlay", 1)
			castBar.Text:SetPoint("center", 0, 0)

			castBar.BorderShield = castBar:CreateTexture(nil, "overlay", nil, 5)
			castBar.BorderShield:Hide()

			castBar.Icon = castBar:CreateTexture(nil, "overlay", nil, 4)
			castBar.Icon:Hide()

			castBar.Spark = castBar:CreateTexture(nil, "overlay", nil, 3)
			castBar.Spark:SetBlendMode("ADD")

			--time left on the cast
			castBar.percentText = castBar:CreateFontString(nil, "overlay", "SystemFont_Shadow_Small")
			castBar.percentText:SetDrawLayer("overlay", 7)

			--statusbar texture
			castBar.barTexture = castBar:CreateTexture(nil, "artwork", nil, -6)
			castBar:SetStatusBarTexture(castBar.barTexture)

			--animations fade in and out
			local fadeOutAnimationHub = detailsFramework:CreateAnimationHub(castBar, detailsFramework.CastFrameFunctions.Animation_FadeOutStarted, detailsFramework.CastFrameFunctions.Animation_FadeOutFinished)
			fadeOutAnimationHub.alpha1 = detailsFramework:CreateAnimation(fadeOutAnimationHub, "ALPHA", 1, 1, 1, 0)
			castBar.fadeOutAnimation = fadeOutAnimationHub

			local fadeInAnimationHub = detailsFramework:CreateAnimationHub(castBar, detailsFramework.CastFrameFunctions.Animation_FadeInStarted, detailsFramework.CastFrameFunctions.Animation_FadeInFinished)
			fadeInAnimationHub.alpha1 = detailsFramework:CreateAnimation(fadeInAnimationHub, "ALPHA", 1, 0.150, 0, 1)
			castBar.fadeInAnimation = fadeInAnimationHub

			--animatios flash
			local flashTexture = castBar:CreateTexture(nil, "overlay", nil, 7)
			flashTexture:SetTexture(1, 1, 1, 1)
			flashTexture:SetAllPoints()
			flashTexture:SetAlpha(0)
			flashTexture:Hide()
			flashTexture:SetBlendMode("ADD")
			castBar.flashTexture = flashTexture

			local flashAnimationHub = detailsFramework:CreateAnimationHub(flashTexture, function() flashTexture:Show() end, function() flashTexture:Hide() end)
			detailsFramework:CreateAnimation(flashAnimationHub, "ALPHA", 1, 0.2, 0, 0.8)
			detailsFramework:CreateAnimation(flashAnimationHub, "ALPHA", 2, 0.2, 1, 0)
			castBar.flashAnimation = flashAnimationHub
		end

	--mixins
	detailsFramework:Mixin(castBar, detailsFramework.CastFrameFunctions)
	detailsFramework:Mixin(castBar, detailsFramework.StatusBarFunctions)

	castBar:SetTexture([[Interface\WorldStateFrame\WORLDSTATEFINALSCORE-HIGHLIGHT]])

	--settings and hooks
	local settings = detailsFramework.table.copy({}, detailsFramework.CastFrameFunctions.Settings)
	if (settingsOverride) then
		detailsFramework.table.copy(settings, settingsOverride)
	end
	castBar.Settings = settings

	local hookList = detailsFramework.table.copy({}, detailsFramework.CastFrameFunctions.HookList)
	castBar.HookList = hookList

	--initialize the cast bar
	castBar:Initialize()

	return castBar
end

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--unit frame

--[=[
	DF:CreateUnitFrame(parent, name, settingsOverride)
	creates a very basic unit frame with a healthbar, castbar and power bar
	each unit frame has a .Settings table which isn't shared among other unit frames created with this method
	all members names are the same as the unit frame from the retail game

	@parent = frame to pass for the CreateFrame function
	@name = absolute name of the frame, if omitted a random name is created
	@settingsOverride = table with keys and values to replace the defaults from the framework

--]=]


------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--unit frame

	--return true if the unit has been claimed by another player (health bar is gray)
	local isUnitTapDenied = function(unit)
		return unit and not UnitPlayerControlled(unit) and UnitIsTapDenied(unit)
	end

	---@class df_unitframesettings : table
	---@field ClearUnitOnHide boolean true
	---@field ShowCastBar boolean true
	---@field ShowPowerBar boolean true
	---@field ShowUnitName boolean true
	---@field ShowBorder boolean true
	---@field CanModifyHealhBarColor boolean true
	---@field ColorByAggro boolean false
	---@field FixedHealthColor boolean false
	---@field UseFriendlyClassColor boolean true
	---@field UseEnemyClassColor boolean true
	---@field ShowTargetOverlay boolean true
	---@field BorderColor table
	---@field CanTick boolean
	---@field Width number
	---@field Height number
	---@field PowerBarHeight number
	---@field CastBarHeight number

	---@class df_unitframemixin
	---@field WidgetType string
	---@field Settings df_unitframesettings
	---@field SetHealthBarColor fun(self:df_unitframe, r:number, g:number?, b:number?, a:number?)
	---@field SetUnit fun(self:df_unitframe, unit:string?) sets the unit to be shown in the unit frame
	---@field OnTick fun(self:df_unitframe, deltaTime:number?) if CanTick is true, this function will be called every frame

	detailsFramework.UnitFrameFunctions = {
		WidgetType = "unitFrame",

		Settings = {
			--unit frames
			ClearUnitOnHide = true, --if tue it'll set the unit to nil when the unit frame is set to hide
			ShowCastBar = true, --if this is false, the cast bar for the unit won't be shown
			ShowPowerBar = true, --if true it'll show the power bar for the unit, e.g. the mana bar
			ShowUnitName = true, --if false, the unit name won't show
			ShowBorder = true, --if false won't show the border frame

			--health bar color
			CanModifyHealhBarColor = true, --if false it won't change the color of the health bar
			ColorByAggro = false, --if true it'll color the healthbar with red color when the unit has aggro on player
			FixedHealthColor = false, --color override with a table {r=1, g=1, b=1}
			UseFriendlyClassColor = true, --make the healthbar class color for friendly players
			UseEnemyClassColor = true, --make the healthbar class color for enemy players

			--misc
			ShowTargetOverlay = true, --shows a highlighht for the player current target
			BorderColor = detailsFramework:CreateColorTable(0, 0, 0, 1), --border color, set to alpha zero for no border
			CanTick = false, --if true it'll run the OnTick event

			--size
			Width = 100,
			Height = 20,
			PowerBarHeight = 4,
			CastBarHeight = 8,
		},

		UnitFrameEvents = {
			--run for all units
			{"PLAYER_ENTERING_WORLD"},
			{"PARTY_MEMBER_DISABLE"},
			{"PARTY_MEMBER_ENABLE"},
			{"PLAYER_TARGET_CHANGED"},

			--run for one unit
			{"UNIT_NAME_UPDATE", true},
			{"UNIT_CONNECTION", true},
			{"UNIT_ENTERED_VEHICLE", true},
			{"UNIT_EXITED_VEHICLE", true},
			{"UNIT_PET", true},
			{"UNIT_THREAT_LIST_UPDATE", true},
		},

		--used when a event is triggered to quickly check if is a unit event
		IsUnitEvent = {
			["UNIT_NAME_UPDATE"] = true,
			["UNIT_CONNECTION"] = true,
			["UNIT_ENTERED_VEHICLE"] = true,
			["UNIT_EXITED_VEHICLE"] = true,
			["UNIT_PET"] = true,
			["UNIT_THREAT_LIST_UPDATE"] = true,
		},

		Initialize = function(self)
			self.border:SetBorderColor(self.Settings.BorderColor)

			PixelUtil.SetWidth(self, self.Settings.Width, 1)
			PixelUtil.SetHeight(self, self.Settings.Height, 1)

			PixelUtil.SetPoint(self.powerBar, "bottomleft", self, "bottomleft", 0, 0, 1, 1)
			PixelUtil.SetPoint(self.powerBar, "bottomright", self, "bottomright", 0, 0, 1, 1)
			PixelUtil.SetHeight(self.powerBar, self.Settings.PowerBarHeight, 1)

			--make the castbar overlap the powerbar
			PixelUtil.SetPoint(self.castBar, "bottomleft", self, "bottomleft", 0, 0, 1, 1)
			PixelUtil.SetPoint(self.castBar, "bottomright", self, "bottomright", 0, 0, 1, 1)
			PixelUtil.SetHeight(self.castBar, self.Settings.CastBarHeight, 1)
		end,

		SetHealthBarColor = function(self, r, g, b, a)
			self.healthBar:SetColor(r, g, b, a)
		end,

		--register all events which will be used by the unit frame
		RegisterEvents = function(self)
			--register events
			for index, eventTable in ipairs(self.UnitFrameEvents) do
				local event, isUnitEvent = unpack(eventTable)
				if (not isUnitEvent) then
					self:RegisterEvent(event)
				else
					self:RegisterUnitEvent (event, self.unit, self.displayedUnit ~= unit and self.displayedUnit or nil)
				end
			end

			--check settings and unregister events for disabled features
			if (not self.Settings.ColorByAggro) then
				self:UnregisterEvent ("UNIT_THREAT_LIST_UPDATE")
			end

			--set scripts
			self:SetScript("OnEvent", self.OnEvent)
			self:SetScript("OnHide", self.OnHide)

			if (self.Settings.CanTick) then
				self:SetScript("OnUpdate", self.OnTick)
			end
		end,

		--unregister events, called when this unit frame losses its unit
		UnregisterEvents = function(self)
			for index, eventTable in ipairs(self.UnitFrameEvents) do
				local event, firstUnit, secondUnit = unpack(eventTable)
				self:UnregisterEvent(event)
			end

			self:SetScript("OnEvent", nil)
			self:SetScript("OnUpdate", nil)
			self:SetScript("OnHide", nil)
		end,

		--call every tick
		OnTick = function(self, deltaTime) end, --if overrided, set 'CanTick' to true on the settings table

		--when an event happen for this unit, send it to the apropriate function
		OnEvent = function(self, event, ...)
			--run the function for this event
			local eventFunc = self[event]
			if (eventFunc) then
				--is this event an unit event?
				if (self.IsUnitEvent[event]) then
					local unit = ...
					--check if is for this unit (even if the event is registered only for the unit)
					if (unit == self.unit or unit == self.displayedUnit) then
						eventFunc(self, ...)
					end
				else
					eventFunc(self, ...)
				end
			end
		end,

		OnHide = function(self)
			if (self.Settings.ClearUnitOnHide) then
				self:SetUnit(nil)
			end
		end,

		--run if the unit currently shown is different than the new one
		SetUnit = function(self, unit)
			if (unit ~= self.unit or unit == nil) then
				self.unit = unit --absolute unit
				self.displayedUnit = unit --~todo rename to 'displayedUnit' for back compatibility with older scripts in Plater
				self.unitInVehicle = nil --true when the unit is in a vehicle

				if (unit) then
					self:RegisterEvents()

					self.guid = UnitGUID(unit)
					self.class = select(2, UnitClass(unit))
					self.name = UnitName(unit)

					self.healthBar:SetUnit(unit, self.displayedUnit)

					--is using castbars?
					if (self.Settings.ShowCastBar) then
						self.castBar:SetUnit(unit, self.displayedUnit)
					else
						self.castBar:SetUnit(nil)
					end

					--is using powerbars?
					if (self.Settings.ShowPowerBar) then
						self.powerBar:SetUnit(unit, self.displayedUnit)
					else
						self.powerBar:SetUnit(nil)
					end

					--is using the border?
					if (self.Settings.ShowBorder) then
						self.border:Show()
					else
						self.border:Hide()
					end

					if (not self.Settings.ShowUnitName) then
						self.unitName:Hide()
					end
				else
					self:UnregisterEvents()
					self.healthBar:SetUnit(nil)
					self.castBar:SetUnit(nil)
					self.powerBar:SetUnit(nil)
				end

				self:UpdateUnitFrame()
			end
		end,

		--if the unit is controlling a vehicle, need to show the vehicle instead
		--.unit and .displayedUnit is always the same execept when the unit is controlling a vehicle, then .displayedUnit is the unitID for the vehicle
		--todo: see what 'UnitTargetsVehicleInRaidUI' is, there's a call for this in the CompactUnitFrame.lua but zero documentation
		CheckVehiclePossession = function(self)
			--this unit is possessing a vehicle?
			local unitPossessVehicle = UnitHasVehicleUI(self.unit) or false
			if (unitPossessVehicle) then
				if (not self.unitInVehicle) then
					if (UnitIsUnit("player", self.unit)) then
						self.displayedUnit = "vehicle"
						self.unitInVehicle = true
						self:RegisterEvents()
						self:UpdateAllWidgets()
						return true
					end

					local prefix, id, suffix = string.match(self.unit, "([^%d]+)([%d]*)(.*)") --CompactUnitFrame.lua
					local vehicleUnitID = prefix .. "pet" .. id .. suffix
					if (UnitExists(vehicleUnitID)) then
						self.displayedUnit = vehicleUnitID
						self.unitInVehicle = true
						self:RegisterEvents()
						self:UpdateAllWidgets()
						return true
					end
				end
			end

			if (self.unitInVehicle) then
				self.displayedUnit = self.unit
				self.unitInVehicle = nil
				self:RegisterEvents()
				self:UpdateAllWidgets()
			end
		end,

		--find a color for the health bar, if a color has been passed in the arguments use it instead, 'CanModifyHealhBarColor' must be true for this function run
		UpdateHealthColor = function(self, r, g, b)
			--check if color changes is disabled
			if (not self.Settings.CanModifyHealhBarColor) then
				return
			end

			local unit = self.displayedUnit

			--check if a color has been passed within the parameters
			if (r) then
				--check if passed a special color
				if (type(r) ~= "number") then
					r, g, b = detailsFramework:ParseColors(r)
				end

				self:SetHealthBarColor(r, g, b)
				return
			end

			--check if there is a color override in the settings
			if (self.Settings.FixedHealthColor) then
				local FixedHealthColor = self.Settings.FixedHealthColor
				r, g, b = FixedHealthColor.r, FixedHealthColor.g, FixedHealthColor.b
				self:SetHealthBarColor(r, g, b)
				return
			end

			--check if the unit is a player
			if (UnitIsPlayer(unit)) then
				--check if the unit is disconnected (in case it is a player
				if (not UnitIsConnected(unit)) then
					self:SetHealthBarColor(.5, .5, .5)
					return
				end

				--is a friendly or enemy player?
				if (UnitIsFriend ("player", unit)) then
					if (self.Settings.UseFriendlyClassColor) then
						local _, className = UnitClass(unit)
						if (className) then
							local classColor = RAID_CLASS_COLORS[className]
							if (classColor) then
								self:SetHealthBarColor(classColor.r, classColor.g, classColor.b)
								return
							end
						end
					else
						self:SetHealthBarColor(0, 1, 0)
						return
					end
				else
					if (self.Settings.UseEnemyClassColor) then
						local _, className = UnitClass(unit)
						if (className) then
							local classColor = RAID_CLASS_COLORS[className]
							if (classColor) then
								self:SetHealthBarColor(classColor.r, classColor.g, classColor.b)
								return
							end
						end
					else
						self:SetHealthBarColor(1, 0, 0)
						return
					end
				end
			end

			--is tapped?
			if (isUnitTapDenied(unit)) then
				self:SetHealthBarColor(.6, .6, .6)
				return
			end

			--is this is a npc attacking the player?
			if (self.Settings.ColorByAggro) then
				local _, threatStatus = UnitDetailedThreatSituation("player", unit)
				if (threatStatus) then
					self:SetHealthBarColor(1, 0, 0)
					return
				end
			end

			-- get the regular color by selection
			r, g, b = UnitSelectionColor(unit)
			self:SetHealthBarColor (r, g, b)
		end,

		--misc
		UpdateName = function(self)
			if (not self.Settings.ShowUnitName) then
				return
			end

			--unit name without realm names by default
			local name = UnitName(self.unit)
			self.unitName:SetText(name)
			self.unitName:Show()
		end,

		--this runs when the player it self changes its target, need to update the current target overlay
		--todo: add focus overlay
		UpdateTargetOverlay = function(self)
			if (not self.Settings.ShowTargetOverlay) then
				self.targetOverlay:Hide()
				return
			end

			if (UnitIsUnit(self.displayedUnit, "target")) then
				self.targetOverlay:Show()
			else
				self.targetOverlay:Hide()
			end
		end,

		UpdateAllWidgets = function(self)
			if (UnitExists(self.displayedUnit)) then
				local unit = self.unit
				local displayedUnit = self.displayedUnit

				self:SetUnit(unit, displayedUnit)

				--is using castbars?
				if (self.Settings.ShowCastBar) then
					self.castBar:SetUnit(unit, displayedUnit)
				end

				--is using powerbars?
				if (self.Settings.ShowPowerBar) then
					self.powerBar:SetUnit(unit, displayedUnit)
				end

				self:UpdateName()
				self:UpdateTargetOverlay()
				self:UpdateHealthColor()
			end
		end,

		--update the unit frame and its widgets
		UpdateUnitFrame = function(self)
			local unitInVehicle = self:CheckVehiclePossession()

			--if the unit is inside a vehicle, the vehicle possession function will call an update on all widgets
			if (not unitInVehicle) then
				self:UpdateAllWidgets()
			end
		end,

		--event handles
		PLAYER_ENTERING_WORLD = function(self, ...)
			self:UpdateUnitFrame()
		end,

		--update overlays when the player changes its target
		PLAYER_TARGET_CHANGED = function(self, ...)
			self:UpdateTargetOverlay()
		end,

		--unit received a name update
		UNIT_NAME_UPDATE = function(self, ...)
			self:UpdateName()
		end,

		--this is registered only if .settings.ColorByAggro is true
		UNIT_THREAT_LIST_UPDATE = function(self, ...)
			if (self.Settings.ColorByAggro) then
				self:UpdateHealthColor()
			end
		end,

		--vehicle
		UNIT_ENTERED_VEHICLE = function(self, ...)
			self:UpdateUnitFrame()
		end,
		UNIT_EXITED_VEHICLE = function(self, ...)
			self:UpdateUnitFrame()
		end,

		--pet
		UNIT_PET = function(self, ...)
			self:UpdateUnitFrame()
		end,

		--player connection
		UNIT_CONNECTION = function(self, ...)
			if (UnitIsConnected (self.unit)) then
				self:UpdateUnitFrame()
			end
		end,

		PARTY_MEMBER_ENABLE = function(self, ...)
			if (UnitIsConnected(self.unit)) then
				self:UpdateName()
			end
		end,
	}

---@class df_unitframe : button, df_unitframemixin
---@field unit string
---@field displayedUnit string
---@field guid guid
---@field class class
---@field name actorname
---@field unitInVehicle boolean
---@field border frame
---@field overlayFrame frame
---@field unitName fontstring
---@field healthBar df_healthbar
---@field castBar df_castbar
---@field powerBar df_powerbar
---@field targetOverlay texture
---@field Settings df_unitframesettings

local globalBaseFrameLevel = 1 -- to be increased + used across each new plate

-- ~unitframe
---create a unit frame with a health bar, cast bar and power bar
---@param parent frame
---@param name string?
---@param unitFrameSettingsOverride table?
---@param healthBarSettingsOverride table?
---@param castBarSettingsOverride table?
---@param powerBarSettingsOverride table?
---@return df_unitframe
function detailsFramework:CreateUnitFrame(parent, name, unitFrameSettingsOverride, healthBarSettingsOverride, castBarSettingsOverride, powerBarSettingsOverride)
	local parentName = name or ("DetailsFrameworkUnitFrame" .. tostring(math.random(1, 100000000)))

	--create the main unit frame
	local mewUnitFrame = CreateFrame("button", parentName, parent, "BackdropTemplate")

	--base level
	--local baseFrameLevel = f:GetFrameLevel()
	local baseFrameLevel = globalBaseFrameLevel
	globalBaseFrameLevel = globalBaseFrameLevel + 10

	mewUnitFrame:SetFrameLevel(baseFrameLevel)

	--create the healthBar
	local healthBar = detailsFramework:CreateHealthBar(mewUnitFrame, nil, healthBarSettingsOverride)
	healthBar:SetFrameLevel(baseFrameLevel + 1)
	mewUnitFrame.healthBar = healthBar

	--create the power bar
	local powerBar = detailsFramework:CreatePowerBar(mewUnitFrame, nil, powerBarSettingsOverride)
	powerBar:SetFrameLevel(baseFrameLevel + 2)
	mewUnitFrame.powerBar = powerBar

	--create the castBar
	local castBar = detailsFramework:CreateCastBar(mewUnitFrame, nil, castBarSettingsOverride)
	castBar:SetFrameLevel(baseFrameLevel + 3)
	mewUnitFrame.castBar = castBar

	--border frame
	local borderFrame = detailsFramework:CreateBorderFrame(mewUnitFrame, mewUnitFrame:GetName() .. "Border")
	borderFrame:SetFrameLevel(mewUnitFrame:GetFrameLevel() + 5)
	mewUnitFrame.border = borderFrame

	--overlay frame (widgets that need to stay above the unit frame)
	local overlayFrame = CreateFrame("frame", "$parentOverlayFrame", mewUnitFrame, "BackdropTemplate")
	overlayFrame:SetFrameLevel(mewUnitFrame:GetFrameLevel() + 6)
	mewUnitFrame.overlayFrame = overlayFrame

	--unit frame layers
		do
			--artwork
			mewUnitFrame.unitName = mewUnitFrame:CreateFontString(nil, "artwork", "GameFontHighlightSmall")
			PixelUtil.SetPoint(mewUnitFrame.unitName, "topleft", healthBar, "topleft", 2, -2, 1, 1)

			--target overlay - it's parented in the healthbar so other widgets won't get the overlay
			mewUnitFrame.targetOverlay = overlayFrame:CreateTexture(nil, "artwork")
			mewUnitFrame.targetOverlay:SetTexture(healthBar:GetTexture())
			mewUnitFrame.targetOverlay:SetBlendMode("ADD")
			mewUnitFrame.targetOverlay:SetAlpha(.5)
			mewUnitFrame.targetOverlay:SetAllPoints(healthBar)
		end

	--mixins
		--inject mixins
		detailsFramework:Mixin(mewUnitFrame, detailsFramework.UnitFrameFunctions)

		--create the settings table and copy the overrides into it, the table is set into the frame after the mixin
		local unitFrameSettings = detailsFramework.table.copy({}, detailsFramework.UnitFrameFunctions.Settings)
		if (unitFrameSettingsOverride) then
			unitFrameSettings = detailsFramework.table.copy(unitFrameSettings, unitFrameSettingsOverride)
		end
		mewUnitFrame.Settings = unitFrameSettings

	--initialize scripts
		--unitframe
		mewUnitFrame:Initialize()

	return mewUnitFrame
end