-- This decides what to do if detection is 'Automatic'
-- This is placed in ZoneController instead of the Zone object due to the ZoneControllers all-knowing group-minded logic
function ZoneController.updateDetection(zone)
	local detectionTypes = {
		["enterDetection"] = "_currentEnterDetection",
		["exitDetection"] = "_currentExitDetection",
	}
	for detectionType, currentDetectionName in pairs(detectionTypes) do
		local detection = zone[detectionType]
		if detection == enum.Detection.Automatic then
			if charactersTotalVolume > WHOLE_BODY_DETECTION_LIMIT then
				detection = enum.Detection.Centre
			else
				detection = enum.Detection.WholeBody
			end
		end
		zone[currentDetectionName] = detection
	end
end

function ZoneController._formHeartbeat(registeredTriggerType)
	local heartbeatConnection = heartbeatConnections[registeredTriggerType]
	if heartbeatConnection then return end
	-- This will only ever connect once per triggerType per server
	-- This means instead of initiating a loop per-zone we can handle everything within
	-- a singular connection. This is particularly beneficial for player-orinetated
	-- raycasting, where a raycast only needs to be cast once per interval, as apposed
	-- to every zone per interval
	-- I utilise heartbeat with os.clock() to provide precision (where needed) and flexibility
	local nextCheck = 0
	heartbeatConnection = heartbeat:Connect(function()
		local clockTime = os.clock()
		if clockTime >= nextCheck then
			local lowestAccuracy
			local lowestDetection
			for zone, _ in pairs(activeZones) do
				if zone.activeTriggers[registeredTriggerType] then
					local zAccuracy = zone.accuracy
					if lowestAccuracy == nil or zAccuracy < lowestAccuracy then
						lowestAccuracy = zAccuracy
					end
					ZoneController.updateDetection(zone)
					local zDetection = zone._currentEnterDetection
					if lowestDetection == nil or zDetection < lowestDetection then
						lowestDetection = zDetection
					end
				end
			end
			local highestAccuracy = lowestAccuracy
			local zonesAndOccupants = heartbeatActions[registeredTriggerType](lowestDetection)
			for zone, _ in pairs(activeZones) do
				if zone.activeTriggers[registeredTriggerType] then
					local zAccuracy = zone.accuracy
					local occupantsDict = zonesAndOccupants[zone] or {}
					local occupantsPresent = false
					for k,v in pairs(occupantsDict) do
						occupantsPresent = true
						break
					end
					if occupantsPresent and zAccuracy > highestAccuracy then
						highestAccuracy = zAccuracy
					end
					zone:_updateOccupants(registeredTriggerType, occupantsDict)
				end
			end
			----
			local cooldown = enum.Accuracy.getProperty(highestAccuracy)
			nextCheck = clockTime + cooldown
			----
		end
	end)
	heartbeatConnections[registeredTriggerType] = heartbeatConnection
end

function ZoneController._deregisterConnection(registeredZone, registeredTriggerType)
	activeConnections -= 1
	if activeTriggers[registeredTriggerType] == 1 then
		activeTriggers[registeredTriggerType] = nil
		local heartbeatConnection = heartbeatConnections[registeredTriggerType]
		if heartbeatConnection then
			heartbeatConnections[registeredTriggerType] = nil
			heartbeatConnection:Disconnect()
		end
	else
		activeTriggers[registeredTriggerType] -= 1
	end
	registeredZone.activeTriggers[registeredTriggerType] = nil
	if dictLength(registeredZone.activeTriggers) == 0 then
		activeZones[registeredZone] = nil
		ZoneController._updateZoneDetails()
	end
	if registeredZone.touchedConnectionActions[registeredTriggerType] then
		registeredZone:_disconnectTouchedConnection(registeredTriggerType)
	end
end

function ZoneController._updateZoneDetails()
	activeParts = {}
	activePartToZone = {}
	allParts = {}
	allPartToZone = {}
	activeZonesTotalVolume = 0
	for zone, _ in pairs(registeredZones) do
		local isActive = activeZones[zone]
		if isActive then
			activeZonesTotalVolume += zone.volume
		end
		for _, zonePart in pairs(zone.groupParts) do
			if isActive then
				table.insert(activeParts, zonePart)
				activePartToZone[zonePart] = zone
			end
			table.insert(allParts, zonePart)
			allPartToZone[zonePart] = zone
		end
	end
end

function ZoneController._getZonesAndPlayers(zonesDictToCheck, zoneCustomVolume, onlyActiveZones, recommendedDetection)
	local totalZoneVolume = zoneCustomVolume
	if not totalZoneVolume then
		for zone, _ in pairs(zonesDictToCheck) do
			totalZoneVolume += zone.volume
		end
	end
	local zonesAndOccupants = {}
	if charactersTotalVolume < totalZoneVolume then
		-- If the volume of all *characters* within the server is *less than* the total
		-- volume of all active zones (i.e. zones which listen for .playerEntered)
		-- then it's more efficient cast regions and rays within each character and
		-- then determine the zones they belong to
		for _, plr in pairs(players:GetPlayers()) do
			local touchingZones = ZoneController.getTouchingZones(plr, onlyActiveZones, recommendedDetection)
			for _, zone in pairs(touchingZones) do
				if not onlyActiveZones or zone.activeTriggers["player"] then
					fillOccupants(zonesAndOccupants, zone, plr)
				end
			end
		end
	else
		-- If the volume of all *active zones* within the server is *less than* the total
		-- volume of all characters, then it's more efficient to perform the region and raycast
		-- checks directly within each zone to determine players inside
		for zone, _ in pairs(zonesDictToCheck) do
			if not onlyActiveZones or zone.activeTriggers["player"] then
				local result = workspace:FindPartsInRegion3WithWhiteList(zone.region, bodyParts, #bodyParts)
				local playersDict = {}
				for _, bodyPart in pairs(result) do
					local parentName = bodyPart.Parent.Name
					if not playersDict[parentName] then
						playersDict[parentName] = players:GetPlayerFromCharacter(bodyPart.Parent)
					end
				end
				for _, plr in pairs(playersDict) do
					if plr and zone:findPlayer(plr) then
						fillOccupants(zonesAndOccupants, zone, plr)
					end
				end
			end
		end
	end
	return zonesAndOccupants
end