--[[
============================================================================
CollectionToast.lua  (added in v14.6.0, Tier 2 Feature 1.7)
============================================================================

A toast notification that flashes briefly when the user collects a new
appearance. Subscribes to ETB_ITEM_COLLECTED (fired from CollectionHistory).

Design constraints:
  - Subtle, not invasive. Plays in the upper-right of the screen by default,
    fades in/out quickly, click to dismiss.
  - Must never block gameplay — strata is HIGH, never DIALOG/FULLSCREEN.
  - Stack: if multiple collections happen rapidly (e.g. opening a bag of
    quest-reward gear), queue them and play sequentially.
  - Settings-aware: a kill-switch lets users disable toasts entirely.

Why a separate module:
  - Self-contained UI surface. Nothing else needs the toast frame.
  - Subscription + frame creation + queue management is genuinely its own
    responsibility, distinct from the data layer (CollectionHistory).
============================================================================
]]--
local ADDON_NAME, NS = ...
local ETB = NS.ETB
NS.ETB.CollectionToast = NS.ETB.CollectionToast or {}
local CT = NS.ETB.CollectionToast

CT.queue = CT.queue or {}
CT._isShowing = false

-- Toast lifecycle (in seconds):
--   FADE_IN_DURATION   -- alpha 0 -> 1
--   HOLD_DURATION      -- visible at full alpha
--   FADE_OUT_DURATION  -- alpha 1 -> 0
local FADE_IN_DURATION = 0.3
local HOLD_DURATION = 3.0
local FADE_OUT_DURATION = 0.6

-- =============================================================================
-- INIT
-- =============================================================================

function CT:Initialize()
    if not ETB.EventBus then return end
    if self._initialized then return end

    -- Subscribe to collection events. CollectionHistory fires this with
    -- v15.8.1: EventBus:Fire passes the event name as the first arg to every
    -- callback (see EventBus.lua line 116: pcall(entry.fn, event, ...)).
    -- The old registration `function(itemID, record)` was mis-positioned —
    -- itemID was receiving the string "ETB_ITEM_COLLECTED" and the real
    -- itemID was landing in `record`. That caused the toast to show
    -- "Item ETB_ITEM_COLLECTED" + question-mark icon. Correct signature
    -- starts with `event`.
    ETB.EventBus:Register("ETB_ITEM_COLLECTED", function(event, itemID, record)
        CT:Enqueue(itemID, record)
    end)
    -- v15.8.16: subscribe to set-completion events fired by the new
    -- SetCompletion module. Toast appearance is unified between Blizzard
    -- and community sets (per Q1).
    ETB.EventBus:Register("ETB_SET_COMPLETED", function(event, setKey, setRecord, triggerItemID)
        CT:EnqueueSet(setKey, setRecord, triggerItemID)
    end)
    self._initialized = true
end

-- Settings: respect a per-user toggle. Default ON.
function CT:IsEnabled()
    if ETB.Settings and ETB.Settings.Get then
        local v = ETB.Settings:Get("notifications.collectionToast")
        if v ~= nil then return v end
    end
    return true
end

-- v15.8.16: separate toggle for set-completion toasts. Default ON.
-- Users who like per-item toasts but not set toasts (or vice versa) can
-- toggle independently.
function CT:IsSetToastEnabled()
    if ETB.Settings and ETB.Settings.Get then
        local v = ETB.Settings:Get("notifications.setCompletionToast")
        if v ~= nil then return v end
    end
    return true
end

-- =============================================================================
-- QUEUE
-- =============================================================================

function CT:Enqueue(itemID, record)
    if not self:IsEnabled() then return end
    if not itemID then return end

    -- v14.10.1 fix: record IS the unix timestamp number (CollectionHistory
    -- stores db[itemID] = time() and fires the event passing that value
    -- directly). Earlier code did `record and record.at` assuming record was
    -- a table with an .at field, which crashed with "attempt to index local
    -- 'record' (a number value)" because Lua refuses to index a number.
    -- record may also be nil (e.g. event fired with no payload), in which
    -- case fall back to time().
    local at = (type(record) == "number") and record or time()
    self.queue[#self.queue + 1] = { kind = "item", itemID = itemID, at = at }
    if not self._isShowing then self:PlayNext() end
end

-- v15.8.16: Set-completion queue entry. setRecord is the table from
-- SetCompletion._setRegistry: { name=, kind=, pieces={...}, blzSetID= }.
-- triggerItemID is the item whose collection completed the set — used for
-- the icon fallback so the toast has SOMETHING to show even before the
-- set's representative icon is known.
function CT:EnqueueSet(setKey, setRecord, triggerItemID)
    if not self:IsSetToastEnabled() then return end
    if not setKey or not setRecord then return end
    self.queue[#self.queue + 1] = {
        kind = "set",
        setKey = setKey,
        setRecord = setRecord,
        triggerItemID = triggerItemID,
        at = time(),
    }
    if not self._isShowing then self:PlayNext() end
end

function CT:PlayNext()
    if #self.queue == 0 then
        self._isShowing = false
        return
    end
    self._isShowing = true
    local entry = table.remove(self.queue, 1)
    if entry.kind == "set" then
        self:ShowSet(entry.setKey, entry.setRecord, entry.triggerItemID)
    else
        self:Show(entry.itemID)
    end
end

-- =============================================================================
-- FRAME
-- =============================================================================

function CT:CreateFrame()
    if self.frame then return self.frame end

    local f = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
    f:SetSize(280, 56)
    f:SetPoint("TOPRIGHT", -32, -120)
    f:SetFrameStrata("HIGH")
    f:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 12,
        insets = { left = 4, right = 4, top = 4, bottom = 4 },
    })
    f:EnableMouse(true)
    f:SetScript("OnMouseDown", function(self)
        -- Click to dismiss
        CT:HideNow()
    end)
    f:Hide()
    f:SetAlpha(0)

    f.icon = f:CreateTexture(nil, "ARTWORK")
    f.icon:SetSize(40, 40)
    f.icon:SetPoint("LEFT", 8, 0)
    f.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    f.heading = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.heading:SetPoint("TOPLEFT", f.icon, "TOPRIGHT", 8, -2)
    f.heading:SetText("|cffffd100New appearance!|r")

    f.itemName = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    f.itemName:SetPoint("TOPLEFT", f.icon, "TOPRIGHT", 8, -18)
    f.itemName:SetWidth(220)
    f.itemName:SetJustifyH("LEFT")
    f.itemName:SetWordWrap(false)

    f.tinyHint = f:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
    f.tinyHint:SetPoint("BOTTOMLEFT", f.icon, "BOTTOMRIGHT", 8, 2)
    f.tinyHint:SetText("|cff666666(click to dismiss)|r")

    self.frame = f
    return f
end

function CT:Show(itemID)
    if not itemID then self:PlayNext(); return end
    local f = self:CreateFrame()

    -- v15.7.6: icon source priority.
    --   1. GetItemIcon(itemID) — synchronous on MoP 5.4.8 (iconID is static
    --      client data, always available immediately).
    --   2. GetItemInfo() 10th return — only available if the item's full
    --      info has loaded this session.
    --   3. Question-mark icon — last-resort fallback (formerly the only
    --      path when GetItemInfo returned nil on first access).
    -- The old code went straight to step 3 for any item not in the
    -- client's cache, causing the toast to always show a question mark
    -- for unfamiliar items.
    local name, _, _, _, _, _, _, _, _, infoIcon = GetItemInfo(itemID)
    local icon = (GetItemIcon and GetItemIcon(itemID)) or infoIcon
    f.itemName:SetText(name or ("Item " .. itemID))
    f.icon:SetTexture(icon or "Interface\\Icons\\INV_Misc_QuestionMark")

    -- v15.7.6: if the name is still loading (icon is now reliable), arm a
    -- one-shot GET_ITEM_INFO_RECEIVED listener so the name updates as soon
    -- as the client resolves it. The toast holds for ~3s so there's almost
    -- always time for the round-trip to complete.
    if not name then
        f._waitingItemID = itemID
        if not f._infoListener then
            f._infoListener = CreateFrame("Frame")
            f._infoListener:SetScript("OnEvent", function(_, _, evtItemID)
                if evtItemID == f._waitingItemID and f:IsShown() then
                    local resolvedName = GetItemInfo(evtItemID)
                    if resolvedName then
                        f.itemName:SetText(resolvedName)
                        f._waitingItemID = nil
                    end
                end
            end)
        end
        f._infoListener:RegisterEvent("GET_ITEM_INFO_RECEIVED")
    elseif f._infoListener then
        -- Already-known name path; tear down any listener from a prior toast
        f._infoListener:UnregisterAllEvents()
        f._waitingItemID = nil
    end

    f:Show()
    -- Animate alpha via OnUpdate state machine
    f._phase = "fadein"
    f._phaseElapsed = 0
    f:SetScript("OnUpdate", function(self, elapsed)
        self._phaseElapsed = self._phaseElapsed + elapsed
        if self._phase == "fadein" then
            local p = math.min(1, self._phaseElapsed / FADE_IN_DURATION)
            self:SetAlpha(p)
            if p >= 1 then
                self._phase = "hold"; self._phaseElapsed = 0
            end
        elseif self._phase == "hold" then
            if self._phaseElapsed >= HOLD_DURATION then
                self._phase = "fadeout"; self._phaseElapsed = 0
            end
        elseif self._phase == "fadeout" then
            local p = math.min(1, self._phaseElapsed / FADE_OUT_DURATION)
            self:SetAlpha(1 - p)
            if p >= 1 then
                self:SetScript("OnUpdate", nil)
                self:Hide()
                -- v15.7.6: tear down the info listener when the toast hides;
                -- next toast will re-register if it needs the event.
                if self._infoListener then
                    self._infoListener:UnregisterAllEvents()
                    self._waitingItemID = nil
                end
                CT:PlayNext()
            end
        end
    end)
end

-- v15.8.16: Set-completion toast. Reuses the same frame as the item toast
-- but with set-flavored content: "Set complete!" heading + set name +
-- piece-count subline. Icon comes from the trigger item (the piece whose
-- collection completed the set) — that's the most recent visual the user
-- did something to acquire, so it's contextually meaningful.
--
-- Hold duration is intentionally a little longer for set toasts since
-- completing a set is a bigger deal than collecting one piece.
local SET_HOLD_DURATION = 4.5

function CT:ShowSet(setKey, setRecord, triggerItemID)
    if not setKey or not setRecord then self:PlayNext(); return end
    local f = self:CreateFrame()

    -- Icon: prefer the trigger item's icon. Fall back to a generic
    -- achievement icon if no trigger item.
    local icon = nil
    if triggerItemID then
        icon = (GetItemIcon and GetItemIcon(triggerItemID)) or nil
        if not icon then
            local _, _, _, _, _, _, _, _, _, infoIcon = GetItemInfo(triggerItemID)
            icon = infoIcon
        end
    end
    f.icon:SetTexture(icon or "Interface\\Icons\\Achievement_GuildPerk_HonorableMention")

    -- Heading + name. Yellow "Set complete!" makes it visually distinct
    -- from the gold "New appearance!" item-toast heading without needing
    -- a different frame template.
    f.heading:SetText("|cff00ffff★ Set complete!|r")
    f.itemName:SetText(setRecord.name or "?")

    -- Tiny hint shows piece count + kind tag so users can tell at a glance
    -- whether the set was Blizzard's (PvP/tier/dungeon) or our community
    -- recolor data.
    local kindTag = (setRecord.kind == "community") and "community" or "Blizzard"
    f.tinyHint:SetText(string.format("|cff888888%d pieces · %s|r",
        #(setRecord.pieces or {}), kindTag))

    -- v15.8.16: tear down any item-toast async-name listener that might
    -- still be armed from a prior item toast. Set toasts don't need
    -- async name resolution (set names are static strings from data).
    if f._infoListener then
        f._infoListener:UnregisterAllEvents()
        f._waitingItemID = nil
    end

    f:Show()
    -- Animate alpha via OnUpdate state machine (same as Show, but with the
    -- longer SET_HOLD_DURATION).
    f._phase = "fadein"
    f._phaseElapsed = 0
    f:SetScript("OnUpdate", function(self, elapsed)
        self._phaseElapsed = self._phaseElapsed + elapsed
        if self._phase == "fadein" then
            local p = math.min(1, self._phaseElapsed / FADE_IN_DURATION)
            self:SetAlpha(p)
            if p >= 1 then self._phase = "hold"; self._phaseElapsed = 0 end
        elseif self._phase == "hold" then
            if self._phaseElapsed >= SET_HOLD_DURATION then
                self._phase = "fadeout"; self._phaseElapsed = 0
            end
        elseif self._phase == "fadeout" then
            local p = math.min(1, self._phaseElapsed / FADE_OUT_DURATION)
            self:SetAlpha(1 - p)
            if p >= 1 then
                self:SetScript("OnUpdate", nil)
                self:Hide()
                -- Reset heading to default for the next item toast so we
                -- don't leak "Set complete!" into a subsequent item toast
                -- if the queue has mixed entries.
                f.heading:SetText("|cffffd100New appearance!|r")
                f.tinyHint:SetText("|cff666666(click to dismiss)|r")
                CT:PlayNext()
            end
        end
    end)
end

function CT:HideNow()
    if not self.frame then return end
    self.frame:SetScript("OnUpdate", nil)
    self.frame:Hide()
    self.frame:SetAlpha(0)
    self:PlayNext()
end
