class_name M2AnimationResourceFinalizer
extends RefCounted

## Polls cached animated M2 ResourceLoader requests and finalizes terminal scenes.
## The caller owns permits, material-source lookup and SceneTree materialization.

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

var _animated_scene_finalizer: RefCounted
var _resource_loader_adapter: Object


func _init(
	animated_scene_finalizer: RefCounted = null,
	resource_loader_adapter: Object = null
) -> void:
	_animated_scene_finalizer = animated_scene_finalizer
	if _animated_scene_finalizer == null:
		_animated_scene_finalizer = M2_ANIMATED_SCENE_FINALIZER_SCRIPT.new()
	_resource_loader_adapter = resource_loader_adapter


## Moves terminal threaded requests into the pipeline finalize FIFO.
## Empty resource paths retain the historical immediate static-only outcome.
func poll_terminal_requests(
	load_pipeline_state: RefCounted,
	prototype_cache_state: RefCounted
) -> int:
	if load_pipeline_state == null or prototype_cache_state == null:
		return 0
	var completed_request_count := 0
	var request_records: Array = load_pipeline_state.call(
		"request_records_snapshot"
	)
	for request_variant in request_records:
		if not (request_variant is Dictionary):
			continue
		var request: Dictionary = request_variant
		var normalized_relative_path := String(request.get("normalized", ""))
		var resource_path := String(request.get("path", ""))
		if resource_path.is_empty():
			load_pipeline_state.call(
				"discard_request",
				normalized_relative_path
			)
			prototype_cache_state.call(
				"mark_animation_static",
				normalized_relative_path
			)
			completed_request_count += 1
			continue
		var load_status := load_threaded_get_status(resource_path)
		if (
			load_status != ResourceLoader.THREAD_LOAD_LOADED
			and load_status != ResourceLoader.THREAD_LOAD_FAILED
		):
			continue
		load_pipeline_state.call(
			"complete_request",
			normalized_relative_path,
			load_status
		)
		completed_request_count += 1
	return completed_request_count


## Pops one terminal record and prepares a detached animated scene candidate.
## An empty result means the record was skipped or resolved as static-only.
func prepare_next_candidate(
	load_pipeline_state: RefCounted,
	prototype_cache_state: RefCounted
) -> Dictionary:
	if load_pipeline_state == null or prototype_cache_state == null:
		return {}
	var terminal_record: Dictionary = load_pipeline_state.call(
		"pop_finalize_record"
	)
	if terminal_record.is_empty():
		return {}
	var normalized_relative_path := String(
		terminal_record.get("normalized", "")
	)
	if (
		normalized_relative_path.is_empty()
		or bool(prototype_cache_state.call(
			"has_animated_prototype",
			normalized_relative_path
		))
		or bool(prototype_cache_state.call(
			"is_animation_static",
			normalized_relative_path
		))
	):
		return {}
	if int(terminal_record.get(
		"status",
		ResourceLoader.THREAD_LOAD_FAILED
	)) != ResourceLoader.THREAD_LOAD_LOADED:
		prototype_cache_state.call(
			"mark_animation_static",
			normalized_relative_path
		)
		return {}

	var resource_path := String(terminal_record.get("path", ""))
	var loaded_resource := load_threaded_get(resource_path)
	var candidate := _animated_scene_finalizer.call(
		"instantiate_candidate",
		loaded_resource
	) as Node3D
	if candidate == null:
		prototype_cache_state.call(
			"mark_animation_static",
			normalized_relative_path
		)
		return {}
	return {
		"normalized": normalized_relative_path,
		"path": resource_path,
		"candidate": candidate,
	}


## Repairs, validates and adopts one prepared candidate. The preparation must be
## completed synchronously so its detached candidate always receives an owner.
func finalize_prepared_candidate(
	preparation: Dictionary,
	material_source_root: Node3D,
	prototype_cache_state: RefCounted,
	debug_logging_enabled: bool = false
) -> Node3D:
	if prototype_cache_state == null:
		return null
	var normalized_relative_path := String(preparation.get("normalized", ""))
	var candidate := preparation.get("candidate", null) as Node3D
	if normalized_relative_path.is_empty() or candidate == null:
		return null
	_animated_scene_finalizer.call(
		"repair_materials",
		candidate,
		material_source_root
	)
	var finalization: Dictionary = _animated_scene_finalizer.call(
		"finalize_candidate",
		candidate
	)
	var prototype := finalization.get("prototype", null) as Node3D
	if prototype == null:
		prototype_cache_state.call(
			"mark_animation_static",
			normalized_relative_path
		)
		return null
	var canonical_prototype := prototype_cache_state.call(
		"adopt_animated_prototype",
		normalized_relative_path,
		prototype
	) as Node3D
	if debug_logging_enabled:
		print("M2_ANIM_CACHE path=%s cache=%s players=%d" % [
			normalized_relative_path,
			String(preparation.get("path", "")),
			int(finalization.get("animation_player_count", 0)),
		])
	return canonical_prototype


## Production ResourceLoader status adapter; injectable in synthetic tests.
func load_threaded_get_status(resource_path: String) -> int:
	if _resource_loader_adapter != null:
		return int(_resource_loader_adapter.call(
			"load_threaded_get_status",
			resource_path
		))
	return ResourceLoader.load_threaded_get_status(resource_path)


## Production ResourceLoader terminal-result adapter; injectable in tests.
func load_threaded_get(resource_path: String) -> Resource:
	if _resource_loader_adapter != null:
		return _resource_loader_adapter.call(
			"load_threaded_get",
			resource_path
		) as Resource
	return ResourceLoader.load_threaded_get(resource_path)
