--core/ui_disabler.lua, fades unprotected Blizzard UI out (and hides it) for boss
--encounters (backend). never destroys a frame; an element is hidden once it has fully
--faded out, so the client stops rendering it (the FPS win). ONLY UNPROTECTED frames may
--be registered here -- protected unit frames are never touched, because Hide()/Show() is
--blocked in combat and a boss fight is combat; to declutter unit frames the player uses
--the addon's own lightweight frames instead. driven by core/encounter.lua via the optimizer.

local _ = nil
local addonName, private = ...

local detailsFramework = DetailsFramework

private.Core = private.Core or {}

--constants
local FADE_DURATION = 0.5

---@class FPSPP_UIDisabler
local UIDisabler = {}
private.Core.UIDisabler = UIDisabler

--transient per-element state while hidden/fading: [elementKey] = {targetAlpha, hidden, suppressFinish}.
UIDisabler.state = {}

--persistent cache of one fade hub per element (reused across encounters): [elementKey] = {Hub, Animation}.
UIDisabler.fades = {}

--registry of disable-able elements. get() resolves the Blizzard frame lazily (it may be
--nil on some client flavors); label is shown in chat messages. every entry MUST be an
--unprotected frame (see the file header) -- the minimap is the only one today.
local elements = {
	minimap = {label = "Minimap", get = function() return MinimapCluster end},
}

--runs when a fade finishes: persists the final alpha, and after a full fade-out hides
--unprotected frames so no rendering work remains. skipped when the fade was stopped on
--purpose to reverse direction (suppressFinish).
local onFadeFinished = function(elementKey, frame)
	local elementState = UIDisabler.state[elementKey]
	if not elementState or elementState.suppressFinish then
		return
	end

	frame:SetAlpha(elementState.targetAlpha)
	if elementState.targetAlpha == 0 then
		--fully faded out: hide it so the client does no further rendering work (every
		--registered frame is unprotected, so Hide() is always safe -- even in combat).
		frame:Hide()
		elementState.hidden = true
	else
		--faded fully back in: drop the tracking entry.
		UIDisabler.state[elementKey] = nil
	end
end

--builds (once) and returns the cached fade hub + alpha animation for an element.
---@return table
local ensureFade = function(elementKey, frame)
	local cached = UIDisabler.fades[elementKey]
	if cached then
		return cached
	end

	local hub = detailsFramework:CreateAnimationHub(frame, nil, function()
		onFadeFinished(elementKey, frame)
	end)
	cached = {Hub = hub, Animation = detailsFramework:CreateAnimation(hub, "Alpha", 1, FADE_DURATION, 1, 0)}
	UIDisabler.fades[elementKey] = cached
	return cached
end

--starts (or reverses) a fade on the element toward toAlpha over FADE_DURATION.
local startFade = function(elementKey, frame, toAlpha)
	local fade = ensureFade(elementKey, frame)
	local elementState = UIDisabler.state[elementKey]

	--stop any in-flight fade without running its finish side effects (we are reversing).
	elementState.suppressFinish = true
	fade.Hub:Stop()
	elementState.suppressFinish = false

	local fromAlpha = frame:GetAlpha()
	elementState.targetAlpha = toAlpha
	fade.Animation:SetFromAlpha(fromAlpha)
	fade.Animation:SetToAlpha(toAlpha)
	frame:SetAlpha(fromAlpha)
	fade.Hub:Play()
end

--fades an element out for the duration of a boss encounter. no-op if already out.
---@param elementKey string
function UIDisabler:HideWithFade(elementKey)
	local frame = elements[elementKey].get()
	if not frame then
		return
	end

	local elementState = self.state[elementKey]
	--already hidden or fading out -> nothing to do.
	if elementState and elementState.targetAlpha == 0 then
		return
	end

	self.state[elementKey] = elementState or {}
	startFade(elementKey, frame, 0)
end

--shows an element (if it was hidden) and fades it back in. no-op if it was never hidden.
---@param elementKey string
function UIDisabler:ShowWithFade(elementKey)
	local elementState = self.state[elementKey]
	--never touched, or already fading back in -> nothing to do.
	if not elementState or elementState.targetAlpha == 1 then
		return
	end

	local frame = elements[elementKey].get()
	if not frame then
		self.state[elementKey] = nil
		return
	end

	--re-show frames we actually hid before fading them back into view.
	if elementState.hidden then
		frame:Show()
		elementState.hidden = false
	end

	startFade(elementKey, frame, 1)
end

--instantly restores every element this module has touched (no fade). used on profile
--switch and addon disable.
function UIDisabler:RestoreAll()
	--setting an existing key to nil during traversal is allowed in Lua, so iterating
	--with pairs while clearing each entry is safe.
	for elementKey, elementState in pairs(self.state) do
		local fade = self.fades[elementKey]
		if fade then
			--stop any in-flight fade without firing its finish side effects.
			elementState.suppressFinish = true
			fade.Hub:Stop()
		end

		local frame = elements[elementKey].get()
		if frame then
			--every registered frame is unprotected, so Show() is always combat-safe.
			if elementState.hidden then
				frame:Show()
			end

			frame:SetAlpha(1)
		end

		self.state[elementKey] = nil
	end
end

--returns the element registry (read-only use by the optimizer and docs).
---@return table
function UIDisabler:GetElements()
	return elements
end
