--core/optimizer.lua, coordinates applying and removing optimizations (backend).
--the only module that maps profile keys to engine actions. UI code never touches
--the engines directly; it flips a profile value and calls ApplyProfile.

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

private.Core = private.Core or {}

--WoW global that returns the current Lua call stack as a string; used only by the /fpspp trace
--diagnostic below. the LuaLS definitions omit it, so silence the false "undefined" warning.
---@diagnostic disable-next-line: undefined-field
local debugstack = _G.debugstack

---@class FPSPP_Optimizer
local Optimizer = {}
private.Core.Optimizer = Optimizer

--live-test overrides for the "Live FPS Test" window: which managed graphics CVars the player has
--reduced on the spot, bypassing the context gate. tracked here (not in the profile) because these
--are transient debug overrides -- the window re-reads this each time it opens. [fieldName] = true
--while the live test has that CVar reduced.
Optimizer.liveCVarActive = {}

--maps a profile.graphics.* key to the CVars it sets when enabled. names/values verified against
--warcraft.wiki.gg: graphicsParticleDensity is 0-5 default 4 (1 = Low), graphicsShadowQuality is 0-5
--default 3 (0 = Low, the minimum). all reversible via the CVars engine; SetCVar fails safe on an
--unknown name. (view distance / farclip is now owned solely by the Far Clip slider -- see the
--CVarSlider widget in ui/widgets.lua -- so it is intentionally not a context-gated feature here.)
local graphicsFeatureMap = {
	reduce_particle_density = {graphicsParticleDensity = 1},
	reduce_shadows = {graphicsShadowQuality = 0},
}

--graphics CVars the addon can drop to a reduced value. each is exposed as its own toggle
--(graphics_cvars.*) and is dropped while its toggle is on and the context gate is active
--(see core/context.lua). the table key is the profile field. an
--entry is either single-CVar ({cvar, reduced}) or multi-CVar ({cvars = {name = value}}),
--the latter for a setting that bundles several CVars together (e.g. Liquid Detail also zeroes raid
--water detail, Fog also zeroes raid weather, anti_aliasing zeroes both MSAA and the post-process AA
--mode). MSAA is the expensive AA one (up to ~25 FPS at 8x), post-process AA is nearly free.
--NOTE: the anti-aliasing CVar names should be verified in-client -- if a name is wrong,
--SetCVar simply no-ops (see core/cvars.lua), so a stale name is harmless but ineffective.
local managedGraphicsCVars = {
	--"Liquid Detail": drops graphicsLiquidDetail to 0 AND raid water detail (RAIDWaterDetail) to 0
	--together -- raid water was combined in here, so reducing liquid detail also disables it.
	liquid_detail = {cvars = {graphicsLiquidDetail = 0, RAIDWaterDetail = 0}},
	--"Shadows": disables shadows AND ambient occlusion (SSAO) together -- SSAO was combined in here so
	--turning shadows off also turns it off. graphicsShadowQuality is 0-5 (default 3, 0 = off); also
	--driven by the context-gated "Reduce Shadows" toggle, which shares graphicsShadowQuality.
	shadow_quality = {cvars = {graphicsShadowQuality = 0, SSAO = 0}},
	--"Fog": disables depth-of-field/fog (graphicsDepthEffects) AND raid weather (RAIDweatherDensity)
	--together -- raid weather was combined in here, so removing fog also disables it.
	depth_effects = {cvars = {graphicsDepthEffects = 0, RAIDweatherDensity = 0}},
	outline_mode = {cvar = "graphicsOutlineMode", reduced = 0},
	spell_density = {cvar = "graphicsSpellDensity", reduced = 0},
	environment_detail = {cvar = "graphicsEnvironmentDetail", reduced = 0},
	ground_clutter = {cvar = "graphicsGroundClutter", reduced = 0},
	--"View Distance": snaps view distance to its minimum -- far clip 1500, the graphicsViewDistance
	--quality to 0, and terrain LOD distance to 200, together. big CPU relief in cities and raids. drives
	--the same CVars as the (now disabled) Far Clip and Terrain LOD sliders.
	--NOTE: verify graphicsViewDistance in-client; SetCVar no-ops on an unknown name (harmless).
	view_distance = {cvars = {farclip = 1500, graphicsViewDistance = 0, terrainLodDist = 200}},
	anti_aliasing = {cvars = {MSAAQuality = 0, ffxAntiAliasingMode = 0}},
}

--reduces one managed entry: sets its single CVar, or every CVar in its multi-CVar map.
---@param info table
local setManagedEntry = function(info)
	if info.cvars then
		for cvarName, cvarValue in pairs(info.cvars) do
			private.Core.CVars:Set(cvarName, cvarValue)
		end
	else
		private.Core.CVars:Set(info.cvar, info.reduced)
	end
end

--restores one managed entry: restores its single CVar, or every CVar in its multi-CVar map.
---@param info table
local restoreManagedEntry = function(info)
	if info.cvars then
		for cvarName in pairs(info.cvars) do
			private.Core.CVars:Restore(cvarName)
		end
	else
		private.Core.CVars:Restore(info.cvar)
	end
end

--sets a feature's CVars when enabled, restores them when not.
---@param isEnabled boolean
---@param cvars table
local applyGraphicsFeature = function(isEnabled, cvars)
	if isEnabled then
		for cvarName, cvarValue in pairs(cvars) do
			private.Core.CVars:Set(cvarName, cvarValue)
		end
	else
		for cvarName in pairs(cvars) do
			private.Core.CVars:Restore(cvarName)
		end
	end
end

--applies the active profile. every reduction is gated by the context: it is applied only
--while the addon is enabled AND a chosen context is active (see core/context.lua), and
--restored otherwise. re-run on every context change, encounter event, and toggle. idempotent.
function Optimizer:ApplyProfile()
	local profile = private.Core.Profile:Get()

	--master switch off: restore everything (when configured) and stop.
	if profile.enabled ~= true then
		if profile.safety.restore_on_disable ~= false then
			self:RestoreAll()
		end

		return
	end

	--the single gate: reduce now only while a chosen context is active.
	local reduceNow = private.Core.Context:IsActive()

	--graphics reductions: set each feature's CVars while reducing and its toggle is on.
	for profileKey, cvars in pairs(graphicsFeatureMap) do
		applyGraphicsFeature(reduceNow == true and profile.graphics[profileKey] == true, cvars)
	end

	--managed graphics CVars: applied while EITHER the context-gated toggle (main options panel, profile
	--.graphics_cvars.*, on during a chosen context) OR the live-preview toggle (Live FPS Test window,
	--liveCVarActive) wants it; restored only when neither does. this lets the same CVar be both an
	--encounter reduction and a live preview without the two undoing each other.
	for fieldName, info in pairs(managedGraphicsCVars) do
		local contextWants = reduceNow == true and profile.graphics_cvars[fieldName] == true
		if contextWants or self:IsLiveCVarActive(fieldName) then
			setManagedEntry(info)
		else
			restoreManagedEntry(info)
		end
	end

	--frame hides (minimap/buffs/raid frames/action bars): the manual-hide engine hides the
	--enabled groups while reducing and shows them otherwise (protected ones defer in combat).
	private.Core.ManualHide:SetReduce(reduceNow)

	--"Hide Blizzard Damage Meter": disable Blizzard's built-in meter (damageMeterEnabled = "0") only
	--while reducing and its toggle is on; otherwise restore the player's original value. context-gated
	--like the frame hides above, and reversible through the CVars engine so Restore All / disable /
	--context end puts it back (Restore is a no-op when the addon never touched it).
	if reduceNow == true and profile.ui.disable_damage_meter == true then
		private.Core.CVars:Set("damageMeterEnabled", "0")
	else
		private.Core.CVars:Restore("damageMeterEnabled")
	end

	--party/raid unit frames: NOT a reduction (it adds frames); driven by its own toggle only.
	private.Core.UnitFrames:SetEnabled(profile.unit_frames.enabled == true)
end

--prints a diagnostic snapshot of the hide state to chat (via /fpspp status). used to debug
--reports of "it hid my UI but nothing is enabled": shows the active profile, whether the context
--gate is open, which hide toggles are on, and what is actually hidden right now -- so a hide that
--the main panel does not explain (e.g. a leftover from the Live FPS Test window) is visible.
function Optimizer:PrintStatus()
	local profile = private.Core.Profile:Get()

	private.Print("status -- profile:", private.Core.Profile:GetCurrentName(), "| enabled:", tostring(profile.enabled))
	private.Print("context gate active (would reduce now):", tostring(private.Core.Context:IsActive()))

	local contextOn = {}
	for key, value in pairs(profile.context) do
		if value == true then
			contextOn[#contextOn + 1] = key
		end
	end
	private.Print("context toggles ON:", #contextOn > 0 and table.concat(contextOn, ", ") or "(none)")

	local hideOn = {}
	for key, value in pairs(profile.ui) do
		if value == true then
			hideOn[#hideOn + 1] = "ui." .. key
		end
	end
	for key, value in pairs(profile.manual_hide) do
		if value == true then
			hideOn[#hideOn + 1] = "manual_hide." .. key
		end
	end
	private.Print("hide toggles ON:", #hideOn > 0 and table.concat(hideOn, ", ") or "(none)")

	local hiddenNow = {}
	for index, key in ipairs({"minimap", "buffs", "raid_frames", "action_bars"}) do
		if private.Core.ManualHide:IsGroupHidden(key) then
			hiddenNow[#hiddenNow + 1] = key
		end
	end
	private.Print("groups the addon is hiding RIGHT NOW:", #hiddenNow > 0 and table.concat(hiddenNow, ", ") or "(none)")

	--the ACTUAL Blizzard frame state. if a frame reads shown=false while "hiding RIGHT NOW" is
	--(none), then something OUTSIDE this addon hid it -- run /fpspp trace to find out what.
	local frameReport = {}
	for index, name in ipairs({"MinimapCluster", "BuffFrame", "DebuffFrame"}) do
		local frame = _G[name]
		if frame then
			frameReport[#frameReport + 1] = string.format("%s shown=%s alpha=%.2f", name, tostring(frame:IsShown()), frame:GetAlpha())
		end
	end
	private.Print("actual frame state:", table.concat(frameReport, " | "))
end

--installs a one-time trace (via /fpspp trace) that prints a stack trace whenever the minimap or
--buff frames are hidden. use it when FPS++'s own state says it is hiding nothing but the frames
--still vanish: the printed file paths name the addon actually calling Hide/SetShown. /reload removes it.
function Optimizer:InstallHideTrace()
	if self.hideTraceInstalled then
		private.Print("hide trace is already running -- reproduce the problem and watch chat.")
		return
	end
	self.hideTraceInstalled = true

	--the hook fires AFTER the real Hide/SetShown, so debugstack(2, ...) points at the caller.
	local onHide = function(frameName)
		return function()
			private.Print(frameName .. ":Hide() called from:\n" .. debugstack(2, 8, 0))
		end
	end
	local onSetShown = function(frameName)
		return function(_, shown)
			if not shown then
				private.Print(frameName .. ":SetShown(false) called from:\n" .. debugstack(2, 8, 0))
			end
		end
	end

	for index, frameName in ipairs({"MinimapCluster", "Minimap", "BuffFrame", "DebuffFrame"}) do
		local frame = _G[frameName]
		if frame then
			hooksecurefunc(frame, "Hide", onHide(frameName))
			hooksecurefunc(frame, "SetShown", onSetShown(frameName))
		end
	end

	private.Print("hide trace installed. Trigger the problem (enter the encounter); each hide prints where it came from. /reload to stop.")
end

--restores all Blizzard UI and CVar changes made this session, and turns the custom unit frames off.
function Optimizer:RestoreAll()
	private.Core.UIDisabler:RestoreAll()
	private.Core.CVars:RestoreAll()
	private.Core.ManualHide:SetReduce(false)
	private.Core.UnitFrames:SetEnabled(false)
	--the live-test CVars were restored by CVars:RestoreAll above; drop their override flags so the
	--Live FPS Test window reflects reality (nothing reduced) after a full restore.
	wipe(self.liveCVarActive)
end

--returns whether the live test currently has this managed graphics CVar reduced.
---@param fieldName string
---@return boolean
function Optimizer:IsLiveCVarActive(fieldName)
	return self.liveCVarActive[fieldName] == true
end

--immediately reduces (enabled) or restores (not enabled) one managed graphics CVar, bypassing the
--context gate -- the "Live FPS Test" window drives this so the player can feel a single setting's
--impact on the spot. unknown field names are ignored.
---@param fieldName string
---@param enabled boolean
function Optimizer:SetLiveCVar(fieldName, enabled)
	local info = managedGraphicsCVars[fieldName]
	if not info then
		return
	end

	if enabled then
		setManagedEntry(info)
		self.liveCVarActive[fieldName] = true
	else
		restoreManagedEntry(info)
		self.liveCVarActive[fieldName] = nil
	end
end

--restores every managed graphics CVar the live test reduced (leaving context-driven CVars alone).
--used by the Live FPS Test window's restore button.
function Optimizer:RestoreLiveCVars()
	for fieldName in pairs(self.liveCVarActive) do
		restoreManagedEntry(managedGraphicsCVars[fieldName])
	end

	wipe(self.liveCVarActive)
end
