extends Node
class_name M2NativeAnimator

var mesh_instance_path: NodePath = NodePath("../Mesh")
var mesh_instance: MeshInstance3D
var mesh: ArrayMesh
var bones: Array = []
var surfaces: Array = []
var animation_length: float = 0.0
var playback_speed: float = 1.0

var _time: float = 0.0
var _materials: Array[Material] = []
var _unique_mesh_ready := false
var _prepared := false


func setup(target_mesh_instance: MeshInstance3D, bone_data: Array, surface_data: Array, length_seconds: float) -> void:
	mesh_instance = target_mesh_instance
	if mesh_instance != null:
		mesh_instance_path = get_path_to(mesh_instance)
		mesh = mesh_instance.mesh as ArrayMesh
	bones = bone_data
	surfaces = surface_data
	animation_length = maxf(length_seconds, 0.0)
	_capture_materials()
	_make_mesh_unique()
	_rebuild_mesh(0.0)
	set_process(mesh != null and not bones.is_empty() and not surfaces.is_empty() and animation_length > 0.0)


func _ready() -> void:
	prepare_runtime()


func _process(delta: float) -> void:
	if mesh == null or bones.is_empty() or surfaces.is_empty() or animation_length <= 0.0:
		return
	_time = fmod(_time + delta * playback_speed, animation_length)
	_rebuild_mesh(_time)


func set_phase(phase: float) -> void:
	if animation_length <= 0.0:
		_time = 0.0
	else:
		_time = fposmod(animation_length * phase, animation_length)
	_rebuild_mesh(_time)


func prepare_runtime() -> bool:
	_resolve_mesh_instance()
	_capture_materials()
	_unique_mesh_ready = false
	_make_mesh_unique()
	_rebuild_mesh(_time)
	_prepared = mesh != null and not bones.is_empty() and not surfaces.is_empty() and animation_length > 0.0
	set_process(_prepared)
	return _prepared


func runtime_debug_state() -> Dictionary:
	return {
		"prepared": _prepared,
		"processing": is_processing(),
		"has_mesh": mesh != null,
		"bones": bones.size(),
		"surfaces": surfaces.size(),
		"length": animation_length,
	}


func _resolve_mesh_instance() -> void:
	var resolved := get_node_or_null(mesh_instance_path)
	if resolved is MeshInstance3D:
		mesh_instance = resolved as MeshInstance3D
	if mesh_instance != null:
		mesh = mesh_instance.mesh as ArrayMesh


func _make_mesh_unique() -> void:
	if _unique_mesh_ready or mesh_instance == null or mesh_instance.mesh == null:
		return
	var duplicated := mesh_instance.mesh.duplicate(true) as ArrayMesh
	if duplicated == null:
		return
	mesh_instance.mesh = duplicated
	mesh = duplicated
	_unique_mesh_ready = true


func _capture_materials() -> void:
	if mesh == null:
		return
	if not _materials.is_empty() and _materials.size() == mesh.get_surface_count():
		return
	_materials.clear()
	for surface_index in mesh.get_surface_count():
		_materials.append(mesh.surface_get_material(surface_index))


func _rebuild_mesh(time: float) -> void:
	if mesh == null:
		return
	var bone_matrices := _build_bone_matrices(time)
	if bone_matrices.is_empty():
		return

	mesh.clear_surfaces()
	for surface_index in surfaces.size():
		var source = surfaces[surface_index]
		if not (source is Dictionary):
			continue
		var surface: Dictionary = source
		var base_vertices: PackedVector3Array = surface.get("vertices", PackedVector3Array())
		var base_normals: PackedVector3Array = surface.get("normals", PackedVector3Array())
		var uvs: PackedVector2Array = surface.get("uvs", PackedVector2Array())
		var uvs2: PackedVector2Array = surface.get("uvs2", PackedVector2Array())
		var indices: PackedInt32Array = surface.get("indices", PackedInt32Array())
		var bone_indices: PackedInt32Array = surface.get("bones", PackedInt32Array())
		var weights: PackedFloat32Array = surface.get("weights", PackedFloat32Array())
		if base_vertices.is_empty() or indices.is_empty():
			continue

		var vertices := PackedVector3Array()
		var normals := PackedVector3Array()
		vertices.resize(base_vertices.size())
		if base_normals.size() == base_vertices.size():
			normals.resize(base_normals.size())

		var can_skin := bone_indices.size() == base_vertices.size() * 4 and weights.size() == base_vertices.size() * 4
		for vertex_index in base_vertices.size():
			if can_skin:
				var skinned_pos := Vector3.ZERO
				var skinned_nrm := Vector3.ZERO
				var total_weight := 0.0
				for influence in 4:
					var weight := weights[vertex_index * 4 + influence]
					if weight <= 0.0:
						continue
					var bone_index := bone_indices[vertex_index * 4 + influence]
					if bone_index < 0 or bone_index >= bone_matrices.size():
						continue
					var transform: Transform3D = bone_matrices[bone_index]
					skinned_pos += transform * base_vertices[vertex_index] * weight
					if normals.size() == base_normals.size():
						skinned_nrm += (transform.basis * base_normals[vertex_index]) * weight
					total_weight += weight
				if total_weight > 0.0:
					vertices[vertex_index] = skinned_pos / total_weight
					if normals.size() == base_normals.size():
						normals[vertex_index] = (skinned_nrm / total_weight).normalized()
				else:
					vertices[vertex_index] = base_vertices[vertex_index]
					if normals.size() == base_normals.size():
						normals[vertex_index] = base_normals[vertex_index]
			else:
				vertices[vertex_index] = base_vertices[vertex_index]
				if normals.size() == base_normals.size():
					normals[vertex_index] = base_normals[vertex_index]

		var arrays := []
		arrays.resize(Mesh.ARRAY_MAX)
		arrays[Mesh.ARRAY_VERTEX] = vertices
		if normals.size() == vertices.size():
			arrays[Mesh.ARRAY_NORMAL] = normals
		if uvs.size() == vertices.size():
			arrays[Mesh.ARRAY_TEX_UV] = uvs
		if uvs2.size() == vertices.size():
			arrays[Mesh.ARRAY_TEX_UV2] = uvs2
		arrays[Mesh.ARRAY_INDEX] = indices
		mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
		if surface_index < _materials.size() and _materials[surface_index] != null:
			mesh.surface_set_material(mesh.get_surface_count() - 1, _materials[surface_index])


func _build_bone_matrices(time: float) -> Array[Transform3D]:
	var result: Array[Transform3D] = []
	result.resize(bones.size())
	for i in range(bones.size()):
		var bone: Dictionary = bones[i]
		var pivot: Vector3 = bone.get("pivot", Vector3.ZERO)
		var translation := _sample_vec3_track(bone.get("translation", {}), time, Vector3.ZERO)
		var rotation := _sample_quat_track(bone.get("rotation", {}), time, Quaternion.IDENTITY)
		var scale := _sample_vec3_track(bone.get("scale", {}), time, Vector3.ONE)

		var local := Transform3D(Basis.IDENTITY, pivot + translation)
		local = local * Transform3D(Basis(rotation).scaled(scale), Vector3.ZERO)
		local = local * Transform3D(Basis.IDENTITY, -pivot)

		var parent := int(bone.get("parent", -1))
		if parent >= 0 and parent < i:
			result[i] = result[parent] * local
		else:
			result[i] = local
	return result


func _sample_vec3_track(track: Variant, time: float, fallback: Vector3) -> Vector3:
	if not (track is Dictionary):
		return fallback
	var times: PackedFloat32Array = track.get("times", PackedFloat32Array())
	var values: PackedVector3Array = track.get("values", PackedVector3Array())
	var index := _track_index(times, time)
	if index < 0 or values.is_empty():
		return fallback
	if index >= values.size() - 1 or index >= times.size() - 1:
		return values[mini(index, values.size() - 1)]
	var t0 := times[index]
	var t1 := times[index + 1]
	var alpha := 0.0 if is_equal_approx(t0, t1) else clampf((time - t0) / (t1 - t0), 0.0, 1.0)
	return values[index].lerp(values[index + 1], alpha)


func _sample_quat_track(track: Variant, time: float, fallback: Quaternion) -> Quaternion:
	if not (track is Dictionary):
		return fallback
	var times: PackedFloat32Array = track.get("times", PackedFloat32Array())
	var raw_values: PackedVector4Array = track.get("values", PackedVector4Array())
	var index := _track_index(times, time)
	if index < 0 or raw_values.is_empty():
		return fallback
	var q0 := _quat_from_vec4(raw_values[mini(index, raw_values.size() - 1)])
	if index >= raw_values.size() - 1 or index >= times.size() - 1:
		return q0.normalized()
	var q1 := _quat_from_vec4(raw_values[index + 1])
	var t0 := times[index]
	var t1 := times[index + 1]
	var alpha := 0.0 if is_equal_approx(t0, t1) else clampf((time - t0) / (t1 - t0), 0.0, 1.0)
	return q0.slerp(q1, alpha).normalized()


func _track_index(times: PackedFloat32Array, time: float) -> int:
	if times.is_empty():
		return -1
	if times.size() == 1 or time <= times[0]:
		return 0
	for i in range(times.size() - 1):
		if time >= times[i] and time < times[i + 1]:
			return i
	return times.size() - 1


func _quat_from_vec4(v: Vector4) -> Quaternion:
	return Quaternion(v.x, v.y, v.z, v.w).normalized()
