-- Simulate a raycast.


local function SimulateCast(origin, direction, velocity, castFunction, lengthChangedEvent, rayHitEvent, rayExitedEvent, cosmeticBulletObject, listOrIgnoreDescendantsInstance, ignoreWater, bulletAcceleration, bulletData, whizData, hitData, penetrationData)
	PrintDebug("Cast simulation requested.")
	if type(velocity) == "number" then
		velocity = direction.Unit * velocity
	end
	
	local penetrationPower = penetrationData ~= nil and penetrationData.penetrationDepth or 0
	local penetrationCount = penetrationData ~= nil and penetrationData.penetrationAmount or 0
	local bulletAcceleration = bulletAcceleration or Vector3.new() -- Fix bug reported by Spooce: Failing to pass in the bulletAcceleration parameter throws an error, so add a fallback of Vector3.new()
	local distance = direction.Magnitude -- This will be a unit vector multiplied by the maximum distance.
	local normalizedDir = direction.Unit
	local upgradedDir = (normalizedDir + velocity).Unit -- This rotates the direction of the bullet by the initial velocity, allowing 3D velocity to occur in the first place.
	local initialVelocity = upgradedDir * velocity.Magnitude
	
	local totalDelta = 0
	local distanceTravelled = 0
	local lastPoint = origin
	
	local self = {}
	local isRunningPenetration = false
	local didHitHumanoid = false
	local originalList = listOrIgnoreDescendantsInstance
	local originalCastFunction = castFunction
	
	local function Fire(delta, customAt)
		PrintDebug("Casting for frame.")
		totalDelta = totalDelta + delta
		local at = customAt or GetPositionAtTime(totalDelta, origin, initialVelocity, bulletAcceleration)
		local totalDisplacement = (at - lastPoint) -- This is the displacement from where the ray was on the last from to where the ray is now.
		
		-- NEW BEHAVIOR:
		-- Displacement needs to define velocity. The reason peoples' casts were going crazy was because on the line below I used to multiply by velocity.Magnitude
		-- Okay, cool, so that's effectively the initial velocity. Now say we're at the top of an arc in a physics cast. Bullet sure isn't going the same speed!
		-- We need to recalculate velocity based on displacement, NOT use the defined value.
		-- How I missed this is beyond me.
		-- Bonus ducks: This also allows me to implement GFink's request (see https://devforum.roblox.com/t/making-a-combat-game-with-ranged-weapons-fastcast-may-be-the-module-for-you/133474/282?u=etithespirit)
		
		local segmentVelocity = GetVelocityAtTime(totalDelta, initialVelocity, bulletAcceleration) 
		-- local rayDir = totalDisplacement.Unit * segmentVelocity.Magnitude * delta -- Direction of the ray is the direction from last to now * the velocity * deltaTime
		local rayDir = totalDisplacement.Unit * segmentVelocity.Magnitude * delta
		local hit, point, normal, material = castFunction(lastPoint, rayDir, listOrIgnoreDescendantsInstance, ignoreWater)
		
		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)
		
		lengthChangedEvent:Fire(origin, lastPoint, rayDir.Unit, rayDisplacement, segmentVelocity, cosmeticBulletObject, bulletData, whizData, hitData)
		
		local rayVisualization = nil
		if (delta > 0) then
			rayVisualization = DbgVisualizeSegment(CFrame.new(lastPoint, lastPoint + rayDir), rayDisplacement)
		end
		
		if hit and hit ~= cosmeticBulletObject then
			local start = tick()
            local unit = rayDir.Unit
            local maxExtent = hit.Size.Magnitude * unit
            local exitHit, exitPoint, exitNormal, exitMaterial = CastWithWhitelist(point + maxExtent, -maxExtent, {hit}, ignoreWater)
            --local diff = exitPoint - point
            --local dist = Vector3.new():Dot(unit, diff)
			local dist = (exitPoint - point).Magnitude
			
			-- SANITY CHECK: Don't allow the user to yield or run otherwise extensive code that takes longer than one frame/heartbeat to execute.
			if (penetrationData ~= nil) and (penetrationData.penetrationType == "WallPenetration" and ((penetrationData.canPenetrateFunction ~= nil) and (penetrationPower > dist)) or (penetrationData.penetrationType == "HumanoidPenetration" and ((penetrationData.canPenetrateHumanoidFunction ~= nil) and (penetrationCount > 0)))) then
				if (isRunningPenetration) then
					error("ERROR: The latest call to canPenetrateFunction/canPenetrateHumanoidFunction took too long to complete! This cast is going to suffer desyncs which WILL cause unexpected behavior and errors.")
					-- Use error. This should absolutely abort the cast.
				end
				isRunningPenetration = true
				didHitHumanoid = false
			end
			
			if penetrationData == nil or (penetrationData ~= nil and ((penetrationData.penetrationType == "WallPenetration" and (penetrationPower < dist)) or (penetrationData.penetrationType == "HumanoidPenetration" and ((penetrationData.canPenetrateHumanoidFunction(origin, rayDir.Unit, hit, point, normal, material, segmentVelocity, penetrationCount, hitData) == false) or (penetrationCount <= 0))))) then
				PrintDebug("Penetrating data is nil or penetrationPower is lower than dist/penetrationCount is at 0. Ending cast and firing RayHit.")
				isRunningPenetration = false
				-- Penetrate function is nil, or it's not nil and it returned false (we cannot penetrate this object).
				-- Hit.
				
				RemoveList[self] = true
				rayHitEvent:Fire(origin, rayDir.Unit, hit, point, normal, material, segmentVelocity, cosmeticBulletObject, hitData)
				DbgVisualizeHit(CFrame.new(point), Color3.new(0.2, 1, 0.5))
				return
			else
				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), Color3.new(1, 0.2, 0.2))
				
				PrintDebug("Penetrating returned 'penetratedHumanoid' or 'penetratedObject'/TRUE to penetrate this hit. Processing...")
				isRunningPenetration = false
				-- Nope! We want to penetrate this part.				
				-- Now this is gonna be DISGUSTING. 
				-- We need to run this until we fufill that lost distance, so if some guy decides to layer up like 10 parts right next to eachother, we need to handle all of those in a single frame
				-- The only way to do this isn't particularly pretty but it needs to be quick.
				if penetrationData.penetrationType == "WallPenetration" then
					local penetrationType = penetrationData ~= nil and penetrationData.canPenetrateFunction(origin, rayDir.Unit, hit, point, normal, material, segmentVelocity, hitData)		
					if (castFunction == CastWithWhitelist) then
						-- User is using whitelist. We need to pull this from their list.
						-- n.b. this function is offered by the sandboxed table system. It's not stock.
						if penetrationType == "penetratedHumanoid" and (hit.Parent:FindFirstChildOfClass("Humanoid") and hit.Parent:FindFirstChildOfClass("Humanoid").Health > 0) or (hit.Parent.Parent:FindFirstChildOfClass("Humanoid") and hit.Parent.Parent:FindFirstChildOfClass("Humanoid").Health > 0) then
							if exitHit and FastCast.RayExit then
								rayExitedEvent:Fire(point + maxExtent, -maxExtent, exitHit, exitPoint, exitNormal, exitMaterial, segmentVelocity, hitData)
								DbgVisualizeHit(CFrame.new(exitPoint), Color3.fromRGB(13, 105, 172))
							end
							local humanoid = hit.Parent:FindFirstChildOfClass("Humanoid") or hit.Parent.Parent:FindFirstChildOfClass("Humanoid")
							table.removeObject(listOrIgnoreDescendantsInstance, humanoid.Parent)
							--penetrationPower = penetrationPower
							local toReduce = 1-((dist/penetrationPower/1.1))
							penetrationPower = penetrationPower * toReduce
							PrintDebug("Whitelist cast detected, removed " .. tostring(humanoid.Parent) .. " from the whitelist.")
						elseif penetrationType == "penetratedObject" then
							if exitHit and FastCast.RayExit then
								rayExitedEvent:Fire(point + maxExtent, -maxExtent, exitHit, exitPoint, exitNormal, exitMaterial, segmentVelocity, hitData)
								DbgVisualizeHit(CFrame.new(exitPoint), Color3.fromRGB(13, 105, 172))
							end
							table.removeObject(listOrIgnoreDescendantsInstance, hit)
							--penetrationPower = penetrationPower - dist
							local toReduce = 1-((dist/penetrationPower/1.1))
							penetrationPower = penetrationPower * toReduce
							PrintDebug("Whitelist cast detected, removed " .. tostring(hit) .. " from the whitelist.")
						end
					elseif (castFunction == CastWithBlacklistAndNoHumPenetration) then
						if penetrationType == "penetratedHumanoid" and (hit.Parent:FindFirstChildOfClass("Humanoid") and hit.Parent:FindFirstChildOfClass("Humanoid").Health > 0) or (hit.Parent.Parent:FindFirstChildOfClass("Humanoid") and hit.Parent.Parent:FindFirstChildOfClass("Humanoid").Health > 0) then
							if exitHit and FastCast.RayExit then
								rayExitedEvent:Fire(point + maxExtent, -maxExtent, exitHit, exitPoint, exitNormal, exitMaterial, segmentVelocity, hitData)
								DbgVisualizeHit(CFrame.new(exitPoint), Color3.fromRGB(13, 105, 172))
							end
							local humanoid = hit.Parent:FindFirstChildOfClass("Humanoid") or hit.Parent.Parent:FindFirstChildOfClass("Humanoid")
							table.insert(listOrIgnoreDescendantsInstance, humanoid.Parent)
							--penetrationPower = penetrationPower
							local toReduce = 1-((dist/penetrationPower/1.1))
							penetrationPower = penetrationPower * toReduce
							PrintDebug("Blacklist cast detected, added " .. tostring(humanoid.Parent) .. " to the blacklist.")
						elseif penetrationType == "penetratedObject" then
							if exitHit and FastCast.RayExit then
								rayExitedEvent:Fire(point + maxExtent, -maxExtent, exitHit, exitPoint, exitNormal, exitMaterial, segmentVelocity, hitData)
								DbgVisualizeHit(CFrame.new(exitPoint), Color3.fromRGB(13, 105, 172))
							end
							table.insert(listOrIgnoreDescendantsInstance, hit)
							--penetrationPower = penetrationPower - dist
							local toReduce = 1-((dist/penetrationPower/1.1))
							penetrationPower = penetrationPower * toReduce
							PrintDebug("Blacklist cast detected, added " .. tostring(hit) .. " to the blacklist.")	
						end
					else
						-- This is where things get finnicky.
						-- We can't reparent the object. If we do this, we risk altering behavior of the developer's game which has undesirable effects.
						-- We need to swap cast functions on the fly here. This is gonna get NASTY. Oh well. It should only happen once under this new behavior.
						castFunction = CastWithBlacklistAndNoHumPenetration
						listOrIgnoreDescendantsInstance = listOrIgnoreDescendantsInstance:GetDescendants()	
						if penetrationType == "penetratedHumanoid" and (hit.Parent:FindFirstChildOfClass("Humanoid") and hit.Parent:FindFirstChildOfClass("Humanoid").Health > 0) or (hit.Parent.Parent:FindFirstChildOfClass("Humanoid") and hit.Parent.Parent:FindFirstChildOfClass("Humanoid").Health > 0) then
							if exitHit and FastCast.RayExit then
								rayExitedEvent:Fire(point + maxExtent, -maxExtent, exitHit, exitPoint, exitNormal, exitMaterial, segmentVelocity, hitData)
								DbgVisualizeHit(CFrame.new(exitPoint), Color3.fromRGB(13, 105, 172))
							end
							local humanoid = hit.Parent:FindFirstChildOfClass("Humanoid") or hit.Parent.Parent:FindFirstChildOfClass("Humanoid")
							table.insert(listOrIgnoreDescendantsInstance, humanoid.Parent)
							--penetrationPower = penetrationPower
							local toReduce = 1-((dist/penetrationPower/1.1))
							penetrationPower = penetrationPower * toReduce
							PrintDebug("Stock cast detected, transformed cast into blacklist cast and added " .. tostring(humanoid.Parent) .. " to the blacklist.")
						elseif penetrationType == "penetratedObject" then
							if exitHit and FastCast.RayExit then
								rayExitedEvent:Fire(point + maxExtent, -maxExtent, exitHit, exitPoint, exitNormal, exitMaterial, segmentVelocity, hitData)
								DbgVisualizeHit(CFrame.new(exitPoint), Color3.fromRGB(13, 105, 172))
							end
							table.insert(listOrIgnoreDescendantsInstance, hit)
							--penetrationPower = penetrationPower - dist
							local toReduce = 1-((dist/penetrationPower/1.1))
							penetrationPower = penetrationPower * toReduce
							PrintDebug("Stock cast detected, transformed cast into blacklist cast and added " .. tostring(hit) .. " to the blacklist.")						
						end				
					end
				elseif penetrationData.penetrationType == "HumanoidPenetration" then
					if (castFunction == CastWithWhitelist) then
						-- User is using whitelist. We need to pull this from their list.
						-- n.b. this function is offered by the sandboxed table system. It's not stock.
						if (hit.Parent:FindFirstChildOfClass("Humanoid") and hit.Parent:FindFirstChildOfClass("Humanoid").Health > 0) or (hit.Parent.Parent:FindFirstChildOfClass("Humanoid") and hit.Parent.Parent:FindFirstChildOfClass("Humanoid").Health > 0) then
							didHitHumanoid = true
							local humanoid = hit.Parent:FindFirstChildOfClass("Humanoid") or hit.Parent.Parent:FindFirstChildOfClass("Humanoid")
							table.removeObject(listOrIgnoreDescendantsInstance, humanoid.Parent)
							PrintDebug("Whitelist cast detected, removed " .. tostring(humanoid.Parent) .. " from the whitelist.")
						else
							didHitHumanoid = false
							table.removeObject(listOrIgnoreDescendantsInstance, hit)
							PrintDebug("Whitelist cast detected, removed " .. tostring(hit) .. " from the whitelist.")
						end
					elseif (castFunction == CastWithBlacklist) then
						if (hit.Parent:FindFirstChildOfClass("Humanoid") and hit.Parent:FindFirstChildOfClass("Humanoid").Health > 0) or (hit.Parent.Parent:FindFirstChildOfClass("Humanoid") and hit.Parent.Parent:FindFirstChildOfClass("Humanoid").Health > 0) then
							didHitHumanoid = true
							local humanoid = hit.Parent:FindFirstChildOfClass("Humanoid") or hit.Parent.Parent:FindFirstChildOfClass("Humanoid")
							table.insert(listOrIgnoreDescendantsInstance, humanoid.Parent)
							PrintDebug("Blacklist cast detected, added " .. tostring(humanoid.Parent) .. " to the blacklist.")
						else
							didHitHumanoid = false
							table.insert(listOrIgnoreDescendantsInstance, hit)
							PrintDebug("Blacklist cast detected, added " .. tostring(hit) .. " to the blacklist.")	
						end
					else
						-- This is where things get finnicky.
						-- We can't reparent the object. If we do this, we risk altering behavior of the developer's game which has undesirable effects.
						-- We need to swap cast functions on the fly here. This is gonna get NASTY. Oh well. It should only happen once under this new behavior.
						castFunction = CastWithBlacklist
						listOrIgnoreDescendantsInstance = listOrIgnoreDescendantsInstance:GetDescendants()
						if (hit.Parent:FindFirstChildOfClass("Humanoid") and hit.Parent:FindFirstChildOfClass("Humanoid").Health > 0) or (hit.Parent.Parent:FindFirstChildOfClass("Humanoid") and hit.Parent.Parent:FindFirstChildOfClass("Humanoid").Health > 0) then
							didHitHumanoid = true
							local humanoid = hit.Parent:FindFirstChildOfClass("Humanoid") or hit.Parent.Parent:FindFirstChildOfClass("Humanoid")
							table.insert(listOrIgnoreDescendantsInstance, humanoid.Parent)
							PrintDebug("Stock cast detected, transformed cast into blacklist cast and added " .. tostring(humanoid.Parent) .. " to the blacklist.")
						else
							didHitHumanoid = false
							table.insert(listOrIgnoreDescendantsInstance, hit)
							PrintDebug("Stock cast detected, transformed cast into blacklist cast and added " .. tostring(hit) .. " to the blacklist.")						
						end					
					end					
				end
				
				-- So now just cast again!
				-- Cast with 0 deltaTime and from the prespecified point (this saves a smidge of performance since we don't need to try to recalculate a position value that will come out to be the same thing.)
				PrintDebug("Recasting for penetration...")
				Fire(0, at)
				
				-- Then set lastPoint.
				lastPoint = point
				-- We used to do ^ above, but this caused undesired effects and zero-length casts. Oops.
				
				if penetrationData ~= nil then
					if penetrationData.penetrationType ~= "WallPenetration" then
						if didHitHumanoid then
							penetrationCount = hit and (penetrationCount - 1) or 0
							PrintDebug("penetrationCount is reduced to... "..penetrationCount.." for hitting humanoid.")
						else
							PrintDebug("penetrationCount is not reduced for not hitting humanoid (or the humanoid died).")
						end
					else
						PrintDebug("DISTANCE: "..dist.." studs")
						PrintDebug("CURRENT PENETRATION DEPTH: "..penetrationPower.." studs")
					end
				end
				
				-- And exit the function here too.
				return
			end
		end
		
		-- Then set lastPoint here as well.
		lastPoint = point	
		distanceTravelled = distanceTravelled + rayDisplacement
		
		if distanceTravelled > distance then
			RemoveList[self] = true
			rayHitEvent:Fire(origin, rayDir.Unit, nil, lastPoint, nil, nil, Vector3.new(), cosmeticBulletObject, hitData)
		end
	end
	
	function self.Update(delta)
		Fire(delta)
	end

	--[[Thread:Spawn(function()
		Projectiles[self] = true
	end)]]
	-- Too fast?
	if RunService:IsClient() then
		spawn(function()
			Projectiles[self] = true
		end)
	else
		Thread:Spawn(function()
			Projectiles[self] = true
		end)
	end
end

local function BaseFireMethod(self, origin, directionWithMagnitude, velocity, cosmeticBulletObject, ignoreDescendantsInstance, ignoreWater, bulletAcceleration, bulletData, whizData, hitData, list, isWhitelist, penetrationData)
	MandateType(origin, "Vector3", "origin")
	MandateType(directionWithMagnitude, "Vector3", "directionWithMagnitude")
	assert(typeof(velocity) == "Vector3" or typeof(velocity) == "number", ERR_INVALID_TYPE:format("velocity", "Variant<Vector3, number>", typeof(velocity))) -- This one's an odd one out.
	MandateType(cosmeticBulletObject, "Instance", "cosmeticBulletObject", true)
	MandateType(ignoreDescendantsInstance, "Instance", "ignoreDescendantsInstance", true)
	MandateType(ignoreWater, "boolean", "ignoreWater", true)
	MandateType(bulletAcceleration, "Vector3", "bulletAcceleration", true)
	MandateType(list, "table", "list", true)
	-- isWhitelist is strictly internal so it doesn't need to get sanity checked, because last I checked, I'm not insane c:
	-- ... I hope
	-- However, as of Version 9.0.0, a penetrate function can be specified
	MandateType(penetrationData, "table", "penetrationData", true)

	-- Now get into the guts of this.
	local castFunction = Cast
	local ignoreOrList = ignoreDescendantsInstance
	if list ~= nil then
		ignoreOrList = list
		if isWhitelist then
			castFunction = CastWithWhitelist
		else
			castFunction = (penetrationData and penetrationData.penetrationType ~= "WallPenetration" and CastWithBlacklist or CastWithBlacklistAndNoHumPenetration) or CastWithBlacklistAndNoHumPenetration
		end
	end
	
	SimulateCast(origin, directionWithMagnitude, velocity, castFunction, self.LengthChanged, self.RayHit, self.RayExited, cosmeticBulletObject, ignoreOrList, ignoreWater, bulletAcceleration, bulletData, whizData, hitData, penetrationData)
end