-- KiraUI-Plugin_Bloodlust.lua
-- Bloodlust "lust ready" tracker (Midnight-safe).
--
--   * When that debuff is within N seconds of expiring it shows a Bloodlust icon
--     + countdown (+ optional pixel glow): "lust almost ready".
--   * The instant the debuff falls off (lust is available again) it plays a sound.
--
-- The ready sound is edge-detected (presence boolean), so it works even if the
-- debuff's remaining time is secret. Only the early-warning icon needs the time;
-- if expirationTime is secret it just won't show the warning (sound still fires).

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

local CreateFrame = CreateFrame
local C_Timer = C_Timer
local C_UnitAuras = C_UnitAuras
local GetTime = GetTime
local PlaySoundFile = PlaySoundFile
local issecretvalue = issecretvalue
local pairs, ipairs = pairs, ipairs
local tinsert, tsort = table.insert, table.sort
local mceil = math.ceil
local sformat = string.format

local GPA = C_UnitAuras and C_UnitAuras.GetPlayerAuraBySpellID
ns.hasBloodlust = GPA and true or false
if not ns.hasBloodlust then return end

----------------------------------------------------------------------
-- Ready-alert suppression (shared by Bloodlust + Potion via ns.ReadyAlertSuppressed).
-- A Mythic+ key start resets every cooldown and clears Sated, firing false
-- "lust/potion ready" edges. Suppress the ready sound + text for a window around
-- the M+ countdown / start (and zone-ins) so it never spams before the key.
----------------------------------------------------------------------
if not ns.ReadyAlertSuppressed then
	local GetTime = GetTime
	local suppressUntil = 0
	function ns.ReadyAlertSuppressed() return GetTime() < suppressUntil end
	local function Bump(secs)
		local t = GetTime() + (secs or 6)
		if t > suppressUntil then suppressUntil = t end
	end
	local rs = CreateFrame('Frame')
	rs:RegisterEvent('PLAYER_ENTERING_WORLD')
	rs:RegisterEvent('CHALLENGE_MODE_START')
	rs:RegisterEvent('CHALLENGE_MODE_RESET')
	rs:RegisterEvent('START_TIMER')
	rs:SetScript('OnEvent', function(_, event, arg1, arg2)
		if event == 'START_TIMER' then
			-- on-screen countdown (M+/arena/BG): cover the countdown + buffer so the
			-- key-start cooldown reset is still inside the window.
			local secs = tonumber(arg2) or 10
			if secs > 30 then secs = 30 end
			Bump(secs + 5)
		else
			Bump(6)
		end
	end)
end

local function secret(v) return issecretvalue and issecretvalue(v) end

-- Sated / Exhaustion / Temporal Displacement / Insanity / Fatigued lockout debuffs.
local SATED = { 57723, 57724, 80354, 95809, 160455, 264689, 390435, 428628 }
local BLOODLUST_ICON = 136012 -- Spell_Nature_BloodLust
local DEFAULT_SOUND = '|cFF00FF00Kira - Bloodlust|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, -- show the icon when this many seconds (or fewer) remain
	x = 0,
	y = 150,
	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,
}

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

local function GetCfg() return ns.ModuleCfg('bloodlust', DEFAULTS, Migrate) end
ns.BLGetCfg = GetCfg

----------------------------------------------------------------------
-- Sound (LSM)
----------------------------------------------------------------------
function ns.BLSoundOptions()
	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)
			local sa = tostring(a):gsub('|c%x%x%x%x%x%x%x%x', ''):gsub('|r', ''):lower()
			local sb = tostring(b):gsub('|c%x%x%x%x%x%x%x%x', ''):gsub('|r', ''):lower()
			return sa < sb
		end)
		for _, name in ipairs(names) do out[#out + 1] = { value = name, text = name } end
	end
	tinsert(out, 1, { value = 'None', text = 'None' })
	local cur = GetCfg().sound
	if cur and cur ~= 'None' then
		local found = false
		for _, o in ipairs(out) do if o.value == cur then found = true break end end
		if not found then tinsert(out, 2, { value = cur, text = cur }) end
	end
	return out
end

function ns.BLPlaySound(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), same as Externals
----------------------------------------------------------------------
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 150)
end

local function EnsureFrame()
	if frame then return end
	frame = CreateFrame('Frame', 'KiraUIBloodlust', _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(BLOODLUST_ICON)
	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
	-- our number sits on the cooldown frame so it draws above the swipe
	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.Text:SetFont(ns.GetFont(), cfg.textSize or 22, 'OUTLINE')
	ApplyPosition()
end

local function HideIcon()
	if btn then
		StopGlow(btn)
		btn.__cdExp = 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, expTime)
	EnsureFrame()
	btn.Text:SetText(tostring(mceil(rem)))
	-- swipe across the warning window (set once per lockout, drains over the last
	-- 'threshold' seconds to ready)
	if expTime then
		local thr = GetCfg().threshold or 5
		if btn.__cdExp ~= expTime then
			btn.__cdExp = expTime
			btn.Cooldown:SetCooldown(expTime - thr, thr)
		end
		btn.Cooldown:Show()
	end
	frame:Show()
	btn:Show()
	if not btn.__glow then StartGlow(btn) end
end

----------------------------------------------------------------------
-- Tracking
----------------------------------------------------------------------
local satedActive = false
local satedExp = nil

local function CurrentSated()
	for i = 1, #SATED do
		local a = GPA(SATED[i])
		if a then return a end
	end
	return nil
end

local SyncTicker -- fwd (defined with the countdown ticker below)

local function OnAura()
	-- Detection always runs (even when the icon module is disabled) so the combat-
	-- text + sound both fire on the ready edge regardless of the icon toggle;
	-- only the early-warning ICON respects cfg.enable.
	local cfg = GetCfg()
	local a = CurrentSated()
	if a then
		satedActive = true
		-- secrecy check FIRST: truthiness-testing a secret value errors
		local exp = a.expirationTime
		satedExp = (not secret(exp)) and exp or nil
	elseif satedActive then
		-- falling edge: the lockout ended -> lust is ready again
		satedActive = false
		satedExp = nil
		HideIcon()
		if not previewActive and not (ns.ReadyAlertSuppressed and ns.ReadyAlertSuppressed()) then
			if cfg.soundEnabled then ns.BLPlaySound(cfg.sound) end
			if ns.CTAnnounce then ns.CTAnnounce('bloodlust') end
		end
	end
	SyncTicker()
end

local function Tick()
	if previewActive then return end
	local cfg = GetCfg()
	if not cfg.enable or not satedActive or not satedExp then HideIcon() return end
	local rem = satedExp - GetTime()
	if rem > 0 and rem <= (cfg.threshold or 5) then
		ShowIcon(rem, satedExp)
	else
		HideIcon()
	end
end

-- The countdown clock only has anything to do while the icon module is on AND a
-- readable Sated debuff is actually running. Outside that window the ticker is
-- cancelled instead of waking 10x/second to fall straight out of Tick().
-- SyncTicker is called from every place that changes those inputs: OnAura (the
-- rising/falling edge), PLAYER_ENTERING_WORLD (baseline), Apply (options
-- toggle) and the preview setter.
local tickTicker
SyncTicker = function()
	local want = (not previewActive) and satedActive and satedExp ~= nil
		and GetCfg().enable and true or false
	if want and not tickTicker then
		tickTicker = C_Timer.NewTicker(0.1, Tick)
	elseif not want and tickTicker then
		tickTicker:Cancel()
		tickTicker = nil
		-- one last pass so the icon is cleaned up on the way out (Tick's own
		-- guards no-op it during preview, which manages its own visibility)
		Tick()
	end
end

----------------------------------------------------------------------
-- Apply / preview / drivers
----------------------------------------------------------------------
local function ShowPreview()
	EnsureFrame()
	ApplyVisual()
	local thr = GetCfg().threshold or 5
	btn.Text:SetText(tostring(thr))
	btn.__cdExp = 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() end
	SyncTicker()
end
ns.BLApply = function() C_Timer.After(0, Apply) end

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

local evt = CreateFrame('Frame')
evt:RegisterUnitEvent('UNIT_AURA', 'player')
evt:RegisterEvent('PLAYER_ENTERING_WORLD')
evt:SetScript('OnEvent', function(_, event)
	if event == 'PLAYER_ENTERING_WORLD' then
		-- baseline silently (so logging in while Sated never plays the ready sound)
		local a = CurrentSated()
		satedActive = a and true or false
		if a then
			-- secrecy check FIRST: truthiness-testing a secret value errors
			local exp = a.expirationTime
			satedExp = (not secret(exp)) and exp or nil
		else
			satedExp = nil
		end
		C_Timer.After(0.5, Apply)
		SyncTicker()
		return
	end
	OnAura()
end)

-- Diagnostic: /kiralust while you have a Sated/Exhaustion debuff - reports whether
-- its remaining time is readable (needed for the early-warning icon).
SLASH_KIRALUST1 = '/kiralust'
SlashCmdList.KIRALUST = function()
	local a = CurrentSated()
	if not a then print('|cffff7d0aKiraLust|r: no Sated/Exhaustion debuff on you right now.') return end
	local exp = a.expirationTime
	if secret(exp) then
		print('|cffff7d0aKiraLust|r Sated present. remaining = SECRET.')
		print('|cffff2020=> the early-warning icon cannot time it; the ready sound still works.|r')
	elseif exp == nil then
		print('|cffff7d0aKiraLust|r Sated present, but no expirationTime.')
	else
		print(('|cffff7d0aKiraLust|r Sated present. remaining = %.1f sec (readable).'):format(exp - GetTime()))
		print('|cff20ff20=> the early-warning icon will work.|r')
	end
end
