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

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

--------------------------------------------------------------------------------
-- 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 data cache (stores talent trees per class in SFramesGlobalDB)
--------------------------------------------------------------------------------
local function GetCache()
    if not SFramesGlobalDB then SFramesGlobalDB = {} end
    if not SFramesGlobalDB.talentCache then SFramesGlobalDB.talentCache = {} end
    return SFramesGlobalDB.talentCache
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()

    SFrames:RegisterEvent("CHARACTER_POINTS_CHANGED", function()
        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)
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])
            local cache = GetCache()
            if not cache[this.classKey] and this.classKey ~= SFrames.TalentTree.playerClass then
                GameTooltip:SetOwner(this, "ANCHOR_BOTTOM")
                GameTooltip:AddLine("需先用该职业角色打开天赋面板以缓存数据", 1, 0.5, 0.5)
                GameTooltip:Show()
            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, 25)

    f.simLabel = MakeFS(f.overlay, 11, "LEFT", T.titleColor)
    f.simLabel:SetPoint("LEFT", f.pointsText, "LEFT", 0, -18)
    f.simLabel:SetText("")

    -- Bottom buttons
    f.btnApply = CreateFrame("Button", nil, f)
    f.btnApply:SetWidth(100)
    f.btnApply:SetHeight(26)
    f.btnApply:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -12, 12)
    StyleButton(f.btnApply, "应用天赋")
    f.btnApply.nanamiTooltip = { "应用天赋", "将所有模拟点数提交至服务器。" }
    f.btnApply:SetScript("OnClick", function() SFrames.TalentTree:ApplyVirtualPoints() end)

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

    f.btnSimMode = CreateFrame("Button", nil, f)
    f.btnSimMode:SetWidth(110)
    f.btnSimMode:SetHeight(26)
    f.btnSimMode:SetPoint("RIGHT", f.btnReset, "LEFT", -8, 0)
    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)

    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
    local cache = GetCache()

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

        local hasData = (cinfo.key == self.playerClass) or (cache[cinfo.key] ~= nil)

        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 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)
        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()
        if not IsViewingOwnClass(self) then
            self.frame.btnApply:Hide()
        else
            self.frame.btnApply:Show()
        end
    else
        self.frame.simModeText:SetText("|cff888888模拟加点: 关|r")
        self.frame.simLabel:SetText("")
        self.frame.classBar:Hide()
        self.frame.btnApply:Show()
        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 cache = GetCache()
        if not cache[classKey] 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 - 90)
        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, -10)
        tTitle:SetText("|c" .. GetHex() .. (name or "") .. "|r")

        local tPoints = MakeFS(tabFrame, 12, "CENTER", T.dimText)
        tPoints:SetPoint("TOP", tTitle, "BOTTOM", 0, -2)
        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 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) - 44
            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

            local cachedDesc = nil
            if not IsViewingOwnClass(self) then
                local cd = GetCache()[self.viewingClass]
                if cd and cd[t] and cd[t].talents[i] then
                    cachedDesc = cd[t].talents[i].desc
                end
            end
            btn.cachedDesc = cachedDesc

            if IsViewingOwnClass(self) then
                btn:SetScript("OnEnter", function()
                    GameTooltip_SetDefaultAnchor(GameTooltip, this)
                    GameTooltip:SetTalent(this.tab, this.index)
                    GameTooltip:Show()
                end)
            else
                btn:SetScript("OnEnter", function()
                    GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
                    GameTooltip:AddLine(this.talentName or "?", 1, 1, 1)
                    local vr = SFrames.TalentTree:GetVirtualRank(this.tab, this.index)
                    GameTooltip:AddLine("等级 " .. vr .. "/" .. (this.maxRank or "?"), 0.7, 0.7, 0.7)
                    if this.cachedDesc then
                        GameTooltip:AddLine(" ")
                        for _, line in ipairs(this.cachedDesc) do
                            GameTooltip:AddLine(line, 1, 0.82, 0, 1)
                        end
                    end
                    GameTooltip:Show()
                end)
            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) - 44
                    local cX = (column - 1) * (ICON_SIZE + ICON_SPACING_X) + GRID_PAD_X
                    local cY = -(tier - 1) * (ICON_SIZE + ICON_SPACING_Y) - 44

                    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

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

function SFrames.TalentTree:ResetVirtualPoints()
    self.virtualPoints = {}
    self:Update()
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

    if not self.applyQueue then self.applyQueue = {} end
    self.applyQueue = {}

    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)
            local diff = virtRank - realRank
            if diff > 0 then
                for i = 1, diff do
                    table.insert(self.applyQueue, {tab = tb, index = idx})
                end
            end
        end
    end

    if table.getn(self.applyQueue) > 0 then
        self.frame:SetScript("OnUpdate", function()
            if table.getn(SFrames.TalentTree.applyQueue) > 0 then
                local t = table.remove(SFrames.TalentTree.applyQueue, 1)
                LearnTalent(t.tab, t.index)
            else
                this:SetScript("OnUpdate", nil)
                SFrames.TalentTree.simMode = false
                SFrames.TalentTree:UpdateSimModeLabel()
            end
        end)
    else
        DEFAULT_CHAT_FRAME:AddMessage("|c" .. GetHex() .. "Nanami:|r 没有新的天赋点数需要应用。")
    end
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
