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


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

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

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

				local lim = (entryPos - origin).Magnitude

				if exitPart 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
