package com.tiedup.remake.v2.bondage.client;

import com.mojang.blaze3d.vertex.PoseStack;
import com.tiedup.remake.client.gltf.GltfCache;
import com.tiedup.remake.client.gltf.GltfData;
import com.tiedup.remake.client.gltf.GltfLiveBoneReader;
import com.tiedup.remake.client.gltf.GltfMeshRenderer;
import com.tiedup.remake.client.gltf.GltfSkinningEngine;
import com.tiedup.remake.entities.AbstractTiedUpNpc;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
import com.tiedup.remake.v2.bondage.IV2BondageItem;
import com.tiedup.remake.v2.bondage.IV2EquipmentHolder;
import com.tiedup.remake.v2.bondage.capability.V2BondageEquipmentProvider;
import com.tiedup.remake.v2.furniture.ISeatProvider;
import com.tiedup.remake.v2.furniture.SeatDefinition;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import net.minecraft.client.Minecraft;
import net.minecraft.client.model.HumanoidModel;
import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.entity.LivingEntityRenderer;
import net.minecraft.client.renderer.entity.RenderLayerParent;
import net.minecraft.client.renderer.entity.layers.RenderLayer;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.joml.Matrix4f;

/**
 * Production render layer for V2 bondage equipment.
 * Renders ALL equipped V2 bondage items as GLB meshes on any entity
 * with a {@link HumanoidModel}.
 *
 * <p>Works for both players and NPCs. For players, reads the V2 bondage
 * equipment capability. For NPCs implementing {@link IV2EquipmentHolder}
 * (e.g., EntityDamsel), reads directly from the holder.
 *
 * <p>Each equipped item with a non-null model location gets its own
 * pushPose/popPose pair. Joint matrices are computed per-GLB-model
 * because different GLB models have different skeletons.
 *
 * <p>This layer is always active and renders on ALL entities.
 */
@OnlyIn(Dist.CLIENT)
public class V2BondageRenderLayer<
    T extends LivingEntity,
    M extends HumanoidModel<T>
> extends RenderLayer<T, M> {

    private static final Logger LOGGER = LogManager.getLogger("GltfPipeline");

    /**
     * Y alignment offset to place glTF meshes in the MC PoseStack.
     * After LivingEntityRenderer's scale(-1,-1,1) + translate(0,-1.501,0),
     * the PoseStack origin is at model top (1.501 blocks above feet), Y-down.
     * Translating by 1.501 maps glTF feet to PoseStack feet.
     */
    private static final float ALIGNMENT_Y = 1.501f;

    public V2BondageRenderLayer(RenderLayerParent<T, M> renderer) {
        super(renderer);
    }

    @Override
    public void render(
        PoseStack poseStack,
        MultiBufferSource buffer,
        int packedLight,
        T entity,
        float limbSwing,
        float limbSwingAmount,
        float partialTick,
        float ageInTicks,
        float netHeadYaw,
        float headPitch
    ) {
        // Do NOT use entity.isInvisible() — that hides the local player's own
        // bondage from their F5/F1 self-view under Invisibility. isInvisibleTo
        // handles same-team visibility and spectator viewers correctly BUT
        // returns true for self when teamless (MC default) + Invisibility —
        // excluding `entity == mc.player` preserves the self-view.
        Minecraft mc = Minecraft.getInstance();
        if (mc.player != null && entity != mc.player && entity.isInvisibleTo(mc.player)) {
            return;
        }
        if (entity instanceof Player p && p.isSpectator()) {
            return;
        }

        // Get V2 equipment via capability (Players) or IV2EquipmentHolder (Damsels)
        IV2BondageEquipment equipment = null;
        if (entity instanceof Player player) {
            equipment = player
                .getCapability(V2BondageEquipmentProvider.V2_BONDAGE_EQUIPMENT)
                .orElse(null);
        } else if (entity instanceof IV2EquipmentHolder holder) {
            equipment = holder.getV2Equipment();
        }
        if (equipment == null) {
            return;
        }

        // Get all equipped items (de-duplicated map)
        Map<BodyRegionV2, ItemStack> equipped = equipment.getAllEquipped();
        if (equipped.isEmpty()) {
            return;
        }

        // Skip rendering items in regions blocked by furniture/seat provider
        Set<BodyRegionV2> furnitureBlocked = Set.of();
        if (
            entity.isPassenger() &&
            entity.getVehicle() instanceof ISeatProvider provider
        ) {
            SeatDefinition seat = provider.getSeatForPassenger(entity);
            if (seat != null) {
                furnitureBlocked = seat.blockedRegions();
            }
        }

        M parentModel = this.getParentModel();
        int packedOverlay = LivingEntityRenderer.getOverlayCoords(entity, 0.0f);

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

            // Check if the item implements IV2BondageItem
            if (!(stack.getItem() instanceof IV2BondageItem bondageItem)) {
                continue;
            }

            // Skip if the furniture blocks ANY region this item occupies. A
            // multi-region item (armbinder on {ARMS, HANDS, TORSO}) can be
            // keyed in the de-duplicated map under ARMS but must still skip
            // when a seat blocks only HANDS — hence disjoint() on all regions.
            Set<BodyRegionV2> itemRegions = bondageItem.getOccupiedRegions(stack);
            if (
                !furnitureBlocked.isEmpty() &&
                !Collections.disjoint(itemRegions, furnitureBlocked)
            ) {
                continue;
            }

            // Select slim model variant for Alex-style players or slim Damsels
            boolean isSlim;
            if (entity instanceof AbstractClientPlayer acp) {
                isSlim = "slim".equals(acp.getModelName());
            } else if (entity instanceof AbstractTiedUpNpc npc) {
                isSlim = npc.hasSlimArms();
            } else {
                isSlim = false;
            }
            ResourceLocation modelLocation = (isSlim &&
                bondageItem.supportsSlimModel(stack))
                ? bondageItem.getSlimModelLocation(stack)
                : null;
            if (modelLocation == null) {
                modelLocation = bondageItem.getModelLocation(stack);
            }
            if (modelLocation == null) {
                continue;
            }

            // Load GLB data from cache
            GltfData data = GltfCache.get(modelLocation);
            if (data == null) {
                LOGGER.debug(
                    "[V2Render] Failed to load GLB for item {}: {}",
                    stack.getItem(),
                    modelLocation
                );
                continue;
            }

            // Compute joint matrices for this specific GLB model
            // Each GLB has its own skeleton, so matrices are per-item
            Matrix4f[] joints =
                GltfLiveBoneReader.computeJointMatricesFromModel(
                    parentModel,
                    data,
                    entity
                );
            if (joints == null) {
                // Fallback to GLB-internal skinning
                joints = GltfSkinningEngine.computeJointMatrices(data);
            }

            // Render this item's mesh
            poseStack.pushPose();
            poseStack.translate(0, ALIGNMENT_Y, 0);

            // Check for tint channels — use per-primitive tinted rendering if present
            Map<String, Integer> tintColors = TintColorResolver.resolve(stack);
            if (!tintColors.isEmpty() && data.primitives().size() > 1) {
                // Multi-primitive mesh with tint data: render per-primitive with colors
                RenderType renderType =
                    GltfMeshRenderer.getRenderTypeForDefaultTexture();
                GltfMeshRenderer.renderSkinnedTinted(
                    data,
                    joints,
                    poseStack,
                    buffer,
                    packedLight,
                    packedOverlay,
                    renderType,
                    tintColors
                );
            } else {
                // Standard path: single primitive or no tint data
                GltfMeshRenderer.renderSkinned(
                    data,
                    joints,
                    poseStack,
                    buffer,
                    packedLight,
                    packedOverlay
                );
            }

            poseStack.popPose();
        }
    }
}
