---@class AbstractFramework
local AF = select(2, ...)

-- NOTE: it's highly recommended to use a unique prefix for every event

local callbacks = {
    -- invoke priority
    high = {},
    medium = {},
    low = {},
}

---@param event string
---@param callback fun(event:string, ...:any) function to call when event is fired
---@param priority "high"|"medium"|"low"|nil default is "medium".
---@param tag string|nil for Unregister/Get
function AF.RegisterCallback(event, callback, priority, tag)
    assert(not priority or priority == "high" or priority == "medium" or priority == "low", "Priority must be high, medium, low or nil.")
    local t = callbacks[priority or "medium"]
    if not t[event] then t[event] = {} end
    t[event][callback] = tag or "_no_tag_"
end

local function GetCallback(_callbacks, result, event, tag)
    if _callbacks[event] then
        for fn, v in next, _callbacks[event] do
            if not tag or v == tag then
                tinsert(result, fn)
            end
        end
    end
end

---@param event string
---@param tag string|nil if nil, returns all callbacks for the event
---@return table callbacks list of functions
function AF.GetCallbacks(event, tag)
    local result = {}
    GetCallback(callbacks.high, result, event, tag)
    GetCallback(callbacks.medium, result, event, tag)
    GetCallback(callbacks.low, result, event, tag)
    return result
end

---@param event string
---@param callback function|string function or tag
function AF.UnregisterCallback(event, callback)
    for _, t in next, callbacks do
        if t[event] then
            if type(callback) == "function" then
                t[event][callback] = nil
            elseif type(callback) == "string" then
                for fn, tag in next, t[event] do
                    if tag == callback then
                        t[event][fn] = nil
                        break
                    end
                end
            end
        end
    end
end

---@param event string
function AF.UnregisterAllCallbacks(event)
    for _, t in next, callbacks do
        t[event] = nil
    end
end

---@param event string
---@param callback fun(event:string, ...:any)
---@param priority "high"|"medium"|"low"|nil default is "medium"
function AF.RegisterCallbackOnce(event, callback, priority)
    local function wrapper(...)
        AF.UnregisterCallback(event, wrapper)
        callback(event, ...)
    end
    AF.RegisterCallback(event, wrapper, priority)
end

-- ---@param event string
-- ---@param callback function
-- ---@return boolean
-- --- invalid for one time callbacks
-- function AF.IsCallbackRegistered(event, callback)
--     for _, t in next, callbacks do
--         if t[event] and t[event][callback] then
--             return true
--         end
--     end
--     return false
-- end

---------------------------------------------------------------------
-- debug events
---------------------------------------------------------------------
AF.DEBUG_EVENTS = {
    AF_FIRST_FRAME_RENDERED = false,
    AF_PIXEL_UPDATE = "blazing_tangerine",
    AF_SCALE_CHANGED = "blazing_tangerine",
    -- AF_PIXEL_UPDATE_START = false,
    -- AF_PIXEL_UPDATE_END = false,
    AF_LOADED = "blazing_tangerine",
    AF_POPUPS_READY = "blazing_tangerine",
    AF_PLAYER_DATA_UPDATE = "lightblue",
    AF_PLAYER_SPEC_UPDATE = "lightblue",
    AF_INSTANCE_ENTER = "sand",
    AF_INSTANCE_LEAVE = "sand",
    AF_INSTANCE_STATE_CHANGE = "sand",
    AF_PLAYER_LOGIN = false,
    AF_PLAYER_LOGIN_DELAYED = "gray",
    AF_PLAYER_ENTERING_WORLD_DELAYED = "gray",
    AF_COMBAT_ENTER = false,
    AF_COMBAT_LEAVE = false,
    AF_GROUP_UPDATE = false,
    AF_GROUP_SIZE_CHANGED = false,
    AF_GROUP_TYPE_CHANGED = "lightblue",
    AF_GROUP_PERMISSION_CHANGED = false,
    AF_MARKER_PERMISSION_CHANGED = false,
    AF_JOIN_TEMP_CHANNEL = "classicrose",
    AF_LEAVE_TEMP_CHANNEL = "classicrose",
    AF_UNIT_ITEM_LEVEL_UPDATE = false,
}

---------------------------------------------------------------------
-- fire
---------------------------------------------------------------------
function AF.Fire(event, ...)
    if AFConfig then
        local addon = AF.GetAddon()
        if (addon and AFConfig.debug[addon]) or (not addon and AFConfig.debug.AF_EVENTS) then
            local color = AF.DEBUG_EVENTS[event]
            if color then
                local e = AF.WrapTextInColor(event, type(color) == "string" and color or "white")
                if select("#", ...) > 0 then
                    print(AF.WrapTextInColor("[EVENT]", "hotpink"), e, AF.GetColorStr("gray") .. ":", ...)
                else
                    print(AF.WrapTextInColor("[EVENT]", "hotpink"), e)
                end
            end
        end
    end

    if callbacks.high[event] then
        for fn in next, callbacks.high[event] do
            fn(event, ...)
        end
    end

    if callbacks.medium[event] then
        for fn in next, callbacks.medium[event] do
            fn(event, ...)
        end
    end

    if callbacks.low[event] then
        for fn in next, callbacks.low[event] do
            fn(event, ...)
        end
    end
end

function AF.GetFireFunc(event, ...)
    local a1, a2, a3 = ...
    local numArgs = select("#", ...)
    local args
    if numArgs > 3 then
        args = {...}
    end

    if numArgs == 0 then
        return function()
            AF.Fire(event)
        end
    elseif numArgs == 1 then
        return function()
            AF.Fire(event, a1)
        end
    elseif numArgs == 2 then
        return function()
            AF.Fire(event, a1, a2)
        end
    elseif numArgs == 3 then
        return function()
            AF.Fire(event, a1, a2, a3)
        end
    else
        return function()
            AF.Fire(event, unpack(args, 1, numArgs))
        end
    end
end

---------------------------------------------------------------------
-- addon loaded
---------------------------------------------------------------------
local addonCallbacks = {}

local frame = CreateFrame("Frame")
frame:RegisterEvent("ADDON_LOADED")
frame:SetScript("OnEvent", function(self, event, addon, containsBindings)
    if addonCallbacks[addon] then
        for fn in next, addonCallbacks[addon] do
            fn(addon, containsBindings)
        end
    end
end)

---@param addon string
---@param func fun(addon:string, containsBindings:boolean) function to call when the addon is loaded
function AF.RegisterAddonLoaded(addon, func)
    if not addonCallbacks[addon] then addonCallbacks[addon] = {} end
    addonCallbacks[addon][func] = true
end

---@param addon string
---@param func function
function AF.UnregisterAddonLoaded(addon, func)
    if addonCallbacks[addon] then
        addonCallbacks[addon][func] = nil
    end
end