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


import CompositorServices
import CoreImage
import CoreImage.CIFilterBuiltins

struct MessageVertex {
	let tex: SIMD2<Float32>	//	texture coordinates (u,v)
}

let messageFadeDuration = 0.5	//	in seconds


func makeMessagePipelineState(
	gpuFunctionLibrary: MTLLibrary,
	layerRenderer: LayerRenderer
) -> MTLRenderPipelineState? {

	guard let theVertexFunction = gpuFunctionLibrary.makeFunction(
		name: "messageVertexFunction")
	else {
		assertionFailure("Failed to load messageVertexFunction")
		return nil
	}
	guard let theFragmentFunction = gpuFunctionLibrary.makeFunction(
		name: "messageFragmentFunction")
	else {
		assertionFailure("Failed to load messageFragmentFunction")
		return nil
	}

	//	Vertex descriptor

	//	Create a MTLVertexDescriptor describing the vertex attributes
	//	that the GPU sends along to each vertex. Each vertex attribute
	//	may be a 1-, 2-, 3- or 4-component vector.
	
	let theVertexDescriptor = MTLVertexDescriptor()
	
		//	Say where to find each attribute (there's only one!).

	guard let theTexOffset = MemoryLayout.offset(of: \MessageVertex.tex) else {
		assertionFailure("Failed to find offset of MessageVertex.tex")
		return nil
	}
	theVertexDescriptor.attributes[MessageVertexAttributeTexCoords].format = MTLVertexFormat.float2
	theVertexDescriptor.attributes[MessageVertexAttributeTexCoords].bufferIndex = MessageBufferIndexVertexAttributes
	theVertexDescriptor.attributes[MessageVertexAttributeTexCoords].offset = theTexOffset

		//	Say how to step through each buffer.

	theVertexDescriptor.layouts[MessageBufferIndexVertexAttributes].stepFunction = MTLVertexStepFunction.perVertex
	theVertexDescriptor.layouts[MessageBufferIndexVertexAttributes].stride = MemoryLayout<MessageVertex>.stride

	guard let thePipelineState = ggvrPipelineState(
									layerRenderer: layerRenderer,
									vertexFunction: theVertexFunction,
									fragmentFunction: theFragmentFunction,
									vertexDescriptor: theVertexDescriptor,
									enableBlending: true)	//	for rounded corners
	else {
		assertionFailure("makeMessagePipelineState failed to create thePipelineState.")
		return nil
	}
	
	return thePipelineState
}

func makeMessageDepthStencilState(
	device: MTLDevice
) -> MTLDepthStencilState? {

	//	These curved-space apps use the depth buffer
	//	in non-standard ways, so to keep things simple
	//	let's just slap the headset-based message on top
	//	of whatever is already in the frame buffer.
	//
	//		Note: We need to keep depthWriteEnabled true,
	//		because otherwise visionOS's tile-based
	//		deferred rendering would omit tiles whose
	//		depth buffer has never been touched, which
	//		would leave black squares in the interior
	//		of the message anywhere where there nothing
	//		but empty black space behind it.
	//
	//	For best results, render the message after
	//	rending all other content in the scene.

	let theDepthStencilState = ggvrDepthStencilState(
								device: device,
								depthCompareEnabled: false,
								depthWriteEnabled: true)
	
	return theDepthStencilState
}

func makeMessageMesh(
	device: MTLDevice
) -> MTLBuffer {

	let theTriangleStripVertices = [
		MessageVertex(tex: SIMD2<Float32>( 0.0,  0.0)),
		MessageVertex(tex: SIMD2<Float32>( 0.0,  1.0)),
		MessageVertex(tex: SIMD2<Float32>( 1.0,  0.0)),
		MessageVertex(tex: SIMD2<Float32>( 1.0,  1.0))
	]

	guard let theVertexBuffer = device.makeBuffer(
			bytes: theTriangleStripVertices,
			length: 4 * MemoryLayout<MessageVertex>.stride,
			options: [.storageModeShared]
	) else {
		fatalError("Couldn't make theVertexBuffer in makeMessageMesh()")
	}
	
	return theVertexBuffer
}

private let standardFontSize: Double = 16.0
private let textImageGeneratorScaleFactor: Float = 8.0
private let standardLineHeightInPoints: Double = {
	
	var theSampleAttributedString = AttributedString("Sample")
	theSampleAttributedString.font = .systemFont(ofSize: standardFontSize)

	let theSampleTextImageGenerator = CIFilter.attributedTextImageGenerator()
	theSampleTextImageGenerator.text = NSAttributedString(theSampleAttributedString)
	theSampleTextImageGenerator.scaleFactor = textImageGeneratorScaleFactor
	let theImageHeightInPoints: Double
	if let theSampleTextImage = theSampleTextImageGenerator.outputImage {
	
		theImageHeightInPoints = theSampleTextImage.extent.height
		
	} else {
	
		assertionFailure("The initializer for the standardLineHeightInPixels failed to make theSampleTextImageGenerator")

		//	The following should be a reasonable approximation
		//	to the correct value. Of course if theSampleTextImageGenerator
		//	fails to generate the outputImage here, then
		//	theTextImageGenerator in makeMessageTexture()
		//	is also likely to fail.

		theImageHeightInPoints = 1.25
							   * Double(textImageGeneratorScaleFactor)
							   * standardFontSize
	}
	
	return theImageHeightInPoints
}()
private let standardLineHeightInPixels: Double = {

	//	The CIAttributedTextImageGenerator measures the image in points,
	//	but then makeMessageTexture() renders it into a MTLTexture
	//	at a scale of 1 point = 1 pixel.
	
	return standardLineHeightInPoints
}()
private let standardLineHeightInMeters = 0.03125
let messageTextureMetersPerPixel = standardLineHeightInMeters / standardLineHeightInPixels

func makeMessageTexture(
	device: MTLDevice,
	commandQueue: MTLCommandQueue,
	inputText: [(text: String, relativeFontSize: Double)] // text = "-" => blank line
) -> MTLTexture? {
	
	if inputText.count == 0 {
		return nil
	}

	//	Core Image uses a bottom-up coordinate system,
	//	so let's reverse the order of the inputText's lines.
	let theReversedLines = inputText.reversed()
	
	
	//	Create the desired CIImage.

	let theRawImages = theReversedLines.compactMap() { lineOfText in
	
		let theFontSize = lineOfText.relativeFontSize * standardFontSize
	
		var theAttributedString = AttributedString(lineOfText.text)
		theAttributedString.font = .systemFont(ofSize: theFontSize)
		if lineOfText.text == "-" {
			theAttributedString.foregroundColor = UIColor.clear	//	the UIColor prefix is essential
		}
		
		let theTextImageGenerator = CIFilter.attributedTextImageGenerator()
		theTextImageGenerator.text = NSAttributedString(theAttributedString)
		theTextImageGenerator.scaleFactor = textImageGeneratorScaleFactor
		let theTextImage = theTextImageGenerator.outputImage
		assert(
			theTextImage != nil,
			"makeMessageTexture() failed to make theCIImage for '\(lineOfText)'"
		)
		return theTextImage
	}
	if theRawImages.isEmpty {
		assertionFailure("makeMessageTexture() made no raw images.")
		return nil
	}
	
	let theTextBounds = theRawImages.map() { textImage in textImage.extent }
	let theTotalTextHeight = theTextBounds.reduce(0.0, { runningTotal, textBounds in
		runningTotal + textBounds.height
	})
	let theMaxTextWidth = theTextBounds.reduce(0.0, { runningMax, textBounds in
		max(runningMax, textBounds.width)
	})
	
	let theVerticalPadding   = 0.5 * standardLineHeightInPoints	//	in points
	let theHorizontalPadding = 2.0 * theVerticalPadding			//	in points
	
	let theTotalBounds = CGRect(
							x: 0.0,
							y: 0.0,
							width: theMaxTextWidth + 2.0 * theHorizontalPadding,
							height: theTotalTextHeight + 2.0 * theVerticalPadding)


	let theRoundedRectangleGenerator = CIFilter.roundedRectangleGenerator()
	theRoundedRectangleGenerator.color = .white
	theRoundedRectangleGenerator.extent = theTotalBounds
	theRoundedRectangleGenerator.radius = Float(1.0 * standardLineHeightInPoints)
	guard let theBackgroundImage = theRoundedRectangleGenerator.outputImage
	else {
		assertionFailure("makeMessageTexture() failed to make theRoundedRectangleImage")
		return nil
	}

	var Δy: CGFloat = theVerticalPadding
	let theOffsetImages = theRawImages.map() { rawImage in
	
		let Δx = theHorizontalPadding + 0.5 * (theMaxTextWidth - rawImage.extent.width)

		let theTranslation = CGAffineTransform(translationX: Δx, y: Δy)

		Δy += rawImage.extent.height	//	prepare for the next rawImage
		
		let theOffsetImage = rawImage.transformed(by: theTranslation)
		
		return theOffsetImage
	}

	guard let theImageComposer = CIFilter(name: "CISourceOverCompositing")
	else {
		assertionFailure("makeMessageTexture() failed to make theImageComposer")
		return nil
	}

	var theTextWithBackground = theBackgroundImage
	for theOffsetImage in theOffsetImages {
	
		theImageComposer.setValue(theOffsetImage, forKey: "inputImage")
		theImageComposer.setValue(theTextWithBackground, forKey: "inputBackgroundImage")
		if let theComposedImage = theImageComposer.outputImage {
			theTextWithBackground = theComposedImage
		}
		else {
			assertionFailure("makeMessageTexture() failed to make theComposedImage")
		}
	}
	
	
	//	Render the CIImage into a MTLTexture.

	let theTextureDescriptor = MTLTextureDescriptor.texture2DDescriptor(
								pixelFormat: .rgba16Float,	//	for smoothest corners
								width: Int(theTotalBounds.width),
								height: Int(theTotalBounds.height),
								mipmapped: true)
	theTextureDescriptor.usage = [.renderTarget, .shaderRead, .shaderWrite]
	theTextureDescriptor.storageMode = .private
	guard let theTexture = device.makeTexture(descriptor: theTextureDescriptor)
	else {
		assertionFailure("makeMessageTexture() failed to make theTexture")
		return nil
	}
	
	guard let theColorSpace = CGColorSpace(name: CGColorSpace.displayP3)
	else {
		assertionFailure("makeMessageTexture() failed to make theColorSpace")
		return nil
	}

	let theCIContext = CIContext(mtlDevice: device)

	guard let theCommandBuffer = commandQueue.makeCommandBuffer()
	else {
		assertionFailure("makeMessageTexture() failed to make theCommandBuffer")
		return nil
	}

		theCIContext.render(
						theTextWithBackground,
						to: theTexture,
						commandBuffer: theCommandBuffer,
						bounds: theTotalBounds,
						colorSpace: theColorSpace)

		if let theBlitEncoder = theCommandBuffer.makeBlitCommandEncoder() {
			theBlitEncoder.label = "make mipmap levels"
			theBlitEncoder.generateMipmaps(for: theTexture)
			theBlitEncoder.endEncoding()
		} else {
			assertionFailure("Failed to generate mipmap levels in makeMessageTexture()")
		}

	theCommandBuffer.commit()
	
	return theTexture
}

func makeMessageSampler(
	device: MTLDevice
) -> MTLSamplerState? {

	guard let theSamplerState = ggvrSamplerState(
									device: device,
									sMode: .clampToEdge,
									tMode: .clampToEdge)
	else {
		assertionFailure("makeMessageSampler() failed to make theSamplerState")
		return nil
	}
	
	return theSamplerState
}

func makeMessageUniforms(
	drawable: LayerRenderer.Drawable,
	messageTexture: MTLTexture?,
	verticalOffset: Double
) -> MessageUniforms? {

	guard let theMessageTexture = messageTexture
	else { return nil }

	//	A physical device renders for the left and right eyes,
	//	so theEyeCount is 2. But the simulator renders only
	//	a single view, so theEyeCount is only 1.
	//
	let theEyeCount = drawable.views.count

	let theMVPMatrices = (0 ..< theEyeCount).map() { eyeIndex in

		//	We want to attach the message to the headset,
		//	so the model matrix is always the identity.

		//	theEyePose gives the eye's placement in headset space.
		//	Its inverse maps headset space to eye space.
		let theEyePose = drawable.views[eyeIndex].transform
		let theViewMatrix = theEyePose.inverse
		
		let theProjectionMatrix = drawable.computeProjection(viewIndex: eyeIndex)

		let theModelViewProjectionMatrix
			= theProjectionMatrix * theViewMatrix	//	right-to-left matrix action
		
		return theModelViewProjectionMatrix
	}

	let theWidthInMeters  = Double(theMessageTexture.width ) * messageTextureMetersPerPixel
	let theHeightInMeters = Double(theMessageTexture.height) * messageTextureMetersPerPixel
	
	let theMessageUniforms = MessageUniforms(
	
		//	The modelViewProjection is a tuple, not a Swift Array,
		//	for compatibility with the C-like GPU code.
		
		modelViewProjection: (
			theMVPMatrices[0],
			theEyeCount == 2 ? theMVPMatrices[1] : theMVPMatrices[0]
		),
		
		width:  Float(theWidthInMeters),
		height: Float(theHeightInMeters),
		verticalOffset: Float(verticalOffset)
	)
	
	return theMessageUniforms
}
