-- =============================================================================
-- ETB v12.40.0 — TransmogUI/Preview.lua
-- =============================================================================
-- Hover preview frame: 200×300 DressUpModel shown next to the browser panel.
-- Extracted from the v12.39.0 monolithic TransmogUI.lua.
--
-- Public API:
--   Preview:Create()                    — one-time frame creation
--   Preview:Show(itemData, anchorFrame) — show preview for an item/set
--   Preview:Hide()                      — hide preview
--   Preview:GetFrame()                  — returns the underlying frame (for callers
--                                         that need to test visibility, etc.)
--
-- itemData fields consumed:
--   .itemID   — single item preview
--   .isSet + .items — set preview (items is array of itemIDs or tables w/ itemID)
--   .isHidden — skip preview (used for hidden-slot appearances)
--   .name     — title text
--   .source   — subtitle / source description
--
-- anchorFrame: the frame whose TOPRIGHT edge we anchor to. Passing this in
-- explicitly (rather than reading self.frame.browserPanel from TUI) removes
-- coupling to the parent's internal layout.
-- =============================================================================

local ADDON_NAME, NS = ...
local ETB = NS.ETB

NS.ETB.TransmogUI = NS.ETB.TransmogUI or {}
local Preview = {}
NS.ETB.TransmogUI.Preview = Preview

-- Internal state
Preview.frame = nil
Preview.currentItem = nil  -- used for the delayed-TryOn race check

-- =============================================================================
-- Frame creation (one-time)
-- =============================================================================

function Preview:Create()
    if self.frame then return self.frame end

    local f = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
    f:SetSize(200, 300)
    f:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 12,
        insets = { left = 3, right = 3, top = 3, bottom = 3 }
    })
    f:SetFrameStrata("TOOLTIP")
    f:SetFrameLevel(200)
    f:Hide()

    -- Title at top
    f.title = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    f.title:SetPoint("TOP", 0, -8)
    f.title:SetWidth(185)
    f.title:SetJustifyH("CENTER")
    f.title:SetMaxLines(1)

    -- Model frame
    local model = CreateFrame("DressUpModel", nil, f)
    model:SetSize(180, 240)
    model:SetPoint("TOP", 0, -26)
    model.zoomLevel = 0
    model:EnableMouse(true)
    model:EnableMouseWheel(true)

    -- v15.8.8: bind OnUpdate only while dragging
    local function onUpdateRotate(self)
        local x = GetCursorPosition()
        local diff = (x - (self.startX or x)) * 0.01
        self:SetFacing((self.startRotation or 0) + diff)
    end
    model:SetScript("OnMouseDown", function(self, button)
        if button == "LeftButton" then
            self.startX = GetCursorPosition()
            self.startRotation = self:GetFacing()
            self:SetScript("OnUpdate", onUpdateRotate)
        end
    end)
    model:SetScript("OnMouseUp", function(self)
        self:SetScript("OnUpdate", nil)
    end)
    model:SetScript("OnMouseWheel", function(self, delta)
        self.zoomLevel = math.max(-0.5, math.min(1.5, (self.zoomLevel or 0) + delta * 0.1))
        self:SetCamDistanceScale(1 - self.zoomLevel)
    end)

    model:SetPortraitZoom(0)
    model:SetCamDistanceScale(1.0)
    model:SetPosition(0, 0, -0.15)

    f.model = model
    model:SetUnit("player")

    -- v12.48.3: immediately undress the freshly-loaded player model. The
    -- SetUnit("player") above queues async asset loads for the player's
    -- equipped gear. Without this Undress, those assets settle onto the
    -- model and the first hover (or any hover where late asset loads
    -- finish) shows player gear bleeding through the requested set.
    --
    -- The frame is also hidden via :Hide() at the bottom of Create so
    -- the player never visually sees the equipped state.
    model:Undress()

    -- Source label at bottom
    f.label = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.label:SetPoint("BOTTOM", 0, 6)
    f.label:SetWidth(185)
    f.label:SetJustifyH("CENTER")
    f.label:SetTextColor(0.5, 0.5, 0.5)

    self.frame = f
    return f
end

-- Generation counter to invalidate stale callbacks. Bumped on every Show().
-- All scheduled TryOn callbacks check this before running.
Preview._showGeneration = 0

-- =============================================================================
-- Show / Hide
-- =============================================================================

function Preview:Show(itemData, anchorFrame)
    if not itemData then return end
    if not itemData.itemID and not (itemData.isSet and itemData.items) and not itemData.isHidden then return end
    if itemData.isHidden then return end  -- No preview for hidden slots

    -- Lazy-create on first show
    if not self.frame then self:Create() end
    local f = self.frame
    self.currentItem = itemData

    -- Bump generation to invalidate any in-flight callbacks from prior Show calls.
    -- v12.48.2: this replaces the simple `currentItem ~= captured` race check,
    -- which only protected against the FIRST stale callback but didn't guard
    -- against the staggered cascade of follow-up TryOn timers below.
    self._showGeneration = self._showGeneration + 1
    local thisGen = self._showGeneration

    -- Anchor to right edge of the passed-in anchor frame (typically the
    -- browser panel). Falls back to UIParent center if no anchor given.
    f:ClearAllPoints()
    if anchorFrame then
        f:SetPoint("TOPLEFT", anchorFrame, "TOPRIGHT", 4, 0)
    else
        f:SetPoint("CENTER", UIParent, "CENTER")
    end

    -- Reset model — restore the v12.34.0 PROVEN sequence.
    --
    -- v12.48.4: After 3 unsuccessful hotfix attempts (v12.48.1, v12.48.2,
    -- v12.48.3) targeting the leftover-armor symptom, comparison against the
    -- pre-modulation v12.34.0 source showed that the original code called
    -- `SetUnit("player") + Undress()` synchronously on EVERY Show — not just
    -- once at Create time. The original comment was explicit:
    --     "Reset model SYNCHRONOUSLY like prototype (SetUnit + Undress)"
    --     "ensures no TryOn remnants persist"
    --
    -- During modulation, SetUnit was moved out of Show into Create (one-time)
    -- as an apparent optimization to avoid GPU mesh reload cost. That broke
    -- the proven reset behavior — `Undress()` alone does NOT reliably strip
    -- all TryOn'd items in MoP Classic 5.4.8; the full SetUnit + Undress
    -- combo is what actually clears state.
    --
    -- The cost of repeated SetUnit on the SMALL preview frame (180×240) is
    -- low and acceptable. The main TransmogUI model (380×280) is bigger so
    -- still uses the once-only pattern there for performance reasons.
    f.model:SetUnit("player")
    f.model:Undress()
    f.model:SetRotation(0)
    f.model:SetPosition(0, 0, -0.15)

    -- Title
    if itemData.isSet then
        f.title:SetText("|cffffd100" .. (itemData.name or "Set") .. "|r")
    else
        f.title:SetText(itemData.name or "Preview")
    end

    -- Source label
    f.label:SetText(itemData.source and ("|cff666666" .. itemData.source .. "|r") or "")

    f:Show()

    -- Build the list of itemIDs to TryOn
    local itemIDs = {}
    if itemData.itemID then
        itemIDs[1] = itemData.itemID
    elseif itemData.isSet and itemData.items then
        for _, itemEntry in pairs(itemData.items) do
            local itemID
            if type(itemEntry) == "number" then
                itemID = itemEntry
            elseif type(itemEntry) == "table" then
                itemID = itemEntry.itemID or itemEntry[1]
            end
            if itemID and type(itemID) == "number" then
                itemIDs[#itemIDs + 1] = itemID
            end
        end
    end

    if #itemIDs == 0 then return end

    -- v12.48.4: restored simple v12.34.0 TryOn pattern. The original used a
    -- single C_Timer.After(0.1, ...) callback that ran the TryOn loop
    -- synchronously. We keep the generation-counter check (more robust than
    -- the original's `currentItem ~= captured`) but drop the v12.48.2
    -- staggered-TryOn cascade and re-apply pass — those weren't needed in
    -- the original and added complexity that probably caused timing issues
    -- of their own.
    local self_ = self
    local model = f.model

    C_Timer.After(0.1, function()
        if self_._showGeneration ~= thisGen then return end
        if not f:IsShown() then return end
        for _, itemID in ipairs(itemIDs) do
            pcall(function() model:TryOn("item:" .. itemID) end)
        end
    end)
end

function Preview:Hide()
    self.currentItem = nil  -- clear for legacy callers
    -- Bump generation so any in-flight TryOn callbacks bail out.
    self._showGeneration = self._showGeneration + 1
    if self.frame then
        self.frame:Hide()
    end
end

function Preview:GetFrame()
    return self.frame
end
