-- KiraUI-Plugin_Potion.lua
-- "Potion ready" tracker. Mirrors the Bloodlust module, but instead of an aura it
-- watches the cooldown of a single representative damage-potion item.
--
-- All combat damage potions share one category cooldown (5 min). We just read the
-- item cooldown of POTION_ID below: if it's on cooldown, a potion was used; when it
-- ends, a potion is ready again. No bag scan, no per-potion ID list. Item cooldowns
-- are not secret, so the timer always works. When the cooldown is within N seconds
-- of ending it shows the potion icon + countdown (+ glow); the instant it's ready it
-- plays a sound and (optionally) a combat-text message.

local addonName, ns = ...
local _G = _G

local CreateFrame = CreateFrame
local C_Timer = C_Timer
local C_Item = C_Item
local C_Container = C_Container
local GetTime = GetTime
local PlaySoundFile = PlaySoundFile
local pairs, ipairs = pairs, ipairs
local tinsert, tsort = table.insert, table.sort
local mceil = math.ceil

local POTION_ID = 241308 -- R2 Light's Potential (any damage potion shares this 5-min CD)

-- Item cooldown by itemID -> start, duration, enable. Resolve whichever API exists.
local GetItemCD = (C_Container and C_Container.GetItemCooldown)
	or _G.GetItemCooldown
	or (C_Item and C_Item.GetItemCooldown)

ns.hasPotion = GetItemCD and true or false
if not ns.hasPotion then return end


local function PotionIcon()
	if C_Item and C_Item.GetItemIconByID then
		local i = C_Item.GetItemIconByID(POTION_ID)
		if i then return i end
	end
	local f = (C_Item and C_Item.GetItemInfoInstant) or _G.GetItemInfoInstant
	if f then
		local _, _, _, _, icon = f(POTION_ID)
		if icon then return icon end
	end
	return 967534 -- generic potion fallback
end

local DEFAULT_SOUND = '|cFF00FF00Kira - Potion|r'
local LSM = ns.LSM
local function LCG() return _G.LibStub and _G.LibStub('LibCustomGlow-1.0', true) end

----------------------------------------------------------------------
-- Settings
----------------------------------------------------------------------
local DEFAULTS = {
	enable = false,
	lock = false,
	iconSize = 48,
	textSize = 22,
	threshold = 5,
	x = 0,
	y = 110,
	soundEnabled = true,
	sound = DEFAULT_SOUND,
	glow = true,
	glowType = 'pixel',
	glowColor = { 242 / 255, 242 / 255, 82 / 255, 1 },
	glowLines = 8,
	glowFreq = 0.2,
	glowLength = 0,
	glowThickness = 2,
}

-- repair a saved colour that an older build left as a non-table
local function Migrate(cfg)
	if type(cfg.glowColor) ~= 'table' then cfg.glowColor = { 242 / 255, 242 / 255, 82 / 255, 1 } end
end

local function GetCfg() return ns.ModuleCfg('potion', DEFAULTS, Migrate) end
ns.PTGetCfg = GetCfg

function ns.PTSoundOptions()
	if ns.ExtSoundOptions then return ns.ExtSoundOptions() end
	local out = {}
	local lib = LSM()
	local hash = lib and lib.HashTable and lib:HashTable('sound')
	if hash then
		local names = {}
		for name in pairs(hash) do names[#names + 1] = name end
		tsort(names, function(a, b) return tostring(a):lower() < tostring(b):lower() end)
		for _, name in ipairs(names) do out[#out + 1] = { value = name, text = name } end
	end
	tinsert(out, 1, { value = 'None', text = 'None' })
	return out
end

function ns.PTPlaySound(name)
	if ns.ExtPlaySound then return ns.ExtPlaySound(name) end
	if not name or name == 'None' then return end
	local lib = LSM()
	local path = lib and lib.Fetch and lib:Fetch('sound', name, true)
	if path then PlaySoundFile(path, 'Master') end
end

----------------------------------------------------------------------
-- Glow (LibCustomGlow)
----------------------------------------------------------------------
local function StopGlow(b)
	local lib = LCG()
	if not lib or not b.__glow then return end
	if lib.PixelGlow_Stop then lib.PixelGlow_Stop(b) end
	if lib.AutoCastGlow_Stop then lib.AutoCastGlow_Stop(b) end
	if lib.ButtonGlow_Stop then lib.ButtonGlow_Stop(b) end
	if lib.ProcGlow_Stop then lib.ProcGlow_Stop(b) end
	b.__glow = false
end

local function StartGlow(b)
	local lib = LCG()
	if not lib then return end
	local cfg = GetCfg()
	if not cfg.glow then StopGlow(b) return end
	StopGlow(b)
	local c = cfg.glowColor or { 1, 1, 1, 1 }
	local t = cfg.glowType or 'pixel'
	if t == 'autocast' and lib.AutoCastGlow_Start then
		lib.AutoCastGlow_Start(b, c, cfg.glowLines or 8, cfg.glowFreq or 0.2, 1, 0, 0, nil)
	elseif t == 'button' and lib.ButtonGlow_Start then
		lib.ButtonGlow_Start(b, c, cfg.glowFreq or 0.2)
	elseif t == 'proc' and lib.ProcGlow_Start then
		lib.ProcGlow_Start(b, { color = c })
	elseif lib.PixelGlow_Start then
		local len = (cfg.glowLength and cfg.glowLength > 0) and cfg.glowLength or nil
		lib.PixelGlow_Start(b, c, cfg.glowLines or 8, cfg.glowFreq or 0.2, len, cfg.glowThickness or 2, 0, 0, false, nil)
	end
	b.__glow = true
end

----------------------------------------------------------------------
-- Frame
----------------------------------------------------------------------
local frame, btn
local previewActive = false

local function ApplyPosition()
	if not frame then return end
	local cfg = GetCfg()
	frame:ClearAllPoints()
	frame:SetPoint('CENTER', _G.UIParent, 'CENTER', cfg.x or 0, cfg.y or 110)
end

local function EnsureFrame()
	if frame then return end
	frame = CreateFrame('Frame', 'KiraUIPotion', _G.UIParent)
	frame:SetFrameStrata('MEDIUM')
	frame:SetClampedToScreen(true)
	frame:SetMovable(true)
	frame:EnableMouse(false)
	frame:RegisterForDrag('RightButton')
	ns.MakeDraggable(frame, GetCfg, 'x', 'y', function() ApplyPosition() end)
	btn = CreateFrame('Frame', nil, frame)
	btn.border = btn:CreateTexture(nil, 'BACKGROUND')
	btn.border:SetColorTexture(0, 0, 0, 1)
	btn.border:SetAllPoints()
	btn.Icon = btn:CreateTexture(nil, 'ARTWORK')
	btn.Icon:SetPoint('TOPLEFT', 1, -1)
	btn.Icon:SetPoint('BOTTOMRIGHT', -1, 1)
	btn.Icon:SetTexture(PotionIcon())
	btn.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
	btn.Cooldown = CreateFrame('Cooldown', nil, btn, 'CooldownFrameTemplate')
	btn.Cooldown:SetAllPoints()
	btn.Cooldown:SetDrawEdge(false)
	btn.Cooldown:SetDrawBling(false)
	btn.Cooldown:SetReverse(true)
	if btn.Cooldown.SetHideCountdownNumbers then btn.Cooldown:SetHideCountdownNumbers(true) end
	btn.Text = btn.Cooldown:CreateFontString(nil, 'OVERLAY')
	btn.Text:SetPoint('CENTER')
	frame:Hide()
end

local function ApplyVisual()
	if not frame then return end
	local cfg = GetCfg()
	local sz = cfg.iconSize or 48
	frame:SetSize(sz, sz)
	btn:SetSize(sz, sz)
	btn:ClearAllPoints()
	btn:SetPoint('CENTER', frame, 'CENTER', 0, 0)
	btn.Icon:SetTexture(PotionIcon())
	btn.Text:SetFont(ns.GetFont(), cfg.textSize or 22, 'OUTLINE')
	ApplyPosition()
end

local function HideIcon()
	if btn then
		StopGlow(btn)
		btn.__cdEnd = nil
		if btn.Cooldown then
			if btn.Cooldown.Clear then btn.Cooldown:Clear() else btn.Cooldown:SetCooldown(0, 0) end
		end
	end
	if frame and not previewActive then frame:Hide() end
end

local function ShowIcon(rem, endT)
	EnsureFrame()
	btn.Text:SetText(tostring(mceil(rem)))
	local thr = GetCfg().threshold or 5
	if endT and btn.__cdEnd ~= endT then
		btn.__cdEnd = endT
		btn.Cooldown:SetCooldown(endT - thr, thr)
	end
	if endT then btn.Cooldown:Show() end
	frame:Show()
	btn:Show()
	if not btn.__glow then StartGlow(btn) end
end

----------------------------------------------------------------------
-- Tracking: EVENT-DRIVEN, no permanent polling (was a 0.2s ticker). The
-- potion category CD only changes when an item-cooldown event fires -
-- using a potion, or the M+ key start resetting it - and
-- BAG_UPDATE_COOLDOWN covers all of those. On each event we read the item
-- CD once (still reading, not assuming "5 minutes", so a key-start reset
-- is picked up instantly) and schedule exact wake-ups: one timer at
-- (end - threshold) to show the icon, then a 0.1s ticker ONLY while the
-- countdown text is on screen, and a final wake at the end for the ready
-- alert. Idle cost: zero.
-- Detection always runs (even when the icon module is disabled) so the
-- combat-text "Potion Ready" announce still fires; icon + sound respect
-- their own settings.
----------------------------------------------------------------------
local active = false
local cdEnd = 0
local wakeTimer, textTicker

local function CancelTimers()
	if wakeTimer then wakeTimer:Cancel() wakeTimer = nil end
	if textTicker then textTicker:Cancel() textTicker = nil end
end

local Refresh -- fwd

local function ReadyAlert()
	local cfg = GetCfg()
	HideIcon()
	if not (ns.ReadyAlertSuppressed and ns.ReadyAlertSuppressed()) then
		if cfg.soundEnabled then ns.PTPlaySound(cfg.sound) end
		if ns.CTAnnounce then ns.CTAnnounce('potion') end
	end
end

local function TextTick()
	if previewActive then return end
	local rem = cdEnd - GetTime()
	if rem <= 0 then return end -- the end wake-up handles the alert
	if GetCfg().enable then ShowIcon(rem, cdEnd) else HideIcon() end
end

Refresh = function()
	if previewActive then return end
	CancelTimers()
	local cfg = GetCfg()
	local start, dur = GetItemCD(POTION_ID)
	local now = GetTime()
	if start and dur and start > 0 and dur > 0 and (start + dur) - now > 0 then
		active = true
		cdEnd = start + dur
		local rem = cdEnd - now
		local thr = cfg.threshold or 5
		if rem > thr + 0.05 then
			-- long wait: sleep until the countdown window starts
			HideIcon()
			wakeTimer = C_Timer.NewTimer(rem - thr, Refresh)
		else
			-- countdown window: icon + text, ticking only for these seconds
			if cfg.enable then ShowIcon(rem, cdEnd) else HideIcon() end
			textTicker = C_Timer.NewTicker(0.1, TextTick)
			wakeTimer = C_Timer.NewTimer(rem + 0.05, Refresh)
		end
		return
	end
	-- not on cooldown
	local wasActive = active
	active = false
	if wasActive then ReadyAlert() else HideIcon() end
end

----------------------------------------------------------------------
-- Apply / preview / drivers
----------------------------------------------------------------------
local function ShowPreview()
	EnsureFrame()
	ApplyVisual()
	local thr = GetCfg().threshold or 5
	btn.Text:SetText(tostring(thr))
	btn.__cdEnd = nil
	btn.Cooldown:SetCooldown(GetTime(), thr)
	btn.Cooldown:Show()
	frame:Show()
	btn:Show()
	StartGlow(btn)
end

local function Apply()
	EnsureFrame()
	ApplyVisual()
	frame:EnableMouse(previewActive and not GetCfg().lock or false)
	if previewActive then
		ShowPreview()
	else
		HideIcon()
		Refresh() -- re-schedule with the current threshold/enable settings
	end
end
ns.PTApply = function() C_Timer.After(0, Apply) end

ns.PTIsPreview = function() return previewActive end
ns.PTSetPreview = function(state)
	previewActive = state and true or false
	EnsureFrame()
	frame:EnableMouse(previewActive and not GetCfg().lock or false)
	if previewActive then
		CancelTimers()
		ShowPreview()
	else
		StopGlow(btn)
		frame:Hide()
		Refresh()
	end
end

-- BAG_UPDATE_COOLDOWN fires whenever an item cooldown changes (potion use,
-- key-start reset); it can burst, so coalesce same-frame repeats.
local refreshPending = false
local function QueueRefresh()
	if refreshPending then return end
	refreshPending = true
	C_Timer.After(0.05, function()
		refreshPending = false
		Refresh()
	end)
end

local evt = CreateFrame('Frame')
evt:RegisterEvent('PLAYER_ENTERING_WORLD')
evt:RegisterEvent('BAG_UPDATE_COOLDOWN')
if C_ChallengeMode then evt:RegisterEvent('CHALLENGE_MODE_START') end
evt:SetScript('OnEvent', function(_, event)
	if event == 'PLAYER_ENTERING_WORLD' then
		C_Timer.After(0.5, Apply) -- Apply ends in Refresh()
		return
	end
	QueueRefresh()
end)
