/*
 * © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
 */

package com.tiedup.remake.rig.bridge;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;

import com.tiedup.remake.client.gltf.GltfData;
import com.tiedup.remake.client.gltf.GltfData.Primitive;
import com.tiedup.remake.rig.TiedUpRigConstants;
import com.tiedup.remake.rig.armature.Armature;
import com.tiedup.remake.rig.armature.Joint;
import com.tiedup.remake.rig.asset.AssetAccessor;
import com.tiedup.remake.rig.math.Vec2f;
import com.tiedup.remake.rig.math.Vec3f;
import com.tiedup.remake.rig.mesh.MeshPartDefinition;
import com.tiedup.remake.rig.mesh.SingleGroupVertexBuilder;
import com.tiedup.remake.rig.mesh.SkinnedMesh;
import com.tiedup.remake.rig.mesh.transformer.VanillaModelTransformer.VanillaMeshPartDefinition;

/**
 * Pont Phase 1 : convertit un {@link GltfData} (format GLB legacy TiedUp
 * riggé 11-joints PlayerAnimator) en {@link SkinnedMesh} Epic Fight
 * (biped ~20 joints).
 *
 * <p>Algorithme (voir {@code docs/plans/rig/MIGRATION.md §1.2.1}) :</p>
 * <ol>
 *   <li>Pré-calculer le mapping {@code gltfJointIdx → bipedJointId} via
 *       {@link GlbJointAliasTable} + {@link Armature#searchJointByName}.</li>
 *   <li>Pour chaque vertex :
 *     <ul>
 *       <li>Position / normal / UV depuis {@link GltfData}</li>
 *       <li>Retenir les 3 joints de plus fort poids parmi les 4 glTF</li>
 *       <li>Renormaliser les poids retenus pour sommer à 1.0</li>
 *       <li>Construire le {@link SingleGroupVertexBuilder}</li>
 *     </ul>
 *   </li>
 *   <li>Grouper les indices par {@link Primitive} en autant de
 *       {@link MeshPartDefinition}.</li>
 *   <li>{@link SingleGroupVertexBuilder#loadVertexInformation(List, Map)}
 *       construit le {@link SkinnedMesh}.</li>
 * </ol>
 *
 * <p>Les animations éventuellement embarquées dans le GLB sont <b>ignorées</b> —
 * les animations passent par JSON EF natif via {@code JsonAssetLoader}.</p>
 */
public final class GltfToSkinnedMesh {

	private static final float WEIGHT_EPSILON = 1.0e-4F;

	private GltfToSkinnedMesh() {}

	/**
	 * Convertit un GLB parsé en {@link SkinnedMesh} utilisable par le pipeline
	 * de rendu RIG.
	 *
	 * @param data     données GLB parsées par {@code GlbParser.parse(...)}
	 * @param armature armature biped EF cible (doit être déjà chargée)
	 * @return SkinnedMesh prêt à être rendu
	 * @throws IllegalStateException si {@code armature} est null
	 */
	public static SkinnedMesh convert(GltfData data, AssetAccessor<? extends Armature> armature) {
		if (armature == null || armature.get() == null) {
			throw new IllegalStateException(
				"Armature not loaded — GltfToSkinnedMesh.convert() called before resource reload completed"
			);
		}

		Armature arm = armature.get();
		int[] jointIdMap = buildJointIdMap(data.jointNames(), arm);

		int vertexCount = data.vertexCount();
		float[] positions = data.positions();
		float[] normals = data.normals();
		float[] texCoords = data.texCoords();
		int[] joints = data.joints();
		float[] weights = data.weights();

		List<SingleGroupVertexBuilder> vertices = new ArrayList<>(vertexCount);
		for (int i = 0; i < vertexCount; i++) {
			vertices.add(buildVertex(i, positions, normals, texCoords, joints, weights, jointIdMap));
		}

		Map<MeshPartDefinition, IntList> partIndices = buildPartIndices(data.primitives());

		return SingleGroupVertexBuilder.loadVertexInformation(vertices, partIndices);
	}

	/**
	 * Construit le mapping {@code gltfJointIdx → bipedJointId} une seule fois
	 * avant la boucle vertex. Les noms inconnus retombent sur la racine
	 * {@code Root} (id 0) avec un log WARN.
	 */
	private static int[] buildJointIdMap(String[] gltfJointNames, Armature arm) {
		int[] map = new int[gltfJointNames.length];
		int unknownCount = 0;
		int aliasedCount = 0;
		int rootId = arm.rootJoint != null ? arm.rootJoint.getId() : 0;

		for (int i = 0; i < gltfJointNames.length; i++) {
			String gltfName = gltfJointNames[i];
			String bipedName = GlbJointAliasTable.mapGltfJointName(gltfName);

			if (bipedName == null) {
				TiedUpRigConstants.LOGGER.warn(
					"GltfToSkinnedMesh: unknown joint '{}' — fallback to Root",
					gltfName
				);
				map[i] = rootId;
				unknownCount++;
				continue;
			}

			Joint joint = arm.searchJointByName(bipedName);
			if (joint == null) {
				TiedUpRigConstants.LOGGER.warn(
					"GltfToSkinnedMesh: biped joint '{}' (aliased from '{}') not found in armature — fallback to Root",
					bipedName, gltfName
				);
				map[i] = rootId;
				unknownCount++;
				continue;
			}

			map[i] = joint.getId();
			if (!gltfName.equals(bipedName)) {
				aliasedCount++;
			}
		}

		TiedUpRigConstants.LOGGER.info(
			"GltfToSkinnedMesh: {} joints mapped ({} via alias, {} unknown→Root)",
			gltfJointNames.length, aliasedCount, unknownCount
		);
		return map;
	}

	/**
	 * Construit un vertex individuel : position/normal/UV depuis les arrays
	 * flattened, puis sélection des 3 plus forts poids (drop du 4e) + renormalisation.
	 */
	private static SingleGroupVertexBuilder buildVertex(
			int i,
			float[] positions, float[] normals, float[] texCoords,
			int[] joints, float[] weights,
			int[] jointIdMap) {

		SingleGroupVertexBuilder vb = new SingleGroupVertexBuilder();
		vb.setPosition(new Vec3f(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]));
		vb.setNormal(new Vec3f(normals[i * 3], normals[i * 3 + 1], normals[i * 3 + 2]));
		vb.setTextureCoordinate(new Vec2f(texCoords[i * 2], texCoords[i * 2 + 1]));

		// Récupère les 4 joints/poids glTF, sélectionne les 3 plus forts.
		int[] rawJoints = new int[4];
		float[] rawWeights = new float[4];
		for (int k = 0; k < 4; k++) {
			rawJoints[k] = joints[i * 4 + k];
			rawWeights[k] = weights[i * 4 + k];
		}

		// Trouve l'index du plus faible poids (à drop).
		int minIdx = 0;
		for (int k = 1; k < 4; k++) {
			if (rawWeights[k] < rawWeights[minIdx]) {
				minIdx = k;
			}
		}

		// Build les 3 retenus + compte effective.
		float w0 = 0, w1 = 0, w2 = 0;
		int id0 = 0, id1 = 0, id2 = 0;
		int effectiveCount = 0;
		int slot = 0;
		for (int k = 0; k < 4; k++) {
			if (k == minIdx) continue;
			float w = rawWeights[k];
			int id = jointIdMap[rawJoints[k]];
			switch (slot) {
				case 0 -> { w0 = w; id0 = id; }
				case 1 -> { w1 = w; id1 = id; }
				case 2 -> { w2 = w; id2 = id; }
			}
			if (w > WEIGHT_EPSILON) effectiveCount++;
			slot++;
		}

		// Renormalise les 3 poids pour qu'ils somment à 1.0.
		float sum = w0 + w1 + w2;
		if (sum > WEIGHT_EPSILON) {
			float inv = 1.0F / sum;
			w0 *= inv; w1 *= inv; w2 *= inv;
		} else {
			// Vertex sans skinning (tout-zéro ou bugué) — attache au Root avec poids 1.
			w0 = 1.0F; w1 = 0; w2 = 0;
			id0 = 0; id1 = 0; id2 = 0;
			effectiveCount = 1;
		}

		vb.setEffectiveJointIDs(new Vec3f(id0, id1, id2));
		vb.setEffectiveJointWeights(new Vec3f(w0, w1, w2));
		vb.setEffectiveJointNumber(Math.max(1, effectiveCount));
		return vb;
	}

	/**
	 * Groupe les indices par primitive (= material dans Blender) → une
	 * {@link VanillaMeshPartDefinition} par primitive. Le partName est pris sur
	 * le {@code materialName} si défini, sinon un nom synthétique
	 * {@code "part_N"}.
	 */
	private static Map<MeshPartDefinition, IntList> buildPartIndices(List<Primitive> primitives) {
		Map<MeshPartDefinition, IntList> partIndices = new HashMap<>();
		int fallbackCounter = 0;

		for (Primitive prim : primitives) {
			String partName = prim.materialName();
			if (partName == null || partName.isEmpty()) {
				partName = "part_" + fallbackCounter++;
			}

			MeshPartDefinition partDef = VanillaMeshPartDefinition.of(partName);
			IntList indexList = new IntArrayList(prim.indices().length);
			for (int idx : prim.indices()) {
				indexList.add(idx);
			}
			partIndices.put(partDef, indexList);
		}

		return partIndices;
	}
}
