## Pre-bakes M2 doodad models into PackedScene (.tscn) files for fast runtime loading.
##
## Usage:
##   godot --headless --path <project> --script res://src/tools/bake_m2_cache.gd -- \
##     --map Azeroth --extracted res://data/extracted --output res://data/cache/m2_glb --force
##   Add --only-pattern waterfall to rebuild only matching model paths.
## Add --glb-animations to also emit animated GLB scenes through src/tools/m2_to_gltf.py.
extends SceneTree

const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")

func _initialize() -> void:
	var args := OS.get_cmdline_user_args()
	var map_name    := _arg(args, "--map",       "Azeroth")
	var extracted   := _res(_arg(args, "--extracted", "res://data/extracted"))
	var output_dir  := _res(_arg(args, "--output",    "res://data/cache/m2_glb"))
	var force       := args.has("--force")
	var glb_animations := args.has("--glb-animations")
	var only_patterns := _arg_values(args, "--only-pattern")
	var python_exe  := _arg(args, "--python", "python")
	var converter   := _res(_arg(args, "--converter", "res://src/tools/m2_to_gltf.py"))

	if not ClassDB.class_exists("ADTLoader") or not ClassDB.class_exists("M2Loader"):
		push_error("GDExtension not loaded. Rebuild first.")
		quit(1)
		return

	DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(output_dir))

	# Collect unique M2 paths from all ADT tiles
	var unique: Dictionary = {}
	var adt_loader = ClassDB.instantiate("ADTLoader")
	var map_dir := extracted.path_join("World/Maps/%s" % map_name)
	var dir := DirAccess.open(map_dir)
	if dir == null:
		push_error("Cannot open map dir: %s" % map_dir)
		quit(1)
		return

	print("Scanning ADT tiles...")
	var scanned := 0
	for fname in dir.get_files():
		if not fname.ends_with(".adt"):
			continue
		var parts := fname.trim_suffix(".adt").split("_")
		if parts.size() != 3 or parts[0] != map_name:
			continue
		var data: Dictionary = adt_loader.call("load_adt",
			ProjectSettings.globalize_path(map_dir.path_join(fname)))
		if data.is_empty():
			continue
		for rel in data.get("m2_names", PackedStringArray()):
			var norm := str(rel).replace("\\", "/").to_lower()
			if norm.ends_with(".mdx") or norm.ends_with(".mdl"):
				norm = norm.get_basename() + ".m2"
			if not norm.is_empty() and _matches_only_patterns(norm, only_patterns):
				unique[norm] = true
		scanned += 1

	print("Found %d unique M2 models from %d tiles." % [unique.size(), scanned])

	var m2_loader  = ClassDB.instantiate("M2Loader")
	var baked := 0
	var baked_glb := 0
	var skipped := 0
	var failed := 0
	var failed_glb := 0
	var total := unique.size()
	var i := 0

	for rel_path in unique.keys():
		i += 1
		var stem: String = rel_path.get_basename()
		var out_path := output_dir.path_join(stem + ".tscn")
		var out_glb_path := output_dir.path_join(stem + ".glb")

		var abs_m2 := ProjectSettings.globalize_path(extracted.path_join(rel_path))
		if not FileAccess.file_exists(abs_m2):
			failed += 1
			continue

		if not force and ResourceLoader.exists(out_path):
			if glb_animations:
				if _bake_glb_animation_cache(python_exe, converter, abs_m2, output_dir, out_glb_path, false):
					baked_glb += 1
				else:
					failed_glb += 1
			skipped += 1
			continue

		var data: Dictionary = m2_loader.call("load_m2", abs_m2)
		if data.is_empty() or data.get("vertices", PackedVector3Array()).is_empty():
			failed += 1
			continue

		var node: Node3D = M2_BUILDER_SCRIPT.build(data, extracted)
		if node == null:
			failed += 1
			continue

		_set_owner_recursive(node, node)
		var scene := PackedScene.new()
		var err := scene.pack(node)
		node.free()
		if err != OK:
			push_warning("pack failed for %s: %d" % [rel_path, err])
			failed += 1
			continue

		DirAccess.make_dir_recursive_absolute(
			ProjectSettings.globalize_path(out_path.get_base_dir()))
		err = ResourceSaver.save(scene, out_path)
		if err != OK:
			push_warning("save failed for %s: %d" % [out_path, err])
			failed += 1
			continue

		baked += 1
		if glb_animations:
			if _bake_glb_animation_cache(python_exe, converter, abs_m2, output_dir, out_glb_path, force):
				baked_glb += 1
			else:
				failed_glb += 1
		if i % 50 == 0 or i == total:
			print("[%d/%d] baked=%d glb=%d skipped=%d failed=%d glb_failed=%d" % [
				i, total, baked, baked_glb, skipped, failed, failed_glb])

	print("Done. baked=%d  glb=%d  skipped=%d  failed=%d  glb_failed=%d" % [
		baked, baked_glb, skipped, failed, failed_glb])
	quit(0)


func _arg(args: PackedStringArray, name: String, default: String) -> String:
	var idx := args.find(name)
	if idx >= 0 and idx + 1 < args.size():
		return args[idx + 1]
	return default


func _arg_values(args: PackedStringArray, name: String) -> PackedStringArray:
	var result := PackedStringArray()
	for i in args.size():
		if args[i] != name or i + 1 >= args.size():
			continue
		for part in String(args[i + 1]).split(",", false):
			var value := part.strip_edges().to_lower()
			if not value.is_empty():
				result.append(value)
	return result


func _matches_only_patterns(path: String, patterns: PackedStringArray) -> bool:
	if patterns.is_empty():
		return true
	var lower := path.to_lower()
	for pattern in patterns:
		if lower.contains(pattern):
			return true
	return false


func _res(path: String) -> String:
	if path.begins_with("res://") or path.begins_with("user://"):
		return path
	return "res://" + path.trim_prefix("./").trim_prefix("/")


func _bake_glb_animation_cache(
		python_exe: String,
		converter: String,
		abs_m2: String,
		output_dir: String,
		out_glb_path: String,
		force: bool) -> bool:
	var abs_out_glb := ProjectSettings.globalize_path(out_glb_path)
	if not force and FileAccess.file_exists(abs_out_glb):
		return true
	var abs_converter := ProjectSettings.globalize_path(converter)
	var abs_output := ProjectSettings.globalize_path(output_dir)
	if not FileAccess.file_exists(abs_converter):
		push_warning("M2 GLB converter not found: %s" % converter)
		return false
	var stdout := []
	var exit_code := OS.execute(
		python_exe,
		[abs_converter, abs_m2, abs_output],
		stdout,
		true,
		false)
	if exit_code != 0:
		push_warning("animated GLB bake failed for %s exit=%d\n%s" % [
			abs_m2,
			exit_code,
			"\n".join(stdout)])
		return false
	return FileAccess.file_exists(abs_out_glb)


func _set_owner_recursive(node: Node, owner_root: Node) -> void:
	for child in node.get_children():
		child.owner = owner_root
		_set_owner_recursive(child, owner_root)
