-- =============================================================================
-- ETB v12.62.0 — UI/MainFrame/Construction.lua
-- =============================================================================
-- Fifth MainFrame submodule. Owns UI construction — every CreateXxx method
-- that builds part of the main window's frame hierarchy.
--
-- Methods lifted verbatim from MainFrame.lua with `self` (outer parameter
-- only) -> `owner`, `MF.` -> `owner.`. Inner closure parameters named
-- `self` (e.g. function(self) for OnClick handlers where self is the
-- button) are preserved.
--
-- Public API (all take owner = MainFrame singleton as first arg):
--   Construction:Create(owner)
--   Construction:CreateTopBar(owner, f)
--   Construction:CreateLeftSidebar(owner, f)
--   Construction:CreateTab(owner, parent, text, x, y)
--   Construction:CreateItemGrid(owner, f)
--   Construction:CreateLivePreview(owner, f)
--   Construction:CreateBottomBar(owner, f)
--
-- Globals/Blizzard:
--   CreateFrame, UIParent, GameTooltip, StaticPopup_Show, GetCursorPosition
--   ETBDropdown (NS-stored; resolved at call time inside each method)
-- =============================================================================

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

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

-- =============================================================================
-- STATIC POPUP — Save Outfit
-- =============================================================================
-- v15.8.3: The Save Outfit button (in CreateLivePreview) calls
-- StaticPopup_Show("ETB_SAVE_PREVIEW_OUTFIT"), but the dialog was never
-- defined anywhere — the button silently no-op'd.
--
-- The popup writes to ETB.TransmogUI.ProfileManager (the file-local
-- ProfileManager exposed via TUI.ProfileManager at end of TransmogUI.lua),
-- NOT the global ETB.ProfileManager. The TransmogUI Outfits panel reads
-- from ETB.TransmogUI.ProfileManager's accountOutfits store, so writing
-- there is the only way for saved outfits to actually appear in TransmogUI.
--
-- Defined at file scope per StaticPopupDialogs taint rules — inline
-- definitions inside callbacks would taint the entire popup framework.
--
-- The popup reads `livePreviewSlots` from the active MainFrame singleton
-- (ETB.MainFrame) at OnAccept time, after the user has typed a name.
StaticPopupDialogs["ETB_SAVE_PREVIEW_OUTFIT"] = {
    text = "Name this outfit:",
    button1 = "Save", button2 = "Cancel",
    hasEditBox = true, maxLetters = 48,
    OnAccept = function(self)
        local eb = self.editBox or self.EditBox
        local name = eb and eb:GetText() or ""
        if name == "" then return end

        local mf = ETB.MainFrame
        if not mf or not mf.livePreviewSlots then
            ETB:Print("|cffff4444No active preview to save.|r")
            return
        end

        -- v15.8.4: TransmogUI's outfit loader uses a different slot-name
        -- vocabulary than the main browser's filter-categories. The main
        -- browser keys livePreviewSlots by CATEGORY_INFO names ("1H Swords",
        -- "Daggers", "Off-Hands", etc.), but TransmogUI's SLOT_NAME_TO_INV
        -- only knows {Head, Shoulder, Back, Chest, Shirt, Tabard, Wrist,
        -- Hands, Waist, Legs, Feet, MainHand, OffHand}. Mismatched slots
        -- get silently dropped at load time. We normalize at save time
        -- using GetItemInfoInstant's equipLoc return (synchronous on MoP
        -- 5.4.8 — equipLoc is static client data).
        local function normalizeSlotName(slotName, itemID)
            -- Armor slot names already match between the two systems
            if slotName == "Head" or slotName == "Shoulder" or slotName == "Back"
               or slotName == "Chest" or slotName == "Shirt" or slotName == "Tabard"
               or slotName == "Wrist" or slotName == "Hands" or slotName == "Waist"
               or slotName == "Legs" or slotName == "Feet"
               or slotName == "MainHand" or slotName == "OffHand" then
                return slotName
            end
            -- Weapon category names need translation via equipLoc lookup
            if itemID and GetItemInfoInstant then
                local _, _, _, equipLoc = GetItemInfoInstant(itemID)
                if equipLoc == "INVTYPE_WEAPONOFFHAND" or equipLoc == "INVTYPE_HOLDABLE"
                   or equipLoc == "INVTYPE_SHIELD" then
                    return "OffHand"
                elseif equipLoc == "INVTYPE_2HWEAPON" or equipLoc == "INVTYPE_WEAPON"
                   or equipLoc == "INVTYPE_WEAPONMAINHAND" or equipLoc == "INVTYPE_RANGED"
                   or equipLoc == "INVTYPE_RANGEDRIGHT" or equipLoc == "INVTYPE_THROWN" then
                    return "MainHand"
                end
            end
            -- Unknown slot — return original; load will silently skip it.
            return slotName
        end

        -- Snapshot the current livePreviewSlots into a plain item table,
        -- normalizing weapon category keys to MainHand/OffHand.
        local items = {}
        local count = 0
        for slotName, item in pairs(mf.livePreviewSlots) do
            if item and item.itemID then
                local norm = normalizeSlotName(slotName, item.itemID)
                items[norm] = {
                    itemID = item.itemID,
                    name = item.name,
                    icon = item.icon,
                    quality = item.quality,
                }
                count = count + 1
            end
        end

        if count == 0 then
            ETB:Print("|cffff4444No items in preview to save.|r")
            return
        end

        -- Save to the SAME store TransmogUI reads from
        -- (ETB.TransmogUI.ProfileManager.accountOutfits via SavedVariable
        -- ETBTUI_AccountData), so the new outfit appears in TransmogUI's
        -- saved-outfits list immediately.
        local PM = ETB.TransmogUI and ETB.TransmogUI.ProfileManager
        if not PM or not PM.CreateAccountOutfit then
            ETB:Print("|cffff4444TransmogUI not loaded — cannot save outfit.|r")
            return
        end

        local outfitID = PM:CreateAccountOutfit(name, items)
        if outfitID then
            ETB:Print(string.format("|cff00ff00Outfit saved:|r %s (%d pieces)", name, count))
            ETB:Print("|cffaaaaaa  View it in TransmogUI (/etb transmogui → Outfits tab)|r")
            -- Notify any other panels (e.g. TransmogUI if it's open) so they
            -- refresh their outfit list right away rather than on next open.
            if ETB.EventBus then
                ETB.EventBus:Fire("ETB_OUTFIT_CREATED", outfitID, name)
            end
        else
            ETB:Print("|cffff4444Failed to save outfit (unknown error).|r")
        end
    end,
    EditBoxOnEnterPressed = function(self)
        local parent = self:GetParent()
        if parent and parent.button1 then parent.button1:Click() end
    end,
    EditBoxOnEscapePressed = function(self) self:GetParent():Hide() end,
    timeout = 0, whileDead = true, hideOnEscape = true,
}

-- =============================================================================
-- LAYOUT CONSTANTS (stable config values, duplicated from MainFrame.lua)
-- =============================================================================

local FRAME_WIDTH = 1350
local FRAME_HEIGHT = 850
local SIDEBAR_WIDTH = 210
local PREVIEW_WIDTH = 340

local FRAME_MARGIN = 15
local TITLE_AREA_HEIGHT = 45
local TOPBAR_HEIGHT = 95
local BOTTOMBAR_HEIGHT = 50

local TOPBAR_Y = -(TITLE_AREA_HEIGHT)
local CONTENT_Y = -(TITLE_AREA_HEIGHT + TOPBAR_HEIGHT + 8)
local BOTTOM_MARGIN = FRAME_MARGIN + BOTTOMBAR_HEIGHT + 8

local PREVIEW_SLOTS_LEFT = { "Head", "Shoulder", "Back", "Chest", "Shirt", "Tabard", "Wrist" }
local PREVIEW_SLOTS_RIGHT = { "Hands", "Waist", "Legs", "Feet", "MainHand", "OffHand" }

-- Item grid layout (used by CreateItemGrid)
local ITEM_ROW_HEIGHT = 32
local ROWS_PER_COLUMN = 19
local COLUMNS = 1

function Construction:Create(owner)
    if owner.frame then return owner.frame end
    
    local f = CreateFrame("Frame", "ETBMainFrame", UIParent, "BackdropTemplate")
    f:SetSize(FRAME_WIDTH, FRAME_HEIGHT)
    f:SetPoint("CENTER")
    f:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
        edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Gold-Border",
        tile = true, tileSize = 32, edgeSize = 32,
        insets = { left = 8, right = 8, top = 8, bottom = 8 }
    })
    f:SetMovable(true)
    f:EnableMouse(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", f.StartMoving)
    f:SetScript("OnDragStop", f.StopMovingOrSizing)
    f:SetFrameStrata("HIGH")
    f:Hide()
    
    -- Hide ModelPreview when main frame is hidden (ESC, close button, etc.)
    f:SetScript("OnHide", function()
        if ETB.ModelPreview then
            ETB.ModelPreview:Hide()
        end
        if ETB.AppearanceDetail then
            ETB.AppearanceDetail:Hide()
        end
        -- Free the big filtered items cache
        if owner._filteredItems then
            wipe(owner._filteredItems)
            owner._filteredItems = nil
        end
        -- Clear current page items
        if owner.items then
            wipe(owner.items)
        end
        -- Wipe ALL BlizzardData caches
        if ETB.BlizzardData then
            if ETB.BlizzardData.cache then
                wipe(ETB.BlizzardData.cache.categories)
                wipe(ETB.BlizzardData.cache.sources)
            end
            -- Clear item cache (500 items × 1.2KB = 600KB)
            if ETB.BlizzardData.itemCache and ETB.BlizzardData.itemCache.Clear then
                ETB.BlizzardData.itemCache:Clear()
            end
        end
        -- Force TWO GC cycles — first marks, second frees
        collectgarbage("collect")
        collectgarbage("collect")
    end)
    
    -- ESC closes frame.
    -- v14.10.1 fix: was using f:SetScript("OnKeyDown", ... SetPropagateKeyboardInput ...)
    -- but on MoP Classic 5.4.8 SetPropagateKeyboardInput is a PROTECTED function — the
    -- pcall around it didn't suppress the underlying ADDON_ACTION_BLOCKED event,
    -- so every ESC press logged "AddOn 'EnhancedTransmogBrowser' tried to call the
    -- protected function 'UNKNOWN()'" via BugGrabber. Worse, the OnKeyDown handler
    -- was dead anyway — without f:EnableKeyboard(true), OnKeyDown never fires for
    -- non-secure frames.
    -- The canonical pattern is to register the frame's global name with
    -- UISpecialFrames. The earlier comment claimed this caused taint; that was
    -- incorrect — UISpecialFrames is just a regular Lua table that Blizzard's
    -- core code reads when ESC is pressed. No taint, no protection issue.
    if UISpecialFrames then
        local already = false
        for _, name in ipairs(UISpecialFrames) do
            if name == "ETBMainFrame" then already = true; break end
        end
        if not already then
            tinsert(UISpecialFrames, "ETBMainFrame")
        end
    end
    
    f.closeBtn = CreateFrame("Button", nil, f, "UIPanelCloseButton")
    f.closeBtn:SetPoint("TOPRIGHT", -8, -8)
    
    -- Toggle button: switch to ERB Raid Browser
    f.raidToggle = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
    f.raidToggle:SetSize(80, 22)
    f.raidToggle:SetPoint("TOPRIGHT", -38, -14)
    f.raidToggle:SetText("Raid BiS")
    f.raidToggle:SetScript("OnClick", function()
        if ETB.ToggleUIMode then ETB:ToggleUIMode() end
    end)
    f.raidToggle:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
        GameTooltip:AddLine("Switch to Raid BiS Browser")
        GameTooltip:AddLine("View BiS gear rankings per slot", 1, 1, 1)
        GameTooltip:Show()
    end)
    f.raidToggle:SetScript("OnLeave", function() GameTooltip:Hide() end)
    
    -- Wishlist button
    f.wishlistToggle = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
    f.wishlistToggle:SetSize(80, 22)
    f.wishlistToggle:SetPoint("RIGHT", f.raidToggle, "LEFT", -3, 0)
    f.wishlistToggle:SetText("Wishlist")
    f.wishlistToggle:SetScript("OnClick", function()
        if ETB.WishlistPanel then ETB.WishlistPanel:Toggle() end
    end)
    f.wishlistToggle:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
        GameTooltip:AddLine("Open Wishlist")
        local count = ETB.Wishlist and ETB.Wishlist:GetItemCount() or 0
        GameTooltip:AddLine(count .. " items on your wishlist", 1, 1, 1)
        GameTooltip:Show()
    end)
    f.wishlistToggle:SetScript("OnLeave", function() GameTooltip:Hide() end)
    
    f.title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    f.title:SetPoint("TOPLEFT", 14, -16)
    f.title:SetText("|cffffd100Enhanced Transmog Browser|r")
    
    f.version = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.version:SetPoint("LEFT", f.title, "RIGHT", 6, 0)
    f.version:SetText("|cff555555v" .. (ETB.version or "12.0") .. "|r")
    
    -- v12.63.0 FIX: convert self -> owner so the dispatch goes back through
    -- MainFrame.lua delegators (which dispatch to the right submodule).
    -- `self` here would refer to the Construction module, which has no
    -- MF instance state.
    owner:CreateTopBar(f)
    owner:CreateLeftSidebar(f)
    owner:CreateItemGrid(f)
    owner:CreateLivePreview(f)
    owner:CreateBottomBar(f)

    owner.frame = f
    return f
end


function Construction:CreateTopBar(owner, f)
    local ETBDropdown = NS.ETBDropdown  -- Resolved at call time
    local topBar = CreateFrame("Frame", nil, f, "BackdropTemplate")
    topBar:SetPoint("TOPLEFT", FRAME_MARGIN, TOPBAR_Y)
    topBar:SetPoint("TOPRIGHT", -FRAME_MARGIN, TOPBAR_Y)
    topBar:SetHeight(TOPBAR_HEIGHT)
    topBar:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 12,
        insets = { left = 4, right = 4, top = 4, bottom = 4 }
    })
    f.topBar = topBar
    
    -- Row 1: Current slot + Sort
    f.currentViewingLabel = topBar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    f.currentViewingLabel:SetPoint("TOPLEFT", 10, -6)
    f.currentViewingLabel:SetText("|cff00ff00Head|r")
    
    -- Sort dropdown (right side)
    local sortLabel = topBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    sortLabel:SetPoint("TOPRIGHT", -120, -8)
    sortLabel:SetText("|cffffffffSort:|r")
    
    f.sortDropdown = ETBDropdown:Create("ETBSortDropdown", topBar)
    f.sortDropdown:SetPoint("TOPRIGHT", -8, -4)
    ETBDropdown:SetWidth(f.sortDropdown, 110)
    ETBDropdown:SetText(f.sortDropdown, "Name (A-Z)")
    ETBDropdown:Initialize(f.sortDropdown, function(self, _items_)
        local info = ETBDropdown:CreateInfo()
        for _, s in ipairs({
            { text = "Name (A-Z)", sort = "name", asc = true },
            { text = "Name (Z-A)", sort = "name", asc = false },
            { text = "Quality " .. string.char(226,134,145), sort = "quality", asc = true },
            { text = "Quality " .. string.char(226,134,147), sort = "quality", asc = false },
        }) do
            info.text = s.text
            info.func = function()
                ETBDropdown:SetText(f.sortDropdown, s.text)
                ETB.Filtering:SetSort(s.sort, s.asc)
                owner:RefreshItems()
            end
            ETBDropdown:AddButton(_items_, info)
        end
    end)
    
    -- Armor slot icons (Row 1, after label)
    f.slotIcons = {}
    f.weaponTypeIcons = {}
    
    local armorLabel = topBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    armorLabel:SetPoint("TOPLEFT", 10, -30)
    armorLabel:SetText("|cffffd100Armor:|r")
    
    local armorSlots = {
        { id = 1,  name = "Head" },
        { id = 2,  name = "Shoulder" },
        { id = 3,  name = "Back" },
        { id = 4,  name = "Chest" },
        { id = 5,  name = "Shirt" },
        { id = 6,  name = "Tabard" },
        { id = 7,  name = "Wrist" },
        { id = 8,  name = "Hands" },
        { id = 9,  name = "Waist" },
        { id = 10, name = "Legs" },
        { id = 11, name = "Feet" },
    }
    
    local BUTTON_SIZE = 28
    local BUTTON_SPACING = 30
    local ARMOR_ROW_START_X = 55
    local ARMOR_ROW_Y = -26
    
    for i, slot in ipairs(armorSlots) do
        local btn = CreateFrame("Button", nil, topBar)
        btn:SetSize(BUTTON_SIZE, BUTTON_SIZE)
        btn:SetPoint("TOPLEFT", ARMOR_ROW_START_X + (i-1) * BUTTON_SPACING, ARMOR_ROW_Y)
        
        btn.icon = btn:CreateTexture(nil, "ARTWORK")
        btn.icon:SetAllPoints()
        local catInfo = ETB.CATEGORY_INFO and ETB.CATEGORY_INFO[slot.id]
        btn.icon:SetTexture(catInfo and catInfo.icon or "Interface\\Icons\\INV_Misc_QuestionMark")
        btn.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        
        btn.selected = btn:CreateTexture(nil, "OVERLAY")
        btn.selected:SetAllPoints()
        btn.selected:SetColorTexture(0, 1, 0, 0.4)
        btn.selected:Hide()
        
        btn:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square", "ADD")
        
        btn:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
            GameTooltip:SetText(slot.name)
            GameTooltip:Show()
        end)
        btn:SetScript("OnLeave", function() GameTooltip:Hide() end)
        btn:SetScript("OnClick", function()
            ETB.Filtering:SetCategory(slot.id)
            owner.categoryMode = "armor"
            owner:UpdateSlotHighlight(slot.id)
            owner:UpdateCategoryTabs()
            owner:UpdateCurrentViewingLabel(slot.name)
            owner:UpdateWeaponCategoryUI()
            owner:RefreshItems()
        end)
        
        btn.slotID = slot.id
        btn.slotName = slot.name
        f.slotIcons[slot.id] = btn
    end
    
    -- ==========================================================================
    -- ROW 3: Weapon buttons (16 buttons organized in groups)
    -- ==========================================================================
    
    local WEAPON_ROW_Y = -60
    
    -- 1H Weapons (6 buttons)
    local label1H = topBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    label1H:SetPoint("TOPLEFT", 10, WEAPON_ROW_Y - 3)
    label1H:SetText("|cffffd1001H:|r")
    
    local weapons1H = {
        { id = 14, name = "1H Swords" },
        { id = 13, name = "1H Axes" },
        { id = 15, name = "1H Maces" },
        { id = 16, name = "Daggers" },
        { id = 17, name = "Fist Weapons" },
        { id = 12, name = "Wands" },
    }
    
    local START_1H = 35
    for i, slot in ipairs(weapons1H) do
        local btn = CreateFrame("Button", nil, topBar)
        btn:SetSize(BUTTON_SIZE, BUTTON_SIZE)
        btn:SetPoint("TOPLEFT", START_1H + (i-1) * BUTTON_SPACING, WEAPON_ROW_Y)
        
        btn.icon = btn:CreateTexture(nil, "ARTWORK")
        btn.icon:SetAllPoints()
        local catInfo = ETB.CATEGORY_INFO and ETB.CATEGORY_INFO[slot.id]
        btn.icon:SetTexture(catInfo and catInfo.icon or "Interface\\Icons\\INV_Misc_QuestionMark")
        btn.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        
        btn.selected = btn:CreateTexture(nil, "OVERLAY")
        btn.selected:SetAllPoints()
        btn.selected:SetColorTexture(0, 1, 0, 0.4)
        btn.selected:Hide()
        
        btn:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square", "ADD")
        
        btn:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
            GameTooltip:SetText(slot.name)
            GameTooltip:Show()
        end)
        btn:SetScript("OnLeave", function() GameTooltip:Hide() end)
        btn:SetScript("OnClick", function()
            ETB.Filtering:SetCategory(slot.id)
            owner.categoryMode = "weapons"
            owner:UpdateSlotHighlight(slot.id)
            owner:UpdateCategoryTabs()
            local catInfo2 = ETB.CATEGORY_INFO and ETB.CATEGORY_INFO[slot.id]
            owner:UpdateCurrentViewingLabel(catInfo2 and catInfo2.name or slot.name)
            owner:UpdateWeaponCategoryUI()
            owner:RefreshItems()
        end)
        
        btn.slotID = slot.id
        btn.slotName = slot.name
        f.slotIcons[slot.id] = btn
    end
    
    -- 2H Weapons (5 buttons)
    local label2H = topBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    label2H:SetPoint("TOPLEFT", 225, WEAPON_ROW_Y - 3)
    label2H:SetText("|cffffd1002H:|r")
    
    local weapons2H = {
        { id = 21, name = "2H Swords" },
        { id = 20, name = "2H Axes" },
        { id = 22, name = "2H Maces" },
        { id = 23, name = "Staves" },
        { id = 24, name = "Polearms" },
    }
    
    local START_2H = 250
    for i, slot in ipairs(weapons2H) do
        local btn = CreateFrame("Button", nil, topBar)
        btn:SetSize(BUTTON_SIZE, BUTTON_SIZE)
        btn:SetPoint("TOPLEFT", START_2H + (i-1) * BUTTON_SPACING, WEAPON_ROW_Y)
        
        btn.icon = btn:CreateTexture(nil, "ARTWORK")
        btn.icon:SetAllPoints()
        local catInfo = ETB.CATEGORY_INFO and ETB.CATEGORY_INFO[slot.id]
        btn.icon:SetTexture(catInfo and catInfo.icon or "Interface\\Icons\\INV_Misc_QuestionMark")
        btn.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        
        btn.selected = btn:CreateTexture(nil, "OVERLAY")
        btn.selected:SetAllPoints()
        btn.selected:SetColorTexture(0, 1, 0, 0.4)
        btn.selected:Hide()
        
        btn:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square", "ADD")
        
        btn:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
            GameTooltip:SetText(slot.name)
            GameTooltip:Show()
        end)
        btn:SetScript("OnLeave", function() GameTooltip:Hide() end)
        btn:SetScript("OnClick", function()
            ETB.Filtering:SetCategory(slot.id)
            owner.categoryMode = "weapons"
            owner:UpdateSlotHighlight(slot.id)
            owner:UpdateCategoryTabs()
            local catInfo2 = ETB.CATEGORY_INFO and ETB.CATEGORY_INFO[slot.id]
            owner:UpdateCurrentViewingLabel(catInfo2 and catInfo2.name or slot.name)
            owner:UpdateWeaponCategoryUI()
            owner:RefreshItems()
        end)
        
        btn.slotID = slot.id
        btn.slotName = slot.name
        f.slotIcons[slot.id] = btn
    end
    
    -- Ranged + Off-Hand (5 buttons)
    local labelRanged = topBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    labelRanged:SetPoint("TOPLEFT", 410, WEAPON_ROW_Y - 3)
    labelRanged:SetText("|cffffd100Rng/OH:|r")
    
    local weaponsRanged = {
        { id = 25, name = "Bows" },
        { id = 26, name = "Guns" },
        { id = 27, name = "Crossbows" },
        { id = 18, name = "Shields" },
        { id = 19, name = "Off-Hands" },
    }
    
    local START_RANGED = 480
    for i, slot in ipairs(weaponsRanged) do
        local btn = CreateFrame("Button", nil, topBar)
        btn:SetSize(BUTTON_SIZE, BUTTON_SIZE)
        btn:SetPoint("TOPLEFT", START_RANGED + (i-1) * BUTTON_SPACING, WEAPON_ROW_Y)
        
        btn.icon = btn:CreateTexture(nil, "ARTWORK")
        btn.icon:SetAllPoints()
        local catInfo = ETB.CATEGORY_INFO and ETB.CATEGORY_INFO[slot.id]
        btn.icon:SetTexture(catInfo and catInfo.icon or "Interface\\Icons\\INV_Misc_QuestionMark")
        btn.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        
        btn.selected = btn:CreateTexture(nil, "OVERLAY")
        btn.selected:SetAllPoints()
        btn.selected:SetColorTexture(0, 1, 0, 0.4)
        btn.selected:Hide()
        
        btn:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square", "ADD")
        
        btn:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
            GameTooltip:SetText(slot.name)
            GameTooltip:Show()
        end)
        btn:SetScript("OnLeave", function() GameTooltip:Hide() end)
        btn:SetScript("OnClick", function()
            ETB.Filtering:SetCategory(slot.id)
            owner.categoryMode = "weapons"
            owner:UpdateSlotHighlight(slot.id)
            owner:UpdateCategoryTabs()
            local catInfo2 = ETB.CATEGORY_INFO and ETB.CATEGORY_INFO[slot.id]
            owner:UpdateCurrentViewingLabel(catInfo2 and catInfo2.name or slot.name)
            owner:UpdateWeaponCategoryUI()
            owner:RefreshItems()
        end)
        
        btn.slotID = slot.id
        btn.slotName = slot.name
        f.slotIcons[slot.id] = btn
    end
    
    -- Hidden placeholder frames for compatibility
    f.weaponTypeLabel = topBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.weaponTypeLabel:Hide()
    f.weaponTypeDropdown = ETBDropdown:Create("ETBWeaponTypeDropdown", topBar)
    f.weaponTypeDropdown:Hide()
    f.weaponTypeIconsFrame = CreateFrame("Frame", nil, topBar)
    f.weaponTypeIconsFrame:Hide()
    f.weaponTypeSectionLabel = topBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.weaponTypeSectionLabel:Hide()
end


function Construction:CreateLeftSidebar(owner, f)
    local ETBDropdown = NS.ETBDropdown  -- Resolved at call time
    local sidebar = CreateFrame("Frame", nil, f, "BackdropTemplate")
    sidebar:SetWidth(SIDEBAR_WIDTH)
    sidebar:SetPoint("TOPLEFT", FRAME_MARGIN, CONTENT_Y)
    sidebar:SetPoint("BOTTOMLEFT", FRAME_MARGIN, BOTTOM_MARGIN)
    sidebar:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 12,
        insets = { left = 4, right = 4, top = 4, bottom = 4 }
    })
    sidebar:SetClipsChildren(true)
    f.sidebar = sidebar
    
    -- ==========================================================================
    -- SIDEBAR LAYOUT (all Y positions relative to sidebar top)
    -- Total height available: ~625px
    -- ==========================================================================
    
    local title = sidebar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    title:SetPoint("TOP", 0, -8)
    title:SetText("|cff00ffffFilters|r")
    
    -- Category tabs - inline row
    f.armorTab = owner:CreateTab(sidebar, "Armor", 6, -26)
    f.armorTab:SetScript("OnClick", function()
        owner:HideSetsView()
        owner.categoryMode = "armor"
        owner:UpdateCategoryTabs()
        ETB.Filtering:SetCategory(1)
        owner:UpdateSlotHighlight(1)
        owner:UpdateCurrentViewingLabel("Head")
        owner:UpdateWeaponCategoryUI()
        owner:RefreshItems()
    end)
    
    f.weaponsTab = owner:CreateTab(sidebar, "Weapons", 54, -26)
    f.weaponsTab:SetScript("OnClick", function()
        owner:HideSetsView()
        owner.categoryMode = "weapons"
        owner:UpdateCategoryTabs()
        ETB.Filtering:SetCategory(14)
        owner:UpdateSlotHighlight(14)
        owner:UpdateCurrentViewingLabel("1H Swords")
        owner:UpdateWeaponCategoryUI()
        owner:RefreshItems()
    end)
    
    -- Row 2: Sets (left), Builder (right)
    f.setsTab = owner:CreateTab(sidebar, "Sets", 102, -26)
    f.setsTab:SetScript("OnClick", function()
        owner:HideSetBuilderView()
        owner.categoryMode = "sets"
        owner:UpdateCategoryTabs()
        owner:ShowSetsView()
    end)
    
    f.setBuilderTab = owner:CreateTab(sidebar, "Builder", 150, -26)
    f.setBuilderTab:SetScript("OnClick", function()
        -- Open Set Builder as a separate standalone window
        if ETB.SetBuilder then
            ETB.SetBuilder:Show()
        end
    end)
    
    -- Section divider 2 (below 2x2 tabs)
    local divider2 = sidebar:CreateTexture(nil, "ARTWORK")
    divider2:SetSize(SIDEBAR_WIDTH - 16, 2)
    divider2:SetPoint("TOP", 0, -54)
    divider2:SetColorTexture(0.5, 0.5, 0.5, 0.5)
    
    -- CLASS FILTER SECTION
    local classLabel = sidebar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    classLabel:SetPoint("TOPLEFT", 6, -64)
    classLabel:SetText("")
    
    -- My Class Only checkbox
    f.myClassCheck = CreateFrame("CheckButton", nil, sidebar, "UICheckButtonTemplate")
    f.myClassCheck:SetPoint("TOPLEFT", 4, -60)
    f.myClassCheck:SetChecked(false)
    ETB.Filtering:SetMyClassOnly(false)
    f.myClassCheck:SetScript("OnClick", function(self)
        ETB.Filtering:SetMyClassOnly(self:GetChecked())
        owner:RefreshItems()
    end)
    
    local checkText = sidebar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    checkText:SetPoint("LEFT", f.myClassCheck, "RIGHT", 0, 0)
    checkText:SetText("|cff00ff00My Class Only|r")
    
    -- Group by Appearance: always on (one item per unique visual appearance)
    ETB.Filtering:SetGroupByAppearance(true)
    
    -- Armor Type Filter
    local armorTypeLabel = sidebar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    armorTypeLabel:SetPoint("TOPLEFT", 8, -84)
    armorTypeLabel:SetText("|cffffd100Armor|r")
    
    f.armorTypeDropdown = ETBDropdown:Create("ETBArmorTypeDropdown", sidebar)
    f.armorTypeDropdown:SetPoint("TOPLEFT", 0, -98)
    ETBDropdown:SetWidth(f.armorTypeDropdown, SIDEBAR_WIDTH - 30)
    ETBDropdown:SetText(f.armorTypeDropdown, "All Types")
    ETBDropdown:Initialize(f.armorTypeDropdown, function(self, _items_)
        local info = ETBDropdown:CreateInfo()
        local armorTypes = {
            { value = "all", text = "All Types" },
            { value = "PLATE", text = "Plate" },
            { value = "MAIL", text = "Mail" },
            { value = "LEATHER", text = "Leather" },
            { value = "CLOTH", text = "Cloth" },
        }
        for _, at in ipairs(armorTypes) do
            info.text = at.text
            info.value = at.value
            info.func = function()
                ETBDropdown:SetText(f.armorTypeDropdown, at.text)
                ETB.Filtering:SetArmorType(at.value)
                owner:RefreshItems()
            end
            ETBDropdown:AddButton(_items_, info)
        end
    end)
    
    -- Note: Profile system is still loaded for tracking but UI is removed
    -- Initialize profile system silently if available
    if ETB.ProfileSelector and ETB.ProfileSelector.Initialize then
        ETB.ProfileSelector:Initialize()
    end
    
    -- Section divider 3
    local divider3 = sidebar:CreateTexture(nil, "ARTWORK")
    divider3:SetSize(SIDEBAR_WIDTH - 16, 2)
    divider3:SetPoint("TOP", 0, -124)
    divider3:SetColorTexture(0.5, 0.5, 0.5, 0.5)
    
    -- Source Type
    local sourceLabel = sidebar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    sourceLabel:SetPoint("TOPLEFT", 8, -130)
    sourceLabel:SetText("|cffffd100Source|r")
    
    f.sourceDropdown = ETBDropdown:Create("ETBSourceDropdown", sidebar)
    f.sourceDropdown:SetPoint("TOPLEFT", 0, -144)
    ETBDropdown:SetWidth(f.sourceDropdown, SIDEBAR_WIDTH - 30)
    ETBDropdown:SetText(f.sourceDropdown, "All Sources")
    ETBDropdown:Initialize(f.sourceDropdown, function(self, _items_)
        local info = ETBDropdown:CreateInfo()
        local sourceTypes = {
            { value = "all",        text = "All Sources" },
            { value = "raid",       text = "Raid" },
            { value = "dungeon",    text = "Dungeon" },
            { value = "quest",      text = "Quest" },
            { value = "vendor",     text = "Vendor" },
            { value = "crafted",    text = "Crafted" },
            { value = "pvp",        text = "PvP" },
            { value = "world_drop", text = "World Drop" },
        }
        for _, st in ipairs(sourceTypes) do
            info.text = st.text
            info.value = st.value
            info.func = function()
                ETBDropdown:SetText(f.sourceDropdown, st.text)
                ETB.Filtering:SetSourceType(st.value)
                owner:RefreshItems()
            end
            ETBDropdown:AddButton(_items_, info)
        end
    end)
    
    -- Expansion Filter
    local expansionLabel = sidebar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    expansionLabel:SetPoint("TOPLEFT", 8, -170)
    expansionLabel:SetText("|cffffd100Expansion|r")
    
    f.expansionDropdown = ETBDropdown:Create("ETBExpansionDropdown", sidebar)
    f.expansionDropdown:SetPoint("TOPLEFT", 0, -184)
    ETBDropdown:SetWidth(f.expansionDropdown, SIDEBAR_WIDTH - 30)
    ETBDropdown:SetText(f.expansionDropdown, "All Expansions")
    ETBDropdown:Initialize(f.expansionDropdown, function(self, _items_)
        local info = ETBDropdown:CreateInfo()
        for _, exp in ipairs(ETB.EXPANSIONS) do
            info.text = exp.name
            info.value = exp.id
            info.func = function()
                ETBDropdown:SetText(f.expansionDropdown, exp.name)
                ETB.Filtering:SetExpansion(exp.id)
                owner:RefreshItems()
            end
            ETBDropdown:AddButton(_items_, info)
        end
    end)
    
    -- Color Filter
    local colorLabel = sidebar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    colorLabel:SetPoint("TOPLEFT", 8, -210)
    colorLabel:SetText("|cffffd100Color|r")
    
    f.colorDropdown = ETBDropdown:Create("ETBColorDropdown", sidebar)
    f.colorDropdown:SetPoint("TOPLEFT", 0, -224)
    ETBDropdown:SetWidth(f.colorDropdown, SIDEBAR_WIDTH - 30)
    ETBDropdown:SetText(f.colorDropdown, "All Colors")
    ETBDropdown:Initialize(f.colorDropdown, function(self, _items_)
        local info = ETBDropdown:CreateInfo()
        info.text = "All Colors"
        info.value = nil
        info.func = function()
            ETBDropdown:SetText(f.colorDropdown, "All Colors")
            ETB.Filtering:SetColorFilter(nil)
            owner:RefreshItems()
        end
        ETBDropdown:AddButton(_items_, info)
        
        if ETB.ColorSearch then
            for _, cat in ipairs(ETB.ColorSearch:GetCategories()) do
                info.text = cat.name
                info.value = cat.id
                info.func = function()
                    ETBDropdown:SetText(f.colorDropdown, cat.name)
                    ETB.Filtering:SetColorFilter(cat.id)
                    owner:RefreshItems()
                end
                ETBDropdown:AddButton(_items_, info)
            end
        end
    end)
    
    -- Section divider 4 - Collection filter section
    local divider4 = sidebar:CreateTexture(nil, "ARTWORK")
    divider4:SetSize(SIDEBAR_WIDTH - 16, 2)
    divider4:SetPoint("TOP", 0, -250)
    divider4:SetColorTexture(0.5, 0.5, 0.5, 0.5)
    
    -- Collection Filter Section
    local collectionLabel = sidebar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    collectionLabel:SetPoint("TOPLEFT", 8, -256)
    collectionLabel:SetText("|cffffd100Collection|r")
    
    -- Collection Filter Buttons
    f.collAllBtn = CreateFrame("Button", nil, sidebar, "UIPanelButtonTemplate")
    f.collAllBtn:SetSize(SIDEBAR_WIDTH - 20, 24)
    f.collAllBtn:SetPoint("TOP", 0, -272)
    f.collAllBtn:SetText("» All «")
    f.collAllBtn:SetScript("OnClick", function()
        ETB.Filtering:SetCollectedFilter("all")
        owner:UpdateCollectionButtons("all")
        owner:RefreshItems()
    end)
    
    f.collectedBtn = CreateFrame("Button", nil, sidebar, "UIPanelButtonTemplate")
    f.collectedBtn:SetSize((SIDEBAR_WIDTH - 24) / 2, 24)
    f.collectedBtn:SetPoint("TOPLEFT", 8, -296)
    f.collectedBtn:SetText("Collected")
    f.collectedBtn:SetScript("OnClick", function()
        ETB.Filtering:SetCollectedFilter("collected")
        owner:UpdateCollectionButtons("collected")
        owner:RefreshItems()
    end)
    
    f.notCollectedBtn = CreateFrame("Button", nil, sidebar, "UIPanelButtonTemplate")
    f.notCollectedBtn:SetSize((SIDEBAR_WIDTH - 24) / 2, 24)
    f.notCollectedBtn:SetPoint("LEFT", f.collectedBtn, "RIGHT", 4, 0)
    f.notCollectedBtn:SetText("Need")
    f.notCollectedBtn:SetScript("OnClick", function()
        ETB.Filtering:SetCollectedFilter("uncollected")
        owner:UpdateCollectionButtons("uncollected")
        owner:RefreshItems()
    end)
    
    -- Set initial button colors
    owner:UpdateCollectionButtons("all")
    
    -- Section divider 5 - Stats
    local divider5 = sidebar:CreateTexture(nil, "ARTWORK")
    divider5:SetSize(SIDEBAR_WIDTH - 16, 2)
    divider5:SetPoint("TOP", 0, -324)
    divider5:SetColorTexture(0.5, 0.5, 0.5, 0.5)
    
    -- Collection Stats
    local statsLabel = sidebar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    statsLabel:SetPoint("TOPLEFT", 8, -330)
    statsLabel:SetText("|cffffd100Collection Stats|r")
    
    f.statsOverall = sidebar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.statsOverall:SetPoint("TOPLEFT", 8, -346)
    f.statsOverall:SetText("|cffffffffOverall: 0/0 (0%)|r")
    
    f.statsSlot = sidebar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.statsSlot:SetPoint("TOPLEFT", 8, -360)
    f.statsSlot:SetText("|cffffffffSlot: 0/0 (0%)|r")
    
    f.statsShowing = sidebar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.statsShowing:SetPoint("TOPLEFT", 8, -374)
    f.statsShowing:SetText("|cffffffffShowing: 0 items|r")
    
    -- Refresh Data Button
    f.refreshBtn = CreateFrame("Button", nil, sidebar, "UIPanelButtonTemplate")
    f.refreshBtn:SetSize(SIDEBAR_WIDTH - 16, 20)
    f.refreshBtn:SetPoint("TOP", 0, -394)
    f.refreshBtn:SetText("Refresh Data")
    f.refreshBtn:SetScript("OnClick", function()
        owner:RefreshItems()
    end)
    
    -- Set Builder embed container (below Refresh Data)
    f.setBuilderEmbedContainer = CreateFrame("Frame", nil, sidebar, "BackdropTemplate")
    f.setBuilderEmbedContainer:SetPoint("TOPLEFT", sidebar, 0, -576)
    f.setBuilderEmbedContainer:SetPoint("BOTTOMRIGHT", sidebar, 0, 8)
    f.setBuilderEmbedContainer:SetFrameLevel(sidebar:GetFrameLevel() + 10)
    f.setBuilderEmbedContainer:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 8,
        insets = { left = 4, right = 4, top = 4, bottom = 4 }
    })
    f.setBuilderEmbedContainer:Hide()
    
    -- Note: Removed Armor Slot Dropdown (use top icons instead)
    -- Note: Removed Profile Selector (still load profiles for tracking)
    
    -- Hidden dummy for armor slot label (for compatibility)
    f.armorSlotLabel = sidebar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    f.armorSlotLabel:Hide()
    
    -- Weapon Category Selector (only shown when in weapon mode)
    f.weaponCategoryLabel = sidebar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    f.weaponCategoryLabel:SetPoint("TOPLEFT", 10, -329)
    f.weaponCategoryLabel:SetText("|cffffd100Weapon Category|r")
    f.weaponCategoryLabel:Hide()
    
    f.weaponCategoryDropdown = ETBDropdown:Create("ETBWeaponCategoryDropdown", sidebar)
    f.weaponCategoryDropdown:SetPoint("TOPLEFT", 0, -346)
    ETBDropdown:SetWidth(f.weaponCategoryDropdown, SIDEBAR_WIDTH - 50)
    ETBDropdown:SetText(f.weaponCategoryDropdown, "Select Weapon Type")
    ETBDropdown:Initialize(f.weaponCategoryDropdown, function(self, _items_)
        local info = ETBDropdown:CreateInfo()
        local weaponCategories = {
            { id = 14, name = "1H Swords (399)" },
            { id = 13, name = "1H Axes (256)" },
            { id = 15, name = "1H Maces (390)" },
            { id = 16, name = "Daggers (452)" },
            { id = 17, name = "Fist Weapons (148)" },
            { id = 21, name = "2H Swords (212)" },
            { id = 20, name = "2H Axes (210)" },
            { id = 22, name = "2H Maces (200)" },
            { id = 23, name = "Staves (428)" },
            { id = 24, name = "Polearms (152)" },
            { id = 25, name = "Bows (165)" },
            { id = 26, name = "Guns (150)" },
            { id = 27, name = "Crossbows (87)" },
            { id = 12, name = "Wands (241)" },
            { id = 18, name = "Shields (368)" },
            { id = 19, name = "Off-Hands (263)" },
        }
        for _, cat in ipairs(weaponCategories) do
            info.text = cat.name
            info.value = cat.id
            info.func = function()
                ETBDropdown:SetText(f.weaponCategoryDropdown, cat.name)
                ETB.Filtering:SetCategory(cat.id)
                owner:UpdateSlotHighlight(cat.id)
                local catInfo = ETB.CATEGORY_INFO[cat.id]
                owner:UpdateCurrentViewingLabel(catInfo and catInfo.name or cat.name)
                owner:RefreshItems()
            end
            ETBDropdown:AddButton(_items_, info)
        end
    end)
    f.weaponCategoryDropdown:Hide()
    
    -- Compatibility aliases
    f.weaponTypeLabel = f.weaponCategoryLabel
    f.weaponTypeDropdown = f.weaponCategoryDropdown
end


function Construction:CreateTab(owner, parent, text, x, y)
    local btn = CreateFrame("Button", nil, parent)
    btn:SetSize(46, 22)
    btn:SetPoint("TOPLEFT", x, y)
    
    btn.bg = btn:CreateTexture(nil, "BACKGROUND")
    btn.bg:SetAllPoints()
    btn.bg:SetColorTexture(0.2, 0.2, 0.2, 0.8)
    
    btn.text = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    btn.text:SetAllPoints()
    btn.text:SetText(text)
    
    return btn
end


function Construction:CreateItemGrid(owner, f)
    local grid = CreateFrame("Frame", nil, f, "BackdropTemplate")
    grid:SetPoint("TOPLEFT", f.sidebar, "TOPRIGHT", 14, 0)
    grid:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -(PREVIEW_WIDTH + FRAME_MARGIN + 14), BOTTOM_MARGIN)
    grid:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 12,
        insets = { left = 4, right = 4, top = 4, bottom = 4 }
    })
    f.gridFrame = grid
    f.grid = grid
    grid:SetClipsChildren(true)
    
    -- Enable mouse wheel scrolling for page navigation
    grid:EnableMouseWheel(true)
    grid:SetScript("OnMouseWheel", function(self, delta)
        if delta > 0 then
            -- Scroll up = previous page
            if owner.currentPage > 1 then
                owner.currentPage = owner.currentPage - 1
                owner:LoadPageData(owner.currentPage)
                owner:UpdateDisplay()
                owner:UpdateStats()
            end
        else
            -- Scroll down = next page
            local totalPages = math.ceil(owner.totalItemCount / owner.itemsPerPage)
            if owner.currentPage < totalPages then
                owner.currentPage = owner.currentPage + 1
                owner:LoadPageData(owner.currentPage)
                owner:UpdateDisplay()
                owner:UpdateStats()
            end
        end
    end)
    
    -- Section label
    local gridLabel = grid:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    gridLabel:SetPoint("TOPLEFT", 6, -5)
    gridLabel:SetText("|cff00ffffItems|r")
    
    -- Scroll hint
    local scrollHint = grid:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    scrollHint:SetPoint("TOPRIGHT", -6, -5)
    scrollHint:SetText("|cff555555Scroll to page|r")
    
    owner.itemRows = {}
    for row = 1, ROWS_PER_COLUMN do
        for col = 1, COLUMNS do
            local idx = (row - 1) * COLUMNS + col
            local itemRow = ETB.ItemRow:Acquire(grid)
            itemRow:SetHeight(ITEM_ROW_HEIGHT - 2)
            itemRow:SetPoint("TOPLEFT", 4, -18 - (row-1) * ITEM_ROW_HEIGHT)
            itemRow:SetPoint("RIGHT", grid, "RIGHT", -4, 0)
            itemRow:Hide()
            owner.itemRows[idx] = itemRow
        end
    end
end


function Construction:CreateLivePreview(owner, f)
    local preview = CreateFrame("Frame", nil, f, "BackdropTemplate")
    preview:SetWidth(PREVIEW_WIDTH)
    preview:SetPoint("TOPRIGHT", -FRAME_MARGIN, CONTENT_Y)
    preview:SetPoint("BOTTOMRIGHT", -FRAME_MARGIN, BOTTOM_MARGIN)
    preview:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 12,
        insets = { left = 4, right = 4, top = 4, bottom = 4 }
    })
    f.preview = preview
    
    local title = preview:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOP", 0, -12)
    title:SetText("|cff00ffffLive Preview|r")

    -- v14.4.0 Feature 5.3: small "BG" button cycles preview backgrounds.
    -- Placed in the title row, top-right of preview frame so it doesn't
    -- crowd the model itself.
    f.bgCycleBtn = CreateFrame("Button", nil, preview, "UIPanelButtonTemplate")
    f.bgCycleBtn:SetSize(36, 18)
    f.bgCycleBtn:SetPoint("TOPRIGHT", -8, -8)
    f.bgCycleBtn:SetText("|cffaaaaaaBG|r")
    f.bgCycleBtn:SetScript("OnClick", function()
        if ETB.PreviewBackground then
            local nextPreset = ETB.PreviewBackground:CycleNext()
            if ETB.Print and nextPreset then
                ETB:Print("Preview background: " .. (nextPreset.label or nextPreset.id))
            end
        end
    end)
    f.bgCycleBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
        GameTooltip:SetText("Background")
        GameTooltip:AddLine("Click to cycle through background presets.", 0.8, 0.8, 0.8, true)
        if ETB.PreviewBackground then
            local cur = ETB.PreviewBackground._byID[ETB.PreviewBackground:GetCurrentID()]
            if cur then
                GameTooltip:AddLine("Current: " .. cur.label, 1, 0.82, 0)
            end
        end
        GameTooltip:Show()
    end)
    f.bgCycleBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)

    -- Section divider
    local previewDivider = preview:CreateTexture(nil, "ARTWORK")
    previewDivider:SetSize(PREVIEW_WIDTH - 8, 2)
    previewDivider:SetPoint("TOP", 0, -35)
    previewDivider:SetColorTexture(0.5, 0.5, 0.5, 0.5)
    
    -- Model (BIGGER: 260x420)
    f.previewModel = CreateFrame("DressUpModel", nil, preview)
    f.previewModel:SetSize(260, 420)
    f.previewModel:SetPoint("TOP", 0, -40)
    f.previewModel:SetUnit("player")
    f.previewModel:SetRotation(0)

    -- v14.4.0 Feature 5.3: background swap. Texture sits at draw layer
    -- BACKGROUND, model renders above with its transparent bg.
    if ETB.PreviewBackground then
        ETB.PreviewBackground:Attach(f.previewModel, preview)
    end
    
    -- Mouse rotation/zoom
    f.previewModel:EnableMouse(true)
    f.previewModel:EnableMouseWheel(true)
    f.previewModel.zoomLevel = 0
    -- v15.8.8: install OnUpdate only WHILE the user is dragging. The
    -- previous unconditional handler fired 60×/sec just to check the
    -- self.rotating flag — wasted 14400 calls/min sitting idle. Now
    -- OnUpdate is bound on OnMouseDown and unbound on OnMouseUp, so
    -- there's zero per-frame cost while the browser is open but idle.
    local function onUpdateRotate(self)
        local x = GetCursorPosition()
        owner.previewRotation = (owner.previewRotation or 0) + (x - self.startX) * 0.01
        self:SetRotation(owner.previewRotation)
        self.startX = x
    end
    f.previewModel:SetScript("OnMouseDown", function(self, btn)
        if btn == "LeftButton" then
            self.startX = GetCursorPosition()
            self:SetScript("OnUpdate", onUpdateRotate)
        end
    end)
    f.previewModel:SetScript("OnMouseUp", function(self)
        self:SetScript("OnUpdate", nil)
    end)
    -- Proper zoom using camera distance
    f.previewModel:SetScript("OnMouseWheel", function(self, delta)
        self.zoomLevel = (self.zoomLevel or 0) + delta * 0.1
        self.zoomLevel = math.max(-0.5, math.min(1.5, self.zoomLevel))
        self:SetCamDistanceScale(1 - self.zoomLevel)
    end)
    
    -- Create slot buttons on left side
    f.leftSlotBtns = {}
    for i, slotName in ipairs(PREVIEW_SLOTS_LEFT) do
        local btn = owner:CreateSlotButton(preview, slotName, i)
        btn:SetPoint("TOPLEFT", 5, -30 - (i-1) * 26)
        f.leftSlotBtns[slotName] = btn
    end

    -- Create slot buttons on right side
    f.rightSlotBtns = {}
    for i, slotName in ipairs(PREVIEW_SLOTS_RIGHT) do
        local btn = owner:CreateSlotButton(preview, slotName, i)
        btn:SetPoint("TOPRIGHT", -5, -30 - (i-1) * 26)
        f.rightSlotBtns[slotName] = btn
    end
    
    -- Hint
    local hint = preview:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    hint:SetPoint("TOP", f.previewModel, "BOTTOM", 0, -5)
    hint:SetText("|cff00ff00Drag rotate • Wheel zoom|r")
    
    -- Animation controls (hidden by default - dev tools)
    local animLabel = preview:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    animLabel:SetPoint("TOP", hint, "BOTTOM", 0, -4)
    animLabel:SetText("|cffffd100Pose:|r")
    animLabel:Hide()
    
    -- AnimationData.dbc IDs (NOT Emotes.dbc IDs)
    -- Confirmed from AnimationData.dbc
    local ANIM_PRESETS = {
        { id = 0,   label = "Stand" },      -- AnimData 0: Stand
        { id = 4,   label = "Walk" },       -- AnimData 4: Walk
        { id = 5,   label = "Run" },        -- AnimData 5: Run
        { id = 16,  label = "Attack" },     -- AnimData 16: AttackUnarmed
        { id = 25,  label = "Ready" },      -- AnimData 25: ReadyUnarmed (combat idle)
        { id = 60,  label = "Talk" },       -- AnimData 60: EmoteTalk (confirmed by Wowpedia)
        { id = 32,  label = "Cast" },       -- AnimData 32: SpellCast
        { id = 1,   label = "Death" },      -- AnimData 1: Death
    }
    
    local animBtnWidth = 48
    local animBtnHeight = 18
    local animBtnsPerRow = 4
    local animStartX = -(animBtnWidth * animBtnsPerRow + 3 * (animBtnsPerRow - 1)) / 2
    
    f.animButtons = {}
    for i, preset in ipairs(ANIM_PRESETS) do
        local row = math.floor((i - 1) / animBtnsPerRow)
        local col = (i - 1) % animBtnsPerRow
        
        local animBtn = CreateFrame("Button", nil, preview, "UIPanelButtonTemplate")
        animBtn:SetSize(animBtnWidth, animBtnHeight)
        animBtn:SetPoint("TOP", animLabel, "BOTTOM", animStartX + col * (animBtnWidth + 3) + animBtnWidth / 2, -(row * (animBtnHeight + 2) + 2))
        animBtn:SetText(preset.label)
        animBtn:GetFontString():SetFont(animBtn:GetFontString():GetFont(), 10)
        
        local animID = preset.id
        animBtn:SetScript("OnClick", function()
            for _, btn in ipairs(f.animButtons) do
                btn:GetFontString():SetTextColor(1, 0.82, 0)
            end
            animBtn:GetFontString():SetTextColor(0, 1, 0.5)
            f.previewModel:SetAnimation(animID)
            if f.animIDLabel then
                f.animIDLabel:SetText("|cff00ff00ID: " .. animID .. "|r")
            end
            f._currentAnimID = animID
        end)
        animBtn:Hide()
        f.animButtons[i] = animBtn
    end
    
    -- Animation Tester (hidden - dev tool)
    local presetRows = math.ceil(#ANIM_PRESETS / animBtnsPerRow)
    local testerY = -(presetRows * (animBtnHeight + 2) + 6)
    
    f._currentAnimID = 0
    
    local testerRow = CreateFrame("Frame", nil, preview)
    testerRow:SetSize(PREVIEW_WIDTH - 20, 20)
    testerRow:SetPoint("TOP", animLabel, "BOTTOM", 0, testerY)
    testerRow:Hide()
    
    local minusBtn = CreateFrame("Button", nil, testerRow, "UIPanelButtonTemplate")
    minusBtn:SetSize(22, 18)
    minusBtn:SetPoint("LEFT", testerRow, "CENTER", -55, 0)
    minusBtn:SetText("-")
    minusBtn:SetScript("OnClick", function()
        f._currentAnimID = max(0, (f._currentAnimID or 0) - 1)
        f.previewModel:SetAnimation(f._currentAnimID)
        f.animIDLabel:SetText("|cff00ff00ID: " .. f._currentAnimID .. "|r")
        for _, btn in ipairs(f.animButtons) do
            btn:GetFontString():SetTextColor(1, 0.82, 0)
        end
    end)
    
    f.animIDLabel = testerRow:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.animIDLabel:SetPoint("CENTER", testerRow, "CENTER", 0, 0)
    f.animIDLabel:SetText("|cff00ff00ID: 0|r")
    f.animIDLabel:SetWidth(50)
    f.animIDLabel:SetJustifyH("CENTER")
    
    local plusBtn = CreateFrame("Button", nil, testerRow, "UIPanelButtonTemplate")
    plusBtn:SetSize(22, 18)
    plusBtn:SetPoint("LEFT", testerRow, "CENTER", 33, 0)
    plusBtn:SetText("+")
    plusBtn:SetScript("OnClick", function()
        f._currentAnimID = min(500, (f._currentAnimID or 0) + 1)
        f.previewModel:SetAnimation(f._currentAnimID)
        f.animIDLabel:SetText("|cff00ff00ID: " .. f._currentAnimID .. "|r")
        for _, btn in ipairs(f.animButtons) do
            btn:GetFontString():SetTextColor(1, 0.82, 0)
        end
    end)
    
    local plus10Btn = CreateFrame("Button", nil, testerRow, "UIPanelButtonTemplate")
    plus10Btn:SetSize(28, 18)
    plus10Btn:SetPoint("LEFT", plusBtn, "RIGHT", 2, 0)
    plus10Btn:SetText("+10")
    plus10Btn:GetFontString():SetFont(plus10Btn:GetFontString():GetFont(), 9)
    plus10Btn:SetScript("OnClick", function()
        f._currentAnimID = min(500, (f._currentAnimID or 0) + 10)
        f.previewModel:SetAnimation(f._currentAnimID)
        f.animIDLabel:SetText("|cff00ff00ID: " .. f._currentAnimID .. "|r")
        for _, btn in ipairs(f.animButtons) do
            btn:GetFontString():SetTextColor(1, 0.82, 0)
        end
    end)
    
    f.previewItemName = preview:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    f.previewItemName:SetPoint("TOP", hint, "BOTTOM", 0, -(math.ceil(#ANIM_PRESETS / animBtnsPerRow) * (animBtnHeight + 2) + 30))
    f.previewItemName:SetWidth(260)
    
    -- Buttons (below model + hint text)
    local btnY = -480
    
    f.undressBtn = CreateFrame("Button", nil, preview, "UIPanelButtonTemplate")
    f.undressBtn:SetSize(105, 22)
    f.undressBtn:SetPoint("TOPLEFT", 10, btnY)
    f.undressBtn:SetText("Undress")
    f.undressBtn:SetScript("OnClick", function()
        f.previewModel:Undress()
        wipe(owner.livePreviewSlots)
        owner:UpdateSlotButtons()
    end)
    
    f.myGearBtn = CreateFrame("Button", nil, preview, "UIPanelButtonTemplate")
    f.myGearBtn:SetSize(105, 22)
    f.myGearBtn:SetPoint("TOPRIGHT", -10, btnY)
    f.myGearBtn:SetText("My Gear")
    f.myGearBtn:SetScript("OnClick", function()
        -- Avoid SetUnit which causes graphics reset - just dress
        f.previewModel:Dress()
        wipe(owner.livePreviewSlots)
    end)
    
    f.clearBtn = CreateFrame("Button", nil, preview, "UIPanelButtonTemplate")
    f.clearBtn:SetSize(PREVIEW_WIDTH - 30, 22)
    f.clearBtn:SetPoint("TOP", 0, btnY - 28)
    f.clearBtn:SetText("Clear All")
    f.clearBtn:SetScript("OnClick", function()
        f.previewModel:Undress()
        owner.previewRotation = 0
        f.previewModel:SetRotation(0)
        f.previewModel:SetPosition(0, 0, 0)
        f.previewItemName:SetText("")
        wipe(owner.livePreviewSlots)
        owner:UpdateSlotButtons()
    end)
    
    -- Random Outfit button
    f.randomOutfitBtn = CreateFrame("Button", nil, preview, "UIPanelButtonTemplate")
    f.randomOutfitBtn:SetSize(PREVIEW_WIDTH - 30, 22)
    f.randomOutfitBtn:SetPoint("TOP", 0, btnY - 56)
    f.randomOutfitBtn:SetText("|cffffd100Random Outfit|r")
    f.randomOutfitBtn:SetScript("OnClick", function()
        owner:GenerateRandomOutfit()
    end)
    
    -- See Sources button
    f.seeSourcesBtn = CreateFrame("Button", nil, preview, "UIPanelButtonTemplate")
    f.seeSourcesBtn:SetSize(PREVIEW_WIDTH - 30, 22)
    f.seeSourcesBtn:SetPoint("TOP", 0, btnY - 84)
    f.seeSourcesBtn:SetText("|cff00ffffSee Sources|r")
    f.seeSourcesBtn:SetScript("OnClick", function()
        if ETB.SourcesSummaryPanel then
            ETB.SourcesSummaryPanel:Show(owner.livePreviewSlots)
        end
    end)
    f.seeSourcesBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_TOP")
        GameTooltip:SetText("See Sources")
        GameTooltip:AddLine("View all sources for your outfit items", 0.8, 0.8, 0.8)
        GameTooltip:AddLine("grouped by location for efficient farming", 0.8, 0.8, 0.8)
        GameTooltip:Show()
    end)
    f.seeSourcesBtn:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
    
    -- Save Outfit button
    f.saveOutfitBtn = CreateFrame("Button", nil, preview, "UIPanelButtonTemplate")
    f.saveOutfitBtn:SetSize(PREVIEW_WIDTH - 30, 22)
    f.saveOutfitBtn:SetPoint("TOP", 0, btnY - 112)
    f.saveOutfitBtn:SetText("|cffffd100Save Outfit|r")
    f.saveOutfitBtn:SetScript("OnClick", function()
        -- Count items in live preview
        local count = 0
        for _ in pairs(owner.livePreviewSlots) do count = count + 1 end
        if count == 0 then
            ETB:Print("|cffff4444No items in preview to save.|r")
            return
        end
        
        -- Show name dialog (defined at load time to avoid taint)
        StaticPopup_Show("ETB_SAVE_PREVIEW_OUTFIT")
    end)
    f.saveOutfitBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_TOP")
        GameTooltip:SetText("Save Outfit")
        local count = 0
        for _ in pairs(owner.livePreviewSlots) do count = count + 1 end
        GameTooltip:AddLine("Save the current preview as an outfit", 0.8, 0.8, 0.8)
        GameTooltip:AddLine(count .. " pieces in preview", 1, 0.82, 0)
        GameTooltip:AddLine("Outfit will appear in TransmogUI", 0.5, 0.5, 0.5)
        GameTooltip:Show()
    end)
    f.saveOutfitBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)

    -- v15.8.2: Hide Tabard/Shirt/Weapons checkboxes removed per user
    -- feedback. Hide Shirt and Hide Weapons were never doing anything
    -- visible (model's shirt/weapon state already reflects the player's
    -- worn gear), and Hide Tabard wasn't reliable on MoP 5.4.8. The
    -- settings keys (preview.autoHideTabard, autoHideShirt, autoHideWeapons)
    -- and the ApplyAutoHideSlots function are retained as no-op-when-false
    -- so SavedVariables migration is a no-op and re-enabling later is
    -- straightforward.
end


function Construction:CreateBottomBar(owner, f)
    local bar = CreateFrame("Frame", nil, f, "BackdropTemplate")
    bar:SetPoint("BOTTOMLEFT", FRAME_MARGIN, FRAME_MARGIN)
    bar:SetPoint("BOTTOMRIGHT", -FRAME_MARGIN, FRAME_MARGIN)
    bar:SetHeight(BOTTOMBAR_HEIGHT)
    bar:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 12,
        insets = { left = 4, right = 4, top = 4, bottom = 4 }
    })
    f.bottomBar = bar
    
    -- Previous button (left)
    f.prevBtn = CreateFrame("Button", nil, bar, "UIPanelButtonTemplate")
    f.prevBtn:SetSize(90, 26)
    f.prevBtn:SetPoint("LEFT", 8, 0)
    f.prevBtn:SetText("« Previous")
    f.prevBtn:SetScript("OnClick", function()
        if owner.currentPage > 1 then
            owner.currentPage = owner.currentPage - 1
            owner:UpdateDisplayWithRefresh()
        end
    end)
    
    -- Page text (center-left, inline)
    f.pageText = bar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.pageText:SetPoint("LEFT", f.prevBtn, "RIGHT", 8, 0)
    
    f.itemCountText = bar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.itemCountText:SetPoint("LEFT", f.pageText, "RIGHT", 8, 0)
    
    -- Next button
    f.nextBtn = CreateFrame("Button", nil, bar, "UIPanelButtonTemplate")
    f.nextBtn:SetSize(90, 26)
    f.nextBtn:SetPoint("LEFT", f.itemCountText, "RIGHT", 8, 0)
    f.nextBtn:SetText("Next »")
    f.nextBtn:SetScript("OnClick", function()
        local totalPages = math.ceil(owner.totalItemCount / owner.itemsPerPage)
        if owner.currentPage < totalPages then
            owner.currentPage = owner.currentPage + 1
            owner:UpdateDisplayWithRefresh()
        end
    end)
    
    -- Memory display (far right)
    f.memoryText = bar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.memoryText:SetPoint("RIGHT", bar, "RIGHT", -8, 0)
    f.memoryText:SetText("|cff555555Mem: 0 KB|r")
    
    -- Collection stats (right of next button)
    f.collectionStats = bar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.collectionStats:SetPoint("RIGHT", f.memoryText, "LEFT", -12, 0)
    f.collectionStats:SetText("")
end


