package com.tiedup.remake.client.animation.context;

import com.tiedup.remake.client.gltf.GltfCache;
import com.tiedup.remake.client.gltf.GltfData;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.jetbrains.annotations.Nullable;

/**
 * Resolves which named animation to play from a GLB file based on the current
 * {@link AnimationContext}. Implements three features:
 *
 * <ol>
 *   <li><b>Context-based resolution with fallback chain</b> — tries progressively
 *       less specific animation names until one is found:
 *       <pre>SitStruggle -> Struggle -> SitIdle -> Sit -> Idle -> null</pre></li>
 *   <li><b>Animation variants</b> — if {@code Struggle.1}, {@code Struggle.2},
 *       {@code Struggle.3} exist in the GLB, one is picked at random each time</li>
 *   <li><b>Shared animation templates</b> — animations can come from a separate GLB
 *       file (passed as {@code animationSource} to {@link #resolveAnimationData})</li>
 * </ol>
 *
 * <p>This class is stateless and thread-safe. All methods are static.</p>
 */
@OnlyIn(Dist.CLIENT)
public final class GlbAnimationResolver {

    private GlbAnimationResolver() {}

    /**
     * Resolve the animation data source.
     * If {@code animationSource} is non-null, load that GLB for animations
     * (shared template). Otherwise use the item's own model GLB.
     *
     * @param itemModelLoc    the item's GLB model resource location
     * @param animationSource optional separate GLB containing shared animations
     * @return parsed GLB data, or null if loading failed
     */
    @Nullable
    public static GltfData resolveAnimationData(
        ResourceLocation itemModelLoc,
        @Nullable ResourceLocation animationSource
    ) {
        ResourceLocation source =
            animationSource != null ? animationSource : itemModelLoc;
        return GltfCache.get(source);
    }

    /**
     * Resolve the best animation name from a GLB for the given context.
     * Supports variant selection ({@code Struggle.1}, {@code Struggle.2} -> random pick)
     * and full-body animations ({@code FullWalk}, {@code FullStruggle}).
     *
     * <p>Fallback chain (Full variants checked first at each step):</p>
     * <pre>
     * FullSitStruggle -> SitStruggle -> FullStruggle -> Struggle
     *   -> FullSitIdle -> SitIdle -> FullSit -> Sit
     *   -> FullIdle -> Idle -> null
     * </pre>
     *
     * @param data    the parsed GLB data containing named animations
     * @param context the current animation context (posture + action)
     * @return the animation name to use, or null to use the default (first) clip
     */
    @Nullable
    public static String resolve(GltfData data, AnimationContext context) {
        String prefix = context.getGlbContextPrefix(); // "Sit", "Kneel", "Sneak", "Walk", ""
        String variant = context.getGlbVariant(); // "Idle" or "Struggle"

        // 1. Exact match: "FullHeadSitIdle" then "FullSitIdle" then "SitIdle" (with variants)
        // FullHead variants opt-in to head animation (see GltfPoseConverter.enableSelectiveParts)
        String exact = prefix + variant;
        if (!exact.isEmpty()) {
            String picked = pickWithVariants(data, "FullHead" + exact);
            if (picked != null) return picked;
            picked = pickWithVariants(data, "Full" + exact);
            if (picked != null) return picked;
            picked = pickWithVariants(data, exact);
            if (picked != null) return picked;
        }

        // 2. For struggles: try "FullHeadStruggle" then "FullStruggle" then "Struggle" (with variants)
        if (context.isStruggling()) {
            String picked = pickWithVariants(data, "FullHeadStruggle");
            if (picked != null) return picked;
            picked = pickWithVariants(data, "FullStruggle");
            if (picked != null) return picked;
            picked = pickWithVariants(data, "Struggle");
            if (picked != null) return picked;
        }

        // 3. Context-only: "FullHead{prefix}" then "Full{prefix}" then "{prefix}" (with variants)
        if (!prefix.isEmpty()) {
            String picked = pickWithVariants(data, "FullHead" + prefix);
            if (picked != null) return picked;
            picked = pickWithVariants(data, "Full" + prefix);
            if (picked != null) return picked;
            picked = pickWithVariants(data, prefix);
            if (picked != null) return picked;
        }

        // 4. Variant-only: "FullHeadIdle" then "FullIdle" then "Idle" (with variants)
        {
            String picked = pickWithVariants(data, "FullHead" + variant);
            if (picked != null) return picked;
            picked = pickWithVariants(data, "Full" + variant);
            if (picked != null) return picked;
            picked = pickWithVariants(data, variant);
            if (picked != null) return picked;
        }

        // 5. Default: return null = use first animation clip in GLB
        return null;
    }

    /**
     * Look for an animation by base name, including numbered variants.
     * <ul>
     *   <li>If "Struggle" exists alone, return "Struggle"</li>
     *   <li>If "Struggle.1" and "Struggle.2" exist, pick one randomly</li>
     *   <li>If both "Struggle" and "Struggle.1" exist, include all in the random pool</li>
     * </ul>
     *
     * <p>Variant numbering starts at 1 and tolerates a missing {@code .1}
     * (continues to check {@code .2}). Gaps after index 1 stop the scan.
     * For example, {@code Struggle.1, Struggle.3} would only find
     * {@code Struggle.1} because the gap at index 2 stops iteration.
     * However, if only {@code Struggle.2} exists (no {@code .1}), it will
     * still be found because the scan skips the first gap.</p>
     *
     * @param data     the parsed GLB data
     * @param baseName the base animation name (e.g., "Struggle", "SitIdle")
     * @return the selected animation name, or null if no match found
     */
    @Nullable
    private static String pickWithVariants(GltfData data, String baseName) {
        Map<String, GltfData.AnimationClip> anims = data.namedAnimations();
        List<String> candidates = new ArrayList<>();

        if (anims.containsKey(baseName)) {
            candidates.add(baseName);
        }

        // Check numbered variants: baseName.1, baseName.2, ...
        for (int i = 1; i <= 99; i++) {
            String variantName = baseName + "." + i;
            if (anims.containsKey(variantName)) {
                candidates.add(variantName);
            } else if (i > 1) {
                break; // Stop at first gap after .1
            }
        }

        if (candidates.isEmpty()) return null;
        if (candidates.size() == 1) return candidates.get(0);
        return candidates.get(
            ThreadLocalRandom.current().nextInt(candidates.size())
        );
    }
}
