-- @author DoomTero 2026
-- @copyright 2026 DoomTero - ZomboRut original work. ref:b11c86a4
-- @license Proprietary. Not licensed for reuse, redistribution or derivative works without written permission from DoomTero. Ask - permission is obtainable. See ZomboRut/LICENSE.txt

local ZomboRutScene = require "ZomboRut/ZomboRutScene"

local DEBUG = true
local function log(fmt, ...)
	if DEBUG then DebugLog.log(string.format("[ZR Fade] " .. fmt, ...)) end
end

local CONE_LENGTH  = 4
local SCAN_RADIUS  = CONE_LENGTH + 1

local HYSTERESIS = 1

local MIN_HIDE_MS = 1000

local GRACE_HOLD_MS = 2000

local ORPHAN_TICKS = 300

local function featureEnabled()
	if not (SandboxVars and SandboxVars.ZomboRut) then return true end
	return SandboxVars.ZomboRut.hideSceneZombies ~= false
end

local tracked       = {}
local postHold      = {}
local postHoldCount = 0
local held          = {}

local frame = 0
Events.OnTick.Add(function() frame = frame + 1 end)

local floor = math.floor

local function inSouthCone(player, z, margin)
	margin = margin or 0
	if floor(z:getZ()) ~= floor(player:getZ()) then return false end
	local dx = floor(z:getX()) - floor(player:getX())
	local dy = floor(z:getY()) - floor(player:getY())
	if dx < -margin or dy < -margin then return false end
	local d = dx + dy
	return d >= 0 and d <= (CONE_LENGTH + margin)
end

local function isSceneParticipant(z, player, active)
	if active and z == active.liveOriginal then return true end
	if player.getGrapplingTarget and player:getGrapplingTarget() == z then return true end
	if z.getGrapplingTarget and z:getGrapplingTarget() == player then return true end
	return false
end

local function hide(z)
	if held[z] == nil then held[z] = { f = frame } else held[z].f = frame end
	pcall(function()
		z:setDoRender(false)
		if z.setAlpha       then z:setAlpha(0.0)       end
		if z.setTargetAlpha then z:setTargetAlpha(0.0) end
	end)
end

local function restore(z)
	held[z] = nil
	pcall(function()
		z:setDoRender(true)
		if z.setAlpha       then z:setAlpha(1.0)       end
		if z.setTargetAlpha then z:setTargetAlpha(1.0) end
	end)
end

local function touch(z)
	local e = held[z]
	if e then e.f = frame end
end

local function releaseAll(player, why, holdMs)
	local set = tracked[player]
	if not set then return end
	local now = getTimestampMs()
	local n = 0
	for z, rec in pairs(set) do
		rec.holdUntil = now + (holdMs or 0)
		if postHold[z] == nil then postHoldCount = postHoldCount + 1 end
		postHold[z] = rec
		n = n + 1
	end
	tracked[player] = nil
	if n > 0 then
		log("release (%s): %d zombie(s), restore in %dms unless reclaimed",
			tostring(why), n, holdMs or 0)
	end
end

ZomboRutScene.registerSceneUpdateCallback(function(player, active)
	if not player then return end

	if active and active.cfg and active.cfg.isUtility then
		releaseAll(player, "utility scene", 0)
		return
	end

	if not featureEnabled() then
		releaseAll(player, "feature off", 0)
		return
	end

	local cell  = getCell()
	local zList = cell and cell:getZombieList()
	if not zList then return end

	local set = tracked[player]
	if not set then set = {}; tracked[player] = set end
	local now = getTimestampMs()

	local shouldHide = {}
	for i = 0, zList:size() - 1 do
		local z = zList:get(i)
		if z and z:isAlive() and not isSceneParticipant(z, player, active)
			and z:DistTo(player) <= (SCAN_RADIUS + HYSTERESIS) then
			local margin = set[z] and HYSTERESIS or 0
			if inSouthCone(player, z, margin) then shouldHide[z] = true end
		end
	end

	for z in pairs(shouldHide) do
		local rec = set[z]
		if not rec then
			rec = postHold[z]
			if rec then
				postHold[z] = nil; postHoldCount = postHoldCount - 1
				rec.holdUntil = nil
			else
				rec = { since = now }
			end
			set[z] = rec
			hide(z)
		else
			touch(z)
		end
	end

	for z, rec in pairs(set) do
		if not shouldHide[z] then
			if (now - (rec.since or now)) >= MIN_HIDE_MS then
				set[z] = nil
				restore(z)
			else
				touch(z)
			end
		end
	end

	if DEBUG and active and (active.ticks or 0) % 30 == 0 then
		local n = 0
		for _ in pairs(set) do n = n + 1 end
		if n > 0 then log("cone: hiding %d zombie(s)", n) end
	end
end)

ZomboRutScene.registerSceneEndCallback(function(player, active, result)
	if not player then return end
	releaseAll(player, "scene end (" .. tostring(result) .. ")", GRACE_HOLD_MS)
end)

Events.OnPlayerDeath.Add(function(player)
	if player then releaseAll(player, "player death", 0) end
end)

ZR_SceneZombieFade = ZR_SceneZombieFade or {}

function ZR_SceneZombieFade.flushHold(why)
	if postHoldCount == 0 then return end
	local n = 0
	for _, rec in pairs(postHold) do
		rec.holdUntil = nil
		n = n + 1
	end
	log("hold flushed (%s): %d zombie(s) restore next tick", tostring(why), n)
end

Events.OnPlayerUpdate.Add(function(player)
	if postHoldCount == 0 then return end
	local now = getTimestampMs()
	for z, rec in pairs(postHold) do
		if rec.holdUntil and now < rec.holdUntil then
			touch(z)
		else
			postHold[z] = nil
			postHoldCount = postHoldCount - 1
			restore(z)
		end
	end
end)

Events.OnZombieUpdate.Add(function(zombie)
	local e = held[zombie]
	if e == nil then return end
	if (frame - e.f) > ORPHAN_TICKS then
		restore(zombie)
		return
	end
	if zombie.setAlpha       then zombie:setAlpha(0.0)       end
	if zombie.setTargetAlpha then zombie:setTargetAlpha(0.0) end
end)
