local MAJOR, MINOR = "DiesalTools-2.0", 1
---@alias Diesal.Tools DiesalTools-2.0

---@class DiesalTools-2.0
local DiesalTools = LibStub:NewLibrary(MAJOR, MINOR)

if not DiesalTools then
    return
end -- No Upgrade needed.
-- ~~| Libraries |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- ~~| Lua Upvalues |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
local type, select, pairs, tonumber, tostring = type, select, pairs, tonumber, tostring
local table_concat = table.concat
local setmetatable, getmetatable, next = setmetatable, getmetatable, next
local sub, format, lower, upper, gsub = string.sub, string.format, string.lower, string.upper, string.gsub
local floor, ceil, abs, modf = math.floor, math.ceil, math.abs, math.modf
-- ~~| WoW Upvalues |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
local CreateFrame, UIParent, GetCursorPosition = CreateFrame, UIParent, GetCursorPosition
local GetScreenWidth, GetScreenHeight = GetScreenWidth, GetScreenHeight
-- ~~| Locals |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
local escapeSequences = {
    ["\a"] = "\\a", -- Bell
    ["\b"] = "\\b", -- Backspace
    ["\t"] = "\\t", -- Horizontal tab
    ["\n"] = "\\n", -- Newline
    ["\v"] = "\\v", -- Vertical tab
    ["\f"] = "\\f", -- Form feed
    ["\r"] = "\\r", -- Carriage return
    ["\\"] = "\\\\", -- Backslash
    ['"'] = '\\"', -- Quotation mark
    ["|"] = "||",
}
local lua_keywords = {
    ["and"] = true,
    ["break"] = true,
    ["do"] = true,
    ["else"] = true,
    ["elseif"] = true,
    ["end"] = true,
    ["false"] = true,
    ["for"] = true,
    ["function"] = true,
    ["if"] = true,
    ["in"] = true,
    ["local"] = true,
    ["nil"] = true,
    ["not"] = true,
    ["or"] = true,
    ["repeat"] = true,
    ["return"] = true,
    ["then"] = true,
    ["true"] = true,
    ["until"] = true,
    ["while"] = true,
}
local sub_table = {}
local colors = {
    blue = "|cff" .. "00aaff",
    darkblue = "|cff" .. "004466",
    orange = "|cff" .. "ffaa00",
    darkorange = "|cff" .. "4c3300",
    grey = "|cff" .. "7f7f7f",
    darkgrey = "|cff" .. "414141",
    white = "|cff" .. "ffffff",
    red = "|cff" .. "ff0000",
    green = "|cff" .. "00ff2b",
    yellow = "|cff" .. "ffff00",
    lightyellow = "|cff" .. "ffea7f",
}

local formattedArgs = {}

---@param level number
local function GetCaller(level)
    -- ADDON:LogMessage(debugstack(10,2, 0))
    for trace in debugstack(level, 2, 0):gmatch("(.-)\n") do
        -- Blizzard Sandbox
        local match, _, file, line = trace:find("^.*\\(.-):(%d+)")
        if match then
            return format("%s[%s%s: %s%s%s]|r", colors.orange, colors.yellow, file, colors.lightyellow, line, colors.orange)
        end
        -- PQI DataFile
        local match, _, file, line = trace:find('^%[string "[%s%-]*(.-%.lua).-"%]:(%d+)')
        if match then
            return format("%s[%s%s: %s%s%s]|r", colors.orange, colors.yellow, file, colors.lightyellow, line, colors.orange)
        end
        -- PQR Ability code
        local match, _, file, line = trace:find('^%[string "(.-)"%]:(%d+)')
        if match then
            return format("%s[%s%s: %s%s%s]|r", colors.orange, colors.yellow, file, colors.lightyellow, line, colors.orange)
        end
    end
    return format("%s[%sUnknown Caller%s]|r", colors.orange, colors.red, colors.orange)
end
---@param number number
---@param base? number
local function round(number, base)
    base = base or 1
    return floor((number + base / 2) / base) * base
end
---@param color string | number | table
---@param g? number
---@param b? number
local function getRGBColorValues(color, g, b)
    if type(color) == "number" and type(g) == "number" and type(b) == "number" then
        if color <= 1 and g <= 1 and b <= 1 then
            return round(color * 255), round(g * 255), round(b * 255)
        end
        return color[1], color[2], color[3]
    elseif type(color) == "table" and type(color[1]) == "number" and type(color[2]) == "number" and type(color[3]) == "number" then
        if color[1] <= 1 and color[2] <= 1 and color[3] <= 1 then
            return round(color[1] * 255), round(color[2] * 255), round(color[3] * 255)
        end
        return color[1], color[2], color[3]
    elseif type(color) == "string" then
        return tonumber(sub(color, 1, 2), 16), tonumber(sub(color, 3, 4), 16), tonumber(sub(color, 5, 6), 16)
    end
end

function DiesalTools.GetColor(value)
    if not value then
        return
    end

    if type(value) == "table" and #value >= 3 then
        return value[1] / 255, value[2] / 255, value[3] / 255
    elseif type(value) == "string" then
        return tonumber(sub(value, 1, 2), 16) / 255, tonumber(sub(value, 3, 4), 16) / 255, tonumber(sub(value, 5, 6), 16) / 255
    end
end

-- ~~| API |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

function DiesalTools.Stack()
    print("|r------------------------------| Stack Trace |-------------------------------")
    local stack = debugstack(1, 12, 0)
    for trace in stack:gmatch("(.-)\n") do
        local match, _, file, line, func = trace:find("^.*\\(.-):(%d+).-`(.*)'$")
        if match then
            print(format("%s[%s%s: %s%s%s] %sfunction|r %s|r", colors.orange, colors.yellow, file, colors.lightyellow, line, colors.orange, colors.blue, func))
        end
    end
    print("|r--------------------------------------------------------------------------------")
end

--[[ copy = TableCopy(src, dest, metatable)
	@Arguments:
		dest				Table to copy to
		src					Table to copy
		metatable		if true copies the metatable as well (boolean)
	@Returns:
		table				copy of table
--]]
function DiesalTools.TableCopy(src, dest, metatable)
    if type(src) == "table" then
        if not dest then
            dest = {}
        end
        for sk, sv in next, src, nil do
            dest[DiesalTools.TableCopy(sk)] = DiesalTools.TableCopy(sv)
        end
        if metatable then
            setmetatable(dest, DiesalTools.TableCopy(getmetatable(src)))
        end
    else -- number, string, boolean, etc
        dest = src
    end
    return dest
end

--[[ red, blue, green = GetColor(value)
	@Arguments:
		value		Hex color code or a table contating R,G,B color values
	@Returns:
		red 		Red component of color 	(0-1) (number)
		green 	Green component of color(0-1) (number)
		blue 		Blue component of color (0-1) (number)
-- ]]

-- | Color Tools |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

---@alias ColorType "rgb255" | "rgb1" | "hex" | "hsl"
-- converts a color from RGB[0-1], RGB[0-255], HEX or HSL to RGB[0-1], RGB[0-255] or HEX
---@param from ColorType
---@param to ColorType
---@param v1 string | number | table
---@param v2? string | number
---@param v3? string | number
---@overload fun(from: "rgb1" | "hsl" | "rbg255", to: "rgb1" | "hsl" | "rbg255", v1: number, v2: number, v3: number): number, number, number
---@overload fun(from: "hex", to: "rgb255" | "rgb1" | "hsl", v1: string): number, number, number
---@overload fun(from: "rgb255" | "rgb1" | "hsl", to: "hex", v1: number, v2: number, v3: number): string, nil, nil
function DiesalTools.ConvertColor(from, to, v1, v2, v3)
    if not from or not to or not v1 then
        return
    end
    if type(v1) == "table" then
        v1, v2, v3 = v1[1], v1[2], v1[3]
    end
    local r, g, b
    ---@type ColorType, ColorType
    from, to = from:lower(), to:lower()

    if from == "rgb255" and type(v1) == "number" and type(v2) == "number" and type(v3) == "number" then
        r, g, b = v1, v2, v3
    elseif from == "rgb1" and type(v1) == "number" and type(v2) == "number" and type(v3) == "number" then
        r, g, b = round(v1 * 255), round(v2 * 255), round(v3 * 255)
    elseif from == "hex" and type(v1) == "string" and #v1 > 5 then
        r, g, b = tonumber(sub(v1, 1, 2), 16), tonumber(sub(v1, 3, 4), 16), tonumber(sub(v1, 5, 6), 16)
    elseif from == "hsl" and type(v1) == "number" and type(v2) == "number" and type(v3) == "number" then
        if v2 == 0 then
            v3 = round(v3 * 255)
            r, g, b = v3, v3, v3
        else
            v1, v2, v3 = v1 / 360 * 6, min(max(0, v2), 1), min(max(0, v3), 1)
            local c = (1 - abs(2 * v3 - 1)) * v2
            local x = (1 - abs(v1 % 2 - 1)) * c
            local m = (v3 - 0.5 * c)
            r, g, b = 0, 0, 0
            if v1 < 1 then
                r, g, b = c, x, 0
            elseif v1 < 2 then
                r, g, b = x, c, 0
            elseif v1 < 3 then
                r, g, b = 0, c, x
            elseif v1 < 4 then
                r, g, b = 0, x, c
            elseif v1 < 5 then
                r, g, b = x, 0, c
            else
                r, g, b = c, 0, x
            end

            r, g, b = round((r + m) * 255), round((g + m) * 255), round((b + m) * 255)
        end
    else
        return
    end
    r, g, b = min(255, max(0, r)), min(255, max(0, g)), min(255, max(0, b))

    if to == "rgb255" then
        return r, g, b
    elseif to == "rgb1" then
        return r / 255, g / 255, b / 255
    elseif to == "hex" then
        return format("%02x%02x%02x", r, g, b)
    end
end

-- Mixes a color with pure white to produce a lighter color
---@param color string | table
---@param percent number
---@param to? ColorType
---@param from? ColorType
function DiesalTools.TintColor(color, percent, to, from)
    percent = min(1, max(0, percent))
    from, to = from or "hex", to and to:lower() or "hex"
    local r, g, b = DiesalTools.ConvertColor(from, "rgb255", color)

    if to == "rgb255" then
        return round((255 - r) * percent + r), round((255 - g) * percent + g), round((255 - b) * percent + b)
    elseif to == "rgb1" then
        return round(((255 - r) * percent + r) / 255), round(((255 - g) * percent + g) / 255), round(((255 - b) * percent + b) / 255)
    elseif to == "hex" then
        return format("%02x%02x%02x", round((255 - r) * percent + r), round((255 - g) * percent + g), round((255 - b) * percent + b))
    end
    --  return format("%02x%02x%02x", round((255-r)*percent+r), round((255-g)*percent+g), round((255-b)*percent+b) )
end

-- Mixes a color with pure black to produce a darker color
---@overload fun(color: string, percent: number, to: "hex", from: "hex"): string, nil, nil
---@overload fun(color: string, percent: number, to: "rgb255" | "rgb1", from: ColorType): number, number, number
---@overload fun(color: table, percent: number, to: "hex", from: ColorType): string
function DiesalTools.ShadeColor(color, percent, to, from)
    percent = min(1, max(0, percent))
    from, to = from or "hex", to and to:lower() or "hex"
    local r, g, b = DiesalTools.ConvertColor(from, "rgb255", color)

    if to == "rgb255" then
        return round(-r * percent + r), round(-g * percent + g), round(-b * percent + b)
    elseif to == "rgb1" then
        return round((-r * percent + r) / 255), round((-g * percent + g) / 255), round((-b * percent + b) / 255)
    elseif to == "hex" then
        return format("%02x%02x%02x", round(-r * percent + r), round(-g * percent + g), round(-b * percent + b))
    end
end

-- Mixes a color with the another color to produce an intermediate color.
function DiesalTools.MixColors(color1, color2, percent, to, from)
    percent = min(1, max(0, percent))
    from, to = from or "hex", to and to:lower() or "hex"
    -- to = to and to:lower() or 'hex'

    local r1, g1, b1 = DiesalTools.ConvertColor(from, "rgb255", color1)
    local r2, g2, b2 = DiesalTools.ConvertColor(from, "rgb255", color2)

    if to == "rgb255" then
        return round((r2 - r1) * percent) + r1, round((g2 - g1) * percent) + g1, round((b2 - b1) * percent) + b1
    elseif to == "rgb1" then
        return round(((r2 - r1) * percent + r1) / 255), round(((g2 - g1) * percent + g1) / 255), round(((b2 - b1) * percent + b1) / 255)
    elseif to == "hex" then
        return format("%02x%02x%02x", round((r2 - r1) * percent) + r1, round((g2 - g1) * percent) + g1, round((b2 - b1) * percent) + b1)
    end
end

--- converts color HSL to HEX
function DiesalTools.HSL(v1, v2, v3)
    if not v1 then
        return
    end
    if type(v1) == "table" then
        v1, v2, v3 = v1[1], v1[2], v1[3]
    end
    local r, g, b

    if v2 == 0 then
        v3 = round(v3 * 255)
        r, g, b = v3, v3, v3
    else
        v1, v2, v3 = v1 / 360 * 6, min(max(0, v2), 1), min(max(0, v3), 1)
        local c = (1 - abs(2 * v3 - 1)) * v2
        local x = (1 - abs(v1 % 2 - 1)) * c
        local m = (v3 - 0.5 * c)
        r, g, b = 0, 0, 0
        if v1 < 1 then
            r, g, b = c, x, 0
        elseif v1 < 2 then
            r, g, b = x, c, 0
        elseif v1 < 3 then
            r, g, b = 0, c, x
        elseif v1 < 4 then
            r, g, b = 0, x, c
        elseif v1 < 5 then
            r, g, b = x, 0, c
        else
            r, g, b = c, 0, x
        end

        r, g, b = round((r + m) * 255), round((g + m) * 255), round((b + m) * 255)
    end

    r, g, b = min(255, max(0, r)), min(255, max(0, g)), min(255, max(0, b))

    return format("%02x%02x%02x", r, g, b)
end

--[[ GetTxtColor(value)
	@Arguments:
		value			Hex color code or a table contating R,G,B color values
	@Returns:
		text coloring escape sequence (|cFFFFFFFF)
-- ]]
function DiesalTools.GetTxtColor(value)
    if not value then
        return
    end

    if type(value) == "table" then
        value = string.format("%02x%02x%02x", value[1], value[2], value[3])
    end
    return format("|cff%s", value)
end

-- | Texture Tools |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

--[[ Get a piece of a texture as a cooridnate refernce
-- @Param 	column			the column in the texture
-- @Param 	row				the row in the texture
-- @Param 	size				the size in the texture
-- @Param 	textureWidth	total texture width
-- @Param 	textureHeight	total texture height
-- @Return	edge coordinates for image cropping, plugs directly into Texture:SetTexCoord(left, right, top, bottom)		]]
function DiesalTools.GetIconCoords(column, row, size, textureWidth, textureHeight)
    size = size or 16
    textureWidth = textureWidth or 128
    textureHeight = textureHeight or 16
    return (column * size - size) / textureWidth, (column * size) / textureWidth, (row * size - size) / textureHeight, (row * size) / textureHeight
end

-- | String Tools |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

--[[ Capitalize a string
--	@Param 	str	the string to capatilize
--	@Return	the capitilized string			]]
function DiesalTools.Capitalize(str)
    return (str:gsub("^%l", upper))
end

--[[ Escape all formatting in a WoW-lua string
-- @Param 	str	the string to escape
-- @Return	the escaped string		]]
function DiesalTools.EscapeString(string)
    return string:gsub('[%z\1-\31"\\|\127-\255]', escapeSequences)
end

--[[ ID = CreateID(s)
-- @Param 	s 	string to parse
-- @Return	ID	string stripped of all non letter characters and color codes.]]
function DiesalTools.CreateID(s)
    return gsub(s:gsub("c%x%x%x%x%x%x%x%x", ""), "[^%a%d]", "")
end

--[[ str = TrimString(s)
-- @Param 	s 		string to parse
-- @Return	str	string stripped of leading and trailing spaces.]]
function DiesalTools.TrimString(s)
    return s:gsub("^%s*(.-)%s*$", "%1")
end

--[[ string = SpliceString(string, start, End, txt)
	@Arguments:
		string			string to splice
		start				starting index of splice
		End					ending index of splice
		txt					new text to splice in (string)
	@Returns:
		string			resulting string of the splice

	@example:
		DiesalTools.SpliceString("123456789",2,4,'NEW') -- returns: "1NEW56789"
--]]
function DiesalTools.SpliceString(string, start, End, txt)
    return string:sub(1, start - 1) .. txt .. string:sub(End + 1, -1)
end

--[[ string = Serialize(table)
	@Param	table	- table to to serialize
	@Return	string - serialized table (string)

	@deserialization:
		local func,err = loadstring('serialized table')
	 	if err then error(err) end
	 	return func()
]]
local function serialize_number(number)
    -- no argument checking - called very often
    local text = ("%.17g"):format(number)
    -- on the same platform tostring() and string.format()
    -- return the same results for 1/0, -1/0, 0/0
    -- so we don't need separate substitution table
    return sub_table[text] or text
end
local function impl(t, cat, visited)
    local t_type = type(t)
    if t_type == "table" then
        if not visited[t] then
            visited[t] = true

            cat("{")
            -- Serialize numeric indices
            local next_i = 0
            for i, v in ipairs(t) do
                if i > 1 then -- TODO: Move condition out of the loop
                    cat(",")
                end
                impl(v, cat, visited)
                next_i = i
            end
            next_i = next_i + 1
            -- Serialize hash part
            -- Skipping comma only at first element iff there is no numeric part.
            local need_comma = (next_i > 1)
            for k, v in pairs(t) do
                local k_type = type(k)
                if k_type == "string" then
                    if need_comma then
                        cat(",")
                    end
                    need_comma = true
                    -- TODO: Need "%q" analogue, which would put quotes
                    --       only if string does not match regexp below
                    if not lua_keywords[k] and ("^[%a_][%a%d_]*$"):match(k) then
                        cat(k)
                        cat("=")
                    else
                        cat(format("[%q]=", k))
                    end
                    impl(v, cat, visited)
                else
                    if
                        k_type ~= "number" -- non-string non-number
                        or k >= next_i
                        or k < 1 -- integer key in hash part of the table
                        or k % 1 ~= 0 -- non-integer key
                    then
                        if need_comma then
                            cat(",")
                        end
                        need_comma = true

                        cat("[")
                        impl(k, cat, visited)
                        cat("]=")
                        impl(v, cat, visited)
                    end
                end
            end
            cat("}")
            visited[t] = nil
        else
            -- this loses information on recursive tables
            cat('"table (recursive)"')
        end
    elseif t_type == "number" then
        cat(serialize_number(t))
    elseif t_type == "boolean" then
        cat(tostring(t))
    elseif t == nil then
        cat("nil")
    else
        -- this converts non-serializable (functions) types to strings
        cat(format("%q", tostring(t)))
    end
end
local function tstr_cat(cat, t)
    impl(t, cat, {})
end
function DiesalTools.Serialize(t)
    local buf = {}
    local cat = function(v)
        buf[#buf + 1] = v
    end
    impl(t, cat, {})
    return format("return %s", table_concat(buf))
end

-- | Math Tools |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

--[[ Round a number
-- @Param 	number 	the number to round
-- @Param 	base		the number to round to (can be a decimal)	[Default:1]
-- @Return	the rounded number to base		]]
---@param number number
---@param base? number
function DiesalTools.Round(number, base)
    base = base or 1
    return floor((number + base / 2) / base) * base
end

--[[ Round a number for printing
-- @Param 	number 	the number to round
-- @Param 	idp			number of decimal points to round to	[Default:1]
-- @Return	the rounded number ]]
function DiesalTools.RoundPrint(num, idp)
    return string.format("%." .. (idp or 0) .. "f", num)
end

-- | Frame Tools |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

--[[ GetFrameQuadrant(frame)
	@Arguments:
		frame			frame?!!
	@Returns:
		quadrant   	quadrant of the screen frames center is in
-- ]]
function DiesalTools.GetFrameQuadrant(frame)
    if not frame:GetCenter() then
        return "UNKNOWN", frame:GetName()
    end

    local x, y = frame:GetCenter()
    local screenWidth = GetScreenWidth()
    local screenHeight = GetScreenHeight()
    local quadrant

    if (x > (screenWidth / 4) and x < (screenWidth / 4) * 3) and y > (screenHeight / 4) * 3 then
        quadrant = "TOP"
    elseif x < (screenWidth / 4) and y > (screenHeight / 4) * 3 then
        quadrant = "TOPLEFT"
    elseif x > (screenWidth / 4) * 3 and y > (screenHeight / 4) * 3 then
        quadrant = "TOPRIGHT"
    elseif (x > (screenWidth / 4) and x < (screenWidth / 4) * 3) and y < (screenHeight / 4) then
        quadrant = "BOTTOM"
    elseif x < (screenWidth / 4) and y < (screenHeight / 4) then
        quadrant = "BOTTOMLEFT"
    elseif x > (screenWidth / 4) * 3 and y < (screenHeight / 4) then
        quadrant = "BOTTOMRIGHT"
    elseif x < (screenWidth / 4) and (y > (screenHeight / 4) and y < (screenHeight / 4) * 3) then
        quadrant = "LEFT"
    elseif x > (screenWidth / 4) * 3 and y < (screenHeight / 4) * 3 and y > (screenHeight / 4) then
        quadrant = "RIGHT"
    else
        quadrant = "CENTER"
    end

    return quadrant
end

-- | Misc Tools |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

--[[ Pack(...) blizzard dosnt use 5.2 yet so will have to create this one
	@Arguments:
		...			arguments to pack into a table
	@Returns:
		a new table with all parameters stored into keys 1, 2, etc. and with a field "n" with the total number of parameters
]]
function DiesalTools.Pack(...)
    return { n = select("#", ...), ... }
end

--[[ Unpack(...)
	@Arguments:
		t			table to unpack
	@Returns:
		...		list of arguments
]]
function DiesalTools.Unpack(t)
    if t.n then
        return unpack(t, 1, t.n)
    end
    return unpack(table)
end

---@param zoomPercent number
---@param width number
---@param height number
---@param xOffset? number
---@param yOffset? number
---@return number, number, number, number, number, number, number, number
function DiesalTools.GetTexCoord(zoomPercent, width, height, xOffset, yOffset)
    xOffset = xOffset or 0
    yOffset = yOffset or 0
    local textureWidth = 1 - 0.5 * zoomPercent
    local aspectRatio = width == 0 or height == 0 and 1 or width / height
    ---@type table<number, number>
    local currentCoord = {}
    currentCoord[1], currentCoord[2], currentCoord[3], currentCoord[4], currentCoord[5], currentCoord[6], currentCoord[7], currentCoord[8] = 0, 0, 0, 1, 1, 0, 1, 1

    local xRatio = aspectRatio < 1 and aspectRatio or 1
    local yRatio = aspectRatio > 1 and 1 / aspectRatio or 1
    for i, coord in ipairs(currentCoord) do
        if i % 2 == 1 then
            currentCoord[i] = (coord - 0.5) * textureWidth * xRatio + 0.5 - xOffset
        else
            currentCoord[i] = (coord - 0.5) * textureWidth * yRatio + 0.5 - yOffset
        end
    end

    return unpack(currentCoord)
end

---@param texture string|integer
function DiesalTools.CreateTextureString(texture)
    if type(texture) ~= "string" then
        texture = tostring(texture)
    end

    return string.format("|T%s:0::0:0:64:64:4:60:4:60|t", texture)
end

---@param spell number | Bastion.Spell
---@param text? string
---@param position? "start" | "end"
function DiesalTools.AttachSpellIcon(spell, text, position)
    position = position or "start"
    local icon
    if type(spell) == "table" then
        text = text or spell:GetName()
        icon = spell:GetIcon()
    elseif type(spell) == "number" then
        text = text or GetSpellInfo(spell)
        icon = select(3, GetSpellInfo(spell))
    end

    return string.format("%s %s", position == "start" and DiesalTools.CreateTextureString(icon) or text, position == "end" and DiesalTools.CreateTextureString(icon) or text)
end
