-- Global update loop on heartbeat
-- (for performance / user-friendliness reasons, it is better to do a global loop over all
-- objects at Heartbeat rather than exposing an API method to update individual objects)
local function onHeartbeat()
	-- Cache camera-related values and references
	local camera = workspace.CurrentCamera
	local camPos = camera.CFrame.p
	local look = camera.CFrame.lookVector

	local fov = Constants.FOV_FUDGE_FACTOR * rad(camera.FieldOfView * camera.ViewportSize.x / camera.ViewportSize.y) / 2.0

	for v, _ in pairs(dynamicInstances) do
		local source = v.source.WorldPosition
		local axis = v.drain.WorldPosition - source
		if axis.magnitude < 0.001 then
			axis = Vector3.new(0, 0, -0.001)
		end
		v.length = axis.magnitude
		v.cframe = Util.makeOrientation(source, axis)
		v.part.CFrame = v.cframe
		v.part.Emitter.CFrame = CFrame.new(v.length/2, 0, 0)
		v.part.Emitter.PointLight.Range = v.length
	end

	-- Check if objects are on/off-screen
	for v, _ in pairs(arcInstances) do
		local camToSource = v.cframe.p - camPos
		local camToDrain = v.cframe * vec3(v.length, 0, 0) - camPos

		-- Check if on-screen:
		if acos(dot(camToSource.unit, look)) > fov and acos(dot(camToDrain.unit, look)) > fov then
			-- Else, make visible and reparent, if not already done
			-- Not on-screen, turn invisible and unparent if not already
			if v.visible then
				v.visible = false
				v.segmentsFolder.Parent = nil
			end
		elseif not v.visible then
			v.visible = true
			v.segmentsFolder.Parent = MainFolder
		end

		-- Set average distance to camera for throttling purposes
		v.distance = (camToSource.magnitude + camToDrain.magnitude) / 2
	end

	-- Check if objects should be updated and do so
	for v, _ in pairs(arcInstances) do
		-- Only update enabled and visible objects
		if v.visible then
			-- Update CFrame/Size of instances if object values were changed
			if v.changed then
				v.part.CFrame = v.cframe
				v.part.Emitter.CFrame = CFrame.new(v.length/2, 0, 0)
				v.part.Emitter.PointLight.Range = v.length
				v.changed = false
			end

			-- Update amount of arcs that can be rendered in this step (can be real, not just integer)
			if v.distance < Constants.MAX_DISTANCE then
				v.arcRenderAmount = v.arcRenderAmount + min(
					v.numArcs / 3,
					Constants.ARCS_PER_UPDATE * min(1, throttleDistanceModifier / v.distance)
				)
				updateArc(v)
			end
		end
	end

	-- Increase frame count
	frameCount = frameCount + 1

	-- Calculate time since last throttling check
	local deltaTime = tick() - frameTick

	-- Check if auto-throttling window has passed
	if deltaTime > Constants.AUTO_THROTTLE_FRAME_INTERVAL then
		-- Update auto-throttling ratio based on observed FPS w.r.t. target FPS
		autoThrottleRatio = max(0, min(1, autoThrottleRatio + Constants.AUTO_THROTTLE_INCREMENT
				* (frameCount / deltaTime - Constants.AUTO_THROTTLE_TARGET)))

		-- Update variables dependent on auto-throttling rate
		segmentPerArc = floor(Constants.SEGMENT_PER_ARC_MIN + (difSegmentsPerArc * autoThrottleRatio) + 0.5)
		throttleDistanceModifier = Constants.THROTTLE_DISTANCE_MODIFIER_MIN
			+ (difDistanceModifier * autoThrottleRatio)

		-- Reset tracking variables
		frameCount = 0
		frameTick = tick()
	end
end

local function updateConnection()
	if numInstances <= 0 then
		if heartbeatConnection then
			heartbeatConnection:Disconnect()
			heartbeatConnection = nil
		end
	else
		if not heartbeatConnection then
			heartbeatConnection = RunService.Heartbeat:Connect(onHeartbeat)
		end
	end
end