class_name M2AnimatedSceneFinalizer
extends RefCounted

## Instantiates and validates detached animated M2 scene candidates and repairs
## their surface materials from an existing static material prototype.
## The caller owns ResourceLoader I/O, permits and prototype cache adoption.


## Instantiates one PackedScene as a detached Node3D candidate. Unsupported
## Resources return null; a created non-Node3D root is freed before return.
func instantiate_candidate(resource: Resource) -> Node3D:
	if not (resource is PackedScene):
		return null
	var candidate_root := (resource as PackedScene).instantiate()
	if candidate_root is Node3D:
		return candidate_root as Node3D
	if candidate_root != null:
		candidate_root.free()
	return null


## Applies the historical depth-first source-to-target material mapping. A null
## source or target leaves the candidate unchanged.
func repair_materials(animated_root: Node3D, material_source_root: Node3D) -> void:
	if animated_root == null or material_source_root == null:
		return
	var source_meshes := mesh_instances_in_subtree(material_source_root)
	var target_meshes := mesh_instances_in_subtree(animated_root)
	if source_meshes.is_empty() or target_meshes.is_empty():
		return

	var fallback_material := _first_mesh_material(source_meshes)
	for target_index in target_meshes.size():
		var target := target_meshes[target_index]
		if target == null or target.mesh == null:
			continue
		var source := source_meshes[mini(target_index, source_meshes.size() - 1)]
		for surface_index in target.mesh.get_surface_count():
			var material := _mesh_instance_surface_material(source, surface_index)
			if material == null:
				material = fallback_material
			if material != null:
				target.set_surface_override_material(surface_index, material)


## Accepts a detached candidate only when it contains an AnimationPlayer.
## Rejected candidates are freed synchronously. A successful result transfers
## the exact prototype reference and its AnimationPlayer count to the caller.
func finalize_candidate(candidate_root: Node3D) -> Dictionary:
	if candidate_root == null:
		return {}
	var animation_players := animation_players_in_subtree(candidate_root)
	if animation_players.is_empty():
		candidate_root.free()
		return {}
	return {
		"prototype": candidate_root,
		"animation_player_count": animation_players.size(),
	}


## Returns MeshInstance3D descendants in depth-first preorder, including root.
func mesh_instances_in_subtree(root: Node) -> Array[MeshInstance3D]:
	var mesh_instances: Array[MeshInstance3D] = []
	if root != null:
		_collect_mesh_instances(root, mesh_instances)
	return mesh_instances


## Returns AnimationPlayer descendants in depth-first preorder, including root.
func animation_players_in_subtree(root: Node) -> Array[AnimationPlayer]:
	var animation_players: Array[AnimationPlayer] = []
	if root != null:
		_collect_animation_players(root, animation_players)
	return animation_players


func _collect_mesh_instances(node: Node, result: Array[MeshInstance3D]) -> void:
	if node is MeshInstance3D:
		result.append(node as MeshInstance3D)
	for child in node.get_children():
		_collect_mesh_instances(child, result)


func _collect_animation_players(node: Node, result: Array[AnimationPlayer]) -> void:
	if node is AnimationPlayer:
		result.append(node as AnimationPlayer)
	for child in node.get_children():
		_collect_animation_players(child, result)


func _first_mesh_material(meshes: Array[MeshInstance3D]) -> Material:
	for mesh_instance in meshes:
		if mesh_instance == null or mesh_instance.mesh == null:
			continue
		for surface_index in mesh_instance.mesh.get_surface_count():
			var material := _mesh_instance_surface_material(mesh_instance, surface_index)
			if material != null:
				return material
	return null


func _mesh_instance_surface_material(
	mesh_instance: MeshInstance3D,
	surface_index: int
) -> Material:
	if mesh_instance == null or mesh_instance.mesh == null:
		return null
	if surface_index < mesh_instance.get_surface_override_material_count():
		var override_material := mesh_instance.get_surface_override_material(surface_index)
		if override_material != null:
			return override_material
	if surface_index < mesh_instance.mesh.get_surface_count():
		return mesh_instance.mesh.surface_get_material(surface_index)
	return null
