-- 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.TotalRuntime += 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 function castFunction(lastPoint, rayDir, params) -- Main Cast
		local resultOfCast = targetWorldRoot:Raycast(lastPoint, rayDir, params)
		
		local needRecast = false
		
		if resultOfCast then
			local hitInstance = resultOfCast.Instance

			if hitInstance.Parent:IsA("Accoutrement") then
				local newIgnoreTable = {hitInstance}
				
				for i,v in pairs(params.FilterDescendantsInstances) do
					table.insert(newIgnoreTable, v)
				end
				
				cast.RayInfo.Parameters.FilterDescendantsInstances = newIgnoreTable
				resultOfCast = nil
				
				needRecast = true
			end

			if hitInstance.CanCollide == false and not FindCharacterAncestor(hitInstance) then
				local newIgnoreTable = {hitInstance}
				
				for i,v in pairs(params.FilterDescendantsInstances) do
					table.insert(newIgnoreTable, v)
				end
				
				cast.RayInfo.Parameters.FilterDescendantsInstances = newIgnoreTable
				resultOfCast = nil
				
				needRecast = true
			end
			
			if hitInstance.CanCollide == false and hitInstance.Transparency > 0.2 and not FindCharacterAncestor(hitInstance) then
				local newIgnoreTable = {hitInstance}
				
				for i,v in pairs(params.FilterDescendantsInstances) do
					table.insert(newIgnoreTable, v)
				end
				
				cast.RayInfo.Parameters.FilterDescendantsInstances = newIgnoreTable
				resultOfCast = nil
				
				needRecast = true
			end
		end
		
		if needRecast == true then
			return castFunction(lastPoint, rayDir, cast.RayInfo.Parameters)
		end
		
		return resultOfCast
	end
	
	local resultOfCast = castFunction(lastPoint, rayDir, cast.RayInfo.Parameters)
	
	local point = currentTarget
	local part: Instance? = nil
	local material = Enum.Material.Air
	local normal = Vector3.new()
	
	if (resultOfCast ~= nil) then
		point = resultOfCast.Position
		part = resultOfCast.Instance
		material = resultOfCast.Material
		normal = resultOfCast.Normal
	end
	
	local rayDisplacement = (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, 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.CanPierceCallback ~= nil) then
			if expectingShortCall == false then
				if (cast.StateInfo.IsActivelySimulatingPierce) then
					cast:Terminate()
					error("ERROR: The latest call to CanPierceCallback 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.IsActivelySimulatingPierce = true
		end
		------------------------------
		
		if cast.RayInfo.CanPierceCallback == nil or (cast.RayInfo.CanPierceCallback ~= nil and cast.RayInfo.CanPierceCallback(cast, resultOfCast, segmentVelocity, cast.RayInfo.CosmeticBulletObject) == false) then
			PrintDebug("Piercing function is nil or it returned FALSE to not pierce this hit.")
			cast.StateInfo.IsActivelySimulatingPierce = 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 function castFunction(subPosition, subRayDir, params)
						local subResult = targetWorldRoot:Raycast(subPosition, subRayDir, params)
						
						if subResult then
							local hitInstance = subResult.Instance
							
							if hitInstance.Parent:IsA("Accoutrement") then
								table.insert(params.FilterDescendantsInstances, hitInstance)
								subResult = nil
							end
							
							if hitInstance.CanCollide == false then
								table.insert(params.FilterDescendantsInstances, hitInstance)
								subResult = nil
							end

							if hitInstance.CanCollide == false and hitInstance.Transparency > 0.2 then
								table.insert(params.FilterDescendantsInstances, hitInstance)
								subResult = nil
							end
						end
						
						return subResult
					end
					
					local subResult = castFunction(subPosition, subRayDir, cast.RayInfo.Parameters)
					
					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.CanPierceCallback == nil or (cast.RayInfo.CanPierceCallback ~= nil and cast.RayInfo.CanPierceCallback(cast, subResult, subVelocity, cast.RayInfo.CosmeticBulletObject) == false) then
							-- Still hit even at high res
							cast.StateInfo.IsActivelyResimulating = false
							
							SendRayHit(cast, subResult, subVelocity, cast.RayInfo.CosmeticBulletObject)
							cast:Terminate()
							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 pierceable instead.
							SendRayPierced(cast, 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
					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
				-- This is not a physics cast, or recalculation is off.
				PrintDebug("Hit was successful. Terminating.")
				SendRayHit(cast, resultOfCast, segmentVelocity, cast.RayInfo.CosmeticBulletObject)
				cast:Terminate()
				DbgVisualizeHit(CFrame.new(point), false)
				return
			end
		else
			PrintDebug("Piercing function returned TRUE to pierce 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 currentPierceTestCount = 0
			local originalFilter = params.FilterDescendantsInstances
			local brokeFromSolidObject = false
			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: Pierced on water?
						cast:Terminate()
						error("Do not add Water as a piercable material. If you need to pierce water, set cast.RayInfo.Parameters.IgnoreWater = true instead", 0)
					end
					warn("WARNING: The pierce 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
				
				SendRayPierced(cast, resultOfCast, segmentVelocity, cast.RayInfo.CosmeticBulletObject)
				
				-- List has been updated, so let's cast again.
				
				local function castFunction(lastPoint, rayDir, params)
					local resultOfCast = targetWorldRoot:Raycast(lastPoint, rayDir, params)

					if resultOfCast then
						local hitInstance = resultOfCast.Instance

						if hitInstance.Parent:IsA("Accoutrement") then
							table.insert(params.FilterDescendantsInstances, hitInstance)
							resultOfCast = nil
						end

						if hitInstance.CanCollide == false then
							table.insert(params.FilterDescendantsInstances, hitInstance)
							resultOfCast = nil
						end

						if hitInstance.CanCollide == false and hitInstance.Transparency > 0.2 then
							table.insert(params.FilterDescendantsInstances, hitInstance)
							resultOfCast = nil
						end
					end

					return resultOfCast
				end
				
				resultOfCast = castFunction(lastPoint, rayDir, params)
				
				-- No hit? No simulation. Break.
				if resultOfCast == nil then
					break
				end
				
				if currentPierceTestCount >= MAX_PIERCE_TEST_COUNT then
					warn("WARNING: Exceeded maximum pierce test budget for a single ray segment (attempted to test the same segment " .. MAX_PIERCE_TEST_COUNT .. " times!)")
					break
				end
				currentPierceTestCount = currentPierceTestCount + 1;
				
				if cast.RayInfo.CanPierceCallback(cast, 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.IsActivelySimulatingPierce = false
			
			if brokeFromSolidObject then
				-- We actually hit something while testing.
				PrintDebug("Broke because the ray hit something solid (" .. tostring(resultOfCast.Instance) .. ") while testing for a pierce. Terminating the cast.")
				SendRayHit(cast, resultOfCast, segmentVelocity, cast.RayInfo.CosmeticBulletObject)
				cast:Terminate()
				DbgVisualizeHit(CFrame.new(resultOfCast.Position), false)
				return
			end
			
			-- And exit the function here too.
		end
	end
	
	if (cast.StateInfo.DistanceCovered >= cast.RayInfo.MaxDistance) then
		-- SendRayHit(cast, nil, segmentVelocity, cast.RayInfo.CosmeticBulletObject)
		cast:Terminate()
		DbgVisualizeHit(CFrame.new(currentTarget), false)
	end
end