--[[
    Core/Events.lua

    Two things, deliberately kept in one small module:

      1. A single hidden frame that receives Blizzard game events and fans them
         out to subscribers. One frame for the whole addon instead of one per
         module.
      2. An internal message bus so modules can talk to each other without
         holding references to each other (e.g. the UI reacts to
         "COLLECTION_CHANGED" without knowing who raised it).

    Nothing here polls: no OnUpdate script is installed.
]]

local _, ns = ...

local Events = ns.NewModule("Events")
ns.Events = Events

local listener = CreateFrame("Frame")

-- gameHandlers[event][owner] = function(event, ...)
local gameHandlers = {}
-- messageHandlers[message][owner] = function(message, ...)
local messageHandlers = {}

listener:SetScript("OnEvent", function(_, event, ...)
    local handlers = gameHandlers[event]
    if not handlers then
        return
    end

    for owner, handler in pairs(handlers) do
        local ok, err = pcall(handler, event, ...)
        if not ok then
            ns.Debug:ReportError(("event %s (%s)"):format(event, tostring(owner)), err)
        end
    end
end)

--=============================================================================
-- Game events
--=============================================================================

-- Events the client refused, kept so a module asking twice does not raise the
-- refusal twice.
local refused = {}

--- Registers an event with the client and reports it by name when refused.
--
-- Retail reserves some events for Blizzard's own interface. Asking for one
-- raises ADDON_ACTION_FORBIDDEN, whose dialog names only
-- "Frame:RegisterEvent()" and never says which event. Since this is the single
-- place in the addon that registers anything, it is the only place that can.
--
-- Deliberately not wrapped in pcall: a refusal is not a Lua error, it just
-- leaves the event unregistered, so asking the frame afterwards is what
-- actually tells us. Costs one call per distinct event for the whole session.
-- @return boolean Whether the client accepted the event.
local function ReportRefusal(event)
    refused[event] = true
    ns.Debug:Warn("EVENT", "client refused to register %s", event)

    -- Reaches chat even with diagnostics off, unlike the rest of the channel:
    -- the client has already interrupted the player with a dialog, and leaving
    -- the answer in a buffer nobody knows to dump costs another session. When
    -- diagnostics are on, the line above has already been printed.
    if not ns.Debug:IsEnabled() then
        ns.Print("|cffff6060[EVENT]|r " ..
            ("client refused to register %s"):format(event))
    end
end

local function RegisterWithClient(event)
    listener:RegisterEvent(event)

    if listener.IsEventRegistered and not listener:IsEventRegistered(event) then
        ReportRefusal(event)
        return false
    end

    return true
end

--- Subscribes to a Blizzard game event.
-- @param event string Event name, e.g. "PLAYER_LOGIN".
-- @param owner any Unique subscriber key, used to unregister later.
-- @param handler function Receives (event, ...).
-- @return boolean Whether the subscription is live.
function Events:RegisterEvent(event, owner, handler)
    if refused[event] then
        return false
    end

    if not gameHandlers[event] then
        if not RegisterWithClient(event) then
            return false
        end
        gameHandlers[event] = {}
    end

    gameHandlers[event][owner] = handler
    return true
end

--- Subscribes to a unit event, filtered by the client to the given units.
--
-- UNIT_* events fire for every unit the client knows about -- every nameplate,
-- every group member -- and filtering them in Lua means running our handler
-- hundreds of times a second to discard almost all of it. The client can do
-- that filtering for free.
--
-- One shared frame means one filter per event for the whole addon, so an event
-- registered here must not be registered elsewhere for different units.
-- @param units table Array of unit tokens, e.g. { "target" }.
function Events:RegisterUnitEvent(event, units, owner, handler)
    if refused[event] then
        return false
    end

    if not gameHandlers[event] then
        listener:RegisterUnitEvent(event, unpack(units))

        if listener.IsEventRegistered and not listener:IsEventRegistered(event) then
            ReportRefusal(event)
            return false
        end

        gameHandlers[event] = {}
    end

    gameHandlers[event][owner] = handler
    return true
end

--- Removes one subscriber from an event, unregistering the event entirely
-- once nobody is listening any more.
function Events:UnregisterEvent(event, owner)
    local handlers = gameHandlers[event]
    if not handlers then
        return
    end

    handlers[owner] = nil

    if next(handlers) == nil then
        gameHandlers[event] = nil
        listener:UnregisterEvent(event)
    end
end

--=============================================================================
-- Internal messages
--=============================================================================

--- Subscribes to an internal addon message.
-- @param message string Message name, e.g. "COLLECTION_CHANGED".
-- @param owner any Unique subscriber key.
-- @param handler function Receives (message, ...).
function Events:Subscribe(message, owner, handler)
    if not messageHandlers[message] then
        messageHandlers[message] = {}
    end

    messageHandlers[message][owner] = handler
end

--- Removes one subscriber from an internal message.
function Events:Unsubscribe(message, owner)
    local handlers = messageHandlers[message]
    if handlers then
        handlers[owner] = nil
    end
end

--- Broadcasts an internal addon message.
function Events:Send(message, ...)
    local handlers = messageHandlers[message]
    if not handlers then
        return
    end

    for owner, handler in pairs(handlers) do
        local ok, err = pcall(handler, message, ...)
        if not ok then
            ns.Debug:ReportError(("message %s (%s)"):format(message, tostring(owner)), err)
        end
    end
end
