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

import Metal


extension NEBRenderer {

	//	Fundamental cells for use with the checkerboard tilings.

	struct WallData {
		//	Each face winds clockwise when viewed from the cell's center
		//	in a right-handed coordinate system.
		let corners: [SIMD4<Double>]		//	positions (x,y,z,w) in radians
		let color: simd_half3				//	(r,g,b) in DisplayP3 color space
		let mayCoincideWithPoolTable: Bool	//	Should we omit this wall in the central cell?
	}

	struct CellWall {

		//	The wall is modeled by a triangle strip of NEBVertex instances.
		//	The first face winds clockwise when viewed from the cell's center
		//	in a right-handed coordinate system.
		let vertices: MTLBuffer
		let vertexCount: Int	//	number of vertices in 'vertices'
		let color: simd_half3	//	(r,g,b) in DisplayP3 color space
		let mayCoincideWithPoolTable: Bool	//	Should we omit this wall in the central cell?
	}


// MARK: -
// MARK: Cell walls

	func makeCheckerboardCellWalls(
		device: MTLDevice,
		numRecursiveSubdivisions: Int,
		withFlange: Bool,	//	Serves as adapter between high LOD and low LOD.
							//	See comment in makeCellWall() below for details.
		multicolorLattice: Bool
	) -> [CellWall] {

		let thePolyhedronMaker: (Bool) -> [WallData]
		switch spaceGeometry.geometryType {
		case .spherical:	thePolyhedronMaker = makeRightAngledTetrahedron
		case .euclidean:	thePolyhedronMaker = makeRightAngledCube
		case .hyperbolic:	thePolyhedronMaker = makeRightAngledDodecahedron
		}
		let theWallData = thePolyhedronMaker(multicolorLattice)
		
		let theCellWalls = theWallData.compactMap() { wallData in
		
			makeCellWall(
				device: device,
				wallData: wallData,
				numRecursiveSubdivisions: numRecursiveSubdivisions,
				withFlange: withFlange)
		}
		
		return theCellWalls
	}

	func makeCellWall(
		device: MTLDevice,
		wallData: WallData,
		numRecursiveSubdivisions: Int,
		withFlange: Bool //	Serves as adapter between high LOD and low LOD.
						 //	See comment below for details.
	) -> CellWall? {

		var theCorners = wallData.corners.map() { cornerInRadians in
			spaceGeometry.spaceRadius * cornerInRadians
		}
		
		let theRawFaceCenter = theCorners.reduce(.zero, +)
		let theFaceCenter = spaceGeometry.normalizeToSpaceRadius(theRawFaceCenter)

		//	Append a copy of the first vertex to the end
		//	of the array as well, so the triangle strip will
		//	run all the way around the face. Note that we
		//	do this *after* computing theFaceCenter.
		//
		//		Note: theCorners is never an empty array.
		//
		theCorners.append(theCorners[0])
		
		let theInnerCorners = theCorners.map() { corner in
			spaceGeometry.normalizeToSpaceRadius(
				Double(0.875) * corner + Double(0.125) * theFaceCenter)
		}
		
		//	In a azimuthal equidistant projection in hyperbolic geometry,
		//	the renderer uses a level-of-detail scheme: it draws
		//	the central dodecahedron and its 30 immediate neighbors
		//	at full detail and then draws all remaining instances
		//	at low detail. To avoid a visible gap where a low-detail
		//	dodecahedron meets a high-detail one, we give each wall
		//	of the low-detail dodecahedron a small flange so that
		//	it slightly overlaps its neighbors.
		//
		let theOuterCorners =
			withFlange ?
				theCorners.map() { corner in
					spaceGeometry.normalizeToSpaceRadius(
					Double(1.01) * corner + Double(-0.01) * theFaceCenter)
				}
			: theCorners

		var theInnerVertices = theInnerCorners
		for _ in 0 ..< numRecursiveSubdivisions {
			
				theInnerVertices = subdivideSegments(vertices: theInnerVertices)
		}

		var theOuterVertices = theOuterCorners
		for _ in 0 ..< numRecursiveSubdivisions {
			
				theOuterVertices = subdivideSegments(vertices: theOuterVertices)
		}
		
		let theVertexPairs = zip(theInnerVertices, theOuterVertices)

		let theTriangleStripVertices = theVertexPairs.flatMap() {
			(innerVertex, outerVertex) in

			[
				NEBVertex(pos: SIMD4<Float32>(innerVertex), tex: .zero),
				NEBVertex(pos: SIMD4<Float32>(outerVertex), tex: .zero)
			]
		}

		let theVertexCount = theTriangleStripVertices.count
		guard let theVertexBuffer = device.makeBuffer(
				bytes: theTriangleStripVertices,
				length: theVertexCount * MemoryLayout<NEBVertex>.stride,
				options: [.storageModeShared]
		) else {
			assertionFailure("Couldn't make theVertexBuffer in makeCellWall()")
			return nil
		}

		let theCellWall = CellWall(
							vertices: theVertexBuffer,
							vertexCount: theVertexCount,
							color: wallData.color,
							mayCoincideWithPoolTable: wallData.mayCoincideWithPoolTable)
		
		return theCellWall
	}

	private func subdivideSegments(
		vertices: [SIMD4<Double>]
	) -> [SIMD4<Double>] {

		var theIterator = vertices.makeIterator()
		guard var v = theIterator.next()
		else {
			preconditionFailure("subdivideSegments() received an empty Array")
		}

		var theSubdividedSegments = [v]

		while let theNextV = theIterator.next() {
		
			let theRawMidpoint = 0.5 * (v + theNextV)
			let theMidpoint = spaceGeometry.normalizeToSpaceRadius(theRawMidpoint)
			
			theSubdividedSegments.append(theMidpoint)
			theSubdividedSegments.append(theNextV)
			
			v = theNextV
		}
		
		return theSubdividedSegments
	}


// MARK: -
// MARK: Wall data - spherical tetrahedron

	func makeRightAngledTetrahedron(
		multicolorLattice: Bool
	) -> [WallData] {

		let colorA: simd_half3
		let colorB: simd_half3
		let colorC: simd_half3
		let colorD: simd_half3
		if multicolorLattice {
			colorA = HSVtoRGB(hue: 0.47, saturation: 1.000, value: 0.6)	//	cyan
			colorB = HSVtoRGB(hue: 0.60, saturation: 0.875, value: 0.6)	//	blue
			colorC = HSVtoRGB(hue: 0.38, saturation: 1.000, value: 0.6)	//	green
			colorD = HSVtoRGB(hue: 0.68, saturation: 0.750, value: 0.6)	//	purple
		} else {
			colorA = HSVtoRGB(hue: 0.70, saturation: 0.875, value: 0.5)
			colorB = HSVtoRGB(hue: 0.70, saturation: 0.875, value: 0.6)
			colorC = HSVtoRGB(hue: 0.70, saturation: 0.875, value: 0.7)
			colorD = HSVtoRGB(hue: 0.70, saturation: 0.875, value: 0.8)
		}

		//	Faces wind clockwise when viewed
		//	from inside the tetrahedron
		//	in a right-handed coordinate system.
		let theTetrahedron: [WallData] = [

			WallData(
				corners:
					[
						SIMD4<Double>( 0.0, 0.0, 0.0, 1.0 ),
						SIMD4<Double>( 0.0, 0.0, 1.0, 0.0 ),
						SIMD4<Double>( 0.0, 1.0, 0.0, 0.0 )
					],
				color: colorA,
				mayCoincideWithPoolTable: false
			),
			WallData(
				corners:
					[
						SIMD4<Double>( 0.0, 0.0, 0.0, 1.0 ),
						SIMD4<Double>( 0.0, 1.0, 0.0, 0.0 ),
						SIMD4<Double>( 1.0, 0.0, 0.0, 0.0 )
					],
				color: colorB,
				mayCoincideWithPoolTable: false
			),
			WallData(
				corners:
					[
						SIMD4<Double>( 0.0, 0.0, 0.0, 1.0 ),
						SIMD4<Double>( 1.0, 0.0, 0.0, 0.0 ),
						SIMD4<Double>( 0.0, 0.0, 1.0, 0.0 )
					],
				color: colorC,
				mayCoincideWithPoolTable: true
			),
			WallData(
				corners:
					[
						SIMD4<Double>( 1.0, 0.0, 0.0, 0.0 ),
						SIMD4<Double>( 0.0, 1.0, 0.0, 0.0 ),
						SIMD4<Double>( 0.0, 0.0, 1.0, 0.0 )
					],
				color: colorD,
				mayCoincideWithPoolTable: false
			)
		]
		
		return theTetrahedron
	}

// MARK: -
// MARK: Wall data - Euclidean cube

	func makeRightAngledCube(	//	I guess all cubes are right-angled
		multicolorLattice: Bool
	) -> [WallData] {

		//	thw = table half width in radians
		let thw = 0.5 * GeometryType.euclidean.tableEdgeLengthInRadians

		//	Let opposite faces have the same color.
		let colorA: simd_half3
		let colorB: simd_half3
		let colorC: simd_half3
		if multicolorLattice {
			colorA = HSVtoRGB(hue: 0.72, saturation: 0.875, value: 0.6)	//	purple
			colorB = HSVtoRGB(hue: 0.38, saturation: 1.000, value: 0.6)	//	green
			colorC = HSVtoRGB(hue: 0.60, saturation: 0.875, value: 0.6)	//	blue
		} else {

			//	colorC is the brightest color,
			//	so we use it for the wall directly in front
			//	of the player's starting position.
			colorA = HSVtoRGB(hue: 0.1, saturation: 0.875, value: 0.750)
			colorB = HSVtoRGB(hue: 0.1, saturation: 0.875, value: 0.875)
			colorC = HSVtoRGB(hue: 0.1, saturation: 0.875, value: 1.000)
		}
		
		//	Faces wind clockwise when viewed
		//	from inside the cube
		//	in a right-handed coordinate system.
		let theCube: [WallData] = [

			WallData(
				corners:
					[
						SIMD4<Double>( -thw, -thw, -thw,  1.0 ),
						SIMD4<Double>( -thw, -thw,  thw,  1.0 ),
						SIMD4<Double>( -thw,  thw,  thw,  1.0 ),
						SIMD4<Double>( -thw,  thw, -thw,  1.0 )
					],
				color: colorA,
				mayCoincideWithPoolTable: false
			),
			WallData(
				corners:
					[
						SIMD4<Double>(  thw, -thw, -thw,  1.0 ),
						SIMD4<Double>(  thw,  thw, -thw,  1.0 ),
						SIMD4<Double>(  thw,  thw,  thw,  1.0 ),
						SIMD4<Double>(  thw, -thw,  thw,  1.0 )
					],
				color: colorA,
				mayCoincideWithPoolTable: false
			),
			WallData(
				corners:
					[
						SIMD4<Double>( -thw, -thw, -thw,  1.0 ),
						SIMD4<Double>(  thw, -thw, -thw,  1.0 ),
						SIMD4<Double>(  thw, -thw,  thw,  1.0 ),
						SIMD4<Double>( -thw, -thw,  thw,  1.0 )
					],
				color: colorB,
				mayCoincideWithPoolTable: true
			),
			WallData(
				corners:
					[
						SIMD4<Double>( -thw,  thw, -thw,  1.0 ),
						SIMD4<Double>( -thw,  thw,  thw,  1.0 ),
						SIMD4<Double>(  thw,  thw,  thw,  1.0 ),
						SIMD4<Double>(  thw,  thw, -thw,  1.0 )
					],
				color: colorB,
				mayCoincideWithPoolTable: false
			),
			WallData(
				corners:
					[
						SIMD4<Double>( -thw, -thw, -thw,  1.0 ),
						SIMD4<Double>( -thw,  thw, -thw,  1.0 ),
						SIMD4<Double>(  thw,  thw, -thw,  1.0 ),
						SIMD4<Double>(  thw, -thw, -thw,  1.0 )
					],
				color: colorC,
				mayCoincideWithPoolTable: false
			),
			WallData(
				corners:
					[
						SIMD4<Double>( -thw, -thw,  thw,  1.0 ),
						SIMD4<Double>(  thw, -thw,  thw,  1.0 ),
						SIMD4<Double>(  thw,  thw,  thw,  1.0 ),
						SIMD4<Double>( -thw,  thw,  thw,  1.0 )
					],
				color: colorC,
				mayCoincideWithPoolTable: false
			)
		]
		
		return theCube
	}

// MARK: -
// MARK: Wall data - hyperbolic dodecahedron

	func makeRightAngledDodecahedron(
		multicolorLattice: Bool
	) -> [WallData] {
		
		//	To get started, we need to know the outradius r
		//	of a right-angled hyperbolic dodecahedron. To compute it,
		//	we apply the Hyperbolic Law of Cosines for Angles
		//
		//		cos γ = -cos α cos β + sin α sin β cosh c
		//
		//	to a triangle with vertices
		//
		//		v₀ = any vertex of the dodecahedron
		//		v₂ = the center of an adjacent face
		//		v₃ = the center of the dodecahedron
		//
		//	(The subscripts tell the dimension of the thing
		//	that the given vertex is the center of, which is
		//	why there's no subscript 1 in that list.)
		
		//	The angle at v₂ is clearly a right angle.
	//	let cosθ₂ = 0.0	//	cos(π/2)
		
		//	Keeping in mind that each corner of the dodecahedron
		//	looks locally like the corner of a Euclidean cube,
		//	it's clear that the angle we're looking for at v₀
		//	is the same angle that a "long diagonal" of a Euclidean cube
		//	makes with each "short diagonal" across an adjacent face
		//	of that cube, e.g.
		//
		//		cos θ₀ = (√⅓, √⅓, √⅓)·(√½, √½, 0)
		//			   = √⅔
		//
		let cosθ₀ = sqrt(2.0/3.0)
		let sinθ₀ = sqrt(1.0/3.0)

		//	The method used to compute θ₀ could also be applied
		//	to compute θ₃, but it would be more tedious.
		//	So instead let's imagine a dodecahedral tiling of a unit 2-sphere,
		//	and apply the Spherical Law of Cosines for Angles
		//
		//		cos γ = -cos α cos β + sin α sin β cos c
		//
		//	to a triangle with angles π/2, π/3 and π/5, to solve
		//	for the desired "arc length" θ₃.
		//
		//		cos(π/2) = - cos(π/3) cos(π/5) + sin(π/3) sin(π/5) cos(θ₃)
		//
		let π = Double.pi
		let cosθ₃ = ( 0.5 * cos(π/5) ) / ( sqrt(0.75) * sin(π/5) )
		let sinθ₃ = sqrt( 1.0 - cosθ₃ * cosθ₃ )
		
		//	We can now evaluate the Hyperbolic Law of Cosines for Angles
		//
		//		cos γ = -cos α cos β + sin α sin β cosh c
		//
		//	to get the cosh of the right-angled hyperbolic dodecahedron's outradius:
		//
		let coshR = ( cosθ₀ * cosθ₃ ) / ( sinθ₀ * sinθ₃ )	//	r = 1.2264568712514…
		let sinhR = sqrt( coshR * coshR - 1.0 )
		
		//	All 20 vertices will have w component = cosh(r).
		let w = coshR
		
		//	The xyz part of all 20 vertices will have length sinr(r),
		//	but of course those 20 vertices will sit in 20 different directions
		//	from the dodecahedron's center, namely
		//
		//		(  ±1,  ±1,  ±1  )
		//		(  ±φ, ±1/φ,  0  )
		//		(   0,  ±φ, ±1/φ )
		//		( ±1/φ,  0,  ±φ  )
		//
		//	where φ is the golden ratio. The golden ratio is a root
		//	of the polynomial φ² - φ - 1 = 0, with numerical value
		//	(√5 + 1)/2 = 1.618… . Its inverse 1/φ = φ - 1 has
		//	numerical value (√5 - 1)/2 = 0.618… .
		let  φ   = 0.5 * (sqrt(5.0) + 1)
		let φInv = 0.5 * (sqrt(5.0) - 1)
		
		//	All twenty of those vectors, as written above,
		//	have length √3, so to rescale them to length sinh(r)
		//	we need to multiple them by sinh(r)/√3.
		let factor = sinhR / sqrt(3.0)

		//	To define all the faces, we need only three numbers
		//	(well, four is you count 0.0), so let's precompute
		//	them and give each one a short name.
		let b =  φ   * factor	//	'b' is the biggest of the three numbers
		let m = 1.0  * factor	//	'm' is the middle number
		let s = φInv * factor	//	's' is the smallest of the three numbers


		//	We'll color the faces using the symmetrical coloration
		//	of a dodecahedron described on George Hart's page
		//
		//		https://www.georgehart.com/virtual-polyhedra/colorings.html
		//
		let colorA: simd_half3
		let colorB: simd_half3
		let colorC: simd_half3
		let colorD: simd_half3
		let colorE: simd_half3
		let colorF: simd_half3
		if multicolorLattice {
			colorA = HSVtoRGB(hue: 0.38, saturation: 1.000, value: 0.6)	//	green
			colorB = HSVtoRGB(hue: 0.72, saturation: 0.875, value: 0.6)	//	purple
			colorC = HSVtoRGB(hue: 0.47, saturation: 0.875, value: 0.6)	//	cyan
			colorD = HSVtoRGB(hue: 0.95, saturation: 0.875, value: 0.6)	//	red
			colorE = HSVtoRGB(hue: 0.22, saturation: 0.875, value: 0.6)	//	yellow
			colorF = HSVtoRGB(hue: 0.60, saturation: 0.875, value: 0.6)	//	blue
		} else {
			colorA = HSVtoRGB(hue: 0.58, saturation: 0.875, value: 0.50)
			colorB = HSVtoRGB(hue: 0.58, saturation: 0.875, value: 0.56)
			colorC = HSVtoRGB(hue: 0.58, saturation: 0.875, value: 0.62)
			colorD = HSVtoRGB(hue: 0.58, saturation: 0.875, value: 0.68)
			colorE = HSVtoRGB(hue: 0.58, saturation: 0.875, value: 0.74)
			colorF = HSVtoRGB(hue: 0.58, saturation: 0.875, value: 0.80)
		}

		//	Faces wind clockwise when viewed
		//	from inside the dodecahedron
		//	in a right-handed coordinate system.
		let theDodecahedron: [WallData] = [
		
			WallData(	//	4
				corners:
					[
						SIMD4<Double>(  0, -s, -b, w ),
						SIMD4<Double>(  m, -m, -m, w ),
						SIMD4<Double>(  s, -b,  0, w ),
						SIMD4<Double>( -s, -b,  0, w ),
						SIMD4<Double>( -m, -m, -m, w )
					],
				color: colorA,
				mayCoincideWithPoolTable: true
			),
			WallData(	//	8
				corners:
					[
						SIMD4<Double>(  0,  s,  b, w ),
						SIMD4<Double>(  m,  m,  m, w ),
						SIMD4<Double>(  s,  b,  0, w ),
						SIMD4<Double>( -s,  b,  0, w ),
						SIMD4<Double>( -m,  m,  m, w )
					],
				color: colorA,
				mayCoincideWithPoolTable: false
			),
			WallData(	//	11
				corners:
					[
						SIMD4<Double>(  0, -s,  b, w ),
						SIMD4<Double>( -m, -m,  m, w ),
						SIMD4<Double>( -s, -b,  0, w ),
						SIMD4<Double>(  s, -b,  0, w ),
						SIMD4<Double>(  m, -m,  m, w )
					],
				color: colorB,
				mayCoincideWithPoolTable: false
			),
			WallData(	//	5
				corners:
					[
						SIMD4<Double>(  0,  s, -b, w ),
						SIMD4<Double>( -m,  m, -m, w ),
						SIMD4<Double>( -s,  b,  0, w ),
						SIMD4<Double>(  s,  b,  0, w ),
						SIMD4<Double>(  m,  m, -m, w )
					],
				color: colorB,
				mayCoincideWithPoolTable: false
			),
			WallData(	//	6
				corners:
					[
						SIMD4<Double>( -s, -b,  0, w ),
						SIMD4<Double>( -m, -m,  m, w ),
						SIMD4<Double>( -b, -0,  s, w ),
						SIMD4<Double>( -b, -0, -s, w ),
						SIMD4<Double>( -m, -m, -m, w )
					],
				color: colorC,
				mayCoincideWithPoolTable: false
			),
			WallData(	//	9
				corners:
					[
						SIMD4<Double>(  s,  b,  0, w ),
						SIMD4<Double>(  m,  m,  m, w ),
						SIMD4<Double>(  b, -0,  s, w ),
						SIMD4<Double>(  b, -0, -s, w ),
						SIMD4<Double>(  m,  m, -m, w )
					],
				color: colorC,
				mayCoincideWithPoolTable: false
			),
			WallData(	//	1
				corners:
					[
						SIMD4<Double>( -b, -0, -s, w ),
						SIMD4<Double>( -m,  m, -m, w ),
						SIMD4<Double>(  0,  s, -b, w ),
						SIMD4<Double>(  0, -s, -b, w ),
						SIMD4<Double>( -m, -m, -m, w )
					],
				color: colorD,
				mayCoincideWithPoolTable: false
			),
			WallData(	//	3
				corners:
					[
						SIMD4<Double>(  b, -0,  s, w ),
						SIMD4<Double>(  m,  m,  m, w ),
						SIMD4<Double>(  0,  s,  b, w ),
						SIMD4<Double>(  0, -s,  b, w ),
						SIMD4<Double>(  m, -m,  m, w )
					],
				color: colorD,
				mayCoincideWithPoolTable: false
			),
			WallData(	//	7
				corners:
					[
						SIMD4<Double>(  b, -0, -s, w ),
						SIMD4<Double>(  m, -m, -m, w ),
						SIMD4<Double>(  0, -s, -b, w ),
						SIMD4<Double>(  0,  s, -b, w ),
						SIMD4<Double>(  m,  m, -m, w )
					],
				color: colorE,
				mayCoincideWithPoolTable: false
			),
			WallData(	//	0
				corners:
					[
						SIMD4<Double>( -b, -0,  s, w ),
						SIMD4<Double>( -m, -m,  m, w ),
						SIMD4<Double>(  0, -s,  b, w ),
						SIMD4<Double>(  0,  s,  b, w ),
						SIMD4<Double>( -m,  m,  m, w )
					],
				color: colorE,
				mayCoincideWithPoolTable: false
			),
			WallData(	//	2
				corners:
					[
						SIMD4<Double>(  s, -b,  0, w ),
						SIMD4<Double>(  m, -m, -m, w ),
						SIMD4<Double>(  b, -0, -s, w ),
						SIMD4<Double>(  b, -0,  s, w ),
						SIMD4<Double>(  m, -m,  m, w )
					],
				color: colorF,
				mayCoincideWithPoolTable: false
			),
			WallData(	//	10
				corners:
					[
						SIMD4<Double>( -s,  b,  0, w ),
						SIMD4<Double>( -m,  m, -m, w ),
						SIMD4<Double>( -b, -0, -s, w ),
						SIMD4<Double>( -b, -0,  s, w ),
						SIMD4<Double>( -m,  m,  m, w )
					],
				color: colorF,
				mayCoincideWithPoolTable: false
			)
		]
		
		return theDodecahedron
	}
}
