local _, ns = ...

------------------------------------------------------------
-- Spec ID -> TrinketData key mapping
------------------------------------------------------------
local SPEC_KEY_MAP = {
    -- DPS specs
    [251]  = "Death_Knight",       [252]  = "Death_Knight_Unholy",
    [577]  = "Demon_Hunter_Havoc", [1480] = "Demon_Hunter",
    [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"

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)
panel:Hide()
ns.TrinketPanel = panel

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()

local function AnchorTab()
    if not CharacterFrame or not CharacterFrame:IsShown() then return end
    tab:ClearAllPoints()
    if isExpanded and panel:IsShown() then
        tab:SetParent(panel)
        tab:SetPoint("TOPLEFT", panel, "TOPRIGHT", -3, -20)
    else
        tab:SetParent(CharacterFrame)
        tab:SetPoint("TOPLEFT", CharacterFrame, "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()

    -- Start panel at zero width, animate to full
    panel:ClearAllPoints()
    panel:SetPoint("TOPLEFT", CharacterFrame, "TOPRIGHT", 2, 0)
    panel:SetPoint("BOTTOMLEFT", CharacterFrame, "BOTTOMRIGHT", 2, 0)
    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)

-- "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()
    if ns.RaidFrame then
        if ns.RaidFrame:IsShown() then
            ns.RaidFrame:Hide()
        else
            ns.RaidFrame:Show()
            if ns.SetRaidTab then
                ns.SetRaidTab("trinkets")
            end
        end
    end
end)
allSpecsBtn:SetScript("OnEnter", function(self)
    self:SetBackdropBorderColor(0.0, 0.67, 1.0, 1)
    GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
    GameTooltip:SetText("View trinket rankings 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, -46)
scrollFrame:SetPoint("BOTTOMRIGHT", -26, 8)

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

local rows = {}

------------------------------------------------------------
-- 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

local function GetCachedItemInfo(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

local function GetItemLink(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: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")

local function ShowTrinketDetail(anchorFrame, trinketID, tier, rank, itemInfo)
    local tc = TIER_COLORS[tier] or TIER_COLORS.B
    local quality = TIER_QUALITY and 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)
------------------------------------------------------------
-- Maps our tier to WoW item quality index
local TIER_QUALITY = {
    S = 5, -- Legendary
    A = 4, -- Epic
    B = 3, -- Rare
}

local function SetItemBorder(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
------------------------------------------------------------
function ns:RefreshTrinketPanel()
    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 row = CreateFrame("Button", nil, scrollContent)
        row:SetSize(PANEL_WIDTH - 38, ROW_HEIGHT)
        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(ICON_SIZE, ICON_SIZE)
        iconFrame:SetPoint("TOPLEFT", 4, -4)

        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 tierStr = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        tierStr:SetPoint("TOPLEFT", iconFrame, "TOPRIGHT", 4, 0)
        tierStr:SetText(TIER_LABELS[trinket.tier] or trinket.tier)

        -- Item name (next to tier badge)
        local nameStr = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        nameStr:SetPoint("LEFT", tierStr, "RIGHT", 4, 0)
        nameStr:SetWidth(PANEL_WIDTH - 130)
        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, -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 + ROW_HEIGHT
    end

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

------------------------------------------------------------
-- Hook CharacterFrame
------------------------------------------------------------
local function AnchorToCharacterFrame()
    panel:ClearAllPoints()
    if CharacterFrame and CharacterFrame:IsShown() then
        panel:SetPoint("TOPLEFT", CharacterFrame, "TOPRIGHT", 2, 0)
        panel:SetPoint("BOTTOMLEFT", CharacterFrame, "BOTTOMRIGHT", 2, 0)
    end
end

local hookRegistered = false

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

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

    -- Always show the tab
    tab:Show()

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

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

    CharacterFrame:HookScript("OnShow", OnCharacterFrameShow)
    CharacterFrame:HookScript("OnHide", function()
        panel:Hide()
        tab:Hide()
    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()
