class_name M2AnimatedInstanceMaterializer
extends RefCounted

## Duplicates one ordered animated M2 batch, applies instance render settings,
## starts playback and attaches a non-empty batch to the supplied parent.
## All calls mutate Godot scene objects and therefore belong on the main thread.

const M2_ANIMATED_SCENE_FINALIZER_SCRIPT := preload(
	"res://src/render/m2/m2_animated_scene_finalizer.gd"
)
const M2_ANIMATION_PLAYBACK_CONTROLLER_SCRIPT := preload(
	"res://src/render/m2/m2_animation_playback_controller.gd"
)

var _animated_scene_finalizer := M2_ANIMATED_SCENE_FINALIZER_SCRIPT.new()
var _animation_playback_controller := M2_ANIMATION_PLAYBACK_CONTROLLER_SCRIPT.new()


## Returns the attached batch root and detached diagnostic entries. Invalid or
## empty input, and a batch whose duplicates all fail, returns an empty result.
## The caller owns the returned/attached batch through `m2_parent_root`.
func materialize_batch(
	m2_parent_root: Node3D,
	relative_path: String,
	prototype: Node3D,
	transforms: Array,
	start_index: int,
	instance_count: int,
	batch_serial: int,
	visibility_range_end: float,
	visibility_range_margin: float,
	cast_shadows: bool,
	native_animator_script: Script,
	collect_native_diagnostics: bool
) -> Dictionary:
	if (
		m2_parent_root == null
		or transforms.is_empty()
		or instance_count <= 0
		or prototype == null
	):
		return {}

	var model_name := relative_path.get_file().get_basename()
	var batch_root := Node3D.new()
	batch_root.name = "%s_anim_%d" % [model_name, batch_serial]
	var diagnostic_entries: Array[Dictionary] = []
	for batch_offset in instance_count:
		var instance_index := start_index + batch_offset
		var instance := prototype.duplicate(
			Node.DUPLICATE_SIGNALS | Node.DUPLICATE_GROUPS | Node.DUPLICATE_SCRIPTS
		) as Node3D
		if instance == null:
			continue
		instance.name = "%s_%d" % [model_name, instance_index]
		instance.transform = transforms[instance_index]
		_apply_visibility_range_recursive(
			instance,
			visibility_range_end,
			visibility_range_margin
		)
		_apply_shadow_cast_recursive(instance, cast_shadows)
		_animation_playback_controller.copy_native_animator_data(
			prototype,
			instance,
			native_animator_script
		)
		batch_root.add_child(instance)
		var animation_players := _animated_scene_finalizer.animation_players_in_subtree(
			instance
		)
		var native_diagnostics := _animation_playback_controller.start_instance_playback(
			instance,
			relative_path,
			instance_index,
			native_animator_script,
			animation_players,
			collect_native_diagnostics
		)
		for native_diagnostic in native_diagnostics:
			diagnostic_entries.append({
				"instance_index": instance_index,
				"state": native_diagnostic,
			})

	if batch_root.get_child_count() <= 0:
		batch_root.queue_free()
		return {}
	m2_parent_root.add_child(batch_root)
	return {
		"batch_root": batch_root,
		"native_diagnostics": diagnostic_entries,
	}


func _apply_visibility_range_recursive(
	node: Node,
	range_end: float,
	range_end_margin: float
) -> void:
	if range_end <= 0.0:
		return
	if node is GeometryInstance3D:
		var geometry := node as GeometryInstance3D
		geometry.visibility_range_end = range_end
		geometry.visibility_range_end_margin = range_end_margin
	for child in node.get_children():
		_apply_visibility_range_recursive(child, range_end, range_end_margin)


func _apply_shadow_cast_recursive(node: Node, cast_shadows: bool) -> void:
	if node is GeometryInstance3D:
		var geometry := node as GeometryInstance3D
		geometry.cast_shadow = (
			GeometryInstance3D.SHADOW_CASTING_SETTING_ON
			if cast_shadows
			else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
		)
	for child in node.get_children():
		_apply_shadow_cast_recursive(child, cast_shadows)
