-- ERT/UI/CountdownBar.lua
-- A compact respawn-timer list for the rares you've starred (wanted). When a
-- wanted, tracked rare has a respawn window — counting down to it, in it, or up
-- right now — a slim labelled bar appears: the rare's icon, its NAME, and a live
-- stopwatch (H:MM:SS) ticking down the full time until it's back, with a fill
-- that tracks respawn progress. Click a bar to set a waypoint; drag any bar to
-- move the list. Auto-hides when nothing is on respawn; pop it out (pin) to keep
-- it on screen. Toggle with /etbrare timer, pin with /etbrare popout.

local ADDON_NAME, NS = ...
local ETB = NS.ETB
local ERT = ETB.ERT

local CB = {}
ERT.Countdown = CB

local MAX_BARS = 6
local BAR_W = 208
local BAR_H = 22
local ROW_GAP = 2
local ICON_SZ = 16
local MAX_COUNTDOWN = 6 * 3600   -- don't show predicted windows beyond 6h out

-- Stopwatch clock: H:MM:SS for long respawns, M:SS for short — ticks every second.
local function fmtClock(s)
    s = math.max(0, math.floor(s or 0))
    local h = math.floor(s / 3600); s = s - h * 3600
    local m = math.floor(s / 60); local sec = s - m * 60
    if h > 0 then return string.format("%d:%02d:%02d", h, m, sec) end
    return string.format("%d:%02d", m, sec)
end

local function cdDB()
    if type(ETB_RareDB) ~= "table" then ETB_RareDB = {} end
    if type(ETB_RareDB.countdown) ~= "table" then ETB_RareDB.countdown = { enabled = true } end
    return ETB_RareDB.countdown
end

-- Alert sound played once when a wanted rare's countdown hits zero (its window
-- opens / it comes up). Played on the Master channel so it's heard even with SFX
-- volume low. Sound is on by default (nil treated as enabled).
local SOUND_FILE = "Interface\\AddOns\\EnhancedTransmogBrowser\\ERT\\Sounds\\EH_LichKing_Chapter1Whisper13.ogg"
local function PlayUpSound()
    if cdDB().sound == false then return end
    if PlaySoundFile then pcall(PlaySoundFile, SOUND_FILE, "Master") end
end

-- Representative icon: the first collectible the rare drops, else a generic glyph.
local iconCache = {}
local function iconFor(npcID)
    if iconCache[npcID] then return iconCache[npcID] end
    local mt, pt, ty, it = ERT:GetRareDrops(npcID)
    local first = (mt and mt[1]) or (pt and pt[1]) or (ty and ty[1]) or (it and it[1])
    local icon
    if first and GetItemInfoInstant then
        local _, _, _, _, ic = GetItemInfoInstant(first)
        icon = ic
    end
    icon = icon or "Interface\\Icons\\Ability_Hunter_MarkedForDeath"
    iconCache[npcID] = icon
    return icon
end
CB.InvalidateIcons = function() iconCache = {} end

local function SetWaypoint(npcID)
    local mapID, x, y = ERT:GetLocation(npcID)
    if not (mapID and x and y) then return end
    if C_Map and C_Map.SetUserWaypoint and UiMapPoint and UiMapPoint.CreateFromCoordinates then
        pcall(function()
            C_Map.SetUserWaypoint(UiMapPoint.CreateFromCoordinates(mapID, x, y))
            if C_SuperTrack and C_SuperTrack.SetSuperTrackedUserWaypoint then
                C_SuperTrack.SetSuperTrackedUserWaypoint(true)
            end
        end)
    elseif TomTom and TomTom.AddWaypoint then
        pcall(TomTom.AddWaypoint, TomTom, mapID, x, y, { title = ERT:GetName(npcID) })
    end
end

-- One labelled timer bar: [icon] Name .......... MM:SS  with a progress fill.
local function acquireBar(self, i)
    self.bars = self.bars or {}
    if self.bars[i] then return self.bars[i] end

    local b = CreateFrame("Button", nil, self.bar)
    b:SetSize(BAR_W, BAR_H)
    b:SetPoint("TOPLEFT", 0, -((i - 1) * (BAR_H + ROW_GAP)))

    b.bg = b:CreateTexture(nil, "BACKGROUND")
    b.bg:SetAllPoints()
    b.bg:SetColorTexture(0, 0, 0, 0.72)

    -- Respawn-progress fill: grows from empty (just killed) to full (window opens).
    b.fill = b:CreateTexture(nil, "ARTWORK")
    b.fill:SetPoint("TOPLEFT", 0, 0)
    b.fill:SetPoint("BOTTOMLEFT", 0, 0)
    b.fill:SetWidth(0)
    b.fill:SetColorTexture(0.2, 1, 0.2, 0.30)

    b.icon = b:CreateTexture(nil, "OVERLAY")
    b.icon:SetSize(ICON_SZ, ICON_SZ)
    b.icon:SetPoint("LEFT", 3, 0)
    b.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    b.cd = b:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    b.cd:SetPoint("RIGHT", -4, 0)
    b.cd:SetJustifyH("RIGHT")
    b.cd:SetText("")

    b.name = b:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    b.name:SetPoint("LEFT", b.icon, "RIGHT", 4, 0)
    b.name:SetPoint("RIGHT", b.cd, "LEFT", -4, 0)
    b.name:SetJustifyH("LEFT")
    b.name:SetWordWrap(false)

    b.border = b:CreateTexture(nil, "BORDER")
    b.border:SetPoint("TOPLEFT", -1, 1); b.border:SetPoint("BOTTOMRIGHT", 1, -1)
    b.border:SetColorTexture(0, 0, 0, 0)

    b:SetScript("OnClick", function(bb) if bb.npc then SetWaypoint(bb.npc) end end)
    -- The bars cover the whole list, so dragging one moves the list itself.
    b:RegisterForDrag("LeftButton")
    b:SetScript("OnDragStart", function() if self.bar then self.bar:StartMoving() end end)
    b:SetScript("OnDragStop", function()
        if not self.bar then return end
        self.bar:StopMovingOrSizing()
        local p, _, _, x, y = self.bar:GetPoint()
        local db = cdDB(); db.point, db.x, db.y = p, x, y
    end)
    b:SetScript("OnEnter", function(bb)
        if not bb.npc or not GameTooltip then return end
        GameTooltip:SetOwner(bb, "ANCHOR_RIGHT")
        GameTooltip:AddLine(ERT:GetName(bb.npc))
        if bb.statusLine then GameTooltip:AddLine(bb.statusLine, 1, 1, 1) end
        local zone = ERT:GetZoneName(bb.npc)
        if zone then GameTooltip:AddLine(zone, 0.7, 0.7, 0.7) end
        GameTooltip:AddLine("Click to set a waypoint · drag to move", 0.5, 0.8, 1)
        GameTooltip:Show()
    end)
    b:SetScript("OnLeave", function() if GameTooltip then GameTooltip:Hide() end end)

    self.bars[i] = b
    return b
end

function CB:Build()
    if self.bar then return self.bar end
    local db = cdDB()
    local bar = CreateFrame("Frame", "ERTCountdownBar", UIParent)
    bar:SetSize(BAR_W, BAR_H)
    bar:SetFrameStrata("MEDIUM")
    bar:SetMovable(true); bar:EnableMouse(true)
    bar:RegisterForDrag("LeftButton")
    bar:SetScript("OnDragStart", bar.StartMoving)
    bar:SetScript("OnDragStop", function(b)
        b:StopMovingOrSizing()
        local p, _, _, x, y = b:GetPoint()
        db.point, db.x, db.y = p, x, y
    end)
    if db.point then
        bar:SetPoint(db.point, UIParent, db.point, db.x or 0, db.y or 0)
    else
        bar:SetPoint("TOP", UIParent, "TOP", 0, -160)
    end
    bar:Hide()
    self.bar = bar
    self.bars = {}
    return bar
end

-- When the bar goes idle (nothing currently on respawn), a wanted rare may still
-- be counting down toward the display window from further out. Find the soonest
-- time (seconds from now) until one such rare crosses into the 6h window, so we
-- can schedule a single precise wake-up instead of polling. nil = none pending.
local function soonestFutureCrossing()
    local best
    for _, npc in ipairs(ERT:GetTrackedNPCs()) do
        if ERT:IsWanted(npc) and not ERT:IsIgnored(npc) then
            local st = ERT:GetStatus(npc)
            if st and st.state == "too_early" then
                local opensIn = st.opensIn or 0
                if opensIn > MAX_COUNTDOWN then
                    local untilWindow = opensIn - MAX_COUNTDOWN
                    if not best or untilWindow < best then best = untilWindow end
                end
            end
        end
    end
    return best
end

-- Build the wanted-rare respawn list and paint the bars. Each tracked, wanted
-- rare with a respawn window shows a labelled stopwatch ticking the full time
-- until it's back, with a fill that tracks respawn progress.
function CB:Scan()
    local db = cdDB()
    if not db.enabled then
        if self.bar then self.bar:Hide() end
        self:_StopDriver()
        return
    end
    self:Build()

    local items = {}
    self._wasUp = self._wasUp or {}
    for _, npc in ipairs(ERT:GetTrackedNPCs()) do
        if ERT:IsWanted(npc) and not ERT:IsIgnored(npc) then
            local st = ERT:GetStatus(npc)
            -- Ring once when a wanted rare comes up: the moment its countdown hits
            -- zero (window opens) or it's confirmed alive. Primed on the first
            -- scan so we don't blast every already-up rare at login.
            local isUp = (st.state == "alive" or st.state == "window_open" or st.state == "overdue")
            if self._primed and isUp and not self._wasUp[npc] then PlayUpSound() end
            self._wasUp[npc] = isUp

            local rank, label, color, line, frac
            if st.state == "alive" then
                rank, label, color, line, frac = 0, "UP", { 0.2, 1, 0.2 }, "Up now (confirmed)", 1
            elseif st.state == "overdue" then
                -- Past the projected window, not yet sighted — very likely up.
                rank, label, color, line, frac = 1, "MAY BE UP", { 1, 0.55, 0.15 },
                    "Past estimate — likely up now", 1
            elseif st.state == "window_open" then
                -- Countdown ended; it can spawn anywhere within the window.
                rank, label, color = 2, "MAY BE UP", { 0.85, 1, 0.4 }
                line = "In spawn window — may be up"
                local span = (st.latest or 0) - (st.earliest or 0)
                frac = span > 0 and math.min(1, math.max(0, ((st.since or 0) - (st.earliest or 0)) / span)) or 1
            elseif st.state == "too_early" and (st.opensIn or 1e9) <= MAX_COUNTDOWN then
                local remain = st.opensIn or 0
                rank, label, line = 3, fmtClock(remain), "Respawns in " .. fmtClock(remain)
                if remain <= 60 then color = { 0.4, 1, 0.4 }
                elseif remain <= 300 then color = { 1, 0.9, 0.3 }
                else color = { 0.55, 0.78, 1 } end
                local total = st.earliest or remain
                frac = total > 0 and math.min(1, math.max(0, (st.since or 0) / total)) or 0
            end
            if rank then
                items[#items + 1] = {
                    npc = npc, rank = rank, key = st.opensIn or 0,
                    name = ERT:GetName(npc), label = label, color = color,
                    line = line, frac = frac or 0,
                }
            end
        end
    end
    table.sort(items, function(a, b)
        if a.rank ~= b.rank then return a.rank < b.rank end
        if a.key ~= b.key then return a.key < b.key end
        return (a.name or "") < (b.name or "")
    end)

    local n = math.min(#items, MAX_BARS)
    local pinned = db.pinned and true or false
    for i = 1, MAX_BARS do
        local b = acquireBar(self, i)
        local it = items[i]
        if it then
            b.icon:SetTexture(iconFor(it.npc))
            b.icon:Show()
            b.name:SetText(it.name or ("npc " .. it.npc))
            b.name:SetTextColor(0.95, 0.95, 0.95)
            b.cd:SetText(it.label)
            b.cd:SetTextColor(it.color[1], it.color[2], it.color[3])
            b.fill:SetColorTexture(it.color[1], it.color[2], it.color[3], 0.30)
            b.fill:SetWidth(BAR_W * (it.frac or 0))
            b.fill:Show()
            b.npc = it.npc
            b.statusLine = it.line
            b:Show()
        elseif i == 1 and pinned and n == 0 then
            -- popped out with nothing on respawn: a dim placeholder so the list
            -- stays visible and positionable.
            b.icon:SetTexture("Interface\\Icons\\INV_Misc_PocketWatch_01")
            b.icon:Show()
            b.name:SetText("No wanted rares on respawn")
            b.name:SetTextColor(0.6, 0.6, 0.6)
            b.cd:SetText("—")
            b.cd:SetTextColor(0.6, 0.6, 0.6)
            if b.fill then b.fill:Hide() end
            b.npc = nil
            b.statusLine = nil
            b:Show()
        else
            b:Hide()
        end
    end

    if n > 0 then
        self.bar:SetWidth(BAR_W)
        self.bar:SetHeight(n * BAR_H + (n - 1) * ROW_GAP)
        self.bar:Show()
    elseif pinned then
        self.bar:SetWidth(BAR_W)
        self.bar:SetHeight(BAR_H)
        self.bar:Show()
    else
        self.bar:Hide()
    end

    -- The 1s stopwatch tick is only meaningful while a bar is on screen (or the
    -- list is pinned). Otherwise detach the ticker entirely — zero idle cost —
    -- and, if a wanted rare will enter the display window later, set a single
    -- precise wake-up for that moment. Status changes (kills/sightings) also
    -- re-trigger Scan via ETB_RARE_STATUS_CHANGED, so this covers both paths.
    if n > 0 or pinned then
        self:_StartDriver()
    else
        self:_StopDriver()
        if not self._wakePending then
            local dt = soonestFutureCrossing()
            if dt and C_Timer and C_Timer.After then
                self._wakePending = true
                C_Timer.After(math.max(1, dt + 1), function()
                    self._wakePending = false
                    CB:Scan()
                end)
            end
        end
    end

    self._primed = true
    return n
end

-- The 1s ticker lives on a frame we show/hide on demand: a hidden frame receives
-- no OnUpdate, so hiding it fully detaches the tick (vs. the old design that ran
-- every frame forever after login). Scan() decides when it should run.
function CB:_StartDriver()
    if not self.driver then
        local d = CreateFrame("Frame")
        d._acc = 0
        d:SetScript("OnUpdate", function(fr, e)
            fr._acc = (fr._acc or 0) + (e or 0)
            if fr._acc < 1 then return end
            fr._acc = 0
            CB:Scan()
        end)
        self.driver = d
    end
    self.driver._acc = 0
    self.driver:Show()
end

function CB:_StopDriver()
    if self.driver then self.driver:Hide() end
end

function CB:Start()
    self:Build()
    CB:Scan()   -- Scan attaches the ticker only if there is something to tick.
end

function CB:SetEnabled(v)
    cdDB().enabled = v and true or false
    if not v and self.bar then self.bar:Hide() end
    CB:Scan()
end
function CB:IsEnabled() return cdDB().enabled and true or false end

-- Pop-out (pin): keep the list on screen even when nothing is on respawn, so it
-- can be positioned and watched. Also force-enables the timer.
function CB:SetPinned(v)
    local db = cdDB()
    db.pinned = v and true or false
    if v then db.enabled = true end
    CB:Scan()
end
function CB:IsPinned() return cdDB().pinned and true or false end

-- Up-sound toggle (on by default).
function CB:SetSound(v) cdDB().sound = v and true or false end
function CB:IsSound() return cdDB().sound ~= false end
function CB:TestSound() if PlaySoundFile then pcall(PlaySoundFile, SOUND_FILE, "Master") end end

function ERT:ToggleCountdown()
    CB:SetEnabled(not CB:IsEnabled())
    return CB:IsEnabled()
end
function ERT:PopOutTimer()
    CB:SetPinned(not CB:IsPinned())
    return CB:IsPinned()
end
function ERT:ToggleRareSound()
    CB:SetSound(not CB:IsSound())
    if CB:IsSound() then CB:TestSound() end
    return CB:IsSound()
end

local boot = CreateFrame("Frame")
boot:RegisterEvent("PLAYER_LOGIN")
boot:SetScript("OnEvent", function()
    local function startCountdown()
        CB:Start()
    end
    if ETB.RunWhenStartupIdle then
        ETB:RunWhenStartupIdle("rareCountdown.start", startCountdown, { delay = 2, interval = 2, maxWait = 180 })
    else
        startCountdown()
    end
end)

-- A kill opens a respawn window; a sighting/vignette confirms a rare up. Either
-- way the bar may need to (re)appear and start ticking — so rescan on the
-- Observer's status signal instead of polling on an always-on timer.
if ETB.EventBus and ETB.EventBus.Register then
    ETB.EventBus:Register("ETB_RARE_STATUS_CHANGED", function() CB:Scan() end)
end
