class_name M2RuntimeMeshRebuildClassifier
extends RefCounted

## Memoizes whether raw M2 billboard or UV-rotation metadata requires replacing
## a stale cached runtime mesh. Owns no parser, Mesh, material, Node or RID.

const UV_ROTATION_SPEED_EPSILON := 0.000001
const MAXIMUM_TEXTURE_STAGES := 4

var _rebuild_required_by_normalized_path: Dictionary = {}


## Returns the first memoized rebuild decision for [param normalized_relative_path].
## [param raw_m2_data] is borrowed and not mutated. The decision remains cached
## until [method clear], matching the loader map/shutdown reset lifetime.
func needs_runtime_mesh_rebuild(
		normalized_relative_path: String,
		raw_m2_data: Dictionary) -> bool:
	if _rebuild_required_by_normalized_path.has(normalized_relative_path):
		return bool(_rebuild_required_by_normalized_path[normalized_relative_path])
	var rebuild_required := (
		_raw_data_has_billboards(raw_m2_data)
		or _raw_data_has_uv_rotation(raw_m2_data)
	)
	_rebuild_required_by_normalized_path[normalized_relative_path] = rebuild_required
	return rebuild_required


## Clears every memoized path decision. No Mesh or resource is retained or freed.
func clear() -> void:
	_rebuild_required_by_normalized_path.clear()


## Returns the number of memoized normalized paths.
func cached_path_count() -> int:
	return _rebuild_required_by_normalized_path.size()


## Returns a detached normalized-path to boolean decision snapshot.
func diagnostic_snapshot() -> Dictionary:
	return _rebuild_required_by_normalized_path.duplicate()


func _raw_data_has_billboards(raw_m2_data: Dictionary) -> bool:
	for batch_variant in raw_m2_data.get("batches", []):
		if not (batch_variant is Dictionary):
			continue
		var batch: Dictionary = batch_variant
		if (
				bool(batch.get("has_billboard", false))
				or int(batch.get("billboard_vertex_count", 0)) > 0
		):
			return true
	return false


func _raw_data_has_uv_rotation(raw_m2_data: Dictionary) -> bool:
	var texture_transforms: Array = raw_m2_data.get("texture_transforms", [])
	if texture_transforms.is_empty():
		return false
	var texture_transform_combos: PackedInt32Array = raw_m2_data.get(
		"texture_transform_combos",
		PackedInt32Array()
	)
	if texture_transform_combos.is_empty():
		return false
	for batch_variant in raw_m2_data.get("batches", []):
		if not (batch_variant is Dictionary):
			continue
		var batch: Dictionary = batch_variant
		var texture_count := maxi(1, int(batch.get("texture_count", 1)))
		var base_combo_index := int(batch.get("texture_transform_combo_index", -1))
		for texture_stage in mini(texture_count, MAXIMUM_TEXTURE_STAGES):
			var combo_index := base_combo_index + texture_stage
			if combo_index < 0 or combo_index >= texture_transform_combos.size():
				continue
			var transform_index := int(texture_transform_combos[combo_index])
			if transform_index < 0 or transform_index >= texture_transforms.size():
				continue
			var texture_transform: Dictionary = (
				texture_transforms[transform_index]
				if texture_transforms[transform_index] is Dictionary
				else {}
			)
			var rotation: Vector4 = texture_transform.get(
				"rotation",
				Vector4(1.0, 0.0, 0.0, 1.0)
			)
			if not rotation.is_equal_approx(Vector4(1.0, 0.0, 0.0, 1.0)):
				return true
			if absf(float(texture_transform.get("rotation_speed", 0.0))) > UV_ROTATION_SPEED_EPSILON:
				return true
	return false
