//	GGVRSpaceGeometry.swift
//
//	© 2026 by Jeff Weeks
//	See TermsOfUse.txt

class SpaceGeometry {

	let geometryType: GeometryType

	//	The "radius of the space" makes sense
	//	in all three geometries, as explained in §1.2 of
	//
	//		Jeff Weeks,
	//		Body coherence in curved-space virtual reality games,
	//		Computers & Graphics 97 (2021) 28–41.
	//
	//		  https://www.sciencedirect.com/science/article/pii/S0097849321000443
	//		or
	//		  https://arxiv.org/abs/2011.00510
	//
	//	We'll often work in dimensionless radians, even
	//	in Euclidean geometry. For an explanation of radians
	//	in Euclidean geometry, see §4.1 of that same article.
	//
	let spaceRadius: Double	//	in meters
	
	let spaceDistanceFunction:	//	a function
		@Sendable (
			_ pointA: SIMD4<Double>,	//	in meters
			_ pointB: SIMD4<Double>		//	in meters
		) -> Double						//	returns the distance between points A and B in meters,
										//	as measured along a geodesic in the space

	let northPole: SIMD4<Double>	//	in meters

	init(
		geometryType: GeometryType,
		spaceRadius: Double	//	in meters
	) {
		self.geometryType = geometryType
		self.spaceRadius = spaceRadius

		spaceDistanceFunction = SpaceGeometry.computeSpaceDistanceFunction(
									geometryType: geometryType,
									spaceRadius: spaceRadius)

		northPole = SIMD4<Double>(0.0, 0.0, 0.0, spaceRadius)
	}
	
	private static func computeSpaceDistanceFunction(
		geometryType: GeometryType,
		spaceRadius: Double
	) -> @Sendable (					//	a function:
			_ pointA: SIMD4<Double>,	//	in meters
			_ pointB: SIMD4<Double>		//	in meters
		 ) -> Double					//	returns the distance between points A and B in meters,
										//	as measured along a geodesic in the space
	{
		switch geometryType {
		
		case .spherical:

			return {
				@Sendable (_ pointA: SIMD4<Double>, _ pointB: SIMD4<Double>) -> Double in
				
				//	In the 4-dimensional Euclidean embedding space,
				//	pointA and pointB both have length
				//	equal to the spaceRadius. For conciseness, let
				//
				//		ρ denote the spaceRadius
				//		d denote the great-circle distance between the points, in meters.
				//
				//	Taking the dot product of A and B gives
				//
				//		A·B = |A| |B| cos(d/ρ)
				//			= ρ² cos(d/ρ)
				//	so
				//		d = ρ acos(A·B / ρ²)
				//

				let theDotProduct: Double
					= pointA.x * pointB.x
					+ pointA.y * pointB.y
					+ pointA.z * pointB.z
					+ pointA.w * pointB.w
									
				let theNormalizedDotProduct = theDotProduct / (spaceRadius * spaceRadius)
				
				//	Guard against theNormalizedDotProduct = 1.0000001 or similar.
				let theSafeDotProduct = min(max(theNormalizedDotProduct, -1.0), +1.0)
					
				let theDistanceInRadians = acos(theSafeDotProduct)
				
				let theDistanceInMeters = spaceRadius * theDistanceInRadians
				
				return theDistanceInMeters
			}

		case .euclidean:

			return {
				@Sendable (_ pointA: SIMD4<Double>, _ pointB: SIMD4<Double>) -> Double in
				
				let Δp = SIMD3<Double>(
					pointB.x - pointA.x,
					pointB.y - pointA.y,
					pointB.z - pointA.z
			// omit pointB.w - pointA.w = 0.0
				)
				
				return length(Δp)
			}
			
		case .hyperbolic:

			return {
				@Sendable (_ pointA: SIMD4<Double>, _ pointB: SIMD4<Double>) -> Double in
				
				//	The hyperbolic case is similar to the spherical case,
				//	except that the hyperbolic 3-space is embedded in
				//	a (3+1)-dimensional Minkowski space.
				//	Taking the dot product of A and B gives
				//
				//		A·B = - |A| |B| cosh(d/ρ)
				//			= - ρ² cosh(d/ρ)
				//	so
				//		d = ρ acosh( - A·B / ρ² )
				//

				let theDotProduct: Double
					= pointA.x * pointB.x
					+ pointA.y * pointB.y
					+ pointA.z * pointB.z
					- pointA.w * pointB.w
				
				let theNormalizedDotProduct = -theDotProduct / (spaceRadius * spaceRadius)	//	note the minus sign
				
				//	Guard against theNormalizedDotProduct = 0.9999999 or similar.
				let theSafeDotProduct = max(theNormalizedDotProduct, +1.0)
					
				let theDistanceInRadians = acosh(theSafeDotProduct)
				
				let theDistanceInMeters = spaceRadius * theDistanceInRadians
				
				return theDistanceInMeters
			}
		}
	}

	func normalizeToSpaceRadius(
		_ v: SIMD4<Double>
	) -> SIMD4<Double> {
	
		let theUnitVector = geometryType.normalize(v)
		let theResult = spaceRadius * theUnitVector
		
		return theResult
	}
}

enum GeometryType: String, CaseIterable, Identifiable, Codable {
	
	case spherical
	case euclidean
	case hyperbolic

	var id: String { self.rawValue }

	var name: String {
	
		switch self {
		case .spherical:	return "Spherical"
		case .euclidean:	return "Euclidean"
		case .hyperbolic:	return "Hyperbolic"
		}
	}
	
	var lowerCaseName: String {
	
		switch self {
		case .spherical:	return "spherical"
		case .euclidean:	return "Euclidean"
		case .hyperbolic:	return "hyperbolic"
		}
	}

	var geometrySign: Double {
	
		switch self {
		
		case .spherical:
			return +1.0
			
		case .euclidean:
			return  0.0
			
		case .hyperbolic:
			return -1.0
		}
	}
	
	func normalize(
		_ v: SIMD4<Double>
	) -> SIMD4<Double> {
	
		//	This function is for normalizing points.
		//	To normalize vectors orthogonal to points
		//	we'd need to use a more general system.
	
		let theLengthSquared: Double
		switch self {
		
		case .spherical:
			theLengthSquared = v.x * v.x  +  v.y * v.y  +  v.z * v.z + v.w * v.w
			
		case .euclidean:
			theLengthSquared = v.w * v.w
			
		case .hyperbolic:
			theLengthSquared = -(v.x * v.x  +  v.y * v.y  +  v.z * v.z) + v.w * v.w
		}
		
		precondition(
			theLengthSquared > 0.0,
			"GeometryType.normalize() received a vector of nonpositive squared length")
		
		let theUnitVector = v / theLengthSquared.squareRoot()
		
		return theUnitVector
	}
	
	var metricPair: simd_double2x4 {
	
		switch self {
		
		case .spherical:
			return simd_double2x4(
				SIMD4<Double>(+1, +1, +1, +1),
				SIMD4<Double>(+1, +1, +1, +1)
			)
			
		case .euclidean:
			return simd_double2x4(
				SIMD4<Double>(+1, +1, +1,  0),	//	horizontal metric
				SIMD4<Double>( 0,  0,  0, +1)	//	vertical metric
			)
			
		case .hyperbolic:
			return simd_double2x4(
				SIMD4<Double>(+1, +1, +1, -1),	//	for spacelike vectors
				SIMD4<Double>(-1, -1, -1, +1)	//	for timelike vectors
			)
		}
	}
	
	var gpuGeometryCode: UInt8 {
	
		//	Let's pass the GeometryType to the GPU as a UInt8,
		//	which the vertex function function may interpret
		//	as an ASCII character.
		//
		//		Note:  This information could also be
		//		passed as an enum, but I hestitate to define
		//		a shared enum in GGVR_GPU_Definitions.h
		//		for two reasons:
		//
		//		1. It can be a little delicate to ensure
		//		that the Swift code and the Metal code
		//		both see the enum as the same size integer.
		//
		//		2. I'm not eager to define a second enum
		//		that might cause confusion with the present
		//		GeometryType, which is a Swift enum.
		//
	
		switch self {
		case .spherical:	return UInt8(ascii: "s")
		case .euclidean:	return UInt8(ascii: "e")
		case .hyperbolic:	return UInt8(ascii: "h")
		}
	}
}
