extends SceneTree

# Profile scene runner — manually drives the frame loop.
# In --script --headless mode the automatic main loop doesn't
# iterate, so we drive it ourselves via a manual loop.

const DEFAULT_SCENE := "res://assets/scenes/modular/kit_demo.tscn"
const DEFAULT_CAPTURE_FRAMES := 600
const WARMUP_FRAMES := 60

var capture_frames := DEFAULT_CAPTURE_FRAMES
var scene_path := ""
var frame_count := 0
var recording: Array[Dictionary] = []
var profiling_complete := false

func _init():
	# Parse CLI args
	var args = OS.get_cmdline_args()
	for i in range(args.size()):
		if args[i] == "--scene" and i + 1 < args.size():
			scene_path = args[i + 1]
		if args[i] == "--capture-frames" and i + 1 < args.size():
			capture_frames = args[i + 1].to_int()
	
	if scene_path.is_empty():
		scene_path = DEFAULT_SCENE
	
	print("=== Profile Scene Tool ===")
	print("Engine: ", Engine.get_version_info().string)
	print("Scene: ", scene_path)
	print("Capture frames: ", capture_frames)
	print("Warmup frames: ", WARMUP_FRAMES)
	print("")
	print("Frame,Delta(ms),GPU(ms),Physics(ms),DrawCalls,Tris,Vertices,VRAM(MB),RAM(MB),Objects")
	
	# Load target scene
	var scene = ResourceLoader.load(scene_path)
	if not scene:
		printerr("ERROR: Failed to load scene: ", scene_path)
		quit(1)
		return
	
	var instance = scene.instantiate()
	root.add_child(instance)
	
	# Manual frame loop: Godot doesn't auto-iterate the main loop
	# in --script --headless mode. We iterate it ourselves.
	profiling_complete = false
	_manual_loop()

func _manual_loop() -> void:
	var start_time := Time.get_ticks_usec()
	var frame_delta := 0.016  # Target 60fps step
	
	while not profiling_complete:
		var frame_start := Time.get_ticks_usec()
		
		# Call idle (process) on the scene tree — this triggers 
		# _process() on all nodes including the instanced scene
		#idle(frame_delta)
		
		# Manually iterate — this processes input, idle, and physics
		iteration(frame_delta)
		
		_on_frame(frame_delta)
		
		# Cap frame rate to avoid 100% CPU in headless mode
		var elapsed := (Time.get_ticks_usec() - frame_start) / 1000.0
		var target_ms := 16.0  # ~60fps
		if elapsed < target_ms:
			OS.delay_msec(int(target_ms - elapsed))
		
		# Use elapsed wall time as delta for next frame
		frame_delta = (Time.get_ticks_usec() - start_time) / 1000000.0
		start_time = Time.get_ticks_usec()
	
	_finish_report()

func _on_frame(delta: float) -> void:
	frame_count += 1
	
	# Skip warmup frames
	if frame_count <= WARMUP_FRAMES:
		return
	
	if frame_count > WARMUP_FRAMES + capture_frames:
		profiling_complete = true
		return
	
	var p := Performance
	
	var frame_delta := delta * 1000.0
	var gpu_time := float(RenderingServer.get_frame_setup_time_cpu()) * 1000.0
	var physics_time := p.get_monitor(p.TIME_PHYSICS_PROCESS) * 1000.0
	var draw_calls := int(p.get_monitor(p.RENDER_TOTAL_DRAW_CALLS_IN_FRAME))
	var tris := int(p.get_monitor(p.RENDER_TOTAL_PRIMITIVES_IN_FRAME))
	var verts := int(p.get_monitor(p.RENDER_TOTAL_VERTICES_IN_FRAME))
	var vram_mb := int(p.get_monitor(p.RENDER_VIDEO_MEM_USED)) / (1024 * 1024)
	var ram_mb := int(OS.get_static_memory_usage()) / (1024 * 1024)
	var objects := int(p.get_monitor(p.OBJECT_NODE_COUNT))
	
	recording.append({
		frame = frame_count - WARMUP_FRAMES,
		delta_ms = snapped(frame_delta, 0.01),
		gpu_ms = snapped(gpu_time, 0.01),
		physics_ms = snapped(physics_time, 0.01),
		draw_calls = draw_calls,
		tris = tris,
		verts = verts,
		vram_mb = vram_mb,
		ram_mb = ram_mb,
		objects = objects
	})
	
	print("%d,%.2f,%.2f,%.2f,%d,%d,%d,%d,%d,%d" % [
		frame_count - WARMUP_FRAMES,
		frame_delta, gpu_time, physics_time,
		draw_calls, tris, verts, vram_mb, ram_mb, objects
	])

func _finish_report() -> void:
	print("")
	print("=== Profile Summary ===")
	if recording.is_empty():
		print("No frames recorded!")
		quit(1)
		return
	
	var deltas: Array[float] = []
	for r in recording:
		deltas.append(r.delta_ms)
	deltas.sort()
	
	var total := float(recording.size())
	var n := deltas.size()
	var p50: float = deltas[n * 50 / 100]
	var p95: float = deltas[n * 95 / 100]
	var p99: float = deltas[n * 99 / 100]
	var sum: float = 0.0
	for d in deltas:
		sum += d
	var avg: float = sum / total
	
	var max_dc := 0
	var max_tris := 0
	var max_vram := 0
	for r in recording:
		if r.draw_calls > max_dc: max_dc = r.draw_calls
		if r.tris > max_tris: max_tris = r.tris
		if r.vram_mb > max_vram: max_vram = r.vram_mb
	
	print("Frames captured: ", recording.size())
	print("Avg frame time:  %.2f ms" % avg)
	print("P50 frame time:  %.2f ms" % p50)
	print("P95 frame time:  %.2f ms" % p95)
	print("P99 frame time:  %.2f ms" % p99)
	print("Max draw calls:  %d" % max_dc)
	print("Max triangles:   %d" % max_tris)
	print("Peak VRAM:       %d MB" % max_vram)
	
	var pass_fail := "PASS"
	if p95 > 16.6:
		pass_fail = "FAIL (P95 > 16.6 ms)"
	elif p99 > 50.0:
		pass_fail = "FAIL (P99 spike > 50 ms)"
	print("Result: ", pass_fail)
	print("")
	print("=== Profile complete ===")
	quit(0 if pass_fail == "PASS" else 1)
