class_name M2StaticBuildResourceObserver
extends RefCounted

## Resolves or requests one static M2 build Mesh and fills a resource snapshot.

const OUTCOME_REJECTED := &"rejected"
const OUTCOME_CACHED := &"cached"
const OUTCOME_PENDING := &"pending"
const OUTCOME_REQUESTED := &"requested"
const OUTCOME_MISSING := &"missing"


## Observes cache/request state and adopts the exact static result into snapshot.
func observe(
	resource_snapshot: RefCounted,
	normalized_relative_path: String,
	cache_directory: String,
	mesh_cache_state: RefCounted,
	prototype_cache_state: RefCounted,
	load_pipeline_state: RefCounted
) -> StringName:
	if (
		resource_snapshot == null
		or normalized_relative_path.is_empty()
		or mesh_cache_state == null
		or prototype_cache_state == null
		or load_pipeline_state == null
	):
		return OUTCOME_REJECTED
	if bool(mesh_cache_state.call("has_mesh", normalized_relative_path)):
		resource_snapshot.call(
			"adopt_static_observation",
			mesh_cache_state.call("find_mesh", normalized_relative_path) as Mesh,
			false
		)
		return OUTCOME_CACHED
	if bool(prototype_cache_state.call("is_model_missing", normalized_relative_path)):
		resource_snapshot.call("adopt_static_observation", null, true)
		return OUTCOME_MISSING
	if bool(load_pipeline_state.call("has_request", normalized_relative_path)):
		resource_snapshot.call("adopt_static_observation", null, false)
		return OUTCOME_PENDING

	for cache_resource_path in cache_resource_paths(
		cache_directory,
		normalized_relative_path,
		[".tscn", ".glb"]
	):
		if not ResourceLoader.exists(cache_resource_path):
			continue
		if (
			cache_resource_path.get_extension().to_lower() == "glb"
			and glb_animation_schema(cache_resource_path) == "pivot_prefix_v1"
		):
			continue
		var request_error := ResourceLoader.load_threaded_request(
			cache_resource_path,
			"",
			false,
			ResourceLoader.CACHE_MODE_REUSE
		)
		if request_error == OK or request_error == ERR_BUSY:
			load_pipeline_state.call(
				"remember_request",
				normalized_relative_path,
				cache_resource_path
			)
			resource_snapshot.call("adopt_static_observation", null, false)
			return OUTCOME_REQUESTED
		break

	prototype_cache_state.call("mark_model_missing", normalized_relative_path)
	resource_snapshot.call("adopt_static_observation", null, true)
	return OUTCOME_MISSING


## Returns historical nested/lowercase/basename cache candidates without I/O.
func cache_resource_paths(
	cache_directory: String,
	relative_path: String,
	extensions: Array[String]
) -> PackedStringArray:
	var normalized := relative_path.replace("\\", "/")
	var lower := normalized.to_lower()
	var stems := [
		normalized.get_basename(),
		lower.get_basename(),
		normalized.get_file().get_basename(),
		lower.get_file().get_basename(),
	]
	var result := PackedStringArray()
	for extension in extensions:
		for stem in stems:
			if stem.is_empty():
				continue
			var path := cache_directory.path_join(stem + extension)
			if not result.has(path):
				result.append(path)
	return result


## Reads only the OpenWC animation schema marker from a GLB JSON chunk.
func glb_animation_schema(cache_resource_path: String) -> String:
	var absolute_path := ProjectSettings.globalize_path(cache_resource_path)
	if not FileAccess.file_exists(absolute_path):
		return ""
	var file := FileAccess.open(absolute_path, FileAccess.READ)
	if file == null or file.get_length() < 20:
		return ""
	var magic := file.get_32()
	var version := file.get_32()
	file.get_32()
	if magic != 0x46546c67 or version != 2:
		return ""
	var json_length := int(file.get_32())
	var chunk_type := file.get_32()
	if json_length <= 0 or chunk_type != 0x4e4f534a:
		return ""
	var parsed: Variant = JSON.parse_string(
		file.get_buffer(json_length).get_string_from_utf8()
	)
	if not (parsed is Dictionary):
		return ""
	var asset: Dictionary = (parsed as Dictionary).get("asset", {})
	var extras: Dictionary = asset.get("extras", {})
	return String(extras.get("openwc_m2_anim_schema", ""))
