#!/usr/bin/env godot
# tests/benchmarks/bench_128hz.gd
# 128Hz Load Test Benchmark for GDExtension Simulation Core
#
# Tests SimulationServer at N=8,16,32,64 bots running at 128Hz for
# BENCH_DURATION virtual-seconds each.  Measures achieved tick rate,
# frame time (GDScript + tick overhead), C++ tick time (from get_stats()),
# memory delta, and missed ticks.
#
# Usage:
#   godot --headless --script tests/benchmarks/bench_128hz.gd
#
# Exit code: 0 = benchmark complete, 1 = GDExtension not loadable

extends SceneTree

const TICK_HZ := 128
const BENCH_DURATION_SEC := 10.0
const POPULATION_LEVELS := [8, 16, 32, 64]
const TICK_INTERVAL := 1.0 / TICK_HZ
const EXPECTED_TICKS := int(TICK_HZ * BENCH_DURATION_SEC)

var _results := []


# ---------------------------------------------------------------------------
# Entry
# ---------------------------------------------------------------------------

func _init() -> void:
	print("")
	print("=".repeat(70))
	print("  128Hz Load Test Benchmark — GDExtension Simulation Server")
	print("=".repeat(70))
	print("")

	var vi = Engine.get_version_info()
	print("  Engine:   Godot %d.%d.%d" % [vi.major, vi.minor, vi.patch])
	print("  Platform: %s" % OS.get_name())
	print("  Headless: %s" % str(DisplayServer.get_name() == "headless"))
	print("  Tick Rate: %d Hz" % TICK_HZ)
	print("  Duration:  %.1f sec per level" % BENCH_DURATION_SEC)
	print("  Expected ticks: %d" % EXPECTED_TICKS)
	print("")

	# ---- Diagnostics: is the GDExtension loadable? ----
	if not ClassDB.class_exists("SimulationServer"):
		printerr("")
		printerr("  [FATAL] SimulationServer class is NOT registered in ClassDB.")
		printerr("")
		printerr("  Diagnostic:")
		printerr("    The GDExtension shared library could not be loaded.")
		printerr("")
		printerr("  Expected configuration:")
		printerr("    .gdextension:   res://gdextension/simulation.gdextension")
		printerr("    Linux .so:      res://gdextension/simulation/gdextension/bin/linux/libsimulation.so")
		printerr("")
		printerr("  Possible causes:")
		printerr("    1. The .so has not been compiled — run: cd gdextension/simulation && scons -j$(nproc)")
		printerr("    2. The .gdextension file points to the wrong path")
		printerr("    3. The .so is incompatible with this Godot version (need 4.2+ godot-cpp)")
		printerr("    4. Missing symbol — rebuild with a clean build")
		printerr("")
		quit(1)
		return

	print("  [OK] SimulationServer class is registered in ClassDB")
	print("")

	# Run benchmarks for each population level
	for pop in POPULATION_LEVELS:
		_run_benchmark(pop)

	# Print summary table
	_print_summary()

	quit(0)


# ---------------------------------------------------------------------------
# Single population-level benchmark
# ---------------------------------------------------------------------------

func _run_benchmark(pop: int) -> void:
	print("─".repeat(70))
	print("  Benchmark: %d bots" % pop)
	print("─".repeat(70))

	# Create a fresh SimulationServer for each population level
	# Use ClassDB.instantiate because SimulationServer is a GDExtension
	# class that isn't known to the parser at compile time.
	var server = ClassDB.instantiate("SimulationServer")
	if server == null:
		printerr("  [ERROR] Failed to create SimulationServer instance!")
		return

	# Configure and populate
	server.set_tick_rate(TICK_HZ)
	server.populate_bots(pop)
	server.start()
	server.reset_stats()

	var entity_count = server.get_entity_count()
	print("  Entities spawned: %d" % entity_count)
	assert(entity_count == pop, "Entity count should match requested population")

	# ---- Warm-up: a few ticks to stabilise caches / allocators ----
	for _i in range(10):
		if server.can_tick(TICK_INTERVAL):
			server.tick()
	server.reset_stats()

	# Memory snapshot BEFORE
	var mem_before := OS.get_static_memory_usage()

	# ---- Main benchmark loop (EXPECTED_TICKS iterations) ----
	var tick_count := 0
	var frame_times := []
	var bench_start := Time.get_ticks_usec()

	for i in range(EXPECTED_TICKS):
		var frame_start := Time.get_ticks_usec()

		if server.can_tick(TICK_INTERVAL):
			var snap = server.tick()
			tick_count += 1

		var frame_end := Time.get_ticks_usec()
		frame_times.append(frame_end - frame_start)

	var bench_end := Time.get_ticks_usec()

	# Memory snapshot AFTER
	var mem_after := OS.get_static_memory_usage()
	var mem_delta := mem_after - mem_before

	# Internal C++ timing (from get_stats)
	var stats = server.get_stats()
	var cpp_avg_usec  := float(stats["avg_tick_usec"])
	var cpp_peak_usec := float(stats["peak_tick_usec"])
	var cpp_tick_count := int(stats["tick_count"])

	# Wall-clock timing
	var wall_usec := bench_end - bench_start
	var wall_sec  := wall_usec / 1_000_000.0
	var achieved_hz := float(tick_count) / wall_sec if wall_sec > 0.0 else 0.0

	# Frame-time statistics
	var avg_frame_usec := 0.0
	var min_frame_usec := 0
	var max_frame_usec := 0
	if frame_times.size() > 0:
		var total := 0
		min_frame_usec = frame_times[0]
		max_frame_usec = frame_times[0]
		for ft in frame_times:
			total += ft
			if ft < min_frame_usec:
				min_frame_usec = ft
			if ft > max_frame_usec:
				max_frame_usec = ft
		avg_frame_usec = float(total) / float(frame_times.size())

	# Missed ticks (process fewer than expected)
	var missed_ticks: int = max(0, EXPECTED_TICKS - tick_count)

	# ---- Print per-level results ----
	print("")
	print("  ┌─ Results ──────────────────────────────────────────────┐")
	print("  │  Wall clock:         %8.3f sec                        │" % wall_sec)
	print("  │  Total ticks:        %d / %d (expected)              │" % [tick_count, EXPECTED_TICKS])
	print("  │  Missed ticks:       %d                                │" % missed_ticks)
	print("  │  Achieved tick rate: %.1f Hz                          │" % achieved_hz)
	print("  │                                                         │")
	print("  │  Frame time (GDScript + tick overhead):                 │")
	print("  │    Avg:   %8.1f µs                                    │" % avg_frame_usec)
	print("  │    Min:   %8d µs                                    │" % min_frame_usec)
	print("  │    Max:   %8d µs                                    │" % max_frame_usec)
	print("  │                                                         │")
	print("  │  Tick time (C++ internal, from get_stats()):            │")
	print("  │    Avg:   %8.1f µs                                    │" % cpp_avg_usec)
	print("  │    Peak:  %8.1f µs                                    │" % cpp_peak_usec)
	print("  │    Count: %d                                           │" % cpp_tick_count)
	print("  │                                                         │")
	print("  │  Memory:                                                │")
	print("  │    Before: %s                                     │" % _pad(_fmt_bytes(mem_before), 12))
	print("  │    After:  %s                                     │" % _pad(_fmt_bytes(mem_after), 12))
	print("  │    Delta:  %s                                     │" % _pad(_fmt_bytes(mem_delta), 12))
	print("  └─────────────────────────────────────────────────────────┘")
	print("")

	# Save for summary
	_results.append({
		"pop": pop,
		"achieved_hz": achieved_hz,
		"avg_frame_usec": avg_frame_usec,
		"min_frame_usec": min_frame_usec,
		"max_frame_usec": max_frame_usec,
		"cpp_avg_usec": cpp_avg_usec,
		"cpp_peak_usec": cpp_peak_usec,
		"wall_sec": wall_sec,
		"total_ticks": tick_count,
		"expected_ticks": EXPECTED_TICKS,
		"missed_ticks": missed_ticks,
		"mem_before": mem_before,
		"mem_after": mem_after,
		"mem_delta": mem_delta,
	})

	# Cleanup
	server.stop()
	server = null


# ---------------------------------------------------------------------------
# Summary table
# ---------------------------------------------------------------------------

func _print_summary() -> void:
	print("")
	print("=".repeat(70))
	print("  BENCHMARK SUMMARY")
	print("=".repeat(70))
	print("")

	# Table header
	print("  %-8s  %-12s  %-10s  %-10s  %-8s  %-10s" % [
		"Players", "Tick Rate", "Frame Avg", "C++ Tick", "Missed", "Mem Δ",
	])
	print("  %-8s  %-12s  %-10s  %-10s  %-8s  %-10s" % [
		"", "(Hz)", "(µs)", "Avg (µs)", "Ticks", "",
	])
	print("  " + "─".repeat(60))

	for r in _results:
		var rate_str := "%.1f/%d" % [r["achieved_hz"], TICK_HZ]
		var missed_str := "%d/%d" % [r["missed_ticks"], r["expected_ticks"]]
		print("  %-8s  %-12s  %-10.1f  %-10.1f  %-8s  %-10s" % [
			str(r["pop"]),
			rate_str,
			r["avg_frame_usec"],
			r["cpp_avg_usec"],
			missed_str,
			_fmt_bytes(r["mem_delta"]),
		])

	print("  " + "─".repeat(60))
	print("")
	print("  Legend:")
	print("    Players:   number of simulated bots")
	print("    Tick Rate: achieved Hz / target Hz (128)")
	print("    Frame Avg: average frame time incl. GDScript + tick overhead")
	print("    C++ Tick:  average tick processing time inside C++ (get_stats)")
	print("    Missed:    ticks short of expected / total expected ticks")
	print("    Mem Δ:     static memory delta before/after the benchmark")
	print("")
	if _results.size() > 0:
		var all_zero_missed := true
		var all_above_target := true
		for r in _results:
			if r["missed_ticks"] > 0:
				all_zero_missed = false
			if r["achieved_hz"] < TICK_HZ:
				all_above_target = false

		if all_zero_missed and all_above_target:
			print("  ✓ ALL BENCHMARKS PASSED — simulation maintains 128 Hz at all population levels")
		elif all_above_target:
			print("  ⚠  All levels achieved ≥128 Hz but some missed ticks recorded (check timing)")
		else:
			print("  ✗  Some levels FAILED to maintain 128 Hz — simulation may need optimisation")
			print("     for the higher population levels.")
	print("")
	print("=".repeat(70))
	print("  Benchmark complete.")
	print("=".repeat(70))
	print("")


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

static func _fmt_bytes(bytes: int) -> String:
	if bytes < 0:
		return "-" + _fmt_bytes(-bytes)
	if bytes < 1024:
		return "%d B" % bytes
	elif bytes < 1024 * 1024:
		return "%.1f KB" % (float(bytes) / 1024.0)
	else:
		return "%.2f MB" % (float(bytes) / (1024.0 * 1024.0))


# Left-pad a string to at least `width` characters by adding spaces.
# Godot 4 GDScript doesn't have rjust(), so we roll our own.
static func _pad(s: String, width: int) -> String:
	var needed := width - s.length()
	if needed <= 0:
		return s
	var buf := ""
	for _i in range(needed):
		buf += " "
	return buf + s
