local _, ns = ...

------------------------------------------------------------
-- Spec ID -> TrinketData key mapping
------------------------------------------------------------
local SPEC_KEY_MAP = {
    -- DPS specs
    [251]  = "Death_Knight_Frost",  [252]  = "Death_Knight_Unholy",
    [577]  = "Demon_Hunter_Havoc", [1480] = "Demon_Hunter_Devourer",
    [102]  = "Druid_Balance",      [103]  = "Druid_Feral",
    [105]  = "Druid_Restoration",
    [1467] = "Evoker_Devastation",
    [1468] = "Evoker_Preservation",
    [1473] = "Evoker_Augmentation",
    [253]  = "Hunter_Beast",       [254]  = "Hunter_Marksmanship",
    [255]  = "Hunter_Survival",
    [62]   = "Mage_Arcane",        [63]   = "Mage_Fire",
    [64]   = "Mage_Frost",
    [269]  = "Monk_Windwalker",
    [270]  = "Monk_Mistweaver",
    [65]   = "Paladin_Holy",
    [70]   = "Paladin_Retribution",
    [256]  = "Priest_Discipline",
    [257]  = "Priest_Holy",
    [258]  = "Priest_Shadow",
    [259]  = "Rogue_Outlaw",       [260]  = "Rogue_Outlaw",
    [261]  = "Rogue_Subtlety",
    [262]  = "Shaman_Elemental",   [263]  = "Shaman_Enhancement",
    [264]  = "Shaman_Restoration",
    [265]  = "Warlock_Affliction",  [266]  = "Warlock_Demonology",
    [267]  = "Warlock_Destruction",
    [71]   = "Warrior_Arms",       [72]   = "Warrior_Fury",
    -- Tank specs (DPS output only)
    [250]  = "Death_Knight_Blood",
    [581]  = "Demon_Hunter_Vengeance",
    [268]  = "Monk_Brewmaster",
    [66]   = "Paladin_Protection",
    [73]   = "Warrior_Protection",
}

-- WoW item quality colors for tier borders
-- S = Legendary (orange), A = Epic (purple), B = Rare (blue)
local TIER_COLORS = {
    S = { 1.0, 0.5, 0.0  },   -- legendary orange
    A = { 0.64, 0.21, 0.93 }, -- epic purple
    B = { 0.0, 0.44, 0.87 },  -- rare blue
}

local TIER_LABELS = {
    S = "|cffff8000S|r",  -- legendary
    A = "|cffa335eeA|r",  -- epic
    B = "|cff0070ddB|r",  -- rare
}

local currentFightStyle = "ST"  -- "ST" or "AOE"

-- Forward declarations (defined later in the file)
local GetCachedItemInfo
local SetItemBorder
local GetItemLink

function ns:GetPlayerSpecKey()
    local specIndex = GetSpecialization()
    if not specIndex then return nil end
    local specID = GetSpecializationInfo(specIndex)
    if not specID then return nil end
    return SPEC_KEY_MAP[specID]
end

function ns:GetTrinketRankings(specKey)
    if not ns.TRINKET_RANKINGS then return nil end
    -- New format: ns.TRINKET_RANKINGS.ST[specKey] / .AOE[specKey]
    local byStyle = ns.TRINKET_RANKINGS[currentFightStyle]
    if byStyle and byStyle[specKey] then
        return byStyle[specKey]
    end
    -- Fallback: old format ns.TRINKET_RANKINGS[specKey]
    return ns.TRINKET_RANKINGS[specKey]
end

------------------------------------------------------------
-- Constants
------------------------------------------------------------
local PANEL_WIDTH = 280
local PANEL_HEIGHT = 540
local ICON_SIZE = 28
local ROW_HEIGHT = 38
local MAX_DISPLAY = 20

------------------------------------------------------------
-- Instance detection — map WoW instance names to source names
------------------------------------------------------------
local INSTANCE_TO_SOURCE = {
    -- Raids
    ["The Voidspire"]            = "The Voidspire",
    ["The Dreamrift"]            = "The Dreamrift",
    -- Dungeons
    ["Windrunner Spire"]         = "Windrunner Spire",
    ["Maisara Caverns"]          = "Maisara Caverns",
    ["Magisters' Terrace"]       = "Magisters' Terrace",
    ["Nexus-Point Xenas"]        = "Nexus-Point Xenas",
    ["Pit of Saron"]             = "Pit of Saron",
    ["Seat of the Triumvirate"]  = "Seat of the Triumvirate",
    ["Skyreach"]                 = "Skyreach",
    ["Algeth'ar Academy"]        = "Algeth'ar Academy",
}

local currentInstanceSource = nil

local function UpdateCurrentInstance()
    local _, _, _, _, _, _, _, instanceID = GetInstanceInfo()
    local name = GetInstanceInfo()
    local inInstance, instanceType = IsInInstance()

    if inInstance and (instanceType == "party" or instanceType == "raid") then
        currentInstanceSource = INSTANCE_TO_SOURCE[name]
    else
        currentInstanceSource = nil
    end
end

local instanceFrame = CreateFrame("Frame")
instanceFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA")
instanceFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
instanceFrame:SetScript("OnEvent", function()
    local prevSource = currentInstanceSource
    UpdateCurrentInstance()
    -- Auto-refresh trinket panel if instance changed and panel is showing
    if currentInstanceSource ~= prevSource and panel and panel:IsShown() then
        ns:RefreshTrinketPanel()
    end
end)

------------------------------------------------------------
-- Main panel
------------------------------------------------------------
local panel = CreateFrame("Frame", "YippYappTrinketPanel", UIParent, "BackdropTemplate")
panel:SetSize(PANEL_WIDTH, PANEL_HEIGHT)
panel:SetFrameStrata("HIGH")
panel:SetBackdrop({
    bgFile = "Interface\\Buttons\\WHITE8x8",
    edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
    edgeSize = 16,
    insets = { left = 4, right = 4, top = 4, bottom = 4 },
})
panel:SetBackdropColor(0.08, 0.08, 0.08, 0.95)
panel:SetBackdropBorderColor(0.6, 0.6, 0.6, 1)
ns.SmoothFrame(panel)
panel:Hide()
ns.TrinketPanel = panel

ns._trinketAppMode = false

local function GetExpanded()
    YippYappHelperDB = YippYappHelperDB or {}
    if YippYappHelperDB.trinketExpanded == nil then
        return true -- default to expanded
    end
    return YippYappHelperDB.trinketExpanded
end

local function SetExpanded(val)
    YippYappHelperDB = YippYappHelperDB or {}
    YippYappHelperDB.trinketExpanded = val
end

local isExpanded = GetExpanded()
local SLIDE_DURATION = 0.15

------------------------------------------------------------
-- Side tab (always anchored to CharacterFrame right edge)
-- Clicking slides the trinket panel in/out
------------------------------------------------------------
local TAB_WIDTH = 24
local TAB_HEIGHT = 80

local tab = CreateFrame("Button", "YippYappTrinketTab", UIParent, "BackdropTemplate")
tab:SetSize(TAB_WIDTH, TAB_HEIGHT)
tab:SetFrameStrata("HIGH")
tab:SetFrameLevel(200)
tab:SetBackdrop({
    bgFile = "Interface\\Buttons\\WHITE8x8",
    edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
    edgeSize = 12,
    insets = { left = 2, right = 2, top = 2, bottom = 2 },
})
tab:SetBackdropColor(0.1, 0.1, 0.1, 0.95)
tab:SetBackdropBorderColor(1.0, 0.5, 0.0, 0.8)

local tabTextHolder = CreateFrame("Frame", nil, tab)
tabTextHolder:SetAllPoints(tab)

local tabText = tabTextHolder:CreateFontString(nil, "OVERLAY")
local fontPath, _, fontFlags = GameFontNormal:GetFont()
tabText:SetFont(fontPath, 13, fontFlags)
tabText:SetPoint("CENTER", tabTextHolder, "CENTER", 4, -4)
tabText:SetText("|cffff8000Trinkets|r")

local ag = tabTextHolder:CreateAnimationGroup()
local rot = ag:CreateAnimation("Rotation")
rot:SetDegrees(-90)
rot:SetOrigin("CENTER", 0, 0)
rot:SetDuration(0.001)
rot:SetEndDelay(2147483647)
rot:SetSmoothing("NONE")
ag:Play()

-- Find the actual right edge of the character frame
-- ChonkyCharacterSheet extends content beyond CharacterFrame's bounds using CharacterFrameBg
local function GetCharRightAnchor()
    local hasChonky = C_AddOns and C_AddOns.IsAddOnLoaded and C_AddOns.IsAddOnLoaded("ChonkyCharacterSheet")
    if hasChonky and CharacterFrameBg then
        return CharacterFrameBg, "TOPRIGHT", "BOTTOMRIGHT"
    end
    return CharacterFrame, "TOPRIGHT", "BOTTOMRIGHT"
end

local function AnchorTab()
    if not CharacterFrame or not CharacterFrame:IsShown() then return end
    tab:ClearAllPoints()
    tab:SetScale(1)
    if isExpanded and panel:IsShown() then
        tab:SetParent(panel)
        tab:SetPoint("TOPLEFT", panel, "TOPRIGHT", -3, -20)
    else
        local anchor = GetCharRightAnchor()
        tab:SetParent(CharacterFrame)
        tab:SetPoint("TOPLEFT", anchor, "TOPRIGHT", -3, -20)
    end
    tab:SetFrameStrata("HIGH")
    tab:SetFrameLevel(200)
end

local function SlideOut()
    if not CharacterFrame or not CharacterFrame:IsShown() then return end
    isExpanded = true
    SetExpanded(true)
    ns:RefreshTrinketPanel()

    -- Anchor to actual right edge of character frame
    local anchor, topPt, botPt = GetCharRightAnchor()
    local hasChonky = C_AddOns and C_AddOns.IsAddOnLoaded and C_AddOns.IsAddOnLoaded("ChonkyCharacterSheet")
    if hasChonky then
        panel:SetParent(CharacterFrame)
    else
        panel:SetParent(UIParent)
    end
    panel:SetScale(1)
    panel:ClearAllPoints()
    panel:SetPoint("TOPLEFT", anchor, topPt, 2, 0)
    panel:SetPoint("BOTTOMLEFT", anchor, botPt, 2, 0)
    panel:SetFrameStrata("HIGH")
    panel:SetWidth(1)
    panel:Show()

    local elapsed = 0
    local ticker
    ticker = C_Timer.NewTicker(0.01, function()
        elapsed = elapsed + 0.01
        local progress = math.min(elapsed / SLIDE_DURATION, 1)
        -- Ease out
        local ease = 1 - (1 - progress) * (1 - progress)
        panel:SetWidth(math.max(ease * PANEL_WIDTH, 1))
        AnchorTab()
        if progress >= 1 then
            panel:SetWidth(PANEL_WIDTH)
            AnchorTab()
            ticker:Cancel()
        end
    end)
end

local function SlideIn()
    isExpanded = false
    SetExpanded(false)
    local elapsed = 0
    local startWidth = panel:GetWidth()
    local ticker
    -- Keep tab attached to the panel during the entire collapse animation
    tab:SetParent(panel)
    ticker = C_Timer.NewTicker(0.01, function()
        elapsed = elapsed + 0.01
        local progress = math.min(elapsed / SLIDE_DURATION, 1)
        -- Ease in
        local ease = progress * progress
        local w = startWidth * (1 - ease)
        panel:SetWidth(math.max(w, 1))
        tab:ClearAllPoints()
        tab:SetPoint("TOPLEFT", panel, "TOPRIGHT", -3, -20)
        if progress >= 1 then
            panel:Hide()
            panel:SetWidth(PANEL_WIDTH)
            -- Now snap tab back to CharacterFrame
            AnchorTab()
            ticker:Cancel()
        end
    end)
end

tab:SetScript("OnClick", function()
    if isExpanded and panel:IsShown() then
        SlideIn()
    else
        SlideOut()
    end
end)
tab:SetScript("OnEnter", function(self)
    self:SetBackdropBorderColor(1.0, 0.8, 0.0, 1)
    GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
    GameTooltip:SetText(isExpanded and panel:IsShown() and "Hide Trinket Rankings" or "Show Trinket Rankings")
    GameTooltip:Show()
end)
tab:SetScript("OnLeave", function(self)
    self:SetBackdropBorderColor(1.0, 0.5, 0.0, 0.8)
    GameTooltip_Hide()
end)
tab:Hide()

------------------------------------------------------------
-- Panel header
------------------------------------------------------------
local title = panel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
title:SetPoint("TOPLEFT", 12, -8)
title:SetText("|cffff8000Trinkets|r")

local specLabel = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
specLabel:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -2)
specLabel:SetWidth(PANEL_WIDTH - 24)
specLabel:SetJustifyH("LEFT")
specLabel:SetWordWrap(false)
specLabel:SetTextColor(0.7, 0.7, 0.7)

local disclaimer = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
disclaimer:SetPoint("TOPLEFT", specLabel, "BOTTOMLEFT", 0, -1)
disclaimer:SetWidth(PANEL_WIDTH - 24)
disclaimer:SetJustifyH("LEFT")
disclaimer:SetTextColor(0.45, 0.45, 0.45)
disclaimer:SetText("Simmed with BiS gear — your results may vary")

-- "All Specs" button to open raid trinket window
local allSpecsBtn = CreateFrame("Button", nil, panel, "BackdropTemplate")
allSpecsBtn:SetSize(60, 18)
allSpecsBtn:SetPoint("TOPRIGHT", -8, -6)
allSpecsBtn:SetBackdrop({
    bgFile = "Interface\\Buttons\\WHITE8x8",
    edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
    edgeSize = 10,
    insets = { left = 2, right = 2, top = 2, bottom = 2 },
})
allSpecsBtn:SetBackdropColor(0.05, 0.15, 0.25, 1)
allSpecsBtn:SetBackdropBorderColor(0.0, 0.5, 0.8, 0.7)

local allSpecsText = allSpecsBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
allSpecsText:SetAllPoints()
allSpecsText:SetText("|cff00aaffAll Specs|r")

allSpecsBtn:SetScript("OnClick", function()
    -- Instantly collapse sidebar (no animation) and open the app
    if isExpanded and panel:IsShown() and CharacterFrame and CharacterFrame:IsShown() then
        isExpanded = false
        SetExpanded(false)
        panel:Hide()
        panel:SetWidth(PANEL_WIDTH)
        AnchorTab()
    end
    if ns.AppFrame and ns.ShowAppPage then
        if not ns.AppFrame:IsShown() then
            ns.AppFrame:Show()
        end
        ns:ShowAppPage("trinkets")
    end
end)
allSpecsBtn:SetScript("OnEnter", function(self)
    self:SetBackdropBorderColor(0.0, 0.67, 1.0, 1)
    GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
    GameTooltip:SetText("Browse trinkets for all specs")
    GameTooltip:Show()
end)
allSpecsBtn:SetScript("OnLeave", function(self)
    self:SetBackdropBorderColor(0.0, 0.5, 0.8, 0.7)
    GameTooltip_Hide()
end)

-- Fight style toggle button
local toggleBtn = CreateFrame("Button", nil, panel, "BackdropTemplate")
toggleBtn:SetSize(50, 18)
toggleBtn:SetPoint("RIGHT", allSpecsBtn, "LEFT", -4, 0)
toggleBtn:SetBackdrop({
    bgFile = "Interface\\Buttons\\WHITE8x8",
    edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
    edgeSize = 10,
    insets = { left = 2, right = 2, top = 2, bottom = 2 },
})
toggleBtn:SetBackdropColor(0.15, 0.15, 0.15, 1)
toggleBtn:SetBackdropBorderColor(0.5, 0.5, 0.5, 0.8)

local toggleText = toggleBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
toggleText:SetAllPoints()
toggleText:SetText("|cff00ff00ST|r")

local function UpdateToggleLabel()
    if currentFightStyle == "ST" then
        toggleText:SetText("|cff00ff00ST|r")
    else
        toggleText:SetText("|cffff8800AOE|r")
    end
end

toggleBtn:SetScript("OnClick", function()
    if currentFightStyle == "ST" then
        currentFightStyle = "AOE"
    else
        currentFightStyle = "ST"
    end
    UpdateToggleLabel()
    ns:RefreshTrinketPanel()
end)
toggleBtn:SetScript("OnEnter", function(self)
    GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
    GameTooltip:SetText("Toggle fight style")
    GameTooltip:AddLine(currentFightStyle == "ST" and "Currently: Single Target\nClick for Multi-Target" or "Currently: Multi-Target (DungeonSlice)\nClick for Single Target", 1, 1, 1, true)
    GameTooltip:Show()
end)
toggleBtn:SetScript("OnLeave", GameTooltip_Hide)

-- Divider
local div = panel:CreateTexture(nil, "ARTWORK")
div:SetHeight(1)
div:SetPoint("TOPLEFT", 10, -42)
div:SetPoint("TOPRIGHT", -10, -42)
div:SetColorTexture(0.5, 0.5, 0.5, 0.5)

-- Scroll area
local scrollFrame = CreateFrame("ScrollFrame", nil, panel, "UIPanelScrollFrameTemplate")
scrollFrame:SetPoint("TOPLEFT", 8, -58)
scrollFrame:SetPoint("BOTTOMRIGHT", -26, 8)

local scrollContent = CreateFrame("Frame", nil, scrollFrame)
scrollContent:SetSize(PANEL_WIDTH - 38, 1)
scrollFrame:SetScrollChild(scrollContent)

local rows = {}

------------------------------------------------------------
-- App mode: strip chrome, widen for full-size display
------------------------------------------------------------
------------------------------------------------------------
-- App-mode trinket browser (3-column S/A/B + class/spec picker)
------------------------------------------------------------
local appOverlay = CreateFrame("Frame", nil, panel)
appOverlay:SetAllPoints()
appOverlay:Hide()

-- Class/spec data for the browser (keys must match TrinketData ranking keys)
local CLASS_SPECS = {
    { class = "Death Knight",   file = "DEATHKNIGHT",  specs = { "Death_Knight_Blood", "Death_Knight_Frost", "Death_Knight_Unholy" }, labels = { "Blood", "Frost", "Unholy" } },
    { class = "Demon Hunter",   file = "DEMONHUNTER",  specs = { "Demon_Hunter_Havoc", "Demon_Hunter_Devourer", "Demon_Hunter_Vengeance" }, labels = { "Havoc", "Devourer", "Vengeance" } },
    { class = "Druid",          file = "DRUID",         specs = { "Druid_Feral" }, labels = { "Feral" } },
    { class = "Evoker",         file = "EVOKER",        specs = { "Evoker_Devastation" }, labels = { "Devastation" } },
    { class = "Hunter",         file = "HUNTER",        specs = { "Hunter_Beast_Mastery", "Hunter_Marksmanship", "Hunter_Survival" }, labels = { "Beast Mastery", "Marksmanship", "Survival" } },
    { class = "Mage",           file = "MAGE",          specs = { "Mage_Arcane", "Mage_Fire", "Mage_Frost" }, labels = { "Arcane", "Fire", "Frost" } },
    { class = "Monk",           file = "MONK",          specs = { "Monk_Brewmaster", "Monk_Windwalker" }, labels = { "Brewmaster", "Windwalker" } },
    { class = "Paladin",        file = "PALADIN",       specs = { "Paladin_Protection", "Paladin_Retribution" }, labels = { "Protection", "Retribution" } },
    { class = "Priest",         file = "PRIEST",        specs = { "Priest_Shadow" }, labels = { "Shadow" } },
    { class = "Rogue",          file = "ROGUE",         specs = { "Rogue_Outlaw", "Rogue_Subtlety" }, labels = { "Outlaw", "Subtlety" } },
    { class = "Shaman",         file = "SHAMAN",        specs = { "Shaman_Elemental", "Shaman_Enhancement" }, labels = { "Elemental", "Enhancement" } },
    { class = "Warlock",        file = "WARLOCK",       specs = { "Warlock_Affliction", "Warlock_Demonology", "Warlock_Destruction" }, labels = { "Affliction", "Demonology", "Destruction" } },
    { class = "Warrior",        file = "WARRIOR",       specs = { "Warrior_Arms", "Warrior_Fury", "Warrior_Protection" }, labels = { "Arms", "Fury", "Protection" } },
}

local appBrowseSpecKey = nil  -- nil = "Your Spec" mode
local appBrowseStyle = "ST"

-- ── Row 1: "Your Spec" + class buttons ──
local NAV_Y = -6
local NAV_H = 22
local NAV_GAP = 3

local yourSpecBtn = CreateFrame("Button", nil, appOverlay, "BackdropTemplate")
yourSpecBtn:SetSize(80, NAV_H)
yourSpecBtn:SetPoint("TOPLEFT", 10, NAV_Y)
yourSpecBtn:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8x8", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 8, insets = { left = 2, right = 2, top = 2, bottom = 2 } })
local yourSpecFs = yourSpecBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
yourSpecFs:SetPoint("CENTER")
yourSpecFs:SetText("|cff00ff00Your Spec|r")

local classButtons = {}
local classX = 96
for _, cd in ipairs(CLASS_SPECS) do
    local cb = CreateFrame("Button", nil, appOverlay, "BackdropTemplate")
    cb:SetSize(52, NAV_H)
    cb:SetPoint("TOPLEFT", classX, NAV_Y)
    cb:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8x8", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 8, insets = { left = 2, right = 2, top = 2, bottom = 2 } })
    cb:SetBackdropColor(0.1, 0.1, 0.1, 0.8)
    cb:SetBackdropBorderColor(0.3, 0.3, 0.3, 0.5)

    local cc = RAID_CLASS_COLORS[cd.file]
    local cbFs = cb:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    cbFs:SetPoint("CENTER")
    -- Abbreviate class names to fit
    local short = cd.class:sub(1, 3)
    if cc then
        cbFs:SetText(cc:WrapTextInColorCode(short))
    else
        cbFs:SetText(short)
    end

    cb:SetScript("OnEnter", function(self)
        self:SetBackdropBorderColor(0.6, 0.6, 0.6, 0.8)
    end)
    cb:SetScript("OnLeave", function(self)
        self:SetBackdropBorderColor(0.3, 0.3, 0.3, 0.5)
    end)

    cb.classData = cd
    classButtons[cd.file] = cb
    classX = classX + 52 + NAV_GAP
end

local SPEC_Y = NAV_Y - NAV_H - 4
local specBtnPool = {}

local appToggleBtn = CreateFrame("Button", nil, appOverlay, "BackdropTemplate")
appToggleBtn:SetSize(50, NAV_H)
appToggleBtn:SetPoint("TOPRIGHT", -10, SPEC_Y)
appToggleBtn:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8x8", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 8, insets = { left = 2, right = 2, top = 2, bottom = 2 } })
appToggleBtn:SetBackdropColor(0.15, 0.15, 0.15, 1)
appToggleBtn:SetBackdropBorderColor(0.5, 0.5, 0.5, 0.8)

local appToggleFs = appToggleBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
appToggleFs:SetPoint("CENTER")

local hintFs = appOverlay:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
hintFs:SetPoint("RIGHT", appToggleBtn, "LEFT", -10, 0)
hintFs:SetTextColor(unpack(ns.COLORS.TEXT_TERTIARY))
hintFs:SetText("Click trinket for tooltip")

-- ── 3-column tier display ──
local COL_TOP = SPEC_Y - NAV_H - 8
local COL_GAP = 8
local COL_COUNT = 3
local TIER_IDS = { "S", "A", "B" }
local TIER_NAMES = { S = "|cffff8000S-Tier|r", A = "|cffa335eeA-Tier|r", B = "|cff0070ddB-Tier|r" }

local tierColumns = {}
local tierRowPools = {}

for i, tierId in ipairs(TIER_IDS) do
    -- Column header
    local hdr = appOverlay:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    hdr:SetJustifyH("LEFT")

    -- Plain container (no scrollbar)
    local container = CreateFrame("Frame", nil, appOverlay)

    -- "and X more..." label
    local moreFs = appOverlay:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    moreFs:SetTextColor(0.4, 0.4, 0.4)
    moreFs:Hide()

    tierColumns[tierId] = { header = hdr, container = container, moreFs = moreFs }
    tierRowPools[tierId] = {}
end

-- ── Refresh the 3-column display ──
local function RefreshAppTrinkets()
    local specKey = appBrowseSpecKey
    if not specKey then
        specKey = ns:GetPlayerSpecKey()
    end
    if not specKey then return end

    local byStyle = ns.TRINKET_RANKINGS[appBrowseStyle]
    local rankings = byStyle and byStyle[specKey]
    if not rankings then return end

    -- Split into tiers
    local byTier = { S = {}, A = {}, B = {} }
    for _, t in ipairs(rankings) do
        if byTier[t.tier] then
            table.insert(byTier[t.tier], t)
        end
    end

    local pw = panel:GetWidth() - 20
    local colW = math.floor((pw - (COL_COUNT - 1) * COL_GAP) / COL_COUNT)
    local ROW_H = 42
    local availH = panel:GetHeight() + COL_TOP - 50  -- space for rows

    -- Equipped trinkets
    local equipped = {}
    local t1 = GetInventoryItemID("player", 13)
    local t2 = GetInventoryItemID("player", 14)
    if t1 then equipped[t1] = true end
    if t2 then equipped[t2] = true end

    for i, tierId in ipairs(TIER_IDS) do
        local col = tierColumns[tierId]
        local x = 10 + (i - 1) * (colW + COL_GAP)

        -- Position header
        col.header:ClearAllPoints()
        col.header:SetPoint("TOPLEFT", appOverlay, "TOPLEFT", x, COL_TOP)
        col.header:SetText(TIER_NAMES[tierId])

        -- Position container
        col.container:ClearAllPoints()
        col.container:SetPoint("TOPLEFT", appOverlay, "TOPLEFT", x, COL_TOP - 20)
        col.container:SetSize(colW, availH)

        -- Hide old rows
        local pool = tierRowPools[tierId]
        for _, row in ipairs(pool) do row:Hide() end
        wipe(pool)
        col.moreFs:Hide()

        -- Build rows (cap to available height)
        local items = byTier[tierId] or {}
        local maxRows = math.floor(availH / ROW_H)
        local shown = math.min(#items, maxRows)
        local yOff = 0

        for idx = 1, shown do
            local trinket = items[idx]
            local info = GetCachedItemInfo(trinket.id)
            local isEquipped = equipped[trinket.id]
            local srcInfo = ns.TRINKET_SOURCES and ns.TRINKET_SOURCES[trinket.id]

            local row = CreateFrame("Button", nil, col.container)
            row:SetSize(colW, ROW_H)
            row:SetPoint("TOPLEFT", 0, -yOff)
            row:RegisterForClicks("LeftButtonUp")

            -- Background
            if isEquipped then
                local bg = row:CreateTexture(nil, "BACKGROUND")
                bg:SetAllPoints()
                bg:SetColorTexture(0, 1, 0, 0.08)
            elseif idx % 2 == 0 then
                local bg = row:CreateTexture(nil, "BACKGROUND")
                bg:SetAllPoints()
                bg:SetColorTexture(1, 1, 1, 0.02)
            end

            local hl = row:CreateTexture(nil, "HIGHLIGHT")
            hl:SetAllPoints()
            hl:SetColorTexture(1, 1, 1, 0.04)

            -- Icon
            local iconFrame = CreateFrame("Frame", nil, row)
            iconFrame:SetSize(32, 32)
            iconFrame:SetPoint("LEFT", 4, 0)

            local iconTex = iconFrame:CreateTexture(nil, "ARTWORK")
            iconTex:SetPoint("TOPLEFT", 1, -1)
            iconTex:SetPoint("BOTTOMRIGHT", -1, 1)
            iconTex:SetTexCoord(0.08, 0.92, 0.08, 0.92)
            iconTex:SetTexture(info.icon)

            SetItemBorder(iconFrame, tierId)

            if isEquipped then
                local check = iconFrame:CreateTexture(nil, "OVERLAY")
                check:SetSize(14, 14)
                check:SetPoint("BOTTOMRIGHT", 2, -2)
                check:SetAtlas("common-icon-checkmark")
            end

            -- Name
            local nameFs = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
            nameFs:SetPoint("TOPLEFT", iconFrame, "TOPRIGHT", 6, -2)
            nameFs:SetWidth(colW - 60)
            nameFs:SetJustifyH("LEFT")
            nameFs:SetWordWrap(false)
            if isEquipped then
                nameFs:SetText("|cff00ff00" .. info.name .. "|r")
            else
                nameFs:SetTextColor(0.9, 0.9, 0.9)
                nameFs:SetText(info.name)
            end

            -- Source
            local srcFs = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
            srcFs:SetPoint("TOPLEFT", nameFs, "BOTTOMLEFT", 0, -2)
            srcFs:SetWidth(colW - 60)
            srcFs:SetJustifyH("LEFT")
            srcFs:SetWordWrap(false)
            srcFs:SetTextColor(0.45, 0.45, 0.45)
            local srcText = ""
            if srcInfo then
                srcText = srcInfo.source
                if srcInfo.boss and srcInfo.boss ~= "" then
                    srcText = srcText .. " - " .. srcInfo.boss
                end
            end
            srcFs:SetText(srcText)

            -- Rank number (right-aligned, dim)
            local rankFs = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
            rankFs:SetPoint("RIGHT", -4, 0)
            rankFs:SetTextColor(0.35, 0.35, 0.35)
            rankFs:SetText("#" .. trinket.rank)

            -- Click to toggle tooltip, shift-click to link
            local tid = trinket.id
            local tEquipped = isEquipped
            row._tooltipShown = false
            row:SetScript("OnClick", function(self, button)
                if IsShiftKeyDown() then
                    local link = GetItemLink(tid)
                    if link and ChatEdit_GetActiveWindow() then
                        ChatEdit_InsertLink(link)
                    elseif link then
                        print(link)
                    end
                else
                    if self._tooltipShown then
                        GameTooltip:Hide()
                        self._tooltipShown = false
                    else
                        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
                        -- If equipped, show actual item with real ilvl
                        local shown = false
                        if tEquipped then
                            local t1id = GetInventoryItemID("player", 13)
                            local t2id = GetInventoryItemID("player", 14)
                            if t1id == tid then
                                GameTooltip:SetInventoryItem("player", 13)
                                shown = true
                            elseif t2id == tid then
                                GameTooltip:SetInventoryItem("player", 14)
                                shown = true
                            end
                        end
                        if not shown then
                            GameTooltip:SetItemByID(tid)
                            GameTooltip:AddLine(" ")
                            GameTooltip:AddLine("Rankings simmed at ilvl 272 (Myth 1/6)", 0.5, 0.5, 0.5)
                            GameTooltip:AddLine("Tooltip shows base item", 0.35, 0.35, 0.35)
                        end
                        GameTooltip:Show()
                        self._tooltipShown = true
                    end
                end
            end)
            row:SetScript("OnLeave", function(self)
                if self._tooltipShown then
                    GameTooltip:Hide()
                    self._tooltipShown = false
                end
            end)

            table.insert(pool, row)
            yOff = yOff + ROW_H
        end

        -- "and X more..." if truncated
        local remaining = #items - shown
        if remaining > 0 then
            col.moreFs:ClearAllPoints()
            col.moreFs:SetPoint("TOPLEFT", appOverlay, "TOPLEFT", x + 4, COL_TOP - 20 - yOff - 4)
            col.moreFs:SetText("|cff555555and " .. remaining .. " more...|r")
            col.moreFs:Show()
        end
    end
end

-- ── Navigation logic ──
local selectedClassFile = nil

local function UpdateAppNavHighlights()
    -- Your Spec button
    if not appBrowseSpecKey then
        yourSpecBtn:SetBackdropColor(0.0, 0.3, 0.1, 1)
        yourSpecBtn:SetBackdropBorderColor(0.0, 1.0, 0.3, 0.8)
    else
        yourSpecBtn:SetBackdropColor(0.1, 0.1, 0.1, 0.8)
        yourSpecBtn:SetBackdropBorderColor(0.3, 0.3, 0.3, 0.5)
    end

    -- Class buttons
    for file, cb in pairs(classButtons) do
        if file == selectedClassFile then
            local cc = RAID_CLASS_COLORS[file]
            if cc then
                cb:SetBackdropColor(cc.r * 0.3, cc.g * 0.3, cc.b * 0.3, 1)
                cb:SetBackdropBorderColor(cc.r, cc.g, cc.b, 0.8)
            end
        else
            cb:SetBackdropColor(0.1, 0.1, 0.1, 0.8)
            cb:SetBackdropBorderColor(0.3, 0.3, 0.3, 0.5)
        end
    end

    -- ST/AOE toggle
    if appBrowseStyle == "ST" then
        appToggleFs:SetText("|cff00ff00ST|r")
    else
        appToggleFs:SetText("|cffff8800AOE|r")
    end
end

local function ShowSpecButtons(classData)
    -- Hide all existing spec buttons
    for _, sb in ipairs(specBtnPool) do sb:Hide() end

    if not classData then return end

    local sx = 10
    for i, specKey in ipairs(classData.specs) do
        local sb = specBtnPool[i]
        if not sb then
            sb = CreateFrame("Button", nil, appOverlay, "BackdropTemplate")
            sb:SetHeight(NAV_H)
            sb:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8x8", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 8, insets = { left = 2, right = 2, top = 2, bottom = 2 } })
            sb.fs = sb:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
            sb.fs:SetPoint("CENTER")
            specBtnPool[i] = sb
        end

        local label = classData.labels[i] or specKey
        sb:SetWidth(math.max(sb.fs:GetStringWidth() + 16, 60))
        sb.fs:SetText(label)
        sb:SetWidth(sb.fs:GetStringWidth() + 16)
        sb:ClearAllPoints()
        sb:SetPoint("TOPLEFT", appOverlay, "TOPLEFT", sx, SPEC_Y)

        local isActive = (appBrowseSpecKey == specKey)
        if isActive then
            local cc = RAID_CLASS_COLORS[classData.file]
            if cc then
                sb:SetBackdropColor(cc.r * 0.3, cc.g * 0.3, cc.b * 0.3, 1)
                sb:SetBackdropBorderColor(cc.r, cc.g, cc.b, 0.8)
            end
        else
            sb:SetBackdropColor(0.1, 0.1, 0.1, 0.8)
            sb:SetBackdropBorderColor(0.3, 0.3, 0.3, 0.5)
        end

        sb:SetScript("OnClick", function()
            appBrowseSpecKey = specKey
            ShowSpecButtons(classData)
            UpdateAppNavHighlights()
            RefreshAppTrinkets()
        end)
        sb:SetScript("OnEnter", function(self) self:SetBackdropBorderColor(0.6, 0.6, 0.6, 0.8) end)
        sb:SetScript("OnLeave", function(self)
            if appBrowseSpecKey == specKey then
                local cc = RAID_CLASS_COLORS[classData.file]
                if cc then self:SetBackdropBorderColor(cc.r, cc.g, cc.b, 0.8) return end
            end
            self:SetBackdropBorderColor(0.3, 0.3, 0.3, 0.5)
        end)

        sb:Show()
        sx = sx + sb:GetWidth() + NAV_GAP
    end
end

-- Wire up class buttons
for _, cd in ipairs(CLASS_SPECS) do
    local cb = classButtons[cd.file]
    cb:SetScript("OnClick", function()
        selectedClassFile = cd.file
        appBrowseSpecKey = cd.specs[1]  -- default to first spec
        ShowSpecButtons(cd)
        UpdateAppNavHighlights()
        RefreshAppTrinkets()
    end)
end

-- Wire up "Your Spec" button
yourSpecBtn:SetScript("OnClick", function()
    appBrowseSpecKey = nil
    selectedClassFile = nil
    ShowSpecButtons(nil)
    UpdateAppNavHighlights()
    RefreshAppTrinkets()
end)

-- Wire up ST/AOE toggle
appToggleBtn:SetScript("OnClick", function()
    appBrowseStyle = appBrowseStyle == "ST" and "AOE" or "ST"
    UpdateAppNavHighlights()
    RefreshAppTrinkets()
end)

-- ── SetTrinketAppMode ──
function ns:SetTrinketAppMode(enabled, contentWidth, contentHeight)
    ns._trinketAppMode = enabled
    if enabled then
        panel:SetScale(1)  -- reset scale from CharacterFrame matching
        panel:SetBackdrop(nil)
        local dw, dh = ns:GetAppFrameSize()
        panel:SetSize(contentWidth or dw, contentHeight or (dh - 34))
        -- Hide normal panel content, show app overlay
        title:Hide()
        specLabel:Hide()
        disclaimer:Hide()
        allSpecsBtn:Hide()
        toggleBtn:Hide()
        div:Hide()
        scrollFrame:Hide()
        appOverlay:Show()
        UpdateAppNavHighlights()
        ns:RefreshTrinketPanel()
    else
        panel:SetScale(1)  -- reset scale
        panel:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8x8",
            edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
            edgeSize = 16,
            insets = { left = 4, right = 4, top = 4, bottom = 4 },
        })
        panel:SetBackdropColor(0.08, 0.08, 0.08, 0.95)
        panel:SetBackdropBorderColor(0.6, 0.6, 0.6, 1)
        panel:SetSize(PANEL_WIDTH, PANEL_HEIGHT)
        -- Restore normal panel content
        title:Show()
        specLabel:Show()
        disclaimer:Show()
        allSpecsBtn:Show()
        toggleBtn:Show()
        div:Show()
        scrollFrame:Show()
        appOverlay:Hide()
        scrollContent:SetWidth(PANEL_WIDTH - 38)
        specLabel:SetWidth(PANEL_WIDTH - 24)
        disclaimer:SetWidth(PANEL_WIDTH - 24)
    end
end

------------------------------------------------------------
-- Item info cache + pre-warming
------------------------------------------------------------
local itemCache = {}
local pendingItemIDs = {}  -- set of item IDs we're waiting on
local preWarmDone = false

local function PreWarmItemCache()
    if not ns.TRINKET_RANKINGS then return end
    wipe(pendingItemIDs)
    local seen = {}
    for _, style in ipairs({"ST", "AOE"}) do
        local byStyle = ns.TRINKET_RANKINGS[style]
        if byStyle then
            for _, rankings in pairs(byStyle) do
                for _, trinket in ipairs(rankings) do
                    if not seen[trinket.id] then
                        seen[trinket.id] = true
                        local name = C_Item.GetItemNameByID(trinket.id)
                        if not name then
                            pendingItemIDs[trinket.id] = true
                            C_Item.RequestLoadItemDataByID(trinket.id)
                        end
                    end
                end
            end
        end
    end
    if not next(pendingItemIDs) then
        preWarmDone = true
    end
end

-- Request all trinket item data immediately at file load time
-- ns.TRINKET_RANKINGS is already populated from TrinketData.lua
PreWarmItemCache()

local sourceTooltip = CreateFrame("GameTooltip", "YYHSourceTooltip", nil, "GameTooltipTemplate")
sourceTooltip:SetOwner(WorldFrame, "ANCHOR_NONE")

local function ExtractSourceFromTooltip(itemID)
    sourceTooltip:ClearLines()
    local ok = pcall(sourceTooltip.SetItemByID, sourceTooltip, itemID)
    if not ok then return nil end

    for i = 1, sourceTooltip:NumLines() do
        local line = _G["YYHSourceTooltipTextLeft" .. i]
        if line then
            local text = line:GetText() or ""
            if text:find("Drop") or text:find("Dropped") or text:find("Zone Drop")
                or text:find("Profession") or text:find("Crafted")
                or text:find("Delve") or text:find("Vendor")
                or text:find("Voidspire") or text:find("Dreamrift")
                or text:find("Quel'danas") or text:find("Dungeon") then
                return text
            end
        end
    end
    return nil
end

local function GetSourceText(itemID)
    local info = ns.TRINKET_SOURCES and ns.TRINKET_SOURCES[itemID]
    if info then
        if info.boss and info.boss ~= "" then
            return info.source .. " — " .. info.boss
        end
        return info.source
    end
    return ExtractSourceFromTooltip(itemID)
end

GetCachedItemInfo = function(itemID)
    if itemCache[itemID] then return itemCache[itemID] end

    local name = C_Item.GetItemNameByID(itemID)
    local icon = C_Item.GetItemIconByID(itemID)
    local quality = C_Item.GetItemQualityByID(itemID)
    local source = GetSourceText(itemID)

    if name and icon then
        itemCache[itemID] = { name = name, icon = icon, quality = quality or 3, source = source }
        return itemCache[itemID]
    end

    C_Item.RequestLoadItemDataByID(itemID)
    return { name = "Loading...", icon = 134400, quality = 1, source = nil }
end

GetItemLink = function(itemID)
    local _, link = C_Item.GetItemInfo(itemID)
    return link
end

------------------------------------------------------------
-- Detail popup (shown when clicking a trinket)
------------------------------------------------------------
local detailFrame = CreateFrame("Frame", "YippYappTrinketDetail", UIParent, "BackdropTemplate")
detailFrame:SetSize(260, 160)
detailFrame:SetFrameStrata("DIALOG")
detailFrame:SetBackdrop({
    bgFile = "Interface\\Buttons\\WHITE8x8",
    edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
    edgeSize = 16,
    insets = { left = 4, right = 4, top = 4, bottom = 4 },
})
detailFrame:SetBackdropColor(0.06, 0.06, 0.06, 0.97)
detailFrame:SetBackdropBorderColor(0.5, 0.5, 0.5, 1)
detailFrame:EnableMouse(true)
detailFrame:SetClampedToScreen(true)
detailFrame:Hide()

-- Close on click outside or ESC
detailFrame:SetScript("OnShow", function()
    tinsert(UISpecialFrames, "YippYappTrinketDetail")
end)
detailFrame:SetScript("OnHide", function()
    for i = #UISpecialFrames, 1, -1 do
        if UISpecialFrames[i] == "YippYappTrinketDetail" then
            table.remove(UISpecialFrames, i)
            break
        end
    end
end)

-- Close button
local detailClose = CreateFrame("Button", nil, detailFrame, "UIPanelCloseButton")
detailClose:SetPoint("TOPRIGHT", 0, 0)
detailClose:SetSize(20, 20)

-- Icon
local detailIcon = detailFrame:CreateTexture(nil, "ARTWORK")
detailIcon:SetSize(36, 36)
detailIcon:SetPoint("TOPLEFT", 12, -12)
detailIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

-- Icon border
local detailIconBorder = detailFrame:CreateTexture(nil, "OVERLAY")
detailIconBorder:SetPoint("TOPLEFT", detailIcon, "TOPLEFT", -2, 2)
detailIconBorder:SetPoint("BOTTOMRIGHT", detailIcon, "BOTTOMRIGHT", 2, -2)
detailIconBorder:SetTexture("Interface\\Common\\WhiteIconFrame")

-- Name
local detailName = detailFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
detailName:SetPoint("TOPLEFT", detailIcon, "TOPRIGHT", 8, -2)
detailName:SetWidth(180)
detailName:SetJustifyH("LEFT")
detailName:SetWordWrap(false)

-- Tier + Rank line
local detailTier = detailFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
detailTier:SetPoint("TOPLEFT", detailName, "BOTTOMLEFT", 0, -2)

-- Divider
local detailDiv = detailFrame:CreateTexture(nil, "ARTWORK")
detailDiv:SetHeight(1)
detailDiv:SetPoint("TOPLEFT", 12, -54)
detailDiv:SetPoint("TOPRIGHT", -12, -54)
detailDiv:SetColorTexture(0.4, 0.4, 0.4, 0.5)

-- Source header
local detailSourceHdr = detailFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
detailSourceHdr:SetPoint("TOPLEFT", 12, -60)
detailSourceHdr:SetText("|cffaaaaaaSource:|r")

-- Source text
local detailSource = detailFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
detailSource:SetPoint("TOPLEFT", detailSourceHdr, "BOTTOMLEFT", 0, -4)
detailSource:SetWidth(236)
detailSource:SetJustifyH("LEFT")
detailSource:SetWordWrap(true)

-- Hint text
local detailHint = detailFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
detailHint:SetPoint("BOTTOMLEFT", 12, 10)
detailHint:SetTextColor(0.4, 0.4, 0.4)
detailHint:SetText("Shift-click to link in chat")

-- Maps our tier to WoW item quality index (must be defined before ShowTrinketDetail)
local TIER_QUALITY = {
    S = 5, -- Legendary
    A = 4, -- Epic
    B = 3, -- Rare
}

local function ShowTrinketDetail(anchorFrame, trinketID, tier, rank, itemInfo)
    local tc = TIER_COLORS[tier] or TIER_COLORS.B
    local quality = TIER_QUALITY[tier] or 3

    detailIcon:SetTexture(itemInfo.icon)

    local r, g, b = C_Item.GetItemQualityColor(quality)
    detailIconBorder:SetVertexColor(r, g, b)

    detailName:SetText(itemInfo.name)
    detailName:SetTextColor(r, g, b)

    local tierLabel = TIER_LABELS[tier] or tier
    detailTier:SetText(tierLabel .. "  |cffaaaaaaRank " .. rank .. "|r")

    -- Build source text from hardcoded data first, tooltip fallback
    local srcInfo = ns.TRINKET_SOURCES and ns.TRINKET_SOURCES[trinketID]
    if srcInfo then
        local lines = srcInfo.source
        if srcInfo.boss and srcInfo.boss ~= "" then
            lines = lines .. "\n" .. srcInfo.boss
        end
        detailSource:SetText(lines)
        detailSource:SetTextColor(0.9, 0.9, 0.9)
    elseif itemInfo.source and itemInfo.source ~= "" then
        detailSource:SetText(itemInfo.source)
        detailSource:SetTextColor(0.9, 0.9, 0.9)
    else
        detailSource:SetText("Source unknown")
        detailSource:SetTextColor(0.5, 0.5, 0.5)
    end

    -- Resize frame to fit content
    local sourceHeight = detailSource:GetStringHeight() or 14
    detailFrame:SetHeight(90 + sourceHeight + 24)

    -- Position near the clicked row
    detailFrame:ClearAllPoints()
    detailFrame:SetPoint("TOPLEFT", anchorFrame, "TOPRIGHT", 4, 0)
    detailFrame:Show()
end

------------------------------------------------------------
-- WoW item quality border (same texture as bag items)
------------------------------------------------------------
SetItemBorder = function(iconFrame, tier)
    if not iconFrame._border then
        iconFrame._border = iconFrame:CreateTexture(nil, "OVERLAY")
        iconFrame._border:SetPoint("TOPLEFT", -2, 2)
        iconFrame._border:SetPoint("BOTTOMRIGHT", 2, -2)
        iconFrame._border:SetTexture("Interface\\Common\\WhiteIconFrame")
    end

    local quality = TIER_QUALITY[tier] or 3
    local r, g, b = C_Item.GetItemQualityColor(quality)
    iconFrame._border:SetVertexColor(r, g, b)
    iconFrame._border:Show()
end

------------------------------------------------------------
-- Refresh
------------------------------------------------------------
local refreshRetryTimer = nil

function ns:RefreshTrinketPanel()
    -- Re-warm cache if needed (items may not be loaded yet)
    if not preWarmDone then
        PreWarmItemCache()
        if next(pendingItemIDs) then
            eventFrame:RegisterEvent("ITEM_DATA_LOAD_RESULT")
        end
    end

    -- In app mode, refresh the 3-column display instead
    if ns._trinketAppMode and appOverlay:IsShown() then
        RefreshAppTrinkets()
        -- Check if any items are still loading; schedule retry
        local hasLoading = false
        local specKey = appBrowseSpecKey or ns:GetPlayerSpecKey()
        if specKey then
            local byStyle = ns.TRINKET_RANKINGS[appBrowseStyle]
            local rankings = byStyle and byStyle[specKey]
            if rankings then
                for _, trinket in ipairs(rankings) do
                    local info = itemCache[trinket.id]
                    if not info or info.name == "Loading..." then
                        hasLoading = true
                        break
                    end
                end
            end
        end
        if hasLoading and not refreshRetryTimer then
            refreshRetryTimer = C_Timer.After(0.3, function()
                refreshRetryTimer = nil
                if panel:IsShown() then
                    ns:RefreshTrinketPanel()
                end
            end)
        end
        return
    end

    for _, row in ipairs(rows) do row:Hide() end
    wipe(rows)

    local specKey = ns:GetPlayerSpecKey()
    if not specKey then
        specLabel:SetText("No spec data")
        return
    end

    local rankings = ns:GetTrinketRankings(specKey)
    if not rankings then
        specLabel:SetText(specKey:gsub("_", " ") .. " — no data")
        return
    end

    local isTank = ns.TANK_SPECS and ns.TANK_SPECS[specKey]
    local specIndex = GetSpecialization()
    if specIndex then
        local _, specName = GetSpecializationInfo(specIndex)
        local suffix = isTank and " |cffff8800(DPS only)|r" or ""
        specLabel:SetText((specName or "") .. suffix)
    end

    local equippedTrinkets = {}
    local t1 = GetInventoryItemID("player", 13)
    local t2 = GetInventoryItemID("player", 14)
    if t1 then equippedTrinkets[t1] = true end
    if t2 then equippedTrinkets[t2] = true end

    -- If inside an instance, sort trinkets from this instance to the top
    local sortedRankings = {}
    for _, trinket in ipairs(rankings) do
        table.insert(sortedRankings, trinket)
    end

    if currentInstanceSource then
        specLabel:SetText((specLabel:GetText() or "") .. "  |cff00aaff" .. currentInstanceSource .. "|r")
        table.sort(sortedRankings, function(a, b)
            local srcA = ns.TRINKET_SOURCES and ns.TRINKET_SOURCES[a.id]
            local srcB = ns.TRINKET_SOURCES and ns.TRINKET_SOURCES[b.id]
            local matchA = srcA and srcA.source == currentInstanceSource
            local matchB = srcB and srcB.source == currentInstanceSource
            if matchA ~= matchB then return matchA end
            return a.rank < b.rank
        end)
    end

    local yOffset = 0
    local count = 0
    local drawnDivider = false

    for _, trinket in ipairs(sortedRankings) do
        if count >= MAX_DISPLAY then break end
        count = count + 1

        -- Check if this trinket is from the current instance
        local srcInfo = ns.TRINKET_SOURCES and ns.TRINKET_SOURCES[trinket.id]
        local isFromInstance = currentInstanceSource and srcInfo and srcInfo.source == currentInstanceSource
        local isDimmed = currentInstanceSource and not isFromInstance

        -- Draw a divider between instance trinkets and the rest
        if currentInstanceSource and not isFromInstance and not drawnDivider then
            drawnDivider = true
            local divTex = scrollContent:CreateTexture(nil, "ARTWORK")
            divTex:SetHeight(1)
            divTex:SetPoint("TOPLEFT", 4, -yOffset)
            divTex:SetPoint("RIGHT", scrollContent, "RIGHT", -4, 0)
            divTex:SetColorTexture(0.0, 0.67, 1.0, 0.4)
            table.insert(rows, divTex)
            yOffset = yOffset + 4
        end

        local isEquipped = equippedTrinkets[trinket.id]
        local itemInfo = GetCachedItemInfo(trinket.id)
        local tc = TIER_COLORS[trinket.tier] or TIER_COLORS.B

        local rowWidth = panel:GetWidth() - 38
        local appMode = ns._trinketAppMode
        local rowH = appMode and 48 or ROW_HEIGHT
        local iconSz = appMode and 36 or ICON_SIZE
        local nameFont = appMode and "GameFontNormal" or "GameFontNormalSmall"

        local row = CreateFrame("Button", nil, scrollContent)
        row:SetSize(rowWidth, rowH)
        row:SetPoint("TOPLEFT", 0, -yOffset)
        row:RegisterForClicks("LeftButtonUp")

        -- Row background
        if isFromInstance then
            local bg = row:CreateTexture(nil, "BACKGROUND")
            bg:SetAllPoints()
            bg:SetColorTexture(0, 0.4, 1.0, 0.1)
        elseif isEquipped then
            local bg = row:CreateTexture(nil, "BACKGROUND")
            bg:SetAllPoints()
            bg:SetColorTexture(0, 1, 0, 0.1)
        elseif count % 2 == 0 then
            local bg = row:CreateTexture(nil, "BACKGROUND")
            bg:SetAllPoints()
            bg:SetColorTexture(1, 1, 1, 0.025)
        end

        -- Dim non-instance trinkets when inside a dungeon/raid
        if isDimmed then
            row:SetAlpha(0.4)
        end

        local hl = row:CreateTexture(nil, "HIGHLIGHT")
        hl:SetAllPoints()
        hl:SetColorTexture(1, 1, 1, 0.05)

        -- Item icon with WoW quality border
        local iconFrame = CreateFrame("Frame", nil, row)
        iconFrame:SetSize(iconSz, iconSz)
        iconFrame:SetPoint("LEFT", 6, 0)

        local iconTex = iconFrame:CreateTexture(nil, "ARTWORK")
        iconTex:SetPoint("TOPLEFT", 1, -1)
        iconTex:SetPoint("BOTTOMRIGHT", -1, 1)
        iconTex:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        iconTex:SetTexture(itemInfo.icon)

        SetItemBorder(iconFrame, trinket.tier)

        -- Equipped checkmark
        if isEquipped then
            local check = iconFrame:CreateTexture(nil, "OVERLAY")
            check:SetSize(14, 14)
            check:SetPoint("BOTTOMRIGHT", 2, -2)
            check:SetAtlas("common-icon-checkmark")
        end

        -- Tier badge (text only, colored)
        local tierFont = appMode and "GameFontNormal" or "GameFontNormalSmall"
        local tierStr = row:CreateFontString(nil, "OVERLAY", tierFont)
        tierStr:SetPoint("TOPLEFT", iconFrame, "TOPRIGHT", 6, appMode and -2 or 0)
        tierStr:SetText(TIER_LABELS[trinket.tier] or trinket.tier)

        -- Item name (next to tier badge)
        local nameStr = row:CreateFontString(nil, "OVERLAY", nameFont)
        nameStr:SetPoint("LEFT", tierStr, "RIGHT", 4, 0)
        nameStr:SetWidth(rowWidth - (appMode and 100 or 90))
        nameStr:SetJustifyH("LEFT")
        nameStr:SetWordWrap(false)

        if isEquipped then
            nameStr:SetText("|cff00ff00" .. itemInfo.name .. "|r")
        else
            nameStr:SetTextColor(0.9, 0.9, 0.9)
            nameStr:SetText(itemInfo.name)
        end

        -- Source line (below tier + name)
        local sourceStr = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        sourceStr:SetPoint("TOPLEFT", tierStr, "BOTTOMLEFT", 0, appMode and -3 or -2)
        sourceStr:SetPoint("RIGHT", row, "RIGHT", -4, 0)
        sourceStr:SetJustifyH("LEFT")
        sourceStr:SetWordWrap(false)
        sourceStr:SetTextColor(0.45, 0.45, 0.45)
        sourceStr:SetText(itemInfo.source or "")

        -- Click handling
        local trinketID = trinket.id
        local trinketTier = trinket.tier
        local trinketRank = trinket.rank
        row:SetScript("OnClick", function(self, button)
            if IsShiftKeyDown() then
                local link = GetItemLink(trinketID)
                if link and ChatEdit_GetActiveWindow() then
                    ChatEdit_InsertLink(link)
                elseif link then
                    print(link)
                end
            else
                -- Show detail popup
                local info = GetCachedItemInfo(trinketID)
                ShowTrinketDetail(self, trinketID, trinketTier, trinketRank, info)
            end
        end)

        row:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            GameTooltip:SetItemByID(trinketID)
            GameTooltip:Show()
        end)
        row:SetScript("OnLeave", GameTooltip_Hide)

        table.insert(rows, row)
        yOffset = yOffset + rowH
    end

    scrollContent:SetHeight(math.max(yOffset, 1))

    -- If any items are still loading, schedule a retry
    local hasLoading = false
    for _, trinket in ipairs(sortedRankings) do
        local info = itemCache[trinket.id]
        if not info or info.name == "Loading..." then
            hasLoading = true
            break
        end
    end
    if hasLoading and not refreshRetryTimer then
        refreshRetryTimer = C_Timer.After(0.5, function()
            refreshRetryTimer = nil
            if panel:IsShown() then
                ns:RefreshTrinketPanel()
            end
        end)
    end
end

------------------------------------------------------------
-- Hook CharacterFrame
------------------------------------------------------------
local function AnchorToCharacterFrame()
    panel:ClearAllPoints()
    if CharacterFrame and CharacterFrame:IsShown() then
        local anchor, topPt, botPt = GetCharRightAnchor()
        local hasChonky = C_AddOns and C_AddOns.IsAddOnLoaded and C_AddOns.IsAddOnLoaded("ChonkyCharacterSheet")
        if hasChonky then
            panel:SetParent(CharacterFrame)
        else
            panel:SetParent(UIParent)
        end
        panel:SetScale(1)
        panel:SetPoint("TOPLEFT", anchor, topPt, 2, 0)
        panel:SetPoint("BOTTOMLEFT", anchor, botPt, 2, 0)
        panel:SetFrameStrata("HIGH")
    end
end

local hookRegistered = false

local function OnCharacterFrameShow()
    if ns.upgradeVendorOpen then return end
    if panel.inAppMode then return end

    -- Always show the tab when character frame opens
    tab:Show()
    AnchorTab()

    local specKey = ns:GetPlayerSpecKey()
    local rankings = specKey and ns:GetTrinketRankings(specKey)
    if not specKey or not rankings then
        return
    end

    if isExpanded then
        ns:RefreshTrinketPanel()
        AnchorToCharacterFrame()
        panel:Show()
        AnchorTab()
    end

    -- Delayed re-anchor: addons like ChonkyCharacterSheet resize/scale
    -- CharacterFrame after OnShow, so re-anchor after they're done
    C_Timer.After(0.1, function()
        if CharacterFrame and CharacterFrame:IsShown() and not panel.inAppMode then
            AnchorTab()
            if isExpanded and panel:IsShown() then
                AnchorToCharacterFrame()
            end
        end
    end)
end

local function RegisterCharacterFrameHook()
    if hookRegistered then return end
    if not CharacterFrame then return end

    CharacterFrame:HookScript("OnShow", OnCharacterFrameShow)
    CharacterFrame:HookScript("OnHide", function()
        -- Only hide if the panel isn't in app mode
        if not panel.inAppMode then
            panel:Hide()
        end
        tab:Hide()
    end)
    CharacterFrame:HookScript("OnSizeChanged", function()
        -- Re-anchor when frame is resized (e.g. by ChonkyCharacterSheet)
        if CharacterFrame:IsShown() and not panel.inAppMode then
            AnchorTab()
            if isExpanded and panel:IsShown() then
                AnchorToCharacterFrame()
            end
        end
    end)

    hookRegistered = true

    -- If character frame is already showing right now, trigger immediately
    if CharacterFrame:IsShown() then
        OnCharacterFrameShow()
    end
end

-- Try hooking on multiple events to catch CharacterFrame whenever it loads
local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("ADDON_LOADED")
eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
eventFrame:RegisterEvent("ITEM_DATA_LOAD_RESULT")

local refreshPending = false
local function ScheduleRefresh()
    if refreshPending then return end
    if not panel:IsShown() then return end
    refreshPending = true
    C_Timer.After(0.1, function()
        refreshPending = false
        if panel:IsShown() then
            ns:RefreshTrinketPanel()
        end
    end)
end

eventFrame:SetScript("OnEvent", function(self, event, arg1)
    if event == "ADDON_LOADED" then
        if arg1 == "YippYappHelper" then
            isExpanded = GetExpanded()
        end
        if arg1 == "Blizzard_CharacterFrame" or arg1 == "YippYappHelper" then
            RegisterCharacterFrameHook()
        end
    elseif event == "PLAYER_ENTERING_WORLD" then
        RegisterCharacterFrameHook()
    elseif event == "ITEM_DATA_LOAD_RESULT" then
        local itemID = arg1
        if itemID and pendingItemIDs[itemID] then
            pendingItemIDs[itemID] = nil
            local name = C_Item.GetItemNameByID(itemID)
            local icon = C_Item.GetItemIconByID(itemID)
            local quality = C_Item.GetItemQualityByID(itemID)
            if name and icon then
                local source = GetSourceText(itemID)
                itemCache[itemID] = { name = name, icon = icon, quality = quality or 3, source = source }
            end
            ScheduleRefresh()
        end
        if not next(pendingItemIDs) then
            preWarmDone = true
            self:UnregisterEvent("ITEM_DATA_LOAD_RESULT")
        end
    end
end)

-- Also try immediately
RegisterCharacterFrameHook()
