extends SceneTree

## Builds a lightweight WMO render cache for runtime streaming.
##
## Usage:
##   godot --headless --path <project> --script res://src/tools/build_wmo_streaming_cache.gd -- \
##     --map Azeroth --extracted res://data/extracted --output res://data/cache/wmo_render_v1 --force

const WMO_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/wmo_builder.gd")
const WMO_STREAMING_SCRIPT := preload("res://src/resources/wmo_streaming_resource.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/wmo_render_v1"))
	var force := args.has("--force")

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

	DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(output_dir))

	var unique := _collect_unique_wmos(map_name, extracted)
	if unique.is_empty():
		push_error("No WMO references found for map: %s" % map_name)
		quit(1)
		return

	var wmo_loader = ClassDB.instantiate("WMOLoader")
	var baked := 0
	var skipped := 0
	var failed := 0
	var total := unique.size()
	var index := 0
	var started_ms := Time.get_ticks_msec()

	for rel_path_variant in unique.keys():
		index += 1
		var rel_path := String(rel_path_variant)
		var out_path := _get_wmo_render_path(output_dir, rel_path)
		if not force and ResourceLoader.exists(out_path):
			skipped += 1
			continue

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

		var data: Dictionary = wmo_loader.call("load_wmo", abs_wmo)
		if data.is_empty():
			failed += 1
			continue

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

		var render_resource: Resource = WMO_STREAMING_SCRIPT.new()
		render_resource.set("format_version", WMO_STREAMING_SCRIPT.FORMAT_VERSION)
		render_resource.set("source_path", rel_path)
		_collect_render_payload(node, render_resource)
		node.free()

		if (render_resource.get("meshes") as Array).is_empty() and (render_resource.get("multimeshes") as Array).is_empty():
			failed += 1
			continue

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

		baked += 1
		if index % 10 == 0 or index == total:
			print("[%d/%d] baked=%d skipped=%d failed=%d" % [index, total, baked, skipped, failed])

	var elapsed_ms := Time.get_ticks_msec() - started_ms
	print("WMO render cache finished. baked=%d skipped=%d failed=%d elapsed=%.2fs" % [
		baked,
		skipped,
		failed,
		float(elapsed_ms) / 1000.0,
	])
	WMO_BUILDER_SCRIPT.clear_caches()
	quit(0 if failed == 0 else 2)


func _collect_unique_wmos(map_name: String, extracted: String) -> Dictionary:
	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)
		return unique

	print("Scanning ADT tiles...")
	var scanned := 0
	for file_name in dir.get_files():
		if not file_name.ends_with(".adt"):
			continue
		var parts := file_name.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(file_name)))
		if data.is_empty():
			continue
		for rel in data.get("wmo_names", PackedStringArray()):
			var normalized := str(rel).replace("\\", "/").to_lower()
			if not normalized.is_empty():
				unique[normalized] = true
		scanned += 1

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


func _collect_render_payload(
		node: Node,
		render_resource: Resource,
		parent_transform: Transform3D = Transform3D.IDENTITY) -> void:
	if node.name == "Occluders":
		return

	var node_transform := parent_transform
	if node is Node3D:
		node_transform = parent_transform * (node as Node3D).transform

	if node is MeshInstance3D:
		var mesh_instance := node as MeshInstance3D
		if mesh_instance.mesh != null:
			var names: PackedStringArray = render_resource.get("mesh_names")
			var meshes: Array = render_resource.get("meshes")
			var transforms: Array = render_resource.get("mesh_transforms")
			names.append(mesh_instance.name)
			meshes.append(mesh_instance.mesh)
			transforms.append(node_transform)
			render_resource.set("mesh_names", names)
			render_resource.set("meshes", meshes)
			render_resource.set("mesh_transforms", transforms)
	elif node is MultiMeshInstance3D:
		var multimesh_instance := node as MultiMeshInstance3D
		if multimesh_instance.multimesh != null:
			var names: PackedStringArray = render_resource.get("multimesh_names")
			var multimeshes: Array = render_resource.get("multimeshes")
			var transforms: Array = render_resource.get("multimesh_transforms")
			names.append(multimesh_instance.name)
			multimeshes.append(multimesh_instance.multimesh)
			transforms.append(node_transform)
			render_resource.set("multimesh_names", names)
			render_resource.set("multimeshes", multimeshes)
			render_resource.set("multimesh_transforms", transforms)

	for child in node.get_children():
		_collect_render_payload(child, render_resource, node_transform)


func _get_wmo_render_path(output_dir: String, rel_path: String) -> String:
	var normalized := rel_path.replace("\\", "/")
	return output_dir.path_join(normalized.get_basename() + ".res")


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 _res(path: String) -> String:
	if path.begins_with("res://") or path.begins_with("user://"):
		return path
	return "res://" + path.trim_prefix("./").trim_prefix("/")
