-- ## Author: Marvin Pedlar ## Version: 0.1.2 ## IconTexture: Interface\Icons\Trade_Engineering
-- TalentLoadoutDeleter / Data.lua
-- Pure functions: enumerate, filter, sort talent loadouts.
-- Returns the module table (also attaches to TalentLoadoutDeleter.Data for in-game use).

local TalentLoadoutDeleter = TalentLoadoutDeleter or {}

local Data = {}

function Data.GetLoadouts(specID, activeConfigID)
  local configIDs = C_ClassTalents.GetConfigIDsBySpecID(specID) or {}
  local rows = {}
  for _, id in ipairs(configIDs) do
    local info = C_Traits.GetConfigInfo(id)
    if info ~= nil and info.type == Enum.TraitConfigType.Combat then
      table.insert(rows, {
        id = id,
        name = info.name,
        isActive = (id == activeConfigID),
      })
    end
  end

  table.sort(rows, function(a, b)
    if a.isActive ~= b.isActive then
      return a.isActive  -- active goes first
    end
    return string.lower(a.name) < string.lower(b.name)
  end)

  return rows
end

TalentLoadoutDeleter.Data = Data
--return Data


-- TalentLoadoutDeleter / Hooks.lua
-- Blizzard talent UI integration: Manage button + inline [X].
--
-- Inline X uses Menu.ModifyMenu("MENU_CLASS_TALENT_PROFILE", cb) +
-- MenuTemplates.Attach* helpers exclusively. The compositor cleans every
-- attachment on menu close, so there are no orphans across pool reuse and
-- no taint leaks onto the session-wide menu button pool. Raw key writes
-- (button._foo = x), iteration of pooled children (button:GetChildren()),
-- or CreateFrame with a pooled parent would all taint the pool — see
-- addon-dev-learning.md B1 for the incident this design avoids.

local Hooks = {}
local installed = false

local STOP_TEXTURE = "Interface\\Buttons\\UI-StopButton"
local TIP_DELETE = "Shift-点击删除该方案."
local TIP_ACTIVE = "无法删除使用中的方案."

local function createManageButton(parent)
  local button = CreateFrame("Button", "TalentLoadoutDeleterManageButton",
    parent, "UIPanelButtonTemplate")
  button:SetText("管理天赋方案")
  button:SetSize(120, 26)
  button:SetScript("OnClick", function()
    if TalentLoadoutDeleter.Window and TalentLoadoutDeleter.Window.Toggle then
      TalentLoadoutDeleter.Window.Toggle()
    end
  end)
  return button
end

local function anchorManageButton(button)
  -- Blizzard's SearchBox is anchored LEFT to LoadSystem.RIGHT + 20 (per
  -- Blizzard_ClassTalentsFrame.xml line 236), so we'd overlap it if we
  -- anchored next to LoadSystem itself. Prefer anchoring past the
  -- SearchBox; fall back to LoadSystem with a wide offset if SearchBox
  -- isn't available for some reason.
  local tf = PlayerSpellsFrame and PlayerSpellsFrame.TalentsFrame
  if not tf then
    button:Hide()
    return
  end
  local searchBox = tf.SearchBox
  local loadSystem = tf.LoadSystem
  button:ClearAllPoints()
  if searchBox then
    button:SetPoint("LEFT", searchBox, "RIGHT", 8, 0)
    button:Show()
  elseif loadSystem then
    button:SetPoint("LEFT", loadSystem, "RIGHT", 220, 0)
    button:Show()
  else
    button:Hide()
  end
end

local function isDeletableLoadoutData(data)
  if type(data) ~= "number" then return false end
  local starter = Constants and Constants.TraitConsts
    and Constants.TraitConsts.STARTER_BUILD_TRAIT_CONFIG_ID
  if starter ~= nil and data == starter then return false end
  return true
end

local function decorateLoadoutMenu(ownerRegion, rootDescription, contextData)
  if not (rootDescription and rootDescription.EnumerateElementDescriptions) then return end
  for _, desc in rootDescription:EnumerateElementDescriptions() do
    local configID = desc:GetData()
    if isDeletableLoadoutData(configID) then
      desc:AddInitializer(function(menuButton, description, menu)
        local specID = PlayerUtil and PlayerUtil.GetCurrentSpecID
          and PlayerUtil.GetCurrentSpecID()
        local activeID = C_ClassTalents and C_ClassTalents.GetLastSelectedSavedConfigID
          and C_ClassTalents.GetLastSelectedSavedConfigID(specID)
        local isActive = (configID == activeID)

        local x = MenuTemplates.AttachUtilityButton(menuButton, STOP_TEXTURE, 16, 16)

        if isActive then
          -- Lock disabled → MenuTemplates.SetHierarchyEnabled desaturates
          -- the texture automatically (see MenuTemplates_base.lua line ~209).
          MenuTemplates.SetUtilityButtonLockedEnabledState(x, false)
          MenuTemplates.SetUtilityButtonTooltipText(x, TIP_ACTIVE)
        else
          MenuTemplates.SetUtilityButtonTooltipText(x, TIP_DELETE)
          MenuTemplates.SetUtilityButtonClickHandler(x, function()
            if not IsShiftKeyDown() then return end
            C_ClassTalents.DeleteConfig(configID)
            if menu and menu.Close then menu:Close() end
          end)
        end
      end)
    end
  end
end

function Hooks.Install()
  if installed then return end
  if not (PlayerSpellsFrame and PlayerSpellsFrame.TalentsFrame) then return end

  installed = true

  local button = createManageButton(PlayerSpellsFrame.TalentsFrame)
  TalentLoadoutDeleter._manageButton = button

  hooksecurefunc(PlayerSpellsFrame.TalentsFrame, "Show", function()
    anchorManageButton(button)
  end)
  anchorManageButton(button)

  -- Inline [X] via the Menu.ModifyMenu compositor-aware path. Blizzard
  -- tags the loadout dropdown's menu at Blizzard_ClassTalentsFrame.lua
  -- line ~475: self.LoadSystem:SetMenuTag("MENU_CLASS_TALENT_PROFILE").
  -- The callback fires immediately if Blizzard already generated the
  -- menu, AND on every future regeneration (TRAIT_CONFIG_LIST_UPDATED,
  -- spec change, new loadout created, etc).
  Menu.ModifyMenu("MENU_CLASS_TALENT_PROFILE", decorateLoadoutMenu)
end

TalentLoadoutDeleter.Hooks = Hooks


-- TalentLoadoutDeleter / Window.lua
-- Bulk-manager window.
local Window = {}
local BULK_DELETE_DELAY = 0.5
TalentLoadoutDeleter.state = TalentLoadoutDeleter.state or {}
TalentLoadoutDeleter.state.isBulkDeleting = false
TalentLoadoutDeleter.state.bulkDeleteQueue = nil
TalentLoadoutDeleter.state.bulkDeletePending = nil
local frame
local updateFooter  -- forward-declared; defined below so createFrame closures
                    -- (selectAll OnClick) capture the local upvalue, not _G.

local function createFrame()
  local f = CreateFrame("Frame", "TalentLoadoutDeleterWindow", UIParent, "BasicFrameTemplateWithInset")
  f:SetSize(380, 480)
  f:SetPoint("CENTER", UIParent, "CENTER", 0, 21)
  -- PlayerSpellsFrame is on HIGH strata; we want to float above its
  -- fullscreen-ish background, so put us on DIALOG.
  f:SetFrameStrata("DIALOG")
  f:SetMovable(true)
  f:EnableMouse(true)
  f:RegisterForDrag("LeftButton")
  f:SetScript("OnDragStart", f.StartMoving)
  f:SetScript("OnDragStop", f.StopMovingOrSizing)

  f.TitleText = f.TitleText or _G[f:GetName() .. "TitleText"]
  if f.TitleText then
    f.TitleText:SetText("天赋全删")
  end

  local header = CreateFrame("Frame", nil, f)
  header:SetHeight(20)
  header:SetPoint("TOPLEFT", f, "TOPLEFT", 12, -32)
  header:SetPoint("TOPRIGHT", f, "TOPRIGHT", -32, -32)

  local selectAll = CreateFrame("CheckButton", nil, header, "UICheckButtonTemplate")
  selectAll:SetSize(20, 20)
  selectAll:SetPoint("LEFT", header, "LEFT", 0, 0)
  f.selectAll = selectAll

  local selectAllLabel = header:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
  selectAllLabel:SetPoint("LEFT", selectAll, "RIGHT", 4, 0)
  selectAllLabel:SetText("全选")

  local countLabel = header:CreateFontString(nil, "OVERLAY", "GameFontNormal")
  countLabel:SetPoint("RIGHT", header, "RIGHT", 0, 0)
  countLabel:SetText("未选中")
  f.countLabel = countLabel

  -- Scrollable row container.
  local scroll = CreateFrame("ScrollFrame", f:GetName() .. "Scroll",
    f, "UIPanelScrollFrameTemplate")
  scroll:SetPoint("TOPLEFT", f, "TOPLEFT", 12, -56)
  scroll:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -32, 48)

  local content = CreateFrame("Frame", nil, scroll)
  content:SetSize(340, 1)  -- height adjusted per refresh
  scroll:SetScrollChild(content)
  f.scrollContent = content
  f.scrollRows = {}

  local empty = scroll:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
  empty:SetPoint("CENTER", scroll, "CENTER", 0, 0)
  empty:SetText("已经木有天赋方案可供删除了")
  empty:Hide()
  f.emptyMessage = empty

  local footer = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
  footer:SetSize(340, 28)
  footer:SetPoint("BOTTOM", f, "BOTTOM", 0, 12)
  footer:SetText("删除选中 (0)")
  footer:Disable()
  f.footer = footer

  footer:SetScript("OnClick", function()
    local toDelete = {}
    for _, r in ipairs(f.scrollRows) do
      if r:IsShown() and r.check:GetChecked() and not r.isActive then
        table.insert(toDelete, r.configID)
      end
    end
    if #toDelete == 0 then return end

    local state = TalentLoadoutDeleter.state
    state.isBulkDeleting = true
    state.bulkDeleteQueue = toDelete
    state.bulkDeletePending = nil
    Window.DeleteNext()
  end)

  selectAll:SetScript("OnClick", function(self)
    local checked = self:GetChecked()
    for _, r in ipairs(f.scrollRows) do
      if r:IsShown() and not r.isActive then
        r.check:SetChecked(checked)
      end
    end
    updateFooter(f)
  end)

  f:Hide()
  return f
end

function Window.DeleteNext()
  local state = TalentLoadoutDeleter.state
  if not state.isBulkDeleting or state.bulkDeletePending then return end

  local id = state.bulkDeleteQueue[1]
  if not id then
    state.isBulkDeleting = false
    state.bulkDeleteQueue = nil
    Window.Refresh()
    return
  end

  state.bulkDeletePending = id
  local callOK, deleted = pcall(C_ClassTalents.DeleteConfig, id)
  if not callOK or deleted == false then
    state.bulkDeletePending = nil
    C_Timer.After(BULK_DELETE_DELAY, Window.DeleteNext)
  end
end

local function currentSpecName()
  local specIndex = GetSpecialization()
  if not specIndex then return "" end
  local _, name = GetSpecializationInfo(specIndex)
  return name or ""
end

updateFooter = function(f)
  local selected = 0
  local total = 0
  for _, r in ipairs(f.scrollRows) do
    if r:IsShown() and not r.isActive then
      total = total + 1
      if r.check:GetChecked() then selected = selected + 1 end
    end
  end
  f.footer:SetText(("删除选中 (%d)"):format(selected))
  if selected > 0 then f.footer:Enable() else f.footer:Disable() end
  if f.countLabel then
    f.countLabel:SetText(("%d / %d"):format(selected, total))
  end
end

local ROW_HEIGHT = 24

local function renderRows(content, rows, savedRowFrames)
  -- Reuse existing row frames; create more as needed.
  for i = #savedRowFrames + 1, #rows do
    local r = CreateFrame("Frame", nil, content)
    r:SetHeight(ROW_HEIGHT)
    r:SetPoint("LEFT", content, "LEFT", 0, 0)
    r:SetPoint("RIGHT", content, "RIGHT", 0, 0)

    r.check = CreateFrame("CheckButton", nil, r, "UICheckButtonTemplate")
    r.check:SetSize(20, 20)
    r.check:SetPoint("LEFT", r, "LEFT", 0, 0)
    r.check:SetScript("OnClick", function()
      updateFooter(content:GetParent():GetParent()) -- content -> scroll -> frame
    end)

    r.label = r:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    r.label:SetPoint("LEFT", r.check, "RIGHT", 4, 0)
    r.label:SetPoint("RIGHT", r, "RIGHT", -4, 0)
    r.label:SetJustifyH("LEFT")

    savedRowFrames[i] = r
  end

  for i, row in ipairs(rows) do
    local r = savedRowFrames[i]
    r.configID = row.id
    r.isActive = row.isActive
    if row.isActive then
      r.label:SetText(row.name .. " |cff888888[使用中]|r")
      r.check:SetChecked(false)
      r.check:Disable()
    else
      r.label:SetText(row.name)
      r.check:Enable()
      r.check:SetChecked(false)
    end
    r:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -(i - 1) * ROW_HEIGHT)
    r:Show()
  end

  -- Hide any leftover rows from a previous, longer list.
  for i = #rows + 1, #savedRowFrames do
    savedRowFrames[i]:Hide()
  end

  content:SetHeight(math.max(1, #rows * ROW_HEIGHT))
  updateFooter(content:GetParent():GetParent())
end

function Window.Refresh()
  if not frame then return end
  if frame.TitleText then
    local specName = currentSpecName()
    if specName ~= "" then
      frame.TitleText:SetText("[天赋]方案全删 — " .. specName)
    else
      frame.TitleText:SetText("[天赋]方案全删")
    end
  end

  local specID = PlayerUtil and PlayerUtil.GetCurrentSpecID and PlayerUtil.GetCurrentSpecID()
  local activeID = C_ClassTalents and C_ClassTalents.GetLastSelectedSavedConfigID
    and C_ClassTalents.GetLastSelectedSavedConfigID(specID)
  local rows = (TalentLoadoutDeleter.Data and TalentLoadoutDeleter.Data.GetLoadouts and specID)
    and TalentLoadoutDeleter.Data.GetLoadouts(specID, activeID)
    or {}

  if #rows == 0 then
    if frame.emptyMessage then frame.emptyMessage:Show() end
  else
    if frame.emptyMessage then frame.emptyMessage:Hide() end
  end

  renderRows(frame.scrollContent, rows, frame.scrollRows)
end

function Window.Toggle()
  if not frame then
    frame = createFrame()
  end
  if frame:IsShown() then
    frame:Hide()
  else
    Window.Refresh()
    frame:Show()
  end
end

TalentLoadoutDeleter.Window = Window

TalentLoadoutDeleter.OnLoadoutsChanged = function()
  if frame and frame:IsShown() then
    Window.Refresh()
  end
end

TalentLoadoutDeleter.OnSpecChanged = function()
  if frame and frame:IsShown() then
    frame:Hide()
  end
end


-- TalentLoadoutDeleter / Core.lua
-- Bootstrap + event dispatch.

local TLD = TalentLoadoutDeleter or {}
_G.TalentLoadoutDeleter = TLD  -- top-level handle for debug only; not API

TLD.state = {
  listReady = false,
  isBulkDeleting = false,
}

local dispatcher = CreateFrame("Frame", "TalentLoadoutDeleterDispatcher")
dispatcher:RegisterEvent("ADDON_LOADED")
dispatcher:RegisterEvent("TRAIT_CONFIG_LIST_UPDATED")
dispatcher:RegisterEvent("TRAIT_CONFIG_DELETED")
dispatcher:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED")

local function tryInstallHooks()
  if TLD.Hooks and TLD.Hooks.Install then
    TLD.Hooks.Install()
  end
end

dispatcher:SetScript("OnEvent", function(_, event, arg1)
  if event == "ADDON_LOADED" then
    if arg1 == "OrzUI" then
      -- Self-init: check if PlayerSpells is already up (e.g., reloadui
      -- after talent tab was opened earlier in the session).
      if C_AddOns.IsAddOnLoaded("Blizzard_PlayerSpells") then
        tryInstallHooks()
      end
    elseif arg1 == "Blizzard_PlayerSpells" then
      -- Talent UI loaded — dispatch TLD.Hooks.Install via the helper.
      tryInstallHooks()
    end
  elseif event == "TRAIT_CONFIG_LIST_UPDATED" then
    TLD.state.listReady = true
    if TLD.OnLoadoutsChanged then
      TLD.OnLoadoutsChanged()
    end
  elseif event == "TRAIT_CONFIG_DELETED" then
    if TLD.state.isBulkDeleting and TLD.state.bulkDeletePending == arg1 then
      table.remove(TLD.state.bulkDeleteQueue, 1)
      TLD.state.bulkDeletePending = nil
      if TLD.Window and TLD.Window.DeleteNext then
        C_Timer.After(BULK_DELETE_DELAY, TLD.Window.DeleteNext)
      end
    elseif not TLD.state.isBulkDeleting and TLD.OnLoadoutsChanged then
      TLD.OnLoadoutsChanged()
    end
  elseif event == "PLAYER_SPECIALIZATION_CHANGED" then
    if TLD.OnSpecChanged then
      TLD.OnSpecChanged()
    end
  end
end)

TLD.dispatcher = dispatcher
