-- TOG Tools — Addon Load Monitor (engine)
-- Provides per-addon memory, status, and load timing for the diagnostic tab.
--
-- Timing source (in priority order):
--   1. TOGToolsEarlyData — populated by the !TOGT companion addon, which loads
--      before every other addon (! sorts before all letters). Gives full coverage.
--   2. Own partial capture — this file registers ADDON_LOADED at file-load time
--      (~T in the alphabet), so only addons T–Z have timing if !TOGT is absent.
-- Memory and status are available for every installed addon regardless.

-- DELIBERATELY UNGATED (audit finding D). This module never calls
-- addon:IsModuleEnabled, and that is a decision rather than an oversight: the
-- capture below is the ONLY moment the information exists -- load timings come
-- from ADDON_LOADED / PLAYER_LOGIN during the loading screen and cannot be
-- recovered afterwards -- so a gate would leave a user who re-enables the tab
-- with an empty table until the next reload. It also persists nothing; the whole
-- capture lives in the file-local `_loadData`.
-- The reason is asserted, not just written: `Tests/moduleregistry_spec.lua`
-- requires every registered module to gate or to be exempted there with a
-- reason, and fails if this one quietly starts gating. Change it there too.

local _, addon = ...

local AddonLoad = {}
addon.modules["addonload"] = AddonLoad

-- These five do NOT move as a group, so the feature-detect is per-function, not per-flavour.
-- Verified in the Classic Era client source (F:\Blizzard API Docs\wow-ui-source-classic_era):
--   * GetNumAddOns / GetAddOnInfo / IsAddOnLoaded are documented under Namespace = "C_AddOns"
--     (AddOnsDocumentation.lua) and the bare globals do NOT exist — zero bare call sites in the
--     whole Interface tree, and no Blizzard_Deprecated* fallback. Calling the bare form crashes.
--   * GetAddOnMemoryUsage / UpdateAddOnMemoryUsage are documented under "PerformanceScript",
--     which declares NO Namespace, so on Era these genuinely ARE bare globals. C_AddOns gains
--     them on Retail, which is why the same `or` shape is still right for them.
local _GetNumAddOns         = (C_AddOns and C_AddOns.GetNumAddOns)         or GetNumAddOns
local _GetAddOnInfo         = (C_AddOns and C_AddOns.GetAddOnInfo)         or GetAddOnInfo
local _IsAddOnLoaded        = (C_AddOns and C_AddOns.IsAddOnLoaded)        or IsAddOnLoaded
local _GetAddOnMemoryUsage  = (C_AddOns and C_AddOns.GetAddOnMemoryUsage)  or GetAddOnMemoryUsage
local _UpdateAddOnMemoryUsage = (C_AddOns and C_AddOns.UpdateAddOnMemoryUsage) or UpdateAddOnMemoryUsage

-- ---------------------------------------------------------------------------
-- Partial capture fallback — only used when !TOGT companion is not installed
-- ---------------------------------------------------------------------------
local _captureFrame = CreateFrame("Frame")
local _loadData     = {}   -- [addonName] = { order, offset }
local _captureOrder = 0
local _captureStart = nil
local _loginOffset  = nil

-- debugprofilestop() returns sub-millisecond precision; GetTime() is frame-locked,
-- which causes every ADDON_LOADED event in the same frame to share the same value.
_captureFrame:RegisterEvent("ADDON_LOADED")
_captureFrame:RegisterEvent("PLAYER_LOGIN")
_captureFrame:SetScript("OnEvent", function(_, event, arg1)
    if event == "ADDON_LOADED" then
        local t = debugprofilestop()
        _captureOrder = _captureOrder + 1
        if not _captureStart then _captureStart = t end
        _loadData[arg1] = { order = _captureOrder, offset = (t - _captureStart) / 1000 }
    elseif event == "PLAYER_LOGIN" then
        _loginOffset = _captureStart and ((debugprofilestop() - _captureStart) / 1000) or 0
        _UpdateAddOnMemoryUsage()
        _captureFrame:UnregisterAllEvents()
    end
end)

-- Returns the best available timing table and login offset.
-- Prefers TOGToolsEarlyData (full coverage) over our own partial capture.
local function getTimingSource()
    local early = TOGToolsEarlyData
    if early and early.startTime then
        return early.loadTimes, early.loginTime, true
    end
    return _loadData, _loginOffset, false
end

-- ---------------------------------------------------------------------------
-- Data API — called by AddonLoadTab when drawing/refreshing
-- ---------------------------------------------------------------------------

-- Status sort priority: lower = worse = sorts first when descending.
-- Failures (1-5), neutral (6-7), loaded (8).
local STATUS_PRIORITY = {
    ["Missing"]     = 1,
    ["Dep Missing"] = 2,
    ["Corrupt TOC"] = 3,
    ["Wrong Version"] = 4,
    ["Outdated"]    = 5,
    ["On Demand"]   = 6,
    ["Disabled"]    = 7,
    ["Loaded"]      = 8,
}

-- Neutral reasons: not a user error, not a loading failure.
local NEUTRAL_REASONS = {
    DISABLED     = true,
    DEMAND_LOADED = true,
}

local REASON_LABELS = {
    MISSING           = "Missing",
    DEP_MISSING       = "Dep Missing",
    DISABLED          = "Disabled",
    INTERFACE_VERSION = "Wrong Version",
    CORRUPT           = "Corrupt TOC",
    DEMAND_LOADED     = "On Demand",
    OUTDATED          = "Outdated",
}

-- Returns rows (array) and summary (table).
-- RefreshMemory() should be called first for fresh KB numbers.
function AddonLoad:GetData()
    local timingData, loginOffset, hasCompanion = getTimingSource()
    local rows   = {}
    local sumMem = 0
    local nLoaded, nFailed, nDisabled = 0, 0, 0

    for i = 1, _GetNumAddOns() do
        local name, title, _, loadable, reason = _GetAddOnInfo(i)
        if name then
            local isLoaded = _IsAddOnLoaded(name)
            local mem      = _GetAddOnMemoryUsage(name) or 0
            sumMem         = sumMem + mem

            local status, statusColor

            if isLoaded then
                status      = "Loaded"
                statusColor = "ff44ee44"
                nLoaded     = nLoaded + 1
            elseif not loadable then
                local r = reason or "FAILED"
                status = REASON_LABELS[r] or r
                if NEUTRAL_REASONS[r] then
                    statusColor = "ff888888"
                    nDisabled   = nDisabled + 1
                else
                    statusColor = "ffff5555"
                    nFailed     = nFailed + 1
                end
            else
                -- Loadable but not yet loaded = load-on-demand addon
                status      = "On Demand"
                statusColor = "ffaaaaaa"
                nDisabled   = nDisabled + 1
            end

            local capture = timingData[name]
            rows[#rows + 1] = {
                name         = name,
                title        = (title and title ~= "") and title or name,
                mem          = mem,
                status       = status,
                statusColor  = statusColor,
                statusPrio   = STATUS_PRIORITY[status] or 9,
                order        = capture and capture.order  or nil,
                offset       = capture and capture.offset or nil,
                loadTime     = nil,  -- filled in below
            }
        end
    end

    -- Compute per-addon load time = delta between consecutive ADDON_LOADED events.
    -- Sort timed rows by load order, then diff adjacent offsets.
    --
    -- ⛔ THE DELTA BELONGS TO ROW N, NOT TO ROW N-1, and this comment used to say
    -- the opposite (audit round 14, finding tog-AD). ADDON_LOADED fires AFTER an
    -- addon's files have executed, so the interval between N-1's event and N's
    -- event is the time the client spent loading N. Charging it to N is correct
    -- and `Tests/addonload_spec.lua` pins exactly that ("charges each addon the
    -- gap since the previous one finished": Third finishes at 500ms and is
    -- charged 200, not 500).
    --
    -- The wrong version mattered because the plausible fix it invites -- shifting
    -- the assignment back one row -- would silently blame every addon for its
    -- predecessor's cost, in the one column this tab exists to provide, and
    -- nothing would error.
    --
    -- Row 1's loadTime = 0: there is no earlier event to measure against, so its
    -- cost is genuinely unknown rather than zero. With the companion installed
    -- row 1 is !!TOGT itself, which is why reporting 0 has never been misleading
    -- in practice.
    local timed = {}
    for _, row in ipairs(rows) do
        if row.order then timed[#timed + 1] = row end
    end
    table.sort(timed, function(a, b) return a.order < b.order end)
    for i, row in ipairs(timed) do
        if i == 1 then
            row.loadTime = 0
        else
            row.loadTime = row.offset - timed[i - 1].offset
        end
    end

    return rows, {
        total        = #rows,
        loaded       = nLoaded,
        failed       = nFailed,
        disabled     = nDisabled,
        totalMem     = sumMem,
        loginTime    = loginOffset,
        hasCompanion = hasCompanion,
    }
end

-- Re-snapshot memory from WoW's profiler.  Call before GetData() for fresh values.
function AddonLoad:RefreshMemory()
    _UpdateAddOnMemoryUsage()
end
