-- Helper methods:

local ignoreEmitterList = { Emitter }

local raycastFunctions = {
	[CollisionMode.None] = function(ray, ignoreCharacter)
		return workspace:FindPartOnRayWithIgnoreList(ray, ignoreCharacter and {Emitter, Players.LocalPlayer and Players.LocalPlayer.Character} or ignoreEmitterList)
	end;
	[CollisionMode.Blacklist] = function(ray)
		return workspace:FindPartOnRayWithIgnoreList(ray, collisionList)
	end;
	[CollisionMode.Whitelist] = function(ray)
		return workspace:FindPartOnRayWithWhitelist(ray, collisionList)
	end;
	[CollisionMode.Function] = function(ray)
		local destination = ray.Origin + ray.Direction
		-- draw multiple raycasts concatenated to each other until no hit / valid hit found
		while ray.Direction.magnitude > 0.001 do
			local part, pos, norm, mat = workspace:FindPartOnRayWithIgnoreList(ray, ignoreEmitterList)
			if not part or collisionFunc(part) then
				return part, pos, norm, mat
			end
			local start = pos + ray.Direction.Unit * 0.001
			ray = Ray.new(start, destination - start)
		end
	end;
}
local raycast = raycastFunctions[collisionMode]

local function connectLoop()
	
	local rand = Random.new()
	
	local inside = true					-- Whether camera is currently in a spot occluded from the sky
	local frame = RAIN_UPDATE_PERIOD	-- Frame counter, and force update cycle right now
	
	-- Update Emitter on RenderStepped since it needs to be synced to Camera
	table.insert(connections, RunService.RenderStepped:connect(function()
		
		-- Check if camera is outside or inside
		local part, position = raycast(Ray.new(workspace.CurrentCamera.CFrame.p, -rainDirection * RAIN_SCANHEIGHT), true)
		
		if (not currentCeiling or workspace.CurrentCamera.CFrame.p.y <= currentCeiling) and not part then
			
			-- Camera is outside and under ceiling
				
			if volumeTarget < 1 and not disabled then
				volumeTarget = 1
				TweenService:Create(Sound, TweenInfo.new(.5), {Volume = 1}):Play()
			end
			
			frame = RAIN_UPDATE_PERIOD
			
			local t = math.abs(workspace.CurrentCamera.CFrame.lookVector:Dot(rainDirection))
			
			local center = workspace.CurrentCamera.CFrame.p
			local right = workspace.CurrentCamera.CFrame.lookVector:Cross(-rainDirection)
			right = right.magnitude > 0.001 and right.unit or -rainDirection
			local forward = rainDirection:Cross(right).unit
			
			Emitter.Size = v3(
				RAIN_EMITTER_DIM_DEFAULT,
				RAIN_EMITTER_DIM_DEFAULT,
				RAIN_EMITTER_DIM_DEFAULT + (1 - t)*(RAIN_EMITTER_DIM_MAXFORWARD - RAIN_EMITTER_DIM_DEFAULT)
			)
			
			Emitter.CFrame =
				CFrame.new(
					center.x, center.y, center.z,
					right.x, -rainDirection.x, forward.x,
					right.y, -rainDirection.y, forward.y,
					right.z, -rainDirection.z, forward.z
				)
				+ (1 - t) * workspace.CurrentCamera.CFrame.lookVector * Emitter.Size.Z/3
				- t * rainDirection * RAIN_EMITTER_UP_MODIFIER
			
			Emitter.RainStraight.Enabled = true
			Emitter.RainTopDown.Enabled = true
			
			inside = false
			
		else
			
			-- Camera is inside / above ceiling
			
			Emitter.RainStraight.Enabled = false
			Emitter.RainTopDown.Enabled = false
		
			inside = true
			
		end
		
	end))

	-- Do the other effects on Stepped
	local signal = RunService:IsRunning() and RunService.Stepped or RunService.RenderStepped
	table.insert(connections, signal:connect(function()
		
		frame = frame + 1
		
		-- Only do some updates once every few frames
		if frame >= RAIN_UPDATE_PERIOD then
			
			-- Measure of how much camera is facing down (0-1)
			local t = math.abs(workspace.CurrentCamera.CFrame.lookVector:Dot(rainDirection))
			
			-- More looking down = see straight particles less and see top-down particles more
			local straightSequence = NumberSequence.new {
				NSK010;
				NumberSequenceKeypoint.new(RAIN_TRANSPARENCY_T1, (1 - t)*straightLowAlpha + t, 0);
				NumberSequenceKeypoint.new(RAIN_TRANSPARENCY_T2, (1 - t)*straightLowAlpha + t, 0);
				NSK110;
			}
			local topdownSequence = NumberSequence.new {
				NSK010;
				NumberSequenceKeypoint.new(RAIN_TRANSPARENCY_T1, t*topdownLowAlpha + (1 - t), 0);
				NumberSequenceKeypoint.new(RAIN_TRANSPARENCY_T2, t*topdownLowAlpha + (1 - t), 0);
				NSK110;
			}
			
			-- Find desired rotation for the straight rain particles
			local mapped = workspace.Camera.CFrame:inverse() * (workspace.Camera.CFrame.p - rainDirection)
			local straightRotation = NumberRange.new(math.deg(math.atan2(-mapped.x, mapped.y)))
			
			if inside then
				
				-- Update emitter properties
				for _,v in pairs(rainAttachments) do
					v.RainStraight.Transparency = straightSequence
					v.RainStraight.Rotation = straightRotation
					v.RainTopDown.Transparency = topdownSequence
				end
				
				if not disabled then
					
					-- Only do occluded volume check if not moving towards disabled state
					
					local volume = 0
					
					if (not currentCeiling or workspace.CurrentCamera.CFrame.p.y <= currentCeiling) then
						
						-- Check how far away camera is from a space open to the sky using volume scan grid
						
						local minDistance = RAIN_VOLUME_SCAN_RADIUS
						local rayDirection = -rainDirection * RAIN_SCANHEIGHT
						
						for i = 1, #volumeScanGrid do -- In order, so first hit is closest
							if not raycast(Ray.new(workspace.CurrentCamera.CFrame * volumeScanGrid[i], rayDirection), true) then
								minDistance = volumeScanGrid[i].magnitude
								break
							end
						end
						
						-- Volume is inversely proportionate to minimum distance
						volume = 1 - minDistance / RAIN_VOLUME_SCAN_RADIUS
						
					end
					
					if math.abs(volume - volumeTarget) > .01 then
						-- Value is sufficiently different from previous target, overwrite it
						volumeTarget = volume
						TweenService:Create(Sound, TweenInfo.new(1), {Volume = volumeTarget}):Play()
					end
					
				end
				
			else
				
				-- Update emitter properties
				Emitter.RainStraight.Transparency = straightSequence
				Emitter.RainStraight.Rotation = straightRotation
				Emitter.RainTopDown.Transparency = topdownSequence
				
			end
			
			-- Reset frame counter
			frame = 0
			
		end
		
		local center = workspace.CurrentCamera.CFrame.p
		local right = workspace.CurrentCamera.CFrame.lookVector:Cross(-rainDirection)
		right = right.magnitude > 0.001 and right.unit or -rainDirection
		local forward = rainDirection:Cross(right).unit
		local transform = CFrame.new(
			center.x, center.y, center.z,
			right.x, -rainDirection.x, forward.x,
			right.y, -rainDirection.y, forward.y,
			right.z, -rainDirection.z, forward.z
		)
		local rayDirection = rainDirection * RAIN_OCCLUDECHECK_SCAN_Y
		
		-- Splash and occlusion effects
		for i = 1, numSplashes do
			
			local splashAttachment = splashAttachments[i]
			local rainAttachment = rainAttachments[i]
			
			-- Sample random splash position
			local x = rand:NextNumber(RAIN_OCCLUDECHECK_OFFSET_XZ_MIN, RAIN_OCCLUDECHECK_OFFSET_XZ_MAX)
			local z = rand:NextNumber(RAIN_OCCLUDECHECK_OFFSET_XZ_MIN, RAIN_OCCLUDECHECK_OFFSET_XZ_MAX)
			local part, position, normal = raycast(Ray.new(transform * v3(x, RAIN_OCCLUDECHECK_OFFSET_Y, z), rayDirection))
			
			if part then
				
				-- Draw a splash at hit
				splashAttachment.Position = position + normal * RAIN_SPLASH_CORRECTION_Y
				splashAttachment.RainSplash:Emit(1)
				
				if inside then
					
					-- Draw occlusion rain particles a little bit above the splash position
					local corrected = position - rainDirection * RAIN_SPLASH_STRAIGHT_OFFSET_Y
					if currentCeiling and corrected.Y > currentCeiling and rainDirection.Y < 0 then
						corrected = corrected + rainDirection * (currentCeiling - corrected.Y) / rainDirection.Y
					end
					rainAttachment.CFrame = transform - transform.p + corrected
					rainAttachment.RainStraight:Emit(intensityOccludedRain)
					rainAttachment.RainTopDown:Emit(intensityOccludedRain)
					
				end
				
			elseif inside then
				
				-- Draw occlusion rain particles on the XZ-position at around the camera's height
				local corrected = transform * v3(x, rand:NextNumber(RAIN_NOSPLASH_STRAIGHT_OFFSET_Y_MIN, RAIN_NOSPLASH_STRAIGHT_OFFSET_Y_MAX), z)
				if currentCeiling and corrected.Y > currentCeiling and rainDirection.Y < 0 then
					corrected = corrected + rainDirection * (currentCeiling - corrected.Y) / rainDirection.Y
				end
				rainAttachment.CFrame = transform - transform.p + corrected
				rainAttachment.RainStraight:Emit(intensityOccludedRain)
				rainAttachment.RainTopDown:Emit(intensityOccludedRain)
				
			end
			
		end
		
	end))

end

local function disconnectLoop()
	-- If present, disconnect all RunService connections
	if #connections > 0 then
		for _,v in pairs(connections) do
			v:disconnect()
		end
		connections = {}
	end
end

local function disableSound(tweenInfo)
	
	-- Tween the rain sound to be mute over a given easing function
	volumeTarget = 0
	local tween = TweenService:Create(Sound, tweenInfo, {Volume = 0})
	tween.Completed:connect(function(state)
		if state == Enum.PlaybackState.Completed then
			Sound:Stop()
		end
		tween:Destroy()
	end)
	tween:Play()
	
end

local function disable()
	
	disconnectLoop()
	
	-- Hide Emitter
	Emitter.RainStraight.Enabled = false
	Emitter.RainTopDown.Enabled = false
	Emitter.Size = MIN_SIZE
	
	-- Disable sound now if not tweened into disabled state beforehand
	if not disabled then
		disableSound(TweenInfo.new(RAIN_SOUND_FADEOUT_TIME))
	end
	
end