-- KiraUI-Plugin_EUIInterrupt.lua
-- EllesmereUI nameplate "interrupt ready" tracker.
--
-- Shows YOUR interrupt spell's icon + cooldown swipe on the focus nameplate
-- (or the target nameplate when you have no focus), and only while your
-- interrupt is on cooldown (hidden the moment it's ready, when that option is on).
--
-- The interrupt's cooldown is SECRET in Midnight restricted content, so it is
-- driven through a duration object (HasSecretValues / IsZero), never read as a
-- number - the same approach as the cooldown-pulse feature. Because the remaining
-- time is secret, we cannot draw our own countdown text; we rely on Blizzard's
-- cooldown countdown and only override its FONT SIZE.

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

local ENP = _G.EllesmereNameplates_NS
local hasInt = (ENP ~= nil) and (_G.EllesmereUI ~= nil)

ns.hasEUIInterrupt = hasInt
if not hasInt then return end

local CreateFrame = CreateFrame
local C_Timer = C_Timer
local pairs, ipairs = pairs, ipairs
local UnitExists, UnitIsUnit = UnitExists, UnitIsUnit
local IsPlayerSpell = IsPlayerSpell
local C_Spell = C_Spell
local issecretvalue = issecretvalue
local hooksecurefunc = hooksecurefunc
local GetSpellCooldownDuration = C_Spell and C_Spell.GetSpellCooldownDuration
local GetSpellChargeDuration = C_Spell and C_Spell.GetSpellChargeDuration
local GetSpellCooldown = C_Spell and C_Spell.GetSpellCooldown

-- Exact, non-scaling border width in screen pixels.
local BORDER_PX = 1.5

----------------------------------------------------------------------
-- Settings (defaults place it in the left column; no sound)
----------------------------------------------------------------------
local DEFAULTS = {
	enable = false,
	hideWhenReady = true,
	cooldownText = true,
	size = 30,
	textSize = 14,
	x = -112,
	y = -7,
}

local function GetCfg() return ns.ModuleCfg('euiInterrupt', DEFAULTS) end
ns.EUIIntGetCfg = GetCfg

----------------------------------------------------------------------
-- The player's interrupt spell: pick whichever of these the player knows.
----------------------------------------------------------------------
local INTERRUPTS = {
	6552,   -- Warrior: Pummel
	1766,   -- Rogue: Kick
	2139,   -- Mage: Counterspell
	57994,  -- Shaman: Wind Shear
	47528,  -- Death Knight: Mind Freeze
	183752, -- Demon Hunter: Disrupt
	116705, -- Monk: Spear Hand Strike
	96231,  -- Paladin: Rebuke
	147362, -- Hunter: Counter Shot
	187707, -- Hunter (Survival): Muzzle
	106839, -- Druid: Skull Bash
	351338, -- Evoker: Quell
}
-- Cached: rescanning up to 12 IsPlayerSpell calls on every SPELL_UPDATE_COOLDOWN
-- is pointless - the answer only changes on spec/talent swaps. Self-healing: if
-- the cached spell is no longer known, one full rescan re-resolves it.
local cachedIntID
local function GetInterruptID()
	if cachedIntID and IsPlayerSpell(cachedIntID) then return cachedIntID end
	cachedIntID = nil
	for _, id in ipairs(INTERRUPTS) do
		if IsPlayerSpell(id) then cachedIntID = id return id end
	end
end

local function FetchDuration(spellID)
	local charges = GetSpellChargeDuration and C_Spell.GetSpellCharges(spellID)
	if charges and charges.isActive then return GetSpellChargeDuration(spellID) end
	return GetSpellCooldownDuration(spellID, true)
end

-- Is the interrupt genuinely on its own cooldown? C_Spell.GetSpellCooldown exposes
-- isActive / isOnGCD as plain (non-secret) booleans that stay reliable in combat,
-- unlike the duration object whose remaining time is SECRET there. "isActive and not
-- isOnGCD" means it is on its real cooldown, not merely the global cooldown.
local function InterruptOnCooldown(spellID)
	if GetSpellCooldown then
		local info = GetSpellCooldown(spellID)
		if info ~= nil then
			return (info.isActive and not info.isOnGCD) or false
		end
	end
	local dur = FetchDuration(spellID)
	if dur and not dur:HasSecretValues() then return not dur:IsZero() end
	return false
end

----------------------------------------------------------------------
-- Which plate to attach to: the focus's, or the target's when no focus.
----------------------------------------------------------------------
local function RelevantUnit()
	if UnitExists('focus') then return 'focus' end
	if UnitExists('target') then return 'target' end
	return nil
end

local function FindPlate(unit)
	if not ENP.plates then return end
	for _, p in pairs(ENP.plates) do
		-- UnitIsUnit can return a SECRET boolean for enemy units in restricted
		-- content; secrecy check FIRST, never truthiness-test the raw result
		if p.unit then
			local same = UnitIsUnit(p.unit, unit)
			if not (issecretvalue and issecretvalue(same)) and same then return p end
		end
	end
end

----------------------------------------------------------------------
-- Display
----------------------------------------------------------------------
local UpdateInterrupt -- forward decl
local icon, wasShown

-- Keep the countdown text at the configured size. Blizzard re-applies its own
-- auto-scaled font on the cooldown's FontString, so we hook SetFont (guarded)
-- and re-assert our size - the same enforcement pattern used for font outlines.
local function EnforceCDFont(fs)
	if not (fs and fs.GetFont and fs.SetFont) then return end
	if not fs.__kiraCDHook then
		fs.__kiraCDHook = true
		hooksecurefunc(fs, 'SetFont', function(self)
			if self.__kiraCDGuard then return end
			local font, size, flags = self:GetFont()
			local want = (GetCfg().textSize or 14)
			if font and size ~= want then
				self.__kiraCDGuard = true
				self:SetFont(ns.GetFont(font), want, flags)
				self.__kiraCDGuard = false
			end
		end)
	end
	local font, size, flags = fs:GetFont()
	local want = (GetCfg().textSize or 14)
	if font and size ~= want then fs:SetFont(ns.GetFont(font), want, flags) end
end

local function StyleCDText()
	if not icon or not icon.cd then return end
	for _, r in ipairs({ icon.cd:GetRegions() }) do
		if r and r.GetFont and r.SetFont then EnforceCDFont(r) end
	end
end

local function EnsureIcon()
	if icon then return end
	icon = CreateFrame('Frame', nil, _G.UIParent)
	icon:Hide()

	-- Artwork (cropped so the spell's own bevel/border isn't shown).
	icon.tex = icon:CreateTexture(nil, 'ARTWORK')
	icon.tex:SetAllPoints(icon)
	icon.tex:SetTexCoord(0.08, 0.92, 0.08, 0.92)

	-- A perfect 1px black frame built from four edge lines that never scale
	-- with the icon. Pixel size is pinned via PixelUtil when available.
	local function edge() local t = icon:CreateTexture(nil, 'OVERLAY'); t:SetColorTexture(0, 0, 0, 1); return t end
	icon.bT, icon.bB, icon.bL, icon.bR = edge(), edge(), edge(), edge()

	icon.bT:SetPoint('BOTTOMLEFT', icon, 'TOPLEFT', -BORDER_PX, 0)
	icon.bT:SetPoint('BOTTOMRIGHT', icon, 'TOPRIGHT', BORDER_PX, 0)
	icon.bB:SetPoint('TOPLEFT', icon, 'BOTTOMLEFT', -BORDER_PX, 0)
	icon.bB:SetPoint('TOPRIGHT', icon, 'BOTTOMRIGHT', BORDER_PX, 0)
	icon.bL:SetPoint('TOPRIGHT', icon, 'TOPLEFT', 0, BORDER_PX)
	icon.bL:SetPoint('BOTTOMRIGHT', icon, 'BOTTOMLEFT', 0, -BORDER_PX)
	icon.bR:SetPoint('TOPLEFT', icon, 'TOPRIGHT', 0, BORDER_PX)
	icon.bR:SetPoint('BOTTOMLEFT', icon, 'BOTTOMRIGHT', 0, -BORDER_PX)

	if _G.PixelUtil then
		_G.PixelUtil.SetHeight(icon.bT, BORDER_PX)
		_G.PixelUtil.SetHeight(icon.bB, BORDER_PX)
		_G.PixelUtil.SetWidth(icon.bL, BORDER_PX)
		_G.PixelUtil.SetWidth(icon.bR, BORDER_PX)
	else
		icon.bT:SetHeight(BORDER_PX); icon.bB:SetHeight(BORDER_PX)
		icon.bL:SetWidth(BORDER_PX);  icon.bR:SetWidth(BORDER_PX)
	end

	icon.cd = CreateFrame('Cooldown', nil, icon, 'CooldownFrameTemplate')
	icon.cd:SetAllPoints(icon)
	icon.cd:SetDrawEdge(false)
	icon.cd:SetDrawBling(false)
	icon.cd:SetScript('OnCooldownDone', function() UpdateInterrupt(false) end)
end

local lastRearm = 0
UpdateInterrupt = function(rearm)
	local cfg = GetCfg()
	if not cfg.enable or not GetSpellCooldownDuration then
		if wasShown and icon then icon:Hide() end
		wasShown = false
		return
	end
	EnsureIcon()

	local intID = GetInterruptID()
	if not intID then if wasShown then icon:Hide() end wasShown = false return end

	local unit = RelevantUnit()
	local plate = unit and FindPlate(unit)
	if not plate or not plate.health then
		if wasShown then icon:Hide() end
		wasShown = false
		icon._plate = nil
		return
	end

	if cfg.hideWhenReady and not InterruptOnCooldown(intID) then
		if wasShown then icon:Hide() end wasShown = false return
	end

	-- position + look: only when the plate actually changed (plates recycle) or
	-- an options apply forced a relayout (EUIIntApply clears icon._plate)
	if icon._plate ~= plate then
		icon._plate = plate
		icon:SetParent(plate)
		icon:SetFrameLevel((plate.health:GetFrameLevel() or 1) + 8)
		local sz = cfg.size or 20
		icon:SetSize(sz, sz)
		icon:ClearAllPoints()
		icon:SetPoint('CENTER', plate.health, 'CENTER', cfg.x or 0, cfg.y or 0)
		icon.cd:SetHideCountdownNumbers(not cfg.cooldownText)
	end

	-- texture only changes when the interrupt spell does
	if icon._texFor ~= intID then
		local tex = C_Spell.GetSpellTexture(intID)
		if tex and not (issecretvalue and issecretvalue(tex)) then
			icon.tex:SetTexture(tex)
			icon._texFor = intID
		end
	end

	-- Re-arm the swipe from a fresh duration object: always on (re)show, and
	-- from cooldown events at most 5x/sec. CDR talents keep nudging the real
	-- CD, so periodic re-arms keep the swipe honest without re-arming (and
	-- allocating a duration object) on every GCD.
	local now = _G.GetTime()
	if not wasShown or (rearm and (now - lastRearm) > 0.2) then
		icon.cd:SetCooldownFromDurationObject(FetchDuration(intID))
		lastRearm = now
	end

	if not wasShown then
		icon:Show()
		StyleCDText() -- font hooks persist; only needed at (re)show
	end
	wasShown = true
end
ns.EUIIntApply = function()
	-- options changed: force full relayout, re-texture, re-arm and restyle
	if icon then icon._plate = nil icon._texFor = nil end
	lastRearm = 0
	wasShown = false
	UpdateInterrupt(true)
	StyleCDText()
	-- start/stop the 0.2s poll to match the new enable state (defined below)
	if ns.EUIIntSyncPollTicker then ns.EUIIntSyncPollTicker() end
end

----------------------------------------------------------------------
-- Drivers
----------------------------------------------------------------------
-- SPELL_UPDATE_COOLDOWN fires in bursts (every cast, every CD change), so
-- coalesce: at most ONE UpdateInterrupt per frame, scheduled with a named
-- function (no per-event closure garbage) and a hard early-out while the
-- feature is disabled. The rearm request survives coalescing via pendingRearm.
local pendingUpdate, pendingRearm
local function RunPending()
	local rearm = pendingRearm
	pendingUpdate, pendingRearm = nil, nil
	UpdateInterrupt(rearm)
end
local f = CreateFrame('Frame')
f:RegisterEvent('PLAYER_TARGET_CHANGED')
f:RegisterEvent('PLAYER_FOCUS_CHANGED')
f:RegisterEvent('PLAYER_SPECIALIZATION_CHANGED')
f:RegisterEvent('NAME_PLATE_UNIT_ADDED')
f:RegisterEvent('NAME_PLATE_UNIT_REMOVED')
f:RegisterEvent('SPELL_UPDATE_COOLDOWN')
f:RegisterEvent('SPELL_UPDATE_CHARGES')
f:SetScript('OnEvent', function(_, event)
	if event == 'PLAYER_SPECIALIZATION_CHANGED' then cachedIntID = nil end
	if not GetCfg().enable then return end
	if event == 'SPELL_UPDATE_COOLDOWN' or event == 'SPELL_UPDATE_CHARGES' then
		pendingRearm = true
	end
	if pendingUpdate then return end
	pendingUpdate = true
	C_Timer.After(0, RunPending)
end)

-- Light poll to catch plate/ready transitions promptly. Only runs while the
-- module is enabled - ns.EUIIntApply (called by the options toggle and by
-- ApplyAllSettings on a profile switch) starts and stops it.
local pollTicker
local function SyncPollTicker()
	local want = GetCfg().enable and true or false
	if want and not pollTicker then
		pollTicker = C_Timer.NewTicker(0.2, function() UpdateInterrupt(false) end)
	elseif not want and pollTicker then
		pollTicker:Cancel()
		pollTicker = nil
	end
end
ns.EUIIntSyncPollTicker = SyncPollTicker
SyncPollTicker()