-- WarBoard_ObjectiveTracker.lua
-- A "War Board" section inside Blizzard's real Objective Tracker, holding the WarBoard
-- objectives the player is currently near. Ported from the working SemiDevTools prototype
-- (SDT.BlizzTracker), generalized to add/remove MANY objectives dynamically.
--
-- Public API:
--   WB.Tracker:AddObjective(id, name, label, cur, total)  -- add/update an entry, show section
--   WB.Tracker:RemoveObjective(id)                        -- remove an entry (section hides when empty)
--   WB.Tracker:SetProgress(id, cur, total[, label])       -- update progress numbers
--   WB.Tracker:OnProgress(id)                             -- pull live progress from WarEffort + refresh
--   WB.Tracker:Clear()                                    -- remove all entries
--   WB.Tracker:HasObjective(id)
--
-- ⚠ EXPERIMENTAL INTEGRATION ⚠  (see RISKS at the bottom)
-- This registers a custom module into Blizzard's ObjectiveTrackerManager. That is NOT a
-- stable public API. It is guarded: registration only happens out of combat, every Blizzard
-- call is pcall'd, and if our layout hook ever errors it auto-removes itself so it can never
-- break real quest tracking. It never hides or modifies Blizzard's own modules.

local addonName, WB = ...
WB.Tracker = WB.Tracker or {}
local T = WB.Tracker

local MODULE_FRAME_NAME = "WarBoardObjectiveTrackerModule"
-- uiOrder places our section in the container's sort (Blizzard's are 1..11). A low value sorts
-- it near the top (below Scenario) AND guarantees it gets layout room. Bump to 100 for bottom.
local UI_ORDER = 1.5

T.order = T.order or {}   -- ordered active entries: { id, name, label, cur, total }
T.byId  = T.byId  or {}
T.registered = T.registered or false

-- Right-click context menu for a tracker block: the objective name as a title + an Untrack action
-- that clears it (a manual backstop for the rare case a zone/loading-screen leave doesn't auto-clear).
local function ShowUntrackMenu(owner, id, name)
    if not id then return end
    if MenuUtil and MenuUtil.CreateContextMenu then
        MenuUtil.CreateContextMenu(owner, function(_, root)
            root:CreateTitle(name or "Objective")
            root:CreateButton("Untrack", function() T:RemoveObjective(id) end)
        end)
    elseif EasyMenu then   -- legacy clients
        T._legacyMenu = T._legacyMenu or CreateFrame("Frame", "WarBoardTrackerContextMenu", UIParent, "UIDropDownMenuTemplate")
        EasyMenu({
            { text = name or "Objective", isTitle = true, notCheckable = true },
            { text = "Untrack", notCheckable = true, func = function() T:RemoveObjective(id) end },
        }, T._legacyMenu, "cursor", 0, 0, "MENU")
    end
end

-- ── Build the module (Blizzard template → real Header + ContentsFrame + mixin) ──
function T:BuildModule()
    if self.module then return self.module end

    local module
    local ok = pcall(function()
        module = CreateFrame("Frame", MODULE_FRAME_NAME, UIParent, "ObjectiveTrackerModuleTemplate")
    end)
    if not ok or not module then return nil end

    module.headerText = "War Board"
    if module.SetHeader then pcall(module.SetHeader, module, "War Board") end
    module.uiOrder = UI_ORDER   -- REQUIRED: container sorts by uiOrder; nil → "compare nil with number"

    -- Blizzard calls this during its own layout pass; wrapped in pcall so a failure can never
    -- break the real tracker. Emits one block per active objective: a header (name) + a dashed
    -- progress line ("cur/total label").
    function module:LayoutContents()
        local good, err = pcall(function()
            for _, o in ipairs(T.order) do
                local block = self:GetBlock(o.id)   -- block key = objective id (string is fine)
                -- Right-click a block → context menu (name + Untrack). Keep the current id/name on
                -- the block so the menu is accurate, and hook OnMouseUp exactly once per block.
                block._wbId, block._wbName = o.id, o.name
                block:EnableMouse(true)
                if not block._wbMenuHooked then
                    block._wbMenuHooked = true
                    block:HookScript("OnMouseUp", function(bself, button)
                        if button == "RightButton" then ShowUntrackMenu(bself, bself._wbId, bself._wbName) end
                    end)
                end
                if block.SetHeader then block:SetHeader(o.name or "?") end
                local text
                if o.label and o.total and o.total > 0 then
                    text = string.format("%d/%d %s", o.cur or 0, o.total, o.label)
                else
                    text = o.label or ""
                end
                local line = block:AddObjective("progress", text, nil, nil, OBJECTIVE_DASH_STYLE_SHOW)
                if line and line.Icon then line.Icon:Hide() end
                if not self:LayoutBlock(block) then return end   -- out of room — stop
            end
        end)
        if not good then T:_ScheduleDisable(err) end
    end

    self.module = module
    return module
end

-- ── Registration (lazy, out of combat) ───────────────────────────────────────
function T:EnsureRegistered()
    if self.registered then return true end
    if not (ObjectiveTrackerManager and ObjectiveTrackerFrame) then return false end   -- tracker not init'd yet

    -- Never register mid-combat (taint). Defer to the next out-of-combat moment.
    if InCombatLockdown() then
        if not self._combatWait then
            self._combatWait = CreateFrame("Frame")
            self._combatWait:RegisterEvent("PLAYER_REGEN_ENABLED")
            self._combatWait:SetScript("OnEvent", function(fr)
                fr:UnregisterEvent("PLAYER_REGEN_ENABLED")
                T._combatWait = nil
                if T:EnsureRegistered() then T:Refresh() end
            end)
        end
        return false
    end

    local module = self:BuildModule()
    if not module then return false end

    local OTM, OTF = ObjectiveTrackerManager, ObjectiveTrackerFrame
    -- Preferred path — same one Blizzard uses (manager Init): records us in moduleToContainerMap.
    if OTM.SetModuleContainer then
        local ok = pcall(OTM.SetModuleContainer, OTM, module, OTF)
        if ok and OTM.GetContainerForModule and OTM:GetContainerForModule(module) == OTF then
            self.registered = true
            return true
        end
    end
    -- Fallback: add straight to the container.
    if OTF.AddModule and pcall(OTF.AddModule, OTF, module) then
        self.registered = true
        return true
    end
    return false
end

-- Relayouts are COALESCED. ObjectiveTrackerManager:UpdateAll() relayouts EVERY tracker module —
-- all of Blizzard's quests/achievements too, not just ours — so calling it once per mutation turned
-- a burst of tracker changes (rapid/AoE kills, or units & nameplates streaming in at an objective,
-- which is exactly when players report the stutter) into a full global relayout apiece. We collapse
-- a burst into ONE relayout, fired a fraction of a second later. MarkDirty runs immediately in
-- Refresh, so the single deferred relayout still shows the latest state; the delay is imperceptible
-- for climbing "x/y" ticks and completions still land effectively instantly.
local RELAYOUT_DEBOUNCE = 0.1
local forcePending = false
local function DoForceUpdate()
    local OTM = ObjectiveTrackerManager
    if OTM and OTM.UpdateAll then
        pcall(OTM.UpdateAll, OTM)
    elseif ObjectiveTrackerFrame and ObjectiveTrackerFrame.Update then
        pcall(ObjectiveTrackerFrame.Update, ObjectiveTrackerFrame)
    end
end
function T:ForceUpdate()
    if forcePending then return end                 -- a relayout is already queued; this change rides it
    if not C_Timer then return DoForceUpdate() end  -- no timer available → fall back to immediate
    forcePending = true
    C_Timer.After(RELAYOUT_DEBOUNCE, function()
        forcePending = false
        DoForceUpdate()
    end)
end

function T:Refresh()
    if not (self.registered and self.module) then return end
    if self.module.MarkDirty then pcall(self.module.MarkDirty, self.module) end
    self:ForceUpdate()
end

-- ── Public API ───────────────────────────────────────────────────────────────
function T:AddObjective(id, name, label, cur, total)
    if not id then return end
    local o = self.byId[id]
    if not o then
        o = { id = id }
        self.byId[id] = o
        table.insert(self.order, o)
    end
    o.name  = name  or o.name
    o.label = label or o.label
    o.cur   = cur   or o.cur   or 0
    o.total = total or o.total or 0
    self:EnsureRegistered()
    self:Refresh()
end

function T:RemoveObjective(id)
    if not self.byId[id] then return end
    self.byId[id] = nil
    for i = #self.order, 1, -1 do
        if self.order[i].id == id then table.remove(self.order, i); break end
    end
    self:Refresh()   -- empty module hides itself via Blizzard's EndLayout (state = NoObjectives)
end

function T:SetProgress(id, cur, total, label)
    local o = self.byId[id]
    if not o then return end
    if cur   ~= nil then o.cur   = cur   end
    if total ~= nil then o.total = total end
    if label then o.label = label end
    self:Refresh()
end

-- Pull the live progress for an objective from WarEffort and refresh (called on every kill).
function T:OnProgress(id)
    local o = self.byId[id]
    if not o then return end
    if WB.WarEffort and WB.WarEffort.GetProgress then
        o.cur = WB.WarEffort.GetProgress(id) or o.cur
    end
    -- Reflect a completion: the name picks up " (Repeat)" once the objective is done.
    if WB.WarEffort and WB.WarEffort.DisplayTitleById then
        o.name = WB.WarEffort.DisplayTitleById(id) or o.name
    end
    self:Refresh()
end

function T:HasObjective(id) return self.byId[id] ~= nil end

function T:Clear()
    wipe(self.byId)
    wipe(self.order)
    self:Refresh()
end

-- ── Safety: self-remove (deferred out of the layout pass) if our hook ever errors ──
function T:_ScheduleDisable(reason)
    if self._disablePending then return end
    self._disablePending = true
    C_Timer.After(0, function()
        self._disablePending = false
        T:Disable()
        if WB.TAG then
            print(WB.TAG .. " objective tracker disabled after a layout error (protecting quest tracking): "
                .. tostring(reason))
        end
    end)
end

-- Remove our module from Blizzard's tracker entirely (used on error / teardown).
function T:Disable()
    if self.module then
        pcall(function()
            local OTM = ObjectiveTrackerManager
            local container = (OTM and OTM.GetContainerForModule and OTM:GetContainerForModule(self.module))
                              or ObjectiveTrackerFrame
            if container and container.RemoveModule then container:RemoveModule(self.module) end
            if OTM and OTM.moduleToContainerMap then OTM.moduleToContainerMap[self.module] = nil end
        end)
        pcall(function() self.module:Hide() end)
    end
    self.registered = false
    self:ForceUpdate()
end

-- ════════════════════════════════════════════════════════════════════════════
-- RISKS (Blizzard Objective Tracker integration):
--   • TAINT: once registered, our insecure LayoutContents runs inside Blizzard's tracker
--     Update, which ends in UIParent_ManageFramePositions() (a protected reposition). In
--     combat this can surface as "action blocked" on managed frames. We register only out of
--     combat and lazily (on first nearby objective), but exposure exists while registered.
--   • PATCH FRAGILITY: ObjectiveTrackerManager/container internals, template names, and uiOrder
--     are not public API and have changed across patches — may need fixing on major updates.
--   • If taint ever becomes a problem for live WPvP, swap this module for the self-contained
--     custom-frame tracker (also prototyped in SemiDevTools as SDT.Tracker) — same public API,
--     zero taint, just not docked inside Blizzard's tracker.
-- ════════════════════════════════════════════════════════════════════════════
