

local dversion = 612
local major, minor = "DetailsFramework-1.0", dversion
local DF, oldminor = LibStub:NewLibrary(major, minor)

if (not DF) then
	DetailsFrameworkCanLoad = false
	return
end

_G["DetailsFramework"] = DF

---@cast DF detailsframework

local detailsFramework = DF

--store functions to call when the PLAYER_LOGIN event is triggered
detailsFramework.OnLoginSchedules = {}
local dfFrame = CreateFrame("frame")
dfFrame:RegisterEvent("PLAYER_LOGIN")
dfFrame:SetScript("OnEvent", function(self, event, ...)
	if (event == "PLAYER_LOGIN") then
		C_Timer.After(0, function()
			for _, func in ipairs(detailsFramework.OnLoginSchedules) do
				func()
			end
		end)
	end
end)

DetailsFrameworkCanLoad = true
local SharedMedia = LibStub:GetLibrary("LibSharedMedia-3.0")

local _
local type = type
local unpack = unpack

local IS_WOW_PROJECT_MAINLINE = WOW_PROJECT_ID == WOW_PROJECT_MAINLINE
local IS_WOW_PROJECT_NOT_MAINLINE = WOW_PROJECT_ID ~= WOW_PROJECT_MAINLINE

local UnitPlayerControlled = UnitPlayerControlled
local UnitIsTapDenied = UnitIsTapDenied

-- TWW compatibility:
local GetSpellInfo = GetSpellInfo or function(spellID) if not spellID then return nil end local si = C_Spell.GetSpellInfo(spellID) if si then return si.name, nil, si.iconID, si.castTime, si.minRange, si.maxRange, si.spellID, si.originalIconID end end
local GetSpellBookItemName = GetSpellBookItemName or C_SpellBook.GetSpellBookItemName
local GetNumSpellTabs = GetNumSpellTabs or C_SpellBook.GetNumSpellBookSkillLines
local GetSpellTabInfo = GetSpellTabInfo or function(tabLine) local skillLine = C_SpellBook.GetSpellBookSkillLineInfo(tabLine) if skillLine then return skillLine.name, skillLine.iconID, skillLine.itemIndexOffset, skillLine.numSpellBookItems, skillLine.isGuild, skillLine.offSpecID end end
local SpellBookItemTypeMap = Enum.SpellBookItemType and {[Enum.SpellBookItemType.Spell] = "SPELL", [Enum.SpellBookItemType.None] = "NONE", [Enum.SpellBookItemType.Flyout] = "FLYOUT", [Enum.SpellBookItemType.FutureSpell] = "FUTURESPELL", [Enum.SpellBookItemType.PetAction] = "PETACTION" } or {}
local GetSpellBookItemInfo = GetSpellBookItemInfo or function(...) local si = C_SpellBook.GetSpellBookItemInfo(...) if si then return SpellBookItemTypeMap[si.itemType] or "NONE", (si.itemType == Enum.SpellBookItemType.Flyout or si.itemType == Enum.SpellBookItemType.PetAction) and si.actionID or si.spellID or si.actionID, si end end
local SPELLBOOK_BANK_PLAYER = Enum.SpellBookSpellBank and Enum.SpellBookSpellBank.Player or "player"
local SPELLBOOK_BANK_PET = Enum.SpellBookSpellBank and Enum.SpellBookSpellBank.Pet or "pet"
local IsPassiveSpell = IsPassiveSpell or C_Spell.IsSpellPassive
local GetOverrideSpell = C_SpellBook and C_SpellBook.GetOverrideSpell or C_Spell.GetOverrideSpell or GetOverrideSpell
local HasPetSpells = HasPetSpells or C_SpellBook.HasPetSpells
local GetSpecialization = GetSpecialization or C_SpecializationInfo.GetSpecialization
local GetSpecializationInfo = GetSpecializationInfo or C_SpecializationInfo.GetSpecializationInfo
local spellBookPetEnum = Enum.SpellBookSpellBank and Enum.SpellBookSpellBank.Pet or "pet"

SMALL_NUMBER = 0.000001
ALPHA_BLEND_AMOUNT = 0.8400251

--cache this stuff
local g, b, d, t = GetBuildInfo()
DF.BuildYear = tonumber(d:match("%d+$") or 0)
DF.GamePatch = g --string "10.2.7"
DF.BuildId = b --string "55000"
DF.Toc = t --number 100000
DF.Exp = floor(DF.Toc/10000)

local buildInfo = DF.Toc

DF.dversion = dversion

DF.AuthorInfo = {
	Author = "",
	Name = "Terciob", --terciob
	Discord = "https://discord.gg/AGSzAZX",
	Support = "www.patreon.com",
	SearchVideos = "www.youtube.com",
}

function DF:Msg(msg, ...)
	print("|cFFFFFFAA" .. (self.__name or "Details!Framework:") .. "|r ", msg, ...)
end

function DF:MsgWarning(msg, ...)
	print("|cFFFFFFAA" .. (self.__name or "Details!Framework") .. "|r |cFFFFAA00[Warning]|r", msg, ...)
end

DF.DefaultRoundedCornerPreset = {
	roundness = 6,
	color = {.1, .1, .1, 0.98},
	border_color = {.05, .05, .05, 0.834},
}

DF.internalFunctions = DF.internalFunctions or {}

local PixelUtil = PixelUtil or DFPixelUtil
if (not PixelUtil) then
	--check if is in classic, TBC, or WotLK wow, if it is, build a replacement for PixelUtil
	local gameVersion = GetBuildInfo()
	if (gameVersion:match("%d") == "1" or gameVersion:match("%d") == "2" or gameVersion:match("%d") == "3") then
		PixelUtil = {
			SetWidth = function(self, width) self:SetWidth(width) end,
			SetHeight = function(self, height) self:SetHeight(height) end,
			SetSize = function(self, width, height) self:SetSize(width, height) end,
			SetPoint = function(self, ...) self:SetPoint(...) end,
		}
	end
end

---return r, g, b, a for the default backdrop color used in addons
---@return number
---@return number
---@return number
---@return number
function DF:GetDefaultBackdropColor()
	return 0.1215, 0.1176, 0.1294, 0.8
end

---return if the wow version the player is playing is dragonflight
---@return boolean
function DF.IsDragonflight()
	if (buildInfo < 110000 and buildInfo >= 100000) then return true end
	return false
end

---return if the wow version the player is playing is dragonflight or an expansion after it
---@return boolean
function DF.IsDragonflightAndBeyond()
	return buildInfo >= 100000
end

function DF.ExpansionHasEvoker()
	return buildInfo >= 100000
end

---return true if the wow version is Dragonflight or below
---@return boolean
function DF.IsDragonflightOrBelow()
	return buildInfo < 110000
end

---return if the wow version the player is playing is a classic version of wow
---@return boolean
function DF.IsTimewalkWoW()
    if (buildInfo < 60000) then        return true    end
	return false
end

---return if the wow version the player is playing is the vanilla version of wow
---@return boolean
function DF.IsClassicWow()
    if (buildInfo < 20000) then        return true    end
	return false
end

---return true if the player is playing in the TBC version of wow
---@return boolean
function DF.IsTBCWow()
    if (buildInfo < 30000 and buildInfo >= 20000) then        return true    end
	return false
end

---return true if the player is playing in the WotLK version of wow
---@return boolean
function DF.IsWotLKWow()
    if (buildInfo < 40000 and buildInfo >= 30000) then        return true    end
	return false
end

---return true if the player is playing in the Cataclysm version of wow
---@return boolean
function DF.IsCataWow()
    if (buildInfo < 50000 and buildInfo >= 40000) then        return true    end
	return false
end

---return true if the player is playing in the Mists version of wow
---@return boolean
function DF.IsPandaWow()
    if (buildInfo < 60000 and buildInfo >= 50000) then        return true    end
	return false
end

---return true if the player is playing in the Warlords of Draenor version of wow
---@return boolean
function DF.IsWarlordsWow()
    if (buildInfo < 70000 and buildInfo >= 60000) then        return true    end
	return false
end

---return true if the player is playing in the Legion version of wow
---@return boolean
function DF.IsLegionWow()
	if (buildInfo < 80000 and buildInfo >= 70000) then		return true	end
	return false
end

---return true if the player is playing in the BFA version of wow
---@return boolean
function DF.IsBFAWow()
	if (buildInfo < 90000 and buildInfo >= 80000) then		return true	end
	return false
end

---return true if the player is playing in the Shadowlands version of wow
---@return boolean
function DF.IsShadowlandsWow()
	if (buildInfo < 100000 and buildInfo >= 90000) then		return true	end
	return false
end

---return if the wow version the player is playing is dragonflight
---@return boolean
function DF.IsDragonflightWow()
	if (buildInfo < 110000 and buildInfo >= 100000) then		return true	end
	return false
end

---return if the wow version the player is playing is the war within
---@return boolean
function DF.IsWarWow()
	if (buildInfo < 120000 and buildInfo >= 110000) then		return true	end
	return false
end

function DF.IsTWWWow()
	return DF.IsWarWow()
end

---return true if the player is playing in the WotLK version of wow with the retail api
---@return boolean
function DF.IsNonRetailWowWithRetailAPI()
    local _, _, _, buildInfo = GetBuildInfo()
    if (buildInfo < 60000 and buildInfo >= 30401) or (buildInfo < 20000 and buildInfo >= 11404) then
        return true
    end
	return false
end
DF.IsWotLKWowWithRetailAPI = DF.IsNonRetailWowWithRetailAPI -- this is still in use

function DF.ExpansionHasAugEvoker()
	return DF.IsDragonflightWow() or DF.IsWarWow()
end

---for classic wow, get the role using the texture from the talents frame
local roleBySpecTextureName = {
	DruidBalance = "DAMAGER",
	DruidFeralCombat = "DAMAGER",
	DruidRestoration = "HEALER",

	HunterBeastMastery = "DAMAGER",
	HunterMarksmanship = "DAMAGER",
	HunterSurvival = "DAMAGER",

	MageArcane = "DAMAGER",
	MageFrost = "DAMAGER",
	MageFire = "DAMAGER",

	PaladinCombat = "DAMAGER",
	PaladinHoly = "HEALER",
	PaladinProtection = "TANK",

	PriestHoly = "HEALER",
	PriestDiscipline = "HEALER",
	PriestShadow = "DAMAGER",

	RogueAssassination = "DAMAGER",
	RogueCombat = "DAMAGER",
	RogueSubtlety = "DAMAGER",

	ShamanElementalCombat = "DAMAGER",
	ShamanEnhancement = "DAMAGER",
	ShamanRestoration = "HEALER",

	WarlockCurses = "DAMAGER",
	WarlockDestruction = "DAMAGER",
	WarlockSummoning = "DAMAGER",

	WarriorArm = "DAMAGER",
	WarriorArms = "DAMAGER",
	WarriorFury = "DAMAGER",
	WarriorProtection = "TANK",

	DeathKnightBlood = "TANK",
	DeathKnightFrost = "DAMAGER",
	DeathKnightUnholy = "DAMAGER",
}

---classic, tbc and wotlk role guesser based on the weights of each talent tree
---@return string
function DF:GetRoleByClassicTalentTree()
	if (not DF.IsTimewalkWoW()) then
		return "NONE"
	end

	--amount of tabs existing
	local numTabs = GetNumTalentTabs() or 3

	--store the background textures for each tab
	local pointsPerSpec = {}

	for i = 1, (MAX_TALENT_TABS or 3) do
		if (i <= numTabs) then
			--tab information
			local id, name, description, iconTexture, pointsSpent, fileName = GetTalentTabInfo(i)
			if DF.IsClassicWow() and not fileName then
				--On pre 1.15.3
                name, iconTexture, pointsSpent, fileName = id, name, description, iconTexture
			end
			if (name) then
				table.insert(pointsPerSpec, {name, pointsSpent, fileName})
			end
		end
	end

	local MIN_SPECS = 4

	--put the spec with more talent point to the top
	table.sort(pointsPerSpec, function(t1, t2) return t1[2] > t2[2] end)

	--get the spec with more points spent
	local spec = pointsPerSpec[1]
	if (spec and spec[2] >= MIN_SPECS) then
		local specName = spec[1]
		local spentPoints = spec[2]
		local specTexture = spec[3]

		local role = roleBySpecTextureName[specTexture]
		return role or "NONE"
	end
	return "DAMAGER"
end

local roleStringToNumber = {
	["NONE"] = 0,
	["TANK"] = 1,
	["HEALER"] = 2,
	["DAMAGER"] = 3,
	["SUPPORT"] = 4,
}

local roleNumberToString = {
	[0] = "NONE",
	[1] = "TANK",
	[2] = "HEALER",
	[3] = "DAMAGER",
	[4] = "SUPPORT",
}

function DF:ConvertRole(value, valueType)
	if (valueType) then
		if (type(valueType) == "string") then
			valueType = roleNumberToString[valueType] or valueType
		end

		if (type(valueType) == "number") then
			valueType = roleStringToNumber[valueType] or valueType
		end
	end

	if (type(value) == "string") then
		return roleStringToNumber[value] or 0

	elseif (type(value) == "number") then
		return roleNumberToString[value] or "NONE"
	end

	return value
end

---return the role of the unit, this is safe to use for all versions of wow
---@param unitId string
---@param bUseSupport boolean?
---@param specId number?
---@return string
function DF.UnitGroupRolesAssigned(unitId, bUseSupport, specId)
    local role

    if (specId == 1473 and bUseSupport) then
        return "SUPPORT"
    end

    if (UnitGroupRolesAssigned) then
        role = UnitGroupRolesAssigned(unitId)
    end

    if (role == "NONE") then
        if (GetSpecialization) then
            if (UnitIsUnit(unitId, "player")) then
                local specializationIndex = GetSpecialization() or 0
                local id, name, description, icon, role, primaryStat = GetSpecializationInfo(specializationIndex)
                if (id == 1473 and bUseSupport) then
                    return "SUPPORT"
                end
                return id and role or "NONE"
            end
        else
            --attempt to guess the role by the player spec
            local classLoc, className = UnitClass(unitId)
            if (className == "MAGE" or className == "ROGUE" or className == "HUNTER" or className == "WARLOCK") then
                return "DAMAGER"
            end

            if (Details) then
                --attempt to get the role from Details! Damage Meter
                local guid = UnitGUID(unitId)
                if (guid) then
                    role = Details.cached_roles[guid]
                    if (role) then
                        return role
                    end
                end
            end

            if (UnitIsUnit(unitId, "player")) then
                role = DF:GetRoleByClassicTalentTree()
            end
        end
    end

    return role
end

---return the specializationid of the player it self
---@return number|nil
function DF.GetSpecialization()
	if (GetSpecialization) then
		return GetSpecialization()
	end
	return nil
end

---return the specializationid using the specId
---@param specId unknown
function DF.GetSpecializationInfoByID(specId)
	if (GetSpecializationInfoByID) then
		return GetSpecializationInfoByID(specId)
	end
	return nil
end

function DF.GetSpecializationInfo(...)
	if (GetSpecializationInfo) then
		return GetSpecializationInfo(...)
	end
	return nil
end

function DF.GetSpecializationRole(...)
	if (GetSpecializationRole) then
		return GetSpecializationRole(...)
	end
	return nil
end

--[=[ dump of C_EncounterJournal
	["GetEncountersOnMap"] = function,
	["SetPreviewMythicPlusLevel"] = function,
	["GetLootInfoByIndex"] = function,
	["GetSlotFilter"] = function,
	["IsEncounterComplete"] = function,
	["SetTab"] = function,
	["ResetSlotFilter"] = function,
	["OnOpen"] = function,
	["InstanceHasLoot"] = function,
	["GetSectionIconFlags"] = function,
	["SetPreviewPvpTier"] = function,
	["GetEncounterJournalLink"] = function,
	["GetInstanceForGameMap"] = function,
	["GetSectionInfo"] = function,
	["GetLootInfo"] = function,
	["GetDungeonEntrancesForMap"] = function,
	["OnClose"] = function,
	["SetSlotFilter"] = function,
--]=]

--build dummy encounter journal functions if they doesn't exists
--this is done for compatibility with classic and if in the future EJ_ functions are moved to C_
---@class EncounterJournal : table
---@field EJ_GetInstanceForMap fun(mapId: number)
---@field EJ_GetInstanceInfo fun(journalInstanceID: number)
---@field EJ_SelectInstance fun(journalInstanceID: number)
---@field EJ_GetEncounterInfoByIndex fun(index: number, journalInstanceID: number?)
---@field EJ_GetEncounterInfo fun(journalEncounterID: number)
---@field EJ_SelectEncounter fun(journalEncounterID: number)
---@field EJ_GetSectionInfo fun(sectionID: number)
---@field EJ_GetCreatureInfo fun(index: number, journalEncounterID: number?)
---@field EJ_SetDifficulty fun(difficultyID: number)
---@field EJ_GetNumLoot fun(): number
DF.EncounterJournal = {
	EJ_GetInstanceForMap = EJ_GetInstanceForMap or function() return nil end,
	EJ_GetInstanceInfo = EJ_GetInstanceInfo or function() return nil end,
	EJ_SelectInstance = EJ_SelectInstance or function() return nil end,
	EJ_GetEncounterInfoByIndex = EJ_GetEncounterInfoByIndex or function() return nil end,
	EJ_GetEncounterInfo = EJ_GetEncounterInfo or function() return nil end,
	EJ_SelectEncounter = EJ_SelectEncounter or function() return nil end,
	EJ_GetSectionInfo = EJ_GetSectionInfo or function() return nil end,
	EJ_GetCreatureInfo = EJ_GetCreatureInfo or function() return nil end,
	EJ_SetDifficulty = EJ_SetDifficulty or function() return nil end,
	EJ_GetNumLoot = EJ_GetNumLoot or function() return 0 end,
}

--will always give a very random name for our widgets
local init_counter = math.random(1, 1000000)

DF.LabelNameCounter = DF.LabelNameCounter or init_counter
DF.PictureNameCounter = DF.PictureNameCounter or init_counter
DF.BarNameCounter = DF.BarNameCounter or init_counter
DF.DropDownCounter = DF.DropDownCounter or init_counter
DF.PanelCounter = DF.PanelCounter or init_counter
DF.SimplePanelCounter = DF.SimplePanelCounter or init_counter
DF.ButtonCounter = DF.ButtonCounter or init_counter
DF.SliderCounter = DF.SliderCounter or init_counter
DF.SwitchCounter = DF.SwitchCounter or init_counter
DF.SplitBarCounter = DF.SplitBarCounter or init_counter

DF.FRAMELEVEL_OVERLAY = 750
DF.FRAMELEVEL_BACKGROUND = 150

DF.FrameWorkVersion = tostring(dversion)
function DF:PrintVersion()
	print("Details! Framework Version:", DF.FrameWorkVersion)
end

--get the working folder
do
	local path = string.match(debugstack(1, 1, 0), "AddOns\\(.+)fw.lua")
	if (path) then
		DF.folder = "Interface\\AddOns\\" .. path
	else
		--if not found, try to use the last valid one
		DF.folder = DF.folder or ""
	end
end

DF.debug = false

function DF:GetFrameworkFolder()
	return DF.folder
end

function DF:SetFrameworkDebugState(state)
	DF.debug = state
end


DF.embeds = DF.embeds or {}
local embedFunctions = {
	"RemoveRealName",
	"table",
	"BuildDropDownFontList",
	"SetFontSize",
	"SetFontFace",
	"SetFontColor",
	"GetFontSize",
	"GetFontFace",
	"SetFontOutline",
	"trim",
	"Msg",
	"CreateFlashAnimation",
	"Fade",
	"NewColor",
	"IsHtmlColor",
	"ParseColors",
	"BuildMenu",
	"ShowTutorialAlertFrame",
	"GetNpcIdFromGuid",
	"SetAsOptionsPanel",
	"GetPlayerRole",
	"GetCharacterTalents",
	"GetCharacterPvPTalents",

	"CreateDropDown",
	"CreateButton",
	"CreateColorPickButton",
	"CreateLabel",
	"CreateBar",
	"CreatePanel",
	"CreateFillPanel",
	"ColorPick",
	"IconPick",
	"CreateSimplePanel",
	"CreateChartPanel",
	"CreateImage",
	"CreateScrollBar",
	"CreateSwitch",
	"CreateSlider",
	"CreateSplitBar",
	"CreateTextEntry",
	"Create1PxPanel",
	"CreateOptionsFrame",
	"NewSpecialLuaEditorEntry",
	"ShowPromptPanel",
	"ShowTextPromptPanel",
	"GetTemplate",
	"InstallTemplate",
	"GetFrameworkFolder",
	"ShowPanicWarning",
	"SetFrameworkDebugState",
	"FindHighestParent",
	"OpenInterfaceProfile",
	"CreateInCombatTexture",
	"CreateAnimationHub",
	"CreateAnimation",
	"CreateScrollBox",
	"CreateBorder",
	"FormatNumber",
	"IntegerToTimer",
	"QuickDispatch",
	"Dispatch",
	"CommaValue",
	"RemoveRealmName",
	"Trim",
	"CreateGlowOverlay",
	"CreateAnts",
	"CreateFrameShake",
	"RegisterScriptComm",
	"SendScriptComm",
}

function DF:Embed(target)
	for k, v in pairs(embedFunctions) do
		target[v] = self[v]
	end
	self.embeds[target] = true
	return target
end

function DF:FadeFrame(frame, t)
	if (t == 0) then
		frame.hidden = false
		frame.faded = false
		frame.fading_out = false
		frame.fading_in = false
		frame:Show()
		frame:SetAlpha(1)

	elseif (t == 1) then
		frame.hidden = true
		frame.faded = true
		frame.fading_out = false
		frame.fading_in = false
		frame:SetAlpha(0)
		frame:Hide()
	end
end

------------------------------------------------------------------------------------------------------------
function DF:RandomBool(odds)
	if (odds) then
		local chance = math.random()
		return chance <= odds
	else
		return math.random(1, 2) == 1
	end
end

function DF:SetTexCoordFromAtlasInfo(texture, atlasInfo)
	texture:SetTexCoord(atlasInfo.leftTexCoord, atlasInfo.rightTexCoord, atlasInfo.topTexCoord, atlasInfo.bottomTexCoord)
end

------------------------------------------------------------------------------------------------------------
--table

DF.table = {}

---find a value inside a table and return the index
---@param t table
---@param value any
---@return integer|nil
function DF.table.find(t, value)
	for i = 1, #t do
		if (t[i] == value) then
			return i
		end
	end
end

---find a value inside a sub table
---@param index number
---@param value any
---@return integer|nil
function DF.table.findsubtable(t, index, value)
	for i = 1, #t do
		if (type(t[i]) == "table") then
			if (t[i][index] == value) then
				return i
			end
		end
	end
end

---Loop through parent of the passed object, making a string with parentKeys separated by a dot.
---The loop continues until a parentKey is not found or if the frame has no parent (reach UIParent).
---@param self table
---@param object any
---@return string
function DF:GetParentKeyPath(object)
	local parentKey = object:GetParentKey()
	if (not parentKey) then
		return ""
	end

	local path = "" .. parentKey
	local parent = object:GetParent()

	while (parent) do
		parentKey = parent:GetParentKey()

		if (parentKey) then
			path = parentKey .. "." .. path
		else
			return path
		end

		parent = parent:GetParent()
	end

	return path
end

---Loop through the parent of the passed object, creating a string with parent names and parent keys separated by dots, if the object has no name.
---The loop continues until a parentName is not found or if the frame has no parent (reach UIParent).
---@param self table
---@param object any
---@return string
function DF:GetParentNamePath(object)
	local parent = object
	local path = ""
	while (parent) do
		local parentName = parent:GetName()

		if (not parentName) then
			local parentOfParent = parent:GetParent()
			if (parentOfParent) then
				local parentKey = parentOfParent:GetParentKey()
				if (parentKey) then
					parentName = parentKey
				else
					local result = path:gsub("%.$", "")
					return result
				end
			end
		end

		if (parentName) then
			path = parentName .. "." .. path
		else
			local result = path:gsub("%.$", "")
			return result
		end

		parent = parent:GetParent()
	end

	local result = path:gsub("%.$", "")
	return result
end

---get a value from a table using a path, e.g. getfrompath(tbl, "a.b.c") is the same as tbl.a.b.c
---@param t table
---@param path string
---@param subOffset number?
---@return any
function DF.table.getfrompath(t, path, subOffset)
	if (path:match("%.") or path:match("%[")) then
		local value
		local offset = 0

		for key in path:gmatch("[%w_]+") do
			value = t[key] or t[tonumber(key)]

			--check if the value is nil, if it is, the key does not exists in the table
			if (not value) then
				return
			end

			--update t for the next iteration
			t = value
			offset = offset + 1

			if (subOffset == offset) then
				return value
			end
		end

		return value
	else
		return t[path] or t[tonumber(path)]
	end
end

---set the value of a table using a path, e.g. setfrompath(tbl, "a.b.c", 10) is the same as tbl.a.b.c = 10
---@param t table
---@param path string
---@param value any
---@return boolean?
function DF.table.setfrompath(t, path, value)
	if (path:match("%.") or path:match("%[")) then
		local lastTable
		local lastKey

		--for key in path:gmatch("[%w_]+") do
		for key in path:gmatch("[^%.%[%]]+") do
			lastTable = t
			lastKey = key

			--update t for the next iteration
			t = t[key] or t[tonumber(key)]
		end

		if (lastTable and lastKey) then
			lastTable[lastKey] = value
			return true
		end
	else
		t[path] = value
		return true
	end

	return false
end

---return the amount of keys in a table
---@param t table
---@return number
function DF.table.countkeys(t)
	local count = 0
	for _ in pairs(t) do
		count = count + 1
	end
	return count
end

---find the value inside the table, and it it's not found, add it
---@param t table
---@param index integer|any
---@param value any
---@return boolean
function DF.table.addunique(t, index, value)
	if (not value) then
		value = index
		index = #t + 1
	end

	for i = 1, #t do
		if (t[i] == value) then
			return false
		end
	end

	table.insert(t, index, value)
	return true
end

---get the table 't' and reverse the order of the values within it
---@param t table
---@return table
function DF.table.reverse(t)
	local new = {}
	local index = 1
	for i = #t, 1, -1 do
		new[index] = t[i]
		index = index + 1
	end
	return new
end

---remove a value from an array table
---@param t table
---@param value any
---@return boolean
function DF.table.remove(t, value)
	local bRemoved = false
	local removedAmount = 0

	for i = 1, #t do
		if (t[i] == value) then
			table.remove(t, i)
			bRemoved = true
			removedAmount = removedAmount + 1
		end
	end

	return bRemoved, removedAmount
end

---copy the values from table2 to table1 overwriting existing values, ignores __index and __newindex, keys pointing to a UIObject are preserved
---@param t1 table
---@param t2 table
---@return table
function DF.table.duplicate(t1, t2)
	for key, value in pairs(t2) do
		if (key ~= "__index" and key ~= "__newindex") then
			--preserve a UIObject passing it to the new table with copying it
			if (type(value) == "table" and table.GetObjectType and table:GetObjectType()) then
				t1[key] = value

			elseif (type(value) == "table") then
				t1[key] = t1[key] or {}
				DF.table.copy(t1[key], t2[key])

			else
				t1[key] = value
			end
		end
	end

	return t1
end

---copy the values from table2 to table1 overwriting existing values, ignores __index and __newindex, threat UIObjects as regular tables
---@param t1 table
---@param t2 table
---@return table
function DF.table.copy(t1, t2)
	for key, value in pairs(t2) do
		if (key ~= "__index" and key ~= "__newindex") then
			if (type(value) == "table") then
				t1[key] = t1[key] or {}
				DF.table.copy(t1[key], t2[key])
			else
				t1[key] = value
			end
		end
	end
	return t1
end

---copy from table2 to table1 overwriting values but do not copy data that cannot be compressed
---@param t1 table
---@param t2 table
---@return table
function DF.table.copytocompress(t1, t2)
	for key, value in pairs(t2) do
		if (key ~= "__index" and type(value) ~= "function") then
			if (type(value) == "table") then
				if (not value.GetObjectType) then
					t1[key] = t1[key] or {}
					DF.table.copytocompress(t1[key], t2[key])
				end
			else
				t1 [key] = value
			end
		end
	end
	return t1
end

---remove from table1 the values that are also on table2
---@param table1 table the table to have the values removed
---@param table2 table the reference table
function DF.table.removeduplicate(table1, table2)
    for key, value in pairs(table2) do
        if (type(value) == "table") then
            if (type(table1[key]) == "table") then
                DF.table.removeduplicate(table1[key], value)
				if (not next(table1[key])) then
					table1[key] = nil
				end
            end
        else
			if (type(table1[key]) == "number" and type(value) == "number") then
				if (DF:IsNearlyEqual(table1[key], value, 0.0001)) then
					table1[key] = nil
				end
			else
            	if (table1[key] == value) then
	                table1[key] = nil
            	end
			end
        end
    end
end

---add the indexes of table2 into the end of the table table1
---@param t1 table
---@param t2 table
---@return table
function DF.table.append(t1, t2)
	for i = 1, #t2 do
		t1[#t1+1] = t2[i]
	end
	return t1
end

---receive a table and N arguments, add each argument to the table
---@param t1 table
---@vararg any
function DF.table.inserts(t1, ...)
	for i = 1, select("#", ...) do
		t1[#t1+1] = select(i, ...)
	end
	return t1
end

---copy values that does exist on table2 but not on table1
---@param t1 table
---@param t2 table
---@return table
function DF.table.deploy(t1, t2)
	for key, value in pairs(t2) do
		if (type(value) == "table") then
			--check the t1 type as sometimes the key isn't the same type on both tables
			if (t1[key] == nil or type(t1[key]) == "table") then
				t1[key] = t1[key] or {}
				DF.table.deploy(t1[key], t2[key])
			end
		elseif (t1[key] == nil) then
			t1[key] = value
		end
	end
	return t1
end

--/run print (DetailsFramework.table.dump({{1, 2}, {2, 3}, {4, 5}}))
local function tableToString(t, resultString, deep, seenTables)
    resultString = resultString or ""
    deep = deep or 0
    seenTables = seenTables or {}

    if seenTables[t] then
        resultString = resultString .. "--CIRCULAR REFERENCE\n"
        return resultString
    end

    local space = string.rep("   ", deep)

    seenTables[t] = true

    for key, value in pairs(t) do
		local valueType = type(value)

		if (type(key) == "function") then
			key = "#function#"
		elseif (type(key) == "table") then
			key = "#table#"
		end

		if (type(key) ~= "string" and type(key) ~= "number") then
			key = "unknown?"
		end

        if (valueType == "table") then
			local sUIObjectType = value.GetObjectType and value:GetObjectType()
			if (sUIObjectType) then
				if (type(key) == "number") then
					resultString = resultString .. space .. "[" .. key .. "] = |cFFa9ffa9 " .. sUIObjectType .. " {|r\n"
				else
					resultString = resultString .. space .. "[\"" .. key .. "\"] = |cFFa9ffa9 " .. sUIObjectType .. " {|r\n"
				end
			else
				if (type(key) == "number") then
					resultString = resultString .. space .. "[" .. key .. "] = |cFFa9ffa9 {|r\n"
				else
					resultString = resultString .. space .. "[\"" .. key .. "\"] = |cFFa9ffa9 {|r\n"
				end
			end
            resultString = resultString .. tableToString(value, nil, deep + 1, seenTables)
            resultString = resultString .. space .. "|cFFa9ffa9},|r\n"

		elseif (valueType == "string") then
			resultString = resultString .. space .. "[\"" .. key .. "\"] = \"|cFFfff1c1" .. value .. "|r\",\n"

		elseif (valueType == "number") then
			if (type(key) == "number") then
				resultString = resultString .. space .. "[" .. key .. "] = |cFFffc1f4" .. value .. "|r,\n"
			else
				resultString = resultString .. space .. "[\"" .. key .. "\"] = |cFF94CEA8" .. value .. "|r,\n"
			end

		elseif (valueType == "function") then
			resultString = resultString .. space .. "[\"" .. key .. "\"] = |cFFC586C0function|r,\n"

		elseif (valueType == "boolean") then
			resultString = resultString .. space .. "[\"" .. key .. "\"] = |cFF99d0ff" .. (value and "true" or "false") .. "|r,\n"
		end
    end

    return resultString
end

local function tableToStringSafe(t)
    local seenTables = {}
    return tableToString(t, nil, 0, seenTables)
end

---get the contends of table 't' and return it as a string
---@param t table
---@param resultString string
---@param deep integer
---@return string
function DF.table.dump(t, resultString, deep)
	return tableToStringSafe(t)
end

---grab a text and split it into lines adding each line to an array table
---@param text string
---@return table
function DF:SplitTextInLines(text)
	local lines = {}
	local position = 1
	local startScope, endScope = text:find("\n", position, true)

	while (startScope) do
		if (startScope ~= 1) then
			table.insert(lines, text:sub(position, startScope-1))
		end
		position = endScope + 1
		startScope, endScope = text:find("\n", position, true)
	end

	if (position <= #text) then
		table.insert(lines, text:sub(position))
	end

	return lines
end

DF.strings = {}

---receive an array and output a string with the values separated by commas
---if bDoCompression is true, the string will be compressed using LibDeflate
---@param t table
---@param bDoCompression boolean|nil
---@return string
function DF.strings.tabletostring(t, bDoCompression)
	local newString = ""
	for i = 1, #t do
		newString = newString .. t[i] .. ","
	end

	newString = newString:sub(1, -2)

	if (bDoCompression) then
		local LibDeflate = LibStub:GetLibrary("LibDeflate")
		if (LibDeflate) then
			newString = LibDeflate:CompressDeflate(newString, {level = 9})
		end
	end

	return newString
end

function DF.strings.stringtotable(thisString, bDoCompression)
	if (bDoCompression) then
		local LibDeflate = LibStub:GetLibrary("LibDeflate")
		if (LibDeflate) then
			thisString = LibDeflate:DecompressDeflate(thisString)
		end
	end

	local newTable = {strsplit(",", thisString)}
	return newTable
end

local symbol_1K, symbol_10K, symbol_1B
if (GetLocale() == "koKR") then
	symbol_1K, symbol_10K, symbol_1B = "천", "만", "억"

elseif (GetLocale() == "zhCN") then
	symbol_1K, symbol_10K, symbol_1B = "千", "万", "亿"

elseif (GetLocale() == "zhTW") then
	symbol_1K, symbol_10K, symbol_1B = "千", "萬", "億"
end

---get the game localization and return which symbol need to be used after formatting numbers, this is for asian languages
---@return string
---@return string
---@return string
function DF:GetAsianNumberSymbols()
	if (GetLocale() == "koKR") then
		return "천", "만", "억"

	elseif (GetLocale() == "zhCN") then
		return "千", "万", "亿"

	elseif (GetLocale() == "zhTW") then
		return "千", "萬", "億"
	else
		--return korean as default (if the language is western)
		return "천", "만", "억"
	end
end

if (symbol_1K) then
	---if symbol_1K is valid, the game has an Asian localization, 'DF.FormatNumber' will use Asian symbols to format numbers
	---@param number number
	---@return string
	function DF.FormatNumber(number)
		if (number > 99999999) then
			return format("%.2f", number/100000000) .. symbol_1B
		elseif (number > 999999) then
			return format("%.2f", number/10000) .. symbol_10K
		elseif (number > 99999) then
			return floor(number/10000) .. symbol_10K
		elseif (number > 9999) then
			return format("%.1f", (number/10000)) .. symbol_10K
		elseif (number > 999) then
			return format("%.1f", (number/1000)) .. symbol_1K
		end
		return format("%.1f", number)
	end
else
	---if symbol_1K isn't valid, 'DF.FormatNumber' will use western symbols to format numbers
	---@param number number
	---@return string|number
	function DF.FormatNumber(number)
		if (number > 999999999) then
			return format("%.2f", number/1000000000) .. "B"
		elseif (number > 999999) then
			return format("%.2f", number/1000000) .. "M"
		elseif (number > 99999) then
			return floor(number/1000) .. "K"
		elseif (number > 999) then
			return format("%.1f", (number/1000)) .. "K"
		end
		return floor(number)
	end
end

---format a number with commas
---@param self table
---@param value number
---@return string
function DF:CommaValue(value)
	if (not value) then
		return "0"
	end

	value = floor(value)
	if (value == 0) then
		return "0"
	end

	--source http://richard.warburton.it
	local left, num, right = string.match(value, '^([^%d]*%d)(%d*)(.-)$')
	return left .. (num:reverse():gsub('(%d%d%d)','%1,'):reverse()) .. right
end

---call the function 'callback' for each group member passing the unitID and the extra arguments
---@param self table
---@param callback function
---@vararg any
function DF:GroupIterator(callback, ...)
	if (IsInRaid()) then
		for i = 1, GetNumGroupMembers() do
			DF:QuickDispatch(callback, "raid" .. i, ...)
		end

	elseif (IsInGroup()) then
		for i = 1, GetNumGroupMembers() - 1 do
			DF:QuickDispatch(callback, "party" .. i, ...)
		end
		DF:QuickDispatch(callback, "player", ...)

	else
		DF:QuickDispatch(callback, "player", ...)
	end
end

---receives an object and a percent amount, then calculate the return value by multiplying the min value of the object width or height by the percent received
---@param uiObject uiobject
---@param percent number
---@return number
function DF:GetSizeFromPercent(uiObject, percent)
	local width, height = uiObject:GetSize()
	local minValue = math.min(width, height)
	return minValue * percent
end

---get an integer an format it as string with the time format 16:45
---@param self table
---@param value number
---@return string
function DF:IntegerToTimer(value) --~formattime
	return "" .. math.floor(value/60) .. ":" .. string.format("%02.f", value%60)
end

---remove the realm name from a name
---@param self table
---@param name string
---@return string, number
function DF:RemoveRealmName(name)
	return name:gsub(("%-.*"), "")
end

---remove the owner name of the pet or guardian
---@param self table
---@param name string
---@return string, number
function DF:RemoveOwnerName(name)
	return name:gsub((" <.*"), "")
end

---remove realm and owner names also remove brackets from spell actors
---@param self table
---@param name string
---@return string
function DF:CleanUpName(name)
	name =  DF:RemoveRealmName(name)
	name = DF:RemoveOwnerName(name)
	name = name:gsub("%[%*%]%s", "")
	--remove texture escape sequence
	name = name:gsub("|T.-|t", "")
	return name
end

---remove the realm name from a name
---@param self table
---@param name string
---@return string, number
function DF:RemoveRealName(name)
	return name:gsub(("%-.*"), "")
end

---get the UIObject of type 'FontString' named fontString and set the font size to the maximum value of the arguments
---@param self table
---@param fontString fontstring
---@vararg number
function DF:SetFontSize(fontString, ...)
	local font, _, flags = fontString:GetFont()
	fontString:SetFont(font, math.max(...), flags)
end

---get the UIObject of type 'FontString' named fontString and set the font to the argument fontface
---@param self table
---@param fontString fontstring
---@param fontface string
function DF:SetFontFace(fontString, fontface)
	if (fontface == "DEFAULT") then
		DF:SetFontDefault(fontString)
		return
	end

	local font = SharedMedia:Fetch("font", fontface, true)
	if (font) then
		fontface = font
	end

	local _, size, flags = fontString:GetFont()
	return fontString:SetFont(fontface, size, flags)
end

local dummyFontString = UIParent:CreateFontString(nil, "background", "GameFontNormal")
local defaultFontFile = dummyFontString:GetFont()

function DF:GetTextWidth(text, size)
	if (size) then
		DF:SetFontSize(dummyFontString, size)
	else
		DF:SetFontSize(dummyFontString, 12)
	end

	dummyFontString:SetText(text)
	return dummyFontString:GetStringWidth()
end

---get the UIObject of type 'FontString' and set the default game font into it
---@param self table
---@param fontString fontstring
function DF:SetFontDefault(fontString)
	local _, size, flags = fontString:GetFont()
	return fontString:SetFont(defaultFontFile, size, flags)
end

---get the FontString passed and set the font color
---@param self table
---@param fontString fontstring
---@param r any
---@param g number?
---@param b number?
---@param a number?
function DF:SetFontColor(fontString, r, g, b, a)
	r, g, b, a = DF:ParseColors(r, g, b, a)
	fontString:SetTextColor(r, g, b, a)
end

---get the FontString passed and set the font shadow color and offset
---@param self table
---@param fontString fontstring
---@param r any
---@param g number?
---@param b number?
---@param a number?
---@param x number?
---@param y number?
function DF:SetFontShadow(fontString, r, g, b, a, x, y)
	r, g, b, a = DF:ParseColors(r, g, b, a)
	fontString:SetShadowColor(r, g, b, a)

	local offSetX, offSetY = fontString:GetShadowOffset()
	x = x or offSetX
	y = y or offSetY

	fontString:SetShadowOffset(x, y)
end

---get the FontString object passed and set the rotation of the text shown
---@param self table
---@param fontString fontstring
---@param degrees number
function DF:SetFontRotation(fontString, degrees) --deprecated, use fontString:SetRotation(degrees) | retail use fontString:SetRotation(math.rad(degrees))
	if (type(degrees) == "number") then
		if (not fontString.__rotationAnimation) then
			fontString.__rotationAnimation = DF:CreateAnimationHub(fontString)
			fontString.__rotationAnimation.rotator = DF:CreateAnimation(fontString.__rotationAnimation, "rotation", 1, 0, 0)
			fontString.__rotationAnimation.rotator:SetEndDelay(10^8)
			fontString.__rotationAnimation.rotator:SetSmoothProgress(1)
		end
		fontString.__rotationAnimation.rotator:SetDegrees(degrees)
		fontString.__rotationAnimation:Play()
		fontString.__rotationAnimation:Pause()
	end
end

---receives a string and a color and return the string wrapped with the color using |c and |r scape codes
---@param self table
---@param text string
---@param color any
---@return string
function DF:AddColorToText(text, color) --wrap text with a color
	local r, g, b = DF:ParseColors(color)
	if (not r) then
		return text
	end

	local hexColor = DF:FormatColor("hex", r, g, b)

	text = "|c" .. hexColor .. text .. "|r"

	return text
end

function DF:GetClassColorByClassId(classId)
	local classInfo = C_CreatureInfo.GetClassInfo(classId)
	if (classInfo) then
		local color = RAID_CLASS_COLORS[classInfo.classFile]
		if (color) then
			return color.r, color.g, color.b
		else
			return 1, 1, 1
		end
	end
	return 1, 1, 1
end

---receives a string 'text' and a class name and return the string wrapped with the class color using |c and |r scape codes
---@param self table
---@param text string
---@param className class
---@return string
function DF:AddClassColorToText(text, className)
	if (type(className) == "number") then
		className = DF.ClassIndexToFileName[className]
	end

	if (type(className) ~= "string") then
		return DF:RemoveRealName(text)

	elseif (className == "UNKNOW" or className == "PET") then
		return DF:RemoveRealName(text)
	end

	local color = RAID_CLASS_COLORS[className]
	if (color) then
		text = "|c" .. color.colorStr .. DF:RemoveRealName(text) .. "|r"
	else
		return DF:RemoveRealName(text)
	end

	return text
end

---returns the class icon texture coordinates and texture file path
---@param class string|number
---@return number, number, number, number, string
function DF:GetClassTCoordsAndTexture(class)
	if (type(class) == "number") then
		class = DF.ClassIndexToFileName[class]
	end
	local l, r, t, b = unpack(CLASS_ICON_TCOORDS[class])
	return l, r, t, b, [[Interface\WORLDSTATEFRAME\Icons-Classes]]
end

---create a string with the spell icon and the spell name using |T|t scape codes to add the icon inside the string
---@param self table
---@param spellId any
---@return string
function DF:MakeStringFromSpellId(spellId)
	local spellName, _, spellIcon = GetSpellInfo(spellId)
	if (spellName) then
		return "|T" .. spellIcon .. ":16:16:0:0:64:64:4:60:4:60|t " .. spellName
	end
	return ""
end

---wrap 'text' with the class icon of 'playerName' using |T|t scape codes
---@param text string
---@param playerName string
---@param englishClassName string this is the english class name, not the localized one, english class name is upper case
---@param useSpec boolean|nil
---@param iconSize number|nil
---@return string
function DF:AddClassIconToText(text, playerName, englishClassName, useSpec, iconSize)
	local size = iconSize or 16

	local spec
	if (useSpec) then
		if (Details) then
			local GUID = UnitGUID(playerName)
			if (GUID) then
				spec = Details.cached_specs[GUID]
				if (spec) then
					spec = spec
				end
			end
		else
			if (type(useSpec) == "number") then
				local specId, specName = GetSpecializationInfoByID(useSpec)
			end
		end
	end

	if (spec) then --if spec is valid, the user has Details! installed
		local specString = ""
		local L, R, T, B = unpack(Details.class_specs_coords[spec])
		if (L) then
			specString = "|TInterface\\AddOns\\Details\\images\\spec_icons_normal:" .. size .. ":" .. size .. ":0:0:512:512:" .. (L * 512) .. ":" .. (R * 512) .. ":" .. (T * 512) .. ":" .. (B * 512) .. "|t"
			return specString .. " " .. text
		end
	end

	if (englishClassName) then
		local classString = ""
		--Details.class_coords uses english class names as keys and the values are tables containing texture coordinates
		local L, R, T, B = unpack(Details.class_coords[englishClassName])
		if (L) then
			local imageSize = 128
			classString = "|TInterface\\AddOns\\Details\\images\\classes_small:" .. size .. ":" .. size .. ":0:0:" .. imageSize .. ":" .. imageSize .. ":" .. (L * imageSize) .. ":" .. (R * imageSize) .. ":" .. (T * imageSize) .. ":" .. (B * imageSize) .. "|t"
			return classString .. " " .. text
		end
	end

	return text
end

function DF:AddClassIconToString(text, engClass, size)
	size = size or 16
	local tcoords = CLASS_ICON_TCOORDS[engClass]
	if (tcoords) then
		local l, r, t, b = unpack(tcoords)
		return "|TInterface\\Glues\\CharacterCreate\\UI-CharacterCreate-Classes:" .. size .. ":" .. size .. ":0:0:256:256:" .. (l * 256) .. ":" .. (r * 256) .. ":" .. (t * 256) .. ":" .. (b * 256) .. "|t " .. text
	end
end

function DF:AddSpecIconToString(text, specId, size)
	size = size or 16

	if (not specId) then
		--get the player specId
		local specIndex = GetSpecialization()
		specId = GetSpecializationInfo(specIndex)
		if (not specId) then
			return
		end
	end

	local id, name, description, icon = GetSpecializationInfoByID(specId)
	if (id) then
		return "|T" .. icon .. ":" .. size .. ":" .. size .. ":0:0|t " .. text
	end
end

---create a table with information about a texture (deprecated, use: DetailsFramework:CreateAtlas())
---@param texture any
---@param textureWidth any
---@param textureHeight any
---@param imageWidth any
---@param imageHeight any
---@param left any
---@param right any
---@param top any
---@param bottom any
---@return table
function DF:CreateTextureInfo(texture, textureWidth, textureHeight, left, right, top, bottom, imageWidth, imageHeight)
	local textureInfo = {
		texture = texture,
		width = textureWidth or 16,
		height = textureHeight or 16,
		coords = {left or 0, right or 1, top or 0, bottom or 1},
	}

	textureInfo.imageWidth = imageWidth or textureInfo.width
	textureInfo.imageHeight = imageHeight or textureInfo.height

	return textureInfo
end

---add a texture to the start or end of a string
---@param text string
---@param textureInfo table
---@param bAddSpace any
---@param bAddAfterText any
---@return string
function DF:AddTextureToText(text, textureInfo, bAddSpace, bAddAfterText)
	local texture = textureInfo.texture
	local textureWidth = textureInfo.width
	local textureHeight = textureInfo.height
	local imageWidth = textureInfo.imageWidth or textureWidth
	local imageHeight = textureInfo.imageHeight or textureHeight
	local left, right, top, bottom = unpack(textureInfo.coords)
	left = left or 0
	right = right or 1
	top = top or 0
	bottom = bottom or 1

	if (bAddAfterText) then
		local newString = text .. (bAddSpace and " " or "") .. "|T" .. texture .. ":" .. textureWidth .. ":" .. textureHeight .. ":0:0:" .. imageWidth .. ":" .. imageHeight .. ":" .. (left * imageWidth) .. ":" .. (right * imageWidth) .. ":" .. (top * imageHeight) .. ":" .. (bottom * imageHeight) .. "|t"
		return newString
	else
		local newString = "|T" .. texture .. ":" .. textureWidth .. ":" .. textureHeight .. ":0:0:" .. imageWidth .. ":" .. imageHeight .. ":" .. (left * imageWidth) .. ":" .. (right * imageWidth) .. ":" .. (top * imageHeight) .. ":" .. (bottom * imageHeight) .. "|t" .. (bAddSpace and " " or "") .. text
		return newString
	end
end

---return the size of a fontstring
---@param fontString table
---@return number
function DF:GetFontSize(fontString)
	local _, size = fontString:GetFont()
	return size
end

---return the font of a fontstring
---@param fontString table
---@return string
function DF:GetFontFace(fontString)
	local fontface = fontString:GetFont()
	return fontface
end

local ValidOutlines = {
	["NONE"] = true,
	["MONOCHROME"] = true,
	["OUTLINE"] = true,
	["THICKOUTLINE"] = true,
	["OUTLINEMONOCHROME"] = true,
	["THICKOUTLINEMONOCHROME"] = true,
}

DF.FontOutlineFlags = {
	{"", "None"},
	{"MONOCHROME", "Monochrome"},
	{"OUTLINE", "Outline"},
	{"THICKOUTLINE", "Thick Outline"},
	{"OUTLINEMONOCHROME", "Outline & Monochrome"},
	{"THICKOUTLINEMONOCHROME", "Thick Outline & Monochrome"},
}

---set the outline of a fontstring, outline is a black border around the text, can be "NONE", "MONOCHROME", "OUTLINE" or "THICKOUTLINE"
---@param fontString table
---@param outline outline
function DF:SetFontOutline(fontString, outline)
    local font, fontSize = fontString:GetFont()
    if (outline) then
        if (type(outline) == "string") then
            outline = outline:upper()
        end

        if (ValidOutlines[outline]) then
            outline = outline

        elseif (type(outline) == "boolean" and outline) then
            outline = "OUTLINE"

        elseif (type(outline) == "boolean" and not outline) then
            outline = "" --"NONE"

        elseif (outline == 1) then
            outline = "OUTLINE"

        elseif (outline == 2) then
            outline = "THICKOUTLINE"
        end
    end

    outline = (not outline or outline == "NONE") and "" or outline

    fontString:SetFont(font, fontSize, outline)
end

---remove spaces from the start and end of the string
---@param string string
---@return string
function DF:Trim(string)
	return DF:trim(string)
end
function DF:trim(string)
	local from = string:match"^%s*()"
	return from > #string and "" or string:match(".*%S", from)
end

---truncate removing at a maximum of 10 character from the string
---@param fontString table
---@param maxWidth number
function DF:TruncateTextSafe(fontString, maxWidth)
	local text = fontString:GetText()
	local numIterations = 10

	while (fontString:GetStringWidth() > maxWidth) do
		text = strsub(text, 1, #text-1)
		fontString:SetText(text)
		if (#text <= 1) then
			break
		end

		numIterations = numIterations - 1
		if (numIterations <= 0) then
			break
		end
	end

	text = DF:CleanTruncateUTF8String(text)
	fontString:SetText(text)
end

---truncate removing characters from the string until the maxWidth is reach
---@param fontString table
---@param maxWidth number
function DF:TruncateText(fontString, maxWidth)
	local text = fontString:GetText()

	while (fontString:GetStringWidth() > maxWidth) do
		text = strsub(text, 1, #text - 1)
		fontString:SetText(text)
		if (string.len(text) <= 1) then
			break
		end
	end

	text = DF:CleanTruncateUTF8String(text)
	fontString:SetText(text)
end

---truncate removing text through a binary search with a max of 10 iterations
---@param fontString table
---@param maxWidth number
function DF:TruncateTextSafeBinarySearch(fontString, maxWidth)
	local text = fontString:GetText()
	if text == nil or text == '' then return end

	if fontString:GetUnboundedStringWidth() > maxWidth then
		local left = 1
		local right = #text
		local numIterations = 10

		while left <= right and numIterations > 0 do
			local middle = math.floor((left + right) * 0.5)
			local substring = strsub(text, 1, middle)
			fontString:SetText(substring)

			if fontString:GetUnboundedStringWidth() <= maxWidth then
				left = middle + 1
			else
				right = middle - 1
			end

			numIterations = numIterations - 1
		end

		text = strsub(text, 1, right)
	end

	fontString:SetText(DF:CleanTruncateUTF8String(text))
end

---truncate removing characters from the string until the maxWidth is reach
---@param fontString table
---@param maxWidth number
function DF:TruncateTextBinarySearch(fontString, maxWidth)
	local text = fontString:GetText()
	if text == nil or text == '' then return end

	if fontString:GetUnboundedStringWidth() > maxWidth then
		local left = 1
		local right = #text

		while left <= right do
			local middle = math.floor((left + right) * 0.5)
			local substring = strsub(text, 1, middle)
			fontString:SetText(substring)

			if fontString:GetUnboundedStringWidth() <= maxWidth then
				left = middle + 1
			else
				right = middle - 1
			end
		end

		text = strsub(text, 1, right)
	end

	fontString:SetText(DF:CleanTruncateUTF8String(text))
end

---@param text string
---@return string
function DF:CleanTruncateUTF8String(text)
	if type(text) == "string" and text ~= "" then
		local b1 = (#text > 0) and strbyte(strsub(text, #text, #text)) or nil
		local b2 = (#text > 1) and strbyte(strsub(text, #text-1, #text)) or nil
		local b3 = (#text > 2) and strbyte(strsub(text, #text-2, #text)) or nil

		if b1 and b1 >= 194 and b1 <= 244 then
			text = strsub (text, 1, #text - 1)

		elseif b2 and b2 >= 224 and b2 <= 244 then
			text = strsub (text, 1, #text - 2)

		elseif b3 and b3 >= 240 and b3 <= 244 then
			text = strsub (text, 1, #text - 3)
		end
	end

	return text
end

---truncate the amount of numbers used to show the fraction part of a number
---@param number number
---@param fractionDigits number
---@return number
function DF:TruncateNumber(number, fractionDigits)
	fractionDigits = fractionDigits or 2
	local truncatedNumber = number

	--local truncatedNumber = format("%." .. fractionDigits .. "f", number) --4x slower than:
	--http://lua-users.org/wiki/SimpleRound
	local mult = 10 ^ fractionDigits
	if (number >= 0) then
		truncatedNumber = floor(number * mult + 0.5) / mult
	else
		truncatedNumber = ceil(number * mult + 0.5) / mult
	end

	return truncatedNumber
end

function DF:GetCursorPosition()
	local x, y = GetCursorPosition()
	local scale = UIParent:GetEffectiveScale()
	return x / scale, y / scale
end

---attempt to get the ID of an npc from a GUID
---@param GUID string
---@return number
function DF:GetNpcIdFromGuid(GUID)
	local npcId = select(6, strsplit("-", GUID ))
	if (npcId) then
		npcId = tonumber(npcId)
		return npcId or 0
	end
	return 0
end

function DF.SortOrder1(t1, t2)
	return t1[1] > t2[1]
end
function DF.SortOrder2(t1, t2)
	return t1[2] > t2[2]
end
function DF.SortOrder3(t1, t2)
	return t1[3] > t2[3]
end
function DF.SortOrder1R(t1, t2)
	return t1[1] < t2[1]
end
function DF.SortOrder2R(t1, t2)
	return t1[2] < t2[2]
end
function DF.SortOrder3R(t1, t2)
	return t1[3] < t2[3]
end

---return a list of spells from the player spellbook
---@return table<string, boolean> spellNamesInSpellBook
---@return spellid[] spellIdsInSpellBook
function DF:GetSpellBookSpells()
    local spellNamesInSpellBook = {}
	local spellIdsInSpellBook = {}

    for i = 1, GetNumSpellTabs() do
        local tabName, tabTexture, offset, numSpells, isGuild, offspecId = GetSpellTabInfo(i)

        if (offspecId == 0 and tabTexture ~= 136830) then --don't add spells found in the General tab
            offset = offset + 1
            local tabEnd = offset + numSpells

            for j = offset, tabEnd - 1 do
                local spellType, spellId = GetSpellBookItemInfo(j, SPELLBOOK_BANK_PLAYER)

                if (spellId) then
                    if (spellType ~= "FLYOUT") then
                        local spellName = GetSpellInfo(spellId)
                        if (spellName) then
                            spellNamesInSpellBook[spellName] = true
							spellIdsInSpellBook[#spellIdsInSpellBook+1] = spellId
                        end
                    else
                        local _, _, numSlots, isKnown = GetFlyoutInfo(spellId)
                        if (isKnown and numSlots > 0) then
                            for k = 1, numSlots do
                                local spellID, overrideSpellID, isKnown = GetFlyoutSlotInfo(spellId, k)
                                if (isKnown) then
                                    local spellName = GetSpellInfo(spellID)
                                    spellNamesInSpellBook[spellName] = true
									spellIdsInSpellBook[#spellIdsInSpellBook+1] = spellID
                                end
                            end
                        end
                    end
                end
            end
        end
    end

    return spellNamesInSpellBook, spellIdsInSpellBook
end

function DF:GetHeroTalentId()
    local configId = C_ClassTalents.GetActiveConfigID()
    if (not configId) then
        return 0
    end
    local configInfo = C_Traits.GetConfigInfo(configId)
    for treeIndex, treeId in ipairs(configInfo.treeIDs) do
        local treeNodes = C_Traits.GetTreeNodes(treeId)
        for nodeIdIndex, treeNodeID in ipairs(treeNodes) do
            local traitNodeInfo = C_Traits.GetNodeInfo(configId, treeNodeID)
            if (traitNodeInfo) then
                local activeEntry = traitNodeInfo.activeEntry
                if (activeEntry) then
                    local entryId = activeEntry.entryID
                    local rank = activeEntry.rank
                    if (rank > 0) then
                        local entryInfo = C_Traits.GetEntryInfo(configId, entryId)
						if (not entryInfo.definitionID and entryInfo.subTreeID) then
							return entryInfo.subTreeID
						end
                    end
                end
            end
        end
    end
	return 0
end

---return a table of passive talents, format: [spellId] = true
---@return {Name: string, ID: number, Texture: any, IsSelected: boolean}[]
function DF:GetAllTalents()
	local allTalents = {}

	local configId = C_ClassTalents.GetActiveConfigID()
	if (configId) then
		local configInfo = C_Traits.GetConfigInfo(configId)
		--get the spells from the SPEC from talents
		for treeIndex, treeId in ipairs(configInfo.treeIDs) do
			local treeNodes = C_Traits.GetTreeNodes(treeId)
			for nodeIdIndex, treeNodeID in ipairs(treeNodes) do
				local traitNodeInfo = C_Traits.GetNodeInfo(configId, treeNodeID)
				if (traitNodeInfo) then
					local activeEntry = traitNodeInfo.activeEntry
					local entryIds = traitNodeInfo.entryIDs
					for i = 1, #entryIds do
						local entryId = entryIds[i] --number
						local traitEntryInfo = C_Traits.GetEntryInfo(configId, entryId)
						local borderTypes = Enum.TraitNodeEntryType
						if (traitEntryInfo.type) then -- == borderTypes.SpendCircle
							local definitionId = traitEntryInfo.definitionID
							if definitionId then
								local traitDefinitionInfo = C_Traits.GetDefinitionInfo(definitionId)
								local spellId = traitDefinitionInfo.overriddenSpellID or traitDefinitionInfo.spellID
								local spellName, _, spellTexture = GetSpellInfo(spellId)
								if (spellName) then
									local talentInfo = {Name = spellName, ID = spellId, Texture = spellTexture, IsSelected = (activeEntry and activeEntry.rank and activeEntry.rank > 0) or false}
									allTalents[#allTalents+1] = talentInfo
								end
							end
						end
					end
				end
			end
		end
	end

	return allTalents
end

---return a table where keys are spellIds (number) and the value is true
---@return table<number, boolean>
function DF:GetAvailableSpells()
    local completeListOfSpells = {}

    --this line might not be compatible with classic
    local specId, specName, _, specIconTexture = GetSpecializationInfo(GetSpecialization())
    --local classNameLoc, className, classId = UnitClass("player") --not in use
    local locPlayerRace, playerRace, playerRaceId = UnitRace("player")

    --get racials from the general tab
	local generalTabIndex = 1
    local tabName, tabTexture, offset, numSpells, isGuild, offspecId = GetSpellTabInfo(generalTabIndex)
    offset = offset + 1
    local tabEnd = offset + numSpells
    for entryOffset = offset, tabEnd - 1 do
        local spellType, spellId = GetSpellBookItemInfo(entryOffset, SPELLBOOK_BANK_PLAYER)
        local spellData = LIB_OPEN_RAID_COOLDOWNS_INFO[spellId]
        if (spellData) then
            local raceId = spellData.raceid
            if (raceId) then
                if (type(raceId) == "table") then
                    if (raceId[playerRaceId]) then
                        spellId = GetOverrideSpell(spellId)
                        local spellName = GetSpellInfo(spellId)
                        local bIsPassive = IsPassiveSpell(spellId, SPELLBOOK_BANK_PLAYER)
                        if (spellName and not bIsPassive) then
                            completeListOfSpells[spellId] = true
                        end
                    end

                elseif (type(raceId) == "number") then
                    if (raceId == playerRaceId) then
                        spellId = GetOverrideSpell(spellId)
                        local spellName = GetSpellInfo(spellId)
                        local bIsPassive = IsPassiveSpell(spellId, SPELLBOOK_BANK_PLAYER)
                        if (spellName and not bIsPassive) then
                            completeListOfSpells[spellId] = true
                        end
                    end
                end
            end
        end
    end

	local spellBookPlayerEnum = Enum.SpellBookSpellBank and Enum.SpellBookSpellBank.Player or "player"

	--get spells from the Spec spellbook
	for i = 1, GetNumSpellTabs() do --called "lines" in new v11 api
        local tabName, tabTexture, offset, numSpells, isGuild, offSpecId, shouldHide, specID = GetSpellTabInfo(i)
		if (tabTexture == specIconTexture) then
			offset = offset + 1
			local tabEnd = offset + numSpells
			--local bIsOffSpec = offSpecId ~= 0
			for entryOffset = offset, tabEnd - 1 do
				local spellType, spellId = GetSpellBookItemInfo(entryOffset, spellBookPlayerEnum)
				if (spellId) then
					if (spellType == "SPELL" or spellType == 1) then
						spellId = GetOverrideSpell(spellId)
						local spellName = GetSpellInfo(spellId)
						local bIsPassive = IsPassiveSpell(entryOffset, spellBookPlayerEnum)
						if (spellName and not bIsPassive) then
							completeListOfSpells[spellId] = true --bIsOffSpec == false
						end
					end
				end
			end
		end
    end

	local CONST_SPELLBOOK_CLASSSPELLS_TABID = 2
	local CONST_SPELLBOOK_GENERAL_TABID = 1

    --get class shared spells from the spell book
	--[=
    local tabName, tabTexture, offset, numSpells, isGuild, offSpecId = GetSpellTabInfo(CONST_SPELLBOOK_CLASSSPELLS_TABID)
    offset = offset + 1
    local tabEnd = offset + numSpells
	--local bIsOffSpec = offSpecId ~= 0
    for entryOffset = offset, tabEnd - 1 do
        local spellType, spellId = GetSpellBookItemInfo(entryOffset, spellBookPlayerEnum)
        if (spellId) then
            if (spellType == "SPELL" or spellType == 1) then
                spellId = GetOverrideSpell(spellId)
                local spellName = GetSpellInfo(spellId)
                local bIsPassive = IsPassiveSpell(spellId, spellBookPlayerEnum)

				if (spellName and not bIsPassive) then
                    completeListOfSpells[spellId] = true --bIsOffSpec == false
                end
            end
        end
    end
	--]=]

    local getNumPetSpells = function()
        --'HasPetSpells' contradicts the name and return the amount of pet spells available instead of a boolean
        return HasPetSpells()
    end

    --get pet spells from the pet spellbook
    local numPetSpells = getNumPetSpells()
    if (numPetSpells) then
        for i = 1, numPetSpells do
            local spellName, _, unmaskedSpellId = GetSpellBookItemName(i, spellBookPetEnum)
            if (unmaskedSpellId) then
                unmaskedSpellId = GetOverrideSpell(unmaskedSpellId)
                local bIsPassive = IsPassiveSpell(i, spellBookPetEnum)
                if (spellName and not bIsPassive) then
                    completeListOfSpells[unmaskedSpellId] = true
                end
            end
        end
    end

    return completeListOfSpells
end


------------------------------------------------------------------------------------------------------------------------
--flash animation
local onFinishFlashAnimation = function(self)
	if (self.showWhenDone) then
		self.frame:SetAlpha(1)
	else
		self.frame:SetAlpha(0)
		self.frame:Hide()
	end

	if (self.onFinishFunc) then
		self:onFinishFunc(self.frame)
	end
end

local stopAnimation_Method = function(self)
	local FlashAnimation = self.FlashAnimation
	FlashAnimation:Stop()
end

local startFlash_Method = function(self, fadeInTime, fadeOutTime, flashDuration, showWhenDone, flashInHoldTime, flashOutHoldTime, loopType)
	local flashAnimation = self.FlashAnimation

	local fadeIn = flashAnimation.fadeIn
	local fadeOut = flashAnimation.fadeOut

	fadeIn:Stop()
	fadeOut:Stop()

	fadeIn:SetDuration(fadeInTime or 1)
	fadeIn:SetEndDelay(flashInHoldTime or 0)

	fadeOut:SetDuration(fadeOutTime or 1)
	fadeOut:SetEndDelay(flashOutHoldTime or 0)

	flashAnimation.duration = flashDuration
	flashAnimation.loopTime = flashAnimation:GetDuration()
	flashAnimation.finishAt = GetTime() + flashDuration
	flashAnimation.showWhenDone = showWhenDone

	flashAnimation:SetLooping(loopType or "REPEAT")

	self:Show()
	self:SetAlpha(0)
	flashAnimation:Play()
end

---create a flash animation for a frame
---@param frame table
---@param onFinishFunc function?
---@param onLoopFunc function?
function DF:CreateFlashAnimation(frame, onFinishFunc, onLoopFunc)
	local flashAnimation = frame:CreateAnimationGroup()

	flashAnimation.fadeOut = flashAnimation:CreateAnimation("Alpha")
	flashAnimation.fadeOut:SetOrder(1)
	flashAnimation.fadeOut:SetFromAlpha(0)
	flashAnimation.fadeOut:SetToAlpha(1)

	flashAnimation.fadeIn = flashAnimation:CreateAnimation("Alpha")
	flashAnimation.fadeIn:SetOrder(2)
	flashAnimation.fadeIn:SetFromAlpha(1)
	flashAnimation.fadeIn:SetToAlpha(0)

	frame.FlashAnimation = flashAnimation
	flashAnimation.frame = frame
	flashAnimation.onFinishFunc = onFinishFunc

	flashAnimation:SetScript("OnLoop", onLoopFunc)
	flashAnimation:SetScript("OnFinished", onFinishFlashAnimation)

	frame.Flash = startFlash_Method
	frame.Stop = stopAnimation_Method

	return flashAnimation
end

local onStartPunchAnimation = function(animationGroup)
	local parent = animationGroup:GetParent()
	animationGroup.parentWidth = parent:GetWidth()
	animationGroup.parentHeight = parent:GetHeight()
end

local onStopPunchAnimation = function(animationGroup)
	local parent = animationGroup:GetParent()
	parent:SetWidth(animationGroup.parentWidth)
	parent:SetHeight(animationGroup.parentHeight)
end

function DF:CreatePunchAnimation(frame, scale)
	scale = scale or 1.1
	scale = math.min(scale, 1.9)
	local animationHub = DF:CreateAnimationHub(frame, onStartPunchAnimation, onStopPunchAnimation)
	local scaleUp = DF:CreateAnimation(animationHub, "scale", 1, 0.05, 1, 1, scale, scale, "center", 0, 0)
	local scaleDown = DF:CreateAnimation(animationHub, "scale", 2, 0.05, 1, 1, 1-(scale - 1), 1-(scale - 1), "center", 0, 0)
	return animationHub
end

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--anchoring

function DF:CheckPoints(point1, point2, point3, point4, point5, object)
	if (not point1 and not point2) then
		return "topleft", object.widget:GetParent(), "topleft", 0, 0
	end

	if (type(point1) == "string") then
		local frameGlobal = _G[point1]
		if (frameGlobal and type(frameGlobal) == "table" and frameGlobal.GetObjectType) then
			return DF:CheckPoints(frameGlobal, point2, point3, point4, point5, object)
		end

	elseif (type(point2) == "string") then
		local frameGlobal = _G[point2]
		if (frameGlobal and type(frameGlobal) == "table" and frameGlobal.GetObjectType) then
			return DF:CheckPoints(point1, frameGlobal, point3, point4, point5, object)
		end
	end

	if (type(point1) == "string" and type(point2) == "table") then --setpoint("left", frame, _, _, _)
		if (not point3 or type(point3) == "number") then --setpoint("left", frame, 10, 10)
			point1, point2, point3, point4, point5 = point1, point2, point1, point3, point4
		end

	elseif (type(point1) == "string" and type(point2) == "number") then --setpoint("topleft", x, y)
		point1, point2, point3, point4, point5 = point1, object.widget:GetParent(), point1, point2, point3

	elseif (type(point1) == "number") then --setpoint(x, y)
		point1, point2, point3, point4, point5 = "topleft", object.widget:GetParent(), "topleft", point1, point2

	elseif (type(point1) == "table") then --setpoint(frame, x, y)
		point1, point2, point3, point4, point5 = "topleft", point1, "topleft", point2, point3
	end

	if (not point2) then
		point2 = object.widget:GetParent()
	elseif (point2.dframework) then
		point2 = point2.widget
	end

	return point1 or "topleft", point2, point3 or "topleft", point4 or 0, point5 or 0
end

---@class df_anchor : table
---@field side number 1-8: topleft to top (clockwise); 9: center; 10-13: inside left right top bottom; 14-17: inside topleft, bottomleft bottomright topright
---@field x number
---@field y number


DF.AnchorPoints = {
	"Top Left",
	"Left",
	"Bottom Left",
	"Bottom",
	"Bottom Right",
	"Right",
	"Top Right",
	"Top",
	"Center", --9
	"Inside Left", --10
	"Inside Right", --11
	"Inside Top", --12
	"Inside Bottom", --13
	"Inside Top Left", --14
	"Inside Bottom Left", --15
	"Inside Bottom Right", --16
	"Inside Top Right", --17
}

DF.AnchorPointsByIndex = {
	"topleft", --1
	"left", --2
	"bottomleft", --3
	"bottom", --4
	"bottomright", --5
	"right", --6
	"topright", --7
	"top", --8
	"center", --9
}

DF.AnchorPointsToInside = {
	[9] = 9,
	[8] = 12,
	[7] = 17,
	[6] = 11,
	[5] = 16,
	[4] = 13,
	[3] = 15,
	[2] = 10,
	[1] = 14,
}

DF.InsidePointsToAnchor = {
	[9] = 9,
	[12] = 8,
	[17] = 7,
	[11] = 6,
	[16] = 5,
	[13] = 4,
	[15] = 3,
	[10] = 2,
	[14] = 1,
}

function DF:ConvertAnchorPointToInside(anchorPoint)
	return DF.AnchorPointsToInside[anchorPoint] or anchorPoint
end

local calcPointCoords = function(ninePointsWidget, ninePointsRef, anchorTable, coordIndex, newAnchorSide)
	--get the location of the topleft corner relative to the bottomleft corner of the screen
	---@type df_coordinate
	local widgetPointCoords = ninePointsWidget[coordIndex]
	--get the topleft coords of the reference widget
	---@type df_coordinate
	local refPointCoords = ninePointsRef[coordIndex]

	--calculate the offset of the x and y axis
	local x = refPointCoords.x - widgetPointCoords.x
	local y = refPointCoords.y - widgetPointCoords.y
	anchorTable.x = x
	anchorTable.y = y
	anchorTable.side = newAnchorSide

	print("new anchor side", newAnchorSide, "x", x, "y", y)
end

function DF:ConvertAnchorOffsets(widget, referenceWidget, anchorTable, newAnchorSide)
	if (anchorTable.side == newAnchorSide) then
		return anchorTable
	end

	local ninePoints = DF.Math.GetNinePoints(widget)
	local refNinePoints = DF.Math.GetNinePoints(referenceWidget)

	--the numeration from 1 to 9 is the index within a ninePoints table

	anchorTable.side = newAnchorSide

	if (newAnchorSide == 14) then --inside topleft
		anchorTable.x = ninePoints[1].x - refNinePoints[1].x
		anchorTable.y = ninePoints[1].y - refNinePoints[1].y
		--print("inside topleft", anchorTable.x, anchorTable.y)

	elseif (newAnchorSide == 15) then --inside bottomleft
		anchorTable.x = ninePoints[3].x - refNinePoints[3].x
		anchorTable.y = ninePoints[3].y - refNinePoints[3].y

	elseif (newAnchorSide == 16) then --inside bottomright
		anchorTable.x = refNinePoints[5].x - ninePoints[5].x
		anchorTable.y = refNinePoints[5].y - ninePoints[5].y

	elseif (newAnchorSide == 17) then --inside topright
		anchorTable.x = refNinePoints[7].x - ninePoints[7].x
		anchorTable.y = refNinePoints[7].y - ninePoints[7].y

	elseif (newAnchorSide == 10) then --inside left
		calcPointCoords(ninePoints, refNinePoints, anchorTable, 2, newAnchorSide)

	elseif (newAnchorSide == 11) then --inside right
		calcPointCoords(ninePoints, refNinePoints, anchorTable, 6, newAnchorSide)

	elseif (newAnchorSide == 12) then --inside top
		calcPointCoords(ninePoints, refNinePoints, anchorTable, 8, newAnchorSide)

	elseif (newAnchorSide == 13) then --inside bottom
		calcPointCoords(ninePoints, refNinePoints, anchorTable, 4, newAnchorSide)

	elseif (newAnchorSide == 9) then --center
		calcPointCoords(ninePoints, refNinePoints, anchorTable, 9, newAnchorSide)
	else
		--print("not implemented")
	end
end

local anchoringFunctions = {
	function(frame, anchorTo, offSetX, offSetY) --1 TOP LEFT
		frame:ClearAllPoints()
		frame:SetPoint("bottomleft", anchorTo, "topleft", offSetX, offSetY)
	end,

	function(frame, anchorTo, offSetX, offSetY) --2 LEFT
		frame:ClearAllPoints()
		frame:SetPoint("right", anchorTo, "left", offSetX, offSetY)
	end,

	function(frame, anchorTo, offSetX, offSetY) --3 BOTTOM LEFT
		frame:ClearAllPoints()
		frame:SetPoint("topleft", anchorTo, "bottomleft", offSetX, offSetY)
	end,

	function(frame, anchorTo, offSetX, offSetY) --4 BOTTOM
		frame:ClearAllPoints()
		frame:SetPoint("top", anchorTo, "bottom", offSetX, offSetY)
	end,

	function(frame, anchorTo, offSetX, offSetY) --5 BOTTOM RIGHT
		frame:ClearAllPoints()
		frame:SetPoint("topright", anchorTo, "bottomright", offSetX, offSetY)
	end,

	function(frame, anchorTo, offSetX, offSetY) --6 RIGHT
		frame:ClearAllPoints()
		frame:SetPoint("left", anchorTo, "right", offSetX, offSetY)
	end,

	function(frame, anchorTo, offSetX, offSetY) --7 TOP RIGHT
		frame:ClearAllPoints()
		frame:SetPoint("bottomright", anchorTo, "topright", offSetX, offSetY)
	end,

	function(frame, anchorTo, offSetX, offSetY) --8 TOP
		frame:ClearAllPoints()
		frame:SetPoint("bottom", anchorTo, "top", offSetX, offSetY)
	end,

	function(frame, anchorTo, offSetX, offSetY) --9 CENTER
		frame:ClearAllPoints()
		frame:SetPoint("center", anchorTo, "center", offSetX, offSetY)
	end,

	function(frame, anchorTo, offSetX, offSetY) --10 INSIDE LEFT
		frame:ClearAllPoints()
		frame:SetPoint("left", anchorTo, "left", offSetX, offSetY)
	end,

	function(frame, anchorTo, offSetX, offSetY) --11 INSIDE RIGHT
		frame:ClearAllPoints()
		frame:SetPoint("right", anchorTo, "right", offSetX, offSetY)
	end,

	function(frame, anchorTo, offSetX, offSetY) --12 INSIDE TOP
		frame:ClearAllPoints()
		frame:SetPoint("top", anchorTo, "top", offSetX, offSetY)
	end,

	function(frame, anchorTo, offSetX, offSetY) --13 INSIDE BOTTOM
		frame:ClearAllPoints()
		frame:SetPoint("bottom", anchorTo, "bottom", offSetX, offSetY)
	end,

	function(frame, anchorTo, offSetX, offSetY) --14 INSIDE TOPLEFT to TOPLEFT
		frame:ClearAllPoints()
		frame:SetPoint("topleft", anchorTo, "topleft", offSetX, offSetY)
	end,

	function(frame, anchorTo, offSetX, offSetY) --15 INSIDE BOTTOMLEFT to BOTTOMLEFT
		frame:ClearAllPoints()
		frame:SetPoint("bottomleft", anchorTo, "bottomleft", offSetX, offSetY)
	end,

	function(frame, anchorTo, offSetX, offSetY) --16 INSIDE BOTTOMRIGHT to BOTTOMRIGHT
		frame:ClearAllPoints()
		frame:SetPoint("bottomright", anchorTo, "bottomright", offSetX, offSetY)
	end,

	function(frame, anchorTo, offSetX, offSetY) --17 INSIDE TOPRIGHT to TOPRIGHT
		frame:ClearAllPoints()
		frame:SetPoint("topright", anchorTo, "topright", offSetX, offSetY)
	end,
}

---set the anchor point using a df_anchor table
---@param widget uiobject
---@param anchorTable df_anchor
---@param anchorTo uiobject?
function DF:SetAnchor(widget, anchorTable, anchorTo)
	anchorTo = anchorTo or widget:GetParent()
	anchoringFunctions[anchorTable.side](widget, anchorTo, anchorTable.x, anchorTable.y)
end

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--colors

	---add a new color name, the color can be query using DetailsFramework:ParseColors(colorName)
	---@param colorName string
	---@param red number
	---@param green number
	---@param blue number
	---@param alpha number?
	---@return table
	function DF:NewColor(colorName, red, green, blue, alpha)
		assert(type(colorName) == "string", "DetailsFramework:NewColor(): colorName must be a string.")

		red, green, blue, alpha = DetailsFramework:ParseColors(red, green, blue, alpha)
		local colorTable = DetailsFramework:FormatColor("table", red, green, blue, alpha)

		DF.alias_text_colors[colorName] = colorTable

		return colorTable
	end

	local colorTableMixin = {
		GetColor = function(self)
			return self.r, self.g, self.b, self.a
		end,

		SetColor = function(self, r, g, b, a)
			r, g, b, a = DF:ParseColors(r, g, b, a)
			self.r = r or self.r
			self.g = g or self.g
			self.b = b or self.b
			self.a = a or self.a
		end,

		IsColorTable = true,
	}

	---* takes in a color in one format and converts it to another specified format.
	---* here are the parameters it accepts:
	---* newFormat (string): The format to convert the color to. It can be one of the following: "commastring", "tablestring", "table", "tablemembers", "numbers", "hex".
	---* r (number|string): The red component of the color or a string representing the color.
	---* g (number|nil): The green component of the color. This is optional if r is a string.
	---* b (number|nil): The blue component of the color. This is optional if r is a string.
	---* a (number|nil): The alpha component of the color. This is optional and defaults to 1 if not provided.
	---* decimalsAmount (number|nil): The number of decimal places to round the color components to. This is optional and defaults to 4 if not provided.
	---* The function returns the color in the new format. The return type depends on the newFormat parameter. It can be a string, a table, or four separate number values (for the "numbers" format).
	---* For the "hex" format, it returns a string representing the color in hexadecimal format.
	---@param newFormat string
	---@param r number|string
	---@param g number|nil
	---@param b number|nil
	---@param a number|nil
	---@param decimalsAmount number|nil
	---@return string|table|number|nil
	---@return number|nil
	---@return number|nil
	---@return number|nil
	function DF:FormatColor(newFormat, r, g, b, a, decimalsAmount)
		a = a or 1
		r, g, b, a = DF:ParseColors(r, g, b, a)
		decimalsAmount = decimalsAmount or 4

		r = DF:TruncateNumber(r, decimalsAmount)
		g = DF:TruncateNumber(g, decimalsAmount)
		b = DF:TruncateNumber(b, decimalsAmount)
		a = DF:TruncateNumber(a, decimalsAmount)

		if (newFormat == "commastring") then
			return r .. ", " .. g .. ", " .. b .. ", " .. a

		elseif (newFormat == "tablestring") then
			return "{" .. r .. ", " .. g .. ", " .. b .. ", " .. a .. "}"

		elseif (newFormat == "table") then
			return {r, g, b, a}

		elseif (newFormat == "tablemembers") then
			return {["r"] = r, ["g"] = g, ["b"] = b, ["a"] = a}

		elseif (newFormat == "numbers") then
			return r, g, b, a

		elseif (newFormat == "hex") then
			return format("%.2x%.2x%.2x%.2x", a * 255, r * 255, g * 255, b * 255)
		end
	end

	function DF:CreateColorTable(r, g, b, a)
		local t  = {
			r = r or 1,
			g = g or 1,
			b = b or 1,
			a = a or 1,
		}
		DF:Mixin(t, colorTableMixin)
		return t
	end

	---return true if DF.alias_text_colors has the colorName as a key
	---DF.alias_text_colors is a table where key is a color name and value is an indexed table with the r g b values
	---@param colorName any
	---@return unknown
	function DF:IsHtmlColor(colorName)
		return DF.alias_text_colors[colorName]
	end

	---return the brightness of a color from zero to one
	---@param r number
	---@param g number
	---@param b number
	---@return number
	function DF:GetColorBrightness(r, g, b)
		r, g, b = DF:ParseColors(r, g, b)
		return 0.2134 * r + 0.7152 * g + 0.0721 * b
	end

	---return the hue of a color from red to blue to green to  yellow and back to red
	---@param r number
	---@param g number
	---@param b number
	---@return number
	function DF:GetColorHue(r, g, b)
		r, g, b = DF:ParseColors(r, g, b)

		local minValue, maxValue = math.min(r, g, b), math.max(r, g, b)

		if (maxValue == minValue) then
			return 0

		elseif (maxValue == r) then
			return (g - b) / (maxValue - minValue) % 6

		elseif (maxValue == g) then
			return (b - r) / (maxValue - minValue) + 2

		else
			return (r - g) / (maxValue - minValue) + 4
		end
	end

	---get the values passed and return r g b a color values
	---the function accept color name, tables with r g b a members, indexed tables with r g b a values, numbers, html hex color
	---@param red any
	---@param green any
	---@param blue any
	---@param alpha any
	---@return number
	---@return number
	---@return number
	---@return number
	function DF:ParseColors(red, green, blue, alpha)
		local firstParameter = red

		--the first value passed is a table?
		if (type(firstParameter) == "table") then
			local colorTable = red

			if (colorTable.IsColorTable) then
				--using colorTable mixin
				return colorTable:GetColor()

			elseif (not colorTable[1] and colorTable.r) then
				--{["r"] = 1, ["g"] = 1, ["b"] = 1}
				red, green, blue, alpha = colorTable.r, colorTable.g, colorTable.b, colorTable.a

			else
				--{1, .7, .2, 1}
				red, green, blue, alpha = unpack(colorTable)
			end

		--the first value passed is a string?
		elseif (type(firstParameter) == "string") then
			local colorString = red
			--hexadecimal
			if (string.find(colorString, "#")) then
				colorString = colorString:gsub("#","")
				if (string.len(colorString) == 8) then --with alpha
					red, green, blue, alpha = tonumber("0x" .. colorString:sub(3, 4))/255, tonumber("0x" .. colorString:sub(5, 6))/255, tonumber("0x" .. colorString:sub(7, 8))/255, tonumber("0x" .. colorString:sub(1, 2))/255
				else
					red, green, blue, alpha = tonumber("0x" .. colorString:sub(1, 2))/255, tonumber("0x" .. colorString:sub(3, 4))/255, tonumber("0x" .. colorString:sub(5, 6))/255, 1
				end
			else
				--name of the color
				local colorTable = DF.alias_text_colors[colorString]
				if (colorTable) then
					red, green, blue, alpha = unpack(colorTable)

				--string with number separated by comma
				elseif (colorString:find(",")) then
					local r, g, b, a = strsplit(",", colorString)
					red, green, blue, alpha = tonumber(r), tonumber(g), tonumber(b), tonumber(a)

				else
					--no color found within the string, return default color
					red, green, blue, alpha = unpack(DF.alias_text_colors.none)
				end
			end
		end

		if (not red or type(red) ~= "number") then
			red = 1
		end
		if (not green) or type(green) ~= "number" then
			green = 1
		end
		if (not blue or type(blue) ~= "number") then
			blue = 1
		end
		if (not alpha or type(alpha) ~= "number") then
			alpha = 1
		end

		--saturate the values before returning to make sure they are on the 0 to 1 range
		return Saturate(red), Saturate(green), Saturate(blue), Saturate(alpha)
	end

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--tutorials
	function DF:ShowTutorialAlertFrame(maintext, desctext, clickfunc)
		local TutorialAlertFrame = _G.DetailsFrameworkAlertFrame

		if (not TutorialAlertFrame) then

			TutorialAlertFrame = CreateFrame("frame", "DetailsFrameworkAlertFrame", UIParent, "MicroButtonAlertTemplate")
			TutorialAlertFrame.isFirst = true
			TutorialAlertFrame:SetPoint("left", UIParent, "left", -20, 100)
			TutorialAlertFrame:SetFrameStrata("TOOLTIP")
			TutorialAlertFrame:Hide()

			TutorialAlertFrame:SetScript("OnMouseUp", function(self)
				if (self.clickfunc and type(self.clickfunc) == "function") then
					self.clickfunc()
				end
				self:Hide()
			end)
			TutorialAlertFrame:Hide()
		end

		--
		TutorialAlertFrame.label = type(maintext) == "string" and maintext or type(desctext) == "string" and desctext or ""
		MicroButtonAlert_SetText (TutorialAlertFrame, alert.label)
		--

		TutorialAlertFrame.clickfunc = clickfunc
		TutorialAlertFrame:Show()
	end

	function DF:CreateOptionsFrame(name, title, template) --deprecated?
		template = template or 1

		if (template == 2) then
			local newOptionsFrame = CreateFrame("frame", name, UIParent, "ButtonFrameTemplate")
			table.insert(UISpecialFrames, name)

			newOptionsFrame:SetSize(500, 200)
			newOptionsFrame.RefreshOptions = DF.internalFunctions.RefreshOptionsPanel
			newOptionsFrame.widget_list = {}

			newOptionsFrame:SetScript("OnMouseDown", function(self, button)
				if (button == "RightButton") then
					if (self.moving) then
						self.moving = false
						self:StopMovingOrSizing()
					end
					return newOptionsFrame:Hide()
				elseif (button == "LeftButton" and not self.moving) then
					self.moving = true
					self:StartMoving()
				end
			end)

			newOptionsFrame:SetScript("OnMouseUp", function(self)
				if (self.moving) then
					self.moving = false
					self:StopMovingOrSizing()
				end
			end)

			newOptionsFrame:SetMovable(true)
			newOptionsFrame:EnableMouse(true)
			newOptionsFrame:SetFrameStrata("DIALOG")
			newOptionsFrame:SetToplevel(true)
			newOptionsFrame:Hide()
			newOptionsFrame:SetPoint("center", UIParent, "center")
			newOptionsFrame.TitleText:SetText(title)

			return newOptionsFrame

		elseif (template == 1) then
			local newOptionsFrame = CreateFrame("frame", name, UIParent)
			table.insert(UISpecialFrames, name)

			newOptionsFrame:SetSize(500, 200)
			newOptionsFrame.RefreshOptions = DF.internalFunctions.RefreshOptionsPanel
			newOptionsFrame.widget_list = {}

			newOptionsFrame:SetScript("OnMouseDown", function(self, button)
				if (button == "RightButton") then
					if (self.moving) then
						self.moving = false
						self:StopMovingOrSizing()
					end
					return newOptionsFrame:Hide()
				elseif (button == "LeftButton" and not self.moving) then
					self.moving = true
					self:StartMoving()
				end
			end)

			newOptionsFrame:SetScript("OnMouseUp", function(self)
				if (self.moving) then
					self.moving = false
					self:StopMovingOrSizing()
				end
			end)

			newOptionsFrame:SetMovable(true)
			newOptionsFrame:EnableMouse(true)
			newOptionsFrame:SetFrameStrata("DIALOG")
			newOptionsFrame:SetToplevel(true)
			newOptionsFrame:Hide()
			newOptionsFrame:SetPoint("center", UIParent, "center")

			newOptionsFrame:SetBackdrop({bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", tile = true, tileSize = 16,
			edgeFile = [[Interface\Buttons\WHITE8X8]], edgeSize = 1,
			insets = {left = 1, right = 1, top = 1, bottom = 1}})
			newOptionsFrame:SetBackdropColor(0, 0, 0, .7)

			local textureTitle = newOptionsFrame:CreateTexture(nil, "artwork")
			textureTitle:SetTexture([[Interface\CURSOR\Interact]])
			textureTitle:SetTexCoord(0, 1, 0, 1)
			textureTitle:SetVertexColor(1, 1, 1, 1)
			textureTitle:SetPoint("topleft", newOptionsFrame, "topleft", 2, -3)
			textureTitle:SetWidth(36)
			textureTitle:SetHeight(36)

			local titleLabel = DF:NewLabel(newOptionsFrame, nil, "$parentTitle", nil, title, nil, 20, "yellow")
			titleLabel:SetPoint("left", textureTitle, "right", 2, -1)
			DF:SetFontOutline (titleLabel, true)

			local closeButton = CreateFrame("Button", nil, newOptionsFrame, "UIPanelCloseButton")
			closeButton:SetWidth(32)
			closeButton:SetHeight(32)
			closeButton:SetPoint("TOPRIGHT",  newOptionsFrame, "TOPRIGHT", -3, -3)
			closeButton:SetFrameLevel(newOptionsFrame:GetFrameLevel()+1)

			return newOptionsFrame
		end
	end

------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--~templates

local latinLanguageIds = {"enUS", "deDE", "esES", "esMX", "frFR", "itIT", "ptBR"}
local latinLanguageIdsMap = {
	["enUS"] = true,
	["deDE"] = true,
	["esES"] = true,
	["esMX"] = true,
	["frFR"] = true,
	["itIT"] = true,
	["ptBR"] = true,
}

local alphbets = {
	[latinLanguageIds] = {"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"},
	["zhCN"] = {},
}

--fonts
DF.font_templates = DF.font_templates or {}

--detect which language is the client and select the font accordingly
local clientLanguage = GetLocale()
if (clientLanguage == "enGB") then
	clientLanguage = "enUS"
end

DF.ClientLanguage = clientLanguage

---returns which region the language the client is running, return "western", "russia" or "asia"
---@return string
function DF:GetClientRegion()
	if (clientLanguage == "zhCN" or clientLanguage == "koKR" or clientLanguage == "zhTW") then
		return "asia"
	elseif (clientLanguage == "ruRU") then
		return "russia"
	else
		return "western"
	end
end

DF.registeredFontPaths = DF.registeredFontPaths or {}

-- ~language ~locale ~fontpath
---get a font path to be used for a specific language
---@param languageId string enUS, deDE, esES, esMX, frFR, itIT, ptBR, ruRU, zhCN, zhTW, koKR
---@return string
function DF:GetBestFontPathForLanguage(languageId)
	local fontPath = DF.registeredFontPaths[languageId]
	if (fontPath) then
		return fontPath
	end

	--font paths gotten from creating a FontString with template "GameFontNormal" and getting the font returned from FontString:GetFont()
	if (languageId == "enUS" or languageId == "deDE" or languageId == "esES" or languageId == "esMX" or languageId == "frFR" or languageId == "itIT" or languageId == "ptBR") then
		return [[Fonts\FRIZQT__.TTF]]

	elseif (languageId == "ruRU") then
		return [[Fonts\FRIZQT___CYR.TTF]]

	elseif (languageId == "zhCN") then
		return [[Fonts\ARKai_T.ttf]]

	elseif (languageId == "zhTW") then
		return [[Fonts\blei00d.TTF]]

	elseif (languageId == "koKR") then
		return [[Fonts\2002.TTF]]
	end

	--the locale passed doesn't exists, so pass the enUS
	return [[Fonts\FRIZQT__.TTF]]
end

---return true if the language paren is latin: enUS, deDE, esES, esMX, frFR, itIT, ptBR
---@param languageId string
---@return boolean
function DF:IsLatinLanguage(languageId)
	return latinLanguageIdsMap[languageId]
end

---return a font name to use for the client language
---@param self table
---@param languageId string?
---@param western string?
---@param cyrillic string?
---@param china string?
---@param korean string?
---@param taiwan string?
function DF:GetBestFontForLanguage(languageId, western, cyrillic, china, korean, taiwan)
	if (not languageId) then
		languageId = DF.ClientLanguage
	end

	if (languageId == "enUS" or languageId == "deDE" or languageId == "esES" or languageId == "esMX" or languageId == "frFR" or languageId == "itIT" or languageId == "ptBR") then
		return western or "Friz Quadrata TT"

	elseif (languageId == "ruRU") then
		return cyrillic or "Friz Quadrata TT"

	elseif (languageId == "zhCN") then
		return china or "AR CrystalzcuheiGBK Demibold"

	elseif (languageId == "koKR") then
		return korean or "2002"

	elseif (languageId == "zhTW") then
		return taiwan or "AR CrystalzcuheiGBK Demibold"
	end
end


local templateOnEnter = function(frame)
	if (frame.onenter_backdrop) then
		local r, g, b, a = detailsFramework:ParseColors(frame.onenter_backdrop)
		frame:SetBackdropColor(r, g, b, a)
	end
	if (frame.onenter_backdrop_border_color) then
		local r, g, b, a = detailsFramework:ParseColors(frame.onenter_backdrop_border_color)
		frame:SetBackdropBorderColor(r, g, b, a)
	end
end

local templateOnLeave = function(frame)
	if (frame.onleave_backdrop) then
		local r, g, b, a = detailsFramework:ParseColors(frame.onleave_backdrop)
		frame:SetBackdropColor(r, g, b, a)
	end
	if (frame.onleave_backdrop_border_color) then
		local r, g, b, a = detailsFramework:ParseColors(frame.onleave_backdrop_border_color)
		frame:SetBackdropBorderColor(r, g, b, a)
	end
end

DF.TemplateOnEnter = templateOnEnter
DF.TemplateOnLeave = templateOnLeave

---set a details framework template into a regular frame
---@param self table
---@param frame uiobject
---@param template string
function detailsFramework:SetTemplate(frame, template)
	template = detailsFramework:ParseTemplate("button", template)

	if (frame.SetWidth) then
		if (template.width) then
			PixelUtil.SetWidth(frame, template.width)
		end

		if (template.height) then
			PixelUtil.SetHeight(frame, template.height)
		end
	end

	if (frame.SetBackdrop) then
		if (template.backdrop) then
			frame:SetBackdrop(template.backdrop)
		end

		if (template.backdropcolor) then
			local r, g, b, a = detailsFramework:ParseColors(template.backdropcolor)
			frame:SetBackdropColor(r, g, b, a)
			frame.onleave_backdrop = {r, g, b, a}
		end

		if (template.backdropbordercolor) then
			local r, g, b, a = detailsFramework:ParseColors(template.backdropbordercolor)
			frame:SetBackdropBorderColor(r, g, b, a)
			frame.onleave_backdrop_border_color = {r, g, b, a}
		end

		if (template.onentercolor) then
			local r, g, b, a = detailsFramework:ParseColors(template.onentercolor)
			frame.onenter_backdrop = {r, g, b, a}
			frame:HookScript("OnEnter", templateOnEnter)
			frame.__has_onentercolor_script = true
		end

		if (template.onleavecolor) then
			local r, g, b, a = detailsFramework:ParseColors(template.onleavecolor)
			frame.onleave_backdrop = {r, g, b, a}
			frame:HookScript("OnLeave", templateOnLeave)
			frame.__has_onleavecolor_script = true
		end

		if (template.onenterbordercolor) then
			local r, g, b, a = detailsFramework:ParseColors(template.onenterbordercolor)
			frame.onenter_backdrop_border_color = {r, g, b, a}
			if (not frame.__has_onentercolor_script) then
				frame:HookScript("OnEnter", templateOnEnter)
			end
		end

		if (template.onleavebordercolor) then
			local r, g, b, a = detailsFramework:ParseColors(template.onleavebordercolor)
			frame.onleave_backdrop_border_color = {r, g, b, a}
			if (not frame.__has_onleavecolor_script) then
				frame:HookScript("OnLeave", templateOnLeave)
			end
		end

	elseif (frame.SetColorTexture) then
		if (template.backdropcolor) then
			local r, g, b, a = detailsFramework:ParseColors(template.backdropcolor)
			frame:SetColorTexture(r, g, b, a)
		end
	end

	if (frame.SetIcon) then
		if (template.icon) then
			local iconInfo = template.icon
			frame:SetIcon(iconInfo.texture, iconInfo.width, iconInfo.height, iconInfo.layout, iconInfo.texcoord, iconInfo.color, iconInfo.textdistance, iconInfo.leftpadding)
		end
	end

	if (frame.SetTextColor) then
		if (template.textsize) then
			detailsFramework:SetFontSize(frame, template.textsize)
		end

		if (template.textfont) then
			detailsFramework:SetFontFace(frame, template.textfont)
		end

		if (template.textcolor) then
			detailsFramework:SetFontColor(frame, template.textcolor)
		end

		--horizontal alignment
		if (template.textalign and frame.SetJustifyH) then
			template.textalign = string.lower(template.textalign)

			if (template.textalign == "left" or template.textalign == "<") then
				frame:SetJustifyH("LEFT")

			elseif (template.textalign == "center" or template.textalign == "|") then
				frame:SetJustifyH("CENTER")

			elseif (template.textalign == "right" or template.textalign == ">") then
				frame:SetJustifyH("RIGHT")
			end
		end
	end
end

--DF.font_templates ["ORANGE_FONT_TEMPLATE"] = {color = "orange", size = 11, font = "Accidental Presidency"}
--DF.font_templates ["OPTIONS_FONT_TEMPLATE"] = {color = "yellow", size = 12, font = "Accidental Presidency"}
--DF.font_templates["ORANGE_FONT_TEMPLATE"] = {color = "orange", size = 10, font = DF:GetBestFontForLanguage()}
DF.font_templates["ORANGE_FONT_TEMPLATE"] = {color = {1, 0.8235, 0, 1}, size = 11, font = DF:GetBestFontForLanguage()}
--DF.font_templates["OPTIONS_FONT_TEMPLATE"] = {color = "yellow", size = 9.6, font = DF:GetBestFontForLanguage()}
DF.font_templates["OPTIONS_FONT_TEMPLATE"] = {color = {1, 1, 1, 0.9}, size = 9.6, font = DF:GetBestFontForLanguage()}
DF.font_templates["SMALL_SILVER"] = {color = "silver", size = 9, font = DF:GetBestFontForLanguage()}
--~templates
--dropdowns
DF.dropdown_templates = DF.dropdown_templates or {}
DF.dropdown_templates["OPTIONS_DROPDOWN_TEMPLATE"] = {
	backdrop = {
		edgeFile = [[Interface\Buttons\WHITE8X8]],
		edgeSize = 1,
		bgFile = [[Interface\Tooltips\UI-Tooltip-Background]],
		tileSize = 64,
		tile = true
	},

	--backdropcolor = {0.1, 0.1, 0.1, .7},
	backdropcolor = {0.2, 0.2, 0.2, .7},
	onentercolor = {0.3, 0.3, 0.3, .7},
	backdropbordercolor = {0, 0, 0, .4},
	onenterbordercolor = {0.3, 0.3, 0.3, 0.8},

	dropicon = "Interface\\BUTTONS\\arrow-Down-Down",
	dropiconsize = {16, 16},
	dropiconpoints = {-2, -3},
}

DF.dropdown_templates["OPTIONS_DROPDOWNDARK_TEMPLATE"] = {
	backdrop = {
		edgeFile = [[Interface\Buttons\WHITE8X8]],
		edgeSize = 1,
		bgFile = [[Interface\Tooltips\UI-Tooltip-Background]],
		tileSize = 64,
		tile = true
	},

	backdropcolor = {0.1215, 0.1176, 0.1294, 0.8000},
	backdropbordercolor = {.2, .2, .2, 1},
	onentercolor = {.5, .5, .5, .9},
	onenterbordercolor = {.4, .4, .4, 1},

	dropicon = "Interface\\BUTTONS\\arrow-Down-Down",
	dropiconsize = {16, 16},
	dropiconpoints = {-2, -3},
}

DF.dropdown_templates["OLD_DROPDOWN_TEMPLATE"] = {
	height = 24,

	backdrop = {
		edgeFile = "Interface\\Buttons\\UI-SliderBar-Border",
		edgeSize = 8,
		bgFile = [[Interface\Tooltips\UI-Tooltip-Background]],
		tileSize = 64,
		tile = true,
		insets = {left = 4, right = 4, top = 4, bottom = 4}
	},

	backdropcolor = {0.1215, 0.1176, 0.1294, 0.4000},
	backdropbordercolor = {1, 1, 1, 1},
	onentercolor = {.5, .5, .5, .9},
	onenterbordercolor = {1, 1, 1, 1},

	dropicon = "Interface\\BUTTONS\\arrow-Down-Down",
	dropiconsize = {16, 16},
	dropiconpoints = {-2, -3},
}

--switches
DF.switch_templates = DF.switch_templates or {}
DF.switch_templates["OPTIONS_CHECKBOX_TEMPLATE"] = {
	backdrop = {edgeFile = [[Interface\Buttons\WHITE8X8]], edgeSize = 1, bgFile = [[Interface\Tooltips\UI-Tooltip-Background]], tileSize = 64, tile = true},
	backdropcolor = {1, 1, 1, .5},
	backdropbordercolor = {0, 0, 0, 1},
	width = 18,
	height = 18,
	enabled_backdropcolor = {1, 1, 1, .5},
	disabled_backdropcolor = {1, 1, 1, .2},
	onenterbordercolor = {1, 1, 1, 1},
}

DF.switch_templates["OPTIONS_CIRCLECHECKBOX_TEMPLATE"] = {
	width = 18,
	height = 18,
	is_checkbox = true, --will call SetAsCheckBox()
	checked_texture = [[Interface\CHARACTERFRAME\TempPortraitAlphaMaskSmall]],
	checked_size_percent = 0.7,
	checked_xoffset = 0,
	checked_yoffset = 0,
	checked_color = "dark3",
	rounded_corner = {
		color = {.075, .075, .075, 1},
		border_color = {.2, .2, .2, 1},
		roundness = 8,
	},
}

DF.switch_templates["OPTIONS_CHECKBOX_BRIGHT_TEMPLATE"] = {
	backdrop = {edgeFile = [[Interface\Buttons\WHITE8X8]], edgeSize = 1, bgFile = [[Interface\Tooltips\UI-Tooltip-Background]], tileSize = 64, tile = true},
	backdropcolor = {1, 1, 1, .5},
	backdropbordercolor = {0, 0, 0, 1},
	width = 18,
	height = 18,
	enabled_backdropcolor = {1, 1, 1, .5},
	disabled_backdropcolor = {1, 1, 1, .5},
	onenterbordercolor = {1, 1, 1, 1},
}

--buttons
DF.button_templates = DF.button_templates or {}
DF.button_templates["OPTIONS_BUTTON_TEMPLATE"] = {
	backdrop = {edgeFile = [[Interface\Buttons\WHITE8X8]], edgeSize = 1, bgFile = [[Interface\Tooltips\UI-Tooltip-Background]], tileSize = 64, tile = true},
	backdropcolor = {1, 1, 1, .5},
	backdropbordercolor = {0, 0, 0, 1},
}

DF.button_templates["OPTIONS_CIRCLEBUTTON_TEMPLATE"] = {
	rounded_corner = {
		color = {.075, .075, .075, 1},
		border_color = {.2, .2, .2, 1},
		roundness = 8,
	},
}

DF.button_templates["OPTIONS_BUTTON_GOLDENBORDER_TEMPLATE"] = {
	backdrop = {edgeFile = [[Interface\Buttons\WHITE8X8]], edgeSize = 1, bgFile = [[Interface\Tooltips\UI-Tooltip-Background]], tileSize = 64, tile = true},
	backdropcolor = {1, 1, 1, .5},
	backdropbordercolor = {1, 0.785, 0, 1},
}

DF.button_templates["STANDARD_GRAY"] = {
	backdrop = {edgeFile = [[Interface\Buttons\WHITE8X8]], edgeSize = 1, bgFile = [[Interface\Tooltips\UI-Tooltip-Background]], tileSize = 64, tile = true},
	backdropcolor = {0.2, 0.2, 0.2, 0.502},
	backdropbordercolor = {0, 0, 0, 0.5},
	onentercolor = {0.4, 0.4, 0.4, 0.502},
}

DF.button_templates["OPAQUE_DARK"] = {
	backdrop = {edgeFile = [[Interface\Buttons\WHITE8X8]], edgeSize = 1, bgFile = [[Interface\Buttons\WHITE8X8]], tileSize = 8, tile = true},
	backdropcolor = {0.2, 0.2, 0.2, 1},
	backdropbordercolor = {0, 0, 0, 1},
	onentercolor = {0.4, 0.4, 0.4, 1},
}

--sliders
DF.slider_templates = DF.slider_templates or {}
DF.slider_templates["OPTIONS_SLIDER_TEMPLATE"] = {
	backdrop = {edgeFile = [[Interface\Buttons\WHITE8X8]], edgeSize = 1, bgFile = [[Interface\Tooltips\UI-Tooltip-Background]], tileSize = 64, tile = true},

	--original color wow10:
	--backdropcolor = {1, 1, 1, .5},
	--backdropbordercolor = {0, 0, 0, 1},
	--onentercolor = {1, 1, 1, .5},
	--onenterbordercolor = {1, 1, 1, 1},

	backdropcolor = {0.2, 0.2, 0.2, .7},
	onentercolor = {0.3, 0.3, 0.3, .7},
	backdropbordercolor = {0, 0, 0, .4}, --0.7 original alpha wow10
	onenterbordercolor = {0.3, 0.3, 0.3, 0.8},

	thumbtexture = [[Interface\Tooltips\UI-Tooltip-Background]],
	thumbwidth = 16,
	thumbheight = 14,
	--thumbcolor = {0, 0, 0, 0.5},
	thumbcolor = {.8, .8, .8, 0.5},
}

DF.slider_templates["OPTIONS_SLIDERDARK_TEMPLATE"] = {
	backdrop = {edgeFile = [[Interface\Buttons\WHITE8X8]], edgeSize = 1, bgFile = [[Interface\Tooltips\UI-Tooltip-Background]], tileSize = 64, tile = true},

	backdropcolor = {0.05, 0.05, 0.05, .7},
	onentercolor = {0.3, 0.3, 0.3, .7},
	backdropbordercolor = {0, 0, 0, 1},
	onenterbordercolor = {0, 0, 0, 1},

	thumbtexture = [[Interface\Tooltips\UI-Tooltip-Background]],
	thumbwidth = 24,
	thumbheight = 14,
	thumbcolor = {.8, .8, .8, 0.5},
}

DF.slider_templates["MODERN_SLIDER_TEMPLATE"] = {
	thumbtexture = "Minimal_SliderBar_Button", --atlas name
	thumbwidth = 20,
	thumbheight = 19,
	thumbcolor = {1, 1, 1, 0.924},
	slider_left = "Minimal_SliderBar_Left",
	slider_right = "Minimal_SliderBar_Right",
	slider_middle = "_Minimal_SliderBar_Middle",
	amount_color = "white",
	amount_size = 12,
	amount_outline = "outline",
}

local templateTables = {DF.dropdown_templates, DF.button_templates, DF.switch_templates, DF.slider_templates, DF.font_templates}

---template categories: "font", "dropdown", "button", "switch", "slider"
---receives a template category and a template name or table
---if a template name has been passed, the function will iterate over all template tables to find a template with the name passed
---@param self table
---@param templateCategory templatecategory
---@param template string|table
---@return table
function DF:ParseTemplate(templateCategory, template)
	if (type(template) == "string") then
		local objectType = templateCategory

		if (objectType == "label") then
			templateCategory = "font"

		elseif (objectType == "dropdown" or objectType == "textentry") then
			templateCategory = "dropdown"

		elseif (objectType == "button") then
			templateCategory = "button"

		elseif (objectType == "switch") then
			templateCategory = "switch"

		elseif (objectType == "slider") then
			templateCategory = "slider"
		end

		local templateTable = DF:GetTemplate(templateCategory, template)
		if (templateTable) then
			return templateTable
		end

		--iterate over all template tables to find a template with the name passed
		for i = 1, #templateTables do
			local tTable = templateTables[i]
			if (tTable[template]) then
				return tTable[template]
			end
		end
	else
		return template
	end

	---@cast template table
	return template
end

---register a new template to be used with SetTemplate calls
---@param templateCategory templatecategory
---@param templateName string
---@param template table
---@param parentName any
---@return table
function DF:InstallTemplate(templateCategory, templateName, template, parentName)
	local newTemplate = {}

	--if has a parent, just copy the parent to the new template
	if (parentName and type(parentName) == "string") then
		local parentTemplate = DF:GetTemplate(templateCategory, parentName)
		if (parentTemplate) then
			DF.table.copy(newTemplate, parentTemplate)
		end
	end

	--copy the template passed into the new template
	DF.table.copy(newTemplate, template)

	templateCategory = string.lower(templateCategory)

	local templateTable
	if (templateCategory == "font") then
		templateTable = DF.font_templates

		local font = template.font
		if (font) then
			--fonts passed into the template has default to western
			--the framework will get the game client language and change the font if needed
			font = DF:GetBestFontForLanguage(nil, font)
		end

	elseif (templateCategory == "dropdown") then
		templateTable = DF.dropdown_templates

	elseif (templateCategory == "button") then
		templateTable = DF.button_templates

	elseif (templateCategory == "switch") then
		templateTable = DF.switch_templates

	elseif (templateCategory == "slider") then
		templateTable = DF.slider_templates
	end

	templateTable[templateName] = newTemplate
	return newTemplate
end

function DF:GetTemplate(widgetType, templateName)
	widgetType = string.lower(widgetType)
	local templateTable

	if (widgetType == "font") then
		templateTable = DF.font_templates

	elseif (widgetType == "dropdown") then
		templateTable = DF.dropdown_templates

	elseif (widgetType == "button") then
		templateTable = DF.button_templates

	elseif (widgetType == "switch") then
		templateTable = DF.switch_templates

	elseif (widgetType == "slider") then
		templateTable = DF.slider_templates
	end

	return templateTable[templateName]
end

---get the name of the parent of the passed frame
---@param frame frame
---@return string
function DF:GetParentName(frame)
	local parentName = frame:GetName()
	if (not parentName) then
		error("Details! FrameWork: called $parent but parent was no name.", 2)
	end
	return parentName
end

function DF:Error(errortext)
	print("|cFFFF2222Details! Framework Error|r:", errortext, self.GetName and self:GetName(), self.WidgetType, debugstack (2, 3, 0))
end

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--members

DF.GlobalWidgetControlNames = {
	textentry = "DF_TextEntryMetaFunctions",
	button = "DF_ButtonMetaFunctions",
	panel = "DF_PanelMetaFunctions",
	dropdown = "DF_DropdownMetaFunctions",
	label = "DF_LabelMetaFunctions",
	normal_bar = "DF_NormalBarMetaFunctions",
	image = "DF_ImageMetaFunctions",
	slider = "DF_SliderMetaFunctions",
	split_bar = "DF_SplitBarMetaFunctions",
	aura_tracker = "DF_AuraTracker",
	healthBar = "DF_healthBarMetaFunctions",
	timebar = "DF_TimeBarMetaFunctions",
}

function DF:AddMemberForWidget(widgetName, memberType, memberName, func)
	if (DF.GlobalWidgetControlNames[widgetName]) then
		if (type(memberName) == "string" and (memberType == "SET" or memberType == "GET")) then
			if (func) then
				local widgetControlObject = _G [DF.GlobalWidgetControlNames[widgetName]]

				if (memberType == "SET") then
					widgetControlObject["SetMembers"][memberName] = func
				elseif (memberType == "GET") then
					widgetControlObject["GetMembers"][memberName] = func
				end
			else
				if (DF.debug) then
					error("Details! Framework: AddMemberForWidget invalid function.")
				end
			end
		else
			if (DF.debug) then
				error("Details! Framework: AddMemberForWidget unknown memberName or memberType.")
			end
		end
	else
		if (DF.debug) then
			error("Details! Framework: AddMemberForWidget unknown widget type: " .. (widgetName or "") .. ".")
		end
	end
end

-----------------------------

function DF:OpenInterfaceProfile()
	-- OptionsFrame1/2 should be registered if created with DF:CreateAddOn, so open to them directly
	if self.OptionsFrame1 then
		if SettingsPanel then
			--SettingsPanel:OpenToCategory(self.OptionsFrame1.name)
			local category = SettingsPanel:GetCategoryList():GetCategory(self.OptionsFrame1.name)
			if category then
				SettingsPanel:Open()
				SettingsPanel:SelectCategory(category)
				if self.OptionsFrame2 and category:HasSubcategories() then
					for _, subcategory in pairs(category:GetSubcategories()) do
						if subcategory:GetName() == self.OptionsFrame2.name then
							SettingsPanel:SelectCategory(subcategory)
							break
						end
					end
				end
			end
			return
		elseif InterfaceOptionsFrame_OpenToCategory then
			InterfaceOptionsFrame_OpenToCategory (self.OptionsFrame1)
			if self.OptionsFrame2 then
				InterfaceOptionsFrame_OpenToCategory (self.OptionsFrame2)
			end
			return
		end
	end

	-- fallback (broken as of ElvUI Skins in version 12.18+... maybe fix/change will come)
	InterfaceOptionsFrame_OpenToCategory (self.__name)
	InterfaceOptionsFrame_OpenToCategory (self.__name)
	for i = 1, 100 do
		local button = _G ["InterfaceOptionsFrameAddOnsButton" .. i]
		if (button) then
			local text = _G ["InterfaceOptionsFrameAddOnsButton" .. i .. "Text"]
			if (text) then
				text = text:GetText()
				if (text == self.__name) then
					local toggle = _G ["InterfaceOptionsFrameAddOnsButton" .. i .. "Toggle"]
					if (toggle) then
						if (toggle:GetNormalTexture():GetTexture():find("PlusButton")) then
							--is minimized, need expand
							toggle:Click()
							_G ["InterfaceOptionsFrameAddOnsButton" .. i+1]:Click()
						elseif (toggle:GetNormalTexture():GetTexture():find("MinusButton")) then
							--isn't minimized
							_G ["InterfaceOptionsFrameAddOnsButton" .. i+1]:Click()
						end
					end
					break
				end
			end
		else
			self:Msg("Couldn't not find the profile panel.")
			break
		end
	end
end

-----------------------------
---copy all members from #2 ... to #1 object
---@param object table
---@param ... any
---@return any
function DF:Mixin(object, ...) --safe copy from blizz api
	for i = 1, select("#", ...) do
		local mixin = select(i, ...)
		for key, value in pairs(mixin) do
			object[key] = value
		end
	end
	return object
end

-----------------------------
--animations

---create an animation 'hub' which is an animationGroup but with some extra functions
--tags: create, animation, hub, group, animationgroup, createanimationhub
--prompt example: create an animation group for the object 'variable name' with the start animation function doing 'what to do' and the finish animation function doing 'what to do'
---@param parent uiobject
---@param onPlay function?
---@param onFinished function?
---@return animationgroup
function DF:CreateAnimationHub(parent, onPlay, onFinished)
	local newAnimation = parent:CreateAnimationGroup()
	newAnimation:SetScript("OnPlay", onPlay)
	newAnimation:SetScript("OnFinished", onFinished)
	newAnimation:SetScript("OnStop", onFinished)
	newAnimation.NextAnimation = 1
	return newAnimation
end

---animation descriptions:
--tags: animation, create, alpha, scale, translation, rotation, path, vertexcolor, color, animation type, animation duration, animation order, animation object, return variable
--prompt example: create a new animation of type 'alpha' for the animation group 'variable name', with an order of 'number', a duration of 'number', from alpha 'number' to alpha 'number'
---* Create a new animation for an animation hub created with CreateAnimationHub().
---* Alpha: CreateAnimation(animGroup, "Alpha", order, duration, fromAlpha, toAlpha).
---* Scale: CreateAnimation(animGroup, "Scale", order, duration, fromScaleX, fromScaleY, toScaleX, toScaleY, originPoint, x, y).
---* Translation: CreateAnimation(animGroup, "Translation", order, duration, xOffset, yOffset).
---* Rotation: CreateAnimation(animGroup, "Rotation", order, duration, degrees, originPoint, x, y).
---* Path: CreateAnimation(animGroup, "Path", order, duration, xOffset, yOffset, curveType).
---* VertexColor: CreateAnimation(animGroup, "VertexColor", order, duration, r1, g1, b1, a1, r2, g2, b2, a2).
---@param animationGroup animationgroup the animation group created with CreateAnimationHub()
---@param animationType animationtype "Alpha", "Scale", "Translation", "Rotation", "Path", "VertexColor"
---@param order number the order of the animation, the lower the number, the earlier the animation will play
---@param duration number the duration of the animation in seconds
---@param arg1 any for Alpha: fromAlpha, for Scale: fromScaleX, for Translation: xOffset, for Rotation: degrees, for Path: xOffset, for VertexColor: r1
---@param arg2 any for Alpha: toAlpha, for Scale: fromScaleY, for Translation: yOffset, for Rotation: originPoint, for Path: yOffset, for VertexColor: g1
---@param arg3 any for Scale: toScaleX, for VertexColor: blue1, for Rotation: originXOffset, for Path: curveType, for VertexColor: b1
---@param arg4 any for Scale: toScaleY, for VertexColor: a1, for Rotation: originYOffset, for VertexColor: a1
---@param arg5 any for Scale: originPoint, for VertexColor: r2
---@param arg6 any for Scale: originXOffset, for VertexColor: g2
---@param arg7 any for Scale: originYOffset, for VertexColor: b2
---@param arg8 any for VertexColor: a2
---@return animation
function DF:CreateAnimation(animationGroup, animationType, order, duration, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)
	---@type animation
	local anim = animationGroup:CreateAnimation(animationType)
	--set the order of the animation, the 'order' parameter isn't passed, it will use the NextAnimation property of the animationGroup
	anim:SetOrder(order or animationGroup.NextAnimation)
	--set the duration of the animation
	anim:SetDuration(duration)

	animationType = string.upper(animationType)

	if (animationType == "ALPHA") then
		anim:SetFromAlpha(arg1)
		anim:SetToAlpha(arg2)

	elseif (animationType == "SCALE") then
		if (DF.IsDragonflight() or DF.IsNonRetailWowWithRetailAPI() or DF.IsWarWow()) then
			anim:SetScaleFrom(arg1, arg2)
			anim:SetScaleTo(arg3, arg4)
		else
			anim:SetFromScale(arg1, arg2)
			anim:SetToScale(arg3, arg4)
		end
		anim:SetOrigin(arg5 or "center", arg6 or 0, arg7 or 0) --point, originXOffset, originYOffset

	elseif (animationType == "ROTATION") then
		anim:SetDegrees(arg1) --degree
		anim:SetOrigin(arg2 or "center", arg3 or 0, arg4 or 0) --originPoint, originXOffset, originYOffset

	elseif (animationType == "TRANSLATION") then
		anim:SetOffset(arg1, arg2)

	elseif (animationType == "PATH") then
		local newControlPoint = anim:CreateControlPoint()
		anim:SetCurveType(arg4 or "SMOOTH")
		newControlPoint:SetOffset(arg2, arg3)
		newControlPoint:SetOrder(#anim:GetControlPoints())

	elseif (animationType == "VERTEXCOLOR" or animationType == "COLOR") then
		local r1, g1, b1, a1 = arg1, arg2, arg3, arg4
		local r2, g2, b2, a2 = arg5, arg6, arg7, arg8

		if ((type(r1) == "table" or type(r1) == "string") and (type(g1) == "table" or type(g1) == "string")) then
			r2, g2, b2, a2 = DF:ParseColors(g1)
			r1, g1, b1, a1 = DF:ParseColors(r1)

		elseif ((type(r1) == "table" or type(r1) == "string")) then
			r1, g1, b1, a1 = DF:ParseColors(r1)

		elseif ((type(r2) == "table" or type(r2) == "string")) then
			r2, g2, b2, a2 = DF:ParseColors(r2)
		end

		--CreateColor is a function declared in the game api that return a table with the color values in keys r, g, b, a
		anim:SetStartColor(CreateColor(r1, g1, b1, a1))
		anim:SetEndColor(CreateColor(r2, g2, b2, a2))
	end

	animationGroup.NextAnimation = animationGroup.NextAnimation + 1
	return anim
end

---receives an uiobject, when its parent get hover overed, starts the fade in animation
---start the fade out animation when the mouse leaves the parent
---@param UIObject uiobject
---@param fadeInTime number
---@param fadeOutTime number
---@param fadeInAlpha number
---@param fadeOutAlpha number
function DF:CreateFadeAnimation(UIObject, fadeInTime, fadeOutTime, fadeInAlpha, fadeOutAlpha)
	fadeInTime = fadeInTime or 0.1
	fadeOutTime = fadeOutTime or 0.1
	fadeInAlpha = fadeInAlpha or 1
	fadeOutAlpha = fadeOutAlpha or 0

	local fadeInAnimationHub = DF:CreateAnimationHub(UIObject, function() UIObject:Show(); UIObject:SetAlpha(fadeOutAlpha) end, function() UIObject:SetAlpha(fadeInAlpha) end)
	local fadeIn = DF:CreateAnimation(fadeInAnimationHub, "Alpha", 1, fadeInTime, fadeOutAlpha, fadeInAlpha)

	local fadeOutAnimationHub = DF:CreateAnimationHub(UIObject, nil, function() UIObject:Hide(); UIObject:SetAlpha(0) end)
	local fadeOut = DF:CreateAnimation(fadeOutAnimationHub, "Alpha", 2, fadeOutTime, fadeInAlpha, fadeOutAlpha)

	local scriptFrame
	--hook the parent OnEnter and OnLeave
	if (UIObject:IsObjectType("FontString") or UIObject:IsObjectType("Texture")) then
		scriptFrame = UIObject:GetParent()
	else
		scriptFrame = UIObject
	end

	---@cast scriptFrame frame
	scriptFrame:HookScript("OnEnter", function() fadeOutAnimationHub:Stop(); fadeInAnimationHub:Play() end)
	scriptFrame:HookScript("OnLeave", function() fadeInAnimationHub:Stop(); fadeOutAnimationHub:Play() end)
end

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--frame shakes

--frame shakes rely on OnUpdate scripts, we are using a built-in OnUpdate so is guarantee it'll run
local FrameshakeUpdateFrame = DetailsFrameworkFrameshakeControl or CreateFrame("frame", "DetailsFrameworkFrameshakeControl", UIParent)
--store the frame which has frame shakes registered
FrameshakeUpdateFrame.RegisteredFrames = FrameshakeUpdateFrame.RegisteredFrames or {}

FrameshakeUpdateFrame.RegisterFrame = function(newFrame)
	--add the frame into the registered frames to update
	DF.table.addunique(FrameshakeUpdateFrame.RegisteredFrames, newFrame)
end

--forward declared
local frameshake_DoUpdate

FrameshakeUpdateFrame:SetScript("OnUpdate", function(self, deltaTime)
	for i = 1, #FrameshakeUpdateFrame.RegisteredFrames do
		local parent = FrameshakeUpdateFrame.RegisteredFrames [i]
		--check if there's a shake running
		if (parent.__frameshakes.enabled > 0) then
			--update all shakes for this frame
			for i = 1, #parent.__frameshakes do
				local shakeObject = parent.__frameshakes [i]
				if (shakeObject.IsPlaying) then
					frameshake_DoUpdate(parent, shakeObject, deltaTime)
				end
			end
		end
	end
end)

local frameshake_ShakeFinished = function(parent, shakeObject)
	if (shakeObject.IsPlaying) then
		shakeObject.IsPlaying = false
		shakeObject.TimeLeft = 0
		shakeObject.IsFadingOut = false
		shakeObject.IsFadingIn = false

		--update the amount of shake running on this frame
		parent.__frameshakes.enabled = parent.__frameshakes.enabled - 1

		--restore the default anchors, in case where deltaTime was too small that didn't triggered an update
		for i = 1, #shakeObject.Anchors do
			local anchor = shakeObject.Anchors [i]

			--automatic anchoring and reanching needs to the reviwed in the future
			if (#anchor == 1) then
				local anchorTo = unpack(anchor)
				parent:ClearAllPoints()
				parent:SetPoint(anchorTo)

			elseif (#anchor == 2) then
				local anchorTo, point1 = unpack(anchor)
				parent:ClearAllPoints()
				parent:SetPoint(anchorTo, point1)

			elseif (#anchor == 3) then
				local anchorTo, point1, point2 = unpack(anchor)
				parent:SetPoint(anchorTo, point1, point2)

			elseif (#anchor == 5) then
				local anchorName1, anchorTo, anchorName2, point1, point2 = unpack(anchor)
				parent:SetPoint(anchorName1, anchorTo, anchorName2, point1, point2)
			end
		end
	end
end

--already declared above the update function
frameshake_DoUpdate = function(parent, shakeObject, deltaTime)
	--check delta time
	deltaTime = deltaTime or 0

	--update time left
	shakeObject.TimeLeft = max(shakeObject.TimeLeft - deltaTime, 0)

	if (shakeObject.TimeLeft > 0) then
		--update fade in and out
		if (shakeObject.IsFadingIn) then
			shakeObject.IsFadingInTime = shakeObject.IsFadingInTime + deltaTime
		end
		if (shakeObject.IsFadingOut) then
			shakeObject.IsFadingOutTime = shakeObject.IsFadingOutTime + deltaTime
		end

		--check if can disable fade in
		if (shakeObject.IsFadingIn and shakeObject.IsFadingInTime > shakeObject.FadeInTime) then
			shakeObject.IsFadingIn = false
		end

		--check if can enable fade out
		if (not shakeObject.IsFadingOut and shakeObject.TimeLeft < shakeObject.FadeOutTime) then
			shakeObject.IsFadingOut = true
			shakeObject.IsFadingOutTime = shakeObject.FadeOutTime - shakeObject.TimeLeft
		end

		--update position
		local scaleShake = min(shakeObject.IsFadingIn and (shakeObject.IsFadingInTime / shakeObject.FadeInTime) or 1, shakeObject.IsFadingOut and (1 - shakeObject.IsFadingOutTime / shakeObject.FadeOutTime) or 1)

		if (scaleShake > 0) then
			--delate the time by the frequency on both X and Y offsets
			shakeObject.XSineOffset = shakeObject.XSineOffset + (deltaTime * shakeObject.Frequency)
			shakeObject.YSineOffset = shakeObject.YSineOffset + (deltaTime * shakeObject.Frequency)

			--calc the new position
			local newX, newY
			if (shakeObject.AbsoluteSineX) then
				--absoluting only the sine wave, passing a negative scale will reverse the absolute direction
				newX = shakeObject.Amplitude * math.abs(math.sin(shakeObject.XSineOffset)) * scaleShake * shakeObject.ScaleX
			else
				newX = shakeObject.Amplitude * math.sin(shakeObject.XSineOffset) * scaleShake * shakeObject.ScaleX
			end

			if (shakeObject.AbsoluteSineY) then
				newY = shakeObject.Amplitude * math.abs(math.sin(shakeObject.YSineOffset)) * scaleShake * shakeObject.ScaleY
			else
				newY = shakeObject.Amplitude * math.sin(shakeObject.YSineOffset) * scaleShake * shakeObject.ScaleY
			end

			--apply the offset to the frame anchors
			for i = 1, #shakeObject.Anchors do
				local anchor = shakeObject.Anchors [i]

				if (#anchor == 1 or #anchor == 3) then
					local anchorTo, point1, point2 = unpack(anchor)
					point1 = point1 or 0
					point2 = point2 or 0
					parent:SetPoint(anchorTo, point1 + newX, point2 + newY)

				elseif (#anchor == 5) then
					local anchorName1, anchorTo, anchorName2, point1, point2 = unpack(anchor)
					parent:SetPoint(anchorName1, anchorTo, anchorName2, point1 + newX, point2 + newY)
				end
			end
		end
	else
		frameshake_ShakeFinished(parent, shakeObject)
	end
end

local frameshake_stop = function(parent, shakeObject)
	frameshake_ShakeFinished(parent, shakeObject)
end

--scale direction scales the X and Y coordinates, scale strength scales the amplitude and frequency
local frameshake_play = function(parent, shakeObject, scaleDirection, scaleAmplitude, scaleFrequency, scaleDuration)
	--check if is already playing
	if (shakeObject.TimeLeft > 0) then
		--reset the time left
		shakeObject.TimeLeft = shakeObject.Duration

		if (shakeObject.IsFadingOut) then
			if (shakeObject.FadeInTime > 0) then
				shakeObject.IsFadingIn = true
				--scale the current fade out into fade in, so it starts the fade in at the point where it was fading out
				shakeObject.IsFadingInTime = shakeObject.FadeInTime * (1 - shakeObject.IsFadingOutTime / shakeObject.FadeOutTime)
			else
				shakeObject.IsFadingIn = false
				shakeObject.IsFadingInTime = 0
			end

			--disable fade out and enable fade in
			shakeObject.IsFadingOut = false
			shakeObject.IsFadingOutTime = 0
		end
	else
		--create a new random offset
		shakeObject.XSineOffset = math.pi * 2 * math.random()
		shakeObject.YSineOffset = math.pi * 2 * math.random()

		--store the initial position if case it needs a reset
		shakeObject.StartedXSineOffset = shakeObject.XSineOffset
		shakeObject.StartedYSineOffset = shakeObject.YSineOffset

		--check if there's a fade in time
		if (shakeObject.FadeInTime > 0) then
			shakeObject.IsFadingIn = true
		else
			shakeObject.IsFadingIn = false
		end

		shakeObject.IsFadingInTime = 0
		shakeObject.IsFadingOut = false
		shakeObject.IsFadingOutTime = 0

		--apply custom scale
		shakeObject.ScaleX = (scaleDirection or 1) * shakeObject.OriginalScaleX
		shakeObject.ScaleY = (scaleDirection or 1) * shakeObject.OriginalScaleY
		shakeObject.Frequency = (scaleFrequency or 1) * shakeObject.OriginalFrequency
		shakeObject.Amplitude = (scaleAmplitude or 1) * shakeObject.OriginalAmplitude
		shakeObject.Duration = (scaleDuration or 1) * shakeObject.OriginalDuration

		--update the time left
		shakeObject.TimeLeft = shakeObject.Duration

		--check if is dynamic points
		if (shakeObject.IsDynamicAnchor) then
			wipe(shakeObject.Anchors)
			for i = 1, parent:GetNumPoints() do
				local p1, p2, p3, p4, p5 = parent:GetPoint(i)
				shakeObject.Anchors[#shakeObject.Anchors+1] = {p1, p2, p3, p4, p5}
			end
		end

		--update the amount of shake running on this frame
		parent.__frameshakes.enabled = parent.__frameshakes.enabled + 1

		if (parent:HasScript("OnUpdate") and not parent:GetScript("OnUpdate")) then
			parent:SetScript("OnUpdate", function()end)
		end
	end

	shakeObject.IsPlaying = true

	frameshake_DoUpdate(parent, shakeObject)
end

local frameshake_SetConfig = function(parent, shakeObject, duration, amplitude, frequency, absoluteSineX, absoluteSineY, scaleX, scaleY, fadeInTime, fadeOutTime)
	shakeObject.Amplitude = amplitude or shakeObject.Amplitude
	shakeObject.Frequency = frequency or shakeObject.Frequency
	shakeObject.Duration = duration or shakeObject.Duration
	shakeObject.FadeInTime = fadeInTime or shakeObject.FadeInTime
	shakeObject.FadeOutTime = fadeOutTime or shakeObject.FadeOutTime
	shakeObject.ScaleX  = scaleX or shakeObject.ScaleX
	shakeObject.ScaleY = scaleY or shakeObject.ScaleY

	if (absoluteSineX ~= nil) then
		shakeObject.AbsoluteSineX = absoluteSineX
	end

	if (absoluteSineY ~= nil) then
		shakeObject.AbsoluteSineY = absoluteSineY
	end

	shakeObject.OriginalScaleX = shakeObject.ScaleX
	shakeObject.OriginalScaleY = shakeObject.ScaleY
	shakeObject.OriginalFrequency = shakeObject.Frequency
	shakeObject.OriginalAmplitude = shakeObject.Amplitude
	shakeObject.OriginalDuration = shakeObject.Duration
end

---@class df_frameshake : table
---@field Amplitude number
---@field Frequency number
---@field Duration number
---@field FadeInTime number
---@field FadeOutTime number
---@field ScaleX number
---@field ScaleY number
---@field AbsoluteSineX boolean
---@field AbsoluteSineY boolean
---@field IsPlaying boolean
---@field TimeLeft number
---@field OriginalScaleX number
---@field OriginalScaleY number
---@field OriginalFrequency number
---@field OriginalAmplitude number
---@field OriginalDuration number
---@field PlayFrameShake fun(parent:uiobject, shakeObject:df_frameshake, scaleDirection:number?, scaleAmplitude:number?, scaleFrequency:number?, scaleDuration:number?)
---@field StopFrameShake fun(parent:uiobject, shakeObject:df_frameshake)
---@field SetFrameShakeSettings fun(parent:uiobject, shakeObject:df_frameshake, duration:number?, amplitude:number?, frequency:number?, absoluteSineX:boolean?, absoluteSineY:boolean?, scaleX:number?, scaleY:number?, fadeInTime:number?, fadeOutTime:number?)

---create a frame shake object
---@param parent uiobject
---@param duration number?
---@param amplitude number?
---@param frequency number?
---@param absoluteSineX boolean?
---@param absoluteSineY boolean?
---@param scaleX number?
---@param scaleY number?
---@param fadeInTime number?
---@param fadeOutTime number?
---@param anchorPoints table?
---@return df_frameshake
function DF:CreateFrameShake(parent, duration, amplitude, frequency, absoluteSineX, absoluteSineY, scaleX, scaleY, fadeInTime, fadeOutTime, anchorPoints)
	--create the shake table
	local frameShake = {
		Amplitude = amplitude or 2,
		Frequency = frequency or 5,
		Duration = duration or 0.3,
		FadeInTime = fadeInTime or 0.01,
		FadeOutTime = fadeOutTime or 0.01,
		ScaleX  = scaleX or 0.2,
		ScaleY = scaleY or 1,
		AbsoluteSineX = absoluteSineX,
		AbsoluteSineY = absoluteSineY,
		--
		IsPlaying = false,
		TimeLeft = 0,
	}

	frameShake.OriginalScaleX = frameShake.ScaleX
	frameShake.OriginalScaleY = frameShake.ScaleY
	frameShake.OriginalFrequency = frameShake.Frequency
	frameShake.OriginalAmplitude = frameShake.Amplitude
	frameShake.OriginalDuration = frameShake.Duration

	if (type(anchorPoints) ~= "table") then
		frameShake.IsDynamicAnchor = true
		frameShake.Anchors = {}
	else
		frameShake.Anchors = anchorPoints
	end

	--inject frame shake table into the frame
	if (not parent.__frameshakes) then
		parent.__frameshakes = {
			enabled = 0,
		}
		parent.PlayFrameShake = frameshake_play
		parent.StopFrameShake = frameshake_stop
		parent.UpdateFrameShake = frameshake_DoUpdate
		parent.SetFrameShakeSettings = frameshake_SetConfig

		--register the frame within the frame shake updater
		FrameshakeUpdateFrame.RegisterFrame (parent)
	end

	table.insert(parent.__frameshakes, frameShake)

	return frameShake
end


-----------------------------
--glow overlay

local glow_overlay_play = function(self)
	if (not self:IsShown()) then
		self:Show()
	end
	if (self.animOut) then
		if (self.animOut:IsPlaying()) then
			self.animOut:Stop()
		end
		if (not self.animIn:IsPlaying()) then
			self.animIn:Stop()
			self.animIn:Play()
		end
	elseif (self.ProcStartAnim) then
		if (not self.ProcStartAnim:IsPlaying()) then
			self.ProcStartAnim:Play()
		end
		if (not self.ProcLoop:IsPlaying()) then
			--self.ProcLoop:Play()
		end
	end
end

local glow_overlay_stop = function(self)
	if (self.animOut) then
		if (self.animOut:IsPlaying()) then
			self.animOut:Stop()
		end
		if (self.animIn:IsPlaying()) then
			self.animIn:Stop()
		end
	elseif (self.ProcStartAnim) then
		if (self.ProcStartAnim:IsPlaying()) then
			self.ProcStartAnim:Stop()
		end
	end
	if (self:IsShown()) then
		self:Hide()
	end
end

local glow_overlay_setcolor = function(self, antsColor, glowColor)
	if (antsColor) then
		local r, g, b, a = DF:ParseColors(antsColor)
		self.AntsColor = {r, g, b, a}
		if (self.ants) then
			self.ants:SetVertexColor(r, g, b, a)
		elseif (self.ProcLoopFlipbook) then
			self.ProcLoopFlipbook:SetVertexColor(r, g, b) --no alpha because of animation
			local anim1 = self.ProcLoop:GetAnimations()
			anim1:SetToAlpha(a)
		end
	end

	if (glowColor) then
		local r, g, b, a = DF:ParseColors(glowColor)
		self.GlowColor = {r, g, b, a}
		if (self.outerGlow) then
			self.outerGlow:SetVertexColor(r, g, b, a)
		elseif (self.ProcStartFlipbook) then
			self.ProcStartFlipbook:SetVertexColor(r, g, b) --no alpha because of animation
			local anim1, anim2, anim3 = self.ProcStartAnim:GetAnimations()
			anim1:SetToAlpha(a)
			anim3:SetFromAlpha(a)
		end
	end
end

local glow_overlay_onshow = function(self)
	glow_overlay_play(self)
end

local glow_overlay_onhide = function(self)
	glow_overlay_stop(self)
end

---create a glow overlay around a frame, return a frame and also add parent.overlay to the parent frame
---@param self table
---@param parent frame
---@param antsColor any
---@param glowColor any
function DF:CreateGlowOverlay(parent, antsColor, glowColor)
	local parentName = parent:GetName()
	local frameName = parentName and (parentName .. "Glow2") or "OverlayActionGlow" .. math.random(1, 10000000)

	if (frameName and string.len(frameName) > 50) then --shorten to work around too long names
		frameName = string.sub(frameName, string.len(frameName)-49)
	end

	local glowFrame
	if (buildInfo >= 110107) then --24-05-2025: in the 11.1.7 patch, the template used here does not exist anymore, replacement used
		glowFrame = CreateFrame("frame", frameName, parent, "ActionButtonSpellAlertTemplate")
	else
		glowFrame = CreateFrame("frame", frameName, parent, "ActionBarButtonSpellActivationAlert")
	end

	--local glowFrame = CreateFrame("frame", frameName, parent)
	glowFrame:HookScript("OnShow", glow_overlay_onshow)
	glowFrame:HookScript("OnHide", glow_overlay_onhide)

	glowFrame.Play = glow_overlay_play
	glowFrame.Stop = glow_overlay_stop
	glowFrame.SetColor = glow_overlay_setcolor

	glowFrame:SetColor(antsColor, glowColor)

	glowFrame:Hide()

	parent.overlay = glowFrame
	local frameWidth, frameHeight = parent:GetSize()

	local scale = 1.4

	--make the height/width available before the next frame:
	glowFrame:SetSize(frameWidth * scale, frameHeight * scale)
	glowFrame:SetPoint("topleft", parent, "topleft", -frameWidth * 0.32, frameHeight * 0.36)
	glowFrame:SetPoint("bottomright", parent, "bottomright", frameWidth * 0.32, -frameHeight * 0.36)

	if (glowFrame.outerGlow) then
		glowFrame.outerGlow:SetScale(1.2)
	end

	if (glowFrame.ProcStartFlipbook) then
		glowFrame.ProcStartAnim:Stop()
		glowFrame.ProcStartFlipbook:ClearAllPoints()
		glowFrame.ProcStartFlipbook:SetPoint("TOPLEFT", glowFrame, "TOPLEFT", -frameWidth * scale, frameHeight * scale)
		glowFrame.ProcStartFlipbook:SetPoint("BOTTOMRIGHT", glowFrame, "BOTTOMRIGHT", frameWidth * scale, -frameHeight * scale)
	end

	glowFrame:EnableMouse(false)

	return glowFrame
end

--custom glow with ants animation
local ants_set_texture_offset = function(self, leftOffset, rightOffset, topOffset, bottomOffset)
	leftOffset = leftOffset or 0
	rightOffset = rightOffset or 0
	topOffset = topOffset or 0
	bottomOffset = bottomOffset or 0

	self:ClearAllPoints()
	self:SetPoint("topleft", leftOffset, topOffset)
	self:SetPoint("bottomright", rightOffset, bottomOffset)
end


---create an "ant" animation around the frame, the name "ant" comes from the animation looking like small bright dots moving around the frame
---@param parent frame
---@param antTable df_anttable
---@param leftOffset number?
---@param rightOffset number?
---@param topOffset number?
---@param bottomOffset number?
---@return frame
function DF:CreateAnts(parent, antTable, leftOffset, rightOffset, topOffset, bottomOffset)
	leftOffset = leftOffset or 0
	rightOffset = rightOffset or 0
	topOffset = topOffset or 0
	bottomOffset = bottomOffset or 0

	local antsFrame = CreateFrame("frame", nil, parent)
	antsFrame:SetPoint("topleft", leftOffset, topOffset)
	antsFrame:SetPoint("bottomright", rightOffset, bottomOffset)

	antsFrame.SetOffset = ants_set_texture_offset

	local texture = antsFrame:CreateTexture(nil, "overlay")
	texture:SetAllPoints()
	texture:SetTexture(antTable.Texture)
	texture:SetBlendMode(antTable.BlendMode or "ADD")
	texture:SetVertexColor(DF:ParseColors(antTable.Color or "white"))
	antsFrame.Texture = texture

	antsFrame.AntTable = antTable

	antsFrame:SetScript("OnUpdate", function(self, deltaTime)
		AnimateTexCoords(texture, self.AntTable.TextureWidth, self.AntTable.TextureHeight, self.AntTable.TexturePartsWidth, self.AntTable.TexturePartsHeight, self.AntTable.AmountParts, deltaTime, self.AntTable.Throttle or 0.025)
	end)

	return antsFrame
end

--[=[ --test ants
do
	local f = DF:CreateAnts (UIParent)
end
--]=]

-----------------------------
--borders

local default_border_color1 = .5
local default_border_color2 = .3
local default_border_color3 = .1

local SetBorderAlpha = function(self, alpha1, alpha2, alpha3)
	self.Borders.Alpha1 = alpha1 or self.Borders.Alpha1
	self.Borders.Alpha2 = alpha2 or self.Borders.Alpha2
	self.Borders.Alpha3 = alpha3 or self.Borders.Alpha3

	for _, texture in ipairs(self.Borders.Layer1) do
		texture:SetAlpha(self.Borders.Alpha1)
	end
	for _, texture in ipairs(self.Borders.Layer2) do
		texture:SetAlpha(self.Borders.Alpha2)
	end
	for _, texture in ipairs(self.Borders.Layer3) do
		texture:SetAlpha(self.Borders.Alpha3)
	end
end

local SetBorderColor = function(self, r, g, b)
	for _, texture in ipairs(self.Borders.Layer1) do
		texture:SetColorTexture(r, g, b)
	end
	for _, texture in ipairs(self.Borders.Layer2) do
		texture:SetColorTexture(r, g, b)
	end
	for _, texture in ipairs(self.Borders.Layer3) do
		texture:SetColorTexture(r, g, b)
	end
end

local SetLayerVisibility = function(self, layer1Shown, layer2Shown, layer3Shown)
	for _, texture in ipairs(self.Borders.Layer1) do
		texture:SetShown(layer1Shown)
	end

	for _, texture in ipairs(self.Borders.Layer2) do
		texture:SetShown(layer2Shown)
	end

	for _, texture in ipairs(self.Borders.Layer3) do
		texture:SetShown(layer3Shown)
	end
end

---create a border using three textures for each side of the frame, each texture has a different transparency creating a smooth gradient effect
---the parent frame receives three new methods: SetBorderAlpha(a1, a2, a3), SetBorderColor(r, g, b), SetLayerVisibility(layer1Shown, layer2Shown, layer3Shown)
---@param self table
---@param parent frame
---@param alpha1 number?
---@param alpha2 number?
---@param alpha3 number?
function DF:CreateBorder(parent, alpha1, alpha2, alpha3)
	parent.Borders = {
		Layer1 = {},
		Layer2 = {},
		Layer3 = {},
		Alpha1 = alpha1 or default_border_color1,
		Alpha2 = alpha2 or default_border_color2,
		Alpha3 = alpha3 or default_border_color3,
	}

	parent.SetBorderAlpha = SetBorderAlpha
	parent.SetBorderColor = SetBorderColor
	parent.SetLayerVisibility = SetLayerVisibility

	do
		local leftBorder1 = parent:CreateTexture(nil, "background")
		PixelUtil.SetPoint(leftBorder1, "topleft", parent, "topleft", -1, 1)
		PixelUtil.SetPoint(leftBorder1, "bottomleft", parent, "bottomleft", -1, -1)
		leftBorder1:SetColorTexture(0, 0, 0, alpha1 or default_border_color1)

		local leftBorder2 = parent:CreateTexture(nil, "background")
		PixelUtil.SetPoint(leftBorder2, "topleft", parent, "topleft", -2, 2)
		PixelUtil.SetPoint(leftBorder2, "bottomleft", parent, "bottomleft", -2, -2)
		leftBorder2:SetColorTexture(0, 0, 0, alpha2 or default_border_color2)

		local leftBorder3 = parent:CreateTexture(nil, "background")
		PixelUtil.SetPoint(leftBorder3, "topleft", parent, "topleft", -3, 3)
		PixelUtil.SetPoint(leftBorder3, "bottomleft", parent, "bottomleft", -3, -3)
		leftBorder3:SetColorTexture(0, 0, 0, alpha3 or default_border_color3)

		table.insert(parent.Borders.Layer1, leftBorder1)
		table.insert(parent.Borders.Layer2, leftBorder2)
		table.insert(parent.Borders.Layer3, leftBorder3)
	end

	do
		local topBorder1 = parent:CreateTexture(nil, "background")
		PixelUtil.SetPoint(topBorder1, "topleft", parent, "topleft", 0, 1)
		PixelUtil.SetPoint(topBorder1, "topright", parent, "topright", 1, 1)
		topBorder1:SetColorTexture(0, 0, 0, alpha1 or default_border_color1)

		local topBorder2 = parent:CreateTexture(nil, "background")
		PixelUtil.SetPoint(topBorder2, "topleft", parent, "topleft", -1, 2)
		PixelUtil.SetPoint(topBorder2, "topright", parent, "topright", 2, 2)
		topBorder2:SetColorTexture(0, 0, 0, alpha2 or default_border_color2)

		local topBorder3 = parent:CreateTexture(nil, "background")
		PixelUtil.SetPoint(topBorder3, "topleft", parent, "topleft", -2, 3)
		PixelUtil.SetPoint(topBorder3, "topright", parent, "topright", 3, 3)
		topBorder3:SetColorTexture(0, 0, 0, alpha3 or default_border_color3)

		table.insert(parent.Borders.Layer1, topBorder1)
		table.insert(parent.Borders.Layer2, topBorder2)
		table.insert(parent.Borders.Layer3, topBorder3)
	end

	do
		local rightBorder1 = parent:CreateTexture(nil, "background")
		PixelUtil.SetPoint(rightBorder1, "topright", parent, "topright", 1, 0)
		PixelUtil.SetPoint(rightBorder1, "bottomright", parent, "bottomright", 1, -1)
		rightBorder1:SetColorTexture(0, 0, 0, alpha1 or default_border_color1)

		local rightBorder2 = parent:CreateTexture(nil, "background")
		PixelUtil.SetPoint(rightBorder2, "topright", parent, "topright", 2, 1)
		PixelUtil.SetPoint(rightBorder2, "bottomright", parent, "bottomright", 2, -2)
		rightBorder2:SetColorTexture(0, 0, 0, alpha2 or default_border_color2)

		local rightBorder3 = parent:CreateTexture(nil, "background")
		PixelUtil.SetPoint(rightBorder3, "topright", parent, "topright", 3, 2)
		PixelUtil.SetPoint(rightBorder3, "bottomright", parent, "bottomright", 3, -3)
		rightBorder3:SetColorTexture(0, 0, 0, alpha3 or default_border_color3)

		table.insert(parent.Borders.Layer1, rightBorder1)
		table.insert(parent.Borders.Layer2, rightBorder2)
		table.insert(parent.Borders.Layer3, rightBorder3)
	end

	do
		local bottomBorder1 = parent:CreateTexture(nil, "background")
		PixelUtil.SetPoint(bottomBorder1, "bottomleft", parent, "bottomleft", 0, -1)
		PixelUtil.SetPoint(bottomBorder1, "bottomright", parent, "bottomright", 0, -1)
		bottomBorder1:SetColorTexture(0, 0, 0, alpha1 or default_border_color1)

		local bottomBorder2 = parent:CreateTexture(nil, "background")
		PixelUtil.SetPoint(bottomBorder2, "bottomleft", parent, "bottomleft", -1, -2)
		PixelUtil.SetPoint(bottomBorder2, "bottomright", parent, "bottomright", 1, -2)
		bottomBorder2:SetColorTexture(0, 0, 0, alpha2 or default_border_color2)

		local bottomBorder3 = parent:CreateTexture(nil, "background")
		PixelUtil.SetPoint(bottomBorder3, "bottomleft", parent, "bottomleft", -2, -3)
		PixelUtil.SetPoint(bottomBorder3, "bottomright", parent, "bottomright", 2, -3)
		bottomBorder3:SetColorTexture(0, 0, 0, alpha3 or default_border_color3)

		table.insert(parent.Borders.Layer1, bottomBorder1)
		table.insert(parent.Borders.Layer2, bottomBorder2)
		table.insert(parent.Borders.Layer3, bottomBorder3)
	end
end

--DFNamePlateBorder as copy from "NameplateFullBorderTemplate" -> DF:CreateFullBorder (name, parent)
---@class df_nameplate_border_mixin : table
---@field SetVertexColor fun(self:border_frame, r:number, g:number, b:number, a:number)
---@field GetVertexColor fun(self:border_frame):number, number, number r, g, b
---@field SetBorderSizes fun(self:border_frame, borderSize:number, borderSizeMinPixels:number, upwardExtendHeightPixels:number, upwardExtendHeightMinPixels:number)
---@field UpdateSizes fun(self:border_frame)
---@field Left texture
---@field Right texture
---@field Bottom texture
---@field Top texture
---@field Textures texture[]
---@field borderSize number
---@field borderSizeMinPixels number
---@field upwardExtendHeightPixels number
---@field upwardExtendHeightMinPixels number

local DFNamePlateBorderTemplateMixin = {}

DF.NameplateBorderMixin = DFNamePlateBorderTemplateMixin

function DFNamePlateBorderTemplateMixin:SetVertexColor(r, g, b, a)
	for i, texture in ipairs(self.Textures) do
		texture:SetVertexColor(r, g, b, a);
	end
end

function DFNamePlateBorderTemplateMixin:GetVertexColor()
	for i, texture in ipairs(self.Textures) do
		return texture:GetVertexColor();
	end
end

function DFNamePlateBorderTemplateMixin:SetBorderSizes(borderSize, borderSizeMinPixels, upwardExtendHeightPixels, upwardExtendHeightMinPixels)
	self.borderSize = borderSize;
	self.borderSizeMinPixels = borderSizeMinPixels;
	self.upwardExtendHeightPixels = upwardExtendHeightPixels;
	self.upwardExtendHeightMinPixels = upwardExtendHeightMinPixels;
end

function DFNamePlateBorderTemplateMixin:UpdateSizes()
	local borderSize = self.borderSize or 1;
	local minPixels = self.borderSizeMinPixels or 2;

	local upwardExtendHeightPixels = self.upwardExtendHeightPixels or borderSize;
	local upwardExtendHeightMinPixels = self.upwardExtendHeightMinPixels or minPixels;

	PixelUtil.SetWidth(self.Left, borderSize, minPixels);
	PixelUtil.SetPoint(self.Left, "TOPRIGHT", self, "TOPLEFT", 0, upwardExtendHeightPixels, 0, upwardExtendHeightMinPixels);
	PixelUtil.SetPoint(self.Left, "BOTTOMRIGHT", self, "BOTTOMLEFT", 0, -borderSize, 0, minPixels);

	PixelUtil.SetWidth(self.Right, borderSize, minPixels);
	PixelUtil.SetPoint(self.Right, "TOPLEFT", self, "TOPRIGHT", 0, upwardExtendHeightPixels, 0, upwardExtendHeightMinPixels);
	PixelUtil.SetPoint(self.Right, "BOTTOMLEFT", self, "BOTTOMRIGHT", 0, -borderSize, 0, minPixels);

	PixelUtil.SetHeight(self.Bottom, borderSize, minPixels);
	PixelUtil.SetPoint(self.Bottom, "TOPLEFT", self, "BOTTOMLEFT", 0, 0);
	PixelUtil.SetPoint(self.Bottom, "TOPRIGHT", self, "BOTTOMRIGHT", 0, 0);

	if self.Top then
		PixelUtil.SetHeight(self.Top, borderSize, minPixels);
		PixelUtil.SetPoint(self.Top, "BOTTOMLEFT", self, "TOPLEFT", 0, 0);
		PixelUtil.SetPoint(self.Top, "BOTTOMRIGHT", self, "TOPRIGHT", 0, 0);
	end
end

---@class border_frame : frame, df_nameplate_border_mixin

function DF:CreateFullBorder(name, parent)
	local border = CreateFrame("Frame", name, parent)
	border:SetAllPoints()
	border:SetIgnoreParentScale(true)
	border:SetFrameLevel(border:GetParent():GetFrameLevel())
	border.Textures = {}
	Mixin(border, DFNamePlateBorderTemplateMixin)

	local left = border:CreateTexture("$parentLeft", "BACKGROUND", nil, -8)
	--left:SetDrawLayer("BACKGROUND", -8)
	left:SetColorTexture(1, 1, 1, 1)
	left:SetWidth(1.0)
	left:SetPoint("TOPRIGHT", border, "TOPLEFT", 0, 1.0)
	left:SetPoint("BOTTOMRIGHT", border, "BOTTOMLEFT", 0, -1.0)
	border.Left = left
	table.insert(border.Textures, left)

	local right = border:CreateTexture("$parentRight", "BACKGROUND", nil, -8)
	--right:SetDrawLayer("BACKGROUND", -8)
	right:SetColorTexture(1, 1, 1, 1)
	right:SetWidth(1.0)
	right:SetPoint("TOPLEFT", border, "TOPRIGHT", 0, 1.0)
	right:SetPoint("BOTTOMLEFT", border, "BOTTOMRIGHT", 0, -1.0)
	border.Right = right
	table.insert(border.Textures, right)

	local bottom = border:CreateTexture("$parentBottom", "BACKGROUND", nil, -8)
	--bottom:SetDrawLayer("BACKGROUND", -8)
	bottom:SetColorTexture(1, 1, 1, 1)
	bottom:SetHeight(1.0)
	bottom:SetPoint("TOPLEFT", border, "BOTTOMLEFT", 0, 0)
	bottom:SetPoint("TOPRIGHT", border, "BOTTOMRIGHT", 0, 0)
	border.Bottom = bottom
	table.insert(border.Textures, bottom)

	local top = border:CreateTexture("$parentTop", "BACKGROUND", nil, -8)
	--top:SetDrawLayer("BACKGROUND", -8)
	top:SetColorTexture(1, 1, 1, 1)
	top:SetHeight(1.0)
	top:SetPoint("BOTTOMLEFT", border, "TOPLEFT", 0, 0)
	top:SetPoint("BOTTOMRIGHT", border, "TOPRIGHT", 0, 0)
	border.Top = top
	table.insert(border.Textures, top)

	return border
end

function DF:CreateBorderSolid (parent, size)

end

function DF:CreateBorderWithSpread(parent, alpha1, alpha2, alpha3, size, spread)
	parent.Borders = {
		Layer1 = {},
		Layer2 = {},
		Layer3 = {},
		Alpha1 = alpha1 or default_border_color1,
		Alpha2 = alpha2 or default_border_color2,
		Alpha3 = alpha3 or default_border_color3,
	}

	parent.SetBorderAlpha = SetBorderAlpha
	parent.SetBorderColor = SetBorderColor
	parent.SetLayerVisibility = SetLayerVisibility

	size = size or 1
	local minPixels = 1
	local spread = 0

	--left
	local border1 = parent:CreateTexture(nil, "background")
	border1:SetColorTexture(0, 0, 0, alpha1 or default_border_color1)
	PixelUtil.SetPoint(border1, "topleft", parent, "topleft", -1 + spread, 1 + (-spread), 0, 0)
	PixelUtil.SetPoint(border1, "bottomleft", parent, "bottomleft", -1 + spread, -1 + spread, 0, 0)
	PixelUtil.SetWidth (border1, size, minPixels)

	local border2 = parent:CreateTexture(nil, "background")
	PixelUtil.SetPoint(border2, "topleft", parent, "topleft", -2 + spread, 2 + (-spread))
	PixelUtil.SetPoint(border2, "bottomleft", parent, "bottomleft", -2 + spread, -2 + spread)
	border2:SetColorTexture(0, 0, 0, alpha2 or default_border_color2)
	PixelUtil.SetWidth (border2, size, minPixels)

	local border3 = parent:CreateTexture(nil, "background")
	PixelUtil.SetPoint(border3, "topleft", parent, "topleft", -3 + spread, 3 + (-spread))
	PixelUtil.SetPoint(border3, "bottomleft", parent, "bottomleft", -3 + spread, -3 + spread)
	border3:SetColorTexture(0, 0, 0, alpha3 or default_border_color3)
	PixelUtil.SetWidth (border3, size, minPixels)

	table.insert(parent.Borders.Layer1, border1)
	table.insert(parent.Borders.Layer2, border2)
	table.insert(parent.Borders.Layer3, border3)

	--top
	local border1 = parent:CreateTexture(nil, "background")
	PixelUtil.SetPoint(border1, "topleft", parent, "topleft", 0 + spread, 1 + (-spread))
	PixelUtil.SetPoint(border1, "topright", parent, "topright", 1 + (-spread), 1 + (-spread))
	border1:SetColorTexture(0, 0, 0, alpha1 or default_border_color1)
	PixelUtil.SetHeight(border1, size, minPixels)

	local border2 = parent:CreateTexture(nil, "background")
	PixelUtil.SetPoint(border2, "topleft", parent, "topleft", -1 + spread, 2 + (-spread))
	PixelUtil.SetPoint(border2, "topright", parent, "topright", 2 + (-spread), 2 + (-spread))
	border2:SetColorTexture(0, 0, 0, alpha2 or default_border_color2)
	PixelUtil.SetHeight(border2, size, minPixels)

	local border3 = parent:CreateTexture(nil, "background")
	PixelUtil.SetPoint(border3, "topleft", parent, "topleft", -2 + spread, 3 + (-spread))
	PixelUtil.SetPoint(border3, "topright", parent, "topright", 3 + (-spread), 3 + (-spread))
	border3:SetColorTexture(0, 0, 0, alpha3 or default_border_color3)
	PixelUtil.SetHeight(border3, size, minPixels)

	table.insert(parent.Borders.Layer1, border1)
	table.insert(parent.Borders.Layer2, border2)
	table.insert(parent.Borders.Layer3, border3)

	--right
	local border1 = parent:CreateTexture(nil, "background")
	PixelUtil.SetPoint(border1, "topright", parent, "topright", 1 + (-spread), 0 + (-spread))
	PixelUtil.SetPoint(border1, "bottomright", parent, "bottomright", 1 + (-spread), -1 + spread)
	border1:SetColorTexture(0, 0, 0, alpha1 or default_border_color1)
	PixelUtil.SetWidth (border1, size, minPixels)

	local border2 = parent:CreateTexture(nil, "background")
	PixelUtil.SetPoint(border2, "topright", parent, "topright", 2 + (-spread), 1 + (-spread))
	PixelUtil.SetPoint(border2, "bottomright", parent, "bottomright", 2 + (-spread), -2 + spread)
	border2:SetColorTexture(0, 0, 0, alpha2 or default_border_color2)
	PixelUtil.SetWidth (border2, size, minPixels)

	local border3 = parent:CreateTexture(nil, "background")
	PixelUtil.SetPoint(border3, "topright", parent, "topright", 3 + (-spread), 2 + (-spread))
	PixelUtil.SetPoint(border3, "bottomright", parent, "bottomright", 3 + (-spread), -3 + spread)
	border3:SetColorTexture(0, 0, 0, alpha3 or default_border_color3)
	PixelUtil.SetWidth (border3, size, minPixels)

	table.insert(parent.Borders.Layer1, border1)
	table.insert(parent.Borders.Layer2, border2)
	table.insert(parent.Borders.Layer3, border3)

	local border1 = parent:CreateTexture(nil, "background")
	PixelUtil.SetPoint(border1, "bottomleft", parent, "bottomleft", 0 + spread, -1 + spread)
	PixelUtil.SetPoint(border1, "bottomright", parent, "bottomright", 0 + (-spread), -1 + spread)
	border1:SetColorTexture(0, 0, 0, alpha1 or default_border_color1)
	PixelUtil.SetHeight(border1, size, minPixels)

	local border2 = parent:CreateTexture(nil, "background")
	PixelUtil.SetPoint(border2, "bottomleft", parent, "bottomleft", -1 + spread, -2 + spread)
	PixelUtil.SetPoint(border2, "bottomright", parent, "bottomright", 1 + (-spread), -2 + spread)
	border2:SetColorTexture(0, 0, 0, alpha2 or default_border_color2)
	PixelUtil.SetHeight(border2, size, minPixels)

	local border3 = parent:CreateTexture(nil, "background")
	PixelUtil.SetPoint(border3, "bottomleft", parent, "bottomleft", -2 + spread, -3 + spread)
	PixelUtil.SetPoint(border3, "bottomright", parent, "bottomright", 2 + (-spread), -3 + spread)
	border3:SetColorTexture(0, 0, 0, alpha3 or default_border_color3)
	PixelUtil.SetHeight(border3, size, minPixels)

	table.insert(parent.Borders.Layer1, border1)
	table.insert(parent.Borders.Layer2, border2)
	table.insert(parent.Borders.Layer3, border3)

end

function DF:ReskinSlider(slider, heightOffset)
	if (slider.slider) then
		slider.cima:SetNormalTexture([[Interface\Buttons\Arrow-Up-Up]])
		slider.cima:SetPushedTexture([[Interface\Buttons\Arrow-Up-Down]])
		slider.cima:SetDisabledTexture([[Interface\Buttons\Arrow-Up-Disabled]])
		slider.cima:GetNormalTexture():ClearAllPoints()
		slider.cima:GetPushedTexture():ClearAllPoints()
		slider.cima:GetDisabledTexture():ClearAllPoints()
		slider.cima:GetNormalTexture():SetPoint("center", slider.cima, "center", 1, 1)
		slider.cima:GetPushedTexture():SetPoint("center", slider.cima, "center", 1, 1)
		slider.cima:GetDisabledTexture():SetPoint("center", slider.cima, "center", 1, 1)
		slider.cima:SetSize(16, 16)

		slider.baixo:SetNormalTexture([[Interface\Buttons\Arrow-Down-Up]])
		slider.baixo:SetPushedTexture([[Interface\Buttons\Arrow-Down-Down]])
		slider.baixo:SetDisabledTexture([[Interface\Buttons\Arrow-Down-Disabled]])
		slider.baixo:GetNormalTexture():ClearAllPoints()
		slider.baixo:GetPushedTexture():ClearAllPoints()
		slider.baixo:GetDisabledTexture():ClearAllPoints()
		slider.baixo:GetNormalTexture():SetPoint("center", slider.baixo, "center", 1, -5)
		slider.baixo:GetPushedTexture():SetPoint("center", slider.baixo, "center", 1, -5)
		slider.baixo:GetDisabledTexture():SetPoint("center", slider.baixo, "center", 1, -5)
		slider.baixo:SetSize(16, 16)

		slider.slider:cimaPoint(0, 13)
		slider.slider:baixoPoint(0, -13)
		slider.slider.thumb:SetTexture([[Interface\AddOns\Details\images\icons2]])
		slider.slider.thumb:SetTexCoord(482/512, 492/512, 104/512, 120/512)
		slider.slider.thumb:SetSize(12, 12)
		slider.slider.thumb:SetVertexColor(0.6, 0.6, 0.6, 0.95)

	else
		--up button
		local offset = 1 --space between the scrollbox and the scrollar

		local backgroundColor_Red = 0.1
		local backgroundColor_Green = 0.1
		local backgroundColor_Blue = 0.1
		local backgroundColor_Alpha = 1
		local backdrop_Alpha = 0.3

		do
			local normalTexture = slider.ScrollBar.ScrollUpButton.Normal
			normalTexture:SetTexture([[Interface\Buttons\Arrow-Up-Up]])
			normalTexture:SetTexCoord(0, 1, .2, 1)

			normalTexture:SetPoint("topleft", slider.ScrollBar.ScrollUpButton, "topleft", offset, 0)
			normalTexture:SetPoint("bottomright", slider.ScrollBar.ScrollUpButton, "bottomright", offset, 0)

			local pushedTexture = slider.ScrollBar.ScrollUpButton.Pushed
			pushedTexture:SetTexture([[Interface\Buttons\Arrow-Up-Down]])
			pushedTexture:SetTexCoord(0, 1, .2, 1)

			pushedTexture:SetPoint("topleft", slider.ScrollBar.ScrollUpButton, "topleft", offset, 0)
			pushedTexture:SetPoint("bottomright", slider.ScrollBar.ScrollUpButton, "bottomright", offset, 0)

			local disabledTexture = slider.ScrollBar.ScrollUpButton.Disabled
			disabledTexture:SetTexture([[Interface\Buttons\Arrow-Up-Disabled]])
			disabledTexture:SetTexCoord(0, 1, .2, 1)
			disabledTexture:SetAlpha(.5)

			disabledTexture:SetPoint("topleft", slider.ScrollBar.ScrollUpButton, "topleft", offset, 0)
			disabledTexture:SetPoint("bottomright", slider.ScrollBar.ScrollUpButton, "bottomright", offset, 0)

			slider.ScrollBar.ScrollUpButton:SetSize(16, 16)

			if (not slider.ScrollBar.ScrollUpButton.BackgroundTexture) then
				local backgroundTexture = slider.ScrollBar.ScrollUpButton:CreateTexture(nil, "border")
				slider.ScrollBar.ScrollUpButton.BackgroundTexture = backgroundTexture

				backgroundTexture:SetColorTexture(backgroundColor_Red, backgroundColor_Green, backgroundColor_Blue)
				backgroundTexture:SetAlpha(backgroundColor_Alpha)

				backgroundTexture:SetPoint("topleft", slider.ScrollBar.ScrollUpButton, "topleft", 1, 0)
				backgroundTexture:SetPoint("bottomright", slider.ScrollBar.ScrollUpButton, "bottomright", -1, 0)
			end

			DF:Mixin(slider.ScrollBar.ScrollUpButton, BackdropTemplateMixin)
			slider.ScrollBar.ScrollUpButton:SetBackdrop({edgeFile = [[Interface\Buttons\WHITE8X8]], edgeSize = 1})
			slider.ScrollBar.ScrollUpButton:SetBackdropBorderColor(0, 0, 0, backdrop_Alpha)
		end

		--down button
		do
			local normalTexture = slider.ScrollBar.ScrollDownButton.Normal
			normalTexture:SetTexture([[Interface\Buttons\Arrow-Down-Up]])
			normalTexture:SetTexCoord(0, 1, 0, .8)

			normalTexture:SetPoint("topleft", slider.ScrollBar.ScrollDownButton, "topleft", offset, -4)
			normalTexture:SetPoint("bottomright", slider.ScrollBar.ScrollDownButton, "bottomright", offset, -4)

			local pushedTexture = slider.ScrollBar.ScrollDownButton.Pushed
			pushedTexture:SetTexture([[Interface\Buttons\Arrow-Down-Down]])
			pushedTexture:SetTexCoord(0, 1, 0, .8)

			pushedTexture:SetPoint("topleft", slider.ScrollBar.ScrollDownButton, "topleft", offset, -4)
			pushedTexture:SetPoint("bottomright", slider.ScrollBar.ScrollDownButton, "bottomright", offset, -4)

			local disabledTexture = slider.ScrollBar.ScrollDownButton.Disabled
			disabledTexture:SetTexture([[Interface\Buttons\Arrow-Down-Disabled]])
			disabledTexture:SetTexCoord(0, 1, 0, .8)
			disabledTexture:SetAlpha(.5)

			disabledTexture:SetPoint("topleft", slider.ScrollBar.ScrollDownButton, "topleft", offset, -4)
			disabledTexture:SetPoint("bottomright", slider.ScrollBar.ScrollDownButton, "bottomright", offset, -4)

			slider.ScrollBar.ScrollDownButton:SetSize(16, 16)

			if (not slider.ScrollBar.ScrollDownButton.BackgroundTexture) then
				local backgroundTexture = slider.ScrollBar.ScrollDownButton:CreateTexture(nil, "border")
				slider.ScrollBar.ScrollDownButton.BackgroundTexture = backgroundTexture

				backgroundTexture:SetColorTexture(backgroundColor_Red, backgroundColor_Green, backgroundColor_Blue)
				backgroundTexture:SetAlpha(backgroundColor_Alpha)

				backgroundTexture:SetPoint("topleft", slider.ScrollBar.ScrollDownButton, "topleft", 1, 0)
				backgroundTexture:SetPoint("bottomright", slider.ScrollBar.ScrollDownButton, "bottomright", -1, 0)
			end

			DF:Mixin(slider.ScrollBar.ScrollDownButton, BackdropTemplateMixin)
			slider.ScrollBar.ScrollDownButton:SetBackdrop({edgeFile = [[Interface\Buttons\WHITE8X8]], edgeSize = 1})
			slider.ScrollBar.ScrollDownButton:SetBackdropBorderColor(0, 0, 0, backdrop_Alpha)
		end

		--if the parent has a editbox, this is a code editor
		if (slider:GetParent().editbox) then
			slider.ScrollBar:SetPoint("TOPLEFT", slider, "TOPRIGHT", 12 + offset, -6)
			slider.ScrollBar:SetPoint("BOTTOMLEFT", slider, "BOTTOMRIGHT", 12 + offset, 6 + (heightOffset and heightOffset*-1 or 0))

		else
			slider.ScrollBar:SetPoint("TOPLEFT", slider, "TOPRIGHT", 6, -16)
			slider.ScrollBar:SetPoint("BOTTOMLEFT", slider, "BOTTOMRIGHT", 6, 16 + (heightOffset and heightOffset*-1 or 0))
		end

		slider.ScrollBar.ThumbTexture:SetColorTexture(.5, .5, .5, .3)
		slider.ScrollBar.ThumbTexture:SetSize(14, 8)

		if (not slider.ScrollBar.SliderTexture) then
			local alpha = 1
			local offset = 1
			slider.ScrollBar.SliderTexture = slider.ScrollBar:CreateTexture(nil, "background")
			slider.ScrollBar.SliderTexture:SetColorTexture(backgroundColor_Red, backgroundColor_Green, backgroundColor_Blue)
			slider.ScrollBar.SliderTexture:SetAlpha(backgroundColor_Alpha)
			slider.ScrollBar.SliderTexture:SetPoint("TOPLEFT", slider.ScrollBar, "TOPLEFT", offset, -2)
			slider.ScrollBar.SliderTexture:SetPoint("BOTTOMRIGHT", slider.ScrollBar, "BOTTOMRIGHT", -offset, 2)
		end

		DF:Mixin(slider.ScrollBar, BackdropTemplateMixin)
		slider.ScrollBar:SetBackdrop({edgeFile = [[Interface\Buttons\WHITE8X8]], edgeSize = 1})
		slider.ScrollBar:SetBackdropBorderColor(0, 0, 0, backdrop_Alpha)
	end
end

function DF:GetCurrentClassName()
	local className = UnitClass("player")
	return className
end

function DF:GetCurrentSpecName()
	local specIndex = DF.GetSpecialization()
	if (specIndex) then
		local specId, specName = DF.GetSpecializationInfo(specIndex)
		if (specId and specId ~= 0) then
			return specName
		end
	end
end

function DF:GetCurrentSpec()
	local specIndex = DF.GetSpecialization()
	if (specIndex) then
		local specId = DF.GetSpecializationInfo(specIndex)
		if (specId and specId ~= 0) then
			return specId
		end
	end
end

function DF:GetCurrentSpecId()
	return DF:GetCurrentSpec()
end

local specs_per_class = {
	["DEMONHUNTER"] = {577, 581}, --havoc, vengence
	["DEATHKNIGHT"] = {250, 251, 252},
	["WARRIOR"] = {71, 72, 73},
	["MAGE"] = {62, 63, 64},
	["ROGUE"] = {259, 260, 261},
	["DRUID"] = {102, 103, 104, 105},
	["HUNTER"] = {253, 254, 255},
	["SHAMAN"] = {262, 263, 264},
	["PRIEST"] = {256, 257, 258},
	["WARLOCK"] = {265, 266, 267},
	["PALADIN"] = {65, 66, 70},
	["MONK"] = {268, 269, 270},
	["EVOKER"] = {1467, 1468, 1473},
}


---return an array table with the spec ids the class can have
---@param engClass string
---@return table
function DF:GetClassSpecIDs(engClass)
	return specs_per_class[engClass]
end
function DF:GetClassSpecIds(engClass) --naming conventions
	return DF:GetClassSpecIDs(engClass)
end

--kinda deprecated
local getDragonflightTalents = function()
	if (not ClassTalentFrame) then
		ClassTalentFrame_LoadUI()
	end

	if (not DF.TalentExporter) then
    	local talentExporter = CreateFromMixins(ClassTalentImportExportMixin)
		DF.TalentExporter = talentExporter
	end

	local exportStream = ExportUtil.MakeExportDataStream()

	local configId = C_ClassTalents.GetActiveConfigID()
	if (not configId) then
		return ""
	end

	local configInfo = C_Traits.GetConfigInfo(configId)
	if (not configInfo) then
		return ""
	end

	local currentSpecID = PlayerUtil.GetCurrentSpecID()

	local treeInfo = C_Traits.GetTreeInfo(configId, configInfo.treeIDs[1])
	local treeHash = C_Traits.GetTreeHash(treeInfo.ID)
	local serializationVersion = C_Traits.GetLoadoutSerializationVersion()

	DF.TalentExporter:WriteLoadoutHeader(exportStream, serializationVersion, currentSpecID, treeHash)
	DF.TalentExporter:WriteLoadoutContent(exportStream, configId, treeInfo.ID)

	return exportStream:GetExportString()
end

local getDragonflightTalentsEasy = function()
	local activeConfigID = C_ClassTalents.GetActiveConfigID()
	if (activeConfigID and activeConfigID > 0) then
		return C_Traits.GenerateImportString(activeConfigID)
	end
	return ""
end

--/dump DetailsFramework:GetDragonlightTalentString()
function DF:GetDragonlightTalentString()
	local runOkay, errorText = pcall(getDragonflightTalentsEasy)
	if (not runOkay) then
		DF:Msg("error 0x4517", errorText)
		return ""
	else
		local talentString = errorText
		return talentString
	end
end

local dispatch_error = function(context, errortext)
	error((context or "") .. (errortext or ""))
end

--call a function with payload, if the callback doesn't exists, quit silently
function DF:QuickDispatch(func, ...)
	if (type(func) ~= "function") then
		return
	end

	xpcall(func, geterrorhandler(), ...)

	return true
end

---call a function in safe mode with payload
---@param func function
---@param ... any
---@return any
function DF:Dispatch(func, ...)
	assert(type(func) == "function", "DetailsFramework:Dispatch(func) expect a function as parameter 1. Received: " .. type(func) .. " instead.")
	return select(2, xpcall(func, geterrorhandler(), ...))
end

--[=[
	DF:CoreDispatch(func, context, ...)
	safe call a function making an error window with what caused, context and traceback of the error
	this func is only used inside the framework for sensitive calls where the func must run without errors
	@func = the function which will be called
	@context = what made the function be called
	... parameters to pass in the function call
--]=]
function DF:CoreDispatch(context, func, ...)
	if (type(func) ~= "function") then
		local stack = debugstack(2)
		local errortext = "D!Framework " .. context .. " error: invalid function to call\n====================\n" .. stack .. "\n====================\n"
		error(errortext)
	end

	local okay, result1, result2, result3, result4 = xpcall(func, geterrorhandler(), ...)

	--if (not okay) then --when using pcall
		--local stack = debugstack(2)
		--local errortext = "D!Framework (" .. context .. ") error: " .. result1 .. "\n====================\n" .. stack .. "\n====================\n"
		--error(errortext)
	--end

	return result1, result2, result3, result4
end


DF.ClassIndexToFileName = {
	[6] = "DEATHKNIGHT",
	[1] = "WARRIOR",
	[4] = "ROGUE",
	[8] = "MAGE",
	[5] = "PRIEST",
	[3] = "HUNTER",
	[9] = "WARLOCK",
	[12] = "DEMONHUNTER",
	[7] = "SHAMAN",
	[11] = "DRUID",
	[10] = "MONK",
	[2] = "PALADIN",
	[13] = "EVOKER",
}

--GetNumClasses()

DF.ClassFileNameToIndex = {
	["WARRIOR"] = 1,
	["PALADIN"] = 2,
	["HUNTER"] = 3,
	["ROGUE"] = 4,
	["PRIEST"] = 5,
	["DEATHKNIGHT"] = 6,
	["SHAMAN"] = 7,
	["MAGE"] = 8,
	["WARLOCK"] = 9,
	["MONK"] = 10,
	["DRUID"] = 11,
	["DEMONHUNTER"] = 12,
	["EVOKER"] = 13,
}
DF.ClassCache = {}

function DF:GetClassIdByFileName(fileName)
	return DF.ClassFileNameToIndex[fileName]
end

function DF:GetClassList()
	if (next (DF.ClassCache)) then
		return DF.ClassCache
	end

	for className, classIndex in pairs(DF.ClassFileNameToIndex) do
		local classTable = C_CreatureInfo.GetClassInfo(classIndex)
		if classTable then
			local t = {
				ID = classIndex,
				Name = classTable.className,
				Texture = [[Interface\GLUES\CHARACTERCREATE\UI-CharacterCreate-Classes]],
				TexCoord = CLASS_ICON_TCOORDS[className],
				FileString = className,
			}
			table.insert(DF.ClassCache, t)
		end
	end

	return DF.ClassCache
end

--hardcoded race list
DF.RaceList = {
	[1] = "Human",
	[2] = "Orc",
	[3] = "Dwarf",
	[4] = "NightElf",
	[5] = "Scourge",
	[6] = "Tauren",
	[7] = "Gnome",
	[8] = "Troll",
	[9] = "Goblin",
	[10] = "BloodElf",
	[11] = "Draenei",
	[22] = "Worgen",
	[24] = "Pandaren",
}

DF.AlliedRaceList = {
	[27] = "Nightborne",
	[29] = "HighmountainTauren",
	[31] = "VoidElf",
	[33] = "LightforgedDraenei",
	[35] = "ZandalariTroll",
	[36] = "KulTiran",
	[38] = "DarkIronDwarf",
	[40] = "Vulpera",
	[41] = "MagharOrc",
}

local slotIdToIcon = {
	[1] = "Interface\\ICONS\\" .. "INV_Helmet_29", --head
	[2] = "Interface\\ICONS\\" .. "INV_Jewelry_Necklace_07", --neck
	[3] = "Interface\\ICONS\\" .. "INV_Shoulder_25", --shoulder
	[5] = "Interface\\ICONS\\" .. "INV_Chest_Cloth_08", --chest
	[6] = "Interface\\ICONS\\" .. "INV_Belt_15", --waist
	[7] = "Interface\\ICONS\\" .. "INV_Pants_08", --legs
	[8] = "Interface\\ICONS\\" .. "INV_Boots_Cloth_03", --feet
	[9] = "Interface\\ICONS\\" .. "INV_Bracer_07", --wrist
	[10] = "Interface\\ICONS\\" .. "INV_Gauntlets_17", --hands
	[11] = "Interface\\ICONS\\" .. "INV_Jewelry_Ring_22", --finger 1
	[12] = "Interface\\ICONS\\" .. "INV_Jewelry_Ring_22", --finger 2
	[13] = "Interface\\ICONS\\" .. "INV_Jewelry_Talisman_07", --trinket 1
	[14] = "Interface\\ICONS\\" .. "INV_Jewelry_Talisman_07", --trinket 2
	[15] = "Interface\\ICONS\\" .. "INV_Misc_Cape_19", --back
	[16] = "Interface\\ICONS\\" .. "INV_Sword_39", --main hand
	[17] = "Interface\\ICONS\\" .. "INV_Sword_39", --off hand
}

function DF:GetArmorIconByArmorSlot(equipSlotId)
	return slotIdToIcon[equipSlotId] or ""
end


--store and return a list of character races, always return the non-localized value
DF.RaceCache = {}
function DF:GetCharacterRaceList()
	if (next (DF.RaceCache)) then
		return DF.RaceCache
	end

	for i = 1, 100 do
		local raceInfo = C_CreatureInfo.GetRaceInfo(i)
		if (raceInfo and DF.RaceList [raceInfo.raceID]) then
			table.insert(DF.RaceCache, {Name = raceInfo.raceName, FileString = raceInfo.clientFileString, ID = raceInfo.raceID})
		end

		if IS_WOW_PROJECT_MAINLINE then
			local alliedRaceInfo = C_AlliedRaces.GetRaceInfoByID(i)
			if (alliedRaceInfo and DF.AlliedRaceList [alliedRaceInfo.raceID]) then
				table.insert(DF.RaceCache, {Name = alliedRaceInfo.maleName, FileString = alliedRaceInfo.raceFileString, ID = alliedRaceInfo.raceID})
			end
		end
	end

	return DF.RaceCache
end

--get a list of talents for the current spec the player is using
--if onlySelected return an index table with only the talents the character has selected
--if onlySelectedHash return a hash table with [spelID] = true
function DF:GetCharacterTalents(bOnlySelected, bOnlySelectedHash)
	local talentList = {}
	local version, build, date, tocversion = GetBuildInfo()

	if (tocversion >= 70000 and tocversion <= 99999) then
		for i = 1, 7 do
			for o = 1, 3 do
				local talentID, name, texture, selected, available = GetTalentInfo(i, o, 1)
				if (bOnlySelectedHash) then
					if (selected) then
						talentList[talentID] = true
						break
					end
				elseif (bOnlySelected) then
					if (selected) then
						table.insert(talentList, {Name = name, ID = talentID, Texture = texture, IsSelected = selected})
						break
					end
				else
					table.insert(talentList, {Name = name, ID = talentID, Texture = texture, IsSelected = selected})
				end
			end
		end

	elseif (tocversion >= 100000) then
		if (not bOnlySelected) then
			return DF:GetAllTalents()
		end

		local configId = C_ClassTalents.GetActiveConfigID()
		if (configId) then
			local configInfo = C_Traits.GetConfigInfo(configId)
			--get the spells from the SPEC from talents
			for treeIndex, treeId in ipairs(configInfo.treeIDs) do
				local treeNodes = C_Traits.GetTreeNodes(treeId)

				for nodeIdIndex, treeNodeID in ipairs(treeNodes) do
					local traitNodeInfo = C_Traits.GetNodeInfo(configId, treeNodeID)

					if (traitNodeInfo) then
						local activeEntry = traitNodeInfo.activeEntry
						local entryIds = traitNodeInfo.entryIDs

						for i = 1, #entryIds do
							local entryId = entryIds[i] --number
							local traitEntryInfo = C_Traits.GetEntryInfo(configId, entryId)
							local borderTypes = Enum.TraitNodeEntryType

							if (traitEntryInfo.type) then -- == borderTypes.SpendCircle
								local definitionId = traitEntryInfo.definitionID
								if definitionId then
									local traitDefinitionInfo = C_Traits.GetDefinitionInfo(definitionId)
									local spellId = traitDefinitionInfo.overriddenSpellID or traitDefinitionInfo.spellID
									local spellName, _, spellTexture = GetSpellInfo(spellId)
									local bIsSelected = (activeEntry and activeEntry.rank and activeEntry.rank > 0) or false
									if (spellName and bIsSelected) then
										local talentInfo = {Name = spellName, ID = spellId, Texture = spellTexture, IsSelected = true}
										if (bOnlySelectedHash) then
											talentList[spellId] = talentInfo
										else
											table.insert(talentList, talentInfo)
										end
									end
								end
							end
						end
					end
				end
			end
		end
	end

	return talentList
end

function DF:GetCharacterPvPTalents(onlySelected, onlySelectedHash)
	if (onlySelected or onlySelectedHash) then
		local talentsSelected = C_SpecializationInfo.GetAllSelectedPvpTalentIDs()
		local talentList = {}
		for _, talentID in ipairs(talentsSelected) do
			local _, talentName, texture = GetPvpTalentInfoByID (talentID)
			if (onlySelectedHash) then
				talentList [talentID] = true
			else
				table.insert(talentList, {Name = talentName, ID = talentID, Texture = texture, IsSelected = true})
			end
		end
		return talentList

	else
		local alreadyAdded = {}
		local talentList = {}
		for i = 1, 4 do --4 slots - get talents available in each one
			local slotInfo = C_SpecializationInfo.GetPvpTalentSlotInfo (i)
			if (slotInfo) then
				for _, talentID in ipairs(slotInfo.availableTalentIDs) do
					if (not alreadyAdded [talentID]) then
						local _, talentName, texture, selected = GetPvpTalentInfoByID (talentID)
						table.insert(talentList, {Name = talentName, ID = talentID, Texture = texture, IsSelected = selected})
						alreadyAdded [talentID] = true
					end
				end
			end
		end
		return talentList
	end
end

DF.GroupTypes = {
	{Name = "Arena", ID = "arena"},
	{Name = "Battleground", ID = "pvp"},
	{Name = "Raid", ID = "raid"},
	{Name = "Dungeon", ID = "party"},
	{Name = "Scenario", ID = "scenario"},
	{Name = "Open World", ID = "none"},
}
function DF:GetGroupTypes()
	return DF.GroupTypes
end

---@class roleinfo : table
---@field Name string
---@field ID string
---@field Texture string

---@type roleinfo[]
local roles = {
	{Name = _G.DAMAGER, ID = "DAMAGER", Texture = _G.INLINE_DAMAGER_ICON},
	{Name = _G.HEALER, ID = "HEALER", Texture = _G.INLINE_HEALER_ICON},
	{Name = _G.TANK, ID = "TANK", Texture = _G.INLINE_TANK_ICON},
	{Name = _G.NONE, ID = "NONE", Texture = _G.INLINE_DAMAGER_ICON},
}

DF.RoleTypes = roles

function DF:GetRoleTypes()
	return DF.RoleTypes
end

local roleTexcoord = {
	DAMAGER = "67:132:67:132",
	HEALER = "67:132:0:66",
	TANK = "0:66:67:132",
	NONE = "134:199:67:132",
}

local roleTextures = {
	DAMAGER = "Interface\\LFGFRAME\\UI-LFG-ICON-ROLES",
	TANK = "Interface\\LFGFRAME\\UI-LFG-ICON-ROLES",
	HEALER = "Interface\\LFGFRAME\\UI-LFG-ICON-ROLES",
	NONE = "Interface\\LFGFRAME\\UI-LFG-ICON-ROLES",
}

local roleTexcoord2 = {
	DAMAGER = {67/256, 132/256, 67/256, 132/256},
	HEALER = {67/256, 132/256, 0/256, 66/256},
	TANK = {0/256, 66/256, 67/256, 132/256},
	NONE = {134/256, 199/256, 67/256, 132/256},
}

function DF:GetRoleIconAndCoords(role)
	local texture = roleTextures[role]
	local coords = roleTexcoord2[role]
	return texture, unpack(coords)
end

function DF:AddRoleIconToText(text, role, size)
	if (role and type(role) == "string") then
		local coords = roleTexcoord2[role]
		if (coords) then
			if (type(text) == "string" and role ~= "NONE") then
				size = size or 14
				local coordsToString = floor(coords[1]*256) .. ":" .. floor(coords[2]*256) .. ":" .. floor(coords[3]*256) .. ":" .. floor(coords[4]*256)
				text = "|TInterface\\LFGFRAME\\UI-LFG-ICON-ROLES:" .. size .. ":" .. size .. ":0:0:256:256:" .. coordsToString .. "|t " .. text
				return text
			end
		end
	end

	return text
end

function DF:GetRoleTCoordsAndTexture(roleID)
	local texture, l, r, t, b = DF:GetRoleIconAndCoords(roleID)
	return l, r, t, b, texture
end

-- TODO: maybe make this auto-generaded some day?...
DF.CLEncounterID = {
	{ID = 2423, Name = "The Tarragrue"},
	{ID = 2433, Name = "The Eye of the Jailer"},
	{ID = 2429, Name = "The Nine"},
	{ID = 2432, Name = "Remnant of Ner'zhul"},
	{ID = 2434, Name = "Soulrender Dormazain"},
	{ID = 2430, Name = "Painsmith Raznal"},
	{ID = 2436, Name = "Guardian of the First Ones"},
	{ID = 2431, Name = "Fatescribe Roh-Kalo"},
	{ID = 2422, Name = "Kel'Thuzad"},
	{ID = 2435, Name = "Sylvanas Windrunner"},
}

function DF:GetPlayerRole()
	local assignedRole = DF.UnitGroupRolesAssigned("player")
	if (assignedRole == "NONE") then
		local spec = DF.GetSpecialization()
		return spec and DF.GetSpecializationRole (spec) or "NONE"
	end
	return assignedRole
end

function DF:GetCLEncounterIDs()
	return DF.CLEncounterID
end

DF.ClassSpecs = {
	["DEMONHUNTER"] = {
		[577] = true,
		[581] = true,
	},
	["DEATHKNIGHT"] = {
		[250] = true,
		[251] = true,
		[252] = true,
	},
	["WARRIOR"] = {
		[71] = true,
		[72] = true,
		[73] = true,
	},
	["MAGE"] = {
		[62] = true,
		[63] = true,
		[64] = true,
	},
	["ROGUE"] = {
		[259] = true,
		[260] = true,
		[261] = true,
	},
	["DRUID"] = {
		[102] = true,
		[103] = true,
		[104] = true,
		[105] = true,
	},
	["HUNTER"] = {
		[253] = true,
		[254] = true,
		[255] = true,
	},
	["SHAMAN"] = {
		[262] = true,
		[263] = true,
		[264] = true,
	},
	["PRIEST"] = {
		[256] = true,
		[257] = true,
		[258] = true,
	},
	["WARLOCK"] = {
		[265] = true,
		[266] = true,
		[267] = true,
	},
	["PALADIN"] = {
		[65] = true,
		[66] = true,
		[70] = true,
	},
	["MONK"] = {
		[268] = true,
		[269] = true,
		[270] = true,
	},
	["EVOKER"] = {
		[1467] = true,
		[1468] = true,
		[1473] = true,
	},
}

DF.SpecListByClass = {
	["DEMONHUNTER"] = {
		577,
		581,
	},
	["DEATHKNIGHT"] = {
		250,
		251,
		252,
	},
	["WARRIOR"] = {
		71,
		72,
		73,
	},
	["MAGE"] = {
		62,
		63,
		64,
	},
	["ROGUE"] = {
		259,
		260,
		261,
	},
	["DRUID"] = {
		102,
		103,
		104,
		105,
	},
	["HUNTER"] = {
		253,
		254,
		255,
	},
	["SHAMAN"] = {
		262,
		263,
		264,
	},
	["PRIEST"] = {
		256,
		257,
		258,
	},
	["WARLOCK"] = {
		265,
		266,
		267,
	},
	["PALADIN"] = {
		65,
		66,
		70,
	},
	["MONK"] = {
		268,
		269,
		270,
	},
	["EVOKER"] = {
		1467,
		1468,
		1473,
	},
}

---return if the specId is a valid spec, it'll return false for specIds from the tutorial area
---@param self table
---@param specId number
function DF:IsValidSpecId(specId)
	local _, class = UnitClass("player")
	local specs = DF.ClassSpecs[class]
	return specs and specs[specId] and true or false
end

--given a class and a  specId, return if the specId is a spec from the class passed
function DF:IsSpecFromClass(class, specId)
	return DF.ClassSpecs[class] and DF.ClassSpecs[class][specId]
end

--return a has table where specid is the key and 'true' is the value
function DF:GetClassSpecs(class)
	return DF.ClassSpecs [class]
end

--return a numeric table with spec ids
function DF:GetSpecListFromClass(class)
	return DF.SpecListByClass [class]
end

--return a list with specIds as keys and spellId as value
function DF:GetSpellsForRangeCheck()
	return SpellRangeCheckListBySpec
end

--return a list with specIds as keys and spellId as value
function DF:GetRangeCheckSpellForSpec(specId)
	return SpellRangeCheckListBySpec[specId]
end

function DF.CatchString(...)
	if (not DF.IsDragonflightAndBeyond()) then
		if (type(select(1, ...)) == "table") then
			for i = 1, select("#", ...) do
				local value = select(i, ...)
				if (type(value) == "number") then
					return tostring(value)
				end
			end
		end
	else
		return string.char(...)
	end
end

--key is instanceId from GetInstanceInfo()
-- /dump GetInstanceInfo()
DF.BattlegroundSizes = {
	[2245] = 15, --Deepwind Gorge
	[2106] = 10, --Warsong Gulch
	[2107] = 15, --Arathi Basin
	[566] = 15, --Eye of the Storm
	[30]  = 40,	--Alterac Valley
	[628] = 40, --Isle of Conquest
	[761] = 10, --The Battle for Gilneas
	[726] = 10, --Twin Peaks
	[727] = 10, --Silvershard Mines
	[998] = 10, --Temple of Kotmogu
	[2118] = 40, --Battle for Wintergrasp
	[1191] = 25, --Ashran
	[1803] = 10, --Seething Shore
}

function DF:GetBattlegroundSize(instanceInfoMapId)
	return DF.BattlegroundSizes[instanceInfoMapId]
end

------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--execute range

	function DF.GetExecuteRange(unitId)
		unitId = unitId or "player"

		local classLoc, class = UnitClass(unitId)
		local spec = GetSpecialization()

		if (spec and class) then
			--prist
			if (class == "PRIEST") then
				--playing as shadow?
				local specID = GetSpecializationInfo(spec)
				if (specID and specID ~= 0) then
					if (specID == 258) then --shadow
						local _, _, _, using_SWDeath = GetTalentInfo(5, 2, 1)
						if (using_SWDeath) then
							return 0.20
						end
					end
				end

			elseif (class == "MAGE") then
				--playing fire mage?
				local specID = GetSpecializationInfo(spec)
				if (specID and specID ~= 0) then
					if (specID == 63) then --fire
						local _, _, _, using_SearingTouch = GetTalentInfo(1, 3, 1)
						if (using_SearingTouch) then
							return 0.30
						end
					end
				end

			elseif (class == "WARRIOR") then
				--is playing as a Arms warrior?
				local specID = GetSpecializationInfo(spec)
				if (specID and specID ~= 0) then

					if (specID == 71) then --arms
						local _, _, _, using_Massacre = GetTalentInfo(3, 1, 1)
						if (using_Massacre) then
							--if using massacre, execute can be used at 35% health in Arms spec
							return 0.35
						end
					end

					if (specID == 71 or specID == 72) then --arms or fury
						return 0.20
					end
				end

			elseif (class == "HUNTER") then
				local specID = GetSpecializationInfo(spec)
				if (specID and specID ~= 0) then
					if (specID == 253) then --beast mastery
						--is using killer instinct?
						local _, _, _, using_KillerInstinct = GetTalentInfo(1, 1, 1)
						if (using_KillerInstinct) then
							return 0.35
						end
					end
				end

			elseif (class == "PALADIN") then
				local specID = GetSpecializationInfo(spec)
				if (specID and specID ~= 0) then
					if (specID == 70) then --retribution paladin
						--is using hammer of wrath?
						local _, _, _, using_HammerOfWrath = GetTalentInfo(2, 3, 1)
						if (using_HammerOfWrath) then
							return 0.20
						end
					end
				end
			end
		end
	end


------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--delta seconds reader

if (not DetailsFrameworkDeltaTimeFrame) then
	CreateFrame("frame", "DetailsFrameworkDeltaTimeFrame", UIParent)
end

local deltaTimeFrame = DetailsFrameworkDeltaTimeFrame
deltaTimeFrame:SetScript("OnUpdate", function(self, deltaTime)
	self.deltaTime = deltaTime
end)

function GetWorldDeltaSeconds()
	return deltaTimeFrame.deltaTime
end

function DF:GetWorldDeltaSeconds()
	return deltaTimeFrame.deltaTime
end

------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--build the global script channel for scripts communication
--send and retrieve data sent by othe users in scripts
--Usage:
--DetailsFramework:RegisterScriptComm (ID, function(sourcePlayerName, ...) end)
--DetailsFramework:SendScriptComm (ID, ...)

	local aceComm = LibStub:GetLibrary ("AceComm-3.0", true)
	local LibAceSerializer = LibStub:GetLibrary ("AceSerializer-3.0", true)
	local LibDeflate = LibStub:GetLibrary ("LibDeflate", true)

	DF.RegisteredScriptsComm = DF.RegisteredScriptsComm or {}

	function DF.OnReceiveScriptComm (...)
		local prefix, encodedString, channel, commSource = ...

		local decodedString = LibDeflate:DecodeForWoWAddonChannel (encodedString)
		if (decodedString) then
			local uncompressedString = LibDeflate:DecompressDeflate (decodedString)
			if (uncompressedString) then
				local data = {LibAceSerializer:Deserialize (uncompressedString)}
				if (data[1]) then
					local ID = data[2]
					if (ID) then
						local sourceName = data[4]
						if (Ambiguate (sourceName, "none") == commSource) then
							local func = DF.RegisteredScriptsComm [ID]
							if (func) then
								DF:MakeFunctionSecure(func)
								DF:Dispatch (func, commSource, select(5, unpack(data))) --this use xpcall
							end
						end
					end
				end
			end
		end
	end

	function DF:RegisterScriptComm (ID, func)
		if (ID) then
			if (type(func) == "function") then
				DF.RegisteredScriptsComm [ID] = func
			else
				DF.RegisteredScriptsComm [ID] = nil
			end
		end
	end

	function DF:SendScriptComm (ID, ...)
		if (DF.RegisteredScriptsComm [ID]) then
			local sourceName = UnitName ("player") .. "-" .. GetRealmName()
			local data = LibAceSerializer:Serialize (ID, UnitGUID("player"), sourceName, ...)
			data = LibDeflate:CompressDeflate (data, {level = 9})
			data = LibDeflate:EncodeForWoWAddonChannel (data)
			aceComm:SendCommMessage ("_GSC", data, "PARTY")
		end
	end

	if (aceComm and LibAceSerializer and LibDeflate) then
		aceComm:RegisterComm ("_GSC", DF.OnReceiveScriptComm)
	end

------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--debug

DF.DebugMixin = {

	debug = true,

	CheckPoint = function(self, checkPointName, ...)
		print(self:GetName(), checkPointName, ...)
	end,

	CheckVisibilityState = function(self, widget)

		self = widget or self

		local width, height = self:GetSize()
		width = floor(width)
		height = floor(height)

		local numPoints = self:GetNumPoints()

		print("shown:", self:IsShown(), "visible:", self:IsVisible(), "alpha:", self:GetAlpha(), "size:", width, height, "points:", numPoints)
	end,

	CheckStack = function(self)
		if (Details) then
			local stack = debugstack()
			Details:Dump (stack)
		end
	end,

}

-----------------------------------------------------------------------------------------------------------------------------------------------------------

--returns if the unit is tapped (gray health color when another player hit the unit first)
function DF:IsUnitTapDenied (unitId)
	return unitId and not UnitPlayerControlled(unitId) and UnitIsTapDenied(unitId)
end


-----------------------------------------------------------------------------------------------------------------------------------------------------------
--pool

do
    local get = function(self)
        local object = table.remove(self.notUse, #self.notUse)
        if (object) then
            table.insert(self.inUse, object)
			if (self.onAcquire) then
				DF:QuickDispatch(self.onAcquire, object)
			end
			return object, false
        else
            --need to create the new object
            local newObject = self.newObjectFunc(self, unpack(self.payload))
            if (newObject) then
				self.objectsCreated = self.objectsCreated + 0
				table.insert(self.inUse, newObject)
				if (self.onAcquire) then
					DF:QuickDispatch(self.onAcquire, newObject)
				end
				return newObject, true
            end
        end
	end

	local get_all_inuse = function(self)
		return self.inUse
	end

	local sort = function(self, func)
		if (not func) then
			table.sort(self.inUse, self.sortFunc)
		elseif (func) then
			table.sort(self.inUse, func)
		end
	end

    local release = function(self, object)
        for i = #self.inUse, 1, -1 do
            if (self.inUse[i] == object) then
                table.remove(self.inUse, i)
                table.insert(self.notUse, object)

				if (self.onRelease) then
					DF:QuickDispatch(self.onRelease, object)
				end
                break
            end
        end
    end

    local reset = function(self)
        for i = #self.inUse, 1, -1 do
            local object = table.remove(self.inUse, i)
            table.insert(self.notUse, object)

			if (self.onReset) then
				DF:QuickDispatch(self.onReset, object)
			end
        end
	end

	--only hide objects in use, do not disable them
		local hide = function(self)
			for i = #self.inUse, 1, -1 do
				self.inUse[i]:Hide()
			end
		end

	--only show objects in use, do not enable them
		local show = function(self)
			for i = #self.inUse, 1, -1 do
				self.inUse[i]:Show()
			end
		end

	--return the amount of objects
		local getamount = function(self)
			return #self.notUse + #self.inUse, #self.notUse, #self.inUse
		end

	---@class df_pool : table
	---@field objectsCreated number --amount of objects created
	---@field inUse table[] --objects in use
	---@field notUse table[] --objects not in use
	---@field payload table --payload to be sent to the newObjectFunc
	---@field sortFunc fun(a:table, b:table):boolean --sort function
	---@field onRelease fun(object:table) --function to be called when an object is released
	---@field onReset fun(object:table) --function to be called when the pool is reset
	---@field onAcquire fun(object:table) --function to be called when an object is acquired
	---@field newObjectFunc fun(self:df_pool, ...):table --function to create a new object, it passes the pool and the payload
	---@field PoolConstructor fun(self:df_pool, func:fun(object:table), ...:any) --constructor, in case to use an existing object to behave like a pool
	---@field Get fun(self:df_pool):table --return an object from the pool
	---@field Acquire fun(self:df_pool):table --alias for :Get()
	---@field GetAllInUse fun(self:df_pool):table[] --return all objects in use
	---@field Release fun(self:df_pool, object:table) --release a single object
	---@field Reset fun(self:df_pool) --release all objects and calls OnReset function if any
	---@field ReleaseAll fun(self:df_pool) --alias for :Reset()
	---@field Hide fun(self:df_pool) --hide all objects in use by calling object:Hide()
	---@field Show fun(self:df_pool) --show all objects in use by calling object:Show()
	---@field GetAmount fun(self:df_pool):number, number, number --return the amount of objects in the pool in use + not in use, not in use, in use
	---@field SetOnRelease fun(self:df_pool, func:fun(object:table)) --set a function to be called when an object is released
	---@field SetCallbackOnRelease fun(self:df_pool, func:fun(object:table)) --set a function to be called when an object is released
	---@field SetOnReset fun(self:df_pool, func:fun(object:table)) --set a function to be called when the pool is reset
	---@field SetCallbackOnReleaseAll fun(self:df_pool, func:fun(object:table)) --alias for :SetOnReset()
	---@field SetOnAcquire fun(self:df_pool, func:fun(object:table)) --set a function to be called when an object is acquired
	---@field SetCallbackOnGet fun(self:df_pool, func:fun(object:table)) --alias for :SetOnAcquire()
	---@field RunForInUse fun(self:df_pool, func:fun(object:table)) --run a function for each object in use
	---@field Sort fun(self:df_pool, func:function?) --sort the objects in use
    local poolMixin = {
		Get = get,
		GetAllInUse = get_all_inuse,
        Acquire = get,
        Release = release,
        Reset = reset,
        ReleaseAll = reset,
		Hide = hide,
		Show = show,
		GetAmount = getamount,
		Sort = sort,

		SetSortFunction = function(self, func)
			self.sortFunc = func
		end,

		SetOnRelease = function(self, func)
			self.onRelease = func
		end,

		SetCallbackOnRelease = function(self, func)
			self.onRelease = func
		end,

		SetOnReset = function(self, func)
			self.onReset = func
		end,
		SetCallbackOnReleaseAll = function(self, func)
			self.onReset = func
		end,

		SetOnAcquire = function(self, func)
			self.onAcquire = func
		end,
		SetCallbackOnGet = function(self, func)
			self.onAcquire = func
		end,

		RunForInUse = function(self, func)
			for i = 1, #self.inUse do
				func(self.inUse[i])
			end
		end,

		PoolConstructor = function(self, func, ...)
			self.objectsCreated = 0
			self.inUse = {}
			self.notUse = {}
			self.payload = {...}
			self.newObjectFunc = func
		end,
    }

	DF.PoolMixin = poolMixin

	--~pool
    function DF:CreatePool(func, ...)
        local newPool = {}
        DetailsFramework:Mixin(newPool, poolMixin)
		newPool:PoolConstructor(func, ...)
        return newPool
	end

	--alias
	function DF:CreateObjectPool(func, ...)
		return DF:CreatePool(func, ...)
	end
end


-----------------------------------------------------------------------------------------------------------------------------------------------------------
---bossmobs

DETAILSFRAMEWORK_TIMEBARCACHE = {}

--register phase
function DF:RegisterEncounterPhaseChange(func, ...)
	if (not DETAILSFRAMEWORK_PHASECALLBACKS) then
		DETAILSFRAMEWORK_PHASECALLBACKS = {}
	end
	table.insert(DETAILSFRAMEWORK_PHASECALLBACKS, {callback = func, payload = {...}})
end

--DF:RegisterEncounterPhaseChange(function(...)print("PHASE CHANGED", ...)end, "my payload!")

--unregister phase
function DF:UnregisterEncounterPhaseChange(func)
	if (DETAILSFRAMEWORK_PHASECALLBACKS) then
		for i = #DETAILSFRAMEWORK_PHASECALLBACKS, 1, -1 do
			if (DETAILSFRAMEWORK_PHASECALLBACKS[i].callback == func) then
				table.remove(DETAILSFRAMEWORK_PHASECALLBACKS, i)
			end
		end
	end
end

local sendPhaseNotification = function(phaseId)
	if (DETAILSFRAMEWORK_PHASECALLBACKS) then
		for _, data in ipairs(DETAILSFRAMEWORK_PHASECALLBACKS) do
			DF:Dispatch(data.callback, phaseId, unpack(data.payload))
		end
	end
end

--register time bar
function DF:RegisterEncounterTimeBar(func, ...)
	if (not DETAILSFRAMEWORK_TIMEBARCALLBACKS) then
		DETAILSFRAMEWORK_TIMEBARCALLBACKS = {}
	end
	table.insert(DETAILSFRAMEWORK_TIMEBARCALLBACKS, {callback = func, payload = {...}})
end

--DF:RegisterEncounterTimeBar(function(...) DETAILSFRAMEWORK_TIMEBARCACHE[#DETAILSFRAMEWORK_TIMEBARCACHE+1] = {...} end)

--[=[
bigwigs
table: 0000019DA5382410 BigWigs_StartBar table: 0000019EF3E5B910 441362 Volatile Concoction: Jieon* 8 136227 false nil
table: 0000019DA5382410 BigWigs_StartBar table: 0000019EF3E5B910 443274 Swirls (30) 7.5 538040 false nil
]=]

--unregister time bar
function DF:UnregisterEncounterTimeBar(func)
	if (DETAILSFRAMEWORK_TIMEBARCALLBACKS) then
		for i = #DETAILSFRAMEWORK_TIMEBARCALLBACKS, 1, -1 do
			if (DETAILSFRAMEWORK_TIMEBARCALLBACKS[i].callback == func) then
				table.remove(DETAILSFRAMEWORK_TIMEBARCALLBACKS, i)
			end
		end
	end
end

local sendTimeBarNotification = function(token, barType, id, msg, timer, icon, spellId, colorId, modid)
	if (DETAILSFRAMEWORK_TIMEBARCALLBACKS) then
		for _, data in ipairs(DETAILSFRAMEWORK_TIMEBARCALLBACKS) do
			DF:Dispatch(data.callback, token, barType, id, msg, timer, icon, spellId, colorId, modid, unpack(data.payload))
		end
	end
end

local createBossModsCallback = function()
    if (_G.DBM) then
        local DBM = _G.DBM

		--phase change
        local phaseChangeCallback = function(event, mod, modId, phase, encounterId, stageTotal, arg1, arg2)
        end
		DBM:RegisterCallback("DBM_SetStage", phaseChangeCallback)

		--time bars
        local timerChangeCallback = function(bar_type, id, msg, timer, icon, bartype, spellId, colorId, modid, arg1, arg2)
        end

        DBM:RegisterCallback("DBM_TimerStart", timerChangeCallback)
    end
--[=
	local BigWigsLoader = BigWigsLoader

    if (BigWigsLoader) then -- and not _G.DBM
        --Bigwigs change the phase of an encounter
        if (BigWigsLoader.RegisterMessage) then
			local t = {}
			t.BigWigs_SetStage = function(self, event, module, phase)
				phase = tonumber(phase)
				sendPhaseNotification(phase)
			end
            BigWigsLoader.RegisterMessage(t, "BigWigs_SetStage")
		end

		if (BigWigsLoader.RegisterMessage) then
			local t = {}
			t.BigWigs_StartBar = function(self, event, module, spellId, barText, barTime, iconTexture, ...)
				--table: 0000019DA5382410 BigWigs_StartBar table: 0000019EF3E5B910 441362 Volatile Concoction (14) 20 136227 false nil
				--print("START", self, event, module, spellId, ...)
				sendTimeBarNotification("START", spellId, barText, barTime, iconTexture, ...)
			end

			t.BigWigs_StopBar = function(self, event, module, spellId, ...)
				--print("BW STOP BAR", self, event, module, spellId, ...)
				sendTimeBarNotification("STOP", spellId)
			end

			t.BigWigs_StopBars = function(self, event, module, ...)
				--print("BW STOP BARS", self, event, module, ...)
				sendTimeBarNotification("STOPALL")
			end

			t.BigWigs_PauseBar = function(self, event, module, spellId, ...)
				--print("BW PAUSE BAR", self, event, module, spellId, ...)
				sendTimeBarNotification("PAUSE", spellId)
			end

			t.BigWigs_ResumeBar = function(self, event, module, spellId, ...)
				--print("BW RESUME BAR", self, event, module, spellId, ...)
				sendTimeBarNotification("RESUME", spellId)
			end

			BigWigsLoader.RegisterMessage(t, "BigWigs_StartBar")
			BigWigsLoader.RegisterMessage(t, "BigWigs_StopBar")
			BigWigsLoader.RegisterMessage(t, "BigWigs_StopBars")
			BigWigsLoader.RegisterMessage(t, "BigWigs_PauseBar")
			BigWigsLoader.RegisterMessage(t, "BigWigs_ResumeBar")

			--self:RegisterMessage("BigWigs_StopBars", "StopModuleBars")
        end
    end
	--]=]
end


detailsFramework.OnLoginSchedules[#detailsFramework.OnLoginSchedules+1] = createBossModsCallback



-----------------------------------------------------------------------------------------------------------------------------------------------------------
--forbidden functions on scripts

	--these are functions which scripts cannot run due to security issues
	local forbiddenFunction = {
		--block mail, trades, action house, banks
		["C_AuctionHouse"] 	= true,
		["C_Bank"] = true,
		["C_GuildBank"] = true,
		["SetSendMailMoney"] = true,
		["SendMail"]		= true,
		["SetTradeMoney"]	= true,
		["AddTradeMoney"]	= true,
		["PickupTradeMoney"]	= true,
		["PickupPlayerMoney"]	= true,
		["AcceptTrade"]		= true,

		--frames
		["BankFrame"] 		= true,
		["TradeFrame"]		= true,
		["GuildBankFrame"] 	= true,
		["MailFrame"]		= true,
		["EnumerateFrames"] = true,

		--block run code inside code
		["RunScript"] = true,
		["securecall"] = true,
		["setfenv"] = true,
		["getfenv"] = true,
		["loadstring"] = true,
		["pcall"] = true,
		["xpcall"] = true,
		["getglobal"] = true,
		["setmetatable"] = true,
		["DevTools_DumpCommand"] = true,
		["ChatEdit_SendText"] = true,

		--avoid creating macros
		["SetBindingMacro"] = true,
		["CreateMacro"] = true,
		["EditMacro"] = true,
		["hash_SlashCmdList"] = true,
		["SlashCmdList"] = true,

		--block guild commands
		["GuildDisband"] = true,
		["GuildUninvite"] = true,

		--other things
		["C_GMTicketInfo"] = true,

		--deny messing addons with script support
		["PlaterDB"] = true,
		["_detalhes_global"] = true,
		["WeakAurasSaved"] = true,
	}

	local C_RestrictedSubFunctions = {
		["C_GuildInfo"] = {
			["RemoveFromGuild"] = true,
		},
	}

	--not in use, can't find a way to check within the environment handle
	local addonRestrictedFunctions = {
		["DetailsFramework"] = {
			["SetEnvironment"] = true,
		},

		["Plater"] = {
			["ImportScriptString"] = true,
			["db"] = true,
		},

		["WeakAuras"] = {
			["Add"] = true,
			["AddMany"] = true,
			["Delete"] = true,
			["NewAura"] = true,
		},
	}

    local C_SubFunctionsTable = {}
    for globalTableName, functionTable in pairs(C_RestrictedSubFunctions) do
        C_SubFunctionsTable [globalTableName] = {}
        for functionName, functionObject in pairs(_G[globalTableName]) do
            if (not functionTable[functionName]) then
                C_SubFunctionsTable [globalTableName][functionName] = functionObject
            end
        end
    end

	DF.DefaultSecureScriptEnvironmentHandle = {
		__index = function(env, key)

			if (forbiddenFunction[key]) then
				return nil

			elseif (key == "_G") then
				return env

			elseif (C_SubFunctionsTable[key]) then
				return C_SubFunctionsTable[key]
			end

			return _G[key]
		end
	}

	function DF:SetEnvironment(func, environmentHandle, newEnvironment)
		environmentHandle = environmentHandle or DF.DefaultSecureScriptEnvironmentHandle
		newEnvironment = newEnvironment or {}

		setmetatable(newEnvironment, environmentHandle)
		_G.setfenv(func, newEnvironment)
	end

	function DF:MakeFunctionSecure(func)
		return DF:SetEnvironment(func)
	end


-----------------------------------------------------------------------------------------------------------------------------------------------------------

---receives an object and print debug info about its visibility
---use to know why a frame is not showing
---@param UIObject any
function DF:DebugVisibility(UIObject)
	local bIsShown = UIObject:IsShown()
	print("Is Shown:", bIsShown and "|cFF00FF00true|r" or "|cFFFF0000false|r")

	print("Alpha > 0:", UIObject:GetAlpha() > 0 and "|cFF00FF00true|r" or "|cFFFF0000false|r")

	local bIsVisible = UIObject:IsVisible()
	print("Is Visible:", bIsVisible and "|cFF00FF00true|r" or "|cFFFF0000false|r")

	local width, height = UIObject:GetSize()
	print("Width:", width > 0 and "|cFF00FF00" .. width .. "|r" or "|cFFFF00000|r")
	print("Height:", height > 0 and "|cFF00FF00" .. height .. "|r" or "|cFFFF00000|r")

	local numPoints = UIObject:GetNumPoints()
	print("Num Points:", numPoints > 0 and "|cFF00FF00" .. numPoints .. "|r" or "|cFFFF00000|r")
end

local benchmarkTime = 0
local bBenchmarkEnabled = false
function _G.__benchmark(bNotPrintResult)
	if (not bBenchmarkEnabled) then
		bBenchmarkEnabled = true
		debugprofilestop()
		benchmarkTime = debugprofilestop()
	else
		local elapsed = debugprofilestop() - benchmarkTime
		bBenchmarkEnabled = false

		if (bNotPrintResult) then
			return elapsed
		end

		print("Elapsed Time:", elapsed)
		return elapsed
	end
end

function DF:DebugTexture(texture, left, right, top, bottom)
	return DF:PreviewTexture(texture, left, right, top, bottom)
end

function DF:PreviewTexture(texture, left, right, top, bottom)
	if (texture and type(texture) == "table" and texture.GetObjectType and texture:GetObjectType() == "Texture") then
		DF:Msg("PreviewTexture: you have passed a texture object (uiobject) instead of the texture atlas, filename or id.")
	end

	local preview = DetailsFrameworkTexturePreview or CreateFrame("frame", "DetailsFrameworkTexturePreview", UIParent)
	preview:SetSize(200, 200)
	preview:SetPoint("center")
	preview.texture = DetailsFrameworkTexturePreviewTexture or preview:CreateTexture("DetailsFrameworkTexturePreviewTexture", "artwork")
	preview.texture:SetAllPoints()
	preview.fontString = DetailsFrameworkTexturePreviewFontString or preview:CreateFontString("DetailsFrameworkTexturePreviewFontString", "artwork", "GameFontNormal")
	preview.fontString:SetPoint("center", preview, "center", 0, 0)

	preview.texture:SetTexture("")
	preview.fontString:SetText("")

	--check if the texture passed is an atlas
	if (type(texture) == "string" and C_Texture.GetAtlasInfo(texture)) then
		preview.texture:SetAtlas(texture)

	elseif (type(texture) == "string" and texture:find("|T")) then
		preview.fontString:SetText(texture)

	elseif (type(texture) == "table") then
		preview.texture:SetTexture(texture.file or texture.filename)
		preview.texture:SetTexCoord(texture.leftTexCoord, texture.rightTexCoord, texture.topTexCoord, texture.bottomTexCoord)
	else
		preview.texture:SetTexture(texture)
		preview.texture:SetTexCoord(left or 0, right or 1, top or 0, bottom or 1)
	end

	preview:Show()
end
