if not select(2, ...).IS_LEGACY_CLIENT then return end -- HET-LEGACY-1207: 12.0.7 only
-------------------------------------------------------------------------------
-- Tracker.lua
-- Generic tracker factory. Each tracker (Spatial Paradox, Innervate, etc.)
-- calls ns.Tracker.Create(config, addon) to get an independent tracker object
-- with its own frames, timer state, and preview logic.
-- Polling is centralized in Core.lua — trackers do not own their own poll.
-------------------------------------------------------------------------------
local addonName, ns = ...

ns.Tracker = {}

-- Registry (populated by SpatialParadox.lua, Innervate.lua at load time)
ns.trackerConfigs = {}
ns.trackerOrder   = {}
ns.trackers       = {}

function ns.RegisterTracker(key, config)
    ns.trackerConfigs[key] = config
    ns.trackerOrder[#ns.trackerOrder + 1] = key
end

local RING_TEXTURES = {
    ["Ring_10px"] = "Interface\\AddOns\\BuzzardAuras\\Media\\Ring_10px",
    ["Ring_20px"] = "Interface\\AddOns\\BuzzardAuras\\Media\\Ring_20px",
    ["Ring_30px"] = "Interface\\AddOns\\BuzzardAuras\\Media\\Ring_30px",
    ["Ring_40px"] = "Interface\\AddOns\\BuzzardAuras\\Media\\Ring_40px",
}
ns.RING_TEXTURES = RING_TEXTURES

local GetSpellTexture = C_Spell.GetSpellTexture

-------------------------------------------------------------------------------
-- Frame creation helpers
-------------------------------------------------------------------------------
local function CreateIconFrame(suffix, label, defaultPos, tracker)
    local f = CreateFrame("Frame", "HET" .. suffix .. "IconFrame", UIParent, "BackdropTemplate")
    f:SetSize(40, 40)
    f:SetPoint(defaultPos[1], UIParent, defaultPos[2], defaultPos[3], defaultPos[4])
    f:SetMovable(true)
    f:EnableMouse(true)
    f:RegisterForDrag("LeftButton")
    f:SetClampedToScreen(true)
    f:Hide()

    f:SetScript("OnDragStart", function(self) self:StartMoving() end)
    f:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        local point, _, relPoint, x, y = self:GetPoint()
        tracker:SetDBVal("position", { point, relPoint, x, y })
    end)

    local icon = f:CreateTexture(nil, "ARTWORK")
    icon:SetAllPoints()
    f.icon = icon

    local cooldown = CreateFrame("Cooldown", nil, f, "CooldownFrameTemplate")
    cooldown:SetAllPoints()
    cooldown:SetReverse(true)
    cooldown:SetDrawEdge(false)
    f.cooldown = cooldown

    local highlight = f:CreateTexture(nil, "OVERLAY")
    highlight:SetAllPoints()
    highlight:SetColorTexture(0.1, 0.4, 1.0, 0.35)
    highlight:Hide()
    f.highlight = highlight

    local lbl = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    lbl:SetPoint("TOP", f, "BOTTOM", 0, -4)
    lbl:SetText(label)
    lbl:SetTextColor(0.5, 0.8, 1.0, 1.0)
    lbl:Hide()
    f.label = lbl

    return f
end

local function CreateBarFrame(suffix, label, defaultPos, tracker)
    local f = CreateFrame("Frame", "HET" .. suffix .. "BarFrame", UIParent, "BackdropTemplate")
    f:SetSize(200, 20)
    f:SetPoint(defaultPos[1], UIParent, defaultPos[2], defaultPos[3], defaultPos[4])
    f:SetMovable(true)
    f:EnableMouse(true)
    f:RegisterForDrag("LeftButton")
    f:SetClampedToScreen(true)
    f:Hide()

    f:SetScript("OnDragStart", function(self) self:StartMoving() end)
    f:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        local point, _, relPoint, x, y = self:GetPoint()
        tracker:SetDBVal("position", { point, relPoint, x, y })
    end)

    local bg = f:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints()
    bg:SetColorTexture(0, 0, 0, 0.5)
    f.bg = bg

    local bar = f:CreateTexture(nil, "ARTWORK")
    f.bar = bar

    local highlight = f:CreateTexture(nil, "OVERLAY")
    highlight:SetAllPoints()
    highlight:SetColorTexture(0.1, 0.4, 1.0, 0.35)
    highlight:Hide()
    f.highlight = highlight

    local lbl = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    lbl:SetPoint("TOP", f, "BOTTOM", 0, -4)
    lbl:SetText(label)
    lbl:SetTextColor(0.5, 0.8, 1.0, 1.0)
    lbl:Hide()
    f.label = lbl

    return f
end

local function CreateCircleFrame(suffix, label, defaultPos, tracker)
    local f = CreateFrame("Frame", "HET" .. suffix .. "CircleFrame", UIParent)
    f:SetPoint(defaultPos[1], UIParent, defaultPos[2], defaultPos[3], defaultPos[4])
    f:SetMovable(true)
    f:EnableMouse(true)
    f:RegisterForDrag("LeftButton")
    f:SetClampedToScreen(true)
    f:Hide()

    f:SetScript("OnDragStart", function(self) self:StartMoving() end)
    f:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        local point, _, relPoint, x, y = self:GetPoint()
        tracker:SetDBVal("position", { point, relPoint, x, y })
    end)

    f.bg = ns.CircleProgress.Create(f, "BACKGROUND")
    f.ring = ns.CircleProgress.Create(f)

    local highlight = f:CreateTexture(nil, "OVERLAY")
    highlight:SetAllPoints()
    highlight:SetColorTexture(0.1, 0.4, 1.0, 0.15)
    highlight:Hide()
    f.highlight = highlight

    local lbl = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    lbl:SetPoint("CENTER", f, "CENTER", 0, 0)
    lbl:SetText(label)
    lbl:SetTextColor(0.5, 0.8, 1.0, 1.0)
    lbl:Hide()
    f.label = lbl

    return f
end

-------------------------------------------------------------------------------
-- Tracker methods (copied onto each instance)
-------------------------------------------------------------------------------
local methods = {}
-- Single shared metatable: instances dispatch through __index instead of
-- copying every method onto themselves. Same call speed under LuaJIT,
-- smaller per-instance footprint.
local trackerMT = { __index = methods }

function methods:GetDBVal(key)
    return ns.addon.db.profile[self.config.dbPrefix .. key]
end

function methods:SetDBVal(key, val)
    ns.addon.db.profile[self.config.dbPrefix .. key] = val
end

function methods:HideAllFrames()
    if self.iconFrame then
        -- Bump generation so any pending C_Timer.After hide callback is a no-op.
        self._iconGen = (self._iconGen or 0) + 1
        self.iconFrame.cooldown:Clear()
        self.iconFrame:Hide()
    end
    if self.barFrame then
        self.barFrame:SetScript("OnUpdate", nil)
        self.barFrame:Hide()
    end
    if self.circleFrame then
        self.circleFrame:SetScript("OnUpdate", nil)
        self.circleFrame.bg:Hide()
        self.circleFrame.ring:Hide()
        self.circleFrame:Hide()
    end
end

function methods:SetupBar(frame)
    local orientation = self:GetDBVal("barOrientation") or "horizontal"
    local c = self:GetDBVal("barCircleColor")
    local bw = self:GetDBVal("barWidth")
    local bh = self:GetDBVal("barHeight")
    -- Cache for _onBarUpdate so it doesn't have to do GetDBVal (string concat)
    -- on every render frame. These values can only change while the bar isn't
    -- running (config dialog), at which point SetupBar runs again.
    self.barOrientation = orientation
    self.barWidth = bw
    self.barHeight = bh
    frame:SetSize(bw, bh)
    frame.bar:ClearAllPoints()
    frame.bar:SetColorTexture(c[1], c[2], c[3], c[4])
    if orientation == "vertical" then
        frame.bar:SetPoint("TOPLEFT")
        frame.bar:SetPoint("TOPRIGHT")
        frame.bar:SetHeight(bh)
    else
        frame.bar:SetPoint("TOPLEFT")
        frame.bar:SetPoint("BOTTOMLEFT")
        frame.bar:SetWidth(bw)
    end
end

function methods:SetupCircle(frame)
    local c = self:GetDBVal("barCircleColor")
    local texKey = self:GetDBVal("ringTexture")
    local texPath = RING_TEXTURES[texKey] or RING_TEXTURES["Ring_20px"]
    local size = self:GetDBVal("circleRadius") * 2
    frame.bg:SetRingSize(size)
    frame.bg:SetTexturePath(texPath)
    frame.bg:SetColor(0, 0, 0, 0.5)
    frame.bg:SetProgress(1)
    frame.bg:Show()
    frame.ring:SetRingSize(size)
    frame.ring:SetTexturePath(texPath)
    frame.ring:SetColor(c[1], c[2], c[3], c[4])
end

function methods:PlayAlert()
    local mode = self:GetDBVal("alertMode")
    if mode == "sound" then
        local soundName = self:GetDBVal("soundName")
        if soundName and soundName ~= "None" then
            local LSM = LibStub("LibSharedMedia-3.0", true)
            if LSM then
                local path = LSM:Fetch("sound", soundName)
                if path then
                    local channel = self:GetDBVal("soundChannel") or "Master"
                    PlaySoundFile(path, channel)
                end
            end
        end
    elseif mode == "tts" and self.config.ttsPhrase then
        local voiceID = self:GetDBVal("ttsVoice") or 0
        local volume = self:GetDBVal("ttsVolume") or 50
        local rate = C_TTSSettings and C_TTSSettings.GetSpeechRate() or 0
        C_VoiceChat.SpeakText(voiceID, self.config.ttsPhrase, rate, volume, true)
    end
end

function methods:TriggerTimer()
    local duration = self.config.getTimerDuration(self)
    self.timerDuration = duration
    self.timerRemaining = duration
    self.timerActive = true

    self:PlayAlert()
    self:HideAllFrames()

    local style = self:GetDBVal("trackerStyle")

    if style == "icon" then
        local frame = self.iconFrame
        if self.cachedIcon then
            frame.icon:SetTexture(self.cachedIcon)
        end
        frame.highlight:Hide()
        frame.label:Hide()
        local sz = self:GetDBVal("iconSize")
        frame:SetSize(sz, sz)
        frame:Show()

        frame.cooldown:SetDrawSwipe(self:GetDBVal("cooldownSwipe"))
        frame.cooldown:SetCooldown(GetTime(), duration)
        -- Bump generation and schedule the hide. No per-frame OnUpdate.
        self._iconGen = self._iconGen + 1
        self._scheduleIconHide(duration, self._iconGen)

    elseif style == "bar" then
        local frame = self.barFrame
        self:SetupBar(frame)
        frame.highlight:Hide()
        frame.label:Hide()
        frame:Show()
        frame:SetScript("OnUpdate", self._onBarUpdate)

    elseif style == "circle" then
        local frame = self.circleFrame
        self:SetupCircle(frame)
        frame.ring:SetProgress(1)
        frame.highlight:Hide()
        frame.label:Hide()
        frame:Show()
        frame:SetScript("OnUpdate", self._onCircleUpdate)
    end
end

function methods:ShowPreview()
    self.previewActive = true
    self:HideAllFrames()

    local style = self:GetDBVal("trackerStyle")

    if style == "icon" then
        local frame = self.iconFrame
        local sz = self:GetDBVal("iconSize")
        frame:SetSize(sz, sz)

        local tex = self.cachedIcon or GetSpellTexture(self.config.displaySpell)
        if tex then
            frame.icon:SetTexture(tex)
            if not self.cachedIcon then
                self.cachedIcon = tex
            end
        end
        frame.cooldown:Clear()
        frame.highlight:Show()
        frame:Show()

    elseif style == "bar" then
        local frame = self.barFrame
        self:SetupBar(frame)
        frame.highlight:Show()
        frame:Show()

    elseif style == "circle" then
        local frame = self.circleFrame
        self:SetupCircle(frame)
        frame.ring:SetProgress(1)
        frame.highlight:Show()
        frame:Show()
    end
end

function methods:HidePreview()
    -- Only tear down the preview if a preview was actually shown. Without
    -- this guard, closing the options panel would hide a real running
    -- timer (which calls HideAllFrames -> bumps _iconGen / clears OnUpdate).
    if not self.previewActive then return end
    self.previewActive = false
    self:HideAllFrames()
end

function methods:IsPreviewActive()
    return self.previewActive
end

function methods:RefreshPreview()
    if self.previewActive then
        self:ShowPreview()
    end
end

function methods:Stop()
    self.timerActive = false
    self:HideAllFrames()
    if self.config.onStop then
        self.config.onStop(self)
    end
end

function methods:RestorePosition(frame)
    if not frame then return end
    local pos = self:GetDBVal("position")
    if pos then
        frame:ClearAllPoints()
        frame:SetPoint(pos[1], UIParent, pos[2], pos[3], pos[4])
    end
end

function methods:CacheIcon()
    self.cachedIcon = GetSpellTexture(self.config.displaySpell)
end

-------------------------------------------------------------------------------
-- Factory
-------------------------------------------------------------------------------
function ns.Tracker.Create(config, addon)
    local tracker = setmetatable({
        config = config,
        -- State
        timerRemaining = 0,
        timerDuration = 0,
        timerActive = false,
        previewActive = false,
        cachedIcon = nil,
        -- Frames (set below)
        iconFrame = nil,
        barFrame = nil,
        circleFrame = nil,
        -- Hoisted from config so PollAll only does one table indirection
        -- per call instead of two (tracker.config -> .poll / .isActive).
        poll = config.poll,
        isActive = config.isActive,
        -- _isLive is (enabled AND applicable). Recomputed by Core whenever
        -- enabled state, spec, or applicability could change; PollAll reads
        -- it directly so there's no per-tick GetDBVal string concat.
        _isLive = false,
        -- Generation counter for icon-style timers. Each TriggerTimer/Stop
        -- bumps it; the C_Timer.After callback captures the value when
        -- scheduled and only acts if it still matches — so a late callback
        -- from a stale timer can't hide a freshly triggered icon.
        _iconGen = 0,
    }, trackerMT)

    -- Icon style uses a C_Timer.After (no per-frame OnUpdate). The Blizzard
    -- Cooldown widget handles the swipe animation by itself; all we need is
    -- a single callback at the end to hide the frame. _iconGen guards against
    -- a stale callback firing after Stop() / a new trigger.
    tracker._scheduleIconHide = function(duration, gen)
        C_Timer.After(duration, function()
            if tracker._iconGen ~= gen then return end
            tracker.timerActive = false
            local frame = tracker.iconFrame
            if frame then
                frame:Hide()
                frame.cooldown:Clear()
            end
        end)
    end

    tracker._onBarUpdate = function(frame, elapsed)
        if not tracker.timerActive then return end
        tracker.timerRemaining = tracker.timerRemaining - elapsed
        if tracker.timerRemaining <= 0 then
            tracker.timerActive = false
            frame:SetScript("OnUpdate", nil)
            frame:Hide()
            return
        end
        local progress = tracker.timerRemaining / tracker.timerDuration
        -- Values cached by SetupBar; no per-frame string concat.
        if tracker.barOrientation == "vertical" then
            local fillHeight = tracker.barHeight * progress
            if fillHeight < 0.1 then fillHeight = 0.1 end
            frame.bar:SetHeight(fillHeight)
        else
            local fillWidth = tracker.barWidth * progress
            if fillWidth < 0.1 then fillWidth = 0.1 end
            frame.bar:SetWidth(fillWidth)
        end
    end

    tracker._onCircleUpdate = function(frame, elapsed)
        if not tracker.timerActive then return end
        tracker.timerRemaining = tracker.timerRemaining - elapsed
        if tracker.timerRemaining <= 0 then
            tracker.timerActive = false
            frame:SetScript("OnUpdate", nil)
            frame:Hide()
            return
        end
        local progress = tracker.timerRemaining / tracker.timerDuration
        frame.ring:SetProgress(progress)
    end

    -- Create frames
    local defaultPos = config.defaultPos or { "CENTER", "CENTER", 0, 200 }
    tracker.iconFrame = CreateIconFrame(config.frameSuffix, config.label, defaultPos, tracker)
    tracker.barFrame = CreateBarFrame(config.frameSuffix, config.label, defaultPos, tracker)
    tracker.circleFrame = CreateCircleFrame(config.frameSuffix, config.label, defaultPos, tracker)

    -- Restore saved positions
    tracker:RestorePosition(tracker.iconFrame)
    tracker:RestorePosition(tracker.barFrame)
    tracker:RestorePosition(tracker.circleFrame)

    -- Cache icon
    tracker:CacheIcon()

    return tracker
end
