@tool
extends SceneTree

# =============================================================================
# validate_lights.gd — Dynamic Light Count / Bake Mode Validator
# =============================================================================
# Audits all light nodes in a Godot 4 scene for:
#   - Dynamic light count ≤ 4 (budget for mobile/competitive FPS)
#   - Each light's bake mode is set correctly (BakeMode = STATIC or DYNAMIC)
#   - Light energy / range / attenuation are within reasonable bounds
#   - Shadow configuration is appropriate
#
# Usage:
#   godot --headless --script client/scripts/validate_lights.gd -- res://path/to/map.tscn
#
# Exit codes:
#   0  — All light checks passed
#   1  — One or more light checks failed
#   2  — Scene file not found or cannot be loaded
# =============================================================================

const MAX_DYNAMIC_LIGHTS := 4
const MAX_LIGHT_ENERGY := 50.0
const MIN_LIGHT_ENERGY := 0.1
const MAX_LIGHT_RANGE := 100.0
const MAX_LIGHT_SPOT_ANGLE := 180.0
const WARN_TOTAL_LIGHTS := 8

# BakeMode enum values as Godot defines them
const BAKE_MODE_DISABLED := 0
const BAKE_MODE_STATIC := 1
const BAKE_MODE_DYNAMIC := 2

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

# Light stat tracking
var _total_lights := 0
var _dynamic_lights := 0
var _baked_lights := 0
var _disabled_lights := 0
var _shadowed_lights := 0
var _light_details: Array[Dictionary] = []

# Per-type counts
var _directional_lights := 0
var _omni_lights := 0
var _spot_lights := 0


func _init() -> void:
	_parse_arguments()

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

	print("")
	print("==========================================")
	print("  LIGHT VALIDATOR")
	print("  Scene: ", _scene_path)
	print("==========================================")
	print("")

	print("  Light budget:")
	print("    Max dynamic lights: ", MAX_DYNAMIC_LIGHTS)
	print("    Recommended baking: All static geometry lights")
	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)

	# Collect all lights
	_analyze_lights(_scene_instance)

	# Run checks
	_run_dynamic_light_count_check()
	_run_bake_mode_checks()
	_run_energy_checks()
	_run_shadow_checks()
	_print_light_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()


# ---------------------------------------------------------------------------
# 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)


# ---------------------------------------------------------------------------
# Light analysis
# ---------------------------------------------------------------------------

func _analyze_lights(node: Node) -> void:
	"""Recursively walk scene collecting light data."""
	var class_name := node.get_class()

	if class_name in ["DirectionalLight3D", "OmniLight3D", "SpotLight3D"]:
		_total_lights += 1
		var info := _collect_light_info(node, class_name)
		_light_details.append(info)

		# Count by type
		match class_name:
			"DirectionalLight3D":
				_directional_lights += 1
			"OmniLight3D":
				_omni_lights += 1
			"SpotLight3D":
				_spot_lights += 1

		# Track bake mode
		var bake_mode := 0
		if "bake_mode" in node:
			bake_mode = node.bake_mode
		match bake_mode:
			BAKE_MODE_DISABLED:
				_disabled_lights += 1
				_dynamic_lights += 1
			BAKE_MODE_STATIC:
				_baked_lights += 1
			BAKE_MODE_DYNAMIC:
				_dynamic_lights += 1

		# Track shadows
		if "shadow" in node and node.shadow:
			_shadowed_lights += 1

	for child in node.get_children():
		_analyze_lights(child)


func _collect_light_info(node: Node, class_name: String) -> Dictionary:
	"""Collect all relevant properties from a light node."""
	var info := {
		"name": node.name,
		"type": class_name,
		"path": _get_node_path(node),
		"enabled": node.get("visible") if "visible" in node else true,
		"bake_mode": node.get("bake_mode") if "bake_mode" in node else 0,
		"energy": node.get("energy") if "energy" in node else 1.0,
		"shadow": node.get("shadow") if "shadow" in node else false,
		"shadow_bias": node.get("shadow_bias") if "shadow_bias" in node else 0.0,
		"indirect_energy": node.get("indirect_energy") if "indirect_energy" in node else 1.0,
		"light_specular": node.get("light_specular") if "light_specular" in node else 0.5,
	}

	# Type-specific properties
	match class_name:
		"OmniLight3D":
			info["range"] = node.get("omni_range") if "omni_range" in node else 0.0
			info["attenuation"] = node.get("omni_attenuation") if "omni_attenuation" in node else 1.0
		"SpotLight3D":
			info["range"] = node.get("spot_range") if "spot_range" in node else 0.0
			info["attenuation"] = node.get("spot_attenuation") if "spot_attenuation" in node else 1.0
			info["spot_angle"] = node.get("spot_angle") if "spot_angle" in node else 45.0
			info["spot_angle_attenuation"] = node.get("spot_angle_attenuation") if "spot_angle_attenuation" in node else 1.0
		"DirectionalLight3D":
			info["range"] = -1.0
			info["directional_shadow_mode"] = node.get("directional_shadow_mode") if "directional_shadow_mode" in node else 0

	return info


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)


func _get_bake_mode_name(mode: int) -> String:
	match mode:
		BAKE_MODE_DISABLED: return "Disabled (Dynamic)"
		BAKE_MODE_STATIC:   return "Static (Baked)"
		BAKE_MODE_DYNAMIC:  return "Dynamic"
	return "Unknown(" + str(mode) + ")"


# ---------------------------------------------------------------------------
# Light checks
# ---------------------------------------------------------------------------

func _run_dynamic_light_count_check() -> void:
	"""Check that dynamic light count is within budget."""
	print("--- Dynamic Light Count ---")

	if _total_lights == 0:
		_warn("No lights found in scene at all (verify map lighting)")
		return

	_pass("Total lights: " + str(_total_lights) + " (Dir: " + str(_directional_lights) +
		", Omni: " + str(_omni_lights) + ", Spot: " + str(_spot_lights) + ")")

	if _dynamic_lights <= MAX_DYNAMIC_LIGHTS:
		_pass("Dynamic lights: " + str(_dynamic_lights) + " (budget: ≤ " + str(MAX_DYNAMIC_LIGHTS) + ")")
	else:
		_fail("Too many dynamic lights: " + str(_dynamic_lights) +
			" (budget: ≤ " + str(MAX_DYNAMIC_LIGHTS) + ")" +
			" — consider baking " + str(_dynamic_lights - MAX_DYNAMIC_LIGHTS) + " light(s)")

	# Warn if total lights (including baked) is very high
	if _total_lights > WARN_TOTAL_LIGHTS:
		_warn("Total light count (" + str(_total_lights) + ") exceeds " + str(WARN_TOTAL_LIGHTS) +
			" — verify all lights are necessary")


func _run_bake_mode_checks() -> void:
	"""Check that each light has an appropriate bake mode."""
	print("")
	print("--- Bake Mode Validation ---")

	var proper_bake := 0
	var improper_bake := 0

	for info in _light_details:
		var bake_mode := info.bake_mode as int
		var light_name := info.name as String
		var light_type := info.type as String
		var bake_name := _get_bake_mode_name(bake_mode)

		if bake_mode == BAKE_MODE_STATIC:
			proper_bake += 1
			_pass(light_name + " (" + light_type + "): bake_mode = Static ✓")
		elif bake_mode == BAKE_MODE_DISABLED:
			# Dynamic lights are OK if count is within budget
			improper_bake += 1
			_warn(light_name + " (" + light_type + "): bake_mode = Disabled (fully dynamic)")
		elif bake_mode == BAKE_MODE_DYNAMIC:
			improper_bake += 1
			_warn(light_name + " (" + light_type + "): bake_mode = Dynamic")

	if improper_bake > 0:
		_warn(str(improper_bake) + " light(s) are not set to Static bake mode" +
			" — use Static for baked geometry lights")

	# DirectionalLight3D should generally be Baked (Static) for main sun
	for info in _light_details:
		if info.type == "DirectionalLight3D" and info.bake_mode == BAKE_MODE_DISABLED:
			_warn("DirectionalLight3D '" + info.name + "' is set to Disabled bake mode" +
				" — set to Static (Baked) for performance")


func _run_energy_checks() -> void:
	"""Check that light energy values are within reasonable ranges."""
	print("")
	print("--- Light Energy / Range Checks ---")

	for info in _light_details:
		var energy := info.energy as float
		var light_name := info.name as String
		var light_type := info.type as String

		if energy > MAX_LIGHT_ENERGY:
			_warn(light_name + " (" + light_type + "): energy = " + str(energy) +
				" — very high (max recommended: " + str(MAX_LIGHT_ENERGY) + ")")
		elif energy < MIN_LIGHT_ENERGY:
			_warn(light_name + " (" + light_type + "): energy = " + str(energy) +
				" — very low (min recommended: " + str(MIN_LIGHT_ENERGY) + ")")
		else:
			_pass(light_name + " (" + light_type + "): energy = " + str(energy) + " (within [0.1, 50.0])")

		# Range check (not applicable to DirectionalLight3D)
		if info.has("range") and info.range > 0:
			var range_val := info.range as float
			if range_val > MAX_LIGHT_RANGE:
				_warn(light_name + " (" + light_type + "): range = " + str(range_val) +
					" — very large (max recommended: " + str(MAX_LIGHT_RANGE) + ")")

		# Spot angle check
		if info.has("spot_angle"):
			var angle := info.spot_angle as float
			if angle > MAX_LIGHT_SPOT_ANGLE:
				_warn(light_name + " (SpotLight3D): spot_angle = " + str(angle) +
					" — exceeds 180° (will project backwards)")


func _run_shadow_checks() -> void:
	"""Validate shadow configuration on lights."""
	print("")
	print("--- Shadow Configuration ---")

	for info in _light_details:
		var has_shadow := info.shadow as bool
		var light_name := info.name as String

		if has_shadow:
			# Shadowed lights are more expensive
			if info.bake_mode == BAKE_MODE_STATIC:
				_pass(light_name + ": shadows enabled, bake_mode = Static (good — shadows will be baked)")
			else:
				_warn(light_name + ": dynamic shadows enabled — expensive for real-time rendering")

	# Count shadowed dynamic lights
	var dynamic_shadow_count := 0
	for info in _light_details:
		if info.shadow and info.bake_mode != BAKE_MODE_STATIC:
			dynamic_shadow_count += 1

	if dynamic_shadow_count > 2:
		_warn(str(dynamic_shadow_count) + " lights have dynamic shadows — recommended max 2 for performance")


# ---------------------------------------------------------------------------
# Light report
# ---------------------------------------------------------------------------

func _print_light_report() -> void:
	"""Print a comprehensive light report."""
	print("")
	print("--- Light Report ---")
	print("  Total lights:        ", _total_lights)
	print("  DirectionalLight3D:  ", _directional_lights)
	print("  OmniLight3D:         ", _omni_lights)
	print("  SpotLight3D:         ", _spot_lights)
	print("  Dynamic (non-baked): ", _dynamic_lights)
	print("  Baked (Static):      ", _baked_lights)
	print("  Disabled:            ", _disabled_lights)
	print("  Shadowed:            ", _shadowed_lights)
	print("")

	# Light detail table
	if _light_details.size() > 0:
		print("  Light Details:")
		print("  " + "-".repeat(90))
		print("  %-25s %-18s %-12s %-8s %-6s %-8s" % ["Name", "Type", "Bake Mode", "Energy", "Shadow", "Range"])
		print("  " + "-".repeat(90))
		for info in _light_details:
			var bake_mode_name := _get_bake_mode_name(info.bake_mode)
			var range_str := str(info.range) if info.has("range") and info.range > 0 else "N/A"
			var shadow_str := "Yes" if info.shadow else "No"
			print("  %-25s %-18s %-12s %-8.1f %-6s %-8s" % [
				info.name, info.type, bake_mode_name, info.energy, shadow_str, range_str
			])
		print("  " + "-".repeat(90))
		print("")


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

func _print_summary() -> void:
	"""Print final summary of all light checks."""
	print("==========================================")
	print("  LIGHT VALIDATION SUMMARY")
	print("==========================================")
	print("  Scene:                ", _scene_path)
	print("  Total lights:         ", _total_lights)
	print("  Dynamic lights:       ", _dynamic_lights)
	print("  Baked (Static):       ", _baked_lights)
	print("  Dynamic shadowed:     ", _shadowed_lights)
	print("------------------------------------------")
	print("  Passed:               ", _pass_count)
	print("  Failed:               ", _fail_count)
	print("  Warnings:             ", _warning_count)
	print("------------------------------------------")
	if _fail_count > 0:
		print("  RESULT:               FAILED  ❌")
		printerr("Validation FAILED — ", _fail_count, " light check(s) failed.")
	else:
		print("  RESULT:               PASSED  ✅")
	print("==========================================")
	print("")
