--[[
============================================================================
ETB Modules/BagOverlay.lua  (added in v15.6.0)
============================================================================

B2 from the roadmap: color-coded overlays on bag slots that flag items
whose transmog appearance is NOT collected. Built on top of CollectionAPI
(v15.4.0) so every slot redraw is a single cached lookup.

Design rationale — "show signal, not noise":

  Many similar addons paint every collected slot, every uncollected slot,
  AND every restricted slot, leaving the bag looking like a dashboard.
  We deliberately invert this:

    - COLLECTED or KNOWN_OTHER appearances -> NO overlay (clean)
    - NO_APPEARANCE (consumables, trinkets, etc.) -> NO overlay
    - NOT_COLLECTED + can learn -> YELLOW square (attention!)
    - NOT_COLLECTED + cannot learn -> GRAY square (only shown if user opts
      in via bagOverlay.showUnlearnable — typically noise for most players)

  Result: a bag with 30 items might show 1-3 yellow markers, telling the
  player exactly which items they should keep vs vendor for new appearances.

Implementation notes for MoP Classic 5.4.8:

  - Uses the legacy global frame naming (ContainerFrame1.._N) since the
    newer retail ContainerFrameContainer.ContainerFrames collection doesn't
    exist on this client.
  - Hooks ContainerFrame_Update via hooksecurefunc, the standard pattern
    for piggybacking on Blizzard's bag refresh.
  - Overlay textures are lazy-created on first paint and pooled on each
    itemButton via button.ETBBagOverlay. Subsequent updates only call
    Show/Hide/SetVertexColor, never recreate the texture.
  - Listens for TRANSMOG_COLLECTION_SOURCE_ADDED to repaint the visible
    bag immediately when the player learns a new appearance.

Third-party bag-addon support (v15.8.0):

  - Baganator: registered as a corner widget via the documented public API
    Baganator.API.RegisterCornerWidget. Bottom-left placement, priority 5.
    Players see it in Baganator's customise UI as "ETB Uncollected".
  - Bagnon: hooksecurefunc on Bagnon.Item:Update (Bagnon has no public
    plugin API; this is the standard third-party pattern).
  - ArkInventory and AdiBags remain on the backlog and would each require
    their own integration. Both addons replace ContainerFrame entirely;
    each has a different hook surface. Add when a user reports the gap.
============================================================================
]]--

local ADDON_NAME, NS = ...
local ETB = NS.ETB
if not ETB then return end

ETB.BagOverlay = ETB.BagOverlay or { initialized = false }
local BO = ETB.BagOverlay

-- =============================================================================
-- CONFIG
-- =============================================================================

-- Overlay marker dimensions and placement
local MARKER_SIZE   = 10        -- 10x10 px square
local MARKER_INSET  = 1         -- pixels from the bottom-left corner
local LAYER         = "OVERLAY"
local SUB_LAYER     = 6         -- above the icon (default 0) and the count text (default 5)

-- Colors. RGBA. Alpha controls how "loud" the marker is.
local COLOR_LEARN_ABLE = { 1.0, 0.85, 0.15, 1.0 }    -- yellow: missing & learnable
local COLOR_BORDER     = { 0.05, 0.05, 0.05, 1.0 }   -- dark border for contrast
local COLOR_UNLEARN_ABLE = { 0.55, 0.55, 0.55, 0.85 } -- dim gray: missing but can't equip

-- =============================================================================
-- INTERNAL: per-button overlay creation + state application
-- =============================================================================

-- Create the overlay texture pair (border + fill) on this button if it
-- doesn't already have one. Pooled via button.ETBBagOverlay so subsequent
-- redraws are O(1) — no texture allocations.
local function ensureOverlay(button)
    if button.ETBBagOverlay then return button.ETBBagOverlay end

    -- Border texture: drawn first, slightly larger than the fill
    local border = button:CreateTexture(nil, LAYER, nil, SUB_LAYER)
    border:SetSize(MARKER_SIZE + 2, MARKER_SIZE + 2)
    border:SetPoint("BOTTOMLEFT", button, "BOTTOMLEFT", MARKER_INSET - 1, MARKER_INSET - 1)
    border:SetColorTexture(unpack(COLOR_BORDER))

    -- Fill texture: drawn on top
    local fill = button:CreateTexture(nil, LAYER, nil, SUB_LAYER + 1)
    fill:SetSize(MARKER_SIZE, MARKER_SIZE)
    fill:SetPoint("BOTTOMLEFT", button, "BOTTOMLEFT", MARKER_INSET, MARKER_INSET)
    fill:SetColorTexture(1, 1, 1, 1)  -- white base; actual color applied via SetVertexColor per overlay state

    local overlay = { border = border, fill = fill }
    button.ETBBagOverlay = overlay
    return overlay
end

local function hideOverlay(button)
    local o = button.ETBBagOverlay
    if o then
        o.border:Hide()
        o.fill:Hide()
    end
end

local function showOverlay(button, color)
    local o = ensureOverlay(button)
    o.border:Show()
    o.fill:Show()
    o.fill:SetVertexColor(color[1], color[2], color[3], color[4])
end

-- =============================================================================
-- INTERNAL: paint a button given a known itemID
-- =============================================================================
-- v15.8.0: itemID-keyed paint path. Used by both the default-bag path
-- (which first looks up the itemID via GetContainerItemID) and by third-party
-- bag-addon integrations (Baganator, Bagnon) which already know the itemID
-- from their own state. Centralizes the decision logic in one place.

local function paintButtonForItem(button, itemID)
    if not button then return end

    -- Master toggle: feature off -> always hide
    if not ETB.Settings:Get("bagOverlay.enabled") then
        hideOverlay(button)
        return
    end

    if not itemID then
        hideOverlay(button)
        return
    end

    -- CollectionAPI not loaded yet (edge case during early init)
    if not ETB.CollectionAPI then
        hideOverlay(button)
        return
    end

    local state = ETB.CollectionAPI:GetState(itemID)

    -- v15.8.12: KNOWN_OTHER_DIFFERENT_ARMOR means we have the visual via a
    -- different armor type that the player can't equip — in MoP 5.4.8
    -- transmog is per-armor-type, so the appearance is NOT actually
    -- usable on this item. Treat it the same as NOT_COLLECTED for the
    -- bag-overlay paint decision (still flag the item for the user).
    --
    -- COLLECTED / KNOWN_OTHER -> the player has a usable copy, no marker.
    -- NO_APPEARANCE           -> not transmoggable, no marker.
    -- nil (pending)           -> data not loaded; will repaint via GET_ITEM_INFO_RECEIVED.
    -- NOT_COLLECTED / KNOWN_OTHER_DIFFERENT_ARMOR -> flag the item.
    if state ~= "NOT_COLLECTED" and state ~= "KNOWN_OTHER_DIFFERENT_ARMOR" then
        hideOverlay(button)
        return
    end

    -- Branch on whether the player can actually learn THIS item. For
    -- KNOWN_OTHER_DIFFERENT_ARMOR the player owns the appearance via a
    -- different armor type — CanLearn for THIS item will return false
    -- with WRONG_ARMOR_TYPE, so it falls into the "showUnlearnable" path.
    local canLearn = ETB.CollectionAPI:CanLearn(itemID)
    if canLearn then
        showOverlay(button, COLOR_LEARN_ABLE)
    elseif ETB.Settings:Get("bagOverlay.showUnlearnable") then
        showOverlay(button, COLOR_UNLEARN_ABLE)
    else
        hideOverlay(button)
    end
end

-- =============================================================================
-- INTERNAL: per-slot decision (default Blizzard bag path)
-- =============================================================================

local function updateButton(button, bagID, slotIndex)
    if not ETB.Settings:Get("bagOverlay.enabled") then
        hideOverlay(button)
        return
    end

    if not GetContainerItemID then  -- defensive: API present?
        hideOverlay(button)
        return
    end
    local itemID = GetContainerItemID(bagID, slotIndex)
    if not itemID then
        hideOverlay(button)
        return
    end

    paintButtonForItem(button, itemID)
end

-- =============================================================================
-- INTERNAL: process all visible bag frames
-- =============================================================================
-- Called on ContainerFrame_Update (a single frame refresh) and also on a
-- full RepaintAll (e.g. when collection state changes globally).

local function updateContainerFrame(frame)
    if not frame or not frame.GetID then return end
    local bagID = frame:GetID()
    -- bagID can be 0..NUM_BAG_SLOTS for normal bags, 5..11 for bank bags.
    -- The bank-bag range is also valid (player wants to see what's collected
    -- in the bank too).
    if not bagID then return end

    local name = frame:GetName()
    if not name then return end

    local numSlots = frame.size  -- ContainerFrame stores its slot count here
    if not numSlots then return end

    -- MoP 5.4.8 itemButton naming: <FrameName>Item<N>, where N is 1..size.
    --
    -- The mapping from button index to bag slot is non-obvious: Blizzard
    -- displays slots bottom-to-top, so button 1 is the LAST slot. But the
    -- correct slot index is ALREADY stored on the button by
    -- ContainerFrame_GenerateFrame via itemButton:SetID(index). Use
    -- itemButton:GetID() — that's the canonical source and stays correct
    -- if Blizzard ever changes the display ordering.
    for buttonIdx = 1, numSlots do
        local itemButton = _G[name .. "Item" .. buttonIdx]
        if itemButton then
            local slotIndex = itemButton:GetID()
            updateButton(itemButton, bagID, slotIndex)
        end
    end
end

-- Repaint every currently-shown container frame. Used when a global
-- collection event arrives.
local function repaintAll()
    for i = 1, NUM_CONTAINER_FRAMES or 13 do
        local f = _G["ContainerFrame" .. i]
        if f and f:IsShown() then
            updateContainerFrame(f)
        end
    end
end

-- =============================================================================
-- INITIALIZATION
-- =============================================================================

function BO:Initialize()
    if self.initialized then return end

    -- If the overlay is disabled, avoid installing bag hooks. Listen for the
    -- setting to turn on so toggling it later still works without a reload.
    if ETB.Settings and ETB.Settings:Get("bagOverlay.enabled") == false then
        if not self._settingsListener and ETB.EventBus then
            self._settingsListener = true
            ETB.EventBus:Register("ETB_SETTING_CHANGED", function(_, path, value)
                if path == "bagOverlay.enabled" and value == true then
                    BO:Initialize()
                end
            end)
        end
        return
    end

    -- Hook the standard MoP bag refresh
    if type(ContainerFrame_Update) == "function" then
        hooksecurefunc("ContainerFrame_Update", updateContainerFrame)
    else
        -- ContainerFrame_Update not present on this client (shouldn't happen
        -- on MoP 5.4.8 but defend anyway). Without it, we can't paint.
        if ETB.Debug then ETB:Debug("BagOverlay", "ContainerFrame_Update not found; B2 disabled") end
        return
    end

    -- Repaint on collection changes (player learned a new appearance, or
    -- one was removed somehow). Cache invalidation in CollectionAPI happens
    -- automatically via its own event handler; we just need to redraw.
    local eventFrame = CreateFrame("Frame", "ETBBagOverlayEvents")
    eventFrame:RegisterEvent("TRANSMOG_COLLECTION_SOURCE_ADDED")
    eventFrame:RegisterEvent("TRANSMOG_COLLECTION_SOURCE_REMOVED")
    eventFrame:RegisterEvent("GET_ITEM_INFO_RECEIVED")
    -- v15.8.8: BAG_UPDATE registration removed. The handler had a guard
    -- (`if event ~= "BAG_UPDATE" then`) that made it a no-op — we were
    -- paying for the event dispatch + the Lua callback on every bag tick
    -- for nothing. ContainerFrame_Update is our actual repaint trigger
    -- and that hook still runs on every bag refresh.
    local repaintQueued = false
    local function anyBagShown()
        for i = 1, NUM_CONTAINER_FRAMES or 13 do
            local f = _G["ContainerFrame" .. i]
            if f and f:IsShown() then return true end
        end
        return false
    end
    eventFrame:SetScript("OnEvent", function(_, event)
        -- All three registered events conceptually mean "redraw what's
        -- visible now" — collection changed or new item info arrived.
        -- v15.42.0: GET_ITEM_INFO_RECEIVED arrives in storms during cache
        -- warming. Skip it outright when no bag is open (nothing to repaint),
        -- and debounce so a burst collapses into a single repaint.
        if event == "GET_ITEM_INFO_RECEIVED" and not anyBagShown() then return end
        if repaintQueued then return end
        repaintQueued = true
        C_Timer.After(0.2, function()
            repaintQueued = false
            repaintAll()
        end)
    end)

    -- v15.8.0: Third-party bag-addon integrations. These addons replace
    -- ContainerFrame entirely, so the default ContainerFrame_Update hook
    -- never fires for their item buttons. Each integration uses the
    -- addon's documented or de facto plugin surface.
    self:InstallBaganatorIntegration()
    self:InstallBagnonIntegration()

    -- v15.8.1: ETB.BagOverlay:Initialize fires at PLAYER_LOGIN+1s, but
    -- Baganator (and any LoadOnDemand addon) may not yet be loaded at that
    -- point — InstallBaganator no-ops silently. Subscribe to ADDON_LOADED
    -- so we retry the install when the relevant addon finishes loading.
    -- Use a plain WoW frame here (not EventBus) because ADDON_LOADED is a
    -- WoW event, not an ETB synthetic event.
    local deferredFrame = CreateFrame("Frame")
    deferredFrame:RegisterEvent("ADDON_LOADED")
    deferredFrame:SetScript("OnEvent", function(_, _, addonName)
        if addonName == "Baganator" and not self._baganatorInstalled then
            self:InstallBaganatorIntegration()
        elseif addonName == "Bagnon" and not self._bagnonInstalled then
            self:InstallBagnonIntegration()
        end
    end)

    self.initialized = true
end

-- =============================================================================
-- BAGANATOR INTEGRATION
-- =============================================================================
-- Baganator exposes a public plugin API: Baganator.API.RegisterCornerWidget.
-- We register a corner widget that paints the bottom-left corner of each
-- item button whenever Baganator updates the slot.
--
-- The widget contract per Baganator/API/Main.lua line 218-227:
--   onInit(itemButton) -> Frame   : called once per button; return widget frame
--   onUpdate(widget, details) -> bool : return truthy to show, falsy/nil to hide
--   defaultPosition = { corner = "bottom_left", priority = N }

function BO:InstallBaganatorIntegration()
    if not Baganator or not Baganator.API or not Baganator.API.RegisterCornerWidget then
        return  -- Baganator not installed/loaded — silent no-op
    end
    if self._baganatorInstalled then return end

    -- Reuse the same texture creation pattern. The widget Baganator gives
    -- us back from onInit IS our overlay; we just attach our existing
    -- color/show/hide logic to it.
    local function onInit(itemButton)
        -- Create a parented frame that Baganator can show/hide on the
        -- bottom-left corner. The widget itself is invisible; we paint
        -- two textures ON it (border + fill) using ensureOverlay.
        local widget = CreateFrame("Frame", nil, itemButton)
        widget:SetSize(MARKER_SIZE + 2, MARKER_SIZE + 2)

        local border = widget:CreateTexture(nil, LAYER, nil, SUB_LAYER)
        border:SetAllPoints()
        border:SetColorTexture(unpack(COLOR_BORDER))

        local fill = widget:CreateTexture(nil, LAYER, nil, SUB_LAYER + 1)
        fill:SetPoint("TOPLEFT", border, "TOPLEFT", 1, -1)
        fill:SetPoint("BOTTOMRIGHT", border, "BOTTOMRIGHT", -1, 1)
        fill:SetColorTexture(1, 1, 1, 1)  -- color set per-state in onUpdate

        widget.fill = fill
        return widget
    end

    -- onUpdate is called by Baganator with widget + details. We branch on
    -- the same state machine as paintButtonForItem but write directly to
    -- the widget's textures because Baganator owns the show/hide cycle.
    local function onUpdate(widget, details)
        if not ETB.Settings:Get("bagOverlay.enabled") then return false end
        if not details or not details.itemID then return false end
        if not ETB.CollectionAPI then return false end

        local state = ETB.CollectionAPI:GetState(details.itemID)
        -- v15.8.12: also flag KNOWN_OTHER_DIFFERENT_ARMOR (not usable on this armor type)
        if state ~= "NOT_COLLECTED" and state ~= "KNOWN_OTHER_DIFFERENT_ARMOR" then
            return false
        end

        local canLearn = ETB.CollectionAPI:CanLearn(details.itemID)
        local color
        if canLearn then
            color = COLOR_LEARN_ABLE
        elseif ETB.Settings:Get("bagOverlay.showUnlearnable") then
            color = COLOR_UNLEARN_ABLE
        else
            return false
        end

        widget.fill:SetVertexColor(color[1], color[2], color[3], color[4])
        return true  -- visible
    end

    local ok, err = pcall(Baganator.API.RegisterCornerWidget,
        "ETB Uncollected",      -- label shown in Baganator's customise UI
        "etb_uncollected",      -- stable id
        onUpdate, onInit,
        { corner = "bottom_left", priority = 5 }  -- placement hint
    )
    if ok then
        self._baganatorInstalled = true
    elseif ETB.Debug then
        ETB:Debug("BagOverlay", "Baganator integration failed: " .. tostring(err))
    end
end

-- =============================================================================
-- BAGNON INTEGRATION
-- =============================================================================
-- Bagnon has no public plugin API like Baganator. Third parties hook
-- Bagnon.Item:Update via hooksecurefunc — that's the standard pattern.
-- Bagnon.Item is the global ItemButton class (BagBrother/core/classes/item.lua);
-- ContainerItem inherits from it. Hooking the base class means every
-- bag/bank/guild-bank slot picks up our paint.
--
-- self.info.itemID is the resolved item; self is the button frame itself.

function BO:InstallBagnonIntegration()
    if not Bagnon or not Bagnon.Item or type(Bagnon.Item.Update) ~= "function" then
        return  -- Bagnon not installed/loaded — silent no-op
    end
    if self._bagnonInstalled then return end

    -- hooksecurefunc on a method-as-table-field: post-hook runs AFTER
    -- Bagnon's own Update, so self.info is already populated.
    local ok, err = pcall(function()
        hooksecurefunc(Bagnon.Item, "Update", function(self)
            local itemID = self.info and self.info.itemID or nil
            paintButtonForItem(self, itemID)
        end)
    end)

    if ok then
        self._bagnonInstalled = true
    elseif ETB.Debug then
        ETB:Debug("BagOverlay", "Bagnon integration failed: " .. tostring(err))
    end
end

-- =============================================================================
-- PUBLIC TOGGLE
-- =============================================================================
-- Lets /etb bagoverlay (or the eventual settings UI) flip the master toggle
-- without a /reload. Sets the saved-variable AND repaints immediately.

function BO:SetEnabled(enabled)
    ETB.Settings:Set("bagOverlay.enabled", enabled and true or false)
    repaintAll()
end

function BO:IsEnabled()
    return ETB.Settings:Get("bagOverlay.enabled") and true or false
end

function BO:Toggle()
    local cur = self:IsEnabled()
    self:SetEnabled(not cur)
    if ETB.Print then
        ETB:Print("Bag overlay: " .. (self:IsEnabled() and "|cff00ff00ON|r" or "|cffff6666OFF|r"))
    end
end

-- v15.8.0: public RepaintAll so subcommands (e.g. toggling .showUnlearnable)
-- can request an immediate redraw of visible bags without a /reload.
function BO:RepaintAll()
    repaintAll()
end
