local GetAddOnMetadata = C_AddOns and C_AddOns.GetAddOnMetadata or GetAddOnMetadata

---@class QuestieLib
local QuestieLib = QuestieLoader:CreateModule("QuestieLib")

---@type QuestieDB
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
---@type QuestiePlayer
local QuestiePlayer = QuestieLoader:ImportModule("QuestiePlayer")
---@type QuestieEvent
local QuestieEvent = QuestieLoader:ImportModule("QuestieEvent")
---@type l10n
local l10n = QuestieLoader:ImportModule("l10n")

--- COMPATIBILITY ---
local addonName = QuestieCompat.Is335 and QuestieCompat.addonName or "Questie"

QuestieLib.AddonPath = "Interface\\Addons\\"..addonName.."\\"

local math_abs = math.abs
local math_sqrt = math.sqrt
local math_max = math.max
local math_random = math.random
local tinsert = table.insert
local stringSub = string.sub
local stringGsub = string.gsub
local strim = string.trim
local smatch = string.match
local tonumber = tonumber
local getCurrentTimestamp = GetServerTime or time

-- The original frame which we use to fetch the data required
--                           Classic                          Wotlk Classic
local textWrapFrameObject = _G["QuestLogObjectivesText"] or _G["QuestInfoObjectivesText"]

--[[
    Red: 5+ level above player
    Orange: 3 - 4 level above player
    Yellow: max 2 level below/above player
    Green: 3 - GetQuestGreenRange() level below player (GetQuestGreenRange() changes on specific player levels)
    Gray: More than GetQuestGreenRange() below player
--]]
function QuestieLib:PrintDifficultyColor(level, text, isDailyQuest, isEventQuest, isPvPQuest)
    if isEventQuest == true then
        return "|cFF6ce314" .. text .. "|r" -- Lime
    end
    if isPvPQuest == true then
        return "|cFFE35639" .. text .. "|r" -- Maroon
    end
    if isDailyQuest == true then
        return "|cFF21CCE7" .. text .. "|r" -- Blue
    end

    level = tonumber(level)
    if not level or level == -1 then
        level = QuestiePlayer.GetPlayerLevel()
    end
    local levelDiff = level - QuestiePlayer.GetPlayerLevel()

    if (levelDiff >= 5) then
        return "|cFFFF1A1A" .. text .. "|r" -- Red
    elseif (levelDiff >= 3) then
        return "|cFFFF8040" .. text .. "|r" -- Orange
    elseif (levelDiff >= -2) then
        return "|cFFFFFF00" .. text .. "|r" -- Yellow
    elseif (-levelDiff <= GetQuestGreenRange("player")) then
        return "|cFF40C040" .. text .. "|r" -- Green
    else
        return "|cFFC0C0C0" .. text .. "|r" -- Grey
    end
end

function QuestieLib:GetDifficultyColorPercent(level)
    level = tonumber(level)
    if not level or level == -1 then level = QuestiePlayer.GetPlayerLevel() end
    local levelDiff = level - QuestiePlayer.GetPlayerLevel()

    if (levelDiff >= 5) then
        -- return "|cFFFF1A1A"..text.."|r"; -- Red
        return 1, 0.102, 0.102
    elseif (levelDiff >= 3) then
        -- return "|cFFFF8040"..text.."|r"; -- Orange
        return 1, 0.502, 0.251
    elseif (levelDiff >= -2) then
        -- return "|cFFFFFF00"..text.."|r"; -- Yellow
        return 1, 1, 0
    elseif (-levelDiff <= GetQuestGreenRange("player")) then
        -- return "|cFF40C040"..text.."|r"; -- Green
        return 0.251, 0.753, 0.251
    else
        -- return "|cFFC0C0C0"..text.."|r"; -- Grey
        return 0.753, 0.753, 0.753
    end
end

-- 1.12 color logic
local function RGBToHex(r, g, b)
    if r > 255 then r = 255 end
    if g > 255 then g = 255 end
    if b > 255 then b = 255 end
    return string.format("|cFF%02x%02x%02x", r, g, b)
end

local function FloatRGBToHex(r, g, b) return RGBToHex(r * 254, g * 254, b * 254) end

function QuestieLib:GetRGBForObjective(objective)
    if objective.fulfilled ~= nil and (not objective.Collected) then
        objective.Collected = objective.fulfilled
        objective.Needed = objective.required
    end

    if not objective.Collected or type(objective.Collected) ~= "number" then
        return FloatRGBToHex(0.937, 0.937, 0.937)
    end

    local float = objective.Collected / objective.Needed
    local trackerColor = Questie.db.profile.trackerColorObjectives
    if not trackerColor or trackerColor == "white" or trackerColor == "minimal" then
        -- White
        return "|cFFEEEEEE"
    elseif trackerColor == "whiteAndGreen" then
        -- White and Green
        return objective.Collected == objective.Needed and RGBToHex(40, 255, 40) or FloatRGBToHex(0.937, 0.937, 0.937)
    elseif trackerColor == "whiteToGreen" then
        -- White to Green
        return FloatRGBToHex(0.937 - float / 1.282, 0.937 + float / 15.873, 0.937 - float / 1.282)
    else
        -- Red to Green
        if float <= .50 then return FloatRGBToHex(1, 0 + float * 2, 0) end
        if float > .50 then return FloatRGBToHex(1.843 - float / 0.593, 1, (float * 2 - 1) * 0.157) end
    end
end

---@param questId number
---@param showLevel number @ Whether the quest level should be included
---@param showState boolean @ Whether to show (Complete/Failed)
---@param blizzLike boolean? @Compatibility flag used by older 3.3.5 callers
function QuestieLib:GetColoredQuestName(questId, showLevel, showState, blizzLike)
    local name = QuestieDB.QueryQuestSingle(questId, "name")
    local level, _ = QuestieLib.GetTbcLevel(questId);

    if showLevel then
        name = QuestieLib:GetLevelString(questId, level, blizzLike) .. name
    end

    if Questie.db.profile.enableTooltipsQuestID then
        name = name .. " " .. l10n("(") .. questId .. l10n(")")
    end

    if showState then
        local isComplete = QuestieDB.IsComplete(questId)

        if isComplete == -1 then
            name = name .. " " .. Questie:Colorize(l10n("(") .. l10n("Failed") .. l10n(")"), "red")
        elseif isComplete == 1 then
            name = name .. " " .. Questie:Colorize(l10n("(") .. l10n("Complete") .. l10n(")"), "green")

            -- Quests treated as complete - zero objectives or synthetic objectives
        elseif isComplete == 0 and QuestieDB.GetQuest(questId).isComplete == true then
            name = name .. " " .. Questie:Colorize(l10n("(") .. l10n("Complete") .. l10n(")"), "green")
        end
    end

    return QuestieLib:PrintDifficultyColor(level, name, QuestieDB.IsRepeatable(questId), QuestieEvent:IsEventQuest(questId), QuestieDB.IsPvPQuest(questId))
end

-- The order of these colors is important for the ColorWheel function.
-- Taken from https://tailwindcolor.com/
---@type Color[]
local colors = {
    -- Light (200)         Standard (500)         -- Family
    {0.99, 0.73, 0.73},    {0.94, 0.19, 0.19},    -- Red
    {0.99, 0.81, 0.59},    {0.98, 0.46, 0.05},    -- Orange
    {0.99, 0.93, 0.54},    {0.92, 0.68, 0.05},    -- Yellow
    {0.73, 0.96, 0.80},    {0.13, 0.77, 0.36},    -- Green
    {0.75, 0.87, 0.99},    {0.23, 0.55, 0.94},    -- Blue
    {0.78, 0.82, 0.99},    {0.39, 0.45, 0.94},    -- Indigo
    {0.87, 0.82, 1.00},    {0.55, 0.35, 0.96},    -- Violet
    {0.99, 0.76, 0.89},    {0.93, 0.16, 0.55},    -- Pink
}

-- Shuffle colors on startup
local function shuffleTable(t)
    for i = #t, 2, -1 do
        local j = math_random(1, i)
        t[i], t[j] = t[j], t[i]
    end
end

shuffleTable(colors)

local numColors = #colors
local lastColor = math_random(numColors)

---@return Color
function QuestieLib:ColorWheel()
    lastColor = lastColor + 1
    if lastColor > numColors then
        lastColor = 1
    end
    return colors[lastColor]
end

---@param questId number
---@param name string @The (localized) name of the quest
---@param level number @The quest level
---@param blizzLike boolean @True = [40+], false/nil = [40D/R]
function QuestieLib:GetQuestString(questId, name, level, blizzLike)
    return QuestieLib:GetLevelString(questId, level, blizzLike) .. name
end

--- There are quests in TBC which have a quest level of -1. This indicates that the quest level is the
--- same as the player level. This function should be used whenever accessing the quest or required level.
---@param questId QuestId
---@param playerLevel Level? ---@ PlayerLevel, if nil we fetch current level
---@return Level questLevel
---@return Level requiredLevel
---@return Level requiredMaxLevel
function QuestieLib.GetTbcLevel(questId, playerLevel)
    local questLevel, requiredLevel = QuestieDB.QueryQuestSingle(questId, "questLevel"), QuestieDB.QueryQuestSingle(questId, "requiredLevel")
    if (questLevel == -1) then
        local level = playerLevel or QuestiePlayer.GetPlayerLevel();
        if (requiredLevel > level) then
            questLevel = requiredLevel;
        else
            questLevel = level;
            -- We also set the requiredLevel to the player level so the quest is not hidden without "show low level quests"
            requiredLevel = level;
        end
    end
    return questLevel, requiredLevel, QuestieDB.QueryQuestSingle(questId, "requiredMaxLevel");
end

---Returns the quest type suffix character (e.g., "+" for Elite, "D" for Dungeon)
---@param questId QuestId
---@param blizzLike boolean? @If true, use '+' for group-content tags in classic Blizzard style
---@return string suffix @The suffix character for the quest type
function QuestieLib:GetQuestTypeSuffix(questId, blizzLike)
    local questTagId, questTagName = QuestieDB.GetQuestTagInfo(questId)

    if not questTagId or not questTagName then
        return ""
    end

    local questTagIds = QuestieDB.questTagIds
    local tagElite = questTagIds.ELITE
    local tagPvp = questTagIds.PVP
    local tagRaid = questTagIds.RAID
    local tagDungeon = questTagIds.DUNGEON
    local tagLegendary = questTagIds.LEGENDARY
    local tagEscort = questTagIds.ESCORT
    local tagHeroic = questTagIds.HEROIC
    local tagClass = questTagIds.CLASS
    local tagRaid10 = questTagIds.RAID_10
    local tagRaid25 = questTagIds.RAID_25
    local tagScenario = questTagIds.SCENARIO
    local tagAccount = questTagIds.ACCOUNT
    local tagCelestial = questTagIds.CELESTIAL
    local tagWorldEvent = questTagIds.WORLD_EVENT
    local isGroupContentTag = questTagId == tagRaid or questTagId == tagDungeon or questTagId == tagHeroic or
            (tagRaid10 and questTagId == tagRaid10) or (tagRaid25 and questTagId == tagRaid25) or
            (tagScenario and questTagId == tagScenario) or (tagAccount and questTagId == tagAccount) or
            (tagCelestial and questTagId == tagCelestial) or (tagWorldEvent and questTagId == tagWorldEvent)
    local langCode = l10n:GetUILocale()
    local isMultiByteLocale = langCode == "zhCN" or langCode == "zhTW" or langCode == "koKR" or langCode == "ruRU"

    if questTagId == tagElite then
        return "+"
    elseif questTagId == tagPvp or (tagClass and questTagId == tagClass) or questTagId == tagEscort then
        return ""
    elseif questTagId == tagLegendary then
        return "++"
    elseif blizzLike and isGroupContentTag then
        return "+"
    elseif isMultiByteLocale then
        if questTagId == tagRaid or (tagRaid10 and questTagId == tagRaid10) or (tagRaid25 and questTagId == tagRaid25) then
            return "R"
        elseif questTagId == tagDungeon then
            return "D"
        elseif questTagId == tagHeroic then
            return "H"
        elseif tagScenario and questTagId == tagScenario then
            return "S"
        elseif tagAccount and questTagId == tagAccount then
            return "A"
        elseif tagCelestial and questTagId == tagCelestial then
            return "C"
        elseif tagWorldEvent and questTagId == tagWorldEvent then
            return "W"
        else
            return ""
        end
    else
        -- Fallback: use first character of quest tag name for unknown tags
        -- This preserves backward compatibility with existing UI/tests
        return stringSub(questTagName, 1, 1)
    end
end

---@param questId QuestId
---@param levelOrNameOrIgnored Level|string|nil @Compatibility: older callers pass an ignored 2nd arg before level
---@param levelOrBlizzLike Level|boolean|nil @Either the quest level or the optional blizzLike flag
---@param blizzLike boolean? @Compatibility flag used by older 3.3.5 callers
---@return string levelString @String of format "[40+]"
function QuestieLib:GetLevelString(questId, levelOrNameOrIgnored, levelOrBlizzLike, blizzLike)
    local level

    if type(levelOrNameOrIgnored) == "number" and (levelOrBlizzLike == nil or type(levelOrBlizzLike) == "boolean") and blizzLike == nil then
        level = levelOrNameOrIgnored
        blizzLike = levelOrBlizzLike
    else
        level = levelOrBlizzLike
    end

    local levelString = tostring(level)
    local suffix = QuestieLib:GetQuestTypeSuffix(questId, blizzLike)
    return "[" .. levelString .. suffix .. "] "
end

function QuestieLib:GetRaceString(raceMask)
    if not raceMask or raceMask == QuestieDB.raceKeys.NONE then
        return ""
    end

    if raceMask == QuestieDB.raceKeys.ALL_ALLIANCE then
        return "|cFF1E90FF" .. l10n("Alliance") .. "|r"
    elseif raceMask == QuestieDB.raceKeys.ALL_HORDE then
        return "|cFFDA4450" .. l10n("Horde") .. "|r"
    else
        local raceString = ""
        local raceTable = QuestieLib:UnpackBinary(raceMask)
        local langCode = l10n:GetUILocale()
        local spaceString = ((langCode == "zhCN" or langCode == "zhTW") and "") or " " -- no spaces for chinese strings
        local stringTable = {
            l10n("Human"),
            l10n("Orc"),
            l10n("Dwarf"),
            l10n("Night Elf"),
            l10n("Undead"),
            l10n("Tauren"),
            l10n("Gnome"),
            l10n("Troll"),
            l10n("Goblin"),
            l10n("Blood Elf"),
            l10n("Draenei"),
        }
        local firstRun = true
        for k, v in pairs(raceTable) do
            if v then
                if firstRun then
                    firstRun = false
                else
                    raceString = raceString .. ", "
                end
                raceString = raceString .. stringTable[k]
            end
        end
        return raceString
    end
end

function QuestieLib:GetClassString(classMask)
    if not classMask or classMask == QuestieDB.classKeys.NONE or classMask == QuestieDB.classKeys.ALL_CLASSES then
        return ""
    else
        local classString = ""
        local classTable = QuestieLib:UnpackBinary(classMask)
        local classColors = {
            -- QuestieCompat normalizes the older 3.3.5 RAID_CLASS_COLORS table shape.
            WARRIOR      = "|c" .. select(4, QuestieCompat.GetClassColor("WARRIOR")),
            PALADIN      = "|c" .. select(4, QuestieCompat.GetClassColor("PALADIN")),
            HUNTER       = "|c" .. select(4, QuestieCompat.GetClassColor("HUNTER")),
            ROGUE        = "|c" .. select(4, QuestieCompat.GetClassColor("ROGUE")),
            PRIEST       = "|c" .. select(4, QuestieCompat.GetClassColor("PRIEST")),
            DEATH_KNIGHT = "|c" .. select(4, QuestieCompat.GetClassColor("DEATHKNIGHT")),
            SHAMAN       = "|c" .. select(4, QuestieCompat.GetClassColor("SHAMAN")),
            MAGE         = "|c" .. select(4, QuestieCompat.GetClassColor("MAGE")),
            WARLOCK      = "|c" .. select(4, QuestieCompat.GetClassColor("WARLOCK")),
            DRUID        = "|c" .. select(4, QuestieCompat.GetClassColor("DRUID")),
        }
        local stringTable = {
            -- ingame color codes via RAID_CLASS_COLORS["WARRIOR"] etc
            classColors.WARRIOR .. l10n("Warrior") .. "|r",                 -- 1
            classColors.PALADIN .. l10n("Paladin") .. "|r",                 -- 2
            classColors.HUNTER .. l10n("Hunter") .. "|r",                   -- 4
            classColors.ROGUE .. l10n("Rogue") .. "|r",                     -- 8
            classColors.PRIEST .. l10n("Priest") .. "|r",                   -- 16
            classColors.DEATH_KNIGHT .. l10n("Death Knight") .. "|r",       -- 32
            classColors.SHAMAN .. l10n("Shaman") .. "|r",                   -- 64
            classColors.MAGE .. l10n("Mage") .. "|r",                       -- 128
            classColors.WARLOCK .. l10n("Warlock") .. "|r",                 -- 256
            nil,                                                            -- 512 (unsupported in 3.3.5)
            classColors.DRUID .. l10n("Druid") .. "|r",                     -- 1024
        }
        local firstRun = true
        for k, v in pairs(classTable) do
            if v then
                local className = stringTable[k]
                if className then
                    if firstRun then
                        firstRun = false
                    else
                        classString = classString .. ", "
                    end
                    classString = classString .. className
                end
            end
        end
        return classString
    end
end

function QuestieLib:CacheItemNames(questId)
    local quest = QuestieDB.GetQuest(questId)
    if (quest and quest.ObjectiveData) then
        for _, objectiveDB in pairs(quest.ObjectiveData) do
            if objectiveDB.Type == "item" then
                if not ((QuestieDB.ItemPointers or QuestieDB.itemData)[objectiveDB.Id]) then
                    Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLib:CacheItemNames] Requesting item information for missing itemId:", objectiveDB.Id)
                    local item = Item:CreateFromItemID(objectiveDB.Id)
                    item:ContinueOnItemLoad(
                        function()
                            local itemName = item:GetItemName()
                            if not QuestieDB.itemDataOverrides[objectiveDB.Id] then
                                QuestieDB.itemDataOverrides[objectiveDB.Id] = { itemName, { questId }, {}, {} }
                            else
                                QuestieDB.itemDataOverrides[objectiveDB.Id][1] = itemName
                            end
                            Questie:Debug(Questie.DEBUG_DEVELOP,
                                "[QuestieLib:CacheItemNames] Created item information for item:", itemName, ":", objectiveDB.Id)
                        end)
                end
            end
        end
    end
end

function QuestieLib:Euclid(x, y, i, e)
    -- No need for absolute values as these are used only as squared
    local xd = x - i
    local yd = y - e
    return math_sqrt(xd * xd + yd * yd)
end

function QuestieLib:Maxdist(x, y, i, e)
    return math_max(math_abs(x - i), math_abs(y - e))
end

local cachedVersion

---@return number, number, number
function QuestieLib:GetAddonVersionInfo()
    if (not cachedVersion) then
        cachedVersion = GetAddOnMetadata(addonName, "Version")
    end

    local major, minor, patch = string.match(cachedVersion, "(%d+)%p(%d+)%p(%d+)")

    return tonumber(major), tonumber(minor), tonumber(patch)
end

function QuestieLib:GetAddonVersionString()
    if (not cachedVersion) then
        -- This brings up the ## Version from the TOC
        cachedVersion = GetAddOnMetadata(addonName, "Version")
    end

    return "v" .. cachedVersion
end

-- According to stack overflow, # and table.getn arent reliable (I've experienced this? not sure whats up)
function QuestieLib:Count(table)
    local count = 0
    for _, _ in pairs(table) do count = count + 1 end
    return count
end

-- Credits to Shagu and pfQuest, why reinvent the wheel.
-- https://gitlab.com/shagu/pfQuest/blob/master/compat/pfUI.lua
local sanitize_cache = {}
function QuestieLib:SanitizePattern(pattern)
    if not sanitize_cache[pattern] then
        local ret = pattern
        -- escape magic characters
        ret = stringGsub(ret, "([%+%-%*%(%)%?%[%]%^])", "%%%1")
        -- remove capture indexes
        ret = stringGsub(ret, "%d%$", "")
        -- catch all characters
        ret = stringGsub(ret, "(%%%a)", "%(%1+%)")
        -- convert all %s to .+
        ret = stringGsub(ret, "%%s%+", ".+")
        -- set priority to numbers over strings
        ret = stringGsub(ret, "%(.%+%)%(%%d%+%)", "%(.-%)%(%%d%+%)")
        -- cache it
        sanitize_cache[pattern] = ret
    end

    return sanitize_cache[pattern]
end

local suffixPriority = {
    [""] = 1, -- No suffix (normal quests) - should come first
    ["+"] = 2, -- Elite
    ["S"] = 3, -- Scenario
    ["D"] = 4, -- Dungeon
    ["H"] = 5, -- Heroic
    ["R"] = 6, -- Raid
    ["++"] = 7, -- Legendary
    ["A"] = 8, -- Account
    ["C"] = 9, -- Celestial
    ["W"] = 10, -- World Event
}

local function compareQuestsByLevelAndType(a, b)
    if a[1] ~= b[1] then
        return a[1] < b[1]
    end

    -- if levels are the same, compare by suffix priority
    local suffixA = a[3] or ""
    local suffixB = b[3] or ""
    local priorityA = suffixPriority[suffixA] or 999
    local priorityB = suffixPriority[suffixB] or 999

    if priorityA ~= priorityB then
        return priorityA < priorityB
    end

    return a[2] < b[2]
end

function QuestieLib:SortQuestIDsByLevel(quests)
    local sortedQuestsByLevel = {}

    for q in pairs(quests) do
        local questLevel, _ = QuestieLib.GetTbcLevel(q)
        local suffix = QuestieLib:GetQuestTypeSuffix(q)
        tinsert(sortedQuestsByLevel, {questLevel or 0, q, suffix})
    end
    table.sort(sortedQuestsByLevel, compareQuestsByLevelAndType)

    return sortedQuestsByLevel
end

local randomSeed = 0
function QuestieLib:MathRandomSeed(seed)
    randomSeed = seed
end

function QuestieLib:MathRandom(low_or_high_arg, high_arg)
    local low
    local high
    if low_or_high_arg ~= nil then
        if high_arg ~= nil then
            low = low_or_high_arg
            high = high_arg
        else
            low = 1
            high = low_or_high_arg
        end
    end

    randomSeed = (randomSeed * 214013 + 2531011) % 2 ^ 32
    local rand = (math.floor(randomSeed / 2 ^ 16) % 2 ^ 15) / 0x7fff
    if not high then
        return rand
    end
    return low + math.floor(rand * high)
end

function QuestieLib:UnpackBinary(val)
    local ret = {}
    for q = 0, 25 do
        if bit.band(bit.rshift(val, q), 1) == 1 then
            tinsert(ret, true)
        else
            tinsert(ret, false)
        end
    end
    return ret
end

-- Link contains test bench for regex in lua.
-- https://hastebin.com/anodilisuw.bash
-- QUEST_MONSTERS_KILLED etc. patterns are from WoW API
local L_QUEST_MONSTERS_KILLED = QuestieLib:SanitizePattern(QUEST_MONSTERS_KILLED)
local L_QUEST_ITEMS_NEEDED = QuestieLib:SanitizePattern(QUEST_ITEMS_NEEDED)
local L_QUEST_OBJECTS_FOUND = QuestieLib:SanitizePattern(QUEST_OBJECTS_FOUND)

--- 'FooBar slain: 0/3' --> 'FooBar'
--- 'EpicItem : 0/1' --> 'EpicItem'
---@param text string @requires nil check and first character ~= " " check before call
---@param objectiveType string
function QuestieLib.TrimObjectiveText(text, objectiveType)
    local originalText = text

    if objectiveType == "monster" then
        local n, _, monsterName = smatch(text, L_QUEST_MONSTERS_KILLED)
        if tonumber(monsterName) then -- SOME objectives are reversed in TBC, why blizzard?
            monsterName = n
        end

        if (not monsterName) or (strlen(monsterName) == strlen(originalText)) then
            --The above doesn't seem to work with the chinese, the row below tries to remove the extra numbers.
            text = smatch(monsterName or text, "(.*)：");
        else
            text = monsterName
        end
    elseif objectiveType == "item" then
        local n, _, itemName = smatch(text, L_QUEST_ITEMS_NEEDED)
        if tonumber(itemName) then -- SOME objectives are reversed in TBC, why blizzard?
            itemName = n
        end

        text = itemName
    elseif objectiveType == "object" then
        local n, _, objectName = smatch(text, L_QUEST_OBJECTS_FOUND)
        if tonumber(objectName) then -- SOME objectives are reversed in TBC, why blizzard?
            objectName = n
        end

        text = objectName
    end

    -- If the functions above do not give a good answer fall back to older regex to get something.
    if not text then
        text = smatch(originalText, "^(.*):%s") or smatch(originalText, "%s：(.*)$") or smatch(originalText, "^(.*)：%s") or originalText
    end

    text = strim(text)
    --Questie:Debug(Questie.DEBUG_DEVELOP, "[TrimObjectiveText] \""..originalText.."\" --> \""..text.."\"") -- Comment out this debug for speed when not used.
    return text
end

---@return boolean
function QuestieLib.equals(a, b)
    if a == nil and b == nil then return true end
    if a == nil or b == nil then return false end
    local ta = type(a)
    local tb = type(b)
    if ta ~= tb then return false end

    if ta == "number" then
        return math.abs(a - b) < 0.2
    elseif ta == "table" then
        for k, v in pairs(a) do
            if (not QuestieLib.equals(b[k], v)) then
                return false
            end
        end
        for k, v in pairs(b) do
            if (not QuestieLib.equals(a[k], v)) then
                return false
            end
        end
        return true
    end

    return a == b
end

---@return table A table of the handed parameters plus the 'n' field with the size of the table
function QuestieLib.tpack(...)
    return {n = select("#", ...), ...}
end

--- Wow's own unpack stops at first nil. this version is not speed optimized.
--- Supports just above QuestieLib.tpack func as it requires the 'n' field.
---@param tbl table A table packed with QuestieLib.tpack
---@return table 'n' values of the tbl
function QuestieLib.tunpack(tbl)
    if tbl.n == 0 then
        return nil
    end

    local function recursion(i)
        if i == tbl.n then
            return tbl[i]
        end
        return tbl[i], recursion(i + 1)
    end

    return recursion(1)
end

---@alias TableWeakMode
---| '"v"'        # Weak Value
---| '"k"'        # Weak Key
---| '"kv"'       # Weak Value and Weak Key
---| '""'         # Regular table

---* Memoize a function with a cache
--! This does not support nil, never input nil into the table
---@param func function
---@param __mode TableWeakMode?
---@return table
function QuestieLib:TableMemoizeFunction(func, __mode)
    return setmetatable({}, {
        __index = function(self, k)
            local v = func(k);
            self[k] = v
            return v;
        end,
        __mode = __mode or ""
    });
end

--Part of the GameTooltipWrapDescription function
local textWrapObjectiveFontString
---Emulates the wrapping of a quest description
---@param line string @The line to wrap
---@param prefix string @The prefix to add to the line
---@param combineTrailing boolean @If the last line is only one word, combine it with previous? TRUE=COMBINE, FALSE=NOT COMBINE, default: true
---@param desiredWidth number @Set the desired width to wrap, default: 275
---@return table[] @A table of wrapped lines
function QuestieLib:TextWrap(line, prefix, combineTrailing, desiredWidth)
    if not textWrapObjectiveFontString then
        textWrapObjectiveFontString = UIParent:CreateFontString("questieObjectiveTextString", "ARTWORK", "QuestFont")
        textWrapObjectiveFontString:SetWidth(textWrapFrameObject:GetWidth() or 275) --QuestLogObjectivesText default width = 275
        textWrapObjectiveFontString:SetHeight(0);
        textWrapObjectiveFontString:SetPoint("LEFT");
        textWrapObjectiveFontString:SetJustifyH("LEFT");
        ---@diagnostic disable-next-line: redundant-parameter
        textWrapObjectiveFontString:SetWordWrap(true)
        textWrapObjectiveFontString:SetVertexColor(1, 1, 1, 1) --Set opacity to 0, even if it is shown it should be invisible
        local font, size = textWrapFrameObject:GetFont()
        --Chinese? "Fonts\\ARKai_T.ttf"
        textWrapObjectiveFontString:SetFont(font, size);
        textWrapObjectiveFontString:Hide()
    end

    if (textWrapObjectiveFontString:IsVisible()) then Questie:Error("TextWrap already running... Please report this on GitHub or Discord.") end

    --Set Defaults
    combineTrailing = combineTrailing or true
    --We show the fontstring and set the text to start the process
    --We have to show it or else the functions won't work... But we set the opacity to 0 on creation
    textWrapObjectiveFontString:SetWidth(desiredWidth or textWrapFrameObject:GetWidth() or 275) --QuestLogObjectivesText default width = 275
    textWrapObjectiveFontString:Show()

    local useLine = line

    textWrapObjectiveFontString:SetText(useLine)
    --Is the line wrapped?
    if (textWrapObjectiveFontString:GetUnboundedStringWidth() > textWrapObjectiveFontString:GetWrappedWidth()) then
        local lines = {}
        local startIndex = 1
        local endIndex = 2 --We should be able to start at a later index...
        --This function returns a list of size information per row, so we use this to calculate number of rows
        local numberOfRows = #textWrapObjectiveFontString:CalculateScreenAreaFromCharacterSpan(startIndex, strlen(useLine))
        for row = 1, numberOfRows do
            local lastSpaceIndex
            local indexes
            --We use the previous way to get number of rows to loop through characterindex until we get 2 rows
            repeat
                indexes = textWrapObjectiveFontString:CalculateScreenAreaFromCharacterSpan(startIndex, endIndex)
                --Last space of the line to be used to break a new row
                if (string.sub(useLine, endIndex, endIndex) == " ") then
                    lastSpaceIndex = endIndex
                end
                endIndex = endIndex + 1
                --If we are at the end of characters break and set endIndex to strlen
                if (endIndex > strlen(useLine)) then
                    endIndex = strlen(useLine)
                    lastSpaceIndex = endIndex
                    break
                end
            until (#indexes > 1) --Until more than one row

            --Get the line we calculated
            --First to space then endIndex(chinese)
            local newLine = string.sub(useLine, startIndex, lastSpaceIndex or endIndex)

            --This combines a trailing word to the previous line if it is the only word of the line
            --We check lastSpaceIndex here because the logic will be faulty (chinese client)
            if (row == numberOfRows - 1 and combineTrailing and lastSpaceIndex) then
                --Get the last line, in it's full
                local lastLine = string.sub(useLine, endIndex - 2, strlen(useLine))

                --Does the line not contain any space we combine it into the previous line
                if (not string.find(lastLine, " ")) then
                    newLine = string.sub(useLine, startIndex, strlen(useLine))
                    --print("NL1", newLine)
                    table.insert(lines, prefix .. newLine)
                    --Break the for loop on last line, no more running required
                    break
                end
            end
            --Change the startIndex to be the new line, and add the line to the lines list
            startIndex = endIndex - 2
            endIndex = endIndex

            table.insert(lines, prefix .. newLine)
        end
        textWrapObjectiveFontString:Hide()
        return lines
    else
        --Line was not wrapped, return the string as is.
        textWrapObjectiveFontString:Hide()
        useLine = prefix .. line
        return {useLine}
    end
end

function QuestieLib.GetSpawnDistance(spawnA, spawnB)
    local x1, y1 = spawnA[1], spawnA[2]
    local x2, y2 = spawnB[1], spawnB[2]

    -- Adjust the x-coordinate to account the map scale
    local distanceX = (x1 - x2) * 1.5
    local distanceY = y1 - y2

    return math_sqrt(distanceX * distanceX + distanceY * distanceY)
end

--- Checks if a daily reset has occurred since the player's last login.
---@return boolean True if a daily reset has occurred, false otherwise.
function QuestieLib.DidDailyResetHappenSinceLastLogin()
    if (not Questie.db) or (not Questie.db.global) then
        return true
    end

    Questie.db.global.lastKnownDailyReset = Questie.db.global.lastKnownDailyReset or {}

    local realmName = GetRealmName()
    local lastKnownDailyReset = Questie.db.global.lastKnownDailyReset[realmName]

    if (not lastKnownDailyReset) then
        return true -- No previous login recorded, assume a reset has occurred
    end

    return QuestieCompat.GetServerTime() >= lastKnownDailyReset
end

--- Updates the last known daily reset time to the next reset time.
function QuestieLib.UpdateLastKnownDailyReset()
    if (not Questie.db) or (not Questie.db.global) then
        return
    end

    Questie.db.global.lastKnownDailyReset = Questie.db.global.lastKnownDailyReset or {}

    local realmName = GetRealmName()

    Questie.db.global.lastKnownDailyReset[realmName] = getCurrentTimestamp() + GetQuestResetTime()
end

return QuestieLib
