/*
 * 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.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;

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

import net.minecraft.client.model.EntityModel;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.entity.EntityRenderer;
import net.minecraft.client.renderer.entity.EntityRendererProvider;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.client.event.EntityRenderersEvent;
import net.minecraftforge.client.event.RenderLivingEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.ModLoader;
import net.minecraftforge.fml.common.Mod;

import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.AbstractTiedUpNpc;
import com.tiedup.remake.rig.TiedUpRigConstants;
import com.tiedup.remake.rig.event.PatchedRenderersEvent;
import com.tiedup.remake.rig.patch.LivingEntityPatch;
import com.tiedup.remake.rig.patch.TiedUpCapabilities;
import com.tiedup.remake.rig.tick.RigAnimationTickHandler;

/**
 * Dispatch renderer RIG — Phase 2.6.
 *
 * <p>Singleton stateless qui câble deux événements Forge client-only sur deux
 * buses différents :</p>
 *
 * <ol>
 *   <li><b>MOD bus — {@link EntityRenderersEvent.AddLayers}</b> : construit la
 *       map {@code EntityType → PatchedEntityRenderer} (Phase 2 : PLAYER seul)
 *       et poste {@link PatchedRenderersEvent.Add} sur le mod event bus pour
 *       que des tiers (datapacks, modules TiedUp futurs) puissent enregistrer
 *       leurs propres patched renderers.</li>
 *   <li><b>FORGE bus — {@link RenderLivingEvent.Pre}</b> (priority NORMAL) :
 *       intercepte le render vanilla des entités whitelist, vérifie la
 *       présence d'un patch avec {@code overrideRender() == true}, dispatche
 *       vers le {@link PatchedEntityRenderer} approprié, puis annule le render
 *       vanilla.</li>
 * </ol>
 *
 * <p>Les deux subscribers sont des inner classes (un par bus) — Forge n'autorise
 * pas un même class à être abonnée aux deux buses via annotation.</p>
 *
 * <p><b>Filtre strict d'entités</b> : seuls {@link Player} et
 * {@link AbstractTiedUpNpc} passent le filtre instanceof. Objectif concret :
 * <em>ne pas</em> intercepter les villagers MCA — cf.
 * {@code docs/plans/rig/V3_REWORK_BACKLOG.md} V3-REW-10. Ajouter un EntityType
 * dans {@link #entityRendererProvider} ne suffit pas à déclencher le dispatch,
 * l'entity instance doit aussi matcher le filtre ; ça protège si un tiers
 * registre une clé PLAYER pour un VillagerEntity custom (très improbable mais
 * défensif).</p>
 *
 * <p><b>Priority NORMAL (default)</b> : aligné sur Epic Fight upstream. Aucun cas
 * concret actuel ne justifie de firer avant d'autres mods ; si on constate un
 * conflit d'interception en Phase 3+ (ex : mod cosmetic qui cancel notre
 * render), on réévaluera la priorité à ce moment-là. Si un autre subscriber
 * (NORMAL ou higher) a déjà canceled l'event, on respecte et on skip.</p>
 *
 * <p><b>Robustesse</b> : toute exception pendant le dispatch est logguée une
 * seule fois par UUID d'entity (évite le spam log si le rendu crash pour une
 * entity donnée à chaque frame). L'event n'est pas canceled en cas d'exception
 * → MC fallback sur le rendu vanilla.</p>
 *
 * <p><b>Scope vs EF</b> : fork simplifié de {@code RenderEngine} EF (850 LOC).
 * Supprimé : GUI battlemode, EntityUI indicators, item renderers (stubbed),
 * FirstPersonRenderer, BossHealthOverlay. Gardé : le dispatch core
 * RenderLivingEvent + le pattern addLayers.</p>
 */
@OnlyIn(Dist.CLIENT)
@SuppressWarnings("rawtypes")
public final class TiedUpRenderEngine {

	/**
	 * Provider map — construite en {@link ModBusEvents#onAddLayers} (MOD bus)
	 * puis consultée par {@link ForgeBusEvents#onRenderLiving} (FORGE bus).
	 * Vide tant qu'{@code AddLayers} n'a pas firé → {@code onRenderLiving}
	 * devient no-op.
	 *
	 * <p>Function prend {@link EntityType} en entrée (la context-free factory,
	 * pattern EF) — la {@link EntityRendererProvider.Context} est déjà capturée
	 * en closure dans le lambda builder.</p>
	 */
	private static final Map<EntityType<?>, Function<EntityType<?>, PatchedEntityRenderer>> entityRendererProvider =
		Maps.newHashMap();

	/**
	 * Cache résolu des renderers (construit lazy au 1er hit, via {@link #getRendererFor}).
	 * Invalidé à chaque {@code AddLayers} via {@link ModBusEvents#onAddLayers}.
	 */
	private static final Map<EntityType<?>, PatchedEntityRenderer> rendererCache = Maps.newHashMap();

	/**
	 * UUIDs pour lesquels on a déjà loggé une erreur render (anti-spam).
	 *
	 * <p><b>Thread-safety</b> : le dispatch render se fait côté client render thread,
	 * mais le reset via {@link #resetForTest()} peut provenir d'un test runner thread
	 * et {@link ModBusEvents#onAddLayers} d'un loader thread lors du reload des
	 * resources. On passe par un set concurrent pour éviter toute lost-add /
	 * ConcurrentModificationException pendant un reload en plein milieu d'un tick.</p>
	 */
	private static final Set<UUID> loggedRenderErrors = ConcurrentHashMap.newKeySet();

	private TiedUpRenderEngine() {}

	/**
	 * Résout le renderer pour un {@link EntityType} — lazy-loaded au premier
	 * hit, cached ensuite. Null si aucun renderer enregistré (pass-through vers
	 * vanilla).
	 */
	private static PatchedEntityRenderer getRendererFor(EntityType<?> entityType) {
		return rendererCache.computeIfAbsent(entityType, key -> {
			Function<EntityType<?>, PatchedEntityRenderer> provider = entityRendererProvider.get(key);
			return provider != null ? provider.apply(key) : null;
		});
	}

	/**
	 * Vérifie si un {@link Entity} peut être considéré pour un dispatch RIG
	 * (avant même le lookup patch / renderer). Utilisé par tests unitaires
	 * pour verrouiller le filtre.
	 */
	public static boolean isEligibleForDispatch(Entity entity) {
		return entity instanceof Player || entity instanceof AbstractTiedUpNpc;
	}

	/**
	 * Exposé pour tests unitaires — vérifie la présence d'un provider
	 * enregistré. Ne déclenche PAS la construction du renderer (qui requiert
	 * {@link EntityRendererProvider.Context}).
	 */
	public static boolean hasProviderFor(EntityType<?> entityType) {
		return entityRendererProvider.containsKey(entityType);
	}

	/**
	 * Exposé pour tests unitaires — reset de l'état pour éviter les fuites
	 * entre tests. À ne PAS appeler depuis le code runtime.
	 */
	public static void resetForTest() {
		entityRendererProvider.clear();
		rendererCache.clear();
		loggedRenderErrors.clear();
	}

	// ------------------------------------------------------------------
	// Subscribers — split entre MOD bus (AddLayers) et FORGE bus (RenderLiving)
	// ------------------------------------------------------------------

	/**
	 * Subscriber MOD bus — {@link EntityRenderersEvent.AddLayers} est posté
	 * sur ce bus (événement lié au loading des renderers). Auto-registered via
	 * l'annotation ; pas de wiring explicite requis dans {@code TiedUpMod}.
	 */
	@OnlyIn(Dist.CLIENT)
	@Mod.EventBusSubscriber(modid = TiedUpMod.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT)
	public static final class ModBusEvents {

		private ModBusEvents() {}

		/**
		 * Phase 2 : on enregistre uniquement {@link EntityType#PLAYER} →
		 * {@link TiedUpPlayerRenderer}. Phase 5 ajoutera les NPCs TiedUp (Damsel,
		 * Kidnapper, Maid, Master).
		 */
		@SubscribeEvent
		public static void onAddLayers(EntityRenderersEvent.AddLayers event) {
			final EntityRendererProvider.Context context = event.getContext();

			entityRendererProvider.clear();
			rendererCache.clear();
			// Reset anti-spam log set sur reload resources (F3+T) — sinon un
			// UUID ayant crashé pré-reload resterait marqué et masquerait un
			// nouveau crash post-reload. Borne aussi la taille en session longue.
			loggedRenderErrors.clear();

			// Même raisonnement pour l'anti-spam du tick handler anim : un
			// UUID ayant crashé pré-reload doit pouvoir à nouveau logger
			// post-reload. F3+T = point naturel pour resetter tout l'état
			// log accumulé côté RIG client.
			RigAnimationTickHandler.resetLoggedErrors();

			// Phase 2 : player seul. Le lambda capture la context finale.
			entityRendererProvider.put(EntityType.PLAYER, entityType -> new TiedUpPlayerRenderer(context, entityType));

			// Extension hook : tierces parties peuvent ajouter leurs patched renderers.
			ModLoader.get().postEvent(new PatchedRenderersEvent.Add(entityRendererProvider, context));

			TiedUpRigConstants.LOGGER.info(
				"TiedUpRenderEngine: registered {} patched entity renderer(s)",
				entityRendererProvider.size()
			);
		}
	}

	/**
	 * Subscriber FORGE bus — {@link RenderLivingEvent.Pre} est posté sur ce
	 * bus (événement de rendu par frame). Auto-registered via l'annotation.
	 */
	@OnlyIn(Dist.CLIENT)
	@Mod.EventBusSubscriber(modid = TiedUpMod.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE, value = Dist.CLIENT)
	public static final class ForgeBusEvents {

		private ForgeBusEvents() {}

		/**
		 * Dispatch principal — priority NORMAL (défaut, aligné sur Epic Fight).
		 * Si un subscriber plus prioritaire a déjà canceled l'event, on respecte.
		 *
		 * <p>Generic wildcard avec {@code extends LivingEntity} — Forge fire un
		 * seul event instance générique, on ne peut pas filtrer par type param.
		 * Le filtre se fait par {@code instanceof} sur l'entity, et par
		 * {@link #getRendererFor}.</p>
		 */
		@SubscribeEvent
		public static void onRenderLiving(RenderLivingEvent.Pre<? extends LivingEntity, ? extends EntityModel<? extends LivingEntity>> event) {
			if (event.isCanceled()) {
				return;
			}

			LivingEntity entity = event.getEntity();

			// NPE guard — une entity peut se détacher de son Level (despawn race,
			// dimension teleport) pendant qu'un render frame est encore en flight.
			// Plusieurs call sites downstream (patch.getAnimator().tick(level), etc.)
			// supposent un level non-null. Pas de log : fail silencieusement, MC
			// re-render à la frame suivante. Aligne sur EF qui évite aussi de toucher
			// aux entities sans level via RenderEngine guard.
			if (entity.level() == null) {
				return;
			}

			// STRICT FILTER — Player (Phase 2) + NPCs TiedUp (Phase 5). Protège
			// notamment contre une interception accidentelle des MCA villagers
			// (cf. V3-REW-10 dans V3_REWORK_BACKLOG.md). Bonus : si un tiers mod
			// registre un patched renderer pour un EntityType autre que Player /
			// AbstractTiedUpNpc, le filtre l'arrête (force-opt-in volontaire).
			if (!isEligibleForDispatch(entity)) {
				return;
			}

			PatchedEntityRenderer patchedRenderer = getRendererFor(entity.getType());
			if (patchedRenderer == null) {
				return;
			}

			LivingEntityPatch<?> patch = TiedUpCapabilities.getEntityPatch(entity, LivingEntityPatch.class);
			if (patch == null || !patch.overrideRender()) {
				return;
			}

			try {
				dispatchRender(event, entity, patch, patchedRenderer);
				event.setCanceled(true);
			} catch (Throwable t) {
				// Fallback vanilla — on ne cancel pas pour que MC render normalement.
				// Log une seule fois par UUID (anti-spam : erreur potentiellement à
				// chaque frame sinon).
				if (loggedRenderErrors.add(entity.getUUID())) {
					TiedUpRigConstants.LOGGER.warn(
						"TiedUpRenderEngine: patched render failed for entity {} (type={}), falling back to vanilla render",
						entity.getUUID(), entity.getType(), t
					);
				}
			}
		}

		/**
		 * Dispatch typé — cast raw obligatoire car {@link PatchedEntityRenderer#render}
		 * est fortement typé avec des variances qu'on ne peut pas reconstruire
		 * depuis l'event Forge générique.
		 *
		 * <p>Les casts sont sûrs par construction : l'invariant de
		 * {@link #entityRendererProvider} est que le renderer construit est
		 * compatible avec le {@code EntityType} clé — donc pour
		 * {@code EntityType.PLAYER} → {@link TiedUpPlayerRenderer} qui accepte
		 * {@code AbstractClientPlayer} → {@link LivingEntity} passed. Un
		 * mismatch (improbable : seul registeur Phase 2 = player) crasherait
		 * au premier accès field dans le render body ; désiré fail-fast, le
		 * try/catch supérieur récupère et fallback vanilla.</p>
		 */
		@SuppressWarnings({ "unchecked" })
		private static void dispatchRender(
			RenderLivingEvent.Pre<? extends LivingEntity, ? extends EntityModel<? extends LivingEntity>> event,
			LivingEntity entity,
			LivingEntityPatch<?> patch,
			PatchedEntityRenderer patchedRenderer
		) {
			EntityRenderer<?> vanillaRenderer = event.getRenderer();
			MultiBufferSource buffer = event.getMultiBufferSource();
			PoseStack poseStack = event.getPoseStack();
			int packedLight = event.getPackedLight();
			float partialTick = event.getPartialTick();

			patchedRenderer.render(entity, patch, vanillaRenderer, buffer, poseStack, packedLight, partialTick);
		}
	}
}
