
--[=[
search keys:
~internal
~comms
~timers
~callbacks

~unitinfo
~equipment
~opennotes
~cooldowns
~keystones


Please refer to the docs.txt within this file folder for a guide on how to use this library.
If you get lost on implementing the lib, be free to contact Tercio on Details! discord: https://discord.gg/AGSzAZX or email to terciob@gmail.com
--PLAYER_AVG_ITEM_LEVEL_UPDATE
UnitID:
    UnitID use: "player", "target", "raid18", "party3", etc...
    If passing the unit name, use GetUnitName(unitId, true) or Ambiguate(playerName, 'none')

Code Rules:
    - When a function or variable name refers to 'Player', it indicates the local player.
    - When 'Unit' is use instead, it indicates any entity.
    - Internal callbacks are the internal communication of the library, e.g. when an event triggers it send to all modules that registered that event.
    - Public callbacks are callbacks registered by an external addon.

TODO:
    - add into gear info how many tier set parts the player has
    - raid lockouts normal-heroic-mythic

BUGS:
    - after a /reload, it is not starting new tickers for spells under cooldown

--]=]

---@alias castername string
---@alias castspellid string
---@alias schedulename string

local GetSpecialization = C_SpecializationInfo and C_SpecializationInfo.GetSpecialization or GetSpecialization
local GetSpecializationInfo = C_SpecializationInfo and C_SpecializationInfo.GetSpecializationInfo or GetSpecializationInfo

LIB_OPEN_RAID_CAN_LOAD = false

local versionString, revision, launchDate, gameVersion = GetBuildInfo()

local isExpansion_Dragonflight = function()
	if (gameVersion >= 100000) then
		return true
	end
end

--don't load if it's not retail, emergencial patch due to classic and bcc stuff not transposed yet
if (WOW_PROJECT_ID ~= WOW_PROJECT_MAINLINE and not isExpansion_Dragonflight()) then
    return
end

local major = "LibOpenRaid-1.0"

local CONST_LIB_VERSION = 172

if (LIB_OPEN_RAID_MAX_VERSION) then
    if (CONST_LIB_VERSION <= LIB_OPEN_RAID_MAX_VERSION) then
        return
    end
end

--declare the library within the LibStub
    local libStub = _G.LibStub
    local openRaidLib = libStub:NewLibrary(major, CONST_LIB_VERSION)

    if (not openRaidLib) then
        return
    end

    openRaidLib.__version = CONST_LIB_VERSION
    LIB_OPEN_RAID_CAN_LOAD = true
    LIB_OPEN_RAID_MAX_VERSION = CONST_LIB_VERSION

    --locals
    local unpack = table.unpack or _G.unpack

    openRaidLib.__errors = {} --/dump LibStub:GetLibrary("LibOpenRaid-1.0").__errors

--default values
    openRaidLib.inGroup = false
    openRaidLib.UnitIDCache = {}

    openRaidLib.Util = openRaidLib.Util or {}

    local CONST_CVAR_TEMPCACHE = "LibOpenRaidTempCache"
    local CONST_CVAR_TEMPCACHE_DEBUG = "LibOpenRaidTempCacheDebug"

    --delay to request all data from other players
    local CONST_REQUEST_ALL_DATA_COOLDOWN = 30
    --delay to send all data to other players
    local CONST_SEND_ALL_DATA_COOLDOWN = 30

    --show failures (when the function return an error) results to chat
    local CONST_DIAGNOSTIC_ERRORS = false
    --show the data to be sent and data received from comm
    local CONST_DIAGNOSTIC_COMM = false
    --show data received from other players
    local CONST_DIAGNOSTIC_COMM_RECEIVED = false

    local CONST_COMM_PREFIX = "LRS"
    local CONST_COMM_PREFIX_LOGGED = "LRS_LOGGED"
    local CONST_COMM_FULLINFO_PREFIX = "F"

    local CONST_COMM_COOLDOWNUPDATE_PREFIX = "U"
    local CONST_COMM_COOLDOWNFULLLIST_PREFIX = "C"
    local CONST_COMM_COOLDOWNCHANGES_PREFIX = "S"
    local CONST_COMM_COOLDOWNREQUEST_PREFIX = "Z"

    local CONST_COMM_GEARINFO_FULL_PREFIX = "G"
    local CONST_COMM_GEARINFO_DURABILITY_PREFIX = "R"

    local CONST_COMM_PLAYER_DEAD_PREFIX = "D"
    local CONST_COMM_PLAYER_ALIVE_PREFIX = "A"
    local CONST_COMM_PLAYERINFO_PREFIX = "P"

    local CONST_COMM_KEYSTONE_DATA_PREFIX = "K"
    local CONST_COMM_KEYSTONE_DATAREQUEST_PREFIX = "J"

    local CONST_COMM_OPENNOTES_RECEIVED_PREFIX = "N" --when a note is received
    local CONST_COMM_OPENNOTES_REQUESTED_PREFIX = "Q" --when received a request to send your note

    local CONST_COMM_RATING_DATA_PREFIX = "M"
    local CONST_COMM_RATING_DATAREQUEST_PREFIX = "O"

    local CONST_COMM_SENDTO_PARTY = "0x1"
    local CONST_COMM_SENDTO_RAID = "0x2"
    local CONST_COMM_SENDTO_GUILD = "0x4"

    local CONST_ONE_SECOND = 1.0
    local CONST_TWO_SECONDS = 2.0
    local CONST_THREE_SECONDS = 3.0

    local CONST_SPECIALIZATION_VERSION_CLASSIC = 0
    local CONST_SPECIALIZATION_VERSION_MODERN = 1

    local CONST_COOLDOWN_CHECK_INTERVAL = CONST_THREE_SECONDS  --seconds between cooldown checks (ticker time)
    local CONST_COOLDOWN_TIMELEFT_HAS_CHANGED = CONST_THREE_SECONDS --time tolerance when checking if the cooldown timeleft has changed

    local CONST_COOLDOWN_INDEX_TIMELEFT = 1
    local CONST_COOLDOWN_INDEX_CHARGES = 2
    local CONST_COOLDOWN_INDEX_TIMEOFFSET = 3
    local CONST_COOLDOWN_INDEX_DURATION = 4
    local CONST_COOLDOWN_INDEX_UPDATETIME = 5
    local CONST_COOLDOWN_INDEX_AURA_DURATION = 6

    local CONST_COOLDOWN_INFO_SIZE = 6

    local CONST_USE_DEFAULT_SCHEDULE_TIME = true

    -- Real throttle is 10 messages per 1 second, but we want to be safe due to fact we dont know when it actually resets
    local CONST_COMM_BURST_BUFFER_COUNT = 9

    local GetContainerNumSlots = GetContainerNumSlots or C_Container.GetContainerNumSlots
    local GetContainerItemID = GetContainerItemID or C_Container.GetContainerItemID
    local GetContainerItemLink = GetContainerItemLink or C_Container.GetContainerItemLink

    --from vanilla to cataclysm, the specID did not existed, hence its considered version 0
    --for mists of pandaria and beyond it's version 1
    local getSpecializationVersion = function()
        if (gameVersion >= 50000) then
            return CONST_SPECIALIZATION_VERSION_MODERN
        else
            return CONST_SPECIALIZATION_VERSION_CLASSIC
        end
    end

    function openRaidLib.ShowDiagnosticErrors(value)
        CONST_DIAGNOSTIC_ERRORS = value
    end

    --make the 'pri-nt' word be only used once, this makes easier to find lost debug pri-nts in the code
    local sendChatMessage = function(...)
        print(...)
    end

    openRaidLib.DiagnosticError = function(msg, ...)
        if (CONST_DIAGNOSTIC_ERRORS) then
            sendChatMessage("|cFFFF9922OpenRaidLib|r:", msg, ...)
        end
    end

    local diagnosticFilter = nil
    local diagnosticComm = function(msg, ...)
        if (CONST_DIAGNOSTIC_COMM) then
            if (diagnosticFilter) then
                local lowerMessage = msg:lower()
                if (lowerMessage:find(diagnosticFilter)) then
                    sendChatMessage("|cFFFF9922OpenRaidLib|r:", msg, ...)
                    --dumpt(msg)
                end
            else
                sendChatMessage("|cFFFF9922OpenRaidLib|r:", msg, ...)
            end
        end
    end

    local diagnosticCommReceivedFilter = false
    openRaidLib.diagnosticCommReceived = function(msg, ...)
        if (diagnosticCommReceivedFilter) then
            local lowerMessage = msg:lower()
            if (lowerMessage:find(diagnosticCommReceivedFilter)) then
                sendChatMessage("|cFFFF9922OpenRaidLib|r:", msg, ...)
            end
        else
            sendChatMessage("|cFFFF9922OpenRaidLib|r:", msg, ...)
        end
    end


    openRaidLib.DeprecatedMessage = function(msg)
        sendChatMessage("|cFFFF9922OpenRaidLib|r:", "|cFFFF5555" .. msg .. "|r")
    end

    --set the ticker interval to check if the cooldown has changed
    function openRaidLib.SetCooldownCheckInterval(value)
        CONST_COOLDOWN_CHECK_INTERVAL = value
    end

    local isTimewalkWoW = function()
        local _, _, _, buildInfo = GetBuildInfo()
        if (buildInfo < 40000) then
            return true
        end
    end

    local checkClientVersion = function(...)
        for i = 1, select("#", ...) do
            local clientVersion = select(i, ...)

            if (clientVersion == "retail" and (WOW_PROJECT_ID == WOW_PROJECT_MAINLINE or isExpansion_Dragonflight())) then --retail
                return true

            elseif (clientVersion == "classic_era" and WOW_PROJECT_ID == WOW_PROJECT_CLASSIC) then --classic era (vanila)
                return true

            elseif (clientVersion == "bcc" and WOW_PROJECT_ID == WOW_PROJECT_BURNING_CRUSADE_CLASSIC) then --the burning crusade classic
                return true
            end
        end
    end

--------------------------------------------------------------------------------------------------------------------------------
--~internal cache
--use a console variable to create a flash cache to keep data while the game reload
--this is not a long term database as saved variables are and it get clean up often

C_CVar.RegisterCVar(CONST_CVAR_TEMPCACHE)
C_CVar.RegisterCVar(CONST_CVAR_TEMPCACHE_DEBUG)

--internal namespace
local tempCache = {
    debugString = "",
}

tempCache.copyCache = function(t1, t2)
    for key, value in pairs(t2) do
        if (type(value) == "table") then
            t1[key] = t1[key] or {}
            tempCache.copyCache(t1[key], t2[key])
        else
            t1[key] = value
        end
    end
    return t1
end

--use debug cvar to find issues that occurred during the logoff process
function openRaidLib.PrintTempCacheDebug()
    local debugMessage = C_CVar.GetCVar(CONST_CVAR_TEMPCACHE_DEBUG)
    sendChatMessage("|cFFFF9922OpenRaidLib|r Temp CVar Result:\n", debugMessage)
end

function tempCache.SaveDebugText()
    C_CVar.SetCVar(CONST_CVAR_TEMPCACHE_DEBUG, "0")
    --C_CVar.SetCVar(CONST_CVAR_TEMPCACHE_DEBUG, tempCache.debugString)
end

function tempCache.AddDebugText(text)
    tempCache.debugString = tempCache.debugString .. date("%H:%M:%S") .. "| " .. text .. "\n"
end

function tempCache.SaveCacheOnCVar(data)
    C_CVar.SetCVar(CONST_CVAR_TEMPCACHE, "0")
    --C_CVar.SetCVar(CONST_CVAR_TEMPCACHE, data)
    tempCache.AddDebugText("CVars Saved on saveCahceOnCVar(), Size: " .. #data)
end

function tempCache.RestoreData()
    local data = C_CVar.GetCVar(CONST_CVAR_TEMPCACHE)
    if (data and type(data) == "string" and string.len(data) > 2) then
        local LibAceSerializer = LibStub:GetLibrary("AceSerializer-3.0", true)
        if (LibAceSerializer) then
            local okay, cacheInfo = LibAceSerializer:Deserialize(data)
            if (okay) then
                local age = cacheInfo.createdAt
                --if the data is older than 5 minutes, much has been changed from the group and the data is out dated
                if (age + (60 * 5) < time()) then
                    return
                end

                local unitsInfo = cacheInfo.unitsInfo
                local cooldownsInfo = cacheInfo.cooldownsInfo
                local gearInfo = cacheInfo.gearInfo

                local okayUnitsInfo, unitsInfo = LibAceSerializer:Deserialize(unitsInfo)
                local okayCooldownsInfo, cooldownsInfo = LibAceSerializer:Deserialize(cooldownsInfo)
                local okayGearInfo, gearInfo = LibAceSerializer:Deserialize(gearInfo)

                if (okayUnitsInfo and unitsInfo) then
                    openRaidLib.UnitInfoManager.UnitData = tempCache.copyCache(openRaidLib.UnitInfoManager.UnitData, unitsInfo)
                else
                    tempCache.AddDebugText("invalid UnitInfo")
                end

                if (okayCooldownsInfo and cooldownsInfo) then
                    openRaidLib.CooldownManager.UnitData = tempCache.copyCache(openRaidLib.CooldownManager.UnitData, cooldownsInfo)
                else
                    tempCache.AddDebugText("invalid CooldownsInfo")
                end

                if (okayGearInfo and gearInfo) then
                    openRaidLib.GearManager.UnitData = tempCache.copyCache(openRaidLib.GearManager.UnitData, gearInfo)
                else
                    tempCache.AddDebugText("invalid GearInfo")
                end
            else
                tempCache.AddDebugText("Deserialization not okay, reason: " .. cacheInfo)
            end
        else
            tempCache.AddDebugText("LibAceSerializer not found")
        end
    else
        if (not data) then
            tempCache.AddDebugText("invalid temporary cache: getCVar returned nil")
        elseif (type(data) ~= "string") then
            tempCache.AddDebugText("invalid temporary cache: getCVar did not returned a string")
        elseif (string.len(data) < 2) then
            tempCache.AddDebugText("invalid temporary cache: data length lower than 2 bytes (first login?)")
        else
            tempCache.AddDebugText("invalid temporary cache: no reason found")
        end
    end
end

function tempCache.SaveData()
    tempCache.AddDebugText("SaveData() called.")

    local LibAceSerializer = LibStub:GetLibrary("AceSerializer-3.0", true)
    if (LibAceSerializer) then
        local allUnitsInfo = openRaidLib.UnitInfoManager.UnitData
        local allUnitsCooldowns = openRaidLib.CooldownManager.UnitData
        local allPlayersGear = openRaidLib.GearManager.UnitData

        local cacheInfo = {
            createdAt = time(),
        }

        local unitsInfoSerialized = LibAceSerializer:Serialize(allUnitsInfo)
        local unitsCooldownsSerialized = LibAceSerializer:Serialize(allUnitsCooldowns)
        local playersGearSerialized = LibAceSerializer:Serialize(allPlayersGear)

        if (unitsInfoSerialized) then
            cacheInfo.unitsInfo = unitsInfoSerialized
            tempCache.AddDebugText("SaveData() units info serialized okay.")
        else
            tempCache.AddDebugText("SaveData() units info serialized failed.")
        end

        if (unitsCooldownsSerialized) then
            cacheInfo.cooldownsInfo = unitsCooldownsSerialized
            tempCache.AddDebugText("SaveData() cooldowns info serialized okay.")
        else
            tempCache.AddDebugText("SaveData() cooldowns info serialized failed.")
        end

        if (playersGearSerialized) then
            cacheInfo.gearInfo = playersGearSerialized
            tempCache.AddDebugText("SaveData() gear info serialized okay.")
        else
            tempCache.AddDebugText("SaveData() gear info serialized failed.")
        end

        local cacheInfoSerialized = LibAceSerializer:Serialize(cacheInfo)
        tempCache.SaveCacheOnCVar(cacheInfoSerialized)
    else
        tempCache.AddDebugText("SaveData() AceSerializer not found.")
    end

    tempCache.SaveDebugText()
end


--------------------------------------------------------------------------------------------------------------------------------
--~comms
    openRaidLib.commHandler = {
        aceComm = {},
        eventFrame = CreateFrame("frame"),
    }

    function openRaidLib.commHandler.OnReceiveSafeComm(self, event, prefix, text, channel, sender, target, zoneChannelID, localID, name, instanceID)
        if (prefix == CONST_COMM_PREFIX_LOGGED) then
            sender = Ambiguate(sender, "none")

            --don't receive comms from the player it self
            local playerName = UnitName("player")
            if (playerName == sender) then
                return
            end

            local commId = 0

            --verify if this is a safe comm
            local data = ""
            local bIsSafe = event == "CHAT_MSG_ADDON_LOGGED"
            if (bIsSafe) then
                data = text:gsub("%%", "\n")
                --replace the the first ";" found in the data string with a ",", only the first occurence
                data = data:gsub(";", ",", 1)
                --get the commId
                commId = data:match("#([^#]+)$")
                --remove the commId from the data
                data = data:gsub("#([^#]+)$", "")
                --add the commId to the end of the data after a comma
                data = data .. "," .. commId

                openRaidLib.commHandler.OnReceiveComm(event, CONST_COMM_PREFIX, data, channel, sender, target, zoneChannelID, localID, name, instanceID, true)
            end
        end
    end

    openRaidLib.commHandler.eventFrame:RegisterEvent("CHAT_MSG_ADDON_LOGGED")
    openRaidLib.commHandler.eventFrame:SetScript("OnEvent", openRaidLib.commHandler.OnReceiveSafeComm)

    function openRaidLib.commHandler.aceComm.OnReceiveComm(event, prefix, text, channel, sender, target, zoneChannelID, localID, name, instanceID, bIsSafe)
        --check if the data belong to us
        if (prefix == CONST_COMM_PREFIX) then
            sender = Ambiguate(sender, "none")

            --don't receive comms from the player it self
            local playerName = UnitName("player")
            if (playerName == sender) then
                return
            end

            --if this received data is not a safe comm, then decode it
            local data = text
            if (not bIsSafe) then
                local LibDeflate = LibStub:GetLibrary("LibDeflate")
                local dataCompressed = LibDeflate:DecodeForWoWAddonChannel(data)
                data = LibDeflate:DecompressDeflate(dataCompressed)
            end

            --some users are reporting errors where 'data is nil'. Making some sanitization
            if (not data) then
                openRaidLib.DiagnosticError("Invalid data from player:", sender, "data:", text)
                return
            elseif (type(data) ~= "string") then
                openRaidLib.DiagnosticError("Invalid data from player:", sender, "data:", text, "data type is:", type(data))
                return
            end

            --get the first byte of the data, it indicates what type of data was transmitted
            local dataTypePrefix = data:match("^.")
            if (not dataTypePrefix) then
                openRaidLib.DiagnosticError("Invalid dataTypePrefix from player:", sender, "data:", data, "dataTypePrefix:", dataTypePrefix)
                return
            elseif (openRaidLib.commPrefixDeprecated[dataTypePrefix]) then
                openRaidLib.DiagnosticError("Invalid dataTypePrefix from player:", sender, "data:", data, "dataTypePrefix:", dataTypePrefix)
                return
            end

            --if this is isn't a keystone data comm, check if the lib can receive comms
            if (dataTypePrefix ~= CONST_COMM_KEYSTONE_DATA_PREFIX and dataTypePrefix ~= CONST_COMM_KEYSTONE_DATAREQUEST_PREFIX and dataTypePrefix ~= CONST_COMM_RATING_DATA_PREFIX and dataTypePrefix ~= CONST_COMM_RATING_DATAREQUEST_PREFIX) then
                if (not openRaidLib.IsCommAllowed()) then
                    openRaidLib.DiagnosticError("comm not allowed.")
                    return
                end
            end

            if (CONST_DIAGNOSTIC_COMM_RECEIVED) then
                openRaidLib.diagnosticCommReceived(data)
            end

            --get the table with functions regitered for this type of data
            local callbackTable = openRaidLib.commHandler.commCallback[dataTypePrefix]
            if (not callbackTable) then
                openRaidLib.DiagnosticError("Not callbackTable for dataTypePrefix:", dataTypePrefix, "from player:", sender, "data:", data)
                return
            end

            --convert to table
            local dataAsTable = {strsplit(",", data)}

            --remove the first index (prefix)
            table.remove(dataAsTable, 1)

            --trigger callbacks
            for i = 1, #callbackTable do
                callbackTable[i](dataAsTable, sender)
            end
        end
    end

    local aceComm = LibStub:GetLibrary("AceComm-3.0", true)
    if (aceComm) then
        aceComm:Embed(openRaidLib.commHandler.aceComm)
        openRaidLib.commHandler.aceComm:RegisterComm(CONST_COMM_PREFIX, "OnReceiveComm")
    end


    openRaidLib.commHandler.commCallback = {
                                            --when transmiting
        [CONST_COMM_FULLINFO_PREFIX] = {}, --update all
        [CONST_COMM_COOLDOWNFULLLIST_PREFIX] = {}, --all cooldowns of a player
        [CONST_COMM_COOLDOWNUPDATE_PREFIX] = {}, --an update of a single cooldown
        [CONST_COMM_COOLDOWNCHANGES_PREFIX] = {}, --cooldowns got added or removed
        [CONST_COMM_COOLDOWNREQUEST_PREFIX] = {}, --a unit requested an update on a spell
        [CONST_COMM_GEARINFO_FULL_PREFIX] = {}, --an update of gear information
        [CONST_COMM_GEARINFO_DURABILITY_PREFIX] = {}, --an update of the player gear durability
        [CONST_COMM_PLAYER_DEAD_PREFIX] = {}, --player is dead
        [CONST_COMM_PLAYER_ALIVE_PREFIX] = {}, --player is alive
        [CONST_COMM_PLAYERINFO_PREFIX] = {}, --info about the player
        [CONST_COMM_KEYSTONE_DATA_PREFIX] = {}, --received keystone data
        [CONST_COMM_KEYSTONE_DATAREQUEST_PREFIX] = {}, --received a request to send keystone data
        [CONST_COMM_OPENNOTES_RECEIVED_PREFIX] = {}, --received notes
        [CONST_COMM_OPENNOTES_REQUESTED_PREFIX] = {}, --requested notes
        [CONST_COMM_RATING_DATA_PREFIX] = {}, --received rating data
        [CONST_COMM_RATING_DATAREQUEST_PREFIX] = {}, --received a request to send rating data
    }

    function openRaidLib.commHandler.RegisterORComm(prefix, func)
        --the table for the prefix need to be declared at the 'openRaidLib.commHandler.commCallback' table
        table.insert(openRaidLib.commHandler.commCallback[prefix], func)
    end

    local charactesrPerMessage = 251
    local receivingMsgInParts = {}

    local debugCommReception = CreateFrame("frame")
    debugCommReception:RegisterEvent("CHAT_MSG_ADDON_LOGGED")
    debugCommReception:SetScript("OnEvent", function(self, event, prefix, text, channel, sender, target, zoneChannelID, localID, name, instanceID)
        if (prefix == CONST_COMM_PREFIX_LOGGED) then
            local chunkNumber, totalChunks, data = text:match("^%$(%d+)%$(%d+)(.*)")
            local onlyData = text:match("^(.*)")

            if (not chunkNumber and not totalChunks and onlyData) then
                openRaidLib.commHandler.OnReceiveComm(self, "CHAT_MSG_ADDON_LOGGED", CONST_COMM_PREFIX, onlyData, channel, sender, target, zoneChannelID, localID, name, instanceID)

            elseif (chunkNumber and totalChunks and data) then
                chunkNumber = tonumber(chunkNumber)
                totalChunks = tonumber(totalChunks)

                if (chunkNumber and totalChunks) then
                    if (chunkNumber <= totalChunks and chunkNumber >= 1) then
                        if (not receivingMsgInParts[sender]) then
                            local parts = {}
                            for i = 1, totalChunks do
                                parts[i] = false
                            end
                            receivingMsgInParts[sender] = {
                                totalChunks = totalChunks,
                                chunks = parts
                            }
                        end

                        receivingMsgInParts[sender].chunks[chunkNumber] = data

                        --verify if all parts were received
                        local allPartsReceived = true
                        for i = 1, totalChunks do
                            if (not receivingMsgInParts[sender].chunks[i]) then
                                allPartsReceived = false
                                break
                            end
                        end

                        if (allPartsReceived) then
                            local fullData = ""
                            --sew the parts together
                            for i = 1, totalChunks do
                                fullData = fullData .. receivingMsgInParts[sender].chunks[i]
                            end

                            receivingMsgInParts[sender] = nil
                            openRaidLib.commHandler.OnReceiveComm(self, "CHAT_MSG_ADDON_LOGGED", CONST_COMM_PREFIX, fullData, channel, sender, target, zoneChannelID, localID, name, instanceID)
                        end
                    end
                end
            else
                openRaidLib.DiagnosticError("Logged comm in parts missing information, sender:", sender, "chunkNumber:", chunkNumber, "totalChunks:", totalChunks, "data:", type(data))
            end
        end
    end)

    --@flags
    --0x1: to party
    --0x2: to raid
    --0x4: to guild
    local sendData = function(dataEncoded, channel, bIsSafe, plainText)
        local aceComm = LibStub:GetLibrary("AceComm-3.0", true)
        if (aceComm) then
            if (bIsSafe) then
                plainText = plainText:gsub("\n", "%%")
                plainText = plainText:gsub(",", ";")

                local commId = tostring(GetServerTime() + GetTime())
                plainText = plainText .. "#" .. commId

                if (plainText:len() > 255) then
                    local totalMessages = math.ceil(plainText:len() / charactesrPerMessage)
                    for i = 1, totalMessages do
                        local chunk = plainText:sub((i - 1) * charactesrPerMessage + 1, i * charactesrPerMessage)
                        local chunkNumberAndTotalChuncks = "$" .. i .. "$" .. totalMessages
                        local chunkMessage = chunkNumberAndTotalChuncks .. chunk
                        ChatThrottleLib:SendAddonMessageLogged("NORMAL", CONST_COMM_PREFIX_LOGGED, chunkMessage, channel)
                    end
                else
                    ChatThrottleLib:SendAddonMessageLogged("NORMAL", CONST_COMM_PREFIX_LOGGED, plainText, channel)
                end
            else
                aceComm:SendCommMessage(CONST_COMM_PREFIX, dataEncoded, channel, nil, "ALERT")
            end
        else
            C_ChatInfo.SendAddonMessage(CONST_COMM_PREFIX, dataEncoded, channel)
        end
    end

	if (C_ChatInfo) then
		C_ChatInfo.RegisterAddonMessagePrefix(CONST_COMM_PREFIX_LOGGED)
	else
		RegisterAddonMessagePrefix(CONST_COMM_PREFIX_LOGGED)
	end

    ---@class commdata : table
    ---@field data string
    ---@field channel string
    ---@field bIsSafe boolean
    ---@field plainText string

    ---@type {}[]
    local commScheduler = {};

    local commBurstBufferCount = CONST_COMM_BURST_BUFFER_COUNT;
    local commServerTimeLastThrottleUpdate = GetServerTime();

    do
        --if there's an old version that already registered the comm ticker, cancel it
        if (LIB_OPEN_RAID_COMM_SCHEDULER) then
            LIB_OPEN_RAID_COMM_SCHEDULER:Cancel();
        end

        local newTickerHandle = C_Timer.NewTicker(0.05, function()
            local serverTime = GetServerTime();

            -- Replenish the counter if last server time is not the same as the last throttle update
            -- Clamp it to CONST_COMM_BURST_BUFFER_COUNT
            commBurstBufferCount = math.min((serverTime ~= commServerTimeLastThrottleUpdate) and commBurstBufferCount + 1 or commBurstBufferCount, CONST_COMM_BURST_BUFFER_COUNT);
            commServerTimeLastThrottleUpdate = serverTime;

            -- while (anything in queue) and (throttle allows it)
            while(#commScheduler > 0 and commBurstBufferCount > 0) do
                -- FIFO queue
                ---@type commdata
                local commData = table.remove(commScheduler, 1);
                sendData(commData.data, commData.channel, commData.bIsSafe, commData.plainText);
                commBurstBufferCount = commBurstBufferCount - 1;
            end
        end);

        LIB_OPEN_RAID_COMM_SCHEDULER = newTickerHandle
    end

    function openRaidLib.commHandler.SendCommData(data, flags, bIsSafe)
        local LibDeflate = LibStub:GetLibrary("LibDeflate")
        local dataCompressed = LibDeflate:CompressDeflate(data, {level = 9})
        local dataEncoded = LibDeflate:EncodeForWoWAddonChannel(dataCompressed)

        if (flags) then
            if (bit.band(flags, CONST_COMM_SENDTO_PARTY)) then --send to party
                if (IsInGroup() and not IsInRaid()) then
                    ---@type commdata
                    local commData = {data = dataEncoded, channel = IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and "INSTANCE_CHAT" or "PARTY", bIsSafe = bIsSafe, plainText = data}
                    table.insert(commScheduler, commData)
                end
            end

            if (bit.band(flags, CONST_COMM_SENDTO_RAID)) then --send to raid
                if (IsInRaid()) then
                    local commData = {data = dataEncoded, channel = IsInRaid(LE_PARTY_CATEGORY_INSTANCE) and "INSTANCE_CHAT" or "RAID", bIsSafe = bIsSafe, plainText = data}
                    table.insert(commScheduler, commData)
                end
            end

            if (bit.band(flags, CONST_COMM_SENDTO_GUILD)) then --send to guild
                if (IsInGuild()) then
                    --Guild has no 10 msg restriction so send it directly
                    sendData(dataEncoded, "GUILD");
                end
            end
        else
            if (IsInGroup() and not IsInRaid()) then --in party only
                local commData = {data = dataEncoded, channel = IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and "INSTANCE_CHAT" or "PARTY", bIsSafe = bIsSafe, plainText = data}
                table.insert(commScheduler, commData)

            elseif (IsInRaid()) then
                local commData = {data = dataEncoded, channel = IsInRaid(LE_PARTY_CATEGORY_INSTANCE) and "INSTANCE_CHAT" or "RAID", bIsSafe = bIsSafe, plainText = data}
                table.insert(commScheduler, commData)
            end
        end
	end

--------------------------------------------------------------------------------------------------------------------------------
--~schedule ~timers
    ---@type table<schedulename, number>
    local defaultScheduleCooldownTimeByScheduleName = {
        ["sendAllPlayerCooldownsFromTalentChange_Schedule"] = 2,
        ["talentChangedCallback_Schedule"] = 20,
        ["sendFullData_Schedule"] = 25,
        ["sendAllPlayerCooldowns_Schedule"] = 23,
        ["sendDurability_Schedule"] = 10,
        ["sendAllGearInfo_Schedule"] = 20,
        ["petStatus_Schedule"] = 8,
        ["updatePlayerData_Schedule"] = 22,
        ["sendTalent_Schedule"] = 20,
        ["sendPvPTalent_Schedule"] = 14,
        ["leaveCombat_Schedule"] = 18,
        ["encounterEndCooldownsCheck_Schedule"] = 24,
        --["sendKeystoneInfoToParty_Schedule"] = 2,
        --["sendKeystoneInfoToGuild_Schedule"] = 2,
    }

    openRaidLib.Schedules = {
        registeredUniqueTimers = {}
    }

    local timersCanRunWithoutGroup = {
        ["mainControl"] = {
            ["updatePlayerData_Schedule"] = true
        }
    }

    --run a scheduled function with its payload
    local triggerScheduledTick = function(tickerObject)
        local payload = tickerObject.payload
        local callback = tickerObject.callback
        local bCanRunWithoutGroup = tickerObject.bCanRunWithoutGroup

        if (tickerObject.isUnique) then
            local namespace = tickerObject.namespace
            local scheduleName = tickerObject.scheduleName
            openRaidLib.Schedules.CancelUniqueTimer(namespace, scheduleName)
        end

        --check if the player is still in group
        if (not openRaidLib.IsInGroup()) then
            if (not bCanRunWithoutGroup) then
                return
            end
        end

        local result, errortext = xpcall(callback, geterrorhandler(), unpack(payload))
        --local result, errortext = pcall(callback, unpack(payload))
        if (not result) then
            sendChatMessage("openRaidLib: error on scheduler:", tickerObject.scheduleName, tickerObject.stack)
        end

        return result
    end

    --create a new schedule
    function openRaidLib.Schedules.NewTimer(time, callback, bCanRunWithoutGroup, ...)
        local payload = {...}
        local newTimer = C_Timer.NewTimer(time, triggerScheduledTick)
        newTimer.bCanRunWithoutGroup = bCanRunWithoutGroup
        newTimer.payload = payload
        newTimer.callback = callback
        --newTimer.stack = debugstack()
        return newTimer
    end

    --create an unique schedule
    --if a schedule already exists, cancels it and make a new ~unique
    function openRaidLib.Schedules.NewUniqueTimer(time, callback, namespace, scheduleName, ...)
        --the the schedule uses a default time, get it from the table, if the timer already exists, quit
        if (time == CONST_USE_DEFAULT_SCHEDULE_TIME) then
            if (openRaidLib.Schedules.IsUniqueTimerOnCooldown(namespace, scheduleName)) then
                return
            end
            time = defaultScheduleCooldownTimeByScheduleName[scheduleName] or time
        else
            openRaidLib.Schedules.CancelUniqueTimer(namespace, scheduleName)
        end

        local bCanRunWithoutGroup = timersCanRunWithoutGroup[namespace] and timersCanRunWithoutGroup[namespace][scheduleName]

        local newTimer = openRaidLib.Schedules.NewTimer(time, callback, bCanRunWithoutGroup, ...)
        newTimer.namespace = namespace
        newTimer.scheduleName = scheduleName
        --newTimer.stack = debugstack()
        newTimer.isUnique = true

        local registeredUniqueTimers = openRaidLib.Schedules.registeredUniqueTimers
        registeredUniqueTimers[namespace] = registeredUniqueTimers[namespace] or {}
        registeredUniqueTimers[namespace][scheduleName] = newTimer
    end

    --does timer by schedule name exists?
    function openRaidLib.Schedules.IsUniqueTimerOnCooldown(namespace, scheduleName)
        local registeredUniqueTimers = openRaidLib.Schedules.registeredUniqueTimers
        local currentSchedule = registeredUniqueTimers[namespace] and registeredUniqueTimers[namespace][scheduleName]

        if (currentSchedule) then
            return true
        end
        return false
    end

    --cancel an unique schedule
    function openRaidLib.Schedules.CancelUniqueTimer(namespace, scheduleName)
        local registeredUniqueTimers = openRaidLib.Schedules.registeredUniqueTimers
        local currentSchedule = registeredUniqueTimers[namespace] and registeredUniqueTimers[namespace][scheduleName]

        if (currentSchedule) then
            if (not currentSchedule:IsCancelled()) then
                currentSchedule:Cancel()
            end
            registeredUniqueTimers[namespace][scheduleName] = nil
        end
    end

    --cancel all unique timers
    function openRaidLib.Schedules.CancelAllUniqueTimers()
        local registeredUniqueTimers = openRaidLib.Schedules.registeredUniqueTimers
        for namespace, schedulesTable in pairs(registeredUniqueTimers) do
            for scheduleName, timerObject in pairs(schedulesTable) do
                if (timerObject and not timerObject:IsCancelled()) then
                    timerObject:Cancel()
                end
            end
        end
        table.wipe(registeredUniqueTimers)
    end


--------------------------------------------------------------------------------------------------------------------------------
--~public ~callbacks
--these are the events where other addons can register and receive calls
    local allPublicCallbacks = {
        "CooldownListUpdate",
        "CooldownListWipe",
        "CooldownUpdate",
        "CooldownAdded",
        "CooldownRemoved",
        "UnitDeath",
        "UnitAlive",
        "GearListWipe",
        "GearUpdate",
        "GearDurabilityUpdate",
        "UnitInfoUpdate",
        "UnitInfoWipe",
        "TalentUpdate", --deprecated
        "PvPTalentUpdate", --deprecated
        "KeystoneUpdate",
        "KeystoneWipe",
        "NoteUpdated",
        "RatingUpdate",
        "RatingWipe"
    }

    --save build the table to avoid lose registered events on older versions
    openRaidLib.publicCallback = openRaidLib.publicCallback or {}
    openRaidLib.publicCallback.events = openRaidLib.publicCallback.events or {}
    for _, callbackName in ipairs(allPublicCallbacks) do
        openRaidLib.publicCallback.events[callbackName] = openRaidLib.publicCallback.events[callbackName] or {}
    end

    local checkRegisterDataIntegrity = function(addonObject, event, callbackMemberName)
        --check of integrity
        if (type(addonObject) == "string") then
            addonObject = _G[addonObject]
        end

        if (type(addonObject) ~= "table") then
            return 1
        end

        if (not openRaidLib.publicCallback.events[event]) then
            return 2

        elseif (not addonObject[callbackMemberName]) then
            return 3
        end

        return true
    end

    --call the registered function within the addon namespace
    --payload is sent together within the call
    function openRaidLib.publicCallback.TriggerCallback(event, ...)
        local eventCallbacks = openRaidLib.publicCallback.events[event]

        for i = 1, #eventCallbacks do
            local thisCallback = eventCallbacks[i] --got a case where this was nil, which is kinda impossible? | event: CooldownUpdate
            local addonObject = thisCallback[1] --670: attempt to index local 'thisCallback' (a nil value)
            local functionName = thisCallback[2]

            --[=[
                eventCallbacks = {
                    1 = {}
                }

                (for index) = 2
                (for limit) = 2
                (for step) = 1
                i = 2

                thisCallback = nil
            --]=]

            --get the function from within the addon object
            local functionToCallback = addonObject[functionName]

            if (functionToCallback) then
                --if this isn't a function, xpcall trigger an error
                local okay, errorMessage = xpcall(functionToCallback, geterrorhandler(), ...)
                if (not okay) then
                    sendChatMessage("error on callback for event:", event)
                end
            else
                --the registered function wasn't found
            end
        end
    end

    function openRaidLib.RegisterCallback(addonObject, event, callbackMemberName)
        --check of integrity
        local passIntegrityTest = checkRegisterDataIntegrity(addonObject, event, callbackMemberName)
        if (passIntegrityTest and type(passIntegrityTest) ~= "boolean") then
            return passIntegrityTest
        end

        --register
        tinsert(openRaidLib.publicCallback.events[event], {addonObject, callbackMemberName})
        return true
    end

    function openRaidLib.UnregisterCallback(addonObject, event, callbackMemberName)
        --check of integrity
        local passIntegrityTest = checkRegisterDataIntegrity(addonObject, event, callbackMemberName)
        if (passIntegrityTest and type(passIntegrityTest) ~= "boolean") then
            return passIntegrityTest
        end

        for i = 1, #openRaidLib.publicCallback.events[event] do
            local registeredCallback = openRaidLib.publicCallback.events[event][i]
            if (registeredCallback[1] == addonObject and registeredCallback[2] == callbackMemberName) then
                table.remove(openRaidLib.publicCallback.events[event], i)
                break
            end
        end
    end


--------------------------------------------------------------------------------------------------------------------------------
--~internal ~callbacks
--internally, each module can register events through the internal callback to be notified when something happens in the game

    openRaidLib.internalCallback = {}
    openRaidLib.internalCallback.events = {
        ["onEnterGroup"] = {},
        ["onLeaveGroup"] = {},
        ["onLeaveCombat"] = {},
        ["playerCast"] = {},
        ["onEnterWorld"] = {},
        ["talentUpdate"] = {},
        ["pvpTalentUpdate"] = {},
        ["onPlayerDeath"] = {},
        ["onPlayerRess"] = {},
        ["raidEncounterStart"] = {},
        ["raidEncounterEnd"] = {},
        ["mythicDungeonStart"] = {},
        ["playerPetChange"] = {},
        ["mythicDungeonEnd"] = {},
        ["unitAuraRemoved"] = {},
    }

    openRaidLib.internalCallback.RegisterCallback = function(event, func)
        tinsert(openRaidLib.internalCallback.events[event], func)
    end

    openRaidLib.internalCallback.UnRegisterCallback = function(event, func)
        local eventCallbacks = openRaidLib.internalCallback.events[event]
        for i = 1, #eventCallbacks do
            if (eventCallbacks[i] == func) then
                table.remove(eventCallbacks, i)
                break
            end
        end
    end

    function openRaidLib.internalCallback.TriggerEvent(event, ...)
        local eventCallbacks = openRaidLib.internalCallback.events[event]
        for i = 1, #eventCallbacks do
            local functionToCallback = eventCallbacks[i]
            functionToCallback(event, ...)
        end
    end

    --create the frame for receiving game events
    local eventFrame = _G.OpenRaidLibFrame
    if (not eventFrame) then
        eventFrame = CreateFrame("frame", "OpenRaidLibFrame", UIParent)
    end

    local talentChangedCallback = function()
        openRaidLib.internalCallback.TriggerEvent("talentUpdate")
    end
    local delayedTalentChange = function()
        openRaidLib.Schedules.NewUniqueTimer(math.random(4, 8), talentChangedCallback, "TalentChangeEventGroup", "talentChangedCallback_Schedule")
    end

    local eventFunctions = {
        --check if the player joined a group
        ["GROUP_ROSTER_UPDATE"] = function()
            local bEventTriggered = false
            if (openRaidLib.IsInGroup()) then
                if (not openRaidLib.inGroup) then
                    openRaidLib.inGroup = true
                    openRaidLib.internalCallback.TriggerEvent("onEnterGroup")
                    bEventTriggered = true
                end
            else
                if (openRaidLib.inGroup) then
                    openRaidLib.inGroup = false
                    openRaidLib.internalCallback.TriggerEvent("onLeaveGroup")
                    bEventTriggered = true
                end
            end

            if (not bEventTriggered and openRaidLib.IsInGroup()) then --the player didn't left or enter a group
                --the group has changed, trigger a long timer to send full data
                --as the timer is unique, a new change to the group will replace and refresh the time
                --using random time, players won't trigger all at the same time
                local randomTime = 5 + math.random() + math.random(1, 5)
                openRaidLib.Schedules.NewUniqueTimer(randomTime, openRaidLib.mainControl.SendFullData, "mainControl", "sendFullData_Schedule")
            end

            openRaidLib.UpdateUnitIDCache()
        end,

        ["UNIT_SPELLCAST_SUCCEEDED"] = function(...)
            local unitId, castGUID, spellId = ...
            C_Timer.After(0.1, function()
                --some spells has many different spellIds, get the default
                spellId = LIB_OPEN_RAID_SPELL_DEFAULT_IDS[spellId] or spellId
                --trigger internal callbacks
                openRaidLib.internalCallback.TriggerEvent("playerCast", spellId, UnitIsUnit(unitId, "pet"))
            end)
        end,

        ["PLAYER_ENTERING_WORLD"] = function(...)
            --has the selected character just loaded?
            if (not openRaidLib.bHasEnteredWorld) then
                --register events
                openRaidLib.OnEnterWorldRegisterEvents()

                --openRaidLib.AuraTracker.StartScanUnitAuras("player")

                if (IsInGroup()) then
                    openRaidLib.RequestAllData()
                    openRaidLib.UpdateUnitIDCache()
                end

                --this part is under development
                    if (Details) then
                        local detailsEventListener = Details:CreateEventListener()

                        function detailsEventListener:UnitSpecFound(event, unitId, specId, unitGuid)
                            local unitName = GetUnitName(unitId, true) or unitId
                            if (not UnitInParty(unitName) and not UnitInRaid(unitName)) then
                                return
                            end

                            --check if there's unit information about this unit


                            --is still did not received a list of cooldowns from this player
                            if (not openRaidLib.CooldownManager.HasFullCooldownList[unitName]) then
                                --build a generic list from the spec

                            end
                        end

                        function detailsEventListener:UnitTalentsFound(event, unitId, talentTable, unitGuid)
                            local unitName = GetUnitName(unitId, true) or unitId
                            if (not UnitInParty(unitName) and not UnitInRaid(unitName)) then
                                return
                            end

                        end

                        detailsEventListener:RegisterEvent("UNIT_SPEC", "UnitSpecFound")
                        detailsEventListener:RegisterEvent("UNIT_TALENTS", "UnitTalentsFound")
                    end

                openRaidLib.bHasEnteredWorld = true
            end

            openRaidLib.internalCallback.TriggerEvent("onEnterWorld")
        end,

        ["PLAYER_SPECIALIZATION_CHANGED"] = function(...)
            delayedTalentChange()
        end,
        ["PLAYER_TALENT_UPDATE"] = function(...)
            delayedTalentChange()
        end,
        ["TRAIT_CONFIG_UPDATED"] = function(...)
            delayedTalentChange()
        end,
        ["TRAIT_TREE_CURRENCY_INFO_UPDATED"] = function(...)
            --delayedTalentChange()
        end,

        --SPELLS_CHANGED

        ["PLAYER_PVP_TALENT_UPDATE"] = function(...)
            openRaidLib.internalCallback.TriggerEvent("pvpTalentUpdate")
        end,

        ["PLAYER_DEAD"] = function(...)
            openRaidLib.mainControl.UpdatePlayerAliveStatus()
        end,
        ["PLAYER_ALIVE"] = function(...)
            openRaidLib.mainControl.UpdatePlayerAliveStatus()
        end,
        ["PLAYER_UNGHOST"] = function(...)
            openRaidLib.mainControl.UpdatePlayerAliveStatus()
        end,

        ["PLAYER_REGEN_DISABLED"] = function(...)
            --entered in combat
        end,

        ["PLAYER_REGEN_ENABLED"] = function(...)
            openRaidLib.internalCallback.TriggerEvent("onLeaveCombat")
        end,

        ["UPDATE_INVENTORY_DURABILITY"] = function(...)
            --an item has changed its durability
            --do not trigger this event  while in combat
            if (not InCombatLockdown()) then
                openRaidLib.Schedules.NewUniqueTimer(5 + math.random(0, 4), openRaidLib.GearManager.SendDurability, "GearManager", "sendDurability_Schedule")
            end
        end,

        ["PLAYER_EQUIPMENT_CHANGED"] = function(...)
            --player changed an equipment
            openRaidLib.Schedules.NewUniqueTimer(4 + math.random(0, 5), openRaidLib.GearManager.SendAllGearInfo, "GearManager", "sendAllGearInfo_Schedule")
        end,

        ["ENCOUNTER_START"] = function()
            if (IsInRaid() or IsInGroup()) then
                openRaidLib.UpdateUnitIDCache()
                openRaidLib.internalCallback.TriggerEvent("raidEncounterStart")
            end
        end,

        ["ENCOUNTER_END"] = function()
            if (IsInRaid()) then
                openRaidLib.internalCallback.TriggerEvent("raidEncounterEnd")
            end
        end,

        ["CHALLENGE_MODE_START"] = function()
            openRaidLib.internalCallback.TriggerEvent("mythicDungeonStart")
        end,

        ["UNIT_PET"] = function(unitId)
            if (UnitIsUnit(unitId, "player")) then
                openRaidLib.Schedules.NewUniqueTimer(1.1, function() openRaidLib.internalCallback.TriggerEvent("playerPetChange") end, "mainControl", "petStatus_Schedule")
                --if the pet is alive, register to know when it dies
                if (UnitExists("pet") and UnitHealth("pet") >= 1) then
                    eventFrame:RegisterUnitEvent("UNIT_FLAGS", "pet")
                end
            end
        end,

        ["UNIT_FLAGS"] = function(unitId)
            local petHealth = UnitHealth(unitId)
            if (petHealth < 1) then
                eventFrame:UnregisterEvent("UNIT_FLAGS")
                openRaidLib.eventFunctions["UNIT_PET"]("player")
            end
        end,

        ["CHALLENGE_MODE_COMPLETED"] = function()
            openRaidLib.internalCallback.TriggerEvent("mythicDungeonEnd")
        end,

        ["PLAYER_LOGOUT"] = function()
            tempCache.SaveData()
        end,
    }
    openRaidLib.eventFunctions = eventFunctions

    eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")

    eventFrame:SetScript("OnEvent", function(self, event, ...)
        local eventCallbackFunc = eventFunctions[event]
        eventCallbackFunc(...)
    end)

    --run when PLAYER_ENTERING_WORLD triggers, this avoid any attempt of getting information without the game has completed the load process
    function openRaidLib.OnEnterWorldRegisterEvents()
        eventFrame:RegisterEvent("GROUP_ROSTER_UPDATE")
        eventFrame:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", "player", "pet")
        eventFrame:RegisterEvent("PLAYER_REGEN_DISABLED")
        eventFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
        eventFrame:RegisterEvent("UPDATE_INVENTORY_DURABILITY")
        eventFrame:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
        eventFrame:RegisterEvent("UNIT_PET")
        eventFrame:RegisterEvent("PLAYER_DEAD")
        eventFrame:RegisterEvent("PLAYER_ALIVE")
        eventFrame:RegisterEvent("PLAYER_UNGHOST")
        eventFrame:RegisterEvent("PLAYER_LOGOUT")

        if (checkClientVersion("retail")) then
            eventFrame:RegisterEvent("PLAYER_TALENT_UPDATE")
            eventFrame:RegisterEvent("PLAYER_PVP_TALENT_UPDATE")
            eventFrame:RegisterEvent("ENCOUNTER_END")
            eventFrame:RegisterEvent("CHALLENGE_MODE_START")
            eventFrame:RegisterEvent("CHALLENGE_MODE_COMPLETED")
            eventFrame:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED")
            eventFrame:RegisterEvent("TRAIT_TREE_CURRENCY_INFO_UPDATED")
            eventFrame:RegisterEvent("TRAIT_CONFIG_UPDATED")
            eventFrame:RegisterEvent("ENCOUNTER_START")
        end
    end


--------------------------------------------------------------------------------------------------------------------------------
--~main ~control

    openRaidLib.mainControl = {
        playerAliveStatus = {},
    }

    --send full data (all data available)
    function openRaidLib.mainControl.SendFullData()
        --send player data
        openRaidLib.UnitInfoManager.SendAllPlayerInfo()

        --send gear data
        openRaidLib.GearManager.SendAllGearInfo()

        --send cooldown data
        openRaidLib.CooldownManager.SendAllPlayerCooldowns()
    end

    openRaidLib.mainControl.onEnterWorld = function()
        --update the alive status of the player
        openRaidLib.mainControl.UpdatePlayerAliveStatus(true)

        --the game client is fully loadded and all information is available
        if (openRaidLib.IsInGroup()) then
            openRaidLib.Schedules.NewUniqueTimer(1.0, openRaidLib.mainControl.SendFullData, "mainControl", "sendFullData_Schedule")
        end
    end

    --update player data, even if not in group
    --called on every player_entering_world event
    openRaidLib.mainControl.UpdatePlayerData = function()
        local unitName = UnitName("player")
        --player data
            local playerFullInfo = openRaidLib.UnitInfoManager.GetPlayerFullInfo()
            openRaidLib.UnitInfoManager.AddUnitInfo(unitName, unpack(playerFullInfo)) --unpack: specId, talentsString, pvpTalentsTableUnpacked

        --gear info
            --C_Timer.After(2, function()
                local playerGearInfo = openRaidLib.GearManager.GetPlayerFullGearInfo()
                openRaidLib.GearManager.AddUnitGearList(unitName, unpack(playerGearInfo))
            --end)

        --cooldowns
            openRaidLib.CooldownManager.UpdatePlayerCooldownsLocally()
    end

    --this function runs on all Player Entering World, it is delayed due to covenant data many times aren't available after a cold login
    function openRaidLib.mainControl.scheduleUpdatePlayerData()
        openRaidLib.Schedules.NewUniqueTimer(1.0, openRaidLib.mainControl.UpdatePlayerData, "mainControl", "updatePlayerData_Schedule")
    end

    function openRaidLib.UpdatePlayer()
        return openRaidLib.mainControl.UpdatePlayerData()
    end

    openRaidLib.mainControl.OnEnterGroup = function()
        --the player entered in a group
        --schedule to send data
        openRaidLib.Schedules.NewUniqueTimer(1.0, openRaidLib.mainControl.SendFullData, "mainControl", "sendFullData_Schedule")
    end

    openRaidLib.mainControl.OnLeftGroup = function()
        --the player left a group
        --wipe group data (each module registers the OnLeftGroup)

        --cancel all schedules
        openRaidLib.Schedules.CancelAllUniqueTimers()

        --wipe alive status
        table.wipe(openRaidLib.mainControl.playerAliveStatus)

        --toggle off comms
    end

    openRaidLib.mainControl.OnPlayerDeath = function()
        local playerName = UnitName("player")
        openRaidLib.mainControl.playerAliveStatus[playerName] = false

        local dataToSend = "" .. CONST_COMM_PLAYER_DEAD_PREFIX
        openRaidLib.commHandler.SendCommData(dataToSend)
        diagnosticComm("OnPlayerDeath| " .. dataToSend) --debug

        openRaidLib.publicCallback.TriggerCallback("UnitDeath", "player")
    end

    openRaidLib.mainControl.OnPlayerRess = function()
        local playerName = UnitName("player")
        openRaidLib.mainControl.playerAliveStatus[playerName] = true

        local dataToSend = "" .. CONST_COMM_PLAYER_ALIVE_PREFIX
        openRaidLib.commHandler.SendCommData(dataToSend)
        diagnosticComm("OnPlayerRess| " .. dataToSend) --debug

        openRaidLib.publicCallback.TriggerCallback("UnitAlive", "player")
    end

    openRaidLib.internalCallback.RegisterCallback("onEnterWorld", openRaidLib.mainControl.onEnterWorld)
    openRaidLib.internalCallback.RegisterCallback("onEnterWorld", openRaidLib.mainControl.scheduleUpdatePlayerData)
    openRaidLib.internalCallback.RegisterCallback("onEnterGroup", openRaidLib.mainControl.OnEnterGroup)
    openRaidLib.internalCallback.RegisterCallback("onLeaveGroup", openRaidLib.mainControl.OnLeftGroup)
    openRaidLib.internalCallback.RegisterCallback("onPlayerDeath", openRaidLib.mainControl.OnPlayerDeath)
    openRaidLib.internalCallback.RegisterCallback("onPlayerRess", openRaidLib.mainControl.OnPlayerRess)

    --a player in the group died
    openRaidLib.commHandler.RegisterORComm(CONST_COMM_PLAYER_DEAD_PREFIX, function(data, unitName)
        openRaidLib.mainControl.playerAliveStatus[unitName] = false
        openRaidLib.publicCallback.TriggerCallback("UnitDeath", openRaidLib.GetUnitID(unitName))
    end)

    --a player in the group is now alive
    openRaidLib.commHandler.RegisterORComm(CONST_COMM_PLAYER_ALIVE_PREFIX, function(data, unitName)
        openRaidLib.mainControl.playerAliveStatus[unitName] = true
        openRaidLib.publicCallback.TriggerCallback("UnitAlive", openRaidLib.GetUnitID(unitName))
    end)


    function openRaidLib.mainControl.UpdatePlayerAliveStatus(onLogin)
        if (UnitIsDeadOrGhost("player")) then
            if (openRaidLib.playerAlive) then
                openRaidLib.playerAlive = false

                --trigger event if this isn't from login
                if (not onLogin) then
                    openRaidLib.internalCallback.TriggerEvent("onPlayerDeath")
                end
            end
        else
            if (not openRaidLib.playerAlive) then
                openRaidLib.playerAlive = true

                --trigger event if this isn't from login
                if (not onLogin) then
                    openRaidLib.internalCallback.TriggerEvent("onPlayerRess")
                end
            end
        end
    end


--------------------------------------------------------------------------------------------------------------------------------
--~all, request data from all players

    --send a request to all players in the group to send their data
    function openRaidLib.RequestAllData()
        --the the player isn't in group, don't send the request
		if (not IsInGroup()) then
			return
		end

        openRaidLib.requestAllInfoCooldown = openRaidLib.requestAllInfoCooldown or 0

        --check if the player can sent another request
        if (openRaidLib.requestAllInfoCooldown > GetTime()) then
            return
        end

        openRaidLib.commHandler.SendCommData(CONST_COMM_FULLINFO_PREFIX)
        diagnosticComm("RequestAllInfo| " .. CONST_COMM_FULLINFO_PREFIX) --debug

        openRaidLib.requestAllInfoCooldown = GetTime() + CONST_REQUEST_ALL_DATA_COOLDOWN
        return true
    end

    --this function handles the request from another player to send all data
    function openRaidLib.commHandler.SendFullData()
        openRaidLib.mainControl.SendFullData()
    end

    openRaidLib.commHandler.RegisterORComm(CONST_COMM_FULLINFO_PREFIX, function(data, sourceName)
        openRaidLib.sendRequestedAllInfoCooldown = openRaidLib.sendRequestedAllInfoCooldown or 0

        --check if there's some delay before sending the data
        if (openRaidLib.sendRequestedAllInfoCooldown > GetTime()) then
            --reschedule the function call
            openRaidLib.Schedules.NewUniqueTimer(openRaidLib.sendRequestedAllInfoCooldown - GetTime(), openRaidLib.commHandler.SendFullData, "CommHandler", "sendFullData_Schedule")
            return
        end

        openRaidLib.Schedules.NewUniqueTimer(math.random(1, 6), openRaidLib.commHandler.SendFullData, "CommHandler", "sendFullData_Schedule")

        --set the delay for the next request
        openRaidLib.sendRequestedAllInfoCooldown = GetTime() + CONST_SEND_ALL_DATA_COOLDOWN
    end)

--------------------------------------------------------------------------------------------------------------------------------
--~player general ~info ~unit ~unitinfo

    ---@class unitinfomanager : table
    ---@field Version number
    ---@field UnitData table<string, unitinfodata>
    ---@field GetAllUnitsInfo fun():table<string, unitinfodata>
    ---@field GetUnitInfo fun(unitId:string, createNew:boolean?):unitinfodata
    ---@field EraseData fun()
    ---@field SetUnitInfo fun(unitName:string, unitInfo:unitinfodata, specId:number, talentsString:string, pvpTalentsTableUnpacked:table)
    ---@field UpdateUnitInfo fun(unitName:string, specId:number?, talentsString:string?, pvpTalentsTableUnpacked:table?)
    ---@field AddUnitInfo fun(unitName:string,                        specId:number, talentsString:string, pvpTalentsTableUnpacked:table)
    ---@field OnReceiveUnitFullInfo fun(data:table, unitName:string)
    ---@field SendAllPlayerInfo fun()
    ---@field GetPlayerFullInfo fun():unitinfocomm
    ---@field GetPlayerPvPTalents fun():table<number, number, number>
    ---@field OnPlayerTalentChanged fun()
    ---@field OnReceivePvPTalentsUpdate fun(data:table, unitName:string)
    ---@field OnPlayerLeaveGroup fun()
    ---@field SendPlayerInfoAfterCombat fun()
    ---@field OnLeaveCombat fun()

    ---@class unitinfodata : table
    ---@field specId number
    ---@field specName string
    ---@field heroTalentId number
    ---@field role string
    ---@field talents string
    ---@field pvpTalents table<number, number, number>
    ---@field class string
    ---@field classId number
    ---@field className string
    ---@field name string
    ---@field nameFull string

    ---@class unitinfocomm : table
    ---@field [1] number specId
    ---@field [2] string talents
    ---@field [3] table<number, number, number> pvpTalents

    --API calls
        --return a table containing all information of units
        --format: [playerName-realm] = {information}
        function openRaidLib.GetAllUnitsInfo()
            return openRaidLib.UnitInfoManager.GetAllUnitsInfo()
        end

        --return a table containing information of a single unit
        function openRaidLib.GetUnitInfo(unitId)
            local unitName = GetUnitName(unitId, true) or unitId
            return openRaidLib.UnitInfoManager.GetUnitInfo(unitName)
        end

    --manager constructor
        ---@type unitinfomanager
        ---@diagnostic disable-next-line: missing-fields
        local UnitInfoManager = {}
        UnitInfoManager.UnitData = {}
        UnitInfoManager.Version = 1

        openRaidLib.UnitInfoManager = UnitInfoManager

        ---@type unitinfodata
        local unitTablePrototype = {
            specId = 0,
            specName = "",
            heroTalentId = 0,
            role = "",
            talents = "", --export string
            pvpTalents = {}, --should be 3 spellIds
            class = "",
            classId = 0,
            className = "",
            name = "",
            nameFull = "",
        }

    ---@return unitinfodata
    local createNewUnitInfo = function()
        ---@type unitinfodata
        ---@diagnostic disable-next-line: missing-fields
        local newUnitInfo = {}
        openRaidLib.TCopy(newUnitInfo, unitTablePrototype)
        return newUnitInfo
    end

    function UnitInfoManager.GetAllUnitsInfo()
        return UnitInfoManager.UnitData
    end

    --get the unit table or create a new one if 'createNew' is true
    function UnitInfoManager.GetUnitInfo(unitName, createNew)
        ---@type unitinfodata
        local unitInfo = UnitInfoManager.UnitData[unitName]
        if (not unitInfo and createNew) then
            unitInfo = createNewUnitInfo()
            UnitInfoManager.UnitData[unitName] = unitInfo
        end
        return unitInfo
    end

    function UnitInfoManager.EraseData()
        table.wipe(UnitInfoManager.UnitData)
    end

    ---@param unitName string
    ---@param unitInfo unitinfodata
    ---@param specId number
    ---@param talentsString string
    ---@param pvpTalentsTableUnpacked table<number, number, number>
    function UnitInfoManager.SetUnitInfo(unitName, unitInfo, specId, talentsString, pvpTalentsTableUnpacked)
        if (not GetSpecializationInfoByID) then --tbc hot fix
            return
        end

        local specId, specName, specDescription, specIcon, role, classFile, classLocName = GetSpecializationInfoByID(specId or 0)
        local className, classString, classId = UnitClass(unitName)

        --cold login bug where the player class info cannot be retrived by the player name, after a /reload it's all good
        if (not className) then
            local playerName = UnitName("player")
            if (playerName == unitName) then
                className, classString, classId = UnitClass("player")
            end
        end

        local talentString, heroTalent = openRaidLib.ParseTalentString(talentsString)

        unitInfo.specId = specId or unitInfo.specId
        unitInfo.specName = specName or unitInfo.specName
        unitInfo.heroTalentId = heroTalent or unitInfo.heroTalentId
        unitInfo.role = role or "DAMAGER"
        unitInfo.talents = talentString or ""
        unitInfo.pvpTalents = pvpTalentsTableUnpacked or unitInfo.pvpTalents
        unitInfo.class = classString
        unitInfo.classId = classId
        unitInfo.className = className
        unitInfo.name = unitName:gsub(("%-.*"), "")
        unitInfo.nameFull = unitName
    end

    function UnitInfoManager.AddUnitInfo(unitName, specId, talentsString, pvpTalentsTableUnpacked)
        local unitInfo = UnitInfoManager.GetUnitInfo(unitName, true) --returning nil
        UnitInfoManager.SetUnitInfo(unitName, unitInfo, specId, talentsString, pvpTalentsTableUnpacked)
        openRaidLib.publicCallback.TriggerCallback("UnitInfoUpdate", openRaidLib.GetUnitID(unitName), UnitInfoManager.UnitData[unitName], UnitInfoManager.GetAllUnitsInfo())
    end

    function UnitInfoManager.UpdateUnitInfo(playerName, specId, talentsString, pvpTalentsTableUnpacked)
        local unitInfo = UnitInfoManager.GetUnitInfo(playerName, true)
        UnitInfoManager.SetUnitInfo(playerName, unitInfo, specId or unitInfo.specId, talentsString or unitInfo.talents, pvpTalentsTableUnpacked or unitInfo.pvpTalents)
        openRaidLib.publicCallback.TriggerCallback("UnitInfoUpdate", openRaidLib.GetUnitID(playerName), UnitInfoManager.UnitData[playerName], UnitInfoManager.GetAllUnitsInfo())
    end

    function UnitInfoManager.OnReceiveUnitFullInfo(data, unitName)
        --triggered when the lib receives a unit information from another player in the raid
        local specId = tonumber(data[1])
        local talentsString = data[2]

        local unitInfoVersion = data[7]
        if (type(unitInfoVersion) ~= "string" or not unitInfoVersion:find("!")) then
            openRaidLib.DiagnosticError("UnitInfoManager.OnReceiveUnitFullInfo: invalid version data", unitInfoVersion)
            return
        end

        local versionNumber = tonumber(unitInfoVersion:match("!(%d+)"))
        if (not versionNumber or versionNumber < UnitInfoManager.Version) then
            openRaidLib.DiagnosticError("UnitInfoManager.OnReceiveUnitFullInfo: invalid version number", versionNumber, UnitInfoManager.Version)
            return
        end

        --unpack the pvp talents data as a ipairs table
        local pvpTalentsIndex = 3
        local pvpTalentsSize = 3
        local pvpTalentsTableUnpacked = openRaidLib.UnpackTable(data, pvpTalentsIndex, false, false, pvpTalentsSize)

        --add to the list of players information and also trigger a public callback
        UnitInfoManager.AddUnitInfo(unitName, specId, talentsString, pvpTalentsTableUnpacked)
    end
    openRaidLib.commHandler.RegisterORComm(CONST_COMM_PLAYERINFO_PREFIX, UnitInfoManager.OnReceiveUnitFullInfo)


function UnitInfoManager.SendAllPlayerInfo()
    local playerInfo = UnitInfoManager.GetPlayerFullInfo()
    local dataToSend = CONST_COMM_PLAYERINFO_PREFIX .. ","
    dataToSend = dataToSend .. playerInfo[1] .. "," --spec id
    dataToSend = dataToSend .. playerInfo[2] .. "," --talents string
    dataToSend = dataToSend .. openRaidLib.PackTable(playerInfo[3]) .. ",!1" --player talents pvp

    --send the data
    openRaidLib.commHandler.SendCommData(dataToSend)
    diagnosticComm("SendGetUnitInfoFullData| " .. dataToSend) --debug
end

--player info format:
--index 1: number: specId
--index 2: talents as string
--index 3: pvp talents as table
---@return unitinfocomm
function UnitInfoManager.GetPlayerFullInfo()
    local playerInfo = {}

    if (isTimewalkWoW()) then
        --indexes: specId, renown, covenant, talent, conduits, pvp talents
        --return a placeholder table
        return {0, "", {0, 0, 0}}
    end

    local specId = 0
    if (getSpecializationVersion() == CONST_SPECIALIZATION_VERSION_MODERN) then
        local selectedSpecialization = GetSpecialization()
        if (selectedSpecialization) then
            specId = GetSpecializationInfo(selectedSpecialization) or 0
        end
    end
    table.insert(playerInfo, specId)

    --player class-spec talents
    local talentsAsString = openRaidLib.GetDragonFlightTalentsAsString()
    table.insert(playerInfo, talentsAsString)

    --pvp talents
    local pvpTalents = UnitInfoManager.GetPlayerPvPTalents()
    table.insert(playerInfo, pvpTalents)

    return playerInfo
end

--/dump LibStub:GetLibrary("LibOpenRaid-1.0", true).UnitInfoManager.GetUnitInfo(UnitName("player"))

function UnitInfoManager.OnPlayerTalentChanged()
    --this talent update could be a specialization change, so we need to pass the specId as well
    local playerName = UnitName("player")
    local unitInfo = UnitInfoManager.GetUnitInfo(playerName, true)
    local specId = 0

    if (getSpecializationVersion() == CONST_SPECIALIZATION_VERSION_MODERN) then
        local selectedSpecialization = GetSpecialization()
        if (selectedSpecialization) then
            specId = GetSpecializationInfo(selectedSpecialization) or 0
        end
    end

    UnitInfoManager.SetUnitInfo(playerName, unitInfo, specId, openRaidLib.GetDragonFlightTalentsAsString(), UnitInfoManager.GetPlayerPvPTalents())

    --trigger public callback event
    openRaidLib.publicCallback.TriggerCallback("TalentUpdate", "player", unitInfo.talents, unitInfo, UnitInfoManager.GetAllUnitsInfo())

    --schedule send to the group
    openRaidLib.Schedules.NewUniqueTimer(2 + math.random(0, 1), UnitInfoManager.SendAllPlayerInfo, "UnitInfoManager", "sendAllPlayerInfo_Schedule")
end
openRaidLib.internalCallback.RegisterCallback("talentUpdate", UnitInfoManager.OnPlayerTalentChanged)

function UnitInfoManager.OnPlayerLeaveGroup()
    local unitName = UnitName("player")
    --clear the data
    UnitInfoManager.EraseData()

    --trigger a public callback
    openRaidLib.publicCallback.TriggerCallback("UnitInfoWipe", UnitInfoManager.UnitData)

    --need to build the player info again
    local playerFullInfo = UnitInfoManager.GetPlayerFullInfo()
    UnitInfoManager.AddUnitInfo(unitName, unpack(playerFullInfo))
end
openRaidLib.internalCallback.RegisterCallback("onLeaveGroup", UnitInfoManager.OnPlayerLeaveGroup)

--send data when leaving combat
function UnitInfoManager.SendPlayerInfoAfterCombat()
    openRaidLib.Schedules.NewUniqueTimer(2 + math.random(0, 1), UnitInfoManager.SendAllPlayerInfo, "UnitInfoManager", "sendAllPlayerInfo_Schedule")
end

function UnitInfoManager.OnLeaveCombat()
    openRaidLib.Schedules.NewUniqueTimer(1 + math.random(1, 4), UnitInfoManager.SendPlayerInfoAfterCombat, "UnitInfoManager", "leaveCombat_Schedule")
end
openRaidLib.internalCallback.RegisterCallback("onLeaveCombat", UnitInfoManager.OnLeaveCombat)


--------------------------------------------------------------------------------------------------------------------------------
--~equipment ~gear
    openRaidLib.GearManager = {
        --structure: [playerName] = {ilevel = 100, durability = 100, weaponEnchant = 0, noGems = {}, noEnchants = {}}
        UnitData = {},
    }

    local gearTablePrototype = {
        ilevel = 0,
        durability = 0,
        weaponEnchant = 0,
        noGems = {},
        noEnchants = {},
    }

    function openRaidLib.GetAllUnitsGear()
        return openRaidLib.GearManager.GetAllUnitsGear()
    end

    function openRaidLib.GetUnitGear(unitId, createNew)
        local unitName = GetUnitName(unitId, true) or unitId
        return openRaidLib.GearManager.GetUnitGear(unitName)
    end

    function openRaidLib.GearManager.GetAllUnitsGear()
        return openRaidLib.GearManager.UnitData
    end

    function openRaidLib.GearManager.GetUnitGear(unitName, createNew)
        local unitGearInfo = openRaidLib.GearManager.UnitData[unitName]
        if (not unitGearInfo and createNew) then
            unitGearInfo = {}
            openRaidLib.TCopy(unitGearInfo, gearTablePrototype)
            openRaidLib.GearManager.UnitData[unitName] = unitGearInfo
        end
        return unitGearInfo
    end

    --clear data stored
    function openRaidLib.GearManager.EraseData()
        table.wipe(openRaidLib.GearManager.UnitData)
    end

    function openRaidLib.GearManager.OnPlayerLeaveGroup()
        local unitName = GetUnitName("player")

        --clear the data
        openRaidLib.GearManager.EraseData()

        --trigger a public callback
        openRaidLib.publicCallback.TriggerCallback("GearListWipe", openRaidLib.GearManager.UnitData)

        --need to build the player gear again
        local playerGearInfo = openRaidLib.GearManager.GetPlayerFullGearInfo()
        openRaidLib.GearManager.AddUnitGearList(unitName, unpack(playerGearInfo))
    end
    openRaidLib.internalCallback.RegisterCallback("onLeaveGroup", openRaidLib.GearManager.OnPlayerLeaveGroup)

    --when the player is ressed while in a group, send the cooldown list
    function openRaidLib.GearManager.OnPlayerRess()
        --check if is in group
        if (openRaidLib.IsInGroup()) then
            openRaidLib.Schedules.NewUniqueTimer(1.0 + math.random(0.0, 6.0), openRaidLib.GearManager.SendDurability, "GearManager", "sendDurability_Schedule")
        end
    end
    openRaidLib.internalCallback.RegisterCallback("onPlayerRess", openRaidLib.GearManager.OnPlayerRess)

    --send data when leaving combat
    function openRaidLib.GearManager.SendGearInfoAfterCombat()
        openRaidLib.GearManager.SendAllGearInfo()
    end
    function openRaidLib.GearManager.OnLeaveCombat()
        openRaidLib.Schedules.NewUniqueTimer(1 + math.random(1, 4), openRaidLib.GearManager.SendGearInfoAfterCombat, "GearManager", "leaveCombat_Schedule")
    end
    openRaidLib.internalCallback.RegisterCallback("onLeaveCombat", openRaidLib.GearManager.OnLeaveCombat)

    --send only the gear durability
    function openRaidLib.GearManager.SendDurability()
        local dataToSend = "" .. CONST_COMM_GEARINFO_DURABILITY_PREFIX .. ","
        local averageGearDurability, lowestDurability = openRaidLib.GearManager.GetPlayerGearDurability()

        dataToSend = dataToSend .. averageGearDurability

        --send the data
        openRaidLib.commHandler.SendCommData(dataToSend)
        diagnosticComm("SendGearDurabilityData| " .. dataToSend) --debug
    end

    function openRaidLib.GearManager.OnReceiveGearDurability(data, unitName)
        local durability = tonumber(data[1])
        openRaidLib.GearManager.UpdateUnitGearDurability(unitName, durability)
    end
    openRaidLib.commHandler.RegisterORComm(CONST_COMM_GEARINFO_DURABILITY_PREFIX, openRaidLib.GearManager.OnReceiveGearDurability)

    --on receive the durability (sent when the player get a ress)
    function openRaidLib.GearManager.UpdateUnitGearDurability(unitName, durability)
        local unitGearInfo = openRaidLib.GearManager.GetUnitGear(unitName)
        if (unitGearInfo) then
            unitGearInfo.durability = durability
            openRaidLib.publicCallback.TriggerCallback("GearDurabilityUpdate", openRaidLib.GetUnitID(unitName), durability, unitGearInfo, openRaidLib.GearManager.GetAllUnitsGear())
        end
    end

    --get gear information from what the player has equipped at the moment
    function openRaidLib.GearManager.GetPlayerFullGearInfo()
        --get the player class and specId
        local _, playerClass = UnitClass("player")
        local specId = openRaidLib.GetPlayerSpecId()
        --get which attribute the spec uses
        local specMainAttribute = openRaidLib.specAttribute[playerClass][specId] --1 int, 2 dex, 3 str

        if (not specId or not specMainAttribute) then
            return {0, 0, 0, {}, {}, {}, 0, 0}
        end

        --item level
        local itemLevel = openRaidLib.GearManager.GetPlayerItemLevel()

        --repair status
        local gearDurability, lowestItemDurability = openRaidLib.GearManager.GetPlayerGearDurability()

        --get weapon enchant
        local weaponEnchant, mainHandEnchantId, offHandEnchantId = openRaidLib.GearManager.GetPlayerWeaponEnchant()

        --enchants and gems
        local slotsWithoutGems, slotsWithoutEnchant = openRaidLib.GearManager.GetPlayerGemsAndEnchantInfo()

        --full gear list {{slotId, gemAmount, itemLevel, itemLink}, {slotId, gemAmount, itemLevel, itemLink}, }
        local equippedGearList = openRaidLib.GearManager.BuildPlayerEquipmentList()

        --build the table with the gear information
        local playerGearInfo = {}
        playerGearInfo[#playerGearInfo+1] = itemLevel           --[1] - one index
        playerGearInfo[#playerGearInfo+1] = gearDurability      --[2] - one index
        playerGearInfo[#playerGearInfo+1] = weaponEnchant       --[3] - one index
        playerGearInfo[#playerGearInfo+1] = slotsWithoutEnchant --[4] - undefined
        playerGearInfo[#playerGearInfo+1] = slotsWithoutGems    --[5] - undefined
        playerGearInfo[#playerGearInfo+1] = equippedGearList    --[6] - undefined
        playerGearInfo[#playerGearInfo+1] = mainHandEnchantId   --[7]
        playerGearInfo[#playerGearInfo+1] = offHandEnchantId    --[8]

        return playerGearInfo
    end

    --when received the gear update from another player, store it and trigger a callback
    function openRaidLib.GearManager.AddUnitGearList(unitName, itemLevel, durability, weaponEnchant, noEnchantTable, noGemsTable, equippedGearList, mainHandEnchantId, offHandEnchantId)
        local unitGearInfo = openRaidLib.GearManager.GetUnitGear(unitName, true)

        unitGearInfo.ilevel = itemLevel
        unitGearInfo.durability = durability
        unitGearInfo.weaponEnchant = weaponEnchant
        unitGearInfo.noGems = noGemsTable
        unitGearInfo.noEnchants = noEnchantTable
        unitGearInfo.mainHandEnchantId = mainHandEnchantId
        unitGearInfo.offHandEnchantId = offHandEnchantId

        --parse and replace the 'equippedGearList'
        openRaidLib.GearManager.BuildEquipmentItemLinks(equippedGearList)

        unitGearInfo.equippedGear = equippedGearList

        local tierAmount = 0

        for i = 1, #equippedGearList do
            if (equippedGearList[i].isTier) then
                tierAmount = tierAmount + 1
            end
        end

        unitGearInfo.tierAmount = tierAmount

        openRaidLib.publicCallback.TriggerCallback("GearUpdate", openRaidLib.GetUnitID(unitName), unitGearInfo, openRaidLib.GearManager.GetAllUnitsGear())
    end

    --triggered when the lib receives a gear information from another player in the raid
    --@data: table received from comm
    --@unitName: player name
    function openRaidLib.GearManager.OnReceiveGearFullInfo(data, unitName)
        local itemLevel = tonumber(data[1]) --1 index
        local durability = tonumber(data[2]) --1 index
        local weaponEnchant = tonumber(data[3]) --1 index

        local noEnchantTableSize = tonumber(data[4])
        local noGemsTableIndex = tonumber(noEnchantTableSize + 5) --5 is the three first indexes, the enchant table size and +1 to jump to next index
        local noGemsTableSize = data[noGemsTableIndex]

        local equippedGearListIndex = tonumber(noEnchantTableSize + noGemsTableSize + 6) --6 is the same has the 5 but +1 index for the gems table size

        local equippedGearListSize = data[equippedGearListIndex]

        local mainHandEnchantId, offHandEnchantId = 0, 0
        if equippedGearListSize then
            local mainHandEnchantIdIndex = tonumber(noEnchantTableSize + noGemsTableSize + equippedGearListSize + 7)
            mainHandEnchantId = tonumber(data[mainHandEnchantIdIndex]) or 0
            local offHandEnchantIdIndex = tonumber(mainHandEnchantIdIndex + 1)
            offHandEnchantId = tonumber(data[offHandEnchantIdIndex]) or 0
        end

        --unpack the enchant data as a ipairs table
        local noEnchantTableUnpacked = openRaidLib.UnpackTable(data, 4, false, false, noEnchantTableSize)
        --unpack the gems data as a ipairs table
        local noGemsTableUnpacked = openRaidLib.UnpackTable(data, noGemsTableIndex, false, false, noGemsTableSize)
        --unpack the full gear
        local equippedGearListUnpacked = equippedGearListIndex and openRaidLib.UnpackTable(data, equippedGearListIndex, false, true, 4) or {}

        --add to the list of gear information
        openRaidLib.GearManager.AddUnitGearList(unitName, itemLevel, durability, weaponEnchant, noEnchantTableUnpacked, noGemsTableUnpacked, equippedGearListUnpacked, mainHandEnchantId, offHandEnchantId)
    end
    openRaidLib.commHandler.RegisterORComm(CONST_COMM_GEARINFO_FULL_PREFIX, openRaidLib.GearManager.OnReceiveGearFullInfo)

    --todo: on changing an item in the inventory, send an update only for the slot that got changed

    function openRaidLib.GearManager.SendAllGearInfo()
        --get gear information, gear info has 6 indexes:
        --[1] int item level
        --[2] int durability
        --[3] int weapon enchant
        --[4] table with integers of equipSlot without enchant
        --[5] table with integers of equipSlot which has a gem slot but the slot is empty
        --[6] table with all gear from the player
        --[7] int mainHandEnchantId
        --[8] int offHandEnchantId

        local dataToSend = "" .. CONST_COMM_GEARINFO_FULL_PREFIX .. ","
        local playerGearInfo = openRaidLib.GearManager.GetPlayerFullGearInfo()

        --update the player table
        openRaidLib.GearManager.AddUnitGearList(UnitName("player"), unpack(playerGearInfo))

        dataToSend = dataToSend .. playerGearInfo[1] .. "," --item level
        dataToSend = dataToSend .. playerGearInfo[2] .. "," --durability
        dataToSend = dataToSend .. playerGearInfo[3] .. "," --weapon enchant
        dataToSend = dataToSend .. openRaidLib.PackTable(playerGearInfo[4]) .. "," --slots without enchant
        dataToSend = dataToSend .. openRaidLib.PackTable(playerGearInfo[5]) .. "," -- slots with empty gem sockets
        dataToSend = dataToSend .. openRaidLib.PackTableAndSubTables(playerGearInfo[6]) .. "," --full equipped equipment
        dataToSend = dataToSend .. playerGearInfo[7] .. "," --main hand weapon enchant
        dataToSend = dataToSend .. playerGearInfo[8] --off hand weapon enchant

        --send the data
        openRaidLib.commHandler.SendCommData(dataToSend)
        diagnosticComm("SendGearFullData| " .. dataToSend) --debug
    end


--------------------------------------------------------------------------------------------------------------------------------
--~open ~notes ~opennotes

---type and prototype for the note system, when adding or removeing fields, this is the only place to change

---@class noteinfo : table
---@field note string
---@field commId string

---@type noteinfo
local notePrototype = {
    note = "",
    commId = "",
}

openRaidLib.OpenNotesManager = {
    --structure: [playerName] = {note = "note text", lastUpdate = 0}
    ---@type table<string, noteinfo>
    UnitData = {},
}

--the note context saves the context of when the note was last sent, this is to avoid the player sending a note used on other dungeon or group when the a note is request
local noteContext = {
    mapId = 0,
    difficultyId = 0,
    instanceType = "none",
    ---@type table<string, boolean>
    groupMembers = {},
    time = 0,
}

local checkContext = function()
    if (noteContext.time == 0) then
        return false
    end

    local name, instanceType, difficultyID, difficultyName, maxPlayers, dynamicDifficulty, isDynamic, instanceID, instanceGroupSize, LfgDungeonID = GetInstanceInfo()

    if (noteContext.mapId ~= instanceID or noteContext.difficultyId ~= difficultyID or noteContext.instanceType ~= instanceType) then
        return false
    end

    --if the note context time is more than 25 minutes ago, ignore
    if (time() - noteContext.time > 1500) then
        return false
    end

    --check if the group members are the same
    local groupMembers = openRaidLib.GetPlayersInTheGroup()
    for unitName in pairs(noteContext.groupMembers) do
        if (not groupMembers[unitName]) then
            return false
        end
    end

    return true
end

--API notes
---return the table where the notes are stored, format: [playerName] = {note = "note text", lastUpdate = time()}
---can return an empty table if no unit sent note yet
---@return table<string, noteinfo>
function openRaidLib.GetAllUnitsNotes()
    return openRaidLib.OpenNotesManager.GetAllUnitsNotes()
end

---return information about a note for a unit, return value is a table of type noteinfo, see the type declaration to know the fields
---always return values, if the note does not exist it'll return an empty string and 0
---@param unitId string
---@return noteinfo
function openRaidLib.GetUnitNote(unitId)
    ---@type string
    local unitName = GetUnitName(unitId, true) or unitId
    ---@type noteinfo
    local noteInfo = openRaidLib.OpenNotesManager.GetUnitNote(unitName)
    return noteInfo
end

---set a note for the player
---@param note string
function openRaidLib.SetPlayerNote(note)
    assert(type(note) == "string", "OpenRaid: SetPlayerNote(#1) expect a string.")
    assert(note:len() <= 1500, "OpenRaid: SetPlayerNote(#1) too long.")
    openRaidLib.OpenNotesManager.SetUnitNote(UnitName("player"), note, "")
end

---send the player note to the group
function openRaidLib.SendPlayerNote()
    openRaidLib.OpenNotesManager.SendNote()
end



--INTERNAL notes
function openRaidLib.OpenNotesManager.GetAllUnitsNotes()
    return openRaidLib.OpenNotesManager.UnitData
end

---get a unit note, if it does not exist, create a new one
---@param unitName string
---@return noteinfo
function openRaidLib.OpenNotesManager.GetUnitNote(unitName)
    local unitNote = openRaidLib.OpenNotesManager.UnitData[unitName]

    if (not unitNote) then
        local newNote = {}
        openRaidLib.TCopy(newNote, notePrototype)
        openRaidLib.OpenNotesManager.UnitData[unitName] = newNote
        unitNote = newNote
    end

    return unitNote
end

---set a note of a unit, this do not send the note yet, just store it
---@param unitName string
---@param note string
---@param commId string
function openRaidLib.OpenNotesManager.SetUnitNote(unitName, note, commId)
    local unitNote = openRaidLib.OpenNotesManager.GetUnitNote(unitName)
    unitNote.note = note
    unitNote.commId = commId
end

---clear all data stored
function openRaidLib.OpenNotesManager.EraseData()
    table.wipe(openRaidLib.OpenNotesManager.UnitData)

    --create a note for the local player
    local playerName = UnitName("player")
    openRaidLib.OpenNotesManager.GetUnitNote(playerName)
end

---clear all data except the local player
function openRaidLib.OpenNotesManager.EraseDataKeepPlayer()
    local playerName = UnitName("player")
    local localNote = openRaidLib.OpenNotesManager.UnitData[playerName]
    table.wipe(openRaidLib.OpenNotesManager.UnitData)
    openRaidLib.OpenNotesManager.UnitData[playerName] = localNote
end

function openRaidLib.OpenNotesManager.OnPlayerEnterWorld()
    --call erase data hence create a note for the local player
    openRaidLib.OpenNotesManager.EraseData()
end
openRaidLib.internalCallback.RegisterCallback("onEnterWorld", openRaidLib.OpenNotesManager.OnPlayerEnterWorld)

function openRaidLib.OpenNotesManager.OnReceiveNoteData(data, unitName)
    ---@type string
    local note = data[1]
    local commId = data[2]

    if (note and type(note) == "string" and commId and type(commId) == "string") then
        openRaidLib.OpenNotesManager.SetUnitNote(unitName, note, commId)
        ---@type noteinfo
        local unitNote = openRaidLib.OpenNotesManager.GetUnitNote(unitName)
        --trigger public callback
        openRaidLib.publicCallback.TriggerCallback("NoteUpdated", openRaidLib.GetUnitID(unitName), unitNote, openRaidLib.OpenNotesManager.GetAllUnitsNotes())
    end
end
openRaidLib.commHandler.RegisterORComm(CONST_COMM_OPENNOTES_RECEIVED_PREFIX, openRaidLib.OpenNotesManager.OnReceiveNoteData)

local timeOfLastNoteSent = 0
function openRaidLib.OpenNotesManager.SendNote()
    local name, instanceType, difficultyID, difficultyName, maxPlayers, dynamicDifficulty, isDynamic, instanceID, instanceGroupSize, LfgDungeonID = GetInstanceInfo()

    --deny if not in group or if the player is in open world
    if (instanceType == "none") then
        --return
    elseif (not openRaidLib.IsInGroup()) then
        return
    end
--ACTIVE_DELVE_DATA_UPDATE
    ---@type noteinfo
    local playerNote = openRaidLib.OpenNotesManager.GetUnitNote(UnitName("player"))
    if (type(playerNote) == "table" and playerNote.note) then
        assert(type(playerNote.note) == "string", "OpenRaid: SendNote() invalid note.")
        assert(playerNote.note:len() <= 1500, "OpenRaid: SendNote() note too long.")
        assert(playerNote.note:len() >= 50, "OpenRaid: SendNote() note too short.")

        local dataToSend = "" .. CONST_COMM_OPENNOTES_RECEIVED_PREFIX .. "," .. playerNote.note

        local sendFunc = function()
            local flags = nil
            local bIsSafe = true
            openRaidLib.commHandler.SendCommData(dataToSend, flags, bIsSafe)
            diagnosticComm("SendAllNotesData| " .. dataToSend) --debug
        end

        if (timeOfLastNoteSent+5 > time()) then
            openRaidLib.Schedules.NewUniqueTimer(2 + math.random(0, 2) + math.random(), sendFunc, "OpenNotesManager", "sendNoteInfo_Schedule")
        else
            openRaidLib.Schedules.NewUniqueTimer(0.1, sendFunc, "OpenNotesManager", "sendNoteInfo_Schedule")
        end

        timeOfLastNoteSent = time()

        noteContext.time = time()
        noteContext.mapId = instanceID
        noteContext.difficultyId = difficultyID
        noteContext.instanceType = instanceType
        noteContext.groupMembers = openRaidLib.GetPlayersInTheGroup()
    end
end

function openRaidLib.OpenNotesManager.OnReceiveNoteRequest()
    ---@type noteinfo
    local playerNote = openRaidLib.OpenNotesManager.GetUnitNote(UnitName("player"))

    --check if there is text in the note
    if (playerNote and playerNote.note and playerNote.note:len() >= 50) then
        --check if the context is the same
        if (not checkContext()) then
            return
        end
        openRaidLib.Schedules.NewUniqueTimer(2 + math.random(0, 2) + math.random(), openRaidLib.OpenNotesManager.SendNote, "OpenNotesManager", "sendNoteInfo_Schedule")
    end
end
openRaidLib.commHandler.RegisterORComm(CONST_COMM_OPENNOTES_REQUESTED_PREFIX, openRaidLib.OpenNotesManager.OnReceiveNoteRequest)

--------------------------------------------------------------------------------------------------------------------------------
--~cooldowns ~cooldown
openRaidLib.CooldownManager = {
    UnitData = {}, --stores the list of cooldowns each player has sent
    UnitDataFilterCache = {}, --same as the table above but cooldowns are separated has offensive, defensive, etc. FilterCooldowns in functions.lua
    NeedRebuildFilters = {}, --mark people that has invalid filter cache and need to rebuild it
    CooldownTickers = {}, --store C_Timer.NewTicker
    HasFullCooldownList = {}, --store player names with the library
}

--check if a cooldown time has changed or finished
--this function run within a ticker, the internal is CONST_COOLDOWN_CHECK_INTERVAL
local cooldownTimeLeftCheck_Ticker = function(tickerObject)
    local spellId = tickerObject.spellId

    --if the spell does not exists anymore in the player table, cancel the ticker
    local playerName = UnitName("player")
    if (not openRaidLib.CooldownManager.UnitData[playerName][spellId]) then
        tickerObject:Cancel()
        return
    end

    tickerObject.cooldownTimeLeft = tickerObject.cooldownTimeLeft - tickerObject.tickInterval
    local timeLeft, charges, startTimeOffset, duration, auraDuration = openRaidLib.CooldownManager.GetPlayerCooldownStatus(spellId)

    local bUpdateLocally = false

    --is the spell ready to use?, 0.5 seconds for latency compensation
    if (timeLeft <= tickerObject.latencyCompensation) then
        --it's ready
        openRaidLib.CooldownManager.SendPlayerCooldownUpdate(spellId, 0, charges, 0, 0, 0)
        openRaidLib.CooldownManager.CooldownTickers[spellId] = nil
        tickerObject:Cancel()
        bUpdateLocally = true
    else
        --check if the time left has changed, this check if the cooldown got its time reduced and if the cooldown time has been slow down by modRate
        if (not openRaidLib.isNearlyEqual(tickerObject.cooldownTimeLeft, timeLeft, CONST_COOLDOWN_TIMELEFT_HAS_CHANGED)) then
            --there's a deviation, send a comm to communicate the change in the time left
            openRaidLib.CooldownManager.SendPlayerCooldownUpdate(spellId, timeLeft, charges, startTimeOffset, duration, auraDuration)
            tickerObject.cooldownTimeLeft = timeLeft
            bUpdateLocally = true
        end
    end

    if (bUpdateLocally) then
        --get the cooldown time for this spell
        local timeLeft, charges, startTimeOffset, duration, auraDuration = openRaidLib.CooldownManager.GetPlayerCooldownStatus(spellId) --return 5 values
        --update the cooldown
        openRaidLib.CooldownManager.CooldownSpellUpdate(playerName, spellId, timeLeft, charges, startTimeOffset, duration, auraDuration) --need 7 values

        local playerCooldownTable = openRaidLib.GetUnitCooldowns(playerName)
        local cooldownInfo = openRaidLib.GetUnitCooldownInfo(playerName, spellId)
        openRaidLib.publicCallback.TriggerCallback("CooldownUpdate", "player", spellId, cooldownInfo, playerCooldownTable, openRaidLib.CooldownManager.UnitData)
    end
end

--after a spell is casted by the player, start a ticker to check its cooldown
local cooldownStartTicker = function(spellId, cooldownTimeLeft)
    local existingTicker = openRaidLib.CooldownManager.CooldownTickers[spellId]
    if (existingTicker) then
        --if a ticker already exists, might be the cooldown of a charge
        --if the ticker isn't about to expire, just keep the timer
        --when the ticker finishes it'll check again for charges
        if (existingTicker.startTime + existingTicker.cooldownTimeLeft - GetTime() > 2) then
            return
        end

        --cancel the existing ticker
        if (not existingTicker:IsCancelled()) then
            existingTicker:Cancel()
        end
    end

    local tickInterval = CONST_COOLDOWN_CHECK_INTERVAL
    local maxTicks = ceil(cooldownTimeLeft / tickInterval)
    local latencyCompensation = 0

    local cooldownOptions = LIB_OPEN_RAID_COOLDOWNS_CONFIG and LIB_OPEN_RAID_COOLDOWNS_CONFIG[spellId]
    if (cooldownOptions) then
        if (cooldownOptions.tickInterval) then
            tickInterval = cooldownOptions.tickInterval
            maxTicks = ceil(cooldownTimeLeft / tickInterval)
        end
        if (cooldownOptions.latencyCompensation) then
            latencyCompensation = cooldownOptions.latencyCompensation
        end
    end

    --create a new ticker
    local newTicker = C_Timer.NewTicker(tickInterval, cooldownTimeLeftCheck_Ticker, maxTicks)

    --store the ticker
    openRaidLib.CooldownManager.CooldownTickers[spellId] = newTicker
    newTicker.tickInterval = tickInterval
    newTicker.latencyCompensation = latencyCompensation
    newTicker.spellId = spellId
    newTicker.cooldownTimeLeft = cooldownTimeLeft
    newTicker.startTime = GetTime()
    newTicker.endTime = GetTime() + cooldownTimeLeft
end

function openRaidLib.CooldownManager.CleanupCooldownTickers()
    for spellId, tickerObject in pairs(openRaidLib.CooldownManager.CooldownTickers) do
        local timeLeft, charges, startTimeOffset, duration, auraDuration = openRaidLib.CooldownManager.GetPlayerCooldownStatus(spellId)
        if (timeLeft == 0) then
            tickerObject:Cancel()
            openRaidLib.CooldownManager.CooldownTickers[spellId] = nil
        end
    end
end

local cooldownGetUnitTable = function(unitName, shouldWipe)
    local unitCooldownTable = openRaidLib.CooldownManager.UnitData[unitName]
    --check if the unit has a cooldownTable
    if (not unitCooldownTable) then
        unitCooldownTable = {}
        openRaidLib.CooldownManager.UnitData[unitName] = unitCooldownTable
    else
        --as the unit could have changed a talent or spec, wipe the table before using it
        if (shouldWipe) then
            table.wipe(unitCooldownTable)
        end
    end

    return unitCooldownTable
end

local cooldownGetSpellInfo = function(unitName, spellId)
    local unitCooldownTable = cooldownGetUnitTable(unitName)
    local cooldownInfo = unitCooldownTable[spellId]
    return cooldownInfo
end

--update a single cooldown timer
--called when the player casted a cooldown and when received a cooldown update from another player
--only update the db, no other action is taken
--cooldownInfo: [1] timeLeft [2] charges [3] startOffset [4] duration [5] updateTime [6] auraDuration
function openRaidLib.CooldownManager.CooldownSpellUpdate(unitName, spellId, newTimeLeft, newCharges, startTimeOffset, duration, auraDuration)
    --get the cooldown table where all cooldowns are stored for this unit
    local unitCooldownTable = cooldownGetUnitTable(unitName)
    --is this a cooldown info?
    local cooldownInfo = unitCooldownTable[spellId] or {}
    cooldownInfo[CONST_COOLDOWN_INDEX_TIMELEFT] = newTimeLeft
    cooldownInfo[CONST_COOLDOWN_INDEX_CHARGES] = newCharges
    cooldownInfo[CONST_COOLDOWN_INDEX_TIMEOFFSET] = startTimeOffset
    cooldownInfo[CONST_COOLDOWN_INDEX_DURATION] = duration
    cooldownInfo[CONST_COOLDOWN_INDEX_UPDATETIME] = GetTime()
    cooldownInfo[CONST_COOLDOWN_INDEX_AURA_DURATION] = auraDuration
    unitCooldownTable[spellId] = cooldownInfo
end

--API Calls
    --return a table with unit names as key and a table with unit cooldowns as the value
    --table format: [playerName] = {[spellId] = cooldownInfo}
    function openRaidLib.GetAllUnitsCooldown()
        return openRaidLib.CooldownManager.UnitData
    end

    --return a table with all the unit cooldowns
    --table format: [spellId] = cooldownInfo
    function openRaidLib.GetUnitCooldowns(unitId, filter)
        local unitName = GetUnitName(unitId, true) or unitId
        local allCooldowns = openRaidLib.CooldownManager.UnitData[unitName]

        --check if there's a filter and if there's at least one cooldown existing
        if (allCooldowns and next(allCooldowns)) then
            if (filter and filter ~= "") then
                if (type(filter) == "string") then
                    local filterCooldowns = openRaidLib.FilterCooldowns(unitName, allCooldowns, filter)
                    return filterCooldowns
                else
                    openRaidLib.DiagnosticError("CooldownManager|GetUnitCooldowns|filter isn't a string")
                end
            else
                return allCooldowns
            end
        else
            return {}
        end
    end

    function openRaidLib.DoesSpellPassFilters(spellId, filter)
        return openRaidLib.CooldownManager.DoesSpellPassFilters(spellId, filter)
    end

    function openRaidLib.GetSpellFilters(spellId, defaultFilterOnly, customFiltersOnly)
        return openRaidLib.CooldownManager.GetSpellFilters(spellId, defaultFilterOnly, customFiltersOnly)
    end

    --return values about the cooldown time
    --values returned: timeLeft, charges, timeOffset, duration, updateTime
    function openRaidLib.GetCooldownTimeFromUnitSpellID(unitId, spellId)
        local unitCooldownsTable = openRaidLib.GetUnitCooldowns(unitId)
        if (unitCooldownsTable) then
            local cooldownInfo = unitCooldownsTable[spellId]
            if (cooldownInfo) then
                return openRaidLib.CooldownManager.GetCooldownInfoValues(cooldownInfo)
            end
        end
    end

    --return values about the cooldown time from a cooldown info
    --values returned: timeLeft, charges, timeOffset, duration, updateTime
    function openRaidLib.GetCooldownTimeFromCooldownInfo(cooldownInfo)
        if (cooldownInfo) then
            return openRaidLib.CooldownManager.GetCooldownInfoValues(cooldownInfo)
        end
    end

    --return a table containing values about the cooldown time
    --values returned: {timeLeft, charges, timeOffset, duration, updateTime, auraDuration}
    function openRaidLib.GetUnitCooldownInfo(unitId, spellId)
        local unitCooldownsTable = openRaidLib.GetUnitCooldowns(unitId)
        if (unitCooldownsTable) then
            local cooldownInfo = unitCooldownsTable[spellId]
            return cooldownInfo
        end
    end

    local calculatePercent = function(timeOffset, duration, updateTime, charges)
        timeOffset = abs(timeOffset)
        local minValue = updateTime - timeOffset
        local maxValue = minValue + duration
        local currentValue = GetTime()
        local percent = openRaidLib.GetRangePercent(minValue, maxValue, currentValue)
        percent = min(percent, 1)
        local timeLeft = max(maxValue - currentValue, 0)

        --lag compensation
        if (timeLeft <= 2) then
            timeLeft = 0
            if (charges == 0) then
                charges = 1
            end
            minValue = currentValue
            maxValue = 1
            currentValue = 1
        end

        local bIsReady = timeLeft <= 2
        return bIsReady, percent, timeLeft, charges, minValue, maxValue, min(currentValue, maxValue), duration
    end

    --return the values to be use on a progress bar or cooldown frame
    --require a unitId and a spellId to query the values
    --values returned: isReady, timeLeft, charges, normalized percent, minValue, maxValue, currentValue
    --values are in the GetTime() format
    function openRaidLib.GetCooldownStatusFromUnitSpellID(unitId, spellId)
        local timeLeft, charges, timeOffset, duration, updateTime, auraDuration
        local unitCooldownsTable = openRaidLib.GetUnitCooldowns(unitId)
        if (unitCooldownsTable) then
            local cooldownInfo = unitCooldownsTable[spellId]
            if (cooldownInfo) then
                timeLeft, charges, timeOffset, duration, updateTime, auraDuration = openRaidLib.CooldownManager.GetCooldownInfoValues(cooldownInfo)
            end
        end

        return calculatePercent(timeOffset, duration, updateTime, charges)
    end

    ---return the values to be use on a progress bar or cooldown frame
    ---values returned: bIsReady, percent, timeLeft, charges, minValue, maxValue, currentValue, duration
    ---@param cooldownInfo table
    ---@return boolean bIsReady
    ---@return number percent
    ---@return number timeLeft
    ---@return number charges
    ---@return number minValue
    ---@return number maxValue
    ---@return number currentValue
    ---@return number duration
    function openRaidLib.GetCooldownStatusFromCooldownInfo(cooldownInfo)
        local timeLeft, charges, timeOffset, duration, updateTime, auraDuration = openRaidLib.CooldownManager.GetCooldownInfoValues(cooldownInfo)
        if (not timeOffset) then
            return false, 0, 0, 0, 0, 0, 0, 0
        end
        return calculatePercent(timeOffset, duration, updateTime, charges)
    end

--internals
    function openRaidLib.CooldownManager.GetCooldownInfoValues(cooldownInfo)
        local timeLeft, charges, timeOffset, duration, updateTime, auraDuration = unpack(cooldownInfo)
        return timeLeft, charges, timeOffset, duration, updateTime, auraDuration
    end

    function openRaidLib.CooldownManager.OnPlayerCast(event, spellId, isPlayerPet) --~cast
        --player casted a spell, check if the spell is registered as cooldown
        --issue: pet spells isn't in this table yet, might mess with pet interrupts
        if (LIB_OPEN_RAID_PLAYERCOOLDOWNS[spellId]) then --check if the casted spell is a cooldown the player has available
            local playerName = UnitName("player")

            --get the cooldown time for this spell
            local timeLeft, charges, startTimeOffset, duration, auraDuration = openRaidLib.CooldownManager.GetPlayerCooldownStatus(spellId) --return 5 values

            --check for shared cooldown time - warning: this block of code is duplicated at "openRaidLib.commHandler.RegisterORComm(CONST_COMM_COOLDOWNUPDATE_PREFIX"
            local spellData = LIB_OPEN_RAID_COOLDOWNS_INFO[spellId]
            local sharedCooldownId = spellData and spellData.shareid
            if (sharedCooldownId) then
                local spellsWithSharedCooldown = LIB_OPEN_RAID_COOLDOWNS_SHARED_ID[sharedCooldownId]
                for thisSpellId in pairs(spellsWithSharedCooldown) do
                    --don't run for the spell that triggered the shared cooldown
                    if (thisSpellId ~= spellId) then
                        --before triggering the cooldown, check if the player has the spell
                        if (cooldownGetSpellInfo(playerName, thisSpellId)) then --won't have BOP because it's not talented
                            local spellInfo = C_Spell.GetSpellInfo(thisSpellId)
                            openRaidLib.CooldownManager.CooldownSpellUpdate(playerName, thisSpellId, timeLeft, charges, startTimeOffset, duration, auraDuration)

                            local cooldownInfo = cooldownGetSpellInfo(playerName, thisSpellId)
                            local unitCooldownTable = openRaidLib.GetUnitCooldowns(playerName)

                            --trigger a public callback
                            openRaidLib.publicCallback.TriggerCallback("CooldownUpdate", openRaidLib.GetUnitID(playerName), thisSpellId, cooldownInfo, unitCooldownTable, openRaidLib.CooldownManager.UnitData)
                        end
                    end
                end
            end

            --update the cooldown
            openRaidLib.CooldownManager.CooldownSpellUpdate(playerName, spellId, timeLeft, charges, startTimeOffset, duration, auraDuration) --receive 7 values
            local cooldownInfo = cooldownGetSpellInfo(playerName, spellId)
            --trigger a public callback
            local playerCooldownTable = openRaidLib.GetUnitCooldowns(playerName)
            openRaidLib.publicCallback.TriggerCallback("CooldownUpdate", "player", spellId, cooldownInfo, playerCooldownTable, openRaidLib.CooldownManager.UnitData)

            --send to comm
            openRaidLib.CooldownManager.SendPlayerCooldownUpdate(spellId, timeLeft, charges, startTimeOffset, duration, auraDuration)

            --create a timer to monitor the time of this cooldown
            --as there's just a few of them to monitor, there's no issue on creating one timer per spell
            cooldownStartTicker(spellId, timeLeft)
        end
    end

    --when the player is ressed while in a group, send the cooldown list
    function openRaidLib.CooldownManager.OnPlayerRess()
        --check if is in group
        if (openRaidLib.IsInGroup()) then
            openRaidLib.Schedules.NewUniqueTimer(1.0 + math.random(0.0, 6.0), openRaidLib.CooldownManager.SendAllPlayerCooldowns, "CooldownManager", "sendAllPlayerCooldowns_Schedule")
        end
    end

    function openRaidLib.CooldownManager.OnPlayerLeaveGroup()
        --clear the data
        openRaidLib.CooldownManager.EraseData()

        --trigger a public callback
        openRaidLib.publicCallback.TriggerCallback("CooldownListWipe", openRaidLib.CooldownManager.UnitData)

        --recreate the player cooldowns
        openRaidLib.CooldownManager.UpdatePlayerCooldownsLocally()
    end

    --when a talent has changed, it might remove or add a cooldown
    function openRaidLib.CooldownManager.OnPlayerTalentChanged()
        --immediatelly update the player cooldowns locally
        openRaidLib.CooldownManager.UpdatePlayerCooldownsLocally()

        --schedule send to the group, using a large delay to send due to the player might change more talents at once
        openRaidLib.Schedules.NewUniqueTimer(4 + math.random(0, 1), openRaidLib.CooldownManager.SendAllPlayerCooldowns, "CooldownManager", "sendAllPlayerCooldownsFromTalentChange_Schedule")
    end

    --check cooldown reset after a raid encounter ends finishing ongoing timeLeft tickers
    function openRaidLib.CooldownManager.CheckCooldownsAfterEncounterEnd()
        openRaidLib.CooldownManager.CleanupCooldownTickers()
        openRaidLib.Schedules.NewUniqueTimer(1 + math.random(1, 4), openRaidLib.CooldownManager.SendAllPlayerCooldowns, "CooldownManager", "sendAllPlayerCooldowns_Schedule")
    end
    function openRaidLib.CooldownManager.OnEncounterEnd()
        --run on next frame
        openRaidLib.Schedules.NewUniqueTimer(0.1, openRaidLib.CooldownManager.CheckCooldownsAfterEncounterEnd, "CooldownManager", "encounterEndCooldownsCheck_Schedule")
    end
    function openRaidLib.CooldownManager.OnEncounterStart()
        openRaidLib.Schedules.CancelUniqueTimer("CooldownManager", "sendAllPlayerCooldowns_Schedule")
        openRaidLib.Schedules.CancelUniqueTimer("CooldownManager", "sendAllPlayerCooldownsFromTalentChange_Schedule")
        openRaidLib.CooldownManager.SendAllPlayerCooldowns()
    end

    function openRaidLib.CooldownManager.OnMythicPlusStart()
        openRaidLib.Schedules.NewUniqueTimer(0.5, openRaidLib.CooldownManager.SendAllPlayerCooldowns, "CooldownManager", "sendAllPlayerCooldowns_Schedule")
    end

    function openRaidLib.CooldownManager.OnPlayerPetChanged()
        openRaidLib.CooldownManager.CheckCooldownChanges()
    end

    function openRaidLib.CooldownManager.OnAuraRemoved(event, unitId, spellId)
        --under development ~aura
        local timeLeft, charges, startTimeOffset, duration, auraDuration = openRaidLib.CooldownManager.GetPlayerCooldownStatus(spellId)

        --do need to update?
        if (not timeLeft or timeLeft < 1 or not auraDuration or auraDuration < 1) then
            return
        end

        local latencyCompensation = 1

        if (spellId) then
            if (auraDuration > latencyCompensation) then
                --cooldown aura got removed before expiration
                local newAuraDuration = 0
                local unitName = GetUnitName(unitId, true) or unitId
                openRaidLib.CooldownManager.CooldownSpellUpdate(unitName, spellId, timeLeft, charges, startTimeOffset, duration, newAuraDuration)

                --trigger a public callback
                local playerCooldownTable = openRaidLib.GetUnitCooldowns(unitName)
                local cooldownInfo = cooldownGetSpellInfo(unitName, spellId)
                openRaidLib.publicCallback.TriggerCallback("CooldownUpdate", "player", spellId, cooldownInfo, playerCooldownTable, openRaidLib.CooldownManager.UnitData)

                --send to comm
                openRaidLib.CooldownManager.SendPlayerCooldownUpdate(spellId, timeLeft, charges, startTimeOffset, duration, newAuraDuration)
            end
        end
    end

    openRaidLib.internalCallback.RegisterCallback("onLeaveGroup", openRaidLib.CooldownManager.OnPlayerLeaveGroup)
    openRaidLib.internalCallback.RegisterCallback("playerCast", openRaidLib.CooldownManager.OnPlayerCast)
    openRaidLib.internalCallback.RegisterCallback("onPlayerRess", openRaidLib.CooldownManager.OnPlayerRess)
    openRaidLib.internalCallback.RegisterCallback("talentUpdate", openRaidLib.CooldownManager.OnPlayerTalentChanged)
    openRaidLib.internalCallback.RegisterCallback("raidEncounterStart", openRaidLib.CooldownManager.OnEncounterStart)
    openRaidLib.internalCallback.RegisterCallback("raidEncounterEnd", openRaidLib.CooldownManager.OnEncounterEnd)
    openRaidLib.internalCallback.RegisterCallback("onLeaveCombat", openRaidLib.CooldownManager.OnEncounterEnd)
    openRaidLib.internalCallback.RegisterCallback("mythicDungeonStart", openRaidLib.CooldownManager.OnMythicPlusStart)
    openRaidLib.internalCallback.RegisterCallback("playerPetChange", openRaidLib.CooldownManager.OnPlayerPetChanged)
    openRaidLib.internalCallback.RegisterCallback("unitAuraRemoved", openRaidLib.CooldownManager.OnAuraRemoved)

--send a list through comm with cooldowns added or removed
function openRaidLib.CooldownManager.CheckCooldownChanges()
    --important: CheckForSpellsAdeedOrRemoved() already change the cooldowns on the player locally
    local spellsAdded, spellsRemoved = openRaidLib.CooldownManager.CheckForSpellsAdeedOrRemoved()

    --add a prefix to make things easier during unpack
    if (#spellsAdded > 0) then
        tinsert(spellsAdded, 1, "A")
    end

    --insert the spells that has been removed at the end of the spells added table and pack the table
    if (#spellsRemoved > 0) then
        spellsAdded[#spellsAdded+1] = "R"
        for _, spellId in ipairs(spellsRemoved) do
            spellsAdded[#spellsAdded+1] = spellId
        end
    end

    --send a comm if has any changes
    if (#spellsAdded > 0) then
        --pack
        local playerCooldownChangesString = openRaidLib.PackTable(spellsAdded)
        local dataToSend = CONST_COMM_COOLDOWNCHANGES_PREFIX .. ","
        dataToSend = dataToSend .. playerCooldownChangesString

        openRaidLib.commHandler.SendCommData(dataToSend)
        diagnosticComm("CheckCooldownChanges| " .. dataToSend) --debug
    end
end

function openRaidLib.CooldownManager.OnReceiveUnitCooldownChanges(data, unitName)
    local currentCooldowns = openRaidLib.CooldownManager.UnitData[unitName]
    --if does not have the full list of cooldowns of this unit, ignore cooldown add/remove comms

    if (not currentCooldowns or not openRaidLib.CooldownManager.HasFullCooldownList[unitName]) then
        return
    end

    --create a table to be ready to unpack
    local addedCooldowns = {}
    local removedCooldowns = {}
    local bIsCooldownAdded = false
    local bIsCooldownRemoved = false

    --the letters A and R separate cooldowns added and cooldowns removed
    for i = 1, #data do
        local thisData = data[i]

        if (thisData == "A") then
            bIsCooldownAdded = true

        elseif (thisData == "R") then
            bIsCooldownAdded = false
            bIsCooldownRemoved = true
        end

        if (bIsCooldownAdded) then
            thisData = tonumber(thisData)
            if (thisData) then
                addedCooldowns[#addedCooldowns+1] = thisData
            end

        elseif(bIsCooldownRemoved) then
            local spellId = tonumber(thisData)
            if (spellId) then
                removedCooldowns[#removedCooldowns+1] = spellId
            end
        end
    end

    if (#addedCooldowns > 0) then
        tinsert(addedCooldowns, 1, #addedCooldowns) --amount of indexes for UnpackTable()

        local cooldownsAddedUnpacked = openRaidLib.UnpackTable(addedCooldowns, 1, true, true, CONST_COOLDOWN_INFO_SIZE)
        for spellId, cooldownInfo in pairs(cooldownsAddedUnpacked) do
            --add the spell into the list of cooldowns of this unit
            local timeLeft, charges, timeOffset, duration, updateTime, auraDuration = openRaidLib.CooldownManager.GetCooldownInfoValues(cooldownInfo)
            openRaidLib.CooldownManager.CooldownSpellUpdate(unitName, spellId, timeLeft, charges, timeOffset, duration, auraDuration)

            --mark the filter cache of this unit as dirt
            openRaidLib.CooldownManager.NeedRebuildFilters[unitName] = true

            --trigger public callback
            openRaidLib.publicCallback.TriggerCallback("CooldownAdded", openRaidLib.GetUnitID(unitName), spellId, cooldownInfo, openRaidLib.GetUnitCooldowns(unitName), openRaidLib.CooldownManager.UnitData)
        end
    end

    if (#removedCooldowns > 0) then
        for _, spellId in ipairs(removedCooldowns) do
            --remove the spell from this unit cooldown list
            currentCooldowns[spellId] = nil
            --mark the filter cache of this unit as dirt
            openRaidLib.CooldownManager.NeedRebuildFilters[unitName] = true
            --trigger public callback
            openRaidLib.publicCallback.TriggerCallback("CooldownRemoved", openRaidLib.GetUnitID(unitName), spellId, openRaidLib.GetUnitCooldowns(unitName), openRaidLib.CooldownManager.UnitData)
        end
    end

end
openRaidLib.commHandler.RegisterORComm(CONST_COMM_COOLDOWNCHANGES_PREFIX, openRaidLib.CooldownManager.OnReceiveUnitCooldownChanges)

--compare the current list of spells of the player with a new spell list generated
--add or remove spells from the player cooldown list
--return two tables, the first has added spells and is a index table ready to pack and send to comm
--the second table is a index table with a list of spells that has been removed, also ready to pack
function openRaidLib.CooldownManager.CheckForSpellsAdeedOrRemoved()
    local playerName = UnitName("player")
    local currentCooldowns = openRaidLib.CooldownManager.UnitData[playerName]

    if (not currentCooldowns) then
        --generate the list of cooldowns for the player
        openRaidLib.CooldownManager.UpdatePlayerCooldownsLocally()
        currentCooldowns = openRaidLib.CooldownManager.UnitData[playerName]
    end

    local _, newCooldownList = openRaidLib.CooldownManager.GetPlayerCooldownList()
    local spellsAdded, spellsRemoved = {}, {}

    for spellId, cooldownInfo in pairs(newCooldownList) do
        if (not currentCooldowns[spellId]) then
            --a spell has been added
            local timeLeft, charges, timeOffset, duration, updateTime, auraDuration = openRaidLib.CooldownManager.GetCooldownInfoValues(cooldownInfo)
            openRaidLib.CooldownManager.CooldownSpellUpdate(playerName, spellId, timeLeft, charges, timeOffset, duration, auraDuration)

            local timeLeft, charges, startTimeOffset, duration, auraDuration = openRaidLib.CooldownManager.GetPlayerCooldownStatus(spellId) --return 5 values
            spellsAdded[#spellsAdded+1] = spellId
            spellsAdded[#spellsAdded+1] = timeLeft
            spellsAdded[#spellsAdded+1] = charges
            spellsAdded[#spellsAdded+1] = startTimeOffset
            spellsAdded[#spellsAdded+1] = duration
            spellsAdded[#spellsAdded+1] = auraDuration

            --mark the filter cache of this unit as dirt
            openRaidLib.CooldownManager.NeedRebuildFilters[playerName] = true
            openRaidLib.publicCallback.TriggerCallback("CooldownAdded", "player", spellId, cooldownInfo, openRaidLib.GetUnitCooldowns("player"), openRaidLib.CooldownManager.UnitData)
        end
    end

    for spellId in pairs(currentCooldowns) do
        if (not newCooldownList[spellId]) then
            --a spell has been removed
            currentCooldowns[spellId] = nil
            spellsRemoved[#spellsRemoved+1] = spellId
            --mark the filter cache of this unit as dirt
            openRaidLib.CooldownManager.NeedRebuildFilters[playerName] = true
            openRaidLib.publicCallback.TriggerCallback("CooldownRemoved", "player", spellId, openRaidLib.GetUnitCooldowns("player"), openRaidLib.CooldownManager.UnitData)
        end
    end

    return spellsAdded, spellsRemoved
end

--update the list of cooldowns of the player it self locally
--this is called right after changes in the player cooldowns
function openRaidLib.CooldownManager.UpdatePlayerCooldownsLocally(playerCooldownHash)
    if (not playerCooldownHash) then
        playerCooldownHash = select(2, openRaidLib.CooldownManager.GetPlayerCooldownList())
    end
    local playerName = UnitName("player")
    openRaidLib.CooldownManager.AddUnitCooldownsList(playerName, playerCooldownHash)
end

--adds a list of cooldowns for another player in the group
--this is only called from the received cooldown list from comm
function openRaidLib.CooldownManager.AddUnitCooldownsList(unitName, cooldownsTable, noCallback)
    local unitCooldownTable = cooldownGetUnitTable(unitName, true) --sending true to wipe previous data
    openRaidLib.TCopy(unitCooldownTable, cooldownsTable)

    --add the unitName to the list of units detected with the lib
    openRaidLib.CooldownManager.HasFullCooldownList[unitName] = true
    --mark the filter cache of this unit as dirt
    openRaidLib.CooldownManager.NeedRebuildFilters[unitName] = true

    --get the time where the cooldown data was received, this is used with the timeleft and startTimeOffset
    local timeNow = GetTime()
    for spellId, cooldownTable in pairs(cooldownsTable) do
        cooldownTable[CONST_COOLDOWN_INDEX_UPDATETIME] = timeNow
    end

    --trigger a public callback
    if (not noCallback) then
        openRaidLib.publicCallback.TriggerCallback("CooldownListUpdate", openRaidLib.GetUnitID(unitName), unitCooldownTable, openRaidLib.CooldownManager.UnitData)
    end
end

--received a cooldown update from another unit (sent by the function above)
openRaidLib.commHandler.RegisterORComm(CONST_COMM_COOLDOWNUPDATE_PREFIX, function(data, unitName)
    --get data
    local dataAsArray = data
    local spellId = tonumber(dataAsArray[1])
    local cooldownTimer = tonumber(dataAsArray[2])
    local charges = tonumber(dataAsArray[3])
    local startTime = tonumber(dataAsArray[4])
    local duration = tonumber(dataAsArray[5])
    local auraDuration = tonumber(dataAsArray[6])

    --check integrity
    if (not spellId or spellId == 0) then
        return openRaidLib.DiagnosticError("CooldownManager|comm received|spellId is invalid")

    elseif (not cooldownTimer) then
        return openRaidLib.DiagnosticError("CooldownManager|comm received|cooldownTimer is invalid")

    elseif (not charges) then
        return openRaidLib.DiagnosticError("CooldownManager|comm received|charges is invalid")

    elseif (not startTime) then
        return openRaidLib.DiagnosticError("CooldownManager|comm received|startTime is invalid")

    elseif (not duration) then
        return openRaidLib.DiagnosticError("CooldownManager|comm received|duration is invalid")

    elseif (not auraDuration) then
        return openRaidLib.DiagnosticError("CooldownManager|comm received|auraDuration is invalid")
    end

    --check for shared cooldown time
    local spellData = LIB_OPEN_RAID_COOLDOWNS_INFO[spellId] --warning this block of code is duplicated at warning: this block of code is duplicated at "openRaidLib.CooldownManager.OnPlayerCast"
    local sharedCooldownId = spellData and spellData.shareid
    if (sharedCooldownId) then
        local spellsWithSharedCooldown = LIB_OPEN_RAID_COOLDOWNS_SHARED_ID[sharedCooldownId]

        for thisSpellId in pairs(spellsWithSharedCooldown) do
            --don't run for the spell that triggered the shared cooldown
            if (thisSpellId ~= spellId) then
                --before triggering the cooldown, check if the player has the spell
                if (cooldownGetSpellInfo(unitName, thisSpellId)) then
                    local spellInfo = C_Spell.GetSpellInfo(thisSpellId)

                    openRaidLib.CooldownManager.CooldownSpellUpdate(unitName, thisSpellId, cooldownTimer, charges, startTime, duration, auraDuration)

                    local cooldownInfo = cooldownGetSpellInfo(unitName, thisSpellId)
                    local unitCooldownTable = openRaidLib.GetUnitCooldowns(unitName)

                    --trigger a public callback
                    openRaidLib.publicCallback.TriggerCallback("CooldownUpdate", openRaidLib.GetUnitID(unitName), thisSpellId, cooldownInfo, unitCooldownTable, openRaidLib.CooldownManager.UnitData)
                end
            end
        end
    end

    --update
    --unitName, spellId, cooldownTimer, charges, startTime, duration, auraDuration
    openRaidLib.CooldownManager.CooldownSpellUpdate(unitName, spellId, cooldownTimer, charges, startTime, duration, auraDuration)
    local cooldownInfo = cooldownGetSpellInfo(unitName, spellId)
    local unitCooldownTable = openRaidLib.GetUnitCooldowns(unitName)

    --trigger a public callback
    openRaidLib.publicCallback.TriggerCallback("CooldownUpdate", openRaidLib.GetUnitID(unitName), spellId, cooldownInfo, unitCooldownTable, openRaidLib.CooldownManager.UnitData)
end)

--clear data stored, this is called after the player quit from a group
function openRaidLib.CooldownManager.EraseData()
    table.wipe(openRaidLib.CooldownManager.UnitDataFilterCache)
    table.wipe(openRaidLib.CooldownManager.HasFullCooldownList)
    table.wipe(openRaidLib.CooldownManager.NeedRebuildFilters)
    table.wipe(openRaidLib.CooldownManager.UnitData)
end

--send to comm all cooldowns available for the player
function openRaidLib.CooldownManager.SendAllPlayerCooldowns()
    --get the full cooldown list
    local playerCooldownList, playerCooldownHash = openRaidLib.CooldownManager.GetPlayerCooldownList()
    --update the player cooldowns locally
    openRaidLib.CooldownManager.UpdatePlayerCooldownsLocally(playerCooldownHash)

    local dataToSend = "" .. CONST_COMM_COOLDOWNFULLLIST_PREFIX .. ","

    --pack
    local playerCooldownString = openRaidLib.PackTable(playerCooldownList)
    dataToSend = dataToSend .. playerCooldownString

    --send the data
    openRaidLib.commHandler.SendCommData(dataToSend)
    diagnosticComm("SendAllPlayerCooldowns| " .. dataToSend) --debug
end

--send to comm a specific cooldown that was just used, a charge got available or its cooldown is over (ready to use)
function openRaidLib.CooldownManager.SendPlayerCooldownUpdate(spellId, cooldownTimeLeft, charges, startTimeOffset, duration, auraDuration)
    local dataToSend = "" .. CONST_COMM_COOLDOWNUPDATE_PREFIX .. "," .. spellId .. "," .. cooldownTimeLeft .. "," .. charges .. "," .. startTimeOffset .. "," .. duration .. "," .. auraDuration
    openRaidLib.commHandler.SendCommData(dataToSend)
    diagnosticComm("SendPlayerCooldownUpdate| " .. dataToSend) --debug
end

--triggered when the lib receives a full list of cooldowns from another player in the raid
--@data: table received from comm
--@unitName: player name

function openRaidLib.CooldownManager.OnReceiveUnitCooldowns(data, unitName)
    --unpack the table as a pairs table
    local unpackedTable = openRaidLib.UnpackTable(data, 1, true, true, CONST_COOLDOWN_INFO_SIZE)

    --[=[ --debug for data received from Evokers
    local _, class = UnitClass(unitName)
    if (class == "EVOKER") then
        print(unitName)
        dumpt(unpackedTable)
    end
    --]=]

    --add the list of cooldowns
    openRaidLib.CooldownManager.AddUnitCooldownsList(unitName, unpackedTable)
end
openRaidLib.commHandler.RegisterORComm(CONST_COMM_COOLDOWNFULLLIST_PREFIX, openRaidLib.CooldownManager.OnReceiveUnitCooldowns)

--send a comm requesting other units in the raid to send an update on the requested spell
--any unit in the raid that has this cooldown should send a CONST_COMM_COOLDOWNUPDATE_PREFIX
--@spellId: spellId to query
function openRaidLib.CooldownManager.RequestCooldownInfo(spellId)
    local dataToSend = "" .. CONST_COMM_COOLDOWNREQUEST_PREFIX .. "," .. spellId
    openRaidLib.commHandler.SendCommData(dataToSend)
    diagnosticComm("RequestCooldownInfo| " .. dataToSend) --debug
end

function openRaidLib.RequestCooldownInfo(spellId) --api alias
    return openRaidLib.CooldownManager.RequestCooldownInfo(spellId)
end

function openRaidLib.CooldownManager.OnReceiveRequestForCooldownInfoUpdate(data, unitName)
    local spellId = tonumber(data[1])

    --check if this unit has this cooldown in its list of cooldowns
    if (not cooldownGetSpellInfo(UnitName("player"), spellId)) then
        return
    end

    --get the cooldown time for this spell
    local timeLeft, charges, startTimeOffset, duration, auraDuration = openRaidLib.CooldownManager.GetPlayerCooldownStatus(spellId)
    openRaidLib.CooldownManager.SendPlayerCooldownUpdate(spellId, timeLeft, charges, startTimeOffset, duration, auraDuration)
end
openRaidLib.commHandler.RegisterORComm(CONST_COMM_COOLDOWNREQUEST_PREFIX, openRaidLib.CooldownManager.OnReceiveRequestForCooldownInfoUpdate)

--------------------------------------------------------------------------------------------------------------------------------
--~keystones

    ---@class keystoneinfo
    ---@field level number
    ---@field mapID number
    ---@field challengeMapID number
    ---@field classID number
    ---@field rating number
    ---@field mythicPlusMapID number
    ---@field specID number

    --manager constructor
    openRaidLib.KeystoneInfoManager = {
        --structure:
        --[playerName] = keystoneinfo
        ---@type table<string, keystoneinfo>
        KeystoneData = {},
    }

    --search the player backpack to find a mythic keystone
    --with the keystone object, it'll attempt to get the mythicPlusMapID to be used with C_ChallengeMode.GetMapUIInfo(mythicPlusMapID)
    --ATM we are obligated to do this due to C_MythicPlus.GetOwnedKeystoneMapID() return the same mapID for the two Tazavesh dungeons
    local getMythicPlusMapID = function()
        for backpackId = 0, 4 do
            for slotId = 1, GetContainerNumSlots(backpackId) do
                local itemId = GetContainerItemID(backpackId, slotId)
                if (itemId == LIB_OPEN_RAID_MYTHICKEYSTONE_ITEMID) then
                    local itemLink = GetContainerItemLink(backpackId, slotId)
                    local destroyedItemLink = itemLink:gsub("|", "")
                    local color, itemID, mythicPlusMapID = strsplit(":", destroyedItemLink)
                    return tonumber(mythicPlusMapID)
                end
            end
        end
    end

    local checkForKeystoneChange = function()
        --clear the timer reference
        openRaidLib.KeystoneInfoManager.KeystoneChangedTimer = nil

        --check if the player has a keystone in the backpack by quering the keystone level
        local level = C_MythicPlus.GetOwnedKeystoneLevel()
        if (not level) then
            return
        end
        local mapID = C_MythicPlus.GetOwnedKeystoneMapID()

        --get the current player keystone info and then compare with the keystone info from the bag, if there is differences update the player keystone info
        local unitName = UnitName("player")
        ---@type keystoneinfo
        local keystoneInfo = openRaidLib.KeystoneInfoManager.GetKeystoneInfo(unitName, true)

        if (keystoneInfo.level ~= level or keystoneInfo.mapID ~= mapID) then
            openRaidLib.KeystoneInfoManager.UpdatePlayerKeystoneInfo(keystoneInfo)
            --hack: trigger a received data request to send data to party and guild when logging in
            openRaidLib.KeystoneInfoManager.OnReceiveRequestData()
        end
    end

    local bagUpdateEventFrame = _G["OpenRaidBagUpdateFrame"] or CreateFrame("frame", "OpenRaidBagUpdateFrame")
    bagUpdateEventFrame:RegisterEvent("BAG_UPDATE")
    bagUpdateEventFrame:RegisterEvent("ITEM_CHANGED")
    bagUpdateEventFrame:SetScript("OnEvent", function(bagUpdateEventFrame, event, ...)
        if (openRaidLib.KeystoneInfoManager.KeystoneChangedTimer) then
            return
        else
            openRaidLib.KeystoneInfoManager.KeystoneChangedTimer = C_Timer.NewTimer(2, checkForKeystoneChange)
        end
    end)

    --public callback does not check if the keystone has changed from the previous callback

    --API calls
        --return a table containing all information of units
        --format: [playerName-realm] = {information}
        function openRaidLib.GetAllKeystonesInfo()
            return openRaidLib.KeystoneInfoManager.GetAllKeystonesInfo()
        end

        --return a table containing information of a single unit
        function openRaidLib.GetKeystoneInfo(unitId)
            local unitName = GetUnitName(unitId, true) or unitId
            return openRaidLib.KeystoneInfoManager.GetKeystoneInfo(unitName)
        end

        function openRaidLib.RequestKeystoneDataFromGuild()
            if (IsInGuild()) then
                local dataToSend = "" .. CONST_COMM_KEYSTONE_DATAREQUEST_PREFIX
                openRaidLib.commHandler.SendCommData(dataToSend, 0x4)
                diagnosticComm("RequestKeystoneDataFromGuild| " .. dataToSend) --debug
                return true
            else
                return false
            end
        end

        function openRaidLib.RequestKeystoneDataFromParty()
            if (IsInGroup() and not IsInRaid()) then
                local dataToSend = "" .. CONST_COMM_KEYSTONE_DATAREQUEST_PREFIX
                openRaidLib.commHandler.SendCommData(dataToSend, 0x1)
                diagnosticComm("RequestKeystoneDataFromParty| " .. dataToSend) --debug
                return true
            else
                return false
            end
        end

        function openRaidLib.RequestKeystoneDataFromRaid()
            if (IsInRaid()) then
                local dataToSend = "" .. CONST_COMM_KEYSTONE_DATAREQUEST_PREFIX
                openRaidLib.commHandler.SendCommData(dataToSend, 0x2)
                diagnosticComm("RequestKeystoneDataFromRaid| " .. dataToSend) --debug
                return true
            else
                return false
            end
        end

        function openRaidLib.WipeKeystoneData()
            wipe(openRaidLib.KeystoneInfoManager.KeystoneData)
            --trigger public callback
            openRaidLib.publicCallback.TriggerCallback("KeystoneWipe", openRaidLib.KeystoneInfoManager.KeystoneData)

            --keystones are only available on retail
            if (not checkClientVersion("retail")) then
                return
            end

            --generate keystone info for the player
            local unitName = UnitName("player")
            local keystoneInfo = openRaidLib.KeystoneInfoManager.GetKeystoneInfo(unitName, true)
            openRaidLib.KeystoneInfoManager.UpdatePlayerKeystoneInfo(keystoneInfo)

            openRaidLib.publicCallback.TriggerCallback("KeystoneUpdate", unitName, keystoneInfo, openRaidLib.KeystoneInfoManager.KeystoneData)
            return true
        end

    --privite stuff, these function can still be called, but not advised

        ---@type keystoneinfo
        local keystoneTablePrototype = {
            level = 0,
            mapID = 0,
            challengeMapID = 0,
            classID = 0,
            rating = 0,
            mythicPlusMapID = 0,
            specID = 0,
        }

    function openRaidLib.KeystoneInfoManager.UpdatePlayerKeystoneInfo(keystoneInfo)
        keystoneInfo.level = C_MythicPlus.GetOwnedKeystoneLevel() or 0
        keystoneInfo.mapID = C_MythicPlus.GetOwnedKeystoneMapID() or 0 --returning nil?
        keystoneInfo.mythicPlusMapID = getMythicPlusMapID() or 0
        keystoneInfo.challengeMapID = C_MythicPlus.GetOwnedKeystoneChallengeMapID() or 0

        local _, _, playerClassID = UnitClass("player")
        keystoneInfo.classID = playerClassID

        local ratingSummary = C_PlayerInfo.GetPlayerMythicPlusRatingSummary("player")
        keystoneInfo.rating = ratingSummary and ratingSummary.currentSeasonScore or 0

        local specID = GetSpecializationInfo(GetSpecialization()) or 0
        keystoneInfo.specID = specID
    end

    function openRaidLib.KeystoneInfoManager.GetAllKeystonesInfo()
        return openRaidLib.KeystoneInfoManager.KeystoneData
    end

    --get the keystone info table or create a new one if 'createNew' is true
    function openRaidLib.KeystoneInfoManager.GetKeystoneInfo(unitName, createNew)
        local keystoneInfo = openRaidLib.KeystoneInfoManager.KeystoneData[unitName]
        if (not keystoneInfo and createNew) then
            keystoneInfo = {}
            openRaidLib.TCopy(keystoneInfo, keystoneTablePrototype)
            openRaidLib.KeystoneInfoManager.KeystoneData[unitName] = keystoneInfo
        end
        return keystoneInfo
    end

    local getKeystoneInfoToComm = function()
        local playerName = UnitName("player")
        local keystoneInfo = openRaidLib.KeystoneInfoManager.GetKeystoneInfo(playerName, true)
        openRaidLib.KeystoneInfoManager.UpdatePlayerKeystoneInfo(keystoneInfo)

        local dataToSend = CONST_COMM_KEYSTONE_DATA_PREFIX .. "," .. keystoneInfo.level .. "," .. keystoneInfo.mapID .. "," .. keystoneInfo.challengeMapID .. "," .. keystoneInfo.classID .. "," .. keystoneInfo.rating .. "," .. keystoneInfo.mythicPlusMapID .. "," .. keystoneInfo.specID
        return dataToSend
    end

    function openRaidLib.KeystoneInfoManager.SendPlayerKeystoneInfoToParty()
        local dataToSend = getKeystoneInfoToComm()
        openRaidLib.commHandler.SendCommData(dataToSend, CONST_COMM_SENDTO_PARTY)
        diagnosticComm("SendPlayerKeystoneInfoToParty| " .. dataToSend) --debug
    end

    function openRaidLib.KeystoneInfoManager.SendPlayerKeystoneInfoToGuild()
        if (LIB_OPEN_RAID_MYTHIC_PLUS_DND) then
            return
        end
        local dataToSend = getKeystoneInfoToComm()
        openRaidLib.commHandler.SendCommData(dataToSend, CONST_COMM_SENDTO_GUILD)
        diagnosticComm("SendPlayerKeystoneInfoToGuild| " .. dataToSend) --debug
    end

    local latestSend = 0

    --when a request data is received, only send the data to party and guild
    --sending stuff to raid need to be called my the application with 'openRaidLib.RequestKeystoneDataFromRaid()'
    function openRaidLib.KeystoneInfoManager.OnReceiveRequestData()
        if (not checkClientVersion("retail")) then
            return
        end

        if (latestSend+1 > GetTime()) then
            return
        end
        latestSend = GetTime()

        --update the information about the key stone the player has
        local keystoneInfo = openRaidLib.KeystoneInfoManager.GetKeystoneInfo(UnitName("player"), true)
        openRaidLib.KeystoneInfoManager.UpdatePlayerKeystoneInfo(keystoneInfo)

        local _, instanceType = GetInstanceInfo()
        if (instanceType == "party") then
            openRaidLib.Schedules.NewUniqueTimer(math.random(1), openRaidLib.KeystoneInfoManager.SendPlayerKeystoneInfoToParty, "KeystoneInfoManager", "sendKeystoneInfoToParty_Schedule")

        elseif (instanceType == "raid" or instanceType == "pvp") then
            openRaidLib.Schedules.NewUniqueTimer(math.random(0, 10) + math.random(1), openRaidLib.KeystoneInfoManager.SendPlayerKeystoneInfoToParty, "KeystoneInfoManager", "sendKeystoneInfoToParty_Schedule")

        else
            openRaidLib.Schedules.NewUniqueTimer(math.random(1), openRaidLib.KeystoneInfoManager.SendPlayerKeystoneInfoToParty, "KeystoneInfoManager", "sendKeystoneInfoToParty_Schedule")
        end

        if (IsInGuild()) then
            local delay = math.random(0, 2) + math.random()
            C_Timer.After(delay, openRaidLib.KeystoneInfoManager.SendPlayerKeystoneInfoToGuild)
            --openRaidLib.Schedules.NewUniqueTimer(delay, openRaidLib.KeystoneInfoManager.SendPlayerKeystoneInfoToGuild, "KeystoneInfoManager", "sendKeystoneInfoToGuild_Schedule")
        end
    end
    openRaidLib.commHandler.RegisterORComm(CONST_COMM_KEYSTONE_DATAREQUEST_PREFIX, openRaidLib.KeystoneInfoManager.OnReceiveRequestData)

    function openRaidLib.KeystoneInfoManager.OnReceiveKeystoneData(data, unitName)
        if (not checkClientVersion("retail")) then
            return
        end

        local level = tonumber(data[1])
        local mapID = tonumber(data[2])
        local challengeMapID = tonumber(data[3])
        local classID = tonumber(data[4])
        local rating = tonumber(data[5])
        local mythicPlusMapID = tonumber(data[6])
        local specID = tonumber(data[7]) or 0

        if (level and mapID and challengeMapID and classID and rating and mythicPlusMapID and specID) then
            local keystoneInfo = openRaidLib.KeystoneInfoManager.GetKeystoneInfo(unitName, true)
            keystoneInfo.level = level
            keystoneInfo.mapID = mapID
            keystoneInfo.mythicPlusMapID = mythicPlusMapID
            keystoneInfo.challengeMapID = challengeMapID
            keystoneInfo.classID = classID
            keystoneInfo.rating = rating
            keystoneInfo.specID = specID

            --trigger public callback
            openRaidLib.publicCallback.TriggerCallback("KeystoneUpdate", unitName, keystoneInfo, openRaidLib.KeystoneInfoManager.KeystoneData)
        end
    end
    openRaidLib.commHandler.RegisterORComm(CONST_COMM_KEYSTONE_DATA_PREFIX, openRaidLib.KeystoneInfoManager.OnReceiveKeystoneData)

    --on entering a group, send keystone information for the party
    function openRaidLib.KeystoneInfoManager.OnPlayerEnterGroup()
        --keystones are only available on retail
        if (not checkClientVersion("retail")) then
            return
        end

        if (IsInGroup() and not IsInRaid()) then
            --update the information about the key stone the player has
            local keystoneInfo = openRaidLib.KeystoneInfoManager.GetKeystoneInfo(UnitName("player"), true)
            openRaidLib.KeystoneInfoManager.UpdatePlayerKeystoneInfo(keystoneInfo)

            --send to the group which keystone the player has
            openRaidLib.Schedules.NewUniqueTimer(1 + math.random(0, 2) + math.random(), openRaidLib.KeystoneInfoManager.SendPlayerKeystoneInfoToParty, "KeystoneInfoManager", "sendKeystoneInfoToParty_Schedule")
        end
    end

    local keystoneManagerOnPlayerEnterWorld = function()
        --hack: trigger a received data request to send data to party and guild when logging in
        openRaidLib.KeystoneInfoManager.OnReceiveRequestData()

        --trigger public callback
        local unitName = UnitName("player")
        local keystoneInfo = openRaidLib.KeystoneInfoManager.GetKeystoneInfo(unitName, true)
        openRaidLib.KeystoneInfoManager.UpdatePlayerKeystoneInfo(keystoneInfo)

        openRaidLib.publicCallback.TriggerCallback("KeystoneUpdate", unitName, keystoneInfo, openRaidLib.KeystoneInfoManager.KeystoneData)
    end

    function openRaidLib.KeystoneInfoManager.OnPlayerEnterWorld()
        --keystones are only available on retail
        if (not checkClientVersion("retail")) then
            return
        end

        --attempt to load keystone item link as reports indicate it can be nil
        getMythicPlusMapID()

        C_Timer.After(2, keystoneManagerOnPlayerEnterWorld)
    end

    function openRaidLib.KeystoneInfoManager.OnMythicDungeonFinished()
        --keystones are only available on retail
        if (not checkClientVersion("retail")) then
            return
        end
        --hack: on received data send data to party and guild
        openRaidLib.KeystoneInfoManager.OnReceiveRequestData()

        --trigger public callback
        local unitName = UnitName("player")
        local keystoneInfo = openRaidLib.KeystoneInfoManager.GetKeystoneInfo(unitName, true)
        openRaidLib.KeystoneInfoManager.UpdatePlayerKeystoneInfo(keystoneInfo)

        openRaidLib.publicCallback.TriggerCallback("KeystoneUpdate", unitName, keystoneInfo, openRaidLib.KeystoneInfoManager.KeystoneData)
    end

    openRaidLib.internalCallback.RegisterCallback("onEnterWorld", openRaidLib.KeystoneInfoManager.OnPlayerEnterWorld)
    openRaidLib.internalCallback.RegisterCallback("onEnterGroup", openRaidLib.KeystoneInfoManager.OnPlayerEnterGroup)
    openRaidLib.internalCallback.RegisterCallback("mythicDungeonEnd", openRaidLib.KeystoneInfoManager.OnMythicDungeonFinished)

--------------------------------------------------------------------------------------------------------------------------------
--~rating

    ---@class MythicPlusRatingMapSummary
    ---@field challengeModeID number
    ---@field mapScore number
    ---@field bestRunLevel number
    ---@field bestRunDurationMS number
    ---@field finishedSuccess boolean

    ---@class ratinginfo
    ---@field classID number
    ---@field currentSeasonScore number
    ---@field runs MythicPlusRatingMapSummary[]

    --manager constructor
    openRaidLib.RatingInfoManager = {
        --structure:
        --[playerName] = ratinginfo
        ---@type table<string, ratinginfo>
        RatingData = {},
    }

    --API calls
        --return a table containing all information of units
        --format: [playerName-realm] = {information}
        function openRaidLib.GetAllRatingInfo()
            return openRaidLib.RatingInfoManager.GetAllRatingInfo()
        end

        --return a table containing information of a single unit
        function openRaidLib.GetRatingInfo(unitId)
            local unitName = GetUnitName(unitId, true) or unitId
            return openRaidLib.RatingInfoManager.GetRatingInfo(unitName)
        end

        function openRaidLib.RequestRatingDataFromGuild()
            if (IsInGuild()) then
                local dataToSend = "" .. CONST_COMM_RATING_DATAREQUEST_PREFIX
                openRaidLib.commHandler.SendCommData(dataToSend, 0x4)
                diagnosticComm("RequestRatingDataFromGuild| " .. dataToSend) --debug
                return true
            else
                return false
            end
        end

        function openRaidLib.RequestRatingDataFromParty()
            if (IsInGroup() and not IsInRaid()) then
                local dataToSend = "" .. CONST_COMM_RATING_DATAREQUEST_PREFIX
                openRaidLib.commHandler.SendCommData(dataToSend, 0x1)
                diagnosticComm("RequestRatingDataFromParty| " .. dataToSend) --debug
                return true
            else
                return false
            end
        end

        function openRaidLib.RequestRatingDataFromRaid()
            if (IsInRaid()) then
                local dataToSend = "" .. CONST_COMM_RATING_DATAREQUEST_PREFIX
                openRaidLib.commHandler.SendCommData(dataToSend, 0x2)
                diagnosticComm("RequestRatingDataFromRaid| " .. dataToSend) --debug
                return true
            else
                return false
            end
        end

        function openRaidLib.WipeRatingData()
            wipe(openRaidLib.RatingInfoManager.RatingData)
            --trigger public callback
            openRaidLib.publicCallback.TriggerCallback("RatingWipe", openRaidLib.RatingInfoManager.RatingData)

            --rating are only available on retail
            if (not checkClientVersion("retail")) then
                return
            end

            --generate rating info for the player
            local unitName = UnitName("player")
            local ratingInfo = openRaidLib.RatingInfoManager.GetRatingInfo(unitName, true)
            openRaidLib.RatingInfoManager.UpdatePlayerRatingInfo(ratingInfo)

            openRaidLib.publicCallback.TriggerCallback("RatingUpdate", unitName, ratingInfo, openRaidLib.RatingInfoManager.RatingData)
            return true
        end

    --privite stuff, these function can still be called, but not advised
        ---@type ratinginfo
        local ratingTablePrototype = {
            classID = 0,
            currentSeasonScore = 0,
            runs = {}
        }

    function openRaidLib.RatingInfoManager.UpdatePlayerRatingInfo(ratingInfo)
        --- I really just want this whole thing
        local summary = C_PlayerInfo.GetPlayerMythicPlusRatingSummary("player")

        ratingInfo.currentSeasonScore = summary and summary.currentSeasonScore or 0
        ratingInfo.runs = summary and summary.runs or {}

        local _, _, playerClassID = UnitClass("player")
        ratingInfo.classID = playerClassID
    end

    function openRaidLib.RatingInfoManager.GetAllRatingInfo()
        return openRaidLib.RatingInfoManager.RatingData
    end

    --get the rating info table or create a new one if 'createNew' is true
    function openRaidLib.RatingInfoManager.GetRatingInfo(unitName, createNew)
        local ratingInfo = openRaidLib.RatingInfoManager.RatingData[unitName]
        if (not ratingInfo and createNew) then
            ratingInfo = {}
            openRaidLib.TCopy(ratingInfo, ratingTablePrototype)
            openRaidLib.RatingInfoManager.RatingData[unitName] = ratingInfo
        end
        return ratingInfo
    end

    local getRatingInfoToComm = function()
        local playerName = UnitName("player")
        local ratingInfo = openRaidLib.RatingInfoManager.GetRatingInfo(playerName, true)
        openRaidLib.RatingInfoManager.UpdatePlayerRatingInfo(ratingInfo)

        local dataToSend = "" .. CONST_COMM_RATING_DATA_PREFIX .. ","

        local runs = {}
        for _, runInfo in ipairs(ratingInfo.runs) do
            runs[#runs+1] = {
                runInfo.challengeModeID,
                runInfo.bestRunDurationMS,
                runInfo.finishedSuccess and 1 or 0,
                runInfo.mapScore,
                runInfo.bestRunLevel
            }
        end

        dataToSend = dataToSend .. ratingInfo.classID .. ","
        dataToSend = dataToSend .. ratingInfo.currentSeasonScore .. ","
        dataToSend = dataToSend .. openRaidLib.PackTableAndSubTables(runs)

        return dataToSend
    end

    function openRaidLib.RatingInfoManager.SendPlayerRatingInfoToParty()
        local dataToSend = getRatingInfoToComm()
        openRaidLib.commHandler.SendCommData(dataToSend, CONST_COMM_SENDTO_PARTY)
        diagnosticComm("SendPlayerRatingInfoToParty| " .. dataToSend) --debug
    end

    function openRaidLib.RatingInfoManager.SendPlayerRatingInfoToGuild()
        local dataToSend = getRatingInfoToComm()
        openRaidLib.commHandler.SendCommData(dataToSend, CONST_COMM_SENDTO_GUILD)
        diagnosticComm("SendPlayerRatingInfoToGuild| " .. dataToSend) --debug
    end

    --when a request data is received, only send the data to party and guild
    --sending stuff to raid need to be called my the application with 'openRaidLib.RequestRatingDataFromRaid()'
    function openRaidLib.RatingInfoManager.OnReceiveRequestData()
        if (not checkClientVersion("retail")) then
            return
        end

        --update the information about the key stone the player has
        local ratingInfo = openRaidLib.RatingInfoManager.GetRatingInfo(UnitName("player"), true)
        openRaidLib.RatingInfoManager.UpdatePlayerRatingInfo(ratingInfo)

        local _, instanceType = GetInstanceInfo()
        if (instanceType == "party") then
            openRaidLib.Schedules.NewUniqueTimer(math.random(1), openRaidLib.RatingInfoManager.SendPlayerRatingInfoToParty, "RatingInfoManager", "sendRatingInfoToParty_Schedule")

        elseif (instanceType == "raid" or instanceType == "pvp") then
            openRaidLib.Schedules.NewUniqueTimer(math.random(0, 30) + math.random(1), openRaidLib.RatingInfoManager.SendPlayerRatingInfoToParty, "RatingInfoManager", "sendRatingInfoToParty_Schedule")

        else
            openRaidLib.Schedules.NewUniqueTimer(math.random(4), openRaidLib.RatingInfoManager.SendPlayerRatingInfoToParty, "RatingInfoManager", "sendRatingInfoToParty_Schedule")
        end

        if (IsInGuild()) then
            openRaidLib.Schedules.NewUniqueTimer(math.random(0, 6) + math.random(), openRaidLib.RatingInfoManager.SendPlayerRatingInfoToGuild, "RatingInfoManager", "sendRatingInfoToGuild_Schedule")
        end
    end
    openRaidLib.commHandler.RegisterORComm(CONST_COMM_RATING_DATAREQUEST_PREFIX, openRaidLib.RatingInfoManager.OnReceiveRequestData)

    function openRaidLib.RatingInfoManager.OnReceiveRatingData(data, unitName)
        if (not checkClientVersion("retail")) then
            return
        end

        local classID = tonumber(data[1])
        local currentSeasonScore = tonumber(data[2])

        local unpackedTable = openRaidLib.UnpackTable(data, 3, false, true, 5) -- 5 is the number of items in the run table

        local runs = {}
        for _, runInfo in ipairs(unpackedTable) do
            local challengeModeID, bestRunDurationMS, finishedSuccess, mapScore, bestRunLevel = unpack(runInfo)

            runs[#runs+1] = {
                challengeModeID = challengeModeID,
                bestRunDurationMS = bestRunDurationMS,
                finishedSuccess = finishedSuccess == 1 and true or false,
                mapScore = mapScore,
                bestRunLevel = bestRunLevel
            }
        end

        local ratingInfo = openRaidLib.RatingInfoManager.GetRatingInfo(unitName, true)
        ratingInfo.classID = classID
        ratingInfo.currentSeasonScore = currentSeasonScore
        ratingInfo.runs = runs

        --trigger public callback
        openRaidLib.publicCallback.TriggerCallback("RatingUpdate", unitName, ratingInfo, openRaidLib.RatingInfoManager.RatingData)
    end
    openRaidLib.commHandler.RegisterORComm(CONST_COMM_RATING_DATA_PREFIX, openRaidLib.RatingInfoManager.OnReceiveRatingData)

    --on entering a group, send rating information for the party
    function openRaidLib.RatingInfoManager.OnPlayerEnterGroup()
        --rating is only available on retail
        if (not checkClientVersion("retail")) then
            return
        end

        if (IsInGroup() and not IsInRaid()) then
            --update the information about the rating the player has
            local ratingInfo = openRaidLib.RatingInfoManager.GetRatingInfo(UnitName("player"), true)
            openRaidLib.RatingInfoManager.UpdatePlayerRatingInfo(ratingInfo)

            --send to the group what rating the player has
            openRaidLib.Schedules.NewUniqueTimer(1 + math.random(0, 2) + math.random(), openRaidLib.RatingInfoManager.SendPlayerRatingInfoToParty, "RatingInfoManager", "sendRatingInfoToParty_Schedule")
        end
    end

    local ratingManagerOnPlayerEnterWorld = function()
        --hack: trigger a received data request to send data to party and guild when logging in
        openRaidLib.RatingInfoManager.OnReceiveRequestData()

        --trigger public callback
        local unitName = UnitName("player")
        local ratingInfo = openRaidLib.RatingInfoManager.GetRatingInfo(unitName, true)
        openRaidLib.RatingInfoManager.UpdatePlayerRatingInfo(ratingInfo)

        openRaidLib.publicCallback.TriggerCallback("RatingUpdate", unitName, ratingInfo, openRaidLib.RatingInfoManager.RatingData)
    end

    function openRaidLib.RatingInfoManager.OnPlayerEnterWorld()
        --rating is only available on retail
        if (not checkClientVersion("retail")) then
            return
        end

        C_Timer.After(2, ratingManagerOnPlayerEnterWorld)
    end

    function openRaidLib.RatingInfoManager.OnMythicDungeonFinished()
        --rating is only available on retail
        if (not checkClientVersion("retail")) then
            return
        end
        --hack: on received data send data to party and guild
        openRaidLib.RatingInfoManager.OnReceiveRequestData()

        --trigger public callback
        local unitName = UnitName("player")
        local ratingInfo = openRaidLib.RatingInfoManager.GetRatingInfo(unitName, true)
        openRaidLib.RatingInfoManager.UpdatePlayerRatingInfo(ratingInfo)

        openRaidLib.publicCallback.TriggerCallback("RatingUpdate", unitName, ratingInfo, openRaidLib.RatingInfoManager.RatingData)
    end

    openRaidLib.internalCallback.RegisterCallback("onEnterWorld", openRaidLib.RatingInfoManager.OnPlayerEnterWorld)
    openRaidLib.internalCallback.RegisterCallback("onEnterGroup", openRaidLib.RatingInfoManager.OnPlayerEnterGroup)
    openRaidLib.internalCallback.RegisterCallback("mythicDungeonEnd", openRaidLib.RatingInfoManager.OnMythicDungeonFinished)

--------------------------------------------------------------------------------------------------------------------------------
--data

local createLocalCooldownTracker = function()
    local cdTrackerFrame = CreateFrame("frame")
    cdTrackerFrame:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")
    local allCooldownsFromLib = LIB_OPEN_RAID_COOLDOWNS_INFO

    ---@type table<castername, table<castspellid, number>>
    local recentCastedSpells =  {}

    cdTrackerFrame:SetScript("OnEvent", function(self, event, ...)
        if (event == "UNIT_SPELLCAST_SUCCEEDED") then
            local unitId, castGUID, spellId = ...

            --don't track spells casted by the player
            local bUnitIsThePlayer = UnitIsUnit(unitId, "player")
            if (not bUnitIsThePlayer) then
                --get the caster name and check if it's a unit in the group
                local casterName = GetUnitName(unitId, true)
                if (casterName) then
                    local unitInGroup = UnitInParty(unitId) or UnitInRaid(unitId)
                    if (unitInGroup) then
                        --check if the library has the spell in the list of cooldowns
                        local spellData = allCooldownsFromLib[spellId]

                        --check for overwrite spell ids

                        if (spellData) then
                            --check for cast_success spam from channel spells using a cooldown timer
                            local unitCastCooldown = recentCastedSpells[casterName]
                            if (not unitCastCooldown) then
                                unitCastCooldown = {}
                                recentCastedSpells[casterName] = unitCastCooldown
                            end

                            --don't register the cooldown if the spell was casted recently
                            if (not unitCastCooldown[spellId] or unitCastCooldown[spellId]+5 < GetTime()) then
                                unitCastCooldown[spellId] = GetTime()

                                --local auraName, texture, count, auraType, auraDuration, expirationTime = openRaidLib.AuraTracker.FindBuffDurationByUnitName(casterName, casterName, spellId)
                                local auraDuration = openRaidLib.CooldownManager.GetSpellBuffDuration(spellId, unitId)

                                --trigger a cooldown usage
                                local timeLeft = spellData.cooldown
                                local duration = spellData.duration or 0
                                local newCharges = 0
                                local startTimeOffset = 0
                                local buffDuration = auraDuration or spellData.duration or 0

                                openRaidLib.CooldownManager.CooldownSpellUpdate(casterName, spellId, timeLeft, newCharges, startTimeOffset, duration, buffDuration)
                                local cooldownInfo = cooldownGetSpellInfo(casterName, spellId)
                                local unitCooldownsTable = openRaidLib.GetUnitCooldowns(casterName)

                                --trigger a public callback
                                openRaidLib.publicCallback.TriggerCallback("CooldownUpdate", openRaidLib.GetUnitID(casterName), spellId, cooldownInfo, unitCooldownsTable, openRaidLib.CooldownManager.UnitData)
                            end
                        end
                    end
                end
            end
        end
    end)
end

--vintage cooldown tracker and interrupt tracker
C_Timer.After(0.1, function()
    createLocalCooldownTracker()
end)

tempCache.RestoreData()