local function N(N,C,D,Z)debug.profilebegin("\113\117\101\114\121\080\111\105\110\116")local j=#blacklist D=D+nearPlaneZ local l=N+C*D local O=inf local a=inf local S=N local t=0 repeat local j,F=workspace:FindPartOnRayWithIgnoreList(ray(S,l-S),blacklist,false,true)t+=1 if j then local M=t>=QUERY_POINT_CAST_LIMIT if canOcclude(j)or M then local C={j}local S=workspace:FindPartOnRayWithWhitelist(ray(l,F-l),C,true)local t=(F-N).Magnitude if S and not M then local N=false if Z then N=workspace:FindPartOnRayWithWhitelist(ray(Z,l-Z),C,true)or workspace:FindPartOnRayWithWhitelist(ray(l,Z-l),C,true)end if N then a=t elseif D<O then O=t end else a=t end end blacklist[#blacklist+1]=j S=F-C*.001 end until a<inf or not j eraseFromEnd(blacklist,j)debug.profileend()return O-nearPlaneZ,a-nearPlaneZ end local function C(C,D)debug.profilebegin("\113\117\101\114\121\086\105\101\119\112\111\114\116")local Z=C.p local j=C.rightVector local l=C.upVector local O=-C.lookVector local a=camera.ViewportSize local S=inf local t=inf for C=0,1,1 do local F=j*((C-.5)*projX)for j=0,1,1 do local M=l*((j-.5)*projY)local Y=Z+nearPlaneZ*(F+M)local J=(camera:ViewportPointToRay(a.x*C,a.y*j)).Origin local L,H=N(Y,O,D,J)if H<S then S=H end if L<t then t=L end end end debug.profileend()return t,S end local function D(C,D,Z)debug.profilebegin("\116\101\115\116\080\114\111\109\111\116\105\111\110")local j=C.p local l=C.rightVector local O=C.upVector local a=-C.lookVector do debug.profilebegin("\101\120\116\114\097\112\111\108\097\116\101")local C=.0625 local l=1.25 local O=(getCollisionPoint(j,Z.posVelocity*l)-j).Magnitude local a=Z.posVelocity.magnitude for C=0,min(l,Z.rotVelocity.magnitude+O/a),C do local j=Z.extrapolate(C)if N(j.p,-j.lookVector,D)>=D then return false end end debug.profileend()end do debug.profilebegin("\116\101\115\116\079\102\102\115\101\116\115")for C,Z in ipairs(SCAN_SAMPLE_OFFSETS)do local S=Z local t=getCollisionPoint(j,l*S.x+O*S.y)if N(t,((j+a*D)-t).Unit,D)==inf then return false end end debug.profileend()end debug.profileend()return true end local function Z(N,Z,j)debug.profilebegin("\112\111\112\112\101\114")subjectRoot=subjectPart and subjectPart:GetRootPart()or subjectPart local l=Z local O,a=C(N,Z)if a<l then l=a end if O<l and D(N,Z,j)then l=O end subjectRoot=nil debug.profileend()return l end return Z
--------------------------------------------------------------------------------

local function queryPoint(origin, unitDir, dist, lastPos)
	debug.profilebegin("queryPoint")

	local originalSize = #blacklist

	dist = dist + nearPlaneZ
	local target = origin + unitDir * dist

	local softLimit = inf
	local hardLimit = inf
	local movingOrigin = origin

	local numPierced = 0

	repeat
		local entryPart, entryPos =
			workspace:FindPartOnRayWithIgnoreList(ray(movingOrigin, target - movingOrigin), blacklist, false, true)
		numPierced += 1

		if entryPart then
			-- forces the current iteration into a hard limit to cap the number of raycasts
			local earlyAbort = numPierced >= QUERY_POINT_CAST_LIMIT

			if canOcclude(entryPart) or earlyAbort then
				local wl = { entryPart }
				local exitPart = workspace:FindPartOnRayWithWhitelist(ray(target, entryPos - target), wl, true)

				local lim = (entryPos - origin).Magnitude

				if exitPart and not earlyAbort then
					local promote = false
					if lastPos then
						promote = workspace:FindPartOnRayWithWhitelist(ray(lastPos, target - lastPos), wl, true)
							or workspace:FindPartOnRayWithWhitelist(ray(target, lastPos - target), wl, true)
					end

					if promote then
						-- Ostensibly a soft limit, but the camera has passed through it in the last frame, so promote to a hard limit.
						hardLimit = lim
					elseif dist < softLimit then
						-- Trivial soft limit
						softLimit = lim
					end
				else
					-- Trivial hard limit
					hardLimit = lim
				end
			end

			blacklist[#blacklist + 1] = entryPart
			movingOrigin = entryPos - unitDir * 1e-3
		end
	until hardLimit < inf or not entryPart

	eraseFromEnd(blacklist, originalSize)

	debug.profileend()
	return softLimit - nearPlaneZ, hardLimit - nearPlaneZ
end

local function queryViewport(focus, dist)
	debug.profilebegin("queryViewport")

	local fP = focus.p
	local fX = focus.rightVector
	local fY = focus.upVector
	local fZ = -focus.lookVector

	local viewport = camera.ViewportSize

	local hardBoxLimit = inf
	local softBoxLimit = inf

	-- Center the viewport on the PoI, sweep points on the edge towards the target, and take the minimum limits
	for viewX = 0, 1 do
		local worldX = fX * ((viewX - 0.5) * projX)

		for viewY = 0, 1 do
			local worldY = fY * ((viewY - 0.5) * projY)

			local origin = fP + nearPlaneZ * (worldX + worldY)
			local lastPos = camera:ViewportPointToRay(viewport.x * viewX, viewport.y * viewY).Origin

			local softPointLimit, hardPointLimit = queryPoint(origin, fZ, dist, lastPos)

			if hardPointLimit < hardBoxLimit then
				hardBoxLimit = hardPointLimit
			end
			if softPointLimit < softBoxLimit then
				softBoxLimit = softPointLimit
			end
		end
	end
	debug.profileend()

	return softBoxLimit, hardBoxLimit
end

local function testPromotion(focus, dist, focusExtrapolation)
	debug.profilebegin("testPromotion")

	local fP = focus.p
	local fX = focus.rightVector
	local fY = focus.upVector
	local fZ = -focus.lookVector

	do
		-- Dead reckoning the camera rotation and focus
		debug.profilebegin("extrapolate")

		local SAMPLE_DT = 0.0625
		local SAMPLE_MAX_T = 1.25

		local maxDist = (getCollisionPoint(fP, focusExtrapolation.posVelocity * SAMPLE_MAX_T) - fP).Magnitude
		-- Metric that decides how many samples to take
		local combinedSpeed = focusExtrapolation.posVelocity.magnitude

		for dt = 0, min(SAMPLE_MAX_T, focusExtrapolation.rotVelocity.magnitude + maxDist / combinedSpeed), SAMPLE_DT do
			local cfDt = focusExtrapolation.extrapolate(dt) -- Extrapolated CFrame at time dt

			if queryPoint(cfDt.p, -cfDt.lookVector, dist) >= dist then
				return false
			end
		end

		debug.profileend()
	end

	do
		-- Test screen-space offsets from the focus for the presence of soft limits
		debug.profilebegin("testOffsets")

		for _, offset in ipairs(SCAN_SAMPLE_OFFSETS) do
			local scaledOffset = offset
			local pos = getCollisionPoint(fP, fX * scaledOffset.x + fY * scaledOffset.y)
			if queryPoint(pos, (fP + fZ * dist - pos).Unit, dist) == inf then
				return false
			end
		end

		debug.profileend()
	end

	debug.profileend()
	return true
end

local function Popper(focus, targetDist, focusExtrapolation)
	debug.profilebegin("popper")

	subjectRoot = subjectPart and subjectPart:GetRootPart() or subjectPart

	local dist = targetDist
	local soft, hard = queryViewport(focus, targetDist)
	if hard < dist then
		dist = hard
	end
	if soft < dist and testPromotion(focus, targetDist, focusExtrapolation) then
		dist = soft
	end

	subjectRoot = nil

	debug.profileend()
	return dist
end

return Popper
