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

import SwiftUI
import CompositorServices


//	This app does *not* use Default Main-Actor Isolation.
//	In contrast to the traditional non-VR Geometry Games,
//	where almost everything runs on the Main Actor with
//	only a few long computations computations happening
//	in background tasks, this app effectively splits
//	into two parts:
//
//		In the Control Panel,
//			everything runs on the Main Actor.
//
//		In the ImmersiveSpace,
//			the render loop runs the whole show in its own Task,
//			which runs on its own TaskExecutor.
//
//	If Default Main-Actor Isolation were enabled,
//	we'd have to mark lots and lots of stuff
//	(even 'let' constants!) as 'nonisolated'.
//	So we leave the Default Actor Isolation as 'nonisolated'.
//	We do, however, enable Approachable Concurrency.

//	Note:  If you go to
//
//		Target > Build Settings > Swift Compiler - Custom Flags > Other Swift Flags > Debug
//
//	and add the lines
//
//		-Xfrontend
//		-warn-long-function-bodies=100
//		-Xfrontend
//		-warn-long-expression-type-checking=5
//
//	the compiler will measure the time required for type-checking individual lines of code
//	and for compiling individual functions, and will issue warnings for slow ones.
//	Unfortunately there are many lines of code that are slow to type-check.
//
//	Note: Accessing a component of a vector v as v.x is faster to type check
//	than accessing it as v[0].

//#warning("makeScreenshots is enabled -- disable for public release")
let makeScreenshots: Bool = false

//	When people use the app only briefly at a lecture or other public event,
//	we'll also want to strengthen the Guardian Angel,
//	weaken the computer player, and make the table a little higher.
//
//		Caution: We want to leave all settings fully adjustable
//		for real-time flexibility. So setting different default values
//		doesn't quite guarantee that the actual values won't get
//		unadvertantly altered before the lecture begins.
//		I've left myself a reminder in my lecture notes (in Keynote)
//		to double-check all the desired values at some point
//		before the talk (they persist in User Prefs).
//
//#warning("Be sure forTalks is false when submitting to the App Store.")
let forTalks = false
//	During the talk itself, I'll want the prompts to do away quickly
//	so I can show people the geodesic parallelism/divergence/convergence.
//	But in a pure try-it-yourself session, it's better to suppress
//	that option (and its distracting green/red button).
let allowBrieferPrompts = false

//	When starting a game of spherical pool, the player may be
//	given an option to override central projection and
//	use azimuthal equidistant projection instead.
//	So we must pass the azimuthalProjection as a parameter
//	to ImmersiveSpace(), rather than relying on a @State variable.
struct ImmersiveSpaceParameters: Hashable, Codable {
	let geometryType: GeometryType
	let azimuthalProjection: GGVRAzimuthalProjection
}

@main
struct NEBApp: App {

	@AppStorage("Player handedness") var playerHandedness: PlayersHand = .right

	//	How well should the computer play?
	@AppStorage("Difficulty") var computerPlayerDifficulty: Double	//	∈ 0.0 ... 1.0
									 = forTalks ? 0.25 : 0.5
	
	//	Especially at public events where each player might have only
	//	a couple minutes to try the app, novices might appreciate
	//	a helping hand to make their shots more successful.
	@AppStorage("Guardian angel factor") var guardianAngelFactor: Double	//	∈ 0.0 ... 1.0
									= forTalks ? 1.0 : 0.75
	
	@AppStorage("Table height") var tableHeight: Double = defaultTableHeight	//	in meters

	//	keepTableLevel is true by default because it lets
	//	the player hold the cue stick at a more comfortable angle.
	//
	//	Players may disable keepTableLevel
	//
	//	  - to experience fully authentic head-tracking
	//		  in non-Euclidean geometry or
	//
	//	  - to keep the north pole (where several walls meet)
	//		  out of the player's way.
	//
	@AppStorage("Keep table level") var keepTableLevel: Bool = true

	@AppStorage("Multicolor lattice") var multicolorLattice: Bool = false

	//	When forTalks is enabled we want to show the in-game messages
	//	only briefly when I'm using the app myself during the demonstration
	//	part of the talk, but show them for the whole time they're
	//	relevant when guests are using the app.
	//	When forTalks is disabled we always show them for the whole time.
	@State var brieferPrompts: Bool = false

	//	Apple's .upperLimbVisibility() page
	//
	//		https://developer.apple.com/documentation/swiftui/view/upperlimbvisibility(_:)
	//
	//	says
	//
	//		Note that this modifier only sets a preference and ultimately
	//		the system will decide if it will honor it or not. The system
	//		may be unable to honor the preference if the immersive space
	//		is currently not visible.
	//
	//	My first reaction was, how could the immersive space already be visible,
	//	if we supply the .upperLimbVisibility() modifier as part of
	//	our request to create it? The resolution of this paradox is that
	//	visionOS uses the (transient!) SwiftUI ImmersiveSpace struct
	//	to create an immersive space in some private internal format.
	//	If we specify the upper-limb visibility as
	//
	//		.upperLimbVisibility(hideHands ? .hidden : .visible)
	//
	//	we can switch from visible hands to hidden hands at some time
	//	after the immersive view is already visible. In particular,
	//	changing the parameter passed to .upperLimbVisibility()
	//	doesn't cause visionOS to re-create the immersive view.
	//	It keeps the immersive view that's already visible and
	//	just changes the upper-limb visibility. Whew!
	@State var hideHands: Bool = false

	@Environment(\.openWindow) var openWindow
	@Environment(\.dismissWindow) var dismissWindow
	@Environment(\.scenePhase) var scenePhase


	init() {
	
		//	Let the NEBSoundPlayer load its sounds
		//	well before the player takes his/her first shot.
		Task { await NEBSoundPlayer.shared.hello() }
	}

	var body: some Scene {

		WindowGroup(id: "Control panel", for: Int.self) { _ in
		
			NEBControlPanel(
				playerHandedness: $playerHandedness,
				computerPlayerDifficulty: $computerPlayerDifficulty,
				guardianAngelFactor: $guardianAngelFactor,
				tableHeight: $tableHeight,
				keepTableLevel: $keepTableLevel,
				multicolorLattice: $multicolorLattice,
				brieferPrompts: $brieferPrompts,
				hideHands: $hideHands)
			
		} defaultValue: { 0 }	//	assigns value 0 to the initial window
		.defaultSize(width: 256.0, height: 376.0)

		ImmersiveSpace(for: ImmersiveSpaceParameters.self) { parameters in

			CompositorLayer(configuration: GGVRCompositorLayerConfiguration()
			) { layerRenderer in
			
				assert(
					parameters.wrappedValue != nil,
					"ImmersiveSpace() received nil parameters")
				let theGeometryType = parameters.wrappedValue?.geometryType ?? .euclidean
				let theAzimuthalProjection = parameters.wrappedValue?.azimuthalProjection ?? .central
			
				let theOpenControlPanelClosure = {  @Sendable () -> Void in
					Task { @MainActor in
						openWindow(id: "Control panel", value: 0)
						if forTalks { brieferPrompts = false }
					}
				}

#warning("layerRenderer isn't sendable, so when Strict Concurrency Checking is set to 'complete', the compiler complains here. Xcode's template code has this same problem.")
				Task(executorPreference: GGVRRendererTaskExecutor.shared) {
				
					guard let theRenderer = await NEBRenderer(
							layerRenderer: layerRenderer,
							playerHandedness: playerHandedness,
							computerPlayerDifficulty: computerPlayerDifficulty,
							guardianAngelFactor: guardianAngelFactor,
							keepTableLevel: keepTableLevel,
							azimuthalProjection: theAzimuthalProjection,
							multicolorLattice: multicolorLattice,
							geometryType: theGeometryType,
							tableHeight: tableHeight,
							brieferPrompts: brieferPrompts,
							openControlPanel: theOpenControlPanelClosure)
					else {
						preconditionFailure("Failed to create theRenderer")
					}

					await theRenderer.startWorldAndHandTracking()
					await theRenderer.runRenderLoop()
					
					//	Note: runRenderLoop() contains no 'await' calls
					//	and runs without interruption, which is just
					//	what we want for a render loop.
					//	But be aware that if some other Task
					//	tries to call one of theRenderer's methods,
					//	the call will never be received.
				}
			}
		}
		.immersionStyle(selection: .constant(.full), in: .full)
		.upperLimbVisibility(hideHands ? .hidden : .visible)
		.persistentSystemOverlays(.hidden)
			//	Asks visionOS not to interpret palm-up as a request for a home button.
			//	visionOS may, at whim, choose not to comply.
	}
}
