/*
 * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
 * by the Epic Fight Team, licensed under GPLv3.
 * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
 */

package com.tiedup.remake.rig.render;

import java.util.List;
import java.util.Map;

import org.joml.Vector4f;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.mojang.blaze3d.vertex.PoseStack;

import net.minecraft.client.Minecraft;
import net.minecraft.client.model.EntityModel;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.entity.EntityRendererProvider;
import net.minecraft.client.renderer.entity.LivingEntityRenderer;
import net.minecraft.client.renderer.entity.layers.RenderLayer;
import net.minecraft.client.renderer.texture.OverlayTexture;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.LivingEntity;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.common.MinecraftForge;

import com.tiedup.remake.rig.armature.Armature;
import com.tiedup.remake.rig.asset.AssetAccessor;
import com.tiedup.remake.rig.event.PrepareModelEvent;
import com.tiedup.remake.rig.math.MathUtils;
import com.tiedup.remake.rig.math.OpenMatrix4f;
import com.tiedup.remake.rig.mesh.SkinnedMesh;
import com.tiedup.remake.rig.mixin.client.MixinLivingEntityRenderer;
import com.tiedup.remake.rig.patch.LivingEntityPatch;

/**
 * Orchestrateur principal du rendu RIG pour les entités vivantes — remplace
 * {@link LivingEntityRenderer} vanilla pour les entités patched.
 *
 * <p>Flow de {@link #render} :</p>
 * <ol>
 *   <li>super.render : nameplate</li>
 *   <li>mulPoseStack : applique modelMatrix (rotation body + scale + upsideDown)</li>
 *   <li>prepareVanillaModel : setup les champs vanilla du {@link EntityModel} (riding/young,
 *       walkAnimation, head yRot) — utile pour que les layers vanilla non patchés voient
 *       le même état que s'ils étaient rendus par vanilla</li>
 *   <li>setArmaturePose : bake la pose courante dans {@code armature.getPoseMatrices()}</li>
 *   <li>prepareModel : mesh.initialize() (détecte hidden parts)</li>
 *   <li>mesh.draw : dispatch vers shader compute ou CPU skinning via {@code drawPosed}</li>
 *   <li>renderLayer : itère les layers vanilla mappés vers des {@link PatchedLayer} (ex.
 *       armor, item-in-hand, cape) et les customLayers enregistrés manuellement</li>
 *   <li>Debug hitboxes si F3+B actif</li>
 * </ol>
 *
 * <p><b>Stripped vs EF source</b> (volontaire, Phase 2.5) :</p>
 * <ul>
 *   <li>Chargement JSON datapack-driven des {@code animated_layers/{entity_type}.json} —
 *       TiedUp n'a pas de customLayers data-driven pour l'instant</li>
 *   <li>{@code LayerRenderer} interface (registre public des addPatchedLayer/addCustomLayer) —
 *       conservé en API mais pas exposé via interface dédiée</li>
 *   <li>{@code entitypatch.getEntityDecorations()} — decoration overlays (glow custom, elite
 *       tint) pas implémentés côté {@link LivingEntityPatch} TiedUp. Couleur et overlay
 *       utilisent les defaults vanilla.</li>
 * </ul>
 *
 * @param <E>  entité vivante
 * @param <T>  patch RIG associé
 * @param <M>  EntityModel vanilla
 * @param <R>  renderer vanilla ({@code LivingEntityRenderer})
 * @param <AM> mesh skinné dérivé
 */
@OnlyIn(Dist.CLIENT)
public abstract class PatchedLivingEntityRenderer<E extends LivingEntity, T extends LivingEntityPatch<E>, M extends EntityModel<E>, R extends LivingEntityRenderer<E, M>, AM extends SkinnedMesh> extends PatchedEntityRenderer<E, T, R, AM> {

	protected final Map<Class<?>, PatchedLayer<E, T, M, ? extends RenderLayer<E, M>>> patchedLayers = Maps.newHashMap();
	protected final List<PatchedLayer<E, T, M, ? extends RenderLayer<E, M>>> customLayers = Lists.newArrayList();

	protected PatchedLivingEntityRenderer(EntityRendererProvider.Context context, EntityType<?> entityType) {
		// RIG Phase 2.5 : chargement JSON datapack des animated_layers strippé
		// (EF L55-81). Si besoin futur d'customLayers data-driven, réimplémenter
		// via FileToIdConverter.json("animated_layers/" + type.getPath()).
	}

	@Override
	public void render(E entity, T entitypatch, R renderer, MultiBufferSource buffer, PoseStack poseStack, int packedLight, float partialTicks) {
		super.render(entity, entitypatch, renderer, buffer, poseStack, packedLight, partialTicks);

		Minecraft mc = Minecraft.getInstance();
		MixinLivingEntityRenderer livingEntityRendererAccessor = (MixinLivingEntityRenderer) renderer;

		boolean isVisible = livingEntityRendererAccessor.invokeIsBodyVisible(entity);
		boolean isVisibleToPlayer = !isVisible && !entity.isInvisibleTo(mc.player);
		boolean isGlowing = mc.shouldEntityAppearGlowing(entity);
		RenderType renderType = livingEntityRendererAccessor.invokeGetRenderType(entity, isVisible, isVisibleToPlayer, isGlowing);
		Armature armature = entitypatch.getArmature();

		poseStack.pushPose();
		this.mulPoseStack(poseStack, armature, entity, entitypatch, partialTicks);
		this.prepareVanillaModel(entity, renderer.getModel(), renderer, partialTicks);
		this.setArmaturePose(entitypatch, armature, partialTicks);

		if (renderType != null) {
			AM mesh = this.getMeshProvider(entitypatch).get();
			this.prepareModel(mesh, entity, entitypatch, renderer);

			PrepareModelEvent prepareModelEvent = new PrepareModelEvent(this, mesh, entitypatch, buffer, poseStack, packedLight, partialTicks);

			if (!MinecraftForge.EVENT_BUS.post(prepareModelEvent)) {
				// RIG Phase 2.5 : EntityDecorations (color/light/overlay modifiers) pas
				// implémentées côté LivingEntityPatch TiedUp. On utilise les defaults
				// vanilla : alpha 0.15 si invisible-mais-visible-pour-player, sinon 1.0.
				Vector4f color = new Vector4f(1.0F, 1.0F, 1.0F, isVisibleToPlayer ? 0.15F : 1.0F);

				mesh.draw(poseStack, buffer, renderType, packedLight, color.x(), color.y(), color.z(), color.w(), this.getOverlayCoord(entity, entitypatch, partialTicks), armature, armature.getPoseMatrices());
			}
		}

		if (!entity.isSpectator()) {
			this.renderLayer(renderer, entitypatch, entity, armature.getPoseMatrices(), buffer, poseStack, packedLight, partialTicks);
		}

		if (renderType != null) {
			if (mc.getEntityRenderDispatcher().shouldRenderHitBoxes() && entitypatch.getClientAnimator() != null) {
				entitypatch.getClientAnimator().renderDebuggingInfoForAllLayers(poseStack, buffer, partialTicks);
			}
		}

		poseStack.popPose();
	}

	protected void prepareVanillaModel(E entity, M model, LivingEntityRenderer<E, M> renderer, float partialTicks) {
		boolean shouldSit = entity.isPassenger() && (entity.getVehicle() != null && entity.getVehicle().shouldRiderSit());
		model.riding = shouldSit;
		model.young = entity.isBaby();
		float f = Mth.rotLerp(partialTicks, entity.yBodyRotO, entity.yBodyRot);
		float f1 = Mth.rotLerp(partialTicks, entity.yHeadRotO, entity.yHeadRot);
		float f2 = f1 - f;

		if (shouldSit && entity.getVehicle() instanceof LivingEntity livingentity) {
			f = Mth.rotLerp(partialTicks, livingentity.yBodyRotO, livingentity.yBodyRot);
			f2 = f1 - f;
			float f3 = Mth.wrapDegrees(f2);

			if (f3 < -85.0F) {
				f3 = -85.0F;
			}

			if (f3 >= 85.0F) {
				f3 = 85.0F;
			}

			f = f1 - f3;
			if (f3 * f3 > 2500.0F) {
				f += f3 * 0.2F;
			}

			f2 = f1 - f;
		}

		float f6 = Mth.lerp(partialTicks, entity.xRotO, entity.getXRot());

		if (LivingEntityRenderer.isEntityUpsideDown(entity)) {
			f6 *= -1.0F;
			f2 *= -1.0F;
		}

		float f7 = ((MixinLivingEntityRenderer) renderer).invokeGetBob(entity, partialTicks);
		float f8 = 0.0F;
		float f5 = 0.0F;

		if (!shouldSit && entity.isAlive()) {
			f8 = entity.walkAnimation.speed(partialTicks);
			f5 = entity.walkAnimation.position() - entity.walkAnimation.speed() * (1.0F - partialTicks);

			if (entity.isBaby()) {
				f5 *= 3.0F;
			}

			if (f8 > 1.0F) {
				f8 = 1.0F;
			}
		}

		model.prepareMobModel(entity, f5, f8, partialTicks);
		model.setupAnim(entity, f5, f8, f7, f2, f6);
	}

	protected void prepareModel(AM mesh, E entity, T entitypatch, R renderer) {
		mesh.initialize();
	}

	protected void renderLayer(LivingEntityRenderer<E, M> renderer, T entitypatch, E entity, OpenMatrix4f[] poses, MultiBufferSource buffer, PoseStack poseStack, int packedLight, float partialTicks) {
		float f = MathUtils.lerpBetween(entity.yBodyRotO, entity.yBodyRot, partialTicks);
		float f1 = MathUtils.lerpBetween(entity.yHeadRotO, entity.yHeadRot, partialTicks);
		float f2 = f1 - f;
		float f7 = entity.getViewXRot(partialTicks);
		float bob = ((MixinLivingEntityRenderer) renderer).invokeGetBob(entity, partialTicks);

		// P0-BUG-002 (review Phase 2.5) : copie défensive de renderer.layers —
		// la liste est mutable et peut être touchée par des mods tiers (cosmetics,
		// wildfire gender, skin layers 3D) qui ajoutent un layer runtime via
		// AddLayers event. Sans copie, CME garantie dès qu'un autre mod mute
		// la liste pendant notre itération.
		for (RenderLayer<E, M> layer : Lists.newArrayList(renderer.layers)) {
			Class<?> layerClass = layer.getClass();

			if (layerClass.isAnonymousClass()) {
				layerClass = layerClass.getSuperclass();
			}

			if (this.patchedLayers.containsKey(layerClass)) {
				// P2-RISK-004 (review Phase 2.5) : cast unchecked scopé aux 2 call
				// sites qui en ont besoin (raw-typed dispatch PatchedLayer pour
				// éviter l'explosion de wildcard genericity sur 4 type params).
				// L'invariant runtime est garanti par addPatchedLayer qui binde
				// Class<?> → PatchedLayer<E,T,M,? extends RenderLayer<E,M>>.
				@SuppressWarnings({ "rawtypes", "unchecked" })
				PatchedLayer patchedLayer = this.patchedLayers.get(layerClass);
				patchedLayer.renderLayer(entity, entitypatch, layer, poseStack, buffer, packedLight, poses, bob, f2, f7, partialTicks);
			}
		}

		for (PatchedLayer<E, T, M, ? extends RenderLayer<E, M>> patchedLayer : this.customLayers) {
			@SuppressWarnings({ "rawtypes", "unchecked" })
			PatchedLayer raw = patchedLayer;
			raw.renderLayer(entity, entitypatch, null, poseStack, buffer, packedLight, poses, bob, f2, f7, partialTicks);
		}
	}

	protected int getOverlayCoord(E entity, T entitypatch, float partialTicks) {
		// RIG Phase 2.5 : EntityDecorations.modifyOverlay strippé — utilise
		// les defaults vanilla (hurt flash + death fade).
		int initU = 0;
		int initV = OverlayTexture.v(entity.hurtTime > 0 || entity.deathTime > 0);
		return OverlayTexture.pack(initU, initV);
	}

	@Override
	public void mulPoseStack(PoseStack poseStack, Armature armature, E entity, T entitypatch, float partialTicks) {
		super.mulPoseStack(poseStack, armature, entity, entitypatch, partialTicks);

		if (entity.isCrouching()) {
			poseStack.translate(0.0D, 0.15D, 0.0D);
		}
	}

	public void addPatchedLayer(Class<?> originalLayerClass, PatchedLayer<E, T, M, ? extends RenderLayer<E, M>> patchedLayer) {
		this.patchedLayers.putIfAbsent(originalLayerClass, patchedLayer);
	}

	public void addCustomLayer(PatchedLayer<E, T, M, ? extends RenderLayer<E, M>> patchedLayer) {
		this.customLayers.add(patchedLayer);
	}

	protected float getDefaultLayerHeightCorrection() {
		return 1.15F;
	}
}
