-- =============================================================================
-- ETB — UI/MainFrame/ModelGrid.lua
-- =============================================================================
-- Per-cell 3D model view for the appearance browser, mimicking Blizzard's
-- native Appearances grid: armor framed on the player body, weapons floating.
-- Toggled from the top bar; shares the existing pagination + filtered-item
-- pipeline (owner.items / owner.currentPage). The hover preview is untouched —
-- cells reuse ETB.ModelPreview on enter and ETB.ItemRow:OnClick on click.
--
-- Apply path (native-fidelity, patch 5.5.4+): each cell is a plain DressUpModel
-- driven exactly like Blizzard's wardrobe item cells — model:SetItemAppearance(
-- visualID) loads the appearance on Blizzard's neutral preview model, and the
-- appearance's own UI camera (C_TransmogCollection.GetAppearanceCameraID +
-- Model_ApplyUICamera) frames it: per-slot crop for armor, rear view for cloaks,
-- bodiless float for weapons. Earlier "blank" cells came from pairing
-- SetItemAppearance with a wrong (harvested) camera that threw the model
-- off-screen — applying the appearance's real camera is the missing piece. A
-- TryOn + SetPortraitZoom fallback covers items with no resolvable appearance ID.
--
-- Public API:
--   ModelGrid:Init(owner, grid)   — build the grid frame + toggle (once)
--   ModelGrid:Toggle(owner)       — flip list <-> model
--   ModelGrid:Apply(owner)        — show/hide per owner.viewMode
--   ModelGrid:Render(owner)       — lay out + apply the current page
-- =============================================================================

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

NS.ETB.MainFrame = NS.ETB.MainFrame or ETB.MainFrame or {}
local ModelGrid = {}
NS.ETB.MainFrame.ModelGrid = ModelGrid

local COLS = 5
ModelGrid.COLS = COLS  -- exposed so arrow-nav knows the grid's row stride
local PAD = 6

-- =============================================================================
-- SETUP
-- =============================================================================

function ModelGrid:Init(owner, grid)
    if owner._modelGridInit then return end
    owner._modelGridInit = true
    owner.viewMode = owner.viewMode or "list"

    -- Container over the item-grid area.
    local mg = CreateFrame("Frame", nil, grid)
    mg:SetPoint("TOPLEFT", 4, -18)
    mg:SetPoint("BOTTOMRIGHT", -4, 4)
    mg:Hide()
    mg.cells = {}
    owner.modelGridFrame = mg

    -- Mouse wheel = page nav (mirror the row grid's behavior).
    mg:EnableMouseWheel(true)
    mg:SetScript("OnMouseWheel", function(_, delta) ModelGrid:OnWheel(owner, delta) end)

    -- Toggle button in the top bar, left of the sort dropdown.
    local host = owner.frame and owner.frame.topBar
    local btn = CreateFrame("Button", nil, host or grid, "UIPanelButtonTemplate")
    btn:SetSize(74, 22)
    if host and owner.frame.sortDropdown then
        btn:SetPoint("TOPRIGHT", owner.frame.sortDropdown, "TOPLEFT", -8, 0)
    elseif host then
        btn:SetPoint("TOPRIGHT", -8, -4)
    else
        btn:SetPoint("TOPRIGHT", grid, "TOPRIGHT", -6, -2)
    end
    btn:SetText("3D View")
    btn:SetScript("OnClick", function() ModelGrid:Toggle(owner) end)
    owner.modelToggleBtn = btn
end

function ModelGrid:OnWheel(owner, delta)
    if delta > 0 then
        if owner.currentPage > 1 then
            owner.currentPage = owner.currentPage - 1
            owner:LoadPageData(owner.currentPage)
            owner:UpdateDisplay()
            owner:UpdateStats()
        end
    else
        local totalPages = math.max(1, math.ceil((owner.totalItemCount or 0) / (owner.itemsPerPage or 19)))
        if owner.currentPage < totalPages then
            owner.currentPage = owner.currentPage + 1
            owner:LoadPageData(owner.currentPage)
            owner:UpdateDisplay()
            owner:UpdateStats()
        end
    end
end

-- =============================================================================
-- TOGGLE / SHOW-HIDE
-- =============================================================================

function ModelGrid:Toggle(owner)
    owner.viewMode = (owner.viewMode == "model") and "list" or "model"
    self:Apply(owner)
end

function ModelGrid:Apply(owner)
    if not owner.modelGridFrame then return end
    if owner.viewMode == "model" then
        if owner.modelToggleBtn then owner.modelToggleBtn:SetText("List View") end
        for _, row in ipairs(owner.itemRows or {}) do
            row.itemData = nil; row.index = nil; row:Hide()
        end
        owner.modelGridFrame:Show()
        self:Render(owner)
    else
        if owner.modelToggleBtn then owner.modelToggleBtn:SetText("3D View") end
        owner.modelGridFrame:Hide()
        self:HideAllCells(owner)
        owner:UpdateDisplay()
    end
end

function ModelGrid:HideAllCells(owner)
    local mg = owner.modelGridFrame
    if not mg then return end
    for _, cell in ipairs(mg.cells) do
        cell.itemData = nil
        cell:Hide()
    end
end

-- =============================================================================
-- CELL POOL
-- =============================================================================

-- Attach the cell's model. Blizzard's wardrobe item cells are plain DressUpModels
-- driven by SetItemAppearance (confirmed by shipping addons), framed by the
-- appearance's own UI camera — that's what we replicate. Anchored to leave a ~16px
-- strip at the bottom for the name label.
local function attachCellModel(cell)
    local m = CreateFrame("DressUpModel", nil, cell)
    m:SetPoint("TOPLEFT", 3, -3)
    m:SetPoint("BOTTOMRIGHT", -3, 16)
    m:EnableMouse(false)
    cell.model = m
end

function ModelGrid:EnsureCells(owner, n)
    local mg = owner.modelGridFrame
    for i = #mg.cells + 1, n do
        local cell = CreateFrame("Button", nil, mg,
            BackdropTemplateMixin and "BackdropTemplate" or nil)
        cell:EnableMouse(true)
        cell:RegisterForClicks("AnyUp")
        cell:SetScript("OnEnter", function(self)
            if self.itemData and ETB.ModelPreview then
                ETB.ModelPreview:PreviewItem(self.itemData)
            end
        end)
        cell:SetScript("OnLeave", function()
            if ETB.ModelPreview then ETB.ModelPreview:Hide() end
        end)
        cell:SetScript("OnClick", function(self, btn)
            if self.itemData and ETB.ItemRow and ETB.ItemRow.OnClick then
                pcall(ETB.ItemRow.OnClick, ETB.ItemRow, self, btn)
            end
        end)
        if cell.SetBackdrop then
            cell:SetBackdrop({
                edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 10,
                insets = { left = 2, right = 2, top = 2, bottom = 2 },
            })
            cell:SetBackdropBorderColor(0.3, 0.3, 0.3, 0.8)
        end
        attachCellModel(cell)
        local fs = cell:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        fs:SetPoint("BOTTOMLEFT", 3, 3)
        fs:SetPoint("BOTTOMRIGHT", -3, 3)
        fs:SetJustifyH("CENTER")
        fs:SetWordWrap(false)
        fs:SetMaxLines(1)
        cell.nameFS = fs
        mg.cells[i] = cell
    end
end

-- =============================================================================
-- RENDER
-- =============================================================================

-- Repaint only single visible cells' name labels (cheap; never re-dresses models).
function ModelGrid:RefreshVisibleNames()
    local owner = self._activeOwner
    local mg = owner and owner.modelGridFrame
    if not mg or not mg:IsShown() or not mg.cells then return end
    for _, cell in ipairs(mg.cells) do
        local item = cell.itemData
        if cell:IsShown() and item and cell.nameFS then
            local nm = item.name
            if (not nm or nm == "" or nm == "Loading...") and ETB.RequestItemInfoThrottled then
                ETB:RequestItemInfoThrottled(item.itemID)
            end
            if nm and nm ~= "" then
                local color = (ETB.GetQualityColor and ETB:GetQualityColor(item.quality or 1)) or "|cffffffff"
                cell.nameFS:SetText(color .. nm .. "|r")
            end
        end
    end
end

-- One debounced GET_ITEM_INFO_RECEIVED listener so names fill in live as the
-- client resolves them, without re-rendering (and without re-dressing) the grid.
function ModelGrid:EnsureNameRefresher()
    if self._nameFrame then return end
    local f = CreateFrame("Frame")
    self._nameFrame = f
    f:RegisterEvent("GET_ITEM_INFO_RECEIVED")
    f:SetScript("OnEvent", function()
        if f._pending then return end
        f._pending = true
        C_Timer.After(0.25, function()
            f._pending = false
            ModelGrid:RefreshVisibleNames()
        end)
    end)
end

function ModelGrid:Render(owner)
    local mg = owner.modelGridFrame
    if not mg or not mg:IsShown() then return end

    self._activeOwner = owner
    self:EnsureNameRefresher()

    local items = owner.items or {}
    local perPage = owner.itemsPerPage or 19
    local count = math.min(#items, perPage)
    self:EnsureCells(owner, math.max(perPage, count))

    -- Kick off async item-info loads for the whole page up front so names (and the
    -- cached data SetItemAppearance benefits from) resolve sooner instead of
    -- trickling in cell-by-cell.
    for i = 1, count do
        local it = items[i]
        if it and it.itemID then
            if ETB.RequestItemInfoThrottled then
                ETB:RequestItemInfoThrottled(it.itemID)
            elseif C_Item and C_Item.RequestLoadItemDataByID then
                pcall(C_Item.RequestLoadItemDataByID, it.itemID)
            end
        end
    end

    local gridW = mg:GetWidth()
    if not gridW or gridW < 50 then gridW = 560 end
    local cellW = math.floor((gridW - PAD * (COLS + 1)) / COLS)
    local cellH = math.floor(cellW * 1.25)

    local startIdx = (owner.currentPage - 1) * perPage + 1

    for i, cell in ipairs(mg.cells) do
        local item = items[i]
        if item and i <= count then
            cell.index = startIdx + i - 1  -- global index, mirrors list rows (for arrow-nav + selection)
            local col = (i - 1) % COLS
            local row = math.floor((i - 1) / COLS)
            cell:ClearAllPoints()
            cell:SetSize(cellW, cellH)
            cell:SetPoint("TOPLEFT", PAD + col * (cellW + PAD), -(PAD + row * (cellH + PAD)))
            cell:Show()
            self:ConfigureCell(cell, item, i)
        else
            cell.index = nil
            cell.itemData = nil
            cell._dressedItemID = nil
            if cell.nameFS then cell.nameFS:SetText("") end
            cell:Hide()
        end
    end

    -- Re-assert the selection highlight after the cells re-lay-out.
    self:HighlightSelected(owner)
end

-- Gold fill on the 3D cell whose global index matches owner.selectedGlobalIndex
-- (mirrors the list-row highlight; lazily creates the overlay per cell).
function ModelGrid:HighlightSelected(owner)
    local mg = owner.modelGridFrame
    if not mg or not mg.cells then return end
    local sel = owner.selectedGlobalIndex
    for _, cell in ipairs(mg.cells) do
        if not cell._selTex then
            local t = cell:CreateTexture(nil, "OVERLAY")
            t:SetAllPoints()
            t:SetColorTexture(1, 0.82, 0, 0.20)
            t:Hide()
            cell._selTex = t
        end
        if sel and cell:IsShown() and cell.index == sel and cell.itemData then
            cell._selTex:Show()
        else
            cell._selTex:Hide()
        end
    end
end

-- Fallback framing via SetPortraitZoom (DressUpModel TryOn path) — only used when
-- an item has no resolvable appearance/visual ID for SetItemAppearance.
local SLOT_ZOOM = {
    head = 0.82, shoulder = 0.50, back = 0.45, chest = 0.52, shirt = 0.52,
    tabard = 0.45, wrist = 0.30, hands = 0.30, waist = 0.42, legs = 0.0, feet = 0.0,
}

local function resolveVisualID(item)
    if item.visualID and item.visualID > 0 then return item.visualID end
    if item.appearanceID and item.appearanceID > 0 then return item.appearanceID end
    local TC = _G.C_TransmogCollection
    if TC and TC.GetItemInfo then
        local ok, a = pcall(TC.GetItemInfo, item.itemID)
        if ok and a and a > 0 then return a end
    end
    return nil
end

-- After applying the appearance's own camera, nudge the distance for slots whose
-- authored camera reads too tight in our cell size. 1.0 = unchanged.
local ZOOM_ADJUST = { head = 1.15 }

-- Apply the appearance's own UI camera (the native per-slot framing, and the
-- rear view for cloaks). Returns true if a real appearance camera was applied.
local function applyAppearanceCamera(model, item, visualID, key)
    local TC = _G.C_TransmogCollection
    local camID
    if TC and TC.GetAppearanceCameraID and visualID then
        local ok, id = pcall(TC.GetAppearanceCameraID, visualID)
        if ok and id and id > 0 then camID = id end
    end
    if not camID and TC and TC.GetAppearanceCameraIDBySource and item.sourceID then
        local ok, id = pcall(TC.GetAppearanceCameraIDBySource, item.sourceID)
        if ok and id and id > 0 then camID = id end
    end
    if camID and _G.Model_ApplyUICamera then
        local ok = pcall(_G.Model_ApplyUICamera, model, camID)
        if ok then
            local mult = key and ZOOM_ADJUST[key]
            if mult and model.GetCameraDistance and model.SetCameraDistance then
                local d = model:GetCameraDistance()
                if d and d > 0 then pcall(model.SetCameraDistance, model, d * mult) end
            end
            return true
        end
    end
    return false
end

function ModelGrid:ConfigureCell(cell, item, idx)
    if not item or not item.itemID or not cell:IsShown() then return end
    cell.itemData = item

    -- Name + border refresh cheaply on every call (async name updates re-render).
    if cell.nameFS then
        local nm = item.name
        if (not nm or nm == "" or nm == "Loading...") and ETB.RequestItemInfoThrottled then
            ETB:RequestItemInfoThrottled(item.itemID)
        end
        local color = (ETB.GetQualityColor and ETB:GetQualityColor(item.quality or 1)) or "|cffffffff"
        cell.nameFS:SetText(color .. (nm or "") .. "|r")
    end
    if cell.SetBackdropBorderColor then
        if item.isCollected then
            cell:SetBackdropBorderColor(0.15, 0.8, 0.15, 0.9)
        else
            cell:SetBackdropBorderColor(0.4, 0.4, 0.4, 0.8)
        end
    end

    -- Skip the (expensive) dress if this cell already shows this item — async
    -- name resolution triggers many re-renders with unchanged items.
    if cell._dressedItemID == item.itemID then return end
    cell._dressedItemID = item.itemID

    local key, bodiless = ETB.ModelCameras.ResolveKey(item.itemID)
    local visualID = resolveVisualID(item)

    -- Queue model setup so 19 DressUpModel operations never land on one frame.
    local jobKey = "modelGrid.cell." .. tostring(cell)
    if ETB.CancelWork then ETB:CancelWork(jobKey) end
    ETB:QueueWork(jobKey, function()
        if cell.itemData ~= item then return end
        local model = cell.model
        if not model then return end

        if visualID and (model.SetItemAppearance or model.TryOn) then
            -- NATIVE framing via the appearance's own UI camera for both armor and
            -- weapons. Render differs: weapons are bodiless (SetItemAppearance floats
            -- the blade), but armor needs a body to sit on — a bare DressUpModel
            -- won't auto-spawn one here — so armor is worn on a player body via the
            -- proven SetUnit + Undress + TryOn path.
            if bodiless and model.SetItemAppearance then
                pcall(function() model:SetItemAppearance(visualID) end)
            else
                pcall(function() model:SetUnit("player") end)
                pcall(function() model:Undress() end)
                ETB:QueueWork(jobKey .. ".tryon", function()
                    if cell.itemData ~= item then return end
                    local arg = (item.sourceID and item.sourceID > 0) and item.sourceID
                        or ((item.link and item.link ~= "") and item.link or ("item:" .. item.itemID))
                    pcall(function() model:TryOn(arg) end)
                    applyAppearanceCamera(model, item, visualID, key)
                end, { pauseInCombat = true, startupIdleOptions = { ignoreQuietWindow = true } })
            end
            applyAppearanceCamera(model, item, visualID, key)
            -- One delayed camera re-assert is enough for most cells and avoids
            -- tripling the per-page model work.
            C_Timer.After(0.35, function()
                if cell.itemData == item then applyAppearanceCamera(model, item, visualID, key) end
            end)
        else
            -- Fallback (no resolvable appearance ID): dress a player body via TryOn
            -- and frame with portrait zoom.
            local zoom = SLOT_ZOOM[key] or 0
            pcall(model.SetUnit, model, "player")
            pcall(model.Undress, model)
            if model.SetPortraitZoom then pcall(model.SetPortraitZoom, model, zoom) end
            pcall(model.SetFacing, model, 0.35)
            ETB:QueueWork(jobKey .. ".fallbackTryon", function()
                if cell.itemData ~= item then return end
                local link = (item.link and item.link ~= "") and item.link or ("item:" .. item.itemID)
                pcall(model.TryOn, model, link)
                if model.SetSheathed then pcall(model.SetSheathed, model, not bodiless) end
                if model.SetPortraitZoom then pcall(model.SetPortraitZoom, model, zoom) end
                pcall(model.SetFacing, model, 0.35)
            end, { pauseInCombat = true, startupIdleOptions = { ignoreQuietWindow = true } })
        end
    end, { pauseInCombat = true, startupIdleOptions = { ignoreQuietWindow = true } })
end

ETB:Debug("ModelGrid", "ModelGrid.lua loaded")
