--[[
	CameraUtils - Math utility functions shared by multiple camera scripts
	2018 Camera Update - AllYourBlox
--]]

--!strict

local FFlagUserCameraToggleDontSetMouseIconEveryFrame
do
	local success, value = pcall(function()
		return UserSettings():IsUserFeatureEnabled("UserCameraToggleDontSetMouseIconEveryFrame")
	end)
	FFlagUserCameraToggleDontSetMouseIconEveryFrame = success and value
end

local FFlagUserCameraToggleDontSetMouseBehaviorOrRotationTypeEveryFrame
do
	local success, value = pcall(function()
		return UserSettings():IsUserFeatureEnabled("UserCameraToggleDontSetMouseBehaviorOrRotationTypeEveryFrame")
	end)
	FFlagUserCameraToggleDontSetMouseBehaviorOrRotationTypeEveryFrame = success and value
end

local Players = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")
local UserGameSettings = UserSettings():GetService("UserGameSettings")

local CameraUtils = {}

local function round(num: number)
	return math.floor(num + 0.5)
end

-- Critically damped spring class for fluid motion effects
local Spring = {} do
	Spring.__index = Spring

	-- Initialize to a given undamped frequency and default position
	function Spring.new(freq, pos)
		return setmetatable({
			freq = freq,
			goal = pos,
			pos = pos,
			vel = 0,
		}, Spring)
	end

	-- Advance the spring simulation by `dt` seconds
	function Spring:step(dt: number)
		local f: number = self.freq::number * 2.0 * math.pi
		local g: Vector3 = self.goal
		local p0: Vector3 = self.pos
		local v0: Vector3 = self.vel

		local offset = p0 - g
		local decay = math.exp(-f*dt)

		local p1 = (offset*(1 + f*dt) + v0*dt)*decay + g
		local v1 = (v0*(1 - f*dt) - offset*(f*f*dt))*decay

		self.pos = p1
		self.vel = v1

		return p1
	end
end

CameraUtils.Spring = Spring

-- map a value from one range to another
function CameraUtils.map(x: number, inMin: number, inMax: number, outMin: number, outMax: number): number
	return (x - inMin)*(outMax - outMin)/(inMax - inMin) + outMin
end

-- maps a value from one range to another, clamping to the output range. order does not matter
function CameraUtils.mapClamp(x: number, inMin: number, inMax: number, outMin: number, outMax: number): number
	return math.clamp(
		(x - inMin)*(outMax - outMin)/(inMax - inMin) + outMin,
		math.min(outMin, outMax),
		math.max(outMin, outMax)
	)
end

-- Ritter's loose bounding sphere algorithm
function CameraUtils.getLooseBoundingSphere(parts: {BasePart})
	local points = table.create(#parts)
	for idx, part in pairs(parts) do
		points[idx] = part.Position
	end
	
	-- pick an arbitrary starting point
	local x = points[1]

	-- get y, the point furthest from x
	local y = x
	local yDist = 0

	for _, p in ipairs(points) do
		local pDist = (p - x).Magnitude

		if pDist > yDist then
			y = p
			yDist = pDist
		end
	end

	-- get z, the point furthest from y
	local z = y
	local zDist = 0

	for _, p in ipairs(points) do
		local pDist = (p - y).Magnitude

		if pDist > zDist then
			z = p
			zDist = pDist
		end
	end

	-- use (y, z) as the initial bounding sphere
	local sc = (y + z)*0.5
	local sr = (y - z).Magnitude*0.5

	-- expand sphere to fit any outlying points
	for _, p in ipairs(points) do
		local pDist = (p - sc).Magnitude

		if pDist > sr then
			-- shift to midpoint
			sc = sc + (pDist - sr)*0.5*(p - sc).Unit

			-- expand
			sr = (pDist + sr)*0.5
		end
	end

	return sc, sr
end

-- canonicalize an angle to +-180 degrees
function CameraUtils.sanitizeAngle(a: number): number
	return (a + math.pi)%(2*math.pi) - math.pi
end

-- From TransparencyController
function CameraUtils.Round(num: number, places: number): number
	local decimalPivot = 10^places
	return math.floor(num * decimalPivot + 0.5) / decimalPivot
end

function CameraUtils.IsFinite(val: number): boolean
	return val == val and val ~= math.huge and val ~= -math.huge
end

function CameraUtils.IsFiniteVector3(vec3: Vector3): boolean
	return CameraUtils.IsFinite(vec3.X) and CameraUtils.IsFinite(vec3.Y) and CameraUtils.IsFinite(vec3.Z)
end

-- Legacy implementation renamed
function CameraUtils.GetAngleBetweenXZVectors(v1: Vector3, v2: Vector3): number
	return math.atan2(v2.X*v1.Z-v2.Z*v1.X, v2.X*v1.X+v2.Z*v1.Z)
end

function CameraUtils.RotateVectorByAngleAndRound(camLook: Vector3, rotateAngle: number, roundAmount: number): number
	if camLook.Magnitude > 0 then
		camLook = camLook.Unit
		local currAngle = math.atan2(camLook.Z, camLook.X)
		local newAngle = round((math.atan2(camLook.Z, camLook.X) + rotateAngle) / roundAmount) * roundAmount
		return newAngle - currAngle
	end
	return 0
end

-- K is a tunable parameter that changes the shape of the S-curve
-- the larger K is the more straight/linear the curve gets
local k = 0.35
local lowerK = 0.8
local function SCurveTranform(t: number)
	t = math.clamp(t, -1, 1)
	if t >= 0 then
		return (k*t) / (k - t + 1)
	end
	return -((lowerK*-t) / (lowerK + t + 1))
end

local DEADZONE = 0.1
local function toSCurveSpace(t: number)
	return (1 + DEADZONE) * (2*math.abs(t) - 1) - DEADZONE
end

local function fromSCurveSpace(t: number)
	return t/2 + 0.5
end

function CameraUtils.GamepadLinearToCurve(thumbstickPosition: Vector2)
	local function onAxis(axisValue)
		local sign = 1
		if axisValue < 0 then
			sign = -1
		end
		local point = fromSCurveSpace(SCurveTranform(toSCurveSpace(math.abs(axisValue))))
		point = point * sign
		return math.clamp(point, -1, 1)
	end
	return Vector2.new(onAxis(thumbstickPosition.X), onAxis(thumbstickPosition.Y))
end

-- This function converts 4 different, redundant enumeration types to one standard so the values can be compared
function CameraUtils.ConvertCameraModeEnumToStandard(enumValue: 
		Enum.TouchCameraMovementMode | 
		Enum.ComputerCameraMovementMode | 
		Enum.DevTouchCameraMovementMode |
		Enum.DevComputerCameraMovementMode): Enum.ComputerCameraMovementMode | Enum.DevComputerCameraMovementMode
	if enumValue == Enum.TouchCameraMovementMode.Default then
		return Enum.ComputerCameraMovementMode.Follow
	end

	if enumValue == Enum.ComputerCameraMovementMode.Default then
		return Enum.ComputerCameraMovementMode.Classic
	end

	if enumValue == Enum.TouchCameraMovementMode.Classic or
		enumValue == Enum.DevTouchCameraMovementMode.Classic or
		enumValue == Enum.DevComputerCameraMovementMode.Classic or
		enumValue == Enum.ComputerCameraMovementMode.Classic then
		return Enum.ComputerCameraMovementMode.Classic
	end

	if enumValue == Enum.TouchCameraMovementMode.Follow or
		enumValue == Enum.DevTouchCameraMovementMode.Follow or
		enumValue == Enum.DevComputerCameraMovementMode.Follow or
		enumValue == Enum.ComputerCameraMovementMode.Follow then
		return Enum.ComputerCameraMovementMode.Follow
	end

	if enumValue == Enum.TouchCameraMovementMode.Orbital or
		enumValue == Enum.DevTouchCameraMovementMode.Orbital or
		enumValue == Enum.DevComputerCameraMovementMode.Orbital or
		enumValue == Enum.ComputerCameraMovementMode.Orbital then
		return Enum.ComputerCameraMovementMode.Orbital
	end

	if enumValue == Enum.ComputerCameraMovementMode.CameraToggle or
		enumValue == Enum.DevComputerCameraMovementMode.CameraToggle then
		return Enum.ComputerCameraMovementMode.CameraToggle
	end

	-- Note: Only the Dev versions of the Enums have UserChoice as an option
	if enumValue == Enum.DevTouchCameraMovementMode.UserChoice or
		enumValue == Enum.DevComputerCameraMovementMode.UserChoice then
		return Enum.DevComputerCameraMovementMode.UserChoice
	end

	-- For any unmapped options return Classic camera
	return Enum.ComputerCameraMovementMode.Classic
end

if FFlagUserCameraToggleDontSetMouseIconEveryFrame then
	local function getMouse()
		local localPlayer = Players.localPlayer
		if not localPlayer then
			Players:GetPropertyChangedSignal("LocalPlayer"):Wait()
			localPlayer = Players.localPlayer
		end
		return localPlayer:GetMouse()
	end

	local savedMouseIcon: string = ""
	local lastMouseIconOverride: string? = nil
	function CameraUtils.setMouseIconOverride(icon: string)
		local mouse = getMouse()
		-- Only save the icon if it was written by another script.
		if mouse.Icon ~= lastMouseIconOverride then
			savedMouseIcon = mouse.Icon
		end

		mouse.Icon = icon
		lastMouseIconOverride = icon
	end

	function CameraUtils.restoreMouseIcon()
		local mouse = getMouse()
		-- Only restore if it wasn't overwritten by another script.
		if mouse.Icon == lastMouseIconOverride then
			mouse.Icon = savedMouseIcon
		end
		lastMouseIconOverride = nil
	end
end

if FFlagUserCameraToggleDontSetMouseBehaviorOrRotationTypeEveryFrame then
	local savedMouseBehavior: Enum.MouseBehavior = Enum.MouseBehavior.Default
	local lastMouseBehaviorOverride: Enum.MouseBehavior? = nil
	function CameraUtils.setMouseBehaviorOverride(value: Enum.MouseBehavior)
		if UserInputService.MouseBehavior ~= lastMouseBehaviorOverride then
			savedMouseBehavior = UserInputService.MouseBehavior
		end

		UserInputService.MouseBehavior = value
		lastMouseBehaviorOverride = value
	end

	function CameraUtils.restoreMouseBehavior()
		if UserInputService.MouseBehavior == lastMouseBehaviorOverride then
			UserInputService.MouseBehavior = savedMouseBehavior
		end
		lastMouseBehaviorOverride = nil
	end

	local savedRotationType: Enum.RotationType = Enum.RotationType.MovementRelative
	local lastRotationTypeOverride: Enum.RotationType? = nil
	function CameraUtils.setRotationTypeOverride(value: Enum.RotationType)
		if UserGameSettings.RotationType ~= lastRotationTypeOverride then
			savedRotationType = UserGameSettings.RotationType
		end

		UserGameSettings.RotationType = value
		lastRotationTypeOverride = value
	end

	function CameraUtils.restoreRotationType()
		if UserGameSettings.RotationType == lastRotationTypeOverride then
			UserGameSettings.RotationType = savedRotationType
		end
		lastRotationTypeOverride = nil
	end
end

return CameraUtils

