-- This loads from, or lazily creates, NumberValue objects for exposed parameters
local function LoadNumberValueParameters()
	-- These initial values do not require change listeners since they are read only once
	LoadOrCreateNumberValueParameter("InitialElevation", "NumberValue", nil)
	LoadOrCreateNumberValueParameter("InitialDistance", "NumberValue", nil)
	
	-- Note: ReferenceAzimuth is also used as an initial value, but needs a change listener because it is used in the calculation of the limits
	LoadOrCreateNumberValueParameter("ReferenceAzimuth", "NumberValue", SetAndBoundsCheckAzimuthValues)
	LoadOrCreateNumberValueParameter("CWAzimuthTravel", "NumberValue", SetAndBoundsCheckAzimuthValues)
	LoadOrCreateNumberValueParameter("CCWAzimuthTravel", "NumberValue", SetAndBoundsCheckAzimuthValues)
	LoadOrCreateNumberValueParameter("MinElevation", "NumberValue", SetAndBoundsCheckElevationValues)
	LoadOrCreateNumberValueParameter("MaxElevation", "NumberValue", SetAndBoundsCheckElevationValues)
	LoadOrCreateNumberValueParameter("MinDistance", "NumberValue", SetAndBoundsCheckDistanceValues)
	LoadOrCreateNumberValueParameter("MaxDistance", "NumberValue", SetAndBoundsCheckDistanceValues)
	LoadOrCreateNumberValueParameter("UseAzimuthLimits", "BoolValue", SetAndBoundsCheckAzimuthValues)
	
	-- Internal values set (in radians, from degrees), plus sanitization
	curAzimuthRad = math.rad(externalProperties["ReferenceAzimuth"])
	curElevationRad = math.rad(externalProperties["InitialElevation"])
	curDistance = externalProperties["InitialDistance"]
	
	SetAndBoundsCheckAzimuthValues()
	SetAndBoundsCheckElevationValues()
	SetAndBoundsCheckDistanceValues()
end

local function CreateOrbitalCamera()
	local module = RootCameraCreator()
	
	LoadNumberValueParameters()	
	
	module.DefaultZoom = curDistance
	
	local tweenAcceleration = math_rad(220)
	local tweenSpeed = math_rad(0)
	local tweenMaxSpeed = math_rad(250)
	local timeBeforeAutoRotate = 2
	
	local lastUpdate = tick()
	module.LastUserPanCamera = tick()
	
	function module:Update()
		module:ProcessTweens()
		local now = tick()
		local timeDelta = (now - lastUpdate)
		local userPanningTheCamera = (self.UserPanningTheCamera == true)
		local camera = 	workspace.CurrentCamera
		local player = PlayersService.LocalPlayer
		local humanoid = self:GetHumanoid()
		local cameraSubject = camera and camera.CameraSubject
		local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat')
		local isOnASkateboard = cameraSubject and cameraSubject:IsA('SkateboardPlatform')
		
		if lastUpdate == nil or now - lastUpdate > 1 then
			module:ResetCameraLook()
			self.LastCameraTransform = nil
		end	
		
		if lastUpdate then
			local gamepadRotation = self:UpdateGamepad()
			
			if self:ShouldUseVRRotation() then				
				self.RotateInput = self.RotateInput + self:GetVRRotationInput()
			else
				-- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from
				local delta = math_min(0.1, now - lastUpdate)
				
				if gamepadRotation ~= ZERO_VECTOR2 then
					userPanningTheCamera = true
					self.RotateInput = self.RotateInput + (gamepadRotation * delta)
				end

				local angle = 0
				if not (isInVehicle or isOnASkateboard) then
					angle = angle + (self.TurningLeft and -120 or 0)
					angle = angle + (self.TurningRight and 120 or 0)
				end
				
				if angle ~= 0 then
					self.RotateInput = self.RotateInput +  Vector2.new(math_rad(angle * delta), 0)
					userPanningTheCamera = true
				end
			end
		end

		-- Reset tween speed if user is panning
		if userPanningTheCamera then
			tweenSpeed = 0
			module.LastUserPanCamera = tick()
		end
		
		local userRecentlyPannedCamera = now - module.LastUserPanCamera < timeBeforeAutoRotate
		local subjectPosition = self:GetSubjectPosition()
		
		if subjectPosition and player and camera then
		
			
			local zoom = self:ZoomCamera(curDistance * currentZoomSpeed)
			
			
			if not userPanningTheCamera and self.LastCameraTransform then
				local isInFirstPerson = self:IsInFirstPerson()
				if (isInVehicle or isOnASkateboard) and lastUpdate and humanoid and humanoid.Torso then
					if isInFirstPerson then
						if self.LastSubjectCFrame and (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
							local y = -findAngleBetweenXZVectors(self.LastSubjectCFrame.lookVector, cameraSubject.CFrame.lookVector)
							if IsFinite(y) then
								self.RotateInput = self.RotateInput + Vector2.new(y, 0)
							end
							tweenSpeed = 0
						end
					elseif not userRecentlyPannedCamera then
						local forwardVector = humanoid.Torso.CFrame.lookVector
						if isOnASkateboard then
							forwardVector = cameraSubject.CFrame.lookVector
						end
						
						tweenSpeed = clamp(0, tweenMaxSpeed, tweenSpeed + tweenAcceleration * timeDelta)

						local percent = clamp(0, 1, tweenSpeed * timeDelta)
						if self:IsInFirstPerson() then
							percent = 1
						end
						
						local y = findAngleBetweenXZVectors(forwardVector, self:GetCameraLook())
						if IsFinite(y) and math_abs(y) > 0.0001 then
							self.RotateInput = self.RotateInput + Vector2.new(y * percent, 0)
						end
					end
				end
			end
			
			local VREnabled = VRService.VREnabled
			camera.Focus = VREnabled and self:GetVRFocus(subjectPosition, timeDelta) or CFrame_new(subjectPosition)

			local cameraFocusP = camera.Focus.p
			if VREnabled and not self:IsInFirstPerson() then
				local cameraHeight = self:GetCameraHeight()
				local vecToSubject = (subjectPosition - camera.CFrame.p)
				local distToSubject = vecToSubject.magnitude

				-- Only move the camera if it exceeded a maximum distance to the subject in VR
				if distToSubject > zoom or self.RotateInput.x ~= 0 then
					local desiredDist = math_min(distToSubject, zoom)
					vecToSubject = self:RotateCamera(vecToSubject.unit * XZ_VECTOR, Vector2.new(self.RotateInput.x, 0)) * desiredDist
					local newPos = cameraFocusP - vecToSubject
					local desiredLookDir = camera.CFrame.lookVector
					if self.RotateInput.x ~= 0 then
						desiredLookDir = vecToSubject
					end
					local lookAt = Vector3.new(newPos.x + desiredLookDir.x, newPos.y, newPos.z + desiredLookDir.z)
					self.RotateInput = ZERO_VECTOR2
					
					camera.CFrame = CFrame_new(newPos, lookAt) + Vector3_new(0, cameraHeight, 0)
				end
			else
				-- self.RotateInput is a Vector2 of mouse movement deltas since last update
				curAzimuthRad = curAzimuthRad - self.RotateInput.x
				
				if useAzimuthLimits then
					curAzimuthRad = clamp(curAzimuthRad, minAzimuthAbsoluteRad, maxAzimuthAbsoluteRad)
				else
					curAzimuthRad = (curAzimuthRad ~= 0) and (math.sign(curAzimuthRad) * (math.abs(curAzimuthRad) % TAU)) or 0
				end

				curDistance = clamp(zoom, minDistance, maxDistance)
				
				curElevationRad = clamp(curElevationRad + self.RotateInput.y, minElevationRad,maxElevationRad)
				
				local cameraPosVector = curDistance * ( CFrame.fromEulerAnglesYXZ( -curElevationRad, curAzimuthRad, 0 ) * UNIT_Z ) 
				local camPos = subjectPosition + cameraPosVector
				
				camera.CFrame = CFrame.new(camPos, subjectPosition)
			
				self.RotateInput = ZERO_VECTOR2
			end

			self.LastCameraTransform = camera.CFrame
			self.LastCameraFocus = camera.Focus
			if (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
				self.LastSubjectCFrame = cameraSubject.CFrame
			else
				self.LastSubjectCFrame = nil
			end
		end
		
		lastUpdate = now
	end
	
	function module:GetCameraZoom()
		return curDistance
	end
	
	function module:ZoomCamera(desiredZoom)
		local player = PlayersService.LocalPlayer
		if player then
			curDistance = clamp(desiredZoom, minDistance, maxDistance)
		end

		local isFirstPerson = self:GetCameraZoom() < 2

		--ShiftLockController:SetIsInFirstPerson(isFirstPerson)
		-- set mouse behavior
		self:UpdateMouseBehavior()
		return self:GetCameraZoom()
	end
	
	function module:ZoomCameraBy(zoomScale)
		local newDist = curDistance
		-- Can break into more steps to get more accurate integration
        newDist = self:rk4Integrator(curDistance, zoomScale, 1)
		self:ZoomCamera(newDist)
		return self:GetCameraZoom()
	end

	function module:ZoomCameraFixedBy(zoomIncrement)
		return self:ZoomCamera(self:GetCameraZoom() + zoomIncrement)
	end
	
	function module:SetInitialOrientation(humanoid)
		local newDesiredLook = (humanoid.Torso.CFrame.lookVector - Vector3.new(0,0.23,0)).unit
		local horizontalShift = findAngleBetweenXZVectors(newDesiredLook, self:GetCameraLook())
		local vertShift = math.asin(self:GetCameraLook().y) - math.asin(newDesiredLook.y)
		if not IsFinite(horizontalShift) then
			horizontalShift = 0
		end
		if not IsFinite(vertShift) then
			vertShift = 0
		end
		self.RotateInput = Vector2.new(horizontalShift, vertShift)
	end

	function module.getGamepadPan(name, state, input)
		if input.UserInputType == module.activeGamepad and input.KeyCode == Enum.KeyCode.Thumbstick2 then
			if r3ButtonDown or l3ButtonDown then
			-- R3 or L3 Thumbstick is depressed, right stick controls dolly in/out
				if (input.Position.Y > THUMBSTICK_DEADZONE) then
					currentZoomSpeed = 0.96
				elseif (input.Position.Y < -THUMBSTICK_DEADZONE) then
					currentZoomSpeed = 1.04
				else 
					currentZoomSpeed = 1.00
				end
			else
				if state == Enum.UserInputState.Cancel then
					module.GamepadPanningCamera = ZERO_VECTOR2
					return
				end		
				
				local inputVector = Vector2.new(input.Position.X, -input.Position.Y)
				if inputVector.magnitude > THUMBSTICK_DEADZONE then
					module.GamepadPanningCamera = Vector2.new(input.Position.X, -input.Position.Y)
				else
					module.GamepadPanningCamera = ZERO_VECTOR2
				end
			end
		end
	end

	function module.doGamepadZoom(name, state, input)
		if input.UserInputType == module.activeGamepad and (input.KeyCode == Enum.KeyCode.ButtonR3 or input.KeyCode == Enum.KeyCode.ButtonL3) then
			if (state == Enum.UserInputState.Begin) then
				r3ButtonDown = input.KeyCode == Enum.KeyCode.ButtonR3
				l3ButtonDown = input.KeyCode == Enum.KeyCode.ButtonL3
			elseif (state == Enum.UserInputState.End) then
				if (input.KeyCode == Enum.KeyCode.ButtonR3) then
					r3ButtonDown = false
				elseif (input.KeyCode == Enum.KeyCode.ButtonL3) then
					l3ButtonDown = false
				end
				if (not r3ButtonDown) and (not l3ButtonDown) then
					currentZoomSpeed = 1.00
				end
			end
		end
	end
	
	function module:BindGamepadInputActions()
		local ContextActionService = game:GetService('ContextActionService')
		ContextActionService:BindAction("OrbitalCamGamepadPan", module.getGamepadPan, false, Enum.KeyCode.Thumbstick2)
		ContextActionService:BindAction("OrbitalCamGamepadZoom", module.doGamepadZoom, false, Enum.KeyCode.ButtonR3)
		ContextActionService:BindAction("OrbitalCamGamepadZoomAlt", module.doGamepadZoom, false, Enum.KeyCode.ButtonL3)
	end
	
	return module
end

return CreateOrbitalCamera