## Bakes terrain, M2 doodads and WMO objects in one pass.
##
## Usage:
##   godot --headless --path <project> --script res://src/tools/bake_all.gd -- \
##     --map Azeroth --force
##
## Optional args:
##   --extracted res://data/extracted
##   --terrain-output res://data/cache/baked_terrain_v2
##   --m2-output      res://data/cache/m2_glb
##   --wmo-output     res://data/cache/wmo_tscn
##   --full-texture-size 2048
##   --coarse-texture-size 512
##   --skip-terrain   --skip-m2   --skip-wmo
extends SceneTree

const ADT_BUILDER_SCRIPT  := preload("res://addons/mpq_extractor/loaders/adt_builder.gd")
const BAKED_TILE_SCRIPT   := preload("res://src/resources/baked_adt_tile.gd")
const WMO_BUILDER_SCRIPT  := preload("res://addons/mpq_extractor/loaders/wmo_builder.gd")
const M2_BUILDER_SCRIPT   := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")

var _image_cache: Dictionary = {}


func _initialize() -> void:
	var args := OS.get_cmdline_user_args()

	if not ClassDB.class_exists("ADTLoader"):
		push_error("ADTLoader not found. Rebuild GDExtension first.")
		quit(1); return
	if not ClassDB.class_exists("WMOLoader"):
		push_error("WMOLoader not found. Rebuild GDExtension first.")
		quit(1); return
	if not ClassDB.class_exists("M2Loader"):
		push_error("M2Loader not found. Rebuild GDExtension first.")
		quit(1); return

	var map_name      := _arg(args, "--map",               "Azeroth")
	var extracted     := _res(_arg(args, "--extracted",    "res://data/extracted"))
	var terrain_out   := _res(_arg(args, "--terrain-output","res://data/cache/baked_terrain_v2"))
	var m2_out        := _res(_arg(args, "--m2-output",    "res://data/cache/m2_glb"))
	var wmo_out       := _res(_arg(args, "--wmo-output",   "res://data/cache/wmo_tscn"))
	var full_tex_size := maxi(256, _arg(args, "--full-texture-size",   "2048").to_int())
	var coarse_size   := maxi(128, _arg(args, "--coarse-texture-size", "512").to_int())
	var force         := args.has("--force")
	var skip_terrain  := args.has("--skip-terrain")
	var skip_m2       := args.has("--skip-m2")
	var skip_wmo      := args.has("--skip-wmo")

	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 directory: %s" % map_dir)
		quit(1); return

	var total_start := Time.get_ticks_msec()

	# ── Phase 1: Terrain ─────────────────────────────────────────────────────
	if not skip_terrain:
		print("\n=== Phase 1/3: Terrain ===")
		_bake_terrain(map_name, map_dir, extracted, terrain_out,
			full_tex_size, coarse_size, force)
	else:
		print("\n=== Phase 1/3: Terrain [SKIPPED] ===")

	# ── Phase 2: M2 ──────────────────────────────────────────────────────────
	if not skip_m2:
		print("\n=== Phase 2/3: M2 Doodads ===")
		var unique_m2 := _collect_m2(map_name, map_dir)
		_bake_m2(unique_m2, extracted, m2_out, force)
	else:
		print("\n=== Phase 2/3: M2 Doodads [SKIPPED] ===")

	# ── Phase 3: WMO ─────────────────────────────────────────────────────────
	if not skip_wmo:
		print("\n=== Phase 3/3: WMO Objects ===")
		var unique_wmo := _collect_wmo(map_name, map_dir)
		_bake_wmo(unique_wmo, extracted, wmo_out, force)
	else:
		print("\n=== Phase 3/3: WMO Objects [SKIPPED] ===")

	var elapsed := float(Time.get_ticks_msec() - total_start) / 1000.0
	print("\nAll done in %.1fs." % elapsed)
	WMO_BUILDER_SCRIPT.clear_caches()
	quit(0)


# ── Terrain ───────────────────────────────────────────────────────────────────

func _bake_terrain(map_name: String, map_dir: String, extracted: String,
		output_dir: String, full_tex: int, coarse_tex: int, force: bool) -> void:
	DirAccess.make_dir_recursive_absolute(
		ProjectSettings.globalize_path(output_dir.path_join(map_name)))

	var builder = ADT_BUILDER_SCRIPT.new()
	var loader  = ClassDB.instantiate("ADTLoader")
	var dir     = DirAccess.open(map_dir)
	var baked := 0; var skipped := 0; var failed := 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
		if not parts[1].is_valid_int() or not parts[2].is_valid_int(): continue
		var tx := parts[1].to_int()
		var ty := parts[2].to_int()
		var out_path := output_dir.path_join(map_name).path_join(
			"%s_%d_%d.res" % [map_name, tx, ty])
		if not force and FileAccess.file_exists(ProjectSettings.globalize_path(out_path)):
			skipped += 1; continue

		var data: Dictionary = loader.call("load_adt",
			ProjectSettings.globalize_path(map_dir.path_join(fname)))
		if data.is_empty() or not data.has("chunks"):
			failed += 1; continue

		var payloads: Dictionary = builder.build_baked_tile_render_payload_pair(
			data,
			_image_cache,
			ProjectSettings.globalize_path(extracted),
			0,
			full_tex,
			3,
			coarse_tex)
		var full_payload: Dictionary = payloads.get("full", {})
		var coarse_payload: Dictionary = payloads.get("coarse", {})
		if full_payload.is_empty():
			failed += 1; continue

		var tile: Resource = BAKED_TILE_SCRIPT.new()
		tile.set("format_version",    BAKED_TILE_SCRIPT.FORMAT_VERSION)
		tile.set("map_name",          map_name)
		tile.set("tile_x",            tx)
		tile.set("tile_y",            ty)
		tile.set("tile_origin",       builder.get_tile_origin_for_data(data))
		tile.set("full_texture_size", full_tex)
		tile.set("coarse_texture_size", coarse_tex)
		tile.set("wmo_names",         data.get("wmo_names",    PackedStringArray()))
		tile.set("wmo_placements",    data.get("wmo_placements", []))
		tile.set("m2_names",          data.get("m2_names",     PackedStringArray()))
		tile.set("m2_placements",     data.get("m2_placements",  []))
		tile.set("full_mesh",         full_payload.get("mesh",  null))
		tile.set("coarse_mesh",       coarse_payload.get("mesh", null))

		if ResourceSaver.save(tile, out_path) != OK:
			failed += 1; continue
		baked += 1
		if baked % 25 == 0:
			print("  terrain: baked=%d skipped=%d failed=%d" % [baked, skipped, failed])

	print("  terrain done. baked=%d skipped=%d failed=%d" % [baked, skipped, failed])


# ── M2 ────────────────────────────────────────────────────────────────────────

func _collect_m2(map_name: String, map_dir: String) -> Dictionary:
	var loader = ClassDB.instantiate("ADTLoader")
	var dir    = DirAccess.open(map_dir)
	var unique: Dictionary = {}
	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 = loader.call("load_adt",
			ProjectSettings.globalize_path(map_dir.path_join(fname)))
		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():
				unique[norm] = true
	print("  found %d unique M2 models." % unique.size())
	return unique


func _bake_m2(unique: Dictionary, extracted: String, output_dir: String, force: bool) -> void:
	DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(output_dir))
	var loader = ClassDB.instantiate("M2Loader")
	var baked := 0; var skipped := 0; var failed := 0
	var total := unique.size(); var i := 0

	for rel_path in unique.keys():
		i += 1
		var out_path := output_dir.path_join(rel_path.get_basename() + ".tscn")
		if not force and ResourceLoader.exists(out_path):
			skipped += 1; continue

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

		var data: Dictionary = 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

		var scene := PackedScene.new()
		if scene.pack(node) != OK:
			node.free(); failed += 1; continue
		node.free()

		DirAccess.make_dir_recursive_absolute(
			ProjectSettings.globalize_path(out_path.get_base_dir()))
		if ResourceSaver.save(scene, out_path) != OK:
			failed += 1; continue

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

	print("  m2 done. baked=%d skipped=%d failed=%d" % [baked, skipped, failed])


# ── WMO ───────────────────────────────────────────────────────────────────────

func _collect_wmo(map_name: String, map_dir: String) -> Dictionary:
	var loader = ClassDB.instantiate("ADTLoader")
	var dir    = DirAccess.open(map_dir)
	var unique: Dictionary = {}
	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 = loader.call("load_adt",
			ProjectSettings.globalize_path(map_dir.path_join(fname)))
		for rel in data.get("wmo_names", PackedStringArray()):
			var norm := str(rel).replace("\\", "/").to_lower()
			if not norm.is_empty():
				unique[norm] = true
	print("  found %d unique WMO models." % unique.size())
	return unique


func _bake_wmo(unique: Dictionary, extracted: String, output_dir: String, force: bool) -> void:
	DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(output_dir))
	var loader = ClassDB.instantiate("WMOLoader")
	var baked := 0; var skipped := 0; var failed := 0
	var total := unique.size(); var i := 0

	for rel_path in unique.keys():
		i += 1
		var out_path := output_dir.path_join(rel_path.get_basename() + ".tscn")
		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 = 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 scene := PackedScene.new()
		if scene.pack(node) != OK:
			node.free(); failed += 1; continue
		node.free()

		DirAccess.make_dir_recursive_absolute(
			ProjectSettings.globalize_path(out_path.get_base_dir()))
		if ResourceSaver.save(scene, out_path) != OK:
			failed += 1; continue

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

	print("  wmo done. baked=%d skipped=%d failed=%d" % [baked, skipped, failed])


# ── Helpers ───────────────────────────────────────────────────────────────────

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("/")
