-- Simulate a raycast by one tick.


local function SimulateCast(cast: ActiveCast, delta: number, expectingShortCall: boolean)
	assert(cast.StateInfo.UpdateConnection ~= nil, ERR_OBJECT_DISPOSED)
	PrintDebug("Casting for frame.")
	local latestTrajectory = cast.StateInfo.Trajectories[#cast.StateInfo.Trajectories]
	
	local origin = latestTrajectory.Origin
	local totalDelta = cast.StateInfo.TotalRuntime - latestTrajectory.StartTime
	local initialVelocity = latestTrajectory.InitialVelocity
	local acceleration = latestTrajectory.Acceleration
	
	local lastPoint = GetPositionAtTime(totalDelta, origin, initialVelocity, acceleration)
	local lastVelocity = GetVelocityAtTime(totalDelta, initialVelocity, acceleration)
	local lastDelta = cast.StateInfo.TotalRuntime - latestTrajectory.StartTime
	
	cast.StateInfo.Delta = delta
	cast.StateInfo.TotalRuntime += delta
	cast.StateInfo.TotalRuntime2 += delta
	
	-- Recalculate this.
	totalDelta = cast.StateInfo.TotalRuntime - latestTrajectory.StartTime
		
	local currentTarget = GetPositionAtTime(totalDelta, origin, initialVelocity, acceleration)
	local segmentVelocity = GetVelocityAtTime(totalDelta, initialVelocity, acceleration) 
	local totalDisplacement = currentTarget - lastPoint -- This is the displacement from where the ray was on the last from to where the ray is now.
	
	local rayDir = totalDisplacement.Unit * segmentVelocity.Magnitude * delta
	local targetWorldRoot = cast.RayInfo.WorldRoot
	local resultOfCast
	
	local point = currentTarget
	local part: Instance? = nil
	local material = Enum.Material.Air
	local normal = Vector3.new()
		
	if cast.RayInfo.RaycastHitbox then
		for _, v in next, cast.RayInfo.RaycastHitbox do
			resultOfCast = targetWorldRoot:Raycast((cast.RayInfo.CurrentCFrame * v).p, rayDir, cast.RayInfo.Parameters)
			if (resultOfCast ~= nil) then
				point = resultOfCast.Position
				part = resultOfCast.Instance
				material = resultOfCast.Material
				normal = resultOfCast.Normal
				break
			end
		end
	else
		resultOfCast = targetWorldRoot:Raycast(lastPoint, rayDir, cast.RayInfo.Parameters)
		if (resultOfCast ~= nil) then
			point = resultOfCast.Position
			part = resultOfCast.Instance
			material = resultOfCast.Material
			normal = resultOfCast.Normal
		end
	end
	
	local rayDisplacement = ((cast.RayInfo.RaycastHitbox and currentTarget or point) - lastPoint).Magnitude
	-- For clarity -- totalDisplacement is how far the ray would have traveled if it hit nothing,
	-- and rayDisplacement is how far the ray really traveled (which will be identical to totalDisplacement if it did indeed hit nothing)
	
	SendLengthChanged(cast, (cast.RayInfo.RaycastHitbox and currentTarget or point), lastPoint, rayDir.Unit, rayDisplacement, segmentVelocity, cast.RayInfo.CosmeticBulletObject)
	cast.StateInfo.DistanceCovered += rayDisplacement
	
	local rayVisualization: ConeHandleAdornment? = nil
	if (delta > 0) then
		rayVisualization = DbgVisualizeSegment(CFrame.new(lastPoint, lastPoint + rayDir), rayDisplacement)
	end
	
	-- HIT DETECTED. Handle all that garbage, and also handle behaviors 1 and 2 (default behavior, go high res when hit) if applicable.
	-- CAST BEHAVIOR 2 IS HANDLED IN THE CODE THAT CALLS THIS FUNCTION.
	
	if part and part ~= cast.RayInfo.CosmeticBulletObject then
		local start = tick()
		PrintDebug("Hit something, testing now.")
		
		-- SANITY CHECK: Don't allow the user to yield or run otherwise extensive code that takes longer than one frame/heartbeat to execute.
		if (cast.RayInfo.CanPenetrateCallback ~= nil) then
			if expectingShortCall == false then
				if (cast.StateInfo.IsActivelySimulatingPenetration) then
					cast:Terminate()
					error("ERROR: The latest call to CanPenetrateCallback took too long to complete! This cast is going to suffer desyncs which WILL cause unexpected behavior and errors. Please fix your performance problems, or remove statements that yield (e.g. wait() calls)")
					-- Use error. This should absolutely abort the cast.
				end
			end
			-- expectingShortCall is used to determine if we are doing a forced resolution increase, in which case this will be called several times in a single frame, which throws this error.
			cast.StateInfo.IsActivelySimulatingPenetration = true
		end
		------------------------------
				
		if cast.RayInfo.CanPenetrateCallback == nil or (cast.RayInfo.CanPenetrateCallback ~= nil and cast.RayInfo.CanPenetrateCallback(cast, origin, rayDir.Unit, resultOfCast, segmentVelocity, cast.RayInfo.CosmeticBulletObject) == false) then
			PrintDebug("Penetrating function is nil or it returned FALSE to not penetrate this hit.")
			cast.StateInfo.IsActivelySimulatingPenetration = false
			
			if (cast.StateInfo.HighFidelityBehavior == 2 and latestTrajectory.Acceleration ~= Vector3.new() and cast.StateInfo.HighFidelitySegmentSize ~= 0) then
				cast.StateInfo.CancelHighResCast = false -- Reset this here.
				
				if cast.StateInfo.IsActivelyResimulating then
					cast:Terminate()
					error("Cascading cast lag encountered! The caster attempted to perform a high fidelity cast before the previous one completed, resulting in exponential cast lag. Consider increasing HighFidelitySegmentSize.")
				end
				

				cast.StateInfo.IsActivelyResimulating = true
				
				-- This is a physics based cast and it needs to be recalculated.
				PrintDebug("Hit was registered, but recalculation is on for physics based casts. Recalculating to verify a real hit...")
				
				-- Split this ray segment into smaller segments of a given size.
				-- In 99% of cases, it won't divide evently (e.g. I have a distance of 1.25 and I want to divide into 0.1 -- that won't work)
				-- To fix this, the segments need to be stretched slightly to fill the space (rather than having a single shorter segment at the end)
				
				local numSegmentsDecimal = rayDisplacement / cast.StateInfo.HighFidelitySegmentSize -- say rayDisplacement is 5.1, segment size is 0.5 -- 10.2 segments
				local numSegmentsReal = math.floor(numSegmentsDecimal) -- 10 segments + 0.2 extra segments
				local realSegmentLength = rayDisplacement / numSegmentsReal -- this spits out 0.51, which isn't exact to the defined 0.5, but it's close
				
				-- Now the real hard part is converting this to time.
				local timeIncrement = delta / numSegmentsReal
				for segmentIndex = 1, numSegmentsReal do
					if cast.StateInfo.CancelHighResCast then
						cast.StateInfo.CancelHighResCast = false
						break
					end
					
					local subPosition = GetPositionAtTime(lastDelta + (timeIncrement * segmentIndex), origin, initialVelocity, acceleration)
					local subVelocity = GetVelocityAtTime(lastDelta + (timeIncrement * segmentIndex), initialVelocity, acceleration) 
					local subRayDir = subVelocity * delta
					local subResult
					
					if cast.RayInfo.RaycastHitbox then
						for _, v in next, cast.RayInfo.RaycastHitbox do
							subResult = targetWorldRoot:Raycast((cast.RayInfo.CurrentCFrame * v).p, subRayDir, cast.RayInfo.Parameters)
							if (subResult ~= nil) then
								break
							end
						end
					else
						subResult = targetWorldRoot:Raycast(subPosition, subRayDir, cast.RayInfo.Parameters)
					end
					
					local subDisplacement = (subPosition - (subPosition + subVelocity)).Magnitude
					
					if (subResult ~= nil) then
						local subDisplacement = (subPosition - subResult.Position).Magnitude
						local dbgSeg = DbgVisualizeSegment(CFrame.new(subPosition, subPosition + subVelocity), subDisplacement)
						if (dbgSeg ~= nil) then dbgSeg.Color3 = Color3.new(0.286275, 0.329412, 0.247059) end
						
						if cast.RayInfo.CanPenetrateCallback == nil or (cast.RayInfo.CanPenetrateCallback ~= nil and cast.RayInfo.CanPenetrateCallback(cast, origin, subRayDir.Unit, subResult, subVelocity, cast.RayInfo.CosmeticBulletObject) == false) then
							if cast.RayInfo.CanHitCallback == nil or (cast.RayInfo.CanHitCallback ~= nil and cast.RayInfo.CanHitCallback(cast, origin, subRayDir.Unit, subResult, subVelocity, cast.RayInfo.CosmeticBulletObject) == false) then
								-- Still hit even at high res
								cast.StateInfo.IsActivelyResimulating = false

								SendRayFinalHit(cast, origin, subRayDir.Unit, subResult, subVelocity, cast.RayInfo.CosmeticBulletObject)
								cast:Terminate(subResult)
								local vis = DbgVisualizeHit(CFrame.new(point), false)
								if (vis ~= nil) then vis.Color3 = Color3.new(0.0588235, 0.87451, 1) end
								return								
							else
								-- Recalculating hit something hittable instead.
								SendRayHit(cast, origin, subRayDir.Unit, subResult, subVelocity, cast.RayInfo.CosmeticBulletObject) -- This may result in CancelHighResCast being set to true.
								local vis = DbgVisualizeHit(CFrame.new(point), true)
								if (vis ~= nil) then vis.Color3 = Color3.new(1, 0.113725, 0.588235) end
								if (dbgSeg ~= nil) then dbgSeg.Color3 = Color3.new(0.305882, 0.243137, 0.329412) end
							end
						end
					else
						local dbgSeg = DbgVisualizeSegment(CFrame.new(subPosition, subPosition + subVelocity), subDisplacement)
						if (dbgSeg ~= nil) then dbgSeg.Color3 = Color3.new(0.286275, 0.329412, 0.247059) end
					end
				end
				
				-- If the script makes it here, then it wasn't a real hit (higher resolution revealed that the low-res hit was faulty)
				-- Just let it keep going.
				cast.StateInfo.IsActivelyResimulating = false
			elseif (cast.StateInfo.HighFidelityBehavior ~= 1 and cast.StateInfo.HighFidelityBehavior ~= 3) then
				cast:Terminate()
				error("Invalid value " .. (cast.StateInfo.HighFidelityBehavior) .. " for HighFidelityBehavior.")
			else
				if cast.RayInfo.CanHitCallback == nil or (cast.RayInfo.CanHitCallback ~= nil and cast.RayInfo.CanHitCallback(cast, origin, rayDir.Unit, resultOfCast, segmentVelocity, cast.RayInfo.CosmeticBulletObject) == false) then
					-- This is not a physics cast, or recalculation is off.
					PrintDebug("Hit was successful. Terminating.")
					SendRayFinalHit(cast, origin, rayDir.Unit, resultOfCast, segmentVelocity, cast.RayInfo.CosmeticBulletObject)
					cast:Terminate(resultOfCast)
					DbgVisualizeHit(CFrame.new(point), false)
					return							
				else
					-- Recalculating hit something hittable instead.
					SendRayHit(cast, origin, rayDir.Unit, resultOfCast, segmentVelocity, cast.RayInfo.CosmeticBulletObject) -- This may result in CancelHighResCast being set to true.
					local vis = DbgVisualizeHit(CFrame.new(point), true)
					if (vis ~= nil) then vis.Color3 = Color3.new(1, 0.113725, 0.588235) end
					local dbgSeg = DbgVisualizeSegment(CFrame.new(lastPoint, lastPoint + rayDir), rayDisplacement)
					if (dbgSeg ~= nil) then dbgSeg.Color3 = Color3.new(0.305882, 0.243137, 0.329412) end
				end
			end
		else			
			PrintDebug("Penetrating function returned TRUE to penetrate this part.")
			if rayVisualization ~= nil then
				rayVisualization.Color3 = Color3.new(0.4, 0.05, 0.05) -- Turn it red to signify that the cast was scrapped.
			end
			DbgVisualizeHit(CFrame.new(point), true)
			
			local params = cast.RayInfo.Parameters
			local alteredParts = {}
			local currentPenetrationTestCount = 0
			local originalFilter = params.FilterDescendantsInstances
			local brokeFromSolidObject = false
			local reallyBrokeFromSolidObject = false
			
			local function Recursive()
				table.clear(alteredParts)
				currentPenetrationTestCount = 0
				brokeFromSolidObject = false
				
				cast.StateInfo.IsActivelySimulatingPenetration = true -- The hell?
				
				while true do
					-- So now what I need to do is redo this entire cast, just with the new filter list

					-- Catch case: Is it terrain?
					if resultOfCast.Instance:IsA("Terrain") then
						if material == Enum.Material.Water then
							-- Special case: Penetrated on water?
							cast:Terminate()
							error("Do not add Water as a piercable material. If you need to penetrate water, set cast.RayInfo.Parameters.IgnoreWater = true instead", 0)
						end
						warn("WARNING: The penetrate callback for this cast returned TRUE on Terrain! This can cause severely adverse effects.")
					end

					if params.FilterType == Enum.RaycastFilterType.Blacklist then
						-- blacklist
						-- DO NOT DIRECTLY TABLE.INSERT ON THE PROPERTY
						local filter = params.FilterDescendantsInstances
						table.insert(filter, resultOfCast.Instance)
						table.insert(alteredParts, resultOfCast.Instance)
						params.FilterDescendantsInstances = filter
					else
						-- whitelist
						-- method implemeneted by custom table system
						-- DO NOT DIRECTLY TABLE.REMOVEOBJECT ON THE PROPERTY
						local filter = params.FilterDescendantsInstances
						table.removeObject(filter, resultOfCast.Instance)
						table.insert(alteredParts, resultOfCast.Instance)
						params.FilterDescendantsInstances = filter
					end

					-- List has been updated, so let's cast again.					
					resultOfCast = targetWorldRoot:Raycast(lastPoint, rayDir, params)
					--[[if cast.RayInfo.RaycastHitbox then
						for _, v in next, cast.RayInfo.RaycastHitbox do
							resultOfCast = targetWorldRoot:Raycast((cast.RayInfo.CurrentCFrame * v).p, rayDir, params)
							if (resultOfCast ~= nil) then
								break
							end
						end
					else
						resultOfCast = targetWorldRoot:Raycast(lastPoint, rayDir, params)
					end]]

					-- No hit? No simulation. Break.
					if resultOfCast == nil then
						break
					end

					if currentPenetrationTestCount >= MAX_PENETRATION_TEST_COUNT then
						warn("WARNING: Exceeded maximum penetrate test budget for a single ray segment (attempted to test the same segment " .. MAX_PENETRATION_TEST_COUNT .. " times!)")
						break
					end
					currentPenetrationTestCount = currentPenetrationTestCount + 1;

					if cast.RayInfo.CanPenetrateCallback(cast, origin, rayDir.Unit, resultOfCast, segmentVelocity, cast.RayInfo.CosmeticBulletObject) == false then
						brokeFromSolidObject = true
						break
					end
				end

				-- Restore the filter to its default state.
				cast.RayInfo.Parameters.FilterDescendantsInstances = originalFilter
				cast.StateInfo.IsActivelySimulatingPenetration = false

				if brokeFromSolidObject then
					if cast.RayInfo.CanHitCallback == nil or (cast.RayInfo.CanHitCallback ~= nil and cast.RayInfo.CanHitCallback(cast, origin, rayDir.Unit, resultOfCast, segmentVelocity, cast.RayInfo.CosmeticBulletObject) == false) then
						reallyBrokeFromSolidObject = true
					else
						-- Recalculating hit something hittable instead.
						SendRayHit(cast, origin, rayDir.Unit, resultOfCast, segmentVelocity, cast.RayInfo.CosmeticBulletObject) -- This may result in CancelHighResCast being set to true.
						local vis = DbgVisualizeHit(CFrame.new(point), true)
						if (vis ~= nil) then vis.Color3 = Color3.new(1, 0.113725, 0.588235) end
						local dbgSeg = DbgVisualizeSegment(CFrame.new(lastPoint, lastPoint + rayDir), rayDisplacement)
						if (dbgSeg ~= nil) then dbgSeg.Color3 = Color3.new(0.305882, 0.243137, 0.329412) end
						Recursive()
					end
				end
			end
			
			Recursive()
			
			if reallyBrokeFromSolidObject then
				-- We actually hit something while testing.
				PrintDebug("Broke because the ray hit something solid (" .. tostring(resultOfCast.Instance) .. ") while testing for a penetration. Terminating the cast.")
				SendRayFinalHit(cast, origin, rayDir.Unit, resultOfCast, segmentVelocity, cast.RayInfo.CosmeticBulletObject)
				cast:Terminate(resultOfCast)
				DbgVisualizeHit(CFrame.new(resultOfCast.Position), false)
				return
			end
			
			-- And exit the function here too.
		end
	end
	
	if cast.RayInfo.TravelType == "Distance" then
		if cast.StateInfo.DistanceCovered >= cast.RayInfo.MaxDistance then
			if cast.UserData.CastBehavior.HitEventOnTermination then
				SendRayFinalHit(cast, origin, rayDir.Unit, resultOfCast, segmentVelocity, cast.RayInfo.CosmeticBulletObject)
			end
			cast:Terminate(nil, true)
			DbgVisualizeHit(CFrame.new(currentTarget), false)			
		end
	else
		if cast.StateInfo.TotalRuntime2 > cast.RayInfo.Lifetime then
			if cast.UserData.CastBehavior.HitEventOnTermination then
				SendRayFinalHit(cast, origin, rayDir.Unit, resultOfCast, segmentVelocity, cast.RayInfo.CosmeticBulletObject)
			end			
			cast:Terminate(nil, true)
			DbgVisualizeHit(CFrame.new(currentTarget), false)	
		end
	end
end