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

import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageItem;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
import java.util.*;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.jetbrains.annotations.Nullable;

/**
 * Maps V2 body regions to PlayerAnimator part names.
 * Bridge between gameplay regions and animation bones.
 *
 * <p>PlayerAnimator uses 6 named parts: head, body, rightArm, leftArm, rightLeg, leftLeg.
 * This mapper translates the 14 {@link BodyRegionV2} gameplay regions into those bone names,
 * enabling the animation system to know which bones are "owned" by equipped bondage items.</p>
 *
 * <p>Regions without a direct bone mapping (NECK, FINGERS, TAIL, WINGS) return empty sets.
 * These regions still affect gameplay (blocking, escape difficulty) but don't directly
 * constrain animation bones.</p>
 */
@OnlyIn(Dist.CLIENT)
public final class RegionBoneMapper {

    /** All PlayerAnimator part names for the player model. */
    public static final Set<String> ALL_PARTS = Set.of(
        "head",
        "body",
        "rightArm",
        "leftArm",
        "rightLeg",
        "leftLeg"
    );

    /**
     * Describes bone ownership for a specific item in the context of all equipped items.
     *
     * <ul>
     *   <li>{@code thisParts} — parts owned exclusively by the winning item</li>
     *   <li>{@code otherParts} — parts owned by other equipped items</li>
     *   <li>{@link #freeParts()} — parts not owned by any item (available for animation)</li>
     *   <li>{@link #enabledParts()} — parts the winning item may animate (owned + free)</li>
     * </ul>
     *
     * <p>When both the winning item and another item claim the same bone,
     * the other item takes precedence (the bone goes to {@code otherParts}).</p>
     */
    public record BoneOwnership(Set<String> thisParts, Set<String> otherParts) {
        /**
         * Parts not owned by any item. These are "free" and can be animated
         * by the winning item IF the GLB contains keyframes for them.
         */
        public Set<String> freeParts() {
            Set<String> free = new HashSet<>(ALL_PARTS);
            free.removeAll(thisParts);
            free.removeAll(otherParts);
            return Collections.unmodifiableSet(free);
        }

        /**
         * Parts the winning item is allowed to animate: its own parts + free parts.
         * Free parts are only actually enabled if the GLB has keyframes for them.
         */
        public Set<String> enabledParts() {
            Set<String> enabled = new HashSet<>(thisParts);
            enabled.addAll(freeParts());
            return Collections.unmodifiableSet(enabled);
        }

        /**
         * Parts that must be disabled on the context layer: parts owned by this item
         * (handled by item layer) + parts owned by other items (handled by their layer).
         * This equals ALL_PARTS minus freeParts.
         */
        public Set<String> disabledOnContext() {
            Set<String> disabled = new HashSet<>(thisParts);
            disabled.addAll(otherParts);
            return Collections.unmodifiableSet(disabled);
        }
    }

    private static final Map<BodyRegionV2, Set<String>> REGION_TO_PARTS;

    static {
        Map<BodyRegionV2, Set<String>> map = new EnumMap<>(BodyRegionV2.class);
        map.put(BodyRegionV2.HEAD, Set.of("head"));
        map.put(BodyRegionV2.EYES, Set.of("head"));
        map.put(BodyRegionV2.EARS, Set.of("head"));
        map.put(BodyRegionV2.MOUTH, Set.of("head"));
        map.put(BodyRegionV2.NECK, Set.of());
        map.put(BodyRegionV2.TORSO, Set.of("body"));
        map.put(BodyRegionV2.ARMS, Set.of("rightArm", "leftArm"));
        map.put(BodyRegionV2.HANDS, Set.of("rightArm", "leftArm"));
        map.put(BodyRegionV2.FINGERS, Set.of());
        map.put(BodyRegionV2.WAIST, Set.of("body"));
        map.put(BodyRegionV2.LEGS, Set.of("rightLeg", "leftLeg"));
        map.put(BodyRegionV2.FEET, Set.of("rightLeg", "leftLeg"));
        map.put(BodyRegionV2.TAIL, Set.of());
        map.put(BodyRegionV2.WINGS, Set.of());
        REGION_TO_PARTS = Collections.unmodifiableMap(map);
    }

    private RegionBoneMapper() {}

    /**
     * Get the PlayerAnimator part names affected by a single body region.
     *
     * @param region the V2 body region
     * @return unmodifiable set of part name strings, never null (may be empty)
     */
    public static Set<String> getPartsForRegion(BodyRegionV2 region) {
        return REGION_TO_PARTS.getOrDefault(region, Set.of());
    }

    /**
     * Compute the union of all PlayerAnimator parts "owned" by equipped bondage items.
     *
     * <p>Iterates over the equipped map (as returned by
     * {@link com.tiedup.remake.v2.bondage.IV2BondageEquipment#getAllEquipped()})
     * and collects every bone affected by each item's occupied regions.</p>
     *
     * @param equipped map from representative region to equipped ItemStack
     * @return unmodifiable set of owned part name strings
     */
    public static Set<String> computeOwnedParts(
        Map<BodyRegionV2, ItemStack> equipped
    ) {
        Set<String> owned = new HashSet<>();
        for (Map.Entry<BodyRegionV2, ItemStack> entry : equipped.entrySet()) {
            ItemStack stack = entry.getValue();
            if (stack.getItem() instanceof IV2BondageItem v2Item) {
                for (BodyRegionV2 region : v2Item.getOccupiedRegions(stack)) {
                    owned.addAll(getPartsForRegion(region));
                }
            }
        }
        return Collections.unmodifiableSet(owned);
    }

    /**
     * Compute per-item bone ownership for a specific "winning" item.
     *
     * <p>Iterates over all equipped items. Parts owned by the winning item
     * go to {@code thisParts}; parts owned by other items go to {@code otherParts}.
     * If both the winning item and another item claim the same bone, the other
     * item takes precedence (conflict resolution: other wins).</p>
     *
     * <p>Uses ItemStack reference equality ({@code ==}) to identify the winning item
     * because the same ItemStack instance is used in the equipped map.</p>
     *
     * @param equipped         map from representative region to equipped ItemStack
     * @param winningItemStack the ItemStack of the highest-priority V2 item with a GLB model
     * @return BoneOwnership describing this item's parts vs other items' parts
     */
    public static BoneOwnership computePerItemParts(
        Map<BodyRegionV2, ItemStack> equipped,
        ItemStack winningItemStack
    ) {
        Set<String> thisParts = new HashSet<>();
        Set<String> otherParts = new HashSet<>();

        // Track which ItemStacks we've already processed to avoid duplicate work
        // (multiple regions can map to the same ItemStack)
        Set<ItemStack> processed = Collections.newSetFromMap(
            new IdentityHashMap<>()
        );

        for (Map.Entry<BodyRegionV2, ItemStack> entry : equipped.entrySet()) {
            ItemStack stack = entry.getValue();
            if (processed.contains(stack)) continue;
            processed.add(stack);

            if (stack.getItem() instanceof IV2BondageItem v2Item) {
                Set<String> itemParts = new HashSet<>();
                for (BodyRegionV2 region : v2Item.getOccupiedRegions(stack)) {
                    itemParts.addAll(getPartsForRegion(region));
                }

                if (stack == winningItemStack) {
                    thisParts.addAll(itemParts);
                } else {
                    otherParts.addAll(itemParts);
                }
            }
        }

        // Conflict resolution: if both this item and another claim the same bone,
        // the other item takes precedence
        thisParts.removeAll(otherParts);

        return new BoneOwnership(
            Collections.unmodifiableSet(thisParts),
            Collections.unmodifiableSet(otherParts)
        );
    }

    /**
     * Result of resolving the highest-priority V2 item with a GLB model.
     * Combines the model location, optional animation source, and the winning ItemStack
     * into a single object so callers don't need two separate iteration passes.
     *
     * @param modelLoc    the GLB model ResourceLocation of the winning item
     * @param animSource  separate GLB for animations (shared template), or null to use modelLoc
     * @param winningItem the actual ItemStack reference (for identity comparison in
     *                    {@link #computePerItemParts})
     */
    public record GlbModelResult(
        ResourceLocation modelLoc,
        @Nullable ResourceLocation animSource,
        ItemStack winningItem
    ) {}

    /**
     * Animation info for a single equipped V2 item.
     * Used by the multi-item animation pipeline to process each item independently.
     *
     * @param modelLoc       GLB model location (for rendering + default animation source)
     * @param animSource     separate animation GLB, or null to use modelLoc
     * @param ownedParts     parts this item exclusively owns (after conflict resolution)
     * @param posePriority   the item's pose priority (for free-bone assignment)
     * @param animationBones per-animation bone whitelist from the data-driven definition.
     *                       Empty map for hardcoded items (no filtering applied).
     */
    public record V2ItemAnimInfo(
        ResourceLocation modelLoc,
        @Nullable ResourceLocation animSource,
        Set<String> ownedParts,
        int posePriority,
        Map<String, Set<String>> animationBones
    ) {}

    /**
     * Find the highest-priority V2 item with a GLB model in the equipped map.
     *
     * <p>Single pass over all equipped items, comparing their
     * {@link IV2BondageItem#getPosePriority()} to select the dominant model.
     * Returns both the model location and the winning ItemStack reference so
     * callers can pass the ItemStack to {@link #computePerItemParts} without
     * a second iteration.</p>
     *
     * @param equipped map of equipped V2 items by body region (may be empty, never null)
     * @return the winning item's model info, or null if no V2 item has a GLB model (V1 fallback)
     */
    @Nullable
    public static GlbModelResult resolveWinningItem(
        Map<BodyRegionV2, ItemStack> equipped
    ) {
        ItemStack bestStack = null;
        ResourceLocation bestModel = null;
        int bestPriority = Integer.MIN_VALUE;
        for (Map.Entry<BodyRegionV2, ItemStack> entry : equipped.entrySet()) {
            ItemStack stack = entry.getValue();
            if (stack.getItem() instanceof IV2BondageItem v2Item) {
                ResourceLocation model = v2Item.getModelLocation(stack);
                if (
                    model != null &&
                    v2Item.getPosePriority(stack) > bestPriority
                ) {
                    bestPriority = v2Item.getPosePriority(stack);
                    bestModel = model;
                    bestStack = stack;
                }
            }
        }
        if (bestStack == null || bestModel == null) return null;

        // Extract animation source from data-driven item definitions.
        // For hardcoded IV2BondageItem implementations, animSource stays null
        // (the model's own animations are used).
        ResourceLocation animSource = null;
        if (bestStack.getItem() instanceof DataDrivenBondageItem) {
            DataDrivenItemDefinition def = DataDrivenItemRegistry.get(
                bestStack
            );
            if (def != null) {
                animSource = def.animationSource();
            }
        }

        return new GlbModelResult(bestModel, animSource, bestStack);
    }

    /**
     * Resolve ALL equipped V2 items with GLB models, with per-item bone ownership.
     *
     * <p>Each item gets ownership of its declared regions' bones. When two items claim
     * the same bone, the higher-priority item wins. The highest-priority item is also
     * designated as the "free bone donor" — it can animate free bones if its GLB has
     * keyframes for them.</p>
     *
     * @param equipped map from representative region to equipped ItemStack
     * @return list of V2ItemAnimInfo, sorted by priority descending. Empty if no V2 items.
     *         The first element (if any) is the free-bone donor.
     */
    public static List<V2ItemAnimInfo> resolveAllV2Items(
        Map<BodyRegionV2, ItemStack> equipped
    ) {
        record ItemEntry(
            ItemStack stack,
            IV2BondageItem v2Item,
            ResourceLocation model,
            @Nullable ResourceLocation animSource,
            Set<String> rawParts,
            int priority,
            Map<String, Set<String>> animationBones
        ) {}

        List<ItemEntry> entries = new ArrayList<>();
        Set<ItemStack> seen = Collections.newSetFromMap(
            new IdentityHashMap<>()
        );

        for (Map.Entry<BodyRegionV2, ItemStack> entry : equipped.entrySet()) {
            ItemStack stack = entry.getValue();
            if (seen.contains(stack)) continue;
            seen.add(stack);

            if (stack.getItem() instanceof IV2BondageItem v2Item) {
                ResourceLocation model = v2Item.getModelLocation(stack);
                if (model == null) continue;

                Set<String> rawParts = new HashSet<>();
                for (BodyRegionV2 region : v2Item.getOccupiedRegions(stack)) {
                    rawParts.addAll(getPartsForRegion(region));
                }
                if (rawParts.isEmpty()) continue;

                ResourceLocation animSource = null;
                Map<String, Set<String>> animBones = Map.of();
                if (stack.getItem() instanceof DataDrivenBondageItem) {
                    DataDrivenItemDefinition def = DataDrivenItemRegistry.get(
                        stack
                    );
                    if (def != null) {
                        animSource = def.animationSource();
                        animBones = def.animationBones();
                    }
                }

                entries.add(
                    new ItemEntry(
                        stack,
                        v2Item,
                        model,
                        animSource,
                        rawParts,
                        v2Item.getPosePriority(stack),
                        animBones
                    )
                );
            }
        }

        if (entries.isEmpty()) return List.of();

        entries.sort((a, b) -> Integer.compare(b.priority(), a.priority()));

        Set<String> claimed = new HashSet<>();
        List<V2ItemAnimInfo> result = new ArrayList<>();

        for (ItemEntry e : entries) {
            Set<String> ownedParts = new HashSet<>(e.rawParts());
            ownedParts.removeAll(claimed);
            if (ownedParts.isEmpty()) continue;
            claimed.addAll(ownedParts);
            result.add(
                new V2ItemAnimInfo(
                    e.model(),
                    e.animSource(),
                    Collections.unmodifiableSet(ownedParts),
                    e.priority(),
                    e.animationBones()
                )
            );
        }

        return Collections.unmodifiableList(result);
    }

    /**
     * Compute the set of all bone parts owned by any item in the resolved list.
     * Used to disable owned parts on the context layer.
     */
    public static Set<String> computeAllOwnedParts(List<V2ItemAnimInfo> items) {
        Set<String> allOwned = new HashSet<>();
        for (V2ItemAnimInfo item : items) {
            allOwned.addAll(item.ownedParts());
        }
        return Collections.unmodifiableSet(allOwned);
    }
}
