-- VDS.ItemNameResolver
-- ============================================================================
-- Async name cache for item IDs. Selectors strict-read state.session.items.names;
-- row factories call Request(id) on a miss to kick the load. The resolver
-- dispatches ITEM_NAME_RESOLVED when the name lands; selectors invalidate
-- via session.items.tick.
--
-- Canonical primitive: ItemEventListener (Blizzard global from
-- Interface/AddOns/Blizzard_ObjectAPI/Mainline/AsyncCallbackSystem.lua).
-- It wraps:
--   * accessor = C_Item.RequestLoadItemDataByID(itemID)
--   * event    = ITEM_DATA_LOAD_RESULT
-- and exposes :AddCallback(itemID, fn) -- fn fires once when the item's
-- data arrives (success=true), or callbacks are cleared if the load fails.
-- Used by AllTheThings and (via the underlying accessor + event) Baganator.
-- No need to hand-roll a pending tracker or subscribe to GET_ITEM_INFO_RECEIVED
-- (which is a separate event for the legacy GetItemInfo load path, NOT for
-- the modern RequestLoadItemDataByID accessor that the listener uses).
--
-- OWNS:       C_Item.GetItemNameByID, ItemEventListener:AddCallback
-- FORBIDDEN:  any other file calling C_Item.GetItemNameByID directly.
--
-- NOTE: methods MUST live inside the Declare spec -- Modules:Declare overwrites
-- VDS[name] with the spec, wiping any pre-Declare assignments.

VDS = VDS or {}

local A = VDS.Constants.ACTIONS

VDS.Modules:Declare({
    name = "ItemNameResolver",
    dependencies = {},

    ownsBlizzardNamespaces = { "C_Item.GetItemNameByID" },

    -- Pre-warm herb item-name cache at PLAYER_LOGIN. Without this, the
    -- warehouse selector emits placeholder names on first paint;
    -- WowScrollBox elementData is captured by reference so already-bound
    -- rows keep placeholder text until natural re-binds. By warming at
    -- module-enable (PLAYER_LOGIN), names are already resolved by the time
    -- the user opens VDS, so first paint shows real names.
    --
    -- Deferring this to first-window-show would re-introduce the bug --
    -- the async resolve would land mid-paint. Pre-warm stays at login.
    onEnable = function(_self)
        if not (_G.VDS_DyeCraftingDB and _G.VDS_DyeCraftingDB.pigments) then return end  -- exception(boundary): shipped Data global may be absent (tests, partial install)
        for _, pig in pairs(_G.VDS_DyeCraftingDB.pigments) do
            if pig.herbs then
                for _, herbID in ipairs(pig.herbs) do
                    VDS.ItemNameResolver:Request(herbID)
                end
            end
        end
    end,

    -- Per-itemID guard against duplicate ItemEventListener:AddCallback
    -- registrations. Without this, every paint of a cold-name row queues
    -- another callback; on busy first-paint sweeps (herb list rendered in
    -- multiple panels) the same itemID fires the listener N times,
    -- dispatching N redundant ITEM_NAME_RESOLVED actions. Mirrors ATT's
    -- per-cache `__Retrieved` flag (src/Classes/Item.Retail.lua).
    -- Cleared inside the callback so post-eviction re-requests are still possible.
    _pending = {},

    -- Debounce staging: ItemEventListener callbacks write into _pendingBatch
    -- (itemID -> name); a 100ms one-shot timer dispatches the batch as a
    -- single ITEM_NAMES_BATCH_RESOLVED action. 50 cold resolves in one frame
    -- -> 1 tick bump instead of 50 (which would cause 50 selector re-derives
    -- per visible widget with session.items.tick in reads).
    _pendingBatch  = {},
    _tickScheduled = false,
    _DEBOUNCE_S    = 0.1,

    -- Read cached name from state; nil on miss (caller may Request to load).
    Peek = function(_self, itemID)
        if not itemID then return nil end
        return VDS.Store:GetState().session.items.names[itemID]
    end,

    Request = function(self, itemID)
        if not itemID then return end
        if VDS.Store:GetState().session.items.names[itemID] then return end

        local name = _G.C_Item.GetItemNameByID(itemID)  -- boundary -- sync if cached
        if name and name ~= "" then
            VDS.Log:Debug("items", string.format("[1] Request id=%d -> sync name '%s'", itemID, name))
            VDS.Store:Dispatch({
                type    = A.ITEM_NAME_RESOLVED,
                payload = { itemID = itemID, name = name },
            })
            return
        end

        -- Cold cache: ItemEventListener handles the full async dance --
        -- calls RequestLoadItemDataByID, listens for ITEM_DATA_LOAD_RESULT,
        -- fires our callback once when data lands. Per-itemID; only ours
        -- fires, no shared event-stream noise to filter.
        if self._pending[itemID] then
            VDS.Log:Debug("items", string.format("[1] Request id=%d -> already pending, skip", itemID))
            return
        end
        self._pending[itemID] = true
        VDS.Log:Debug("items", string.format("[1] Request id=%d -> miss, ItemEventListener:AddCallback", itemID))
        _G.ItemEventListener:AddCallback(itemID, function()  -- boundary
            self._pending[itemID] = nil
            local resolved = _G.C_Item.GetItemNameByID(itemID)
                          or _G.C_Item.GetItemInfo(itemID)
            VDS.Log:Debug("items", string.format("[3] ItemEventListener fired id=%d resolved='%s'",
                itemID, tostring(resolved)))
            if resolved and resolved ~= "" then
                -- Stage into batch (replace existing if duplicate id arrives).
                self._pendingBatch[itemID] = resolved
                self:_ScheduleTick()
            end
        end)
    end,

    -- Debounce timer: collapses all stage'd resolutions in the window into
    -- a single batched dispatch. Re-arming during the window doesn't reset
    -- (the existing timer fires; staging continues into the next batch).
    _ScheduleTick = function(self)
        if self._tickScheduled then return end
        self._tickScheduled = true
        _G.C_Timer.After(self._DEBOUNCE_S, function()
            self._tickScheduled = false
            local batch = self._pendingBatch
            self._pendingBatch = {}
            local entries = {}
            for id, name in pairs(batch) do
                entries[#entries + 1] = { itemID = id, name = name }
            end
            if #entries > 0 then
                VDS.Log:Debug("items", string.format("[4] dispatch batch n=%d", #entries))
                VDS.Store:Dispatch({
                    type    = A.ITEM_NAMES_BATCH_RESOLVED,
                    payload = { entries = entries },
                })
            end
        end)
    end,
})
