SFrames.TalentTree = {}

local ICON_SIZE = 36
local ICON_SPACING_X = 14
local ICON_SPACING_Y = 14
local TAB_WIDTH = 220
local FRAME_WIDTH = (TAB_WIDTH * 3) + 40
local FRAME_HEIGHT = 550

--------------------------------------------------------------------------------
-- Theme: Pink Cat-Paw
--------------------------------------------------------------------------------
local T = SFrames.Theme:Extend()
local function GetHex()
    return (SFrames.Theme and SFrames.Theme:GetAccentHex()) or "ffffb3d9"
end

--------------------------------------------------------------------------------
-- Class definitions
--------------------------------------------------------------------------------
local CLASS_LIST = {
    { key = "WARRIOR",  name = "战士",   color = { 0.78, 0.61, 0.43 } },
    { key = "PALADIN",  name = "圣骑士", color = { 0.96, 0.55, 0.73 } },
    { key = "HUNTER",   name = "猎人",   color = { 0.67, 0.83, 0.45 } },
    { key = "ROGUE",    name = "盗贼",   color = { 1.00, 0.96, 0.41 } },
    { key = "PRIEST",   name = "牧师",   color = { 1.00, 1.00, 1.00 } },
    { key = "SHAMAN",   name = "萨满",   color = { 0.00, 0.44, 0.87 } },
    { key = "MAGE",     name = "法师",   color = { 0.41, 0.80, 0.94 } },
    { key = "WARLOCK",  name = "术士",   color = { 0.58, 0.51, 0.79 } },
    { key = "DRUID",    name = "德鲁伊", color = { 1.00, 0.49, 0.04 } },
}

local CLASS_KEY_LOOKUP = {}
for _, c in ipairs(CLASS_LIST) do
    CLASS_KEY_LOOKUP[string.lower(c.key)] = c.key
    CLASS_KEY_LOOKUP[c.key] = c.key
end

--------------------------------------------------------------------------------
-- Helpers
--------------------------------------------------------------------------------
local function GetFont()
    if SFrames and SFrames.GetFont then return SFrames:GetFont() end
    return "Fonts\\ARIALN.TTF"
end

local function SetRoundBackdrop(frame, bgColor, borderColor)
    frame:SetBackdrop({
        bgFile   = "Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 14,
        insets = { left = 3, right = 3, top = 3, bottom = 3 },
    })
    local bg = bgColor or T.panelBg
    local bd = borderColor or T.panelBorder
    frame:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 1)
    frame:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 1)
end

local function SetPixelBackdrop(frame, bgColor, borderColor)
    frame:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        tile = false, tileSize = 0, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 }
    })
    if bgColor then frame:SetBackdropColor(bgColor[1], bgColor[2], bgColor[3], bgColor[4] or 1) end
    if borderColor then frame:SetBackdropBorderColor(borderColor[1], borderColor[2], borderColor[3], borderColor[4] or 1) end
end

local function CreateShadow(parent, size)
    local s = CreateFrame("Frame", nil, parent)
    local sz = size or 4
    s:SetPoint("TOPLEFT", parent, "TOPLEFT", -sz, sz)
    s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", sz, -sz)
    s:SetFrameLevel(math.max(parent:GetFrameLevel() - 1, 0))
    s:SetBackdrop({
        bgFile   = "Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 16,
        insets = { left = 4, right = 4, top = 4, bottom = 4 },
    })
    s:SetBackdropColor(0, 0, 0, 0.55)
    s:SetBackdropBorderColor(0, 0, 0, 0.4)
    return s
end

local function MakeFS(parent, size, justifyH, color)
    local fs = parent:CreateFontString(nil, "OVERLAY")
    fs:SetFont(GetFont(), size or 11, "OUTLINE")
    fs:SetJustifyH(justifyH or "LEFT")
    local c = color or T.valueText
    fs:SetTextColor(c[1], c[2], c[3])
    return fs
end

local function StyleButton(btn, label)
    SetRoundBackdrop(btn, T.btnBg, T.btnBorder)

    local fs = MakeFS(btn, 12, "CENTER", T.btnText)
    fs:SetPoint("CENTER", btn, "CENTER", 0, 0)
    if label then fs:SetText(label) end
    btn.nanamiLabel = fs

    btn:SetScript("OnEnter", function()
        this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4])
        this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4])
        if this.nanamiLabel then
            this.nanamiLabel:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3])
        end
        if this.nanamiTooltip then
            GameTooltip:SetOwner(this, "ANCHOR_TOP")
            GameTooltip:AddLine(this.nanamiTooltip[1], 1, 1, 1)
            if this.nanamiTooltip[2] then
                GameTooltip:AddLine(this.nanamiTooltip[2], 0.7, 0.7, 0.7)
            end
            GameTooltip:Show()
        end
    end)

    btn:SetScript("OnLeave", function()
        this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4])
        this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4])
        if this.nanamiLabel then
            this.nanamiLabel:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3])
        end
        GameTooltip:Hide()
    end)

    btn:SetScript("OnMouseDown", function()
        this:SetBackdropColor(T.btnDownBg[1], T.btnDownBg[2], T.btnDownBg[3], T.btnDownBg[4])
    end)

    btn:SetScript("OnMouseUp", function()
        this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4])
    end)

    return fs
end

--------------------------------------------------------------------------------
-- Talent code encode / decode (turtlecraft.gg compatible)
--------------------------------------------------------------------------------
local B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
local B64_LOOKUP = {}
for i = 1, 64 do B64_LOOKUP[string.sub(B64, i, i)] = i - 1 end

local function EncodeTreeGrid(grid)
    local lastNonZero = 0
    for i = 1, 28 do
        if (grid[i] or 0) > 0 then lastNonZero = i end
    end
    if lastNonZero == 0 then return "" end

    local bits = {}
    for i = 1, lastNonZero do
        local v = grid[i] or 0
        table.insert(bits, math.mod(math.floor(v / 4), 2))
        table.insert(bits, math.mod(math.floor(v / 2), 2))
        table.insert(bits, math.mod(v, 2))
    end

    while math.mod(table.getn(bits), 6) ~= 0 do
        table.insert(bits, 0)
    end

    local out = ""
    for i = 1, table.getn(bits), 6 do
        local val = bits[i]*32 + bits[i+1]*16 + bits[i+2]*8 + bits[i+3]*4 + bits[i+4]*2 + bits[i+5]
        out = out .. string.sub(B64, val + 1, val + 1)
    end
    return out
end

local function DecodeTreeGrid(str)
    local grid = {}
    if not str or str == "" then
        for i = 1, 28 do grid[i] = 0 end
        return grid
    end

    local bits = {}
    for ci = 1, string.len(str) do
        local ch = string.sub(str, ci, ci)
        local val = B64_LOOKUP[ch]
        if not val then
            for i = 1, 28 do grid[i] = 0 end
            return grid
        end
        table.insert(bits, math.mod(math.floor(val / 32), 2))
        table.insert(bits, math.mod(math.floor(val / 16), 2))
        table.insert(bits, math.mod(math.floor(val / 8), 2))
        table.insert(bits, math.mod(math.floor(val / 4), 2))
        table.insert(bits, math.mod(math.floor(val / 2), 2))
        table.insert(bits, math.mod(val, 2))
    end

    local idx = 1
    for i = 1, table.getn(bits) - 2, 3 do
        grid[idx] = bits[i]*4 + bits[i+1]*2 + bits[i+2]
        idx = idx + 1
        if idx > 28 then break end
    end
    while idx <= 28 do grid[idx] = 0; idx = idx + 1 end
    return grid
end

--------------------------------------------------------------------------------
-- Chat link: rewrite [NUI:...] tokens in chat into clickable hyperlinks
--------------------------------------------------------------------------------
local function FilterNanamiTalentLink(msg)
    if not msg or not string.find(msg, "[NUI:", 1, true) then return msg end
    return (string.gsub(msg, "%[NUI:([^:]+):([^:]+):(%u+):([^%]]+)%]", function(name, pts, ck, code)
        return "|cffFFB3D9|Hnanami:talent:" .. ck .. ":" .. code .. "|h[Nanami天赋: " .. name .. " " .. pts .. "]|h|r"
    end))
end

--------------------------------------------------------------------------------
-- Builds storage
--------------------------------------------------------------------------------
local function GetBuildsStore()
    if not SFramesGlobalDB then SFramesGlobalDB = {} end
    if not SFramesGlobalDB.talentBuilds then SFramesGlobalDB.talentBuilds = {} end
    return SFramesGlobalDB.talentBuilds
end

--------------------------------------------------------------------------------
-- Talent data cache (stores talent trees per class in SFramesGlobalDB)
-- Falls back to NanamiTalentDefaultDB for classes not yet cached locally
--------------------------------------------------------------------------------
local mergedCacheProxy = nil

local function GetCache()
    if not SFramesGlobalDB then SFramesGlobalDB = {} end
    if not SFramesGlobalDB.talentCache then SFramesGlobalDB.talentCache = {} end

    if not mergedCacheProxy then
        mergedCacheProxy = setmetatable({}, {
            __index = function(_, k)
                local v = SFramesGlobalDB.talentCache[k]
                if v ~= nil then return v end
                if NanamiTalentDefaultDB then return NanamiTalentDefaultDB[k] end
                return nil
            end,
            __newindex = function(_, k, v)
                SFramesGlobalDB.talentCache[k] = v
            end,
        })
    end

    return mergedCacheProxy
end

local function HasCacheData(classKey)
    if SFramesGlobalDB and SFramesGlobalDB.talentCache and SFramesGlobalDB.talentCache[classKey] then
        return true, "local"
    end
    if NanamiTalentDefaultDB and NanamiTalentDefaultDB[classKey] then
        return true, "default"
    end
    return false, nil
end

local function CacheCurrentClassData()
    local _, classEn = UnitClass("player")
    if not classEn then return end
    local numTabs = GetNumTalentTabs()
    if numTabs == 0 then return end

    local cache = GetCache()
    local classData = {}

    for t = 1, numTabs do
        local tabName, tabIcon, pointsSpent, background = GetTalentTabInfo(t)
        local treeData = { name = tabName, icon = tabIcon, background = background or "", talents = {}, numTalents = 0 }
        local numTalents = GetNumTalents(t)
        treeData.numTalents = numTalents

        for i = 1, numTalents do
            local tName, tIcon, tier, column, rank, maxRank = GetTalentInfo(t, i)
            local prereqTier, prereqCol = GetTalentPrereqs(t, i)

            local descLines = {}
            local scanTip = _G["NanamiTalentScanTip"]
            if not scanTip then
                scanTip = CreateFrame("GameTooltip", "NanamiTalentScanTip", UIParent, "GameTooltipTemplate")
                scanTip:SetOwner(UIParent, "ANCHOR_NONE")
            end
            scanTip:ClearLines()
            scanTip:SetTalent(t, i)
            local numLines = scanTip:NumLines()
            for li = 2, numLines do
                local lineObj = _G["NanamiTalentScanTipTextLeft" .. li]
                if lineObj then
                    local txt = lineObj:GetText()
                    if txt and txt ~= "" then
                        table.insert(descLines, txt)
                    end
                end
            end

            treeData.talents[i] = {
                name = tName or "",
                icon = tIcon or "",
                tier = tier or 1,
                column = column or 1,
                maxRank = maxRank or 1,
                prereqTier = prereqTier,
                prereqColumn = prereqCol,
                desc = descLines,
            }
        end
        classData[t] = treeData
    end
    classData.numTabs = numTabs
    cache[classEn] = classData
end

--------------------------------------------------------------------------------
-- Data access wrappers (API for own class, cache for others)
--------------------------------------------------------------------------------
local TT = SFrames.TalentTree

local function IsViewingOwnClass(self)
    return not self.viewingClass or self.viewingClass == self.playerClass
end

local function TT_GetNumTabs(self)
    if IsViewingOwnClass(self) then return GetNumTalentTabs() end
    local cache = GetCache()
    local cd = cache[self.viewingClass]
    return cd and cd.numTabs or 0
end

local function TT_GetTabInfo(self, tab)
    if IsViewingOwnClass(self) then return GetTalentTabInfo(tab) end
    local cache = GetCache()
    local cd = cache[self.viewingClass]
    if cd and cd[tab] then
        return cd[tab].name, cd[tab].icon, 0, cd[tab].background
    end
    return "", "", 0, ""
end

local function TT_GetNumTalents(self, tab)
    if IsViewingOwnClass(self) then return GetNumTalents(tab) end
    local cache = GetCache()
    local cd = cache[self.viewingClass]
    if cd and cd[tab] then return cd[tab].numTalents or 0 end
    return 0
end

local function TT_GetTalentInfo(self, tab, index)
    if IsViewingOwnClass(self) then return GetTalentInfo(tab, index) end
    local cache = GetCache()
    local cd = cache[self.viewingClass]
    if cd and cd[tab] and cd[tab].talents[index] then
        local t = cd[tab].talents[index]
        return t.name, t.icon, t.tier, t.column, 0, t.maxRank, nil, true
    end
    return nil
end

local function TT_GetTalentPrereqs(self, tab, index)
    if IsViewingOwnClass(self) then return GetTalentPrereqs(tab, index) end
    local cache = GetCache()
    local cd = cache[self.viewingClass]
    if cd and cd[tab] and cd[tab].talents[index] then
        local t = cd[tab].talents[index]
        return t.prereqTier, t.prereqColumn
    end
    return nil, nil
end

--------------------------------------------------------------------------------
-- Initialize
--------------------------------------------------------------------------------
function SFrames.TalentTree:Initialize()
    local _, classEn = UnitClass("player")
    self.playerClass = classEn

    self:CreateMainFrame()
    self:HookVanillaUI()

    CacheCurrentClassData()

    SFrames:RegisterEvent("CHARACTER_POINTS_CHANGED", function()
        CacheCurrentClassData()
        if self.frame and self.frame:IsShown() then self:Update() end
    end)
    SFrames:RegisterEvent("SPELLS_CHANGED", function()
        if self.frame and self.frame:IsShown() then self:Update() end
    end)

    self:HookChatTalentLinks()
end

--------------------------------------------------------------------------------
-- Main Frame
--------------------------------------------------------------------------------
function SFrames.TalentTree:CreateMainFrame()
    local f = CreateFrame("Frame", "SFramesTalentFrame", UIParent)
    f:SetWidth(FRAME_WIDTH)
    f:SetHeight(FRAME_HEIGHT)
    f:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
    SetRoundBackdrop(f, T.panelBg, T.panelBorder)
    CreateShadow(f, 5)
    f:EnableMouse(true)
    f:SetMovable(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", function() this:StartMoving() end)
    f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
    f:SetFrameStrata("HIGH")
    f:Hide()

    local titleIcoSize = 14
    local titleGap = 4
    f.title = MakeFS(f, 14, "CENTER", T.titleColor)
    f.title:SetPoint("TOP", f, "TOP", (titleIcoSize + titleGap) / 2, -10)
    f.title:SetText("|c" .. GetHex() .. "Nanami|r 天赋系统")
    local titleIco = SFrames:CreateIcon(f, "talent", titleIcoSize)
    titleIco:SetDrawLayer("OVERLAY")
    titleIco:SetVertexColor(T.titleColor[1], T.titleColor[2], T.titleColor[3])
    titleIco:SetPoint("RIGHT", f.title, "LEFT", -titleGap, 0)

    -- Close button
    local closeBtn = CreateFrame("Button", nil, f)
    closeBtn:SetWidth(18)
    closeBtn:SetHeight(18)
    closeBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -8, -8)
    closeBtn:SetFrameLevel(f:GetFrameLevel() + 3)
    SetRoundBackdrop(closeBtn, T.buttonDownBg, T.btnBorder)
    local closeTxt = MakeFS(closeBtn, 10, "CENTER", T.title)
    closeTxt:SetPoint("CENTER", closeBtn, "CENTER", 0, 0)
    closeTxt:SetText("x")
    closeBtn:SetScript("OnClick", function() f:Hide() end)
    closeBtn:SetScript("OnEnter", function()
        this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4])
        this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4])
    end)
    closeBtn:SetScript("OnLeave", function()
        this:SetBackdropColor(T.buttonDownBg[1], T.buttonDownBg[2], T.buttonDownBg[3], T.buttonDownBg[4])
        this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4])
    end)
    f.close = closeBtn

    -- Class selector bar (shown in sim mode)
    local classBar = CreateFrame("Frame", nil, f)
    classBar:SetHeight(20)
    classBar:SetPoint("TOPLEFT", f, "TOPLEFT", 10, -28)
    classBar:SetPoint("TOPRIGHT", f, "TOPRIGHT", -10, -28)
    classBar:SetFrameLevel(f:GetFrameLevel() + 2)
    classBar:Hide()
    f.classBar = classBar

    local numClasses = table.getn(CLASS_LIST)
    local gap = 2
    local barW = FRAME_WIDTH - 20
    local cbW = math.floor((barW - (numClasses - 1) * gap) / numClasses)
    f.classBtns = {}

    for ci, cinfo in ipairs(CLASS_LIST) do
        local cb = CreateFrame("Button", nil, classBar)
        cb:SetWidth(cbW)
        cb:SetHeight(18)
        cb:SetPoint("TOPLEFT", classBar, "TOPLEFT", (ci - 1) * (cbW + gap), 0)
        SetPixelBackdrop(cb, T.slotBg, T.slotBorder)

        local cIcon = SFrames:CreateClassIcon(cb, 14)
        cIcon.overlay:SetPoint("LEFT", cb, "LEFT", 3, 0)
        SFrames:SetClassIcon(cIcon, cinfo.key)
        cb.classIconTex = cIcon

        local cbt = MakeFS(cb, 9, "CENTER", cinfo.color)
        cbt:SetPoint("LEFT", cIcon.overlay, "RIGHT", 1, 0)
        cbt:SetPoint("RIGHT", cb, "RIGHT", -2, 0)
        cbt:SetText(cinfo.name)
        cb.label = cbt
        cb.classKey = cinfo.key
        cb.classColor = cinfo.color
        cb:SetScript("OnClick", function()
            SFrames.TalentTree:SwitchViewClass(this.classKey)
        end)
        cb:SetScript("OnEnter", function()
            this:SetBackdropColor(T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4])
            if this.classKey ~= SFrames.TalentTree.playerClass then
                local hasData, source = HasCacheData(this.classKey)
                if not hasData then
                    GameTooltip:SetOwner(this, "ANCHOR_BOTTOM")
                    GameTooltip:AddLine("该职业天赋数据不可用", 1, 0.5, 0.5)
                    GameTooltip:AddLine("需先用该职业角色登录以缓存数据", 0.7, 0.7, 0.7)
                    GameTooltip:Show()
                elseif source == "default" then
                    GameTooltip:SetOwner(this, "ANCHOR_BOTTOM")
                    GameTooltip:AddLine("使用内置默认数据", 0.7, 0.7, 0.7)
                    GameTooltip:AddLine("登录该职业可获取最新数据", 0.5, 0.5, 0.5)
                    GameTooltip:Show()
                end
            end
        end)
        cb:SetScript("OnLeave", function()
            this:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4])
            GameTooltip:Hide()
        end)
        f.classBtns[ci] = cb
    end

    -- Overlay for text on top of children
    f.overlay = CreateFrame("Frame", nil, f)
    f.overlay:SetAllPoints(f)
    f.overlay:SetFrameLevel(f:GetFrameLevel() + 10)

    f.pointsText = MakeFS(f.overlay, 13, "LEFT", T.titleColor)
    f.pointsText:SetPoint("BOTTOMLEFT", f.overlay, "BOTTOMLEFT", 14, 42)
    f.pointsText:SetWidth(FRAME_WIDTH - 220)

    f.simLabel = MakeFS(f.overlay, 11, "LEFT", T.titleColor)
    f.simLabel:SetPoint("BOTTOMLEFT", f.overlay, "BOTTOMLEFT", 14, 18)
    f.simLabel:SetWidth(FRAME_WIDTH - 120)
    f.simLabel:SetText("")

    local btnLevel = f:GetFrameLevel() + 11

    -- Bottom buttons - Row 1 (lower): preview mode / reset / apply
    f.btnApply = CreateFrame("Button", nil, f)
    f.btnApply:SetWidth(100)
    f.btnApply:SetHeight(26)
    f.btnApply:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -12, 12)
    f.btnApply:SetFrameLevel(btnLevel)
    StyleButton(f.btnApply, "应用天赋")
    f.btnApply.nanamiTooltip = { "应用天赋", "将所有预览点数提交至服务器。" }
    f.btnApply:SetScript("OnClick", function() SFrames.TalentTree:ApplyVirtualPoints() end)
    f.btnApply:Hide()

    f.btnReset = CreateFrame("Button", nil, f)
    f.btnReset:SetWidth(100)
    f.btnReset:SetHeight(26)
    f.btnReset:SetPoint("RIGHT", f.btnApply, "LEFT", -8, 0)
    f.btnReset:SetFrameLevel(btnLevel)
    StyleButton(f.btnReset, "重置预览")
    f.btnReset:SetScript("OnClick", function() SFrames.TalentTree:ResetVirtualPoints() end)
    f.btnReset:Hide()

    f.btnSimMode = CreateFrame("Button", nil, f)
    f.btnSimMode:SetWidth(110)
    f.btnSimMode:SetHeight(26)
    f.btnSimMode:SetPoint("RIGHT", f.btnReset, "LEFT", -8, 0)
    f.btnSimMode:SetFrameLevel(btnLevel)
    StyleButton(f.btnSimMode, "|cff888888预览模式: 关|r")
    f.simModeText = f.btnSimMode.nanamiLabel
    f.btnSimMode.nanamiTooltip = { "预览模式", "开启后可用左键虚拟加点、右键取消，\n确认后点「应用天赋」才会实际扣分。\n可切换职业预览其他职业加点。" }
    f.btnSimMode:SetScript("OnClick", function()
        SFrames.TalentTree.simMode = not SFrames.TalentTree.simMode
        SFrames.TalentTree:UpdateSimModeLabel()
    end)

    -- Bottom buttons - Row 2 (upper): builds / save / import / export
    f.btnExport = CreateFrame("Button", nil, f)
    f.btnExport:SetWidth(60)
    f.btnExport:SetHeight(26)
    f.btnExport:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -12, 42)
    f.btnExport:SetFrameLevel(btnLevel)
    StyleButton(f.btnExport, "导出")
    f.btnExport.nanamiTooltip = { "导出天赋", "导出天赋代码 (含职业标识)，\n可发送到聊天或分享给其他玩家。" }
    f.btnExport:SetScript("OnClick", function() SFrames.TalentTree:ShowExportDialog() end)

    f.btnImport = CreateFrame("Button", nil, f)
    f.btnImport:SetWidth(60)
    f.btnImport:SetHeight(26)
    f.btnImport:SetPoint("RIGHT", f.btnExport, "LEFT", -6, 0)
    f.btnImport:SetFrameLevel(btnLevel)
    StyleButton(f.btnImport, "导入")
    f.btnImport.nanamiTooltip = { "导入天赋", "粘贴天赋代码 (含职业标识)\n或完整 URL，自动切换对应职业预览。" }
    f.btnImport:SetScript("OnClick", function() SFrames.TalentTree:ShowImportDialog() end)

    f.btnSave = CreateFrame("Button", nil, f)
    f.btnSave:SetWidth(60)
    f.btnSave:SetHeight(26)
    f.btnSave:SetPoint("RIGHT", f.btnImport, "LEFT", -6, 0)
    f.btnSave:SetFrameLevel(btnLevel)
    StyleButton(f.btnSave, "保存")
    f.btnSave.nanamiTooltip = { "保存方案", "将当前天赋配置保存为命名方案。" }
    f.btnSave:SetScript("OnClick", function() SFrames.TalentTree:ShowSaveDialog() end)

    f.btnBuilds = CreateFrame("Button", nil, f)
    f.btnBuilds:SetWidth(80)
    f.btnBuilds:SetHeight(26)
    f.btnBuilds:SetPoint("RIGHT", f.btnSave, "LEFT", -6, 0)
    f.btnBuilds:SetFrameLevel(btnLevel)
    StyleButton(f.btnBuilds, "方案管理")
    f.btnBuilds.nanamiTooltip = { "方案管理", "查看、加载、删除已保存的天赋方案。\n可浏览所有职业的方案。" }
    f.btnBuilds:SetScript("OnClick", function() SFrames.TalentTree:ToggleBuildsPanel() end)

    self.frame = f
    self.tabs = {}
    self.virtualPoints = {}
    self.simMode = false
    self.viewingClass = nil

    tinsert(UISpecialFrames, "SFramesTalentFrame")
end

--------------------------------------------------------------------------------
-- Class bar update
--------------------------------------------------------------------------------
function SFrames.TalentTree:UpdateClassBar()
    if not self.frame or not self.frame.classBtns then return end
    local viewing = self.viewingClass or self.playerClass

    for ci, cinfo in ipairs(CLASS_LIST) do
        local cb = self.frame.classBtns[ci]
        if not cb then break end

        local hasData, source = HasCacheData(cinfo.key)
        if cinfo.key == self.playerClass then hasData = true; source = "local" end

        if cinfo.key == viewing then
            SetPixelBackdrop(cb, T.tabActiveBg, cinfo.color)
            cb.label:SetTextColor(cinfo.color[1], cinfo.color[2], cinfo.color[3])
        elseif hasData and source == "local" then
            SetPixelBackdrop(cb, T.slotBg, T.slotBorder)
            cb.label:SetTextColor(cinfo.color[1] * 0.8, cinfo.color[2] * 0.8, cinfo.color[3] * 0.8)
        elseif hasData and source == "default" then
            SetPixelBackdrop(cb, T.slotBg, T.slotBorder)
            cb.label:SetTextColor(cinfo.color[1] * 0.6, cinfo.color[2] * 0.6, cinfo.color[3] * 0.6)
        else
            SetPixelBackdrop(cb, T.emptySlotBg, T.emptySlotBd)
            cb.label:SetTextColor(T.passive[1], T.passive[2], T.passive[3])
        end
    end
end

function SFrames.TalentTree:UpdateSimModeLabel()
    if self.simMode then
        local viewing = self.viewingClass or self.playerClass
        local viewName = viewing or "?"
        for _, c in ipairs(CLASS_LIST) do
            if c.key == viewing then viewName = c.name; break end
        end
        self.frame.simModeText:SetText("|c" .. GetHex() .. "预览: " .. viewName .. "|r")
        self.frame.simLabel:SetText("|c" .. GetHex() .. "[预览] 总点数: 51  左键: 加点  右键: 撤销|r")
        self.frame.classBar:Show()
        self:UpdateClassBar()
        self.frame.btnReset:Show()
        if IsViewingOwnClass(self) then
            self.frame.btnApply:Show()
        else
            self.frame.btnApply:Hide()
        end
    else
        self.frame.simModeText:SetText("|cff888888预览模式: 关|r")
        self.frame.simLabel:SetText("")
        self.frame.classBar:Hide()
        self.frame.btnApply:Hide()
        self.frame.btnReset:Hide()
        if self.viewingClass and self.viewingClass ~= self.playerClass then
            self.viewingClass = self.playerClass
            self.virtualPoints = {}
            self:DestroyTrees()
            self:BuildTrees()
            self:Update()
            return
        end
    end
    self:ResetVirtualPoints()
end

--------------------------------------------------------------------------------
-- Switch class
--------------------------------------------------------------------------------
function SFrames.TalentTree:SwitchViewClass(classKey)
    if not self.simMode then return end
    if classKey == (self.viewingClass or self.playerClass) then return end

    if classKey ~= self.playerClass then
        local hasData = HasCacheData(classKey)
        if not hasData then
            UIErrorsFrame:AddMessage("该职业天赋数据不可用，请先用该职业角色登录或更新默认数据库", 1, 0.5, 0.5, 1)
            return
        end
    end

    self.viewingClass = classKey
    self.virtualPoints = {}
    self:DestroyTrees()
    self:BuildTrees()
    self:UpdateSimModeLabel()
    self:Update()
end

--------------------------------------------------------------------------------
-- Destroy / Rebuild trees
--------------------------------------------------------------------------------
function SFrames.TalentTree:DestroyTrees()
    if self.tabs then
        for _, tabData in pairs(self.tabs) do
            if type(tabData) == "table" and tabData.frame then
                tabData.frame:Hide()
                tabData.frame:ClearAllPoints()
                tabData.frame:SetParent(nil)
            end
        end
    end
    self.tabs = {}
    self.treesBuilt = false
end

--------------------------------------------------------------------------------
-- Build trees (from API or cache)
--------------------------------------------------------------------------------
function SFrames.TalentTree:BuildTrees()
    if self.treesBuilt then return end
    self.treesBuilt = true

    if IsViewingOwnClass(self) then
        CacheCurrentClassData()
    end

    local treeTop = -38
    if self.simMode then treeTop = -52 end

    local numTabs = TT_GetNumTabs(self)
    for t = 1, numTabs do
        local name, icon, pointsSpent, background = TT_GetTabInfo(self, t)

        local tabFrame = CreateFrame("Frame", nil, self.frame)
        tabFrame:SetWidth(TAB_WIDTH)
        tabFrame:SetHeight(FRAME_HEIGHT - 120)
        local offsetX = 10 + ((t - 1) * (TAB_WIDTH + 5))
        tabFrame:SetPoint("TOPLEFT", self.frame, "TOPLEFT", offsetX, treeTop)

        SetRoundBackdrop(tabFrame, T.tabBg, T.tabBorder)

        if background and background ~= "" then
            local bg = tabFrame:CreateTexture(nil, "BACKGROUND")
            bg:SetTexture("Interface\\TalentFrame\\" .. background)
            bg:SetPoint("TOPLEFT", tabFrame, "TOPLEFT", 3, -3)
            bg:SetPoint("BOTTOMRIGHT", tabFrame, "BOTTOMRIGHT", -3, 3)
            bg:SetAlpha(0.35)
        end

        local tTitle = MakeFS(tabFrame, 14, "CENTER", T.titleColor)
        tTitle:SetPoint("TOP", tabFrame, "TOP", 0, -14)
        tTitle:SetText("|c" .. GetHex() .. (name or "") .. "|r")

        local tPoints = MakeFS(tabFrame, 12, "CENTER", T.dimText)
        tPoints:SetPoint("TOP", tTitle, "BOTTOM", 0, -4)
        tabFrame.pointsText = tPoints

        self.tabs[t] = { frame = tabFrame, talents = {}, grid = {} }

        local GRID_PAD_X = math.floor((TAB_WIDTH - (4 * ICON_SIZE + 3 * ICON_SPACING_X)) / 2)
        local GRID_TOP = 50

        local numTalents = TT_GetNumTalents(self, t)
        for i = 1, numTalents do
            local tName, tIcon, tier, column, rank, maxRank = TT_GetTalentInfo(self, t, i)
            if not tName then break end

            if not self.tabs[t].grid[tier] then self.tabs[t].grid[tier] = {} end

            local btn = CreateFrame("Button", "SFramesTalent_" .. (self.viewingClass or "own") .. "_" .. t .. "_" .. i, tabFrame)
            btn:SetWidth(ICON_SIZE)
            btn:SetHeight(ICON_SIZE)

            local x = (column - 1) * (ICON_SIZE + ICON_SPACING_X) + GRID_PAD_X
            local y = -(tier - 1) * (ICON_SIZE + ICON_SPACING_Y) - GRID_TOP
            btn:SetPoint("TOPLEFT", tabFrame, "TOPLEFT", x, y)

            btn.icon = btn:CreateTexture(nil, "ARTWORK")
            btn.icon:SetTexture(tIcon)
            btn.icon:SetAllPoints()

            btn.borderTex = btn:CreateTexture(nil, "OVERLAY")
            btn.borderTex:SetTexture("Interface\\Buttons\\UI-Quickslot2")
            btn.borderTex:SetWidth(ICON_SIZE * 1.5)
            btn.borderTex:SetHeight(ICON_SIZE * 1.5)
            btn.borderTex:SetPoint("CENTER", btn, "CENTER", 0, 0)

            local rankFrame = CreateFrame("Frame", nil, btn)
            rankFrame:SetAllPoints(btn)
            rankFrame:SetFrameLevel(btn:GetFrameLevel() + 2)

            btn.rankBg = rankFrame:CreateTexture(nil, "BACKGROUND")
            btn.rankBg:SetTexture(0, 0, 0, 0.75)
            btn.rankBg:SetWidth(20)
            btn.rankBg:SetHeight(12)
            btn.rankBg:SetPoint("BOTTOMRIGHT", rankFrame, "BOTTOMRIGHT", -1, 1)
            btn.rankBg:Hide()

            btn.rankText = MakeFS(rankFrame, 10, "RIGHT")
            btn.rankText:SetPoint("BOTTOMRIGHT", rankFrame, "BOTTOMRIGHT", -2, 2)
            btn.rankText:SetShadowColor(0, 0, 0, 1)
            btn.rankText:SetShadowOffset(1, -1)
            btn.rankText:SetText("")

            btn.tab = t
            btn.index = i
            btn.tier = tier
            btn.maxRank = maxRank
            btn.talentName = tName

            btn:SetScript("OnEnter", function()
                SFrames.TalentTree:ShowTalentTooltip(this)
            end)
            btn:SetScript("OnLeave", function() GameTooltip:Hide() end)

            btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
            btn:SetScript("OnClick", function()
                SFrames.TalentTree:OnTalentClick(this, arg1)
            end)

            self.tabs[t].talents[i] = btn
            self.tabs[t].grid[tier][column] = btn
        end

        -- Dependency lines
        for i = 1, numTalents do
            local prereqTier, prereqColumn = TT_GetTalentPrereqs(self, t, i)
            if prereqTier and prereqColumn then
                local btn = self.tabs[t].talents[i]
                local pBtn = self.tabs[t].grid[prereqTier] and self.tabs[t].grid[prereqTier][prereqColumn]
                if pBtn then
                    btn.prereqIndex = pBtn.index
                    btn.prereqLines = {}

                    local _, _, tier, column = TT_GetTalentInfo(self, t, i)
                    local pX = (prereqColumn - 1) * (ICON_SIZE + ICON_SPACING_X) + GRID_PAD_X
                    local pY = -(prereqTier - 1) * (ICON_SIZE + ICON_SPACING_Y) - GRID_TOP
                    local cX = (column - 1) * (ICON_SIZE + ICON_SPACING_X) + GRID_PAD_X
                    local cY = -(tier - 1) * (ICON_SIZE + ICON_SPACING_Y) - GRID_TOP

                    local pCenterX = pX + (ICON_SIZE / 2)
                    local pCenterY = pY - (ICON_SIZE / 2)
                    local pBottomY = pY - ICON_SIZE
                    local pRightX  = pX + ICON_SIZE

                    local tCenterX = cX + (ICON_SIZE / 2)
                    local tTopY    = cY
                    local tLeftX   = cX
                    local tRightX  = cX + ICON_SIZE

                    local function CreateLine(x1, y1, x2, y2)
                        local line = tabFrame:CreateTexture(nil, "BACKGROUND")
                        line:SetTexture(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4])
                        if math.abs(x1 - x2) < 0.1 then
                            line:SetWidth(2)
                            line:SetHeight(math.abs(y1 - y2))
                            line:SetPoint("TOP", tabFrame, "TOPLEFT", x1, math.max(y1, y2))
                        else
                            line:SetHeight(2)
                            line:SetWidth(math.abs(x1 - x2))
                            line:SetPoint("LEFT", tabFrame, "TOPLEFT", math.min(x1, x2), y1)
                        end
                        table.insert(btn.prereqLines, line)
                    end

                    if prereqTier == tier then
                        if prereqColumn < column then
                            CreateLine(pRightX, pCenterY, tLeftX, pCenterY)
                        else
                            CreateLine(pX, pCenterY, tRightX, pCenterY)
                        end
                    elseif prereqColumn == column then
                        CreateLine(pCenterX, pBottomY, tCenterX, tTopY)
                    else
                        local midY = pBottomY - (ICON_SPACING_Y / 2) + 2
                        CreateLine(pCenterX, pBottomY, pCenterX, midY)
                        CreateLine(pCenterX, midY, tCenterX, midY)
                        CreateLine(tCenterX, midY, tCenterX, tTopY)
                    end
                end
            end
        end
    end
end

--------------------------------------------------------------------------------
-- Hooks
--------------------------------------------------------------------------------
function SFrames.TalentTree:HookVanillaUI()
    local self = SFrames.TalentTree

    local function ShowCustomTalentFrame()
        if not self.frame then return end
        if not self.frame:IsShown() then
            self:BuildTrees()
            self.frame:Show()
            self:Update()
        end
    end

    local function ToggleCustomTalentFrame()
        if not self.frame then return end
        if self.frame:IsShown() then
            self.frame:Hide()
        else
            ShowCustomTalentFrame()
        end
    end

    ToggleTalentFrame = ToggleCustomTalentFrame

    if not self.hookedShowUIPanel then
        self.hookedShowUIPanel = true
        local orig_ShowUIPanel = ShowUIPanel
        ShowUIPanel = function(frame)
            if frame and frame.GetName then
                local fName = frame:GetName()
                if fName == "TalentFrame" or fName == "TalentMicroButtonAlert" then
                    ShowCustomTalentFrame()
                    return
                end
            end
            if orig_ShowUIPanel then
                orig_ShowUIPanel(frame)
            end
        end

        local orig_HideUIPanel = HideUIPanel
        HideUIPanel = function(frame)
            if frame and frame.GetName and frame:GetName() == "TalentFrame" then
                if self.frame then self.frame:Hide() end
                return
            end
            if orig_HideUIPanel then
                orig_HideUIPanel(frame)
            end
        end
    end

    local orig_TalentFrame_LoadUI = TalentFrame_LoadUI
    if orig_TalentFrame_LoadUI then
        TalentFrame_LoadUI = function()
            orig_TalentFrame_LoadUI()
        end
    end
end

--------------------------------------------------------------------------------
-- Talent tooltip (shared by OnEnter and post-click refresh)
--------------------------------------------------------------------------------
function SFrames.TalentTree:ShowTalentTooltip(btn)
    if IsViewingOwnClass(self) and not self.simMode then
        GameTooltip_SetDefaultAnchor(GameTooltip, btn)
        GameTooltip:SetTalent(btn.tab, btn.index)
        GameTooltip:Show()
        return
    end

    GameTooltip:SetOwner(btn, "ANCHOR_RIGHT")
    GameTooltip:AddLine(btn.talentName or "?", 1, 1, 1)
    local vr = self:GetVirtualRank(btn.tab, btn.index)
    local mr = btn.maxRank or 1
    GameTooltip:AddLine("等级 " .. vr .. "/" .. mr, 0.7, 0.7, 0.7)

    local desc = nil
    local classKey = self.viewingClass or self.playerClass
    if NanamiTalentDefaultDB and NanamiTalentDefaultDB[classKey] then
        local tabData = NanamiTalentDefaultDB[classKey][btn.tab]
        if tabData and tabData.talents and tabData.talents[btn.index] then
            desc = tabData.talents[btn.index].desc
        end
    end
    if not desc then
        local cd = GetCache()[classKey]
        if cd and cd[btn.tab] and cd[btn.tab].talents[btn.index] then
            desc = cd[btn.tab].talents[btn.index].desc
        end
    end

    if desc then
        local n = table.getn(desc)
        if n == mr and desc[1] then
            GameTooltip:AddLine(" ")
            if vr == 0 then
                GameTooltip:AddLine(desc[1], 1, 0.82, 0, 1)
            else
                GameTooltip:AddLine(desc[vr], 1, 0.82, 0, 1)
                if vr < mr and desc[vr + 1] then
                    GameTooltip:AddLine(" ")
                    GameTooltip:AddLine("下一级:", 0, 1, 0)
                    GameTooltip:AddLine(desc[vr + 1], 1, 0.82, 0, 1)
                end
            end
        else
            GameTooltip:AddLine(" ")
            for _, line in ipairs(desc) do
                GameTooltip:AddLine(line, 1, 0.82, 0, 1)
            end
        end
    end
    GameTooltip:Show()
end

--------------------------------------------------------------------------------
-- Virtual point helpers
--------------------------------------------------------------------------------
function SFrames.TalentTree:GetVirtualRank(tab, index)
    if self.virtualPoints[tab] and self.virtualPoints[tab][index] then
        return self.virtualPoints[tab][index]
    end
    if self.simMode then return 0 end
    if IsViewingOwnClass(self) then
        local name, icon, tier, column, rank, maxRank = GetTalentInfo(tab, index)
        return rank or 0
    end
    return 0
end

function SFrames.TalentTree:GetVirtualTreePoints(tab)
    local total = 0
    local n = TT_GetNumTalents(self, tab)
    for i = 1, n do
        total = total + self:GetVirtualRank(tab, i)
    end
    return total
end

function SFrames.TalentTree:GetRemainingUnspent()
    if self.simMode then
        local unspent = 51
        local numTabs = TT_GetNumTabs(self)
        for tb = 1, numTabs do
            unspent = unspent - self:GetVirtualTreePoints(tb)
        end
        return unspent
    else
        local unspent = UnitCharacterPoints("player")
        for tb = 1, GetNumTalentTabs() do
            for idx = 1, GetNumTalents(tb) do
                local name, icon, tier, column, realRank = GetTalentInfo(tb, idx)
                local virtRank = self:GetVirtualRank(tb, idx)
                unspent = unspent - (virtRank - (realRank or 0))
            end
        end
        return unspent
    end
end

--------------------------------------------------------------------------------
-- Talent click
--------------------------------------------------------------------------------
function SFrames.TalentTree:OnTalentClick(btn, buttonType)
    if not self.simMode and IsViewingOwnClass(self) then
        if buttonType == "LeftButton" then
            local tName, tIcon, tier, column, rank, maxRank, isExceptional, meetsPrereq = GetTalentInfo(btn.tab, btn.index)
            local unspent = UnitCharacterPoints("player")
            if rank < maxRank and meetsPrereq and unspent > 0 then
                StaticPopupDialogs["NANAMI_CONFIRM_TALENT"] = {
                    text = "确定学习天赋 [" .. tName .. "] ?\n（等级 " .. rank .. " → " .. (rank+1) .. " / " .. maxRank .. "）",
                    button1 = "确认",
                    button2 = "取消",
                    OnAccept = function()
                        LearnTalent(btn.tab, btn.index)
                    end,
                    timeout = 0,
                    whileDead = true,
                    hideOnEscape = true,
                }
                StaticPopup_Show("NANAMI_CONFIRM_TALENT")
            end
        end
        return
    end

    if not self.simMode then return end

    local _, _, tier, _, _, maxRank = TT_GetTalentInfo(self, btn.tab, btn.index)
    local virtRank = self:GetVirtualRank(btn.tab, btn.index)
    maxRank = maxRank or btn.maxRank

    if not self.virtualPoints[btn.tab] then self.virtualPoints[btn.tab] = {} end
    if not self.virtualPoints[btn.tab][btn.index] then
        self.virtualPoints[btn.tab][btn.index] = self:GetVirtualRank(btn.tab, btn.index)
    end

    if buttonType == "LeftButton" then
        local unspent = self:GetRemainingUnspent()
        if unspent > 0 and virtRank < maxRank then
            local treePts = self:GetVirtualTreePoints(btn.tab)
            local prereqMet = true
            if btn.prereqIndex then
                local pVirtRank = self:GetVirtualRank(btn.tab, btn.prereqIndex)
                local pMaxRank = self.tabs[btn.tab].talents[btn.prereqIndex].maxRank
                if pVirtRank < pMaxRank then prereqMet = false end
            end

            if treePts >= (tier - 1) * 5 and prereqMet then
                self.virtualPoints[btn.tab][btn.index] = virtRank + 1
            end
        end
    elseif buttonType == "RightButton" then
        if virtRank > 0 then
            local canRevert = true
            local treePts = self:GetVirtualTreePoints(btn.tab)
            local numT = TT_GetNumTalents(self, btn.tab)
            for i = 1, numT do
                local childVirtRank = self:GetVirtualRank(btn.tab, i)
                if childVirtRank > 0 then
                    local _, _, qTier = TT_GetTalentInfo(self, btn.tab, i)
                    if qTier and qTier > tier and (treePts - 1) < (qTier - 1) * 5 then
                        canRevert = false
                    end
                    local cBtn = self.tabs[btn.tab].talents[i]
                    if cBtn and cBtn.prereqIndex == btn.index then
                        if childVirtRank > 0 and (virtRank - 1) < maxRank then
                            canRevert = false
                        end
                    end
                end
            end
            if canRevert then
                self.virtualPoints[btn.tab][btn.index] = virtRank - 1
            else
                UIErrorsFrame:AddMessage("无法取消点数：其他已点天赋依赖于此天赋", 1, 0, 0, 1)
            end
        end
    end

    self:Update()
    if GameTooltip:IsVisible() then
        self:ShowTalentTooltip(btn)
    end
end

function SFrames.TalentTree:ResetVirtualPoints()
    self.virtualPoints = {}
    self:Update()
end

function SFrames.TalentTree:SetBottomButtonsEnabled(enabled)
    local btns = { self.frame.btnApply, self.frame.btnReset, self.frame.btnSimMode,
                   self.frame.btnExport, self.frame.btnImport, self.frame.btnSave, self.frame.btnBuilds }
    for _, b in ipairs(btns) do
        if enabled then
            b:Enable()
            if b.nanamiLabel then b.nanamiLabel:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) end
        else
            b:Disable()
            if b.nanamiLabel then b.nanamiLabel:SetTextColor(T.passive[1], T.passive[2], T.passive[3]) end
        end
    end
end

function SFrames.TalentTree:ApplyVirtualPoints()
    if not self.simMode then return end
    if not IsViewingOwnClass(self) then
        DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[错误]|r 只能应用本职业的天赋。")
        return
    end

    local canApply = true
    for tb = 1, GetNumTalentTabs() do
        for idx = 1, GetNumTalents(tb) do
            local name, icon, tier, column, realRank = GetTalentInfo(tb, idx)
            local virtRank = self:GetVirtualRank(tb, idx)
            if realRank > virtRank then
                canApply = false
            end
        end
    end

    if not canApply then
        DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[错误]|r 模拟的点数未包含您已学习的天赋，无法直接应用。")
        return
    end

    self.applyQueue = {}

    for tb = 1, GetNumTalentTabs() do
        local treeTalents = {}
        for idx = 1, GetNumTalents(tb) do
            local name, icon, tier, column, realRank, maxRank = GetTalentInfo(tb, idx)
            local virtRank = self:GetVirtualRank(tb, idx)
            local diff = virtRank - realRank
            if diff > 0 then
                for i = 1, diff do
                    table.insert(treeTalents, {
                        tab = tb, index = idx, tier = tier or 1,
                        name = name or "?", toRank = realRank + i, maxRank = maxRank or 1,
                    })
                end
            end
        end
        table.sort(treeTalents, function(a, b) return a.tier < b.tier end)
        for _, entry in ipairs(treeTalents) do
            table.insert(self.applyQueue, entry)
        end
    end

    if table.getn(self.applyQueue) == 0 then
        DEFAULT_CHAT_FRAME:AddMessage("|c" .. GetHex() .. "Nanami:|r 没有新的天赋点数需要应用。")
        return
    end

    local total = table.getn(self.applyQueue)
    self.applyTotal = total
    self.applyDone = 0
    DEFAULT_CHAT_FRAME:AddMessage("|c" .. GetHex() .. "Nanami:|r 正在应用天赋，请等待完成... (共 " .. total .. " 点)")
    self.frame.simLabel:SetText("|cffff6600正在应用天赋，请勿操作... 0/" .. total .. "|r")
    self:SetBottomButtonsEnabled(false)

    if not self.applyEventFrame then
        self.applyEventFrame = CreateFrame("Frame", "NanamiTalentApplyFrame")
    end
    self.applyEventFrame:UnregisterAllEvents()

    self.applyStallTimer = 0
    self.applyWaiting = false

    local function FinishApply()
        self.applyEventFrame:UnregisterAllEvents()
        self.applyEventFrame:SetScript("OnUpdate", nil)
        self.applyQueue = {}
        self.applyWaiting = false
        self:SetBottomButtonsEnabled(true)
        self.simMode = false
        self:UpdateSimModeLabel()
        DEFAULT_CHAT_FRAME:AddMessage("|c" .. GetHex() .. "Nanami:|r 天赋应用完成！共应用 " .. (self.applyTotal or 0) .. " 个天赋点。")
        self.frame.simLabel:SetText("|cff00ff00天赋应用完成！|r")
    end

    local function TryNextTalent()
        if table.getn(self.applyQueue) == 0 then
            FinishApply()
            return
        end
        local t = table.remove(self.applyQueue, 1)
        self.applyDone = (self.applyDone or 0) + 1
        self.frame.simLabel:SetText("|cffff6600应用中 " .. self.applyDone .. "/" .. total .. ": " .. t.name .. "|r")
        DEFAULT_CHAT_FRAME:AddMessage("|c" .. GetHex() .. "  > " .. t.name .. " (等级 " .. t.toRank .. "/" .. t.maxRank .. ")|r")
        LearnTalent(t.tab, t.index)
        self.applyWaiting = true
        self.applyStallTimer = 0
    end

    self.applyEventFrame:RegisterEvent("CHARACTER_POINTS_CHANGED")
    self.applyEventFrame:SetScript("OnEvent", function()
        if not SFrames.TalentTree.applyWaiting then return end
        SFrames.TalentTree.applyWaiting = false
        SFrames.TalentTree.applyStallTimer = 0
        TryNextTalent()
    end)

    self.applyEventFrame:SetScript("OnUpdate", function()
        if not SFrames.TalentTree.applyWaiting then return end
        SFrames.TalentTree.applyStallTimer = (SFrames.TalentTree.applyStallTimer or 0) + (arg1 or 0)
        if SFrames.TalentTree.applyStallTimer >= 2.0 then
            SFrames.TalentTree.applyStallTimer = 0
            SFrames.TalentTree.applyWaiting = false
            if table.getn(SFrames.TalentTree.applyQueue) > 0 then
                TryNextTalent()
            else
                FinishApply()
            end
        end
    end)

    TryNextTalent()
end

--------------------------------------------------------------------------------
-- Update display
--------------------------------------------------------------------------------
function SFrames.TalentTree:Update()
    if not self.frame or not self.frame:IsShown() then return end
    if not self.tabs or not self.tabs[1] then return end

    local unspent = self:GetRemainingUnspent()
    local realUnspent = UnitCharacterPoints("player")
    local numTabs = TT_GetNumTabs(self)

    for tb = 1, numTabs do
        if not self.tabs[tb] then break end
        local treePts = self:GetVirtualTreePoints(tb)
        self.tabs[tb].frame.pointsText:SetText("已用: |c" .. GetHex() .. treePts .. "|r")

        local numT = TT_GetNumTalents(self, tb)
        for idx = 1, numT do
            local btn = self.tabs[tb].talents[idx]
            if btn then
                local _, _, tier, column, rank, maxRank = TT_GetTalentInfo(self, tb, idx)
                if not IsViewingOwnClass(self) then rank = 0 end
                local virtRank = self:GetVirtualRank(tb, idx)

                if virtRank > 0 or (not self.simMode and rank and rank > 0) then
                    local displayRank = virtRank
                    if not self.simMode and displayRank == 0 and rank and rank > 0 then
                        displayRank = rank
                    end
                    btn.rankText:SetText(displayRank .. "/" .. maxRank)
                    btn.rankBg:Show()
                    btn.rankText:Show()
                else
                    btn.rankText:SetText("")
                    btn.rankText:Hide()
                    btn.rankBg:Hide()
                end

                local prereqMet = true
                if btn.prereqIndex then
                    local pVirtRank = self:GetVirtualRank(tb, btn.prereqIndex)
                    local pMaxRank = self.tabs[tb].talents[btn.prereqIndex].maxRank
                    if pVirtRank < pMaxRank then
                        prereqMet = false
                    end
                end

                local isLearnable = (unspent > 0 and treePts >= (tier - 1) * 5 and virtRank < maxRank and prereqMet)

                if btn.prereqLines then
                    for _, line in ipairs(btn.prereqLines) do
                        if prereqMet then
                            line:SetTexture(T.accent[1], T.accent[2], T.accent[3], 0.8)
                        else
                            line:SetTexture(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4])
                        end
                    end
                end

                local isVirtual = (not self.simMode and virtRank > (rank or 0)) or (self.simMode and virtRank > 0)

                if (not self.simMode and isVirtual) or (self.simMode and isVirtual and virtRank < maxRank) then
                    btn.rankText:SetTextColor(T.title[1], T.title[2], T.title[3])
                    btn.icon:SetVertexColor(T.accentLight[1], T.accentLight[2], T.accentLight[3])
                    btn.borderTex:SetVertexColor(T.accent[1], T.accent[2], T.accent[3])
                elseif virtRank >= maxRank then
                    btn.rankText:SetTextColor(T.title[1], T.title[2], T.title[3])
                    btn.icon:SetVertexColor(1, 1, 1)
                    btn.borderTex:SetVertexColor(T.accent[1], T.accent[2], T.accent[3])
                elseif virtRank > 0 then
                    btn.rankText:SetTextColor(0, 1, 0)
                    btn.icon:SetVertexColor(1, 1, 1)
                    btn.borderTex:SetVertexColor(0, 0.7, 0)
                elseif isLearnable then
                    btn.rankText:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
                    btn.icon:SetVertexColor(1, 1, 1)
                    btn.borderTex:SetVertexColor(T.passive[1], T.passive[2], T.passive[3])
                else
                    btn.rankText:SetTextColor(T.passive[1], T.passive[2], T.passive[3])
                    btn.icon:SetVertexColor(T.passive[1], T.passive[2], T.passive[3])
                    btn.borderTex:SetVertexColor(T.passive[1], T.passive[2], T.passive[3])
                end
            end
        end
    end

    if self.simMode then
        if IsViewingOwnClass(self) then
            self.frame.pointsText:SetText("剩余天赋点: |c" .. GetHex() .. unspent .. " |cff888888(总 " .. realUnspent .. ")|r")
        else
            self.frame.pointsText:SetText("剩余天赋点: |c" .. GetHex() .. unspent .. "|r")
        end
    else
        self.frame.pointsText:SetText("剩余天赋点: |c" .. GetHex() .. realUnspent .. "|r")
    end
end

--------------------------------------------------------------------------------
-- Encode current talents to turtlecraft-compatible string
--------------------------------------------------------------------------------
function SFrames.TalentTree:EncodeTalents()
    local numTabs = TT_GetNumTabs(self)
    local parts = {}
    for t = 1, 3 do
        local grid = {}
        for pos = 1, 28 do grid[pos] = 0 end
        if t <= numTabs then
            local numT = TT_GetNumTalents(self, t)
            for i = 1, numT do
                local _, _, tier, column = TT_GetTalentInfo(self, t, i)
                local rank = self:GetVirtualRank(t, i)
                if tier and column then
                    grid[(tier - 1) * 4 + column] = rank
                end
            end
        end
        parts[t] = EncodeTreeGrid(grid)
    end
    return parts[1] .. "-" .. parts[2] .. "-" .. parts[3]
end

--------------------------------------------------------------------------------
-- Decode turtlecraft string into virtualPoints
--------------------------------------------------------------------------------
function SFrames.TalentTree:DecodeTalents(codeStr)
    if not codeStr or codeStr == "" then return nil end

    local s = codeStr
    local pIdx = string.find(s, "points=", 1, true)
    if pIdx then s = string.sub(s, pIdx + 7) end
    local amp = string.find(s, "&", 1, true)
    if amp then s = string.sub(s, 1, amp - 1) end

    local treeParts = {}
    local rest = s
    while true do
        local dash = string.find(rest, "-", 1, true)
        if dash then
            table.insert(treeParts, string.sub(rest, 1, dash - 1))
            rest = string.sub(rest, dash + 1)
        else
            table.insert(treeParts, rest)
            break
        end
    end

    local grids = {}
    for i = 1, 3 do
        grids[i] = DecodeTreeGrid(treeParts[i] or "")
    end
    return grids
end

--------------------------------------------------------------------------------
-- Get points summary string (e.g. "31/20/0")
--------------------------------------------------------------------------------
function SFrames.TalentTree:GetPointsSummary()
    local numTabs = TT_GetNumTabs(self)
    local pts = {}
    for t = 1, 3 do
        if t <= numTabs then
            pts[t] = self:GetVirtualTreePoints(t)
        else
            pts[t] = 0
        end
    end
    return pts[1] .. "/" .. pts[2] .. "/" .. pts[3]
end

--------------------------------------------------------------------------------
-- Export dialog
--------------------------------------------------------------------------------
function SFrames.TalentTree:ShowExportDialog()
    local code = self:EncodeTalents()
    local classKey = self.viewingClass or self.playerClass
    local className = classKey
    for _, c in ipairs(CLASS_LIST) do
        if c.key == classKey then className = c.name; break end
    end
    local urlBase = "https://talents.turtlecraft.gg/" .. string.lower(classKey) .. "?points="

    if not self.exportFrame then
        local ef = CreateFrame("Frame", "NanamiTalentExportFrame", UIParent)
        ef:SetWidth(440)
        ef:SetHeight(180)
        ef:SetPoint("CENTER", UIParent, "CENTER", 0, 100)
        SetRoundBackdrop(ef, T.panelBg, T.panelBorder)
        CreateShadow(ef, 4)
        ef:EnableMouse(true)
        ef:SetMovable(true)
        ef:RegisterForDrag("LeftButton")
        ef:SetScript("OnDragStart", function() this:StartMoving() end)
        ef:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
        ef:SetFrameStrata("DIALOG")
        ef:Hide()

        ef.title = MakeFS(ef, 13, "CENTER", T.titleColor)
        ef.title:SetPoint("TOP", ef, "TOP", 0, -12)

        ef.desc = MakeFS(ef, 10, "CENTER", T.dimText)
        ef.desc:SetPoint("TOP", ef.title, "BOTTOM", 0, -4)
        ef.desc:SetText("Ctrl+A 全选，Ctrl+C 复制")

        local codeLabel = MakeFS(ef, 10, "LEFT", T.dimText)
        codeLabel:SetPoint("TOPLEFT", ef, "TOPLEFT", 20, -48)
        codeLabel:SetText("天赋代码:")

        local eb = CreateFrame("EditBox", "NanamiTalentExportEB", ef, "InputBoxTemplate")
        eb:SetWidth(340)
        eb:SetHeight(22)
        eb:SetPoint("LEFT", codeLabel, "RIGHT", 6, 0)
        eb:SetFont(GetFont(), 11)
        eb:SetAutoFocus(true)
        eb:SetScript("OnEscapePressed", function() ef:Hide() end)
        eb:SetScript("OnEnterPressed", function() ef:Hide() end)
        ef.editBox = eb

        local urlLabel = MakeFS(ef, 10, "LEFT", T.dimText)
        urlLabel:SetPoint("TOPLEFT", codeLabel, "BOTTOMLEFT", 0, -10)
        urlLabel:SetText("完整链接:")

        local urlEB = CreateFrame("EditBox", "NanamiTalentExportUrlEB", ef, "InputBoxTemplate")
        urlEB:SetWidth(286)
        urlEB:SetHeight(22)
        urlEB:SetPoint("LEFT", urlLabel, "RIGHT", 6, 0)
        urlEB:SetFont(GetFont(), 10)
        urlEB:SetAutoFocus(false)
        urlEB:SetScript("OnEscapePressed", function() ef:Hide() end)
        urlEB:SetScript("OnEnterPressed", function() ef:Hide() end)
        ef.urlEditBox = urlEB

        local copyUrlBtn = CreateFrame("Button", nil, ef)
        copyUrlBtn:SetWidth(50)
        copyUrlBtn:SetHeight(22)
        copyUrlBtn:SetPoint("LEFT", urlEB, "RIGHT", 4, 0)
        StyleButton(copyUrlBtn, "选中")
        copyUrlBtn.nanamiTooltip = { "选中链接", "选中链接文本以便 Ctrl+C 复制" }
        copyUrlBtn:SetScript("OnClick", function()
            ef.urlEditBox:SetFocus()
            ef.urlEditBox:HighlightText()
        end)

        local chatBtn = CreateFrame("Button", nil, ef)
        chatBtn:SetWidth(80)
        chatBtn:SetHeight(24)
        chatBtn:SetPoint("BOTTOMRIGHT", ef, "BOTTOM", -4, 10)
        StyleButton(chatBtn, "发送到聊天")
        chatBtn.nanamiTooltip = { "发送到聊天", "将天赋方案发送到聊天频道，\n同插件用户可点击链接直接导入。" }
        chatBtn:SetScript("OnClick", function()
            local eFrame = SFrames.TalentTree.exportFrame
            SFrames.TalentTree:SendTalentToChat(eFrame.currentCode, eFrame.currentClassKey, eFrame.currentPoints)
            eFrame:Hide()
        end)
        ef.chatBtn = chatBtn

        local closeBtn = CreateFrame("Button", nil, ef)
        closeBtn:SetWidth(60)
        closeBtn:SetHeight(24)
        closeBtn:SetPoint("BOTTOMLEFT", ef, "BOTTOM", 4, 10)
        StyleButton(closeBtn, "关闭")
        closeBtn:SetScript("OnClick", function() ef:Hide() end)

        self.exportFrame = ef
    end

    local shareCode = classKey .. ":" .. code
    local pointsSummary = self:GetPointsSummary()
    self.exportFrame.currentCode = code
    self.exportFrame.currentClassKey = classKey
    self.exportFrame.currentPoints = pointsSummary
    self.exportFrame.title:SetText("|c" .. GetHex() .. "导出天赋 - " .. className .. " (" .. pointsSummary .. ")|r")
    self.exportFrame.editBox:SetText(shareCode)
    self.exportFrame.urlEditBox:SetText(urlBase .. code)
    self.exportFrame:Show()
    self.exportFrame.editBox:HighlightText()
    self.exportFrame.editBox:SetFocus()
end

--------------------------------------------------------------------------------
-- Import dialog
--------------------------------------------------------------------------------
function SFrames.TalentTree:ShowImportDialog()
    if not self.importFrame then
        local imf = CreateFrame("Frame", "NanamiTalentImportFrame", UIParent)
        imf:SetWidth(440)
        imf:SetHeight(130)
        imf:SetPoint("CENTER", UIParent, "CENTER", 0, 100)
        SetRoundBackdrop(imf, T.panelBg, T.panelBorder)
        CreateShadow(imf, 4)
        imf:EnableMouse(true)
        imf:SetMovable(true)
        imf:RegisterForDrag("LeftButton")
        imf:SetScript("OnDragStart", function() this:StartMoving() end)
        imf:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
        imf:SetFrameStrata("DIALOG")
        imf:Hide()

        imf.title = MakeFS(imf, 13, "CENTER", T.titleColor)
        imf.title:SetPoint("TOP", imf, "TOP", 0, -12)
        imf.title:SetText("|c" .. GetHex() .. "导入天赋方案|r")

        imf.desc = MakeFS(imf, 10, "CENTER", T.dimText)
        imf.desc:SetPoint("TOP", imf.title, "BOTTOM", 0, -4)
        imf.desc:SetText("粘贴天赋代码 (如 HUNTER:Eo--)、完整 URL 或聊天链接")

        local eb = CreateFrame("EditBox", "NanamiTalentImportEB", imf, "InputBoxTemplate")
        eb:SetWidth(400)
        eb:SetHeight(24)
        eb:SetPoint("TOP", imf.desc, "BOTTOM", 0, -8)
        eb:SetFont(GetFont(), 12)
        eb:SetAutoFocus(true)
        eb:SetScript("OnEscapePressed", function() imf:Hide() end)
        eb:SetScript("OnEnterPressed", function()
            SFrames.TalentTree:ImportTalentCode(this:GetText())
            imf:Hide()
        end)
        imf.editBox = eb

        local okBtn = CreateFrame("Button", nil, imf)
        okBtn:SetWidth(60)
        okBtn:SetHeight(24)
        okBtn:SetPoint("BOTTOMRIGHT", imf, "BOTTOM", -4, 10)
        StyleButton(okBtn, "导入")
        okBtn:SetScript("OnClick", function()
            SFrames.TalentTree:ImportTalentCode(imf.editBox:GetText())
            imf:Hide()
        end)

        local cancelBtn = CreateFrame("Button", nil, imf)
        cancelBtn:SetWidth(60)
        cancelBtn:SetHeight(24)
        cancelBtn:SetPoint("BOTTOMLEFT", imf, "BOTTOM", 4, 10)
        StyleButton(cancelBtn, "取消")
        cancelBtn:SetScript("OnClick", function() imf:Hide() end)

        self.importFrame = imf
    end

    self.importFrame.editBox:SetText("")
    self.importFrame:Show()
    self.importFrame.editBox:SetFocus()
end

function SFrames.TalentTree:ImportTalentCode(codeStr)
    if not codeStr or codeStr == "" then
        DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[错误]|r 未输入天赋代码。")
        return
    end

    local detectedClass = nil
    local cleanCode = codeStr

    local _, _, urlClassStr = string.find(codeStr, "turtlecraft%.gg/(%a+)")
    if urlClassStr then
        local mapped = CLASS_KEY_LOOKUP[string.lower(urlClassStr)]
        if mapped then detectedClass = mapped end
    end

    if not detectedClass then
        local _, _, prefix, rest = string.find(codeStr, "^(%u+):(.+)$")
        if prefix and CLASS_KEY_LOOKUP[prefix] then
            detectedClass = CLASS_KEY_LOOKUP[prefix]
            cleanCode = rest
        end
    end

    if detectedClass and detectedClass ~= (self.viewingClass or self.playerClass) then
        if detectedClass ~= self.playerClass then
            local hasData = HasCacheData(detectedClass)
            if not hasData then
                UIErrorsFrame:AddMessage("该职业天赋数据不可用，请先用该职业角色登录", 1, 0.5, 0.5, 1)
                return
            end
        end
        if not self.simMode then self.simMode = true end
        self.viewingClass = detectedClass
        self.virtualPoints = {}
        self:DestroyTrees()
        self:BuildTrees()
        self:UpdateSimModeLabel()
    end

    local grids = self:DecodeTalents(cleanCode)
    if not grids then
        DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[错误]|r 天赋代码解析失败。")
        return
    end

    if not self.simMode then
        self.simMode = true
    end

    self:UpdateSimModeLabel()

    self.virtualPoints = {}
    local numTabs = TT_GetNumTabs(self)
    for t = 1, numTabs do
        self.virtualPoints[t] = {}
        local numT = TT_GetNumTalents(self, t)
        for idx = 1, numT do
            local _, _, tier, column, _, maxRank = TT_GetTalentInfo(self, t, idx)
            if tier and column and grids[t] then
                local gridPos = (tier - 1) * 4 + column
                local rank = grids[t][gridPos] or 0
                if rank > (maxRank or 5) then rank = maxRank or 5 end
                self.virtualPoints[t][idx] = rank
            else
                self.virtualPoints[t][idx] = 0
            end
        end
    end

    self:Update()

    local impClassName = ""
    if detectedClass then
        for _, c in ipairs(CLASS_LIST) do
            if c.key == detectedClass then impClassName = c.name; break end
        end
        DEFAULT_CHAT_FRAME:AddMessage("|c" .. GetHex() .. "Nanami:|r 天赋方案已导入到预览模式。(职业: " .. impClassName .. ")")
    else
        DEFAULT_CHAT_FRAME:AddMessage("|c" .. GetHex() .. "Nanami:|r 天赋方案已导入到预览模式。")
    end
end

--------------------------------------------------------------------------------
-- Chat talent link: send / receive
--------------------------------------------------------------------------------
function SFrames.TalentTree:SendTalentToChat(code, classKey, points)
    local className = classKey or ""
    for _, c in ipairs(CLASS_LIST) do
        if c.key == classKey then className = c.name; break end
    end
    local msg = "[NUI:" .. className .. ":" .. (points or "0/0/0") .. ":" .. (classKey or "") .. ":" .. (code or "") .. "]"
    ChatFrame_OpenChat(msg, DEFAULT_CHAT_FRAME)
end

function SFrames.TalentTree:HookChatTalentLinks()
    local origSetItemRef = SetItemRef
    SetItemRef = function(link, text, button)
        local _, _, classKey, code = string.find(link, "^nanami:talent:(%u+):(.+)$")
        if classKey and code then
            local TT = SFrames.TalentTree
            if TT.frame and not TT.frame:IsShown() then
                TT:BuildTrees()
                TT.frame:Show()
            end
            TT:ImportTalentCode(classKey .. ":" .. code)
            return
        end
        if origSetItemRef then
            return origSetItemRef(link, text, button)
        end
    end

    local origChatFrame_OnEvent = ChatFrame_OnEvent
    ChatFrame_OnEvent = function(event)
        if arg1 and type(arg1) == "string" and string.find(arg1, "[NUI:", 1, true) then
            arg1 = FilterNanamiTalentLink(arg1)
        end
        origChatFrame_OnEvent(event)
    end
end

--------------------------------------------------------------------------------
-- Save dialog
--------------------------------------------------------------------------------
function SFrames.TalentTree:ShowSaveDialog()
    if not self.saveFrame then
        local sf = CreateFrame("Frame", "NanamiTalentSaveFrame", UIParent)
        sf:SetWidth(340)
        sf:SetHeight(120)
        sf:SetPoint("CENTER", UIParent, "CENTER", 0, 100)
        SetRoundBackdrop(sf, T.panelBg, T.panelBorder)
        CreateShadow(sf, 4)
        sf:EnableMouse(true)
        sf:SetMovable(true)
        sf:RegisterForDrag("LeftButton")
        sf:SetScript("OnDragStart", function() this:StartMoving() end)
        sf:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
        sf:SetFrameStrata("DIALOG")
        sf:Hide()

        sf.title = MakeFS(sf, 13, "CENTER", T.titleColor)
        sf.title:SetPoint("TOP", sf, "TOP", 0, -12)
        sf.title:SetText("|c" .. GetHex() .. "保存天赋方案|r")

        sf.desc = MakeFS(sf, 10, "CENTER", T.dimText)
        sf.desc:SetPoint("TOP", sf.title, "BOTTOM", 0, -4)
        sf.desc:SetText("输入方案名称")

        local eb = CreateFrame("EditBox", "NanamiTalentSaveEB", sf, "InputBoxTemplate")
        eb:SetWidth(300)
        eb:SetHeight(24)
        eb:SetPoint("TOP", sf.desc, "BOTTOM", 0, -6)
        eb:SetFont(GetFont(), 12)
        eb:SetAutoFocus(true)
        eb:SetMaxLetters(32)
        eb:SetScript("OnEscapePressed", function() sf:Hide() end)
        eb:SetScript("OnEnterPressed", function()
            SFrames.TalentTree:SaveCurrentBuild(this:GetText())
            sf:Hide()
        end)
        sf.editBox = eb

        local okBtn = CreateFrame("Button", nil, sf)
        okBtn:SetWidth(60)
        okBtn:SetHeight(24)
        okBtn:SetPoint("BOTTOMRIGHT", sf, "BOTTOM", -4, 10)
        StyleButton(okBtn, "保存")
        okBtn:SetScript("OnClick", function()
            SFrames.TalentTree:SaveCurrentBuild(sf.editBox:GetText())
            sf:Hide()
        end)

        local cancelBtn = CreateFrame("Button", nil, sf)
        cancelBtn:SetWidth(60)
        cancelBtn:SetHeight(24)
        cancelBtn:SetPoint("BOTTOMLEFT", sf, "BOTTOM", 4, 10)
        StyleButton(cancelBtn, "取消")
        cancelBtn:SetScript("OnClick", function() sf:Hide() end)

        self.saveFrame = sf
    end

    local classKey = self.viewingClass or self.playerClass
    local className = classKey
    for _, c in ipairs(CLASS_LIST) do
        if c.key == classKey then className = c.name; break end
    end
    self.saveFrame.desc:SetText(className .. " - " .. self:GetPointsSummary())
    self.saveFrame.editBox:SetText("")
    self.saveFrame:Show()
    self.saveFrame.editBox:SetFocus()
end

function SFrames.TalentTree:SaveCurrentBuild(name)
    if not name or name == "" then
        DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[错误]|r 请输入方案名称。")
        return
    end

    local classKey = self.viewingClass or self.playerClass
    local code = self:EncodeTalents()
    local points = self:GetPointsSummary()

    local store = GetBuildsStore()
    if not store[classKey] then store[classKey] = {} end
    table.insert(store[classKey], { name = name, code = code, points = points })

    DEFAULT_CHAT_FRAME:AddMessage("|c" .. GetHex() .. "Nanami:|r 方案「" .. name .. "」已保存。(" .. points .. ")")
end

--------------------------------------------------------------------------------
-- Builds management panel
--------------------------------------------------------------------------------
function SFrames.TalentTree:CreateBuildsPanel()
    if self.buildsPanel then return end

    local f = self.frame
    local PANEL_W = 380
    local PANEL_H = 370
    local ENTRIES_PER_PAGE = 8
    local ENTRY_H = 30
    local CLASS_BAR_H = 24

    local p = CreateFrame("Frame", nil, f)
    p:SetWidth(PANEL_W)
    p:SetHeight(PANEL_H)
    p:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 6, 56)
    SetRoundBackdrop(p, { 0.08, 0.08, 0.12, 0.97 }, T.panelBorder)
    CreateShadow(p, 3)
    p:SetFrameLevel(f:GetFrameLevel() + 12)
    p:EnableMouse(true)
    p:Hide()

    p.title = MakeFS(p, 13, "CENTER", T.titleColor)
    p.title:SetPoint("TOP", p, "TOP", 0, -8)
    p.title:SetText("|c" .. GetHex() .. "方案管理|r")

    local closeBtn = CreateFrame("Button", nil, p)
    closeBtn:SetWidth(16)
    closeBtn:SetHeight(16)
    closeBtn:SetPoint("TOPRIGHT", p, "TOPRIGHT", -6, -6)
    closeBtn:SetFrameLevel(p:GetFrameLevel() + 1)
    SetRoundBackdrop(closeBtn, T.buttonDownBg, T.btnBorder)
    local closeTxt = MakeFS(closeBtn, 9, "CENTER", T.title)
    closeTxt:SetPoint("CENTER", closeBtn, "CENTER", 0, 0)
    closeTxt:SetText("x")
    closeBtn:SetScript("OnClick", function() p:Hide() end)
    closeBtn:SetScript("OnEnter", function()
        this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4])
    end)
    closeBtn:SetScript("OnLeave", function()
        this:SetBackdropColor(T.buttonDownBg[1], T.buttonDownBg[2], T.buttonDownBg[3], T.buttonDownBg[4])
    end)

    -- Class selector bar
    local classBarFrame = CreateFrame("Frame", nil, p)
    classBarFrame:SetHeight(CLASS_BAR_H)
    classBarFrame:SetPoint("TOPLEFT", p, "TOPLEFT", 8, -24)
    classBarFrame:SetPoint("TOPRIGHT", p, "TOPRIGHT", -8, -24)
    classBarFrame:SetFrameLevel(p:GetFrameLevel() + 1)
    p.classBarFrame = classBarFrame

    local numClasses = table.getn(CLASS_LIST)
    local cbGap = 2
    local cbW = math.floor((PANEL_W - 16 - (numClasses - 1) * cbGap) / numClasses)
    p.classBtns = {}

    for ci, cinfo in ipairs(CLASS_LIST) do
        local cb = CreateFrame("Button", nil, classBarFrame)
        cb:SetWidth(cbW)
        cb:SetHeight(CLASS_BAR_H - 2)
        cb:SetPoint("TOPLEFT", classBarFrame, "TOPLEFT", (ci - 1) * (cbW + cbGap), 0)
        SetPixelBackdrop(cb, T.slotBg, T.slotBorder)
        cb:SetFrameLevel(classBarFrame:GetFrameLevel() + 1)

        local cIcon = SFrames:CreateClassIcon(cb, 16)
        cIcon.overlay:SetPoint("CENTER", cb, "CENTER", 0, 0)
        SFrames:SetClassIcon(cIcon, cinfo.key)
        cb.classIconTex = cIcon
        cb.classKey = cinfo.key
        cb.classColor = cinfo.color

        cb:SetScript("OnClick", function()
            p.selectedClass = this.classKey
            p.page = 0
            SFrames.TalentTree:RefreshBuildsPanel()
        end)
        cb:SetScript("OnEnter", function()
            this:SetBackdropColor(T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4])
            GameTooltip:SetOwner(this, "ANCHOR_TOP")
            local cn = this.classKey
            for _, c in ipairs(CLASS_LIST) do
                if c.key == cn then cn = c.name; break end
            end
            GameTooltip:AddLine(cn, this.classColor[1], this.classColor[2], this.classColor[3])
            local st = GetBuildsStore()
            local bl = st[this.classKey]
            local cnt = bl and table.getn(bl) or 0
            GameTooltip:AddLine(cnt .. " 个方案", 0.7, 0.7, 0.7)
            GameTooltip:Show()
        end)
        cb:SetScript("OnLeave", function()
            this:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4])
            GameTooltip:Hide()
        end)
        p.classBtns[ci] = cb
    end

    -- List area
    local listTop = -24 - CLASS_BAR_H - 4
    p.emptyText = MakeFS(p, 11, "CENTER", T.dimText)
    p.emptyText:SetPoint("CENTER", p, "CENTER", 0, -10)
    p.emptyText:SetText("暂无已保存的方案")

    p.entries = {}
    p.page = 0

    for i = 1, ENTRIES_PER_PAGE do
        local entry = CreateFrame("Frame", nil, p)
        entry:SetHeight(ENTRY_H)
        entry:SetPoint("TOPLEFT", p, "TOPLEFT", 8, listTop - (i - 1) * (ENTRY_H + 2))
        entry:SetPoint("RIGHT", p, "RIGHT", -8, 0)
        SetPixelBackdrop(entry, T.slotBg, T.slotBorder)
        entry:SetFrameLevel(p:GetFrameLevel() + 1)

        local classIcon = SFrames:CreateClassIcon(entry, 16)
        classIcon.overlay:SetPoint("LEFT", entry, "LEFT", 5, 0)
        entry.classIcon = classIcon

        entry.nameText = MakeFS(entry, 10, "LEFT", T.valueText)
        entry.nameText:SetPoint("LEFT", classIcon.overlay, "RIGHT", 4, 0)

        entry.ptsText = MakeFS(entry, 9, "LEFT", T.dimText)
        entry.ptsText:SetPoint("LEFT", entry.nameText, "RIGHT", 4, 0)

        local delBtn = CreateFrame("Button", nil, entry)
        delBtn:SetWidth(36)
        delBtn:SetHeight(20)
        delBtn:SetPoint("RIGHT", entry, "RIGHT", -3, 0)
        delBtn:SetFrameLevel(entry:GetFrameLevel() + 1)
        StyleButton(delBtn, "删除")
        entry.delBtn = delBtn

        local loadBtn = CreateFrame("Button", nil, entry)
        loadBtn:SetWidth(36)
        loadBtn:SetHeight(20)
        loadBtn:SetPoint("RIGHT", delBtn, "LEFT", -2, 0)
        loadBtn:SetFrameLevel(entry:GetFrameLevel() + 1)
        StyleButton(loadBtn, "加载")
        entry.loadBtn = loadBtn

        local exportBtn = CreateFrame("Button", nil, entry)
        exportBtn:SetWidth(36)
        exportBtn:SetHeight(20)
        exportBtn:SetPoint("RIGHT", loadBtn, "LEFT", -2, 0)
        exportBtn:SetFrameLevel(entry:GetFrameLevel() + 1)
        StyleButton(exportBtn, "导出")
        entry.exportBtn = exportBtn

        entry.nameText:SetPoint("RIGHT", exportBtn, "LEFT", -4, 0)

        entry:Hide()
        p.entries[i] = entry
    end

    -- Page nav
    local pageFrame = CreateFrame("Frame", nil, p)
    pageFrame:SetHeight(22)
    pageFrame:SetPoint("BOTTOMLEFT", p, "BOTTOMLEFT", 8, 6)
    pageFrame:SetPoint("BOTTOMRIGHT", p, "BOTTOMRIGHT", -8, 6)
    p.pageFrame = pageFrame

    p.pageText = MakeFS(pageFrame, 9, "CENTER", T.dimText)
    p.pageText:SetPoint("CENTER", pageFrame, "CENTER", 0, 0)

    local prevBtn = CreateFrame("Button", nil, pageFrame)
    prevBtn:SetWidth(46)
    prevBtn:SetHeight(18)
    prevBtn:SetPoint("LEFT", pageFrame, "LEFT", 0, 0)
    StyleButton(prevBtn, "上一页")
    prevBtn:SetScript("OnClick", function()
        if p.page > 0 then
            p.page = p.page - 1
            SFrames.TalentTree:RefreshBuildsPanel()
        end
    end)
    p.prevBtn = prevBtn

    local nextBtn = CreateFrame("Button", nil, pageFrame)
    nextBtn:SetWidth(46)
    nextBtn:SetHeight(18)
    nextBtn:SetPoint("RIGHT", pageFrame, "RIGHT", 0, 0)
    StyleButton(nextBtn, "下一页")
    nextBtn:SetScript("OnClick", function()
        p.page = p.page + 1
        SFrames.TalentTree:RefreshBuildsPanel()
    end)
    p.nextBtn = nextBtn

    self.buildsPanel = p
end

function SFrames.TalentTree:ToggleBuildsPanel()
    self:CreateBuildsPanel()
    if self.buildsPanel:IsShown() then
        self.buildsPanel:Hide()
    else
        self.buildsPanel.selectedClass = self.viewingClass or self.playerClass
        self.buildsPanel.page = 0
        self:RefreshBuildsPanel()
        self.buildsPanel:Show()
    end
end

function SFrames.TalentTree:ShowBuildsPanel()
    self:CreateBuildsPanel()
    self.buildsPanel.selectedClass = self.viewingClass or self.playerClass
    self.buildsPanel.page = 0
    self:RefreshBuildsPanel()
    self.buildsPanel:Show()
end

function SFrames.TalentTree:RefreshBuildsPanel()
    if not self.buildsPanel then return end
    local p = self.buildsPanel
    local classKey = p.selectedClass or self.viewingClass or self.playerClass
    local className = classKey
    local classColor = { 1, 1, 1 }
    for _, c in ipairs(CLASS_LIST) do
        if c.key == classKey then className = c.name; classColor = c.color; break end
    end

    -- Update class bar highlight
    for ci, cinfo in ipairs(CLASS_LIST) do
        local cb = p.classBtns[ci]
        if cb then
            if cinfo.key == classKey then
                SetPixelBackdrop(cb, T.tabActiveBg, classColor)
            else
                local st = GetBuildsStore()
                local bl = st[cinfo.key]
                local cnt = bl and table.getn(bl) or 0
                if cnt > 0 then
                    SetPixelBackdrop(cb, T.slotBg, T.slotBorder)
                else
                    SetPixelBackdrop(cb, T.emptySlotBg, T.emptySlotBd)
                end
            end
        end
    end

    p.title:SetText("|c" .. GetHex() .. "方案管理 - |r" ..
        string.format("|cff%02x%02x%02x%s|r", classColor[1]*255, classColor[2]*255, classColor[3]*255, className))

    local store = GetBuildsStore()
    local builds = store[classKey] or {}
    local total = table.getn(builds)
    local perPage = 8
    local maxPage = math.max(math.ceil(total / perPage) - 1, 0)
    if p.page > maxPage then p.page = maxPage end

    local startIdx = p.page * perPage + 1

    for i = 1, perPage do
        local entry = p.entries[i]
        local bIdx = startIdx + i - 1
        if bIdx <= total then
            local build = builds[bIdx]
            entry.nameText:SetText(build.name or "?")
            entry.ptsText:SetText("|cff888888(" .. (build.points or "?") .. ")|r")
            entry.buildIdx = bIdx
            entry.buildClass = classKey

            SFrames:SetClassIcon(entry.classIcon, classKey)

            entry.loadBtn:SetScript("OnClick", function()
                SFrames.TalentTree:LoadBuild(this:GetParent().buildClass, this:GetParent().buildIdx)
            end)
            entry.delBtn:SetScript("OnClick", function()
                SFrames.TalentTree:DeleteBuild(this:GetParent().buildClass, this:GetParent().buildIdx)
            end)
            entry.exportBtn:SetScript("OnClick", function()
                local bi = this:GetParent().buildIdx
                local ck = this:GetParent().buildClass
                local bStore = GetBuildsStore()
                local bList = bStore[ck]
                if bList and bList[bi] then
                    local b = bList[bi]
                    SFrames.TalentTree:ShowExportDialogWithCode(b.code, ck, b.points, b.name)
                end
            end)

            entry:Show()
        else
            entry:Hide()
        end
    end

    if total == 0 then
        p.emptyText:Show()
    else
        p.emptyText:Hide()
    end

    if total > perPage then
        p.pageText:SetText((p.page + 1) .. "/" .. (maxPage + 1))
        p.pageFrame:Show()
        if p.page <= 0 then p.prevBtn:Disable() else p.prevBtn:Enable() end
        if p.page >= maxPage then p.nextBtn:Disable() else p.nextBtn:Enable() end
    else
        p.pageFrame:Hide()
    end
end

function SFrames.TalentTree:ShowExportDialogWithCode(code, classKey, points, buildName)
    local className = classKey
    for _, c in ipairs(CLASS_LIST) do
        if c.key == classKey then className = c.name; break end
    end
    local urlBase = "https://talents.turtlecraft.gg/" .. string.lower(classKey) .. "?points="

    if not self.exportFrame then
        self:ShowExportDialog()
        self.exportFrame:Hide()
    end

    local shareCode = classKey .. ":" .. (code or "")
    self.exportFrame.currentCode = code or ""
    self.exportFrame.currentClassKey = classKey
    self.exportFrame.currentPoints = points or "?"
    local titleStr = className .. " (" .. (points or "?") .. ")"
    if buildName then titleStr = buildName .. " - " .. titleStr end
    self.exportFrame.title:SetText("|c" .. GetHex() .. "导出天赋 - " .. titleStr .. "|r")
    self.exportFrame.editBox:SetText(shareCode)
    self.exportFrame.urlEditBox:SetText(urlBase .. (code or ""))
    self.exportFrame:Show()
    self.exportFrame.editBox:HighlightText()
    self.exportFrame.editBox:SetFocus()
end

function SFrames.TalentTree:LoadBuild(classKey, buildIdx)
    classKey = classKey or self.viewingClass or self.playerClass
    local store = GetBuildsStore()
    local builds = store[classKey]
    if not builds or not builds[buildIdx] then return end

    local build = builds[buildIdx]

    if classKey ~= (self.viewingClass or self.playerClass) then
        if classKey ~= self.playerClass then
            local hasData = HasCacheData(classKey)
            if not hasData then
                UIErrorsFrame:AddMessage("该职业天赋数据不可用，无法预览", 1, 0.5, 0.5, 1)
                return
            end
        end
        if not self.simMode then self.simMode = true end
        self.viewingClass = classKey
        self.virtualPoints = {}
        self:DestroyTrees()
        self:BuildTrees()
        self:UpdateSimModeLabel()
    end

    self:ImportTalentCode(build.code)

    if self.buildsPanel then self.buildsPanel:Hide() end
    DEFAULT_CHAT_FRAME:AddMessage("|c" .. GetHex() .. "Nanami:|r 已加载方案「" .. (build.name or "?") .. "」")
end

function SFrames.TalentTree:DeleteBuild(classKey, buildIdx)
    classKey = classKey or self.viewingClass or self.playerClass
    local store = GetBuildsStore()
    local builds = store[classKey]
    if not builds or not builds[buildIdx] then return end

    local name = builds[buildIdx].name or "?"
    table.remove(builds, buildIdx)
    DEFAULT_CHAT_FRAME:AddMessage("|c" .. GetHex() .. "Nanami:|r 已删除方案「" .. name .. "」")
    self:RefreshBuildsPanel()
end

--------------------------------------------------------------------------------
-- Default DB export: serializer + UI
--------------------------------------------------------------------------------
local function SerializeValue(val, indent, depth)
    indent = indent or ""
    depth = depth or 0
    if depth > 20 then return "nil" end
    local ni = indent .. "    "

    if val == nil then return "nil"
    elseif type(val) == "boolean" then return val and "true" or "false"
    elseif type(val) == "number" then return tostring(val)
    elseif type(val) == "string" then
        local s = string.gsub(val, "\\", "\\\\")
        s = string.gsub(s, "\"", "\\\"")
        s = string.gsub(s, "\n", "\\n")
        s = string.gsub(s, "\r", "")
        return "\"" .. s .. "\""
    elseif type(val) == "table" then
        local isArr = true
        local maxN = 0
        for k, _ in pairs(val) do
            if type(k) ~= "number" then isArr = false; break end
            if k > maxN then maxN = k end
        end
        if maxN == 0 then isArr = false end

        local parts = {}
        if isArr then
            for i = 1, maxN do
                table.insert(parts, ni .. SerializeValue(val[i], ni, depth + 1) .. ",")
            end
        else
            local keys = {}
            for k in pairs(val) do table.insert(keys, k) end
            table.sort(keys, function(a, b)
                if type(a) == "number" and type(b) == "number" then return a < b end
                return tostring(a) < tostring(b)
            end)
            for _, k in ipairs(keys) do
                local ks
                if type(k) == "number" then
                    ks = "[" .. k .. "]"
                elseif type(k) == "string" and string.find(k, "^[%a_][%w_]*$") then
                    ks = k
                else
                    ks = "[" .. SerializeValue(k, ni, depth + 1) .. "]"
                end
                table.insert(parts, ni .. ks .. " = " .. SerializeValue(val[k], ni, depth + 1) .. ",")
            end
        end
        if table.getn(parts) == 0 then return "{}" end
        return "{\n" .. table.concat(parts, "\n") .. "\n" .. indent .. "}"
    end
    return "nil"
end

--------------------------------------------------------------------------------
-- /nui talentdb  command handler
--------------------------------------------------------------------------------
function SFrames.TalentTree:HandleTalentDBCommand(args)
    args = args or ""
    if args == "export" then
        self:ExportTalentDB()
    elseif args == "status" or args == "" then
        self:ShowTalentDBStatus()
    else
        SFrames:Print("/nui talentdb        - 查看天赋缓存状态")
        SFrames:Print("/nui talentdb export - 导出默认天赋数据库")
    end
end

function SFrames.TalentTree:ShowTalentDBStatus()
    local rawCache = SFramesGlobalDB and SFramesGlobalDB.talentCache or {}
    local defaultDB = NanamiTalentDefaultDB or {}

    SFrames:Print("=== 天赋数据库状态 ===")
    for _, cls in ipairs(CLASS_LIST) do
        local inCache   = rawCache[cls.key] ~= nil
        local inDefault = defaultDB[cls.key] ~= nil
        local status
        if inCache then
            status = "|cff00ff00已缓存(本地)|r"
        elseif inDefault then
            status = "|cffffff00默认DB|r"
        else
            status = "|cffff0000未缓存|r"
        end
        local r, g, b = cls.color[1], cls.color[2], cls.color[3]
        local hex = string.format("|cff%02x%02x%02x", r * 255, g * 255, b * 255)
        SFrames:Print("  " .. hex .. cls.name .. "|r : " .. status)
    end
end

function SFrames.TalentTree:ExportTalentDB()
    local rawCache = SFramesGlobalDB and SFramesGlobalDB.talentCache
    if not rawCache then
        SFrames:Print("天赋缓存为空，请先登录各职业角色并打开天赋面板。")
        return
    end

    local missing, found = {}, {}
    for _, cls in ipairs(CLASS_LIST) do
        if rawCache[cls.key] then
            table.insert(found, cls.name)
        else
            table.insert(missing, cls.name)
        end
    end

    if table.getn(found) == 0 then
        SFrames:Print("没有任何天赋缓存数据，请先用各职业角色登录。")
        return
    end

    if table.getn(missing) > 0 then
        SFrames:Print("|cffffff00缺少职业:|r " .. table.concat(missing, ", "))
    end

    local exportTbl = {}
    for k, v in pairs(rawCache) do exportTbl[k] = v end

    local output = "-- Auto-generated by /nui talentdb export\n"
                 .. "-- Paste this entire content into TalentDefaultDB.lua\n\n"
                 .. "NanamiTalentDefaultDB = " .. SerializeValue(exportTbl, "") .. "\n"

    self:ShowTalentDBExportDialog(output)
    SFrames:Print("已缓存 " .. table.getn(found) .. " 个职业: " .. table.concat(found, ", "))
end

function SFrames.TalentTree:ShowTalentDBExportDialog(text)
    if not self.talentDBExportFrame then
        local f = CreateFrame("Frame", "NanamiTalentDBExportFrame", UIParent)
        f:SetWidth(620)
        f:SetHeight(420)
        f:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
        SetRoundBackdrop(f, T.panelBg, T.panelBorder)
        CreateShadow(f, 5)
        f:EnableMouse(true)
        f:SetMovable(true)
        f:RegisterForDrag("LeftButton")
        f:SetScript("OnDragStart", function() this:StartMoving() end)
        f:SetScript("OnDragStop",  function() this:StopMovingOrSizing() end)
        f:SetFrameStrata("DIALOG")
        f:Hide()

        f.title = MakeFS(f, 13, "CENTER", T.titleColor)
        f.title:SetPoint("TOP", f, "TOP", 0, -10)
        f.title:SetText("|c" .. GetHex() .. "导出天赋默认数据库|r")

        f.desc = MakeFS(f, 10, "CENTER", T.dimText)
        f.desc:SetPoint("TOP", f.title, "BOTTOM", 0, -4)
        f.desc:SetText("Ctrl+A 全选, Ctrl+C 复制, 粘贴到 TalentDefaultDB.lua")

        local sf = CreateFrame("ScrollFrame", "NanamiTalentDBExportScroll", f, "UIPanelScrollFrameTemplate")
        sf:SetPoint("TOPLEFT", f, "TOPLEFT", 12, -42)
        sf:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -30, 40)

        local eb = CreateFrame("EditBox", "NanamiTalentDBExportEB", sf)
        eb:SetWidth(560)
        eb:SetFont(GetFont(), 10)
        eb:SetMultiLine(true)
        eb:SetAutoFocus(false)
        eb:SetScript("OnEscapePressed", function() this:ClearFocus() end)
        sf:SetScrollChild(eb)
        f.editBox = eb

        local closeBtn = CreateFrame("Button", nil, f)
        closeBtn:SetWidth(60)
        closeBtn:SetHeight(24)
        closeBtn:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -12, 10)
        StyleButton(closeBtn, "关闭")
        closeBtn:SetScript("OnClick", function() f:Hide() end)

        local selBtn = CreateFrame("Button", nil, f)
        selBtn:SetWidth(60)
        selBtn:SetHeight(24)
        selBtn:SetPoint("RIGHT", closeBtn, "LEFT", -6, 0)
        StyleButton(selBtn, "全选")
        selBtn:SetScript("OnClick", function()
            f.editBox:SetFocus()
            f.editBox:HighlightText()
        end)

        self.talentDBExportFrame = f
    end

    self.talentDBExportFrame.editBox:SetText(text or "")
    self.talentDBExportFrame:Show()
    self.talentDBExportFrame.editBox:SetFocus()
    self.talentDBExportFrame.editBox:HighlightText()
end
