--core/manual_hide.lua, show/hide of Blizzard UI groups for the reductions (backend).
--the optimizer calls SetReduce: while the context gate is active, the enabled groups
--(minimap/buffs/raid frames/action bars) are hidden; otherwise they are shown. unprotected
--groups (minimap, buffs) change any time; protected groups (raid frames, action bars) can't
--be shown/hidden during combat, so an in-combat change is deferred and synced automatically
--on PLAYER_REGEN_ENABLED (combat end).

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

private.Core = private.Core or {}

---@class FPSPP_ManualHide
local ManualHide = {}
private.Core.ManualHide = ManualHide

--registry of hide-able groups. frameNames are resolved from _G at use time (missing ones
--are skipped); protected marks groups that cannot be touched in combat; label is shown in
--chat when an in-combat change is postponed.
local groups = {
	minimap = {
		protected = false,
		label = "Minimap",
		frameNames = {"MinimapCluster"},
	},
	buffs = {
		protected = false,
		label = "Buffs",
		frameNames = {"BuffFrame", "DebuffFrame"},
	},
	raid_frames = {
		protected = true,
		label = "Raid frames",
		frameNames = {"CompactRaidFrameContainer"},
	},
	action_bars = {
		protected = true,
		label = "Action bars",
		frameNames = {"MainMenuBar", "MultiBarBottomLeft", "MultiBarBottomRight", "MultiBarLeft", "MultiBarRight", "StanceBar", "PetActionBar"},
	},
}

--maps a group to the interface-section toggle (profile.ui.*) that also requests hiding it,
--on top of its own manual_hide.* toggle: a group hides while reducing if either toggle is on.
--every group is owned solely by this engine (including the minimap) so a single frame is never
--hidden by two systems at once -- that dual ownership left the minimap stuck hidden after an
--encounter, because the other engine hid the frame mid-fade and the fade never completed.
local interfaceToggle = {
	minimap = "disable_minimap",
	buffs = "disable_buffs",
	raid_frames = "disable_raid_frames",
	action_bars = "disable_action_bars",
}

--transient per-group state while hidden by us: [groupKey] = {shownFrames = {...}}.
ManualHide.state = {}

--frame used only to re-sync protected groups when combat ends.
local eventFrame = CreateFrame("Frame")

--resolves a group's existing frames from _G, skipping any that are absent on this client.
---@return table
local resolveFrames = function(groupKey)
	local list = {}
	for index, name in ipairs(groups[groupKey].frameNames) do
		local frame = _G[name]
		if frame then
			list[#list + 1] = frame
		end
	end

	return list
end

--name of the shared ticker that keeps our hidden frames hidden.
local ENFORCE_TICKER = "manual_hide_enforce"

--re-hides any frame that re-showed itself while we intend its group hidden. some Blizzard frames
--(notably the buff frame) re-show on their own aura updates, so a one-time Hide() does not stick.
--protected groups are skipped in combat (they can't be touched then; the combat-end sync covers them).
local enforceHidden = function()
	for groupKey in pairs(ManualHide.state) do
		if not (groups[groupKey].protected and InCombatLockdown()) then
			for index, frame in ipairs(resolveFrames(groupKey)) do
				if frame:IsShown() then
					frame:Hide()
				end
			end
		end
	end
end

--runs the 1s enforce watcher while at least one group is hidden by us, and stops it otherwise, so
--it costs nothing when nothing is hidden. driven off the generic core/ticker.lua registry.
local syncEnforceWatcher = function()
	if next(ManualHide.state) then
		if not private.Core.Ticker:IsRegistered(ENFORCE_TICKER) then
			private.Core.Ticker:Register(ENFORCE_TICKER, 1, enforceHidden)
		end
	else
		private.Core.Ticker:Unregister(ENFORCE_TICKER)
	end
end

--hides a group now (remembering which frames were visible) or shows the remembered ones.
local applyNow = function(groupKey, shouldHide)
	local state = ManualHide.state[groupKey]
	if shouldHide then
		if state then
			return
		end

		state = {shownFrames = {}}
		for index, frame in ipairs(resolveFrames(groupKey)) do
			--only frames that were actually visible get restored later, so an empty focus
			--frame or a disabled action bar stays hidden when we show the group back.
			if frame:IsShown() then
				state.shownFrames[#state.shownFrames + 1] = frame
			end

			frame:Hide()
		end

		ManualHide.state[groupKey] = state
	else
		if not state then
			return
		end

		for index, frame in ipairs(state.shownFrames) do
			frame:Show()
		end

		ManualHide.state[groupKey] = nil
	end

	--start/stop the re-show watcher to match whether anything is hidden now.
	syncEnforceWatcher()
end

--moves a group toward the desired state if allowed. returns false when blocked (a
--protected group cannot be changed while in combat).
---@return boolean applied
local tryApply = function(groupKey, shouldHide)
	local isHidden = ManualHide.state[groupKey] ~= nil
	if shouldHide == isHidden then
		return true
	end

	if groups[groupKey].protected and InCombatLockdown() then
		return false
	end

	applyNow(groupKey, shouldHide)
	return true
end

--applies the reduce gate to the frame groups: while reducing, hides each group whose Live
--Hide (manual_hide.*) or interface (ui.*) toggle is on; otherwise shows them all. silent --
--a protected group blocked in combat is re-synced on combat end. called by the optimizer.
---@param reduceNow boolean
function ManualHide:SetReduce(reduceNow)
	local profile = private.Core.Profile:Get()
	local manual = profile.manual_hide
	for groupKey in pairs(groups) do
		--either the group's own Live Hide toggle or its interface toggle requests the hide.
		local wantHide = manual[groupKey] == true
		local uiKey = interfaceToggle[groupKey]
		if not wantHide and uiKey then
			wantHide = profile.ui[uiKey] == true
		end

		tryApply(groupKey, reduceNow == true and wantHide)
	end
end

--immediately hides (shouldHide) or shows one group, bypassing the context gate -- used by the
--"Live FPS Test" window. shares ManualHide.state with the encounter path, so each frame has a single
--owner of its hidden state (no double-hide). protected groups (raid frames/action bars) cannot change
--in combat: in that case nothing changes and this prints a notice. returns whether it took effect now.
---@param groupKey string
---@param shouldHide boolean
---@return boolean applied
function ManualHide:SetGroupHidden(groupKey, shouldHide)
	if not groups[groupKey] then
		return false
	end

	local applied = tryApply(groupKey, shouldHide)
	if not applied then
		private.Print(groups[groupKey].label .. ": can't change protected frames during combat -- leave combat and try again.")
	end

	return applied
end

--returns whether the group is currently hidden by us.
---@param groupKey string
---@return boolean
function ManualHide:IsGroupHidden(groupKey)
	return self.state[groupKey] ~= nil
end

--shows every hide-able group now (used by the Live FPS Test window's restore button). a protected
--group blocked in combat stays as-is.
function ManualHide:ShowAllGroups()
	for groupKey in pairs(groups) do
		tryApply(groupKey, false)
	end
end

--registers the combat-end re-sync so protected groups that couldn't change in combat are
--applied once it ends. called once from OnInit.
function ManualHide:Initialize()
	eventFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
	--keep the handler short: re-apply the profile now that protected changes are allowed.
	eventFrame:SetScript("OnEvent", function()
		private.Core.Optimizer:ApplyProfile()
	end)
end
