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

package com.tiedup.remake.rig;

import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;

import com.tiedup.remake.rig.anim.AnimationManager;
import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor;
import com.tiedup.remake.rig.anim.client.Layer;
import com.tiedup.remake.rig.anim.client.property.ClientAnimationProperties;
import com.tiedup.remake.rig.anim.types.DirectStaticAnimation;
import com.tiedup.remake.rig.anim.types.StaticAnimation;

/**
 * Phase 2.7 — registry central des {@link StaticAnimation} TiedUp. Expose les
 * accessors statiques (ex. {@link #CONTEXT_STAND_IDLE}) utilisés par les
 * patches + tick handler pour jouer les animations idle / walk / etc.
 *
 * <h2>Placeholder assets</h2>
 * <p>Les JSON associés sont des <b>placeholders procéduraux</b> (2 keyframes
 * identity) à remplacer par des assets Blender-authored. Voir
 * {@code docs/plans/rig/ASSETS_NEEDED.md} section 2 pour la spec de l'anim
 * idle définitive (swing respiration subtle 3 keyframes, 2s boucle).</p>
 *
 * <h2>Lifecycle</h2>
 * <ul>
 *   <li>{@link #initStaticAnimations()} appelé au {@code FMLCommonSetupEvent}
 *       (via {@code event.enqueueWork(...)}). Crée les instances
 *       {@link DirectStaticAnimation} — pas de chargement JSON ici, juste les
 *       métadonnées (registry name, armature, repeat flag).</li>
 *   <li>Le chargement effectif du JSON ({@code JsonAssetLoader}) est lazy :
 *       à la première lecture de {@link StaticAnimation#getAnimationClip()},
 *       donc typiquement à la première frame où l'animation est jouée.</li>
 *   <li>Si l'asset JSON est absent / corrompu, {@code StaticAnimation.loadAnimation}
 *       relance une {@code AssetLoadingException}. Le tick handler
 *       ({@link com.tiedup.remake.rig.tick.RigAnimationTickHandler}) attrape ces
 *       throwables pour éviter un crash complet.</li>
 * </ul>
 *
 * <h2>Dist</h2>
 * <p>Les animations tournent côté client (l'{@code Animator} est créé via
 * {@code ClientAnimator::getAnimator} côté physical client), donc le registry
 * est indépendant du side pour le bootstrap mais toute la lecture JSON passe
 * par {@code Minecraft.getInstance().getResourceManager()} côté client. Les
 * champs sont accessibles côté serveur (validation / logs), seul
 * {@link StaticAnimation#loadAnimation()} est client-heavy (et protégé par
 * la dispatch server/client du {@code AnimationManager.getAnimationResourceManager()}).</p>
 */
public final class TiedUpAnimationRegistry {

    private TiedUpAnimationRegistry() {}

    /** Registry name de l'anim idle par défaut (résolue en
     *  {@code assets/tiedup/animmodels/animations/context_stand_idle.json}). */
    public static final ResourceLocation CONTEXT_STAND_IDLE_ID =
        ResourceLocation.fromNamespaceAndPath(TiedUpRigConstants.MODID, "context_stand_idle");

    /**
     * Anim idle par défaut — joue quand aucune motion active. Placeholder 2
     * keyframes identity (joueur sans mouvement visible) jusqu'à ce qu'un
     * asset authored Blender arrive.
     *
     * <p><b>Attention init order</b> : ce field est {@code null} tant que
     * {@link #initStaticAnimations()} n'a pas tourné. Les sites qui
     * référencent ce field doivent être gardés par un null-check, ou être
     * appelés post-setup (tick handler, patch init, etc.).</p>
     *
     * <p>Utilise {@link DirectStaticAnimation} (vs un hand-written
     * {@code StaticAnimation}) pour hériter du pattern accessor=self +
     * registryName() utilisés dans {@link TiedUpRigRegistry#EMPTY_ANIMATION}.</p>
     */
    public static DirectStaticAnimation CONTEXT_STAND_IDLE;

    /**
     * Construit les {@link StaticAnimation} TiedUp. À appeler exactement une
     * fois par game, en {@code FMLCommonSetupEvent.enqueueWork(...)}.
     *
     * <p>Pas de chargement JSON ici — juste l'instanciation des accessors.
     * La première lecture de {@code getAnimationClip()} déclenchera le load
     * via {@link com.tiedup.remake.rig.asset.JsonAssetLoader}.</p>
     *
     * <p>Idempotent (re-appel sans effet visible) mais pas thread-safe. Ne
     * devrait jamais être appelé hors du mod bus.</p>
     */
    public static void initStaticAnimations() {
        if (CONTEXT_STAND_IDLE != null) {
            // Déjà init (hot-reload setup, test double-init). Log debug seulement.
            TiedUpRigConstants.LOGGER.debug(
                "TiedUpAnimationRegistry.initStaticAnimations: déjà initialisé, skip."
            );
            return;
        }

        try {
            // transitionTime=GENERAL (0.15s = 3 ticks) + isRepeat=true + registryName +
            // armature=BIPED. L'ordre match le ctor DirectStaticAnimation(float, boolean, ResourceLocation, AssetAccessor).
            CONTEXT_STAND_IDLE = new DirectStaticAnimation(
                TiedUpRigConstants.GENERAL_ANIMATION_TRANSITION_TIME,
                /* isRepeat */ true,
                CONTEXT_STAND_IDLE_ID,
                TiedUpArmatures.BIPED
            );

            // Layer BASE + priority LOWEST — idle default, écrasable par toute
            // autre anim. Sans ces props la default du StaticAnimation (LOWEST /
            // BASE_LAYER) s'applique déjà — on les set explicitement pour la doc.
            setLowestBaseLayer(CONTEXT_STAND_IDLE);

            TiedUpRigConstants.LOGGER.info(
                "TiedUpAnimationRegistry: CONTEXT_STAND_IDLE registered ({})",
                CONTEXT_STAND_IDLE_ID
            );
        } catch (Throwable t) {
            // Fallback : log + laisse CONTEXT_STAND_IDLE null. Le tick handler
            // verra null et skippera silencieusement. Évite de tout casser si
            // un asset placeholder est malformé en dev.
            TiedUpRigConstants.LOGGER.error(
                "TiedUpAnimationRegistry: init échoué pour CONTEXT_STAND_IDLE — "
                + "animation idle désactivée. Voir docs/plans/rig/ASSETS_NEEDED.md §2.",
                t
            );
        }
    }

    /**
     * Helper — set LayerType=BASE_LAYER + Priority=LOWEST sur une anim.
     * Évite de forcer les callers à importer {@link ClientAnimationProperties}
     * + {@link Layer}.
     *
     * <p>{@code @OnlyIn(CLIENT)} indirect : les properties
     * {@code LAYER_TYPE}/{@code PRIORITY} sont client-only mais leur écriture
     * via {@code addProperty} ne déclenche pas de class-load de
     * {@code net.minecraft.client.*}. Le tag d'{@code @OnlyIn} serait
     * incorrect ici (le method serait appelé depuis commonSetup). La safety
     * réelle est assurée par le fait que les properties sont juste stockées
     * dans la map et lues plus tard sur client uniquement (via
     * {@code getLayerType()}/{@code getPriority()} tagués
     * {@code @OnlyIn(CLIENT)}).</p>
     */
    private static void setLowestBaseLayer(StaticAnimation anim) {
        anim.addProperty(ClientAnimationProperties.LAYER_TYPE, Layer.LayerType.BASE_LAYER);
        anim.addProperty(ClientAnimationProperties.PRIORITY, Layer.Priority.LOWEST);
    }

    /**
     * Helper — vrai ssi la static anim de référence a fini l'init (tick
     * handler l'utilise en early-return quand Phase 2.7 assets sont absents
     * en dev test).
     */
    @OnlyIn(Dist.CLIENT)
    public static boolean isReady() {
        return CONTEXT_STAND_IDLE != null;
    }

    /**
     * Set (thread-safe) des IDs pour lesquels un WARN de fallback a déjà été
     * émis. Évite le spam log si un consumer appelle
     * {@link #resolveWithFallback(ResourceLocation)} tick après tick avec un ID
     * invalide (ex. un item bondage data-driven qui référence un anim ID cassé,
     * appelé dans la boucle de rendu). Un seul WARN par ID unique sur toute la
     * durée de vie du process (jusqu'à {@link #resetWarnedMissing()}).
     *
     * <p>Pattern inspiré de
     * {@code RigAnimationTickHandler.LOGGED_ERRORS} — {@code ConcurrentHashMap.newKeySet()}
     * pour être safe en cas d'appels concurrents (client tick thread + network
     * handler thread pour {@code PacketPlayRigAnim.handleOnClient}).</p>
     */
    private static final Set<ResourceLocation> WARNED_MISSING_ANIMS = ConcurrentHashMap.newKeySet();

    /**
     * Résout une animation par {@link ResourceLocation} avec fallback safe
     * si le registry ne la connaît pas.
     *
     * <p>Utilisé par le pipeline d'équipement
     * ({@code ClientRigEquipmentHandler.rebuildBondageAnimations}, P3-05) et
     * le packet cinematic ({@code PacketPlayRigAnim.handleOnClient}, P3-12).
     * Un miss dans le registry peut survenir dans plusieurs scénarios :</p>
     * <ul>
     *   <li>Typo modder dans un JSON data-driven bondage item</li>
     *   <li>Datapack pas encore rechargé ({@code /reload} pending)</li>
     *   <li>Animation supprimée entre deux versions du mod</li>
     *   <li>Race entre packet réception et
     *       {@code AnimationManager.apply()} en début de session</li>
     * </ul>
     *
     * <p>Dans tous ces cas, on retourne {@link TiedUpRigRegistry#EMPTY_ANIMATION}
     * — un singleton qui ne joue rien visuellement (pas de keyframe, pose
     * identity). Mieux qu'un NPE pour la robustesse du pipeline : l'anim
     * équipement continue de tourner avec les anim connues, et l'anim inconnue
     * est juste silencieusement un no-op.</p>
     *
     * <p><b>Dedup WARN</b> : un miss donné ne log qu'une fois par session
     * ({@link #WARNED_MISSING_ANIMS}). Ça évite le spam dans la console quand
     * l'ID invalide est consommé tick après tick (ex. équipement resté en place).
     * Le set peut être reset via {@link #resetWarnedMissing()} (hot-reload
     * F3+T, runtime datapack reload).</p>
     *
     * <p><b>Pourquoi réutiliser {@link TiedUpRigRegistry#EMPTY_ANIMATION}</b> vs
     * créer un {@code empty_fallback} séparé : plusieurs sites du runtime
     * ({@code Layer#off}, {@code AnimationPlayer#isEmpty}, {@code LayerOffAnimation#getNextAnimation})
     * testent l'identité via {@code == EMPTY_ANIMATION}. Retourner une autre
     * instance d'empty provoquerait des false-negatives sur ces checks — le
     * runtime penserait qu'une anim réelle joue alors qu'en fait c'est un
     * empty différent. Le singleton canonique évite ce piège.</p>
     *
     * @param id l'ID registry à résoudre (ex. {@code tiedup:idle_context_bound})
     * @return l'{@link AnimationAccessor} enregistré, ou
     *         {@link TiedUpRigRegistry#EMPTY_ANIMATION} si l'ID est inconnu.
     *         Jamais null.
     */
    public static AnimationAccessor<? extends StaticAnimation> resolveWithFallback(
        ResourceLocation id
    ) {
        if (id == null) {
            // null ID : log + fallback. Pas de dedup (cas pathologique — le
            // caller a un bug, pas un miss de datapack).
            TiedUpRigConstants.LOGGER.warn(
                "[TiedUpAnimationRegistry] resolveWithFallback appelé avec id=null, "
                + "using EMPTY_ANIMATION fallback."
            );
            return TiedUpRigRegistry.EMPTY_ANIMATION;
        }

        AnimationAccessor<? extends StaticAnimation> anim = AnimationManager.byKey(id);
        if (anim != null) {
            return anim;
        }

        // Miss — fallback + dedup warn. Set.add retourne true si l'ID n'était
        // pas déjà dans le set → premier miss, on log. Sinon, silent no-op.
        if (WARNED_MISSING_ANIMS.add(id)) {
            TiedUpRigConstants.LOGGER.warn(
                "[TiedUpAnimationRegistry] Animation not found: '{}', using EMPTY_ANIMATION fallback. "
                + "Check datapack JSON or run /reload.",
                id
            );
        }
        return TiedUpRigRegistry.EMPTY_ANIMATION;
    }

    /**
     * Reset le set dedup des WARN missing. Utilisé dans deux contextes :
     * <ul>
     *   <li>Tests unitaires — pour réinitialiser l'état statique entre test
     *       cases (sinon un test qui warn sur un ID pollue les suivants).</li>
     *   <li>Runtime reload (F3+T / datapack reload) — après un reload, des
     *       anims précédemment missing peuvent être dispo maintenant ; on
     *       veut pouvoir re-warn si elles retombent en miss après un autre
     *       reload.</li>
     * </ul>
     *
     * <p>Thread-safe via {@link ConcurrentHashMap#newKeySet()} — pas de
     * synchronisation externe nécessaire.</p>
     */
    public static void resetWarnedMissing() {
        WARNED_MISSING_ANIMS.clear();
    }
}
