class_name M2PrototypeCacheState
extends RefCounted

## Owns detached static/animated M2 prototypes and their negative lookup
## outcomes until final renderer shutdown.

var _static_prototypes_by_path: Dictionary = {}
var _animated_prototypes_by_path: Dictionary = {}
var _missing_model_paths: Dictionary = {}
var _static_animation_paths: Dictionary = {}


## Returns whether a valid static prototype is retained for the normalized path.
func has_static_prototype(normalized_relative_path: String) -> bool:
	return find_static_prototype(normalized_relative_path) != null


## Returns the exact retained static prototype or null.
func find_static_prototype(normalized_relative_path: String) -> Node3D:
	return _find_valid_prototype(_static_prototypes_by_path, normalized_relative_path)


## Adopts a static prototype and returns the canonical retained reference. The
## first valid prototype wins; a later detached candidate is released.
func adopt_static_prototype(
		normalized_relative_path: String,
		prototype: Node3D) -> Node3D:
	return _adopt_prototype(
		_static_prototypes_by_path,
		normalized_relative_path,
		prototype
	)


## Returns whether a valid animated prototype is retained for the normalized path.
func has_animated_prototype(normalized_relative_path: String) -> bool:
	return find_animated_prototype(normalized_relative_path) != null


## Returns the exact retained animated prototype or null.
func find_animated_prototype(normalized_relative_path: String) -> Node3D:
	return _find_valid_prototype(_animated_prototypes_by_path, normalized_relative_path)


## Adopts an animated prototype and returns the canonical retained reference.
func adopt_animated_prototype(
		normalized_relative_path: String,
		prototype: Node3D) -> Node3D:
	return _adopt_prototype(
		_animated_prototypes_by_path,
		normalized_relative_path,
		prototype
	)


## Records that no model prototype can currently be loaded for this path.
func mark_model_missing(normalized_relative_path: String) -> bool:
	return _mark_path(_missing_model_paths, normalized_relative_path)


## Returns whether the model path has a retained missing outcome.
func is_model_missing(normalized_relative_path: String) -> bool:
	return not normalized_relative_path.is_empty() and _missing_model_paths.has(
		normalized_relative_path
	)


## Records that the path must use static presentation instead of animation.
func mark_animation_static(normalized_relative_path: String) -> bool:
	return _mark_path(_static_animation_paths, normalized_relative_path)


## Returns whether the path has a retained static-animation outcome.
func is_animation_static(normalized_relative_path: String) -> bool:
	return not normalized_relative_path.is_empty() and _static_animation_paths.has(
		normalized_relative_path
	)


## Returns detached path-only cache state for diagnostics and verification.
func diagnostic_snapshot() -> Dictionary:
	return {
		"static_prototype_paths": _sorted_paths(_static_prototypes_by_path),
		"animated_prototype_paths": _sorted_paths(_animated_prototypes_by_path),
		"missing_model_paths": _sorted_paths(_missing_model_paths),
		"static_animation_paths": _sorted_paths(_static_animation_paths),
	}


## Releases every retained prototype and clears positive and negative state.
## Detached Nodes are freed synchronously; in-tree Nodes are queued for deletion.
func clear_and_release() -> void:
	_release_prototypes(_static_prototypes_by_path)
	_release_prototypes(_animated_prototypes_by_path)
	_missing_model_paths.clear()
	_static_animation_paths.clear()


func _find_valid_prototype(
		prototypes_by_path: Dictionary,
		normalized_relative_path: String) -> Node3D:
	if normalized_relative_path.is_empty():
		return null
	var prototype_variant: Variant = prototypes_by_path.get(normalized_relative_path)
	if not (prototype_variant is Node3D):
		return null
	var prototype := prototype_variant as Node3D
	return prototype if is_instance_valid(prototype) else null


func _adopt_prototype(
		prototypes_by_path: Dictionary,
		normalized_relative_path: String,
		prototype: Node3D) -> Node3D:
	if normalized_relative_path.is_empty() or prototype == null or not is_instance_valid(prototype):
		return null
	var retained_prototype := _find_valid_prototype(
		prototypes_by_path,
		normalized_relative_path
	)
	if retained_prototype != null:
		if not is_same(retained_prototype, prototype):
			_release_prototype(prototype)
		return retained_prototype
	prototypes_by_path[normalized_relative_path] = prototype
	return prototype


func _mark_path(paths: Dictionary, normalized_relative_path: String) -> bool:
	if normalized_relative_path.is_empty():
		return false
	paths[normalized_relative_path] = true
	return true


func _release_prototypes(prototypes_by_path: Dictionary) -> void:
	for prototype_variant in prototypes_by_path.values():
		if prototype_variant is Node3D:
			_release_prototype(prototype_variant as Node3D)
	prototypes_by_path.clear()


func _release_prototype(prototype: Node3D) -> void:
	if prototype == null or not is_instance_valid(prototype):
		return
	if prototype.is_inside_tree():
		prototype.queue_free()
	else:
		prototype.free()


func _sorted_paths(paths: Dictionary) -> Array[String]:
	var result: Array[String] = []
	for path_variant in paths.keys():
		result.append(String(path_variant))
	result.sort()
	return result
