--[[
============================================================================
UI/TransmogUI/Blizzard.lua  (added in v15.0.0 — Audit Batch 3 Part 4 — FINAL)
============================================================================

Blizzard transmog frame interop and pending-state synchronization for the
TransmogUI orchestrator. Owner-pattern: each method takes `owner` (the TUI
orchestrator) as first argument. The orchestrator keeps thin delegator
methods so existing call sites keep working.

This is the FINAL extraction in the v14.9 → v15.0 architectural arc.
After this file is loaded, TransmogUI.lua is purely:
  - Top-level constants and namespace setup
  - The TUI table declaration with field defaults
  - Initialize / Show / Hide / Toggle lifecycle (orchestration)
  - 29+ thin delegators dispatching to the 9 submodules
  - Some glue helpers (CategoryToSlot, GetCachedItem) used cross-submodule
  - Slash command registration
  - StaticPopupDialogs definitions (must be at file scope per Blizzard taint
    rules — popups read TUI._pendingCapture etc. via file-local TUI which
    IS the orchestrator at runtime)

Methods owned here (9):
  HideBlizzardFrame         — scale + reposition the Blizzard frame to
                              "invisible" without taint-causing API calls
  ShowBlizzardFrame         — restore from saved scale + position state
  HookBlizzardTransmogFrame — no-op shim (kept for callers that reference it)
  SwitchToBlizzardUI        — sync ETB pending → Blizzard, hide ETB UI,
                              restore Blizzard frame, show floating button
  SyncFromBlizzardPending   — read Blizzard's pending transmog state and
                              mirror it into ETB's pendingTransmogs
  ClearBlizzardPending      — clear ALL Blizzard pending slots; uses
                              ClearAllPending if available, else per-slot
  ClearSlotPending          — clear a single Blizzard slot using best-
                              available API (ClearPending OR SetPending(Revert))
  SyncToBlizzardPending     — push ALL ETB pending → Blizzard's SetPending,
                              and CLEAR Blizzard slots that ETB doesn't have
                              pending (prevents stale set remnants)
  PushSlotToBlizzard        — single-slot variant called after each user
                              interaction (SelectAppearance, ToggleSlotHide)

Methods kept on TUI itself:
  GetBlizzardSetItems       — Sets-cluster method, NOT moved here despite
                              having "Blizzard" in the name. Used only by
                              Sets.lua (5 call sites); moving it here would
                              create cross-submodule dispatch for no benefit.
                              Stays in TransmogUI.lua as a delegator-target.
  ImportBlizzardOutfits     — already a thin delegator to Outfits:ImportFromBlizzard
  GetBlizzardOutfitItems    — already a thin delegator to Outfits:GetBlizzardItems

Closure / shadowing notes:
  - All 9 methods are pure synchronous logic. No C_Timer.After callbacks,
    no frame scripts, no `function(self,...)` patterns.
  - Verified by closure-shadowing audit in test_v15_0_extraction.lua.
  - The TRANSMOG_TYPE_APPEARANCE constant is the ONLY non-`owner.X` upvalue
    referenced — declared as a file-local below with the same fallback the
    original code used (LE_TRANSMOG_TYPE_APPEARANCE or 1).

External dependencies pulled from owner:
  owner._hiddenBlizzState  — saved-state table for HideBlizzardFrame
  owner.frame              — for FrameStrata adjustments
  owner.floatingButton     — for SwitchToBlizzardUI re-show
  owner.blizzardTransmogFrame  — captured Blizzard frame reference
  owner.pendingTransmogs   — ETB's pending state, sync target
  owner.itemIDToSourceID   — source cache, populated by SyncFromBlizzardPending
  owner._pushingToBlizzard — re-entrancy guard
  owner._pullingFromBlizzard  — re-entrancy guard for the other direction
  owner._applyingSet       — guard from Apply, used by SyncFromBlizzardPending
  owner.SLOTS              — slot iteration data
  owner:GetTransmogSlotID  — invSlot -> transmog slot ID
  owner:CallSetPending     — three-way SetPending wrapper
  owner:CallSetPendingHidden — hidden-slot SetPending wrapper
  owner:GetCollectedSourceForItem — sourceID lookup
  owner:CanApplyTransmog   — NPC proximity check
============================================================================
]]--
local ADDON_NAME, NS = ...
local ETB = NS.ETB
NS.ETB.TransmogUI = NS.ETB.TransmogUI or {}
NS.ETB.TransmogUI.Blizzard = NS.ETB.TransmogUI.Blizzard or {}
local Blizzard = NS.ETB.TransmogUI.Blizzard

-- =============================================================================
-- FILE-LOCAL CONSTANTS
-- =============================================================================
-- Preserves the original line-43 semantics from TransmogUI.lua: in MoP Classic
-- the LE_TRANSMOG_TYPE_APPEARANCE global may be defined or not, depending on
-- client variant. Falls back to 1 (the documented value for Apply pending type
-- when the Enum constant is absent).
local TRANSMOG_TYPE_APPEARANCE = (LE_TRANSMOG_TYPE_APPEARANCE ~= nil) and LE_TRANSMOG_TYPE_APPEARANCE or 1

-- =============================================================================
-- METHODS (extracted from UI/TransmogUI.lua in v15.0.0)
-- =============================================================================

function Blizzard:HideBlizzardFrame(owner, frame)
    if not frame then return end
    
    wipe(owner._hiddenBlizzState)
    
    -- Only operate on the top-level frame passed to us
    -- SetScale cascades to ALL children automatically — no need to iterate 15 frames
    local ok, point, relativeTo, relativePoint, xOfs, yOfs = pcall(frame.GetPoint, frame, 1)
    if ok then
        owner._hiddenBlizzState[1] = {
            frame = frame,
            scale = frame:GetScale(),
            alpha = frame:GetAlpha(),
            point = point,
            relativeTo = relativeTo,
            relativePoint = relativePoint,
            xOfs = xOfs,
            yOfs = yOfs,
        }
    end
    
    -- SetScale cascades to ALL children — makes frame sub-pixel invisible
    pcall(frame.SetScale, frame, 0.001)
    pcall(frame.SetAlpha, frame, 0)
    pcall(frame.ClearAllPoints, frame)
    pcall(frame.SetPoint, frame, "TOPLEFT", UIParent, "TOPLEFT", -5000, 5000)
    
    -- Raise our frame above everything
    if owner.frame then
        owner.frame:SetFrameStrata("FULLSCREEN")
        owner.frame:SetFrameLevel(100)
    end
end

-- Restore ALL Blizzard transmog frames to their original state
function Blizzard:ShowBlizzardFrame(owner, frame)
    for _, entry in ipairs(owner._hiddenBlizzState) do
        local f = entry.frame
        if f and f.SetScale then
            pcall(f.SetScale, f, entry.scale or 1)
            pcall(f.SetAlpha, f, entry.alpha or 1)
            pcall(f.ClearAllPoints, f)
            if entry.point then
                pcall(f.SetPoint, f, entry.point, entry.relativeTo or UIParent, entry.relativePoint or entry.point, entry.xOfs or 0, entry.yOfs or 0)
            else
                pcall(f.SetPoint, f, "CENTER", UIParent, "CENTER", 0, 0)
            end
            -- NO EnableMouse — restoring scale/position is sufficient,
            -- and calling EnableMouse on Blizzard frames taints secure input
        end
    end
    wipe(owner._hiddenBlizzState)
    
    -- Reset our own frame strata back to DIALOG
    if owner.frame then
        owner.frame:SetFrameStrata("DIALOG")
        owner.frame:SetFrameLevel(100)
    end
end

function Blizzard:HookBlizzardTransmogFrame(owner)
    -- No longer needed - we use autoOpen instead
    -- Kept as empty function in case other code calls it
end

function Blizzard:SwitchToBlizzardUI(owner)
    -- Push all ETB pending to Blizzard BEFORE switching, so nothing is lost
    owner:SyncToBlizzardPending()
    
    owner:Hide()
    -- Only show floating button if we have a Blizzard frame we hid (meaning we're at NPC)
    local hasBlizzFrame = owner.blizzardTransmogFrame ~= nil
    if owner.blizzardTransmogFrame then
        owner:ShowBlizzardFrame(owner.blizzardTransmogFrame)
        owner.blizzardTransmogFrame = nil
    elseif _G.TransmogrifyFrame and _G.TransmogrifyFrame:IsShown() then
        owner:ShowBlizzardFrame(_G.TransmogrifyFrame)
        hasBlizzFrame = true
    end
    -- Re-show floating button only if we restored a Blizzard frame (at NPC)
    if hasBlizzFrame and owner.floatingButton then
        owner.floatingButton:Show()
    end
end

function Blizzard:SyncFromBlizzardPending(owner)
    if not C_Transmog or not C_Transmog.GetPending then return false end
    local synced = 0
    
    -- Detect ToggleOff type for hidden slot detection
    local PENDING_APPLY = (Enum and Enum.TransmogPendingType and Enum.TransmogPendingType.Apply) or 0
    local PENDING_TOGGLEOFF = (Enum and Enum.TransmogPendingType and Enum.TransmogPendingType.ToggleOff) or 1
    
    for _, slot in ipairs(owner.SLOTS) do
        local slotID = owner:GetTransmogSlotID(slot.invSlot)
        local sourceID = nil
        local isHidden = false
        local hasPending = false
        
        -- Method 1: TransmogLocation-based API (MoP Classic native)
        if TransmogUtil and TransmogUtil.CreateTransmogLocation then
            local lok, transmogLocation = pcall(TransmogUtil.CreateTransmogLocation, slotID, 0, 0)
            if lok and transmogLocation then
                local ok, pending = pcall(C_Transmog.GetPending, transmogLocation)
                if ok and pending then
                    if type(pending) == "table" then
                        -- Check pending type: ToggleOff = hidden slot
                        local pendingType = pending.type
                        if pendingType == PENDING_TOGGLEOFF then
                            isHidden = true
                            hasPending = true
                        elseif pendingType == PENDING_APPLY or pendingType == nil then
                            sourceID = pending.transmogID or pending.sourceID
                            if sourceID and sourceID > 0 then
                                hasPending = true
                            end
                        end
                        -- If type is Revert or nil with no sourceID, slot has no pending
                    elseif type(pending) == "number" and pending > 0 then
                        sourceID = pending
                        hasPending = true
                    end
                end
            end
        end
        
        -- Method 2: Legacy 2-arg API fallback (only if Method 1 didn't find anything)
        if not hasPending then
            local ok, pending = pcall(C_Transmog.GetPending, slotID, TRANSMOG_TYPE_APPEARANCE)
            if ok and pending then
                if type(pending) == "table" then
                    local pendingType = pending.type
                    if pendingType == PENDING_TOGGLEOFF then
                        isHidden = true
                        hasPending = true
                    else
                        sourceID = pending.sourceID or pending.transmogID
                        if sourceID and sourceID > 0 then
                            hasPending = true
                        end
                    end
                elseif type(pending) == "number" and pending > 0 then
                    sourceID = pending
                    hasPending = true
                end
            end
        end
        
        if isHidden then
            -- Blizzard has this slot set to HIDDEN — create matching ETB entry
            owner.pendingTransmogs[slot.invSlot] = {
                isHiddenSlot = true,
                name = "Hidden",
                icon = "Interface\\Icons\\Spell_Shadow_SacrificialShield",
            }
            synced = synced + 1
        elseif sourceID and sourceID > 0 and C_TransmogCollection then
            -- Blizzard has a normal appearance pending
            local si = C_TransmogCollection.GetSourceInfo(sourceID)
            if si and si.itemID then
                local name, _, _, _, _, _, _, _, _, icon = GetItemInfo(si.itemID)
                owner.pendingTransmogs[slot.invSlot] = {
                    sourceID = sourceID, itemID = si.itemID,
                    name = name or ("Item " .. si.itemID), icon = icon,
                }
                owner.itemIDToSourceID[si.itemID] = sourceID
                synced = synced + 1
            end
        else
            -- Blizzard has NO pending for this slot — clear from ETB too
            if owner.pendingTransmogs[slot.invSlot] and not owner._applyingSet then
                owner.pendingTransmogs[slot.invSlot] = nil
            end
        end
    end
    return synced > 0
end

-- Clear ALL Blizzard pending transmog slots so old set pieces don't leak through
function Blizzard:ClearBlizzardPending(owner)
    if not C_Transmog then return end
    
    -- Method 1: ClearAllPending if available (retail-style)
    if C_Transmog.ClearAllPending then
        pcall(C_Transmog.ClearAllPending)
        return
    end
    
    -- Method 2+3: Per-slot clearing using best available API
    for _, slot in ipairs(owner.SLOTS) do
        local slotID = owner:GetTransmogSlotID(slot.invSlot)
        if slotID then
            owner:ClearSlotPending(slotID)
        end
    end
end

-- Clear Blizzard pending for a single slot — tries all available methods
function Blizzard:ClearSlotPending(owner, slotID)
    if not C_Transmog then return end
    
    -- Try ClearPending with TransmogLocation first
    if C_Transmog.ClearPending then
        if TransmogUtil and TransmogUtil.CreateTransmogLocation then
            local ok, loc = pcall(TransmogUtil.CreateTransmogLocation, slotID, 0, 0)
            if ok and loc then
                pcall(C_Transmog.ClearPending, loc)
                return
            end
        end
        -- Fallback: plain slotID
        pcall(C_Transmog.ClearPending, slotID)
        return
    end
    
    -- No ClearPending — try SetPending with Revert type
    if C_Transmog.SetPending and TransmogUtil
       and TransmogUtil.CreateTransmogLocation and TransmogUtil.CreateTransmogPendingInfo then
        local noneType = (Enum and Enum.TransmogPendingType and Enum.TransmogPendingType.Revert) or 2
        local lok, loc = pcall(TransmogUtil.CreateTransmogLocation, slotID, 0, 0)
        if lok and loc then
            local pok, pendingInfo = pcall(TransmogUtil.CreateTransmogPendingInfo, noneType)
            if pok and pendingInfo then
                pcall(C_Transmog.SetPending, loc, pendingInfo)
            end
        end
    end
end

-- Push ALL ETB pending transmogs to Blizzard's C_Transmog.SetPending
-- so Blizzard UI stays in sync with what ETB has queued.
-- Also CLEARS Blizzard pending for slots ETB doesn't have pending —
-- this prevents old set remnants from persisting after switching sets.
function Blizzard:SyncToBlizzardPending(owner)
    if not owner:CanApplyTransmog() then return false end
    if not C_Transmog or not C_Transmog.SetPending then return false end
    
    owner._pushingToBlizzard = true
    local synced = 0
    
    -- Track which slots have ETB pending
    local slotsWithPending = {}
    
    for invSlot, pending in pairs(owner.pendingTransmogs) do
        local slotID = owner:GetTransmogSlotID(invSlot)
        if slotID then
            slotsWithPending[invSlot] = true
            
            if pending.isHiddenSlot then
                -- Hidden slot: use ToggleOff pending type
                local ok, result = owner:CallSetPendingHidden(slotID)
                if ok then
                    synced = synced + 1
                end
            else
                -- Normal appearance: use Apply with sourceID
                local sourceID = pending.sourceID
                if not sourceID and pending.itemID then
                    sourceID = owner:GetCollectedSourceForItem(pending.itemID)
                end
                if sourceID then
                    local ok, result = owner:CallSetPending(slotID, sourceID)
                    if ok then
                        synced = synced + 1
                    end
                end
            end
        end
    end
    
    -- Clear Blizzard pending for slots that ETB does NOT have pending
    -- This is critical: when switching from a full set to a partial set,
    -- slots not in the new set must be explicitly cleared from Blizzard
    for _, slot in ipairs(owner.SLOTS) do
        if not slotsWithPending[slot.invSlot] then
            local slotID = owner:GetTransmogSlotID(slot.invSlot)
            if slotID then
                owner:ClearSlotPending(slotID)
            end
        end
    end
    
    owner._pushingToBlizzard = nil
    ETB:Debug("TransmogUI", string.format("SyncToBlizzard: pushed %d pending to Blizzard API", synced))
    return synced > 0
end

-- Push a single slot to Blizzard (called right after SelectAppearance or ToggleSlotHide)
function Blizzard:PushSlotToBlizzard(owner, invSlot)
    if not owner:CanApplyTransmog() then return end
    if owner._pullingFromBlizzard then return end  -- Don't push back what we just pulled
    
    local slotID = owner:GetTransmogSlotID(invSlot)
    if not slotID then return end
    
    local pending = owner.pendingTransmogs[invSlot]
    
    owner._pushingToBlizzard = true
    
    if not pending then
        -- Slot was cleared in ETB — clear Blizzard pending too
        owner:ClearSlotPending(slotID)
    elseif pending.isHiddenSlot then
        -- Hidden slot: use ToggleOff pending type
        owner:CallSetPendingHidden(slotID)
    else
        -- Normal appearance: use Apply with sourceID
        local sourceID = pending.sourceID
        if not sourceID and pending.itemID then
            sourceID = owner:GetCollectedSourceForItem(pending.itemID)
        end
        if sourceID then
            owner:CallSetPending(slotID, sourceID)
        end
    end
    
    owner._pushingToBlizzard = nil
end

