extends SceneTree

# Пакетный конвертер: по всем tools/rig/geom/<char>__<stem>.json запекает анимации из Spine
# и собирает нативные characters/<char>/<stem>_native.tscn (Skeleton2D + Polygon2D + AnimationPlayer).
# Запуск: godot ... --headless --script res://tools/convert_all.gd

const GEOM_DIR := "res://tools/rig/geom"
const FPS := 30.0
const VP := Vector2(1280, 720)

func _initialize():
	var files := []
	var d := DirAccess.open(GEOM_DIR)
	d.list_dir_begin()
	var n := d.get_next()
	while n != "":
		if n.ends_with(".json"): files.append(n)
		n = d.get_next()
	files.sort()

	var ok := 0; var fail := 0; var i := 0
	for fn in files:
		i += 1
		var stem_full: String = fn.get_basename()          # char__stem
		var sep := stem_full.find("__")
		var char := stem_full.substr(0, sep)
		var stem := stem_full.substr(sep + 2)
		var base := "res://characters/%s/spine/%s" % [char, stem]
		if not FileAccess.file_exists(base + ".skel.bytes"):
			fail += 1; continue
		var out := "res://characters/%s/%s_native.tscn" % [char, stem]
		if _convert(base, GEOM_DIR + "/" + fn, out):
			ok += 1
		else:
			fail += 1
		if i % 10 == 0:
			print("... %d/%d (ok=%d)" % [i, files.size(), ok])
			await process_frame
	print("CONVERT ALL done: ok=%d fail=%d из %d" % [ok, fail, files.size()])
	quit()

func _convert(base: String, geom_path: String, out_path: String) -> bool:
	var gf := FileAccess.open(geom_path, FileAccess.READ)
	if gf == null: return false
	var geom: Dictionary = JSON.parse_string(gf.get_as_text()); gf.close()
	if geom == null or not geom.has("bones"): return false

	var anims = _bake(base)
	if anims == null: return false

	var root = _build(geom, anims, base.get_base_dir())
	if root == null: return false
	_set_owner(root, root)

	var packed := PackedScene.new()
	if packed.pack(root) != OK:
		root.free(); return false
	var err := ResourceSaver.save(packed, out_path)
	root.free()
	return err == OK

# ---------- запекание анимаций ----------
func _bake(base: String):
	var atlas := SpineAtlasResource.new()
	if atlas.load_from_atlas_file(base + ".atlas.txt") != OK: return null
	var skf := SpineSkeletonFileResource.new(); skf.load_from_file(base + ".skel.bytes")
	var data := SpineSkeletonDataResource.new()
	data.set_atlas_res(atlas); data.set_skeleton_file_res(skf)
	if not data.is_skeleton_data_loaded(): return null

	var sprite := SpineSprite.new(); sprite.set_skeleton_data_res(data)
	get_root().add_child(sprite)
	var st = sprite.get_animation_state()
	var skel = sprite.get_skeleton()

	var bone_names := []
	for bd in data.get_bones(): bone_names.append(bd.get_bone_name())
	var slot_names := []; var slot_index := {}
	var live_slots = skel.get_slots()
	for i in live_slots.size():
		var sn = live_slots[i].get_data().get_name()
		slot_names.append(sn); slot_index[sn] = i

	var out := { "fps": FPS, "bones": bone_names, "slots": slot_names, "animations": {} }
	for sa in data.get_animations():
		var anim_name = sa.get_name()
		var dur: float = sa.get_duration()
		var nframes := int(round(dur * FPS)) + 1
		st.clear_tracks(); st.set_animation(anim_name, false, 0)
		var frames := []; var draworder := []
		for f in nframes:
			skel.set_bones_to_setup_pose(); skel.set_slots_to_setup_pose()
			st.apply(skel)
			var frame := []
			for nm in bone_names:
				var b = skel.find_bone(nm)
				frame.append([snappedf(b.get_x(),0.001), snappedf(b.get_y(),0.001), snappedf(b.get_rotation(),0.001), snappedf(b.get_scale_x(),0.001), snappedf(b.get_scale_y(),0.001)])
			frames.append(frame)
			var order := []
			for s in skel.get_draw_order(): order.append(slot_index[s.get_data().get_name()])
			draworder.append(order)
			st.update(1.0 / FPS)
		out["animations"][anim_name] = { "duration": dur, "frames": frames, "draworder": draworder }

	get_root().remove_child(sprite); sprite.free()
	return out

# ---------- сборка нативного рига ----------
func _build(geom: Dictionary, anims: Dictionary, spine_dir: String):
	# текстуры страниц атласа
	var pagetex := {}
	for pg in geom.get("pages", []):
		pagetex[pg] = load(spine_dir + "/" + pg)

	var root := Node2D.new(); root.name = "Native"
	var spine := Node2D.new(); spine.name = "SpineRoot"; root.add_child(spine)
	var skel := Skeleton2D.new(); skel.name = "Skeleton2D"; spine.add_child(skel)

	var by_name := {}; var bones := []
	for bd in geom["bones"]:
		var b := Bone2D.new(); b.name = bd["name"]
		b.set_autocalculate_length_and_angle(false); b.set_length(float(bd["length"])); b.set_bone_angle(0.0)
		var t := Transform2D(deg_to_rad(float(bd["rotation"])), Vector2(bd["scaleX"], bd["scaleY"]), 0.0, Vector2(bd["x"], bd["y"]))
		var parent = by_name.get(bd["parent"], skel)
		parent.add_child(b); b.transform = t; b.rest = t
		bones.append(b); by_name[bd["name"]] = b

	var poly_by_slot := {}; var base_rank := {}
	var minv := Vector2(1e9,1e9); var maxv := Vector2(-1e9,-1e9); var rank := 0
	for att in geom["attachments"]:
		var world: Array = att["world"]; var uv: Array = att["uv"]; var tris: Array = att["triangles"]; var weights: Array = att["weights"]
		var vc := world.size() / 2
		if vc < 3: continue
		var tex = pagetex.get(att["page"], null)
		if tex == null: continue
		var ts: Vector2 = tex.get_size()
		var poly := Polygon2D.new(); poly.name = String(att["slot"]); poly.texture = tex
		var pts := PackedVector2Array(); var uvs := PackedVector2Array(); pts.resize(vc); uvs.resize(vc)
		for i in vc:
			var p := Vector2(world[i*2], world[i*2+1]); pts[i] = p
			uvs[i] = Vector2(uv[i*2] * ts.x, uv[i*2+1] * ts.y)
			minv = minv.min(p); maxv = maxv.max(p)
		poly.polygon = pts; poly.uv = uvs
		var polys := []
		for t in range(0, tris.size(), 3): polys.append(PackedInt32Array([tris[t], tris[t+1], tris[t+2]]))
		poly.polygons = polys
		var c: Array = att["color"]; poly.modulate = Color(c[0],c[1],c[2],c[3])
		poly.z_index = rank
		skel.add_child(poly); poly.skeleton = poly.get_path_to(skel)
		poly_by_slot[String(att["slot"])] = poly; base_rank[String(att["slot"])] = rank; rank += 1
		var bw := {}
		for i in vc:
			for pair in weights[i]:
				var bi := int(pair[0])
				if not bw.has(bi): var a := PackedFloat32Array(); a.resize(vc); bw[bi] = a
				bw[bi][i] = float(pair[1])
		for bi in bw: poly.add_bone(skel.get_path_to(bones[bi]), bw[bi])

	if maxv.x < minv.x: root.free(); return null   # нет привязок
	var size := maxv - minv; var center := (minv + maxv) * 0.5
	var fit: float = min(VP.x * 0.55 / max(size.x,1), VP.y * 0.8 / max(size.y,1))
	spine.scale = Vector2(fit, -fit)
	spine.position = Vector2(VP.x*0.5 - center.x*fit, VP.y*0.5 + center.y*fit)

	# AnimationPlayer
	var player := AnimationPlayer.new(); player.name = "AnimationPlayer"; root.add_child(player)
	player.root_node = player.get_path_to(skel)
	var lib := AnimationLibrary.new()
	var bone_names: Array = anims["bones"]; var slot_names: Array = anims.get("slots", [])
	for anim_name in anims["animations"]:
		var a: Dictionary = anims["animations"][anim_name]
		var frames: Array = a["frames"]; var nf := frames.size()
		var anim := Animation.new(); anim.length = max(float(a["duration"]), 0.001); anim.loop_mode = Animation.LOOP_LINEAR
		for bi in bone_names.size():
			var bn: String = bone_names[bi]
			if not by_name.has(bn): continue
			var rel := String(skel.get_path_to(by_name[bn]))
			var f0: Array = frames[0][bi]; var pv := false; var rv := false; var sv := false
			for fr in range(1, nf):
				var v: Array = frames[fr][bi]
				if not pv and (not is_equal_approx(v[0],f0[0]) or not is_equal_approx(v[1],f0[1])): pv = true
				if not rv and not is_equal_approx(v[2],f0[2]): rv = true
				if not sv and (not is_equal_approx(v[3],f0[3]) or not is_equal_approx(v[4],f0[4])): sv = true
			_track(anim, rel+":position", frames, bi, nf, 0, pv)
			_track(anim, rel+":rotation", frames, bi, nf, 1, rv)
			_track(anim, rel+":scale", frames, bi, nf, 2, sv)
		var draworder: Array = a.get("draworder", [])
		if not draworder.is_empty() and not slot_names.is_empty():
			var ranks := {}
			for nm in poly_by_slot: ranks[nm] = []
			for fr in nf:
				var r := 0
				for si in draworder[fr]:
					var sn: String = slot_names[si]
					if poly_by_slot.has(sn): ranks[sn].append(r); r += 1
			for sn in ranks:
				var seq: Array = ranks[sn]; var vary := false
				for v in seq:
					if v != base_rank[sn]: vary = true; break
				if not vary or seq.size() != nf: continue
				var rel := String(skel.get_path_to(poly_by_slot[sn]))
				var ti := anim.add_track(Animation.TYPE_VALUE)
				anim.track_set_path(ti, rel + ":z_index")
				anim.value_track_set_update_mode(ti, Animation.UPDATE_DISCRETE)
				anim.track_set_interpolation_type(ti, Animation.INTERPOLATION_NEAREST)
				for fr in nf: anim.track_insert_key(ti, fr / FPS, seq[fr])
		lib.add_animation(anim_name, anim)
	player.add_animation_library("", lib)
	return root

func _track(anim: Animation, path: String, frames: Array, bi: int, nf: int, kind: int, vary: bool):
	if not vary: return
	var ti := anim.add_track(Animation.TYPE_VALUE)
	anim.track_set_path(ti, path); anim.track_set_interpolation_type(ti, Animation.INTERPOLATION_LINEAR)
	for fr in nf:
		var v: Array = frames[fr][bi]; var val
		match kind:
			0: val = Vector2(v[0], v[1])
			1: val = deg_to_rad(float(v[2]))
			2: val = Vector2(v[3], v[4])
		anim.track_insert_key(ti, fr / FPS, val)

func _set_owner(node: Node, owner: Node):
	for ch in node.get_children():
		ch.owner = owner; _set_owner(ch, owner)
