@tool
extends SceneTree

# =============================================================================
# bake_lightmaps.gd — LightmapGI Configuration Checker
# =============================================================================
# Validates LightmapGI configuration for baking readiness in a Godot 4 scene.
# Checks:
#   - LightmapGI node exists and is properly configured
#   - Quality setting (Low/Medium/High/Ultra)
#   - Bounce count (recommended ≥ 2)
#   - Texel scale (recommended 0.5–4.0, default 1.0)
#   - Interior flag (true for indoor maps)
#   - Denoiser enabled
#   - Max texture size (recommended ≤ 1024)
#   - Light data presence (already baked or not)
#   - Geometry with UV2 (required for baking)
#
# Note: Godot 4 LightmapGI baking is editor-only. This script validates
#       configuration readiness; it does NOT perform the bake itself.
#
# Usage:
#   godot --headless --script client/scripts/bake_lightmaps.gd -- res://path/to/map.tscn
#
# Exit codes:
#   0  — All config checks passed
#   1  — One or more config checks failed
#   2  — Scene file not found or cannot be loaded
# =============================================================================

const RECOMMENDED_BOUNCES := 2
const MIN_BOUNCES := 1
const MAX_BOUNCES := 8
const RECOMMENDED_TEXEL_SCALE := 1.0
const MIN_TEXEL_SCALE := 0.1
const MAX_TEXEL_SCALE := 10.0
const MAX_TEXTURE_SIZE := 1024
const RECOMMENDED_QUALITY := 2  # High

var _pass_count := 0
var _fail_count := 0
var _warning_count := 0
var _scene_path := ""
var _scene_instance: Node = null
var _lightmap_node: Node = null

# UV2 tracking
var _total_meshes := 0
var _meshes_with_uv2 := 0
var _meshes_without_uv2: Array[String] = []
var _mesh_details: Array[Dictionary] = []


func _init() -> void:
	_parse_arguments()

	if _scene_path.is_empty():
		printerr("ERROR: No scene path provided. Usage: godot --headless --script bake_lightmaps.gd -- res://path/to/map.tscn")
		quit(2)
		return

	print("")
	print("==========================================")
	print("  LIGHTMAP CONFIGURATION CHECKER")
	print("  Scene: ", _scene_path)
	print("==========================================")
	print("")

	print("  Note: Godot 4 LightmapGI baking is editor-only.")
	print("  This tool validates lightmap configuration readiness only.")
	print("  To bake: Open scene in editor > Scene > Bake LightmapGI")
	print("")

	var scene: PackedScene = _load_scene(_scene_path)
	if scene == null:
		printerr("ERROR: Scene not found: ", _scene_path)
		quit(2)
		return

	_scene_instance = _instantiate_scene(scene)
	if _scene_instance == null:
		printerr("ERROR: Failed to instantiate scene: ", _scene_path)
		quit(2)
		return

	root.add_child(_scene_instance)

	# Find LightmapGI
	_lightmap_node = _find_node_by_type(_scene_instance, "LightmapGI")

	# Run all checks
	_run_lightmap_node_check()
	if _lightmap_node != null:
		_run_quality_check()
		_run_bounce_check()
		_run_texel_scale_check()
		_run_interior_check()
		_run_denoiser_check()
		_run_max_texture_size_check()
		_run_energy_check()
		_run_baked_data_check()
		_run_process_mode_check()

	# Run geometry UV2 check
	_run_uv2_check()

	# Detailed reports
	_print_lightmap_report()
	_print_uv2_report()

	# Summary
	_print_summary()

	if _fail_count > 0:
		quit(1)
	else:
		quit(0)


# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------

func _parse_arguments() -> void:
	"""Extract scene path from user arguments (after --)."""
	var args: PackedStringArray = OS.get_cmdline_user_args()
	if not args.is_empty():
		_scene_path = args[0].strip_edges()
		return

	var full_args: PackedStringArray = OS.get_cmdline_args()
	var found_sep := false
	for i in range(full_args.size()):
		if full_args[i] == "--":
			found_sep = true
			continue
		if found_sep:
			_scene_path = full_args[i].strip_edges()
			break


# ---------------------------------------------------------------------------
# Scene helpers
# ---------------------------------------------------------------------------

func _load_scene(path: String) -> PackedScene:
	"""Load a .tscn/.scn file."""
	if not ResourceLoader.exists(path):
		if not path.ends_with(".tscn") and not path.ends_with(".scn"):
			path = path + ".tscn"
		if not ResourceLoader.exists(path):
			return null
	return ResourceLoader.load(path, "PackedScene", ResourceLoader.CACHE_MODE_REUSE)


func _instantiate_scene(scene: PackedScene) -> Node:
	return scene.instantiate()


func _find_node_by_type(node: Node, type_name: String) -> Node:
	"""Recursively find first node matching a class name."""
	if node.get_class() == type_name:
		return node
	for child in node.get_children():
		var found := _find_node_by_type(child, type_name)
		if found != null:
			return found
	return null


func _find_nodes_by_type(node: Node, type_name: String) -> Array[Node]:
	"""Recursively find all nodes of a given class."""
	var results: Array[Node] = []
	if node.get_class() == type_name:
		results.append(node)
	for child in node.get_children():
		results.append_array(_find_nodes_by_type(child, type_name))
	return results


# ---------------------------------------------------------------------------
# Check helpers
# ---------------------------------------------------------------------------

func _pass(msg: String) -> void:
	_pass_count += 1
	print("  ✓ ", msg)


func _fail(msg: String) -> void:
	_fail_count += 1
	print("  ✗ ", msg)


func _warn(msg: String) -> void:
	_warning_count += 1
	print("  ⚠ ", msg)


func _check(condition: bool, msg_pass: String, msg_fail: String) -> bool:
	if condition:
		_pass(msg_pass)
	else:
		_fail(msg_fail)
	return condition


# ---------------------------------------------------------------------------
# Individual LightmapGI checks
# ---------------------------------------------------------------------------

func _run_lightmap_node_check() -> void:
	"""Verify LightmapGI node exists."""
	print("--- LightmapGI Node ---")
	if _lightmap_node != null:
		_pass("LightmapGI node found: '" + _lightmap_node.name + "'")
	else:
		_fail("LightmapGI node not found in scene — required for baked lighting")


func _run_quality_check() -> void:
	"""Validate LightmapGI quality setting."""
	print("")
	print("--- Quality ---")
	if _lightmap_node == null:
		return

	var quality: int = _lightmap_node.get("quality") if "quality" in _lightmap_node else -1
	if quality < 0:
		_fail("LightmapGI.quality not available")
		return

	var quality_names := ["Low", "Medium", "High", "Ultra"]
	var quality_name := quality_names[quality] if quality < quality_names.size() else "Unknown"

	if quality >= RECOMMENDED_QUALITY:
		_pass("LightmapGI.quality = " + quality_name + " (" + str(quality) + ") — High or Ultra recommended")
	elif quality == 1:
		_warn("LightmapGI.quality = " + quality_name + " (" + str(quality) + ") — Medium is acceptable, High recommended")
	else:
		_fail("LightmapGI.quality = " + quality_name + " (" + str(quality) + ") — set to at least High (2)")


func _run_bounce_check() -> void:
	"""Validate light bounce count."""
	print("")
	print("--- Bounces ---")
	if _lightmap_node == null:
		return

	var bounces: int = _lightmap_node.get("bounces") if "bounces" in _lightmap_node else -1
	if bounces < 0:
		_fail("LightmapGI.bounces not available")
		return

	if bounces >= RECOMMENDED_BOUNCES:
		_pass("LightmapGI.bounces = " + str(bounces) + " (recommended: ≥ " + str(RECOMMENDED_BOUNCES) + ")")
	elif bounces >= MIN_BOUNCES:
		_warn("LightmapGI.bounces = " + str(bounces) + " — low, consider ≥ " + str(RECOMMENDED_BOUNCES))
	else:
		_fail("LightmapGI.bounces = " + str(bounces) + " — minimum is " + str(MIN_BOUNCES))

	# Warn if too high (performance concern)
	if bounces > MAX_BOUNCES - 2:
		_warn("LightmapGI.bounces = " + str(bounces) + " — high bounce count may impact bake time")


func _run_texel_scale_check() -> void:
	"""Validate texel scale setting."""
	print("")
	print("--- Texel Scale ---")
	if _lightmap_node == null:
		return

	var texel_scale: float = _lightmap_node.get("texel_scale") if "texel_scale" in _lightmap_node else -1.0
	if texel_scale < 0:
		_fail("LightmapGI.texel_scale not available")
		return

	if abs(texel_scale - RECOMMENDED_TEXEL_SCALE) < 0.01:
		_pass("LightmapGI.texel_scale = " + str(texel_scale) + " (exactly " + str(RECOMMENDED_TEXEL_SCALE) + ")")
	elif texel_scale >= MIN_TEXEL_SCALE and texel_scale <= MAX_TEXEL_SCALE:
		if texel_scale < 0.5:
			_warn("LightmapGI.texel_scale = " + str(texel_scale) + " — below 0.5, bake resolution may be too low")
		elif texel_scale > 2.0:
			_warn("LightmapGI.texel_scale = " + str(texel_scale) + " — above 2.0, bake resolution may be unnecessarily high")
		else:
			_pass("LightmapGI.texel_scale = " + str(texel_scale) + " (within reasonable range [0.1, 10.0])")
	else:
		_fail("LightmapGI.texel_scale = " + str(texel_scale) + " — out of expected range [" + str(MIN_TEXEL_SCALE) + ", " + str(MAX_TEXEL_SCALE) + "]")


func _run_interior_check() -> void:
	"""Validate interior flag."""
	print("")
	print("--- Interior ---")
	if _lightmap_node == null:
		return

	var interior: bool = _lightmap_node.get("interior") if "interior" in _lightmap_node else false

	if interior:
		_pass("LightmapGI.interior = true (correct for indoor maps)")
	else:
		_warn("LightmapGI.interior = false — set to true for indoor scenes to avoid light leaking from outside")


func _run_denoiser_check() -> void:
	"""Validate denoiser setting."""
	print("")
	print("--- Denoiser ---")
	if _lightmap_node == null:
		return

	# Godot 4 renamed to use_denoiser
	var use_denoiser: bool = false
	if "use_denoiser" in _lightmap_node:
		use_denoiser = _lightmap_node.get("use_denoiser")
	elif _lightmap_node.has_method("is_using_denoiser"):
		use_denoiser = _lightmap_node.is_using_denoiser()

	if use_denoiser:
		_pass("LightmapGI.use_denoiser = true (recommended for cleaner results)")
	else:
		_warn("LightmapGI.use_denoiser = false — enable denoiser for cleaner lightmap results")


func _run_max_texture_size_check() -> void:
	"""Validate max texture size setting."""
	print("")
	print("--- Max Texture Size ---")
	if _lightmap_node == null:
		return

	var max_tex: int = _lightmap_node.get("max_texture_size") if "max_texture_size" in _lightmap_node else -1
	if max_tex < 0:
		_warn("LightmapGI.max_texture_size not available (using default)")
		return

	if max_tex <= MAX_TEXTURE_SIZE:
		_pass("LightmapGI.max_texture_size = " + str(max_tex) + " (≤ " + str(MAX_TEXTURE_SIZE) + ")")
	else:
		_fail("LightmapGI.max_texture_size = " + str(max_tex) + " — exceeds " + str(MAX_TEXTURE_SIZE) +
			" (consider lowering for memory budget)")


func _run_energy_check() -> void:
	"""Validate lightmap energy setting."""
	print("")
	print("--- Energy ---")
	if _lightmap_node == null:
		return

	var energy: float = _lightmap_node.get("energy") if "energy" in _lightmap_node else 1.0
	_pass("LightmapGI.energy = " + str(energy) + " (verify visually in editor)")

	var indirect_energy: float = _lightmap_node.get("indirect_energy") if "indirect_energy" in _lightmap_node else 1.0
	if abs(indirect_energy - 1.0) > 0.01:
		_warn("LightmapGI.indirect_energy = " + str(indirect_energy) + " (default is 1.0)")


func _run_baked_data_check() -> void:
	"""Check whether lightmap data has been baked."""
	print("")
	print("--- Baked Data ---")
	if _lightmap_node == null:
		return

	var light_data = _lightmap_node.get("light_data") if "light_data" in _lightmap_node else null
	if light_data != null:
		_pass("LightmapGI has baked light data present")
		# Check data size
		if light_data.has_method("get_user_data"):
			var user_data = light_data.get_user_data()
			if user_data != null:
				_warn("Lightmap data size: varies (baked data is present)")
	else:
		_warn("LightmapGI has NO baked data yet — needs baking in editor")


func _run_process_mode_check() -> void:
	"""Validate the lightmap process mode."""
	print("")
	print("--- Process Mode ---")
	if _lightmap_node == null:
		return

	# Godot 4 LightmapGI has a bake_mode or similar property for sub-surface scattering
	if "bake_mode" in _lightmap_node:
		var bake_mode: int = _lightmap_node.get("bake_mode")
		match bake_mode:
			0:
				_pass("LightmapGI.bake_mode = Conservative (0) — safe default")
			1:
				_warn("LightmapGI.bake_mode = Aggressive (1) — may cause artifacts")
			2:
				_pass("LightmapGI.bake_mode = Always Lightmap (2) — good for fully baked scenes")
			_:
				_warn("LightmapGI.bake_mode = " + str(bake_mode) + " — unknown")

	# Check for environment mode (if property exists)
	if "environment_mode" in _lightmap_node:
		var env_mode: int = _lightmap_node.get("environment_mode")
		match env_mode:
			0:
				_pass("LightmapGI.environment_mode = Disabled — no environment lighting (indoor)")
			1:
				_pass("LightmapGI.environment_mode = Enabled — using environment for lighting")
			2:
				_warn("LightmapGI.environment_mode = Ambient Color — verify color is correct")


# ---------------------------------------------------------------------------
# UV2 geometry check
# ---------------------------------------------------------------------------

func _run_uv2_check() -> void:
	"""Scan all MeshInstance3D nodes for UV2 channel presence."""
	print("")
	print("--- Geometry UV2 Check ---")

	var mesh_nodes := _find_nodes_by_type(_scene_instance, "MeshInstance3D")
	_total_meshes = mesh_nodes.size()

	if _total_meshes == 0:
		_warn("No MeshInstance3D nodes found — checking CSG nodes for UV2")

		# Check CSG nodes instead
		var csg_nodes := _find_nodes_by_type(_scene_instance, "CSGCombiner3D")
		csg_nodes.append_array(_find_nodes_by_type(_scene_instance, "CSGBox3D"))
		csg_nodes.append_array(_find_nodes_by_type(_scene_instance, "CSGSphere3D"))
		csg_nodes.append_array(_find_nodes_by_type(_scene_instance, "CSGCylinder3D"))
		csg_nodes.append_array(_find_nodes_by_type(_scene_instance, "CSGTorus3D"))
		csg_nodes.append_array(_find_nodes_by_type(_scene_instance, "CSGPolygon3D"))
		csg_nodes.append_array(_find_nodes_by_type(_scene_instance, "CSGMesh3D"))

		# CSG nodes generate UV2 during baking automatically
		if csg_nodes.size() > 0:
			_pass("Scene uses CSG primitives (" + str(csg_nodes.size()) + ") — UV2 is auto-generated during bake")
		return

	for mi in mesh_nodes:
		var has_uv2 := false
		if "mesh" in mi and mi.mesh != null:
			has_uv2 = _check_mesh_uv2(mi.mesh)

		var info := {
			"name": mi.name,
			"has_uv2": has_uv2,
			"material_count": 0
		}
		_mesh_details.append(info)

		if has_uv2:
			_meshes_with_uv2 += 1
		else:
			_meshes_without_uv2.append(mi.name)

	if _meshes_without_uv2.size() == 0:
		_pass("All " + str(_total_meshes) + " MeshInstance3D nodes have UV2 channel")
	else:
		for mesh_name in _meshes_without_uv2:
			_fail("MeshInstance3D '" + mesh_name + "' lacks UV2 channel — lightmapping will not work")
		_warn(str(_meshes_without_uv2.size()) + "/" + str(_total_meshes) + " meshes missing UV2")

	# Check for sub-surface scattering override on LightmapGI
	if _lightmap_node != null and "subdiv" in _lightmap_node:
		var subdiv: int = _lightmap_node.get("subdiv")
		if subdiv > 0:
			_warn("LightmapGI.subdiv = " + str(subdiv) + " — non-zero subdiv enables subsurface scattering")


func _check_mesh_uv2(mesh) -> bool:
	"""Check if a mesh has UV2 channel data."""
	if mesh == null:
		return false
	if not mesh.has_method("get_surface_count"):
		return false

	var surface_count := mesh.get_surface_count()
	for s in range(surface_count):
		var arrays := mesh.surface_get_arrays(s)
		if arrays is Array and arrays.size() > 0:
			var uv2 := arrays[Mesh.ARRAY_TEX_UV2] as PackedVector2Array
			if uv2 != null and uv2.size() > 0:
				return true
	return false


# ---------------------------------------------------------------------------
# Reports
# ---------------------------------------------------------------------------

func _print_lightmap_report() -> void:
	"""Print a detailed lightmap configuration report."""
	print("")
	print("--- LightmapGI Configuration Report ---")
	if _lightmap_node == null:
		print("  (no LightmapGI node to report)")
		return

	print("  Node path:   ", _get_node_path(_lightmap_node))
	var quality_names := ["Low", "Medium", "High", "Ultra"]
	var q: int = _lightmap_node.get("quality") if "quality" in _lightmap_node else -1
	if q >= 0 and q < quality_names.size():
		print("  Quality:         ", quality_names[q], " (", q, ")")
	else:
		print("  Quality:         ", q)
	print("  Bounces:         ", _lightmap_node.get("bounces") if "bounces" in _lightmap_node else "N/A")
	print("  Texel Scale:     ", _lightmap_node.get("texel_scale") if "texel_scale" in _lightmap_node else "N/A")
	print("  Interior:        ", _lightmap_node.get("interior") if "interior" in _lightmap_node else "N/A")
	print("  Denoiser:        ", _lightmap_node.get("use_denoiser") if "use_denoiser" in _lightmap_node else "N/A")
	print("  Max Tex Size:    ", _lightmap_node.get("max_texture_size") if "max_texture_size" in _lightmap_node else "N/A")
	print("  Energy:          ", _lightmap_node.get("energy") if "energy" in _lightmap_node else "N/A")
	print("  Baked Data:      ", "Yes" if _lightmap_node.get("light_data") != null else "No")
	print("")


func _print_uv2_report() -> void:
	"""Print UV2 details for meshes."""
	print("--- UV2 Report ---")
	print("  Total MeshInstance3D: ", _total_meshes)
	print("  With UV2:             ", _meshes_with_uv2)
	print("  Without UV2:          ", _meshes_without_uv2.size())
	if _meshes_without_uv2.size() > 0:
		print("  Missing UV2 list:")
		for name in _meshes_without_uv2:
			print("    - ", name)
	print("")


func _get_node_path(node: Node) -> String:
	"""Get the scene-tree path of a node."""
	var parts: Array[String] = []
	var current := node
	while current != null and current != _scene_instance:
		parts.push_front(current.name)
		current = current.get_parent()
	return "/".join(parts)


# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------

func _print_summary() -> void:
	"""Print final summary."""
	print("==========================================")
	print("  LIGHTMAP CONFIG SUMMARY")
	print("==========================================")
	print("  Scene:              ", _scene_path)
	print("  LightmapGI:         ", "Found ✓" if _lightmap_node != null else "Missing ✗")
	print("  Quality OK:         ", _check_quality_status())
	print("  Bounces OK:         ", _check_bounce_status())
	print("  Texel Scale OK:     ", _check_texel_status())
	print("  Interior OK:        ", _check_interior_status())
	print("  Denoiser OK:        ", _check_denoiser_status())
	print("  UV2 OK:             ", _check_uv2_status())
	print("------------------------------------------")
	print("  Passed:             ", _pass_count)
	print("  Failed:             ", _fail_count)
	print("  Warnings:           ", _warning_count)
	print("------------------------------------------")
	if _fail_count > 0:
		print("  RESULT:             FAILED  ❌")
		printerr("Lightmap config check FAILED — ", _fail_count, " check(s) failed.")
	else:
		print("  RESULT:             PASSED  ✅")
	print("==========================================")
	print("")


func _check_quality_status() -> String:
	if _lightmap_node == null: return "N/A"
	var q = _lightmap_node.get("quality") if "quality" in _lightmap_node else -1
	return "Yes ✓" if q >= RECOMMENDED_QUALITY else ("No ✗" if q >= 0 else "N/A")


func _check_bounce_status() -> String:
	if _lightmap_node == null: return "N/A"
	var b = _lightmap_node.get("bounces") if "bounces" in _lightmap_node else -1
	return "Yes ✓" if b >= RECOMMENDED_BOUNCES else ("No ✗" if b >= 0 else "N/A")


func _check_texel_status() -> String:
	if _lightmap_node == null: return "N/A"
	var t = _lightmap_node.get("texel_scale") if "texel_scale" in _lightmap_node else -1.0
	if t < 0: return "N/A"
	return "Yes ✓" if t >= 0.5 and t <= 2.0 else "⚠"


func _check_interior_status() -> String:
	if _lightmap_node == null: return "N/A"
	var interior = _lightmap_node.get("interior") if "interior" in _lightmap_node else false
	return "Yes ✓" if interior else "No ✗"


func _check_denoiser_status() -> String:
	if _lightmap_node == null: return "N/A"
	if "use_denoiser" in _lightmap_node:
		return "Yes ✓" if _lightmap_node.get("use_denoiser") else "No ✗"
	if _lightmap_node.has_method("is_using_denoiser"):
		return "Yes ✓" if _lightmap_node.is_using_denoiser() else "No ✗"
	return "N/A"


func _check_uv2_status() -> String:
	if _total_meshes == 0: return "N/A (CSG)"
	if _meshes_without_uv2.size() == 0: return "Yes ✓"
	return str(_meshes_without_uv2.size()) + " missing"
