package com.tiedup.remake.client.renderer.layers;

import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import com.tiedup.remake.client.state.ClothesClientCache;
import com.tiedup.remake.client.texture.DynamicTextureManager;
import com.tiedup.remake.items.clothes.ClothesProperties;
import com.tiedup.remake.items.clothes.GenericClothes;
import java.util.EnumSet;
import java.util.UUID;
import net.minecraft.client.model.HumanoidModel;
import net.minecraft.client.model.PlayerModel;
import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.jetbrains.annotations.Nullable;

/**
 * Helper class for rendering clothes with dynamic textures.
 *
 * <p>IMPORTANT: Clothes are rendered using a PlayerModel (not bondage item model)
 * because clothes textures are standard Minecraft skins (64x64) with player UV mappings.
 *
 * <p>Handles:
 * <ul>
 *   <li>Dynamic texture URL rendering</li>
 *   <li>Full-skin mode (covers entire player model)</li>
 *   <li>Small arms mode (Alex/slim arms)</li>
 *   <li>Layer visibility control</li>
 *   <li>Steve vs Alex model selection</li>
 * </ul>
 *
 * <p>This is called from BondageItemRenderLayer when rendering CLOTHES slot.
 */
@OnlyIn(Dist.CLIENT)
public class ClothesRenderHelper {

    /**
     * Attempt to render clothes with dynamic texture.
     * Uses PlayerModel (not bondage model) because clothes use skin UV mappings.
     *
     * @param clothes The clothes ItemStack
     * @param entity The entity wearing the clothes
     * @param poseStack The pose stack
     * @param buffer The render buffer
     * @param packedLight The packed light value
     * @param packedOverlay The packed overlay value (for hit flash effect)
     * @param parentModel The parent PlayerModel to copy pose from
     * @return true if dynamic texture was rendered, false otherwise
     */
    public static boolean tryRenderDynamic(
        ItemStack clothes,
        LivingEntity entity,
        PoseStack poseStack,
        MultiBufferSource buffer,
        int packedLight,
        int packedOverlay,
        PlayerModel<?> parentModel
    ) {
        if (
            clothes.isEmpty() ||
            !(clothes.getItem() instanceof GenericClothes gc)
        ) {
            return false;
        }

        // Check if ClothesModelCache is initialized
        if (!ClothesModelCache.isInitialized()) {
            return false;
        }

        // Get properties from equipped clothes or remote player cache
        ClothesProperties props = getClothesProperties(clothes, entity);
        if (props == null) {
            return false;
        }

        String dynamicUrl = props.dynamicTextureUrl();
        if (dynamicUrl == null || dynamicUrl.isEmpty()) {
            return false;
        }

        // Get texture from manager
        DynamicTextureManager texManager = DynamicTextureManager.getInstance();
        ResourceLocation texLocation = texManager.getTextureLocation(
            dynamicUrl,
            props.fullSkin()
        );

        if (texLocation == null) {
            // Texture not loaded yet (downloading)
            return false;
        }

        // Determine if we should use slim (Alex) arms
        boolean useSlim = shouldUseSlimArms(clothes, entity, props);

        // Get the appropriate PlayerModel from cache
        PlayerModel<AbstractClientPlayer> clothesModel =
            ClothesModelCache.getModel(useSlim);
        if (clothesModel == null) {
            return false;
        }

        // Copy pose from parent model to clothes model
        copyPose(parentModel, clothesModel);

        // Apply layer visibility settings
        applyLayerVisibility(
            clothesModel,
            props.visibleLayers(),
            props.keepHead()
        );

        // Render the clothes model with dynamic texture
        // Use entityTranslucent for layered rendering (clothes on top of player)
        VertexConsumer vertexConsumer = buffer.getBuffer(
            RenderType.entityTranslucent(texLocation)
        );
        clothesModel.renderToBuffer(
            poseStack,
            vertexConsumer,
            packedLight,
            packedOverlay,
            1.0f,
            1.0f,
            1.0f,
            1.0f
        );

        // Restore visibility (model is shared, need to reset for next render)
        restoreLayerVisibility(clothesModel);

        return true;
    }

    /**
     * Render clothes for NPCs (Damsel, etc.)
     * Uses PlayerModel from cache, copies pose from parent HumanoidModel.
     *
     * @param clothes The clothes ItemStack
     * @param entity The NPC entity wearing clothes
     * @param poseStack The pose stack
     * @param buffer The render buffer
     * @param packedLight The packed light value
     * @param packedOverlay The packed overlay value (for hit flash effect)
     * @param parentModel The parent HumanoidModel to copy pose from
     * @param hasSlimArms Whether the NPC uses slim arms
     * @return true if rendered successfully
     */
    public static boolean tryRenderDynamicForNPC(
        ItemStack clothes,
        LivingEntity entity,
        PoseStack poseStack,
        MultiBufferSource buffer,
        int packedLight,
        int packedOverlay,
        HumanoidModel<?> parentModel,
        boolean hasSlimArms
    ) {
        if (
            clothes.isEmpty() || !(clothes.getItem() instanceof GenericClothes)
        ) {
            return false;
        }

        if (!ClothesModelCache.isInitialized()) {
            return false;
        }

        // Get properties directly from item (NPCs don't use remote cache)
        ClothesProperties props = ClothesProperties.fromStack(clothes);
        if (props == null) {
            return false;
        }

        String dynamicUrl = props.dynamicTextureUrl();
        if (dynamicUrl == null || dynamicUrl.isEmpty()) {
            return false;
        }

        // Get texture from manager
        ResourceLocation texLocation =
            DynamicTextureManager.getInstance().getTextureLocation(
                dynamicUrl,
                props.fullSkin()
            );
        if (texLocation == null) {
            return false; // Still downloading
        }

        // Use slim if: clothes force it OR NPC has slim arms
        boolean useSlim = props.smallArms() || hasSlimArms;

        // Get PlayerModel from cache (same cache as players)
        PlayerModel<AbstractClientPlayer> clothesModel =
            ClothesModelCache.getModel(useSlim);
        if (clothesModel == null) {
            return false;
        }

        // Copy pose from HumanoidModel (NPC) to PlayerModel (clothes)
        copyPoseFromHumanoid(parentModel, clothesModel);

        // Apply layer visibility
        applyLayerVisibility(
            clothesModel,
            props.visibleLayers(),
            props.keepHead()
        );

        // Render
        VertexConsumer vertexConsumer = buffer.getBuffer(
            RenderType.entityTranslucent(texLocation)
        );
        clothesModel.renderToBuffer(
            poseStack,
            vertexConsumer,
            packedLight,
            packedOverlay,
            1.0f,
            1.0f,
            1.0f,
            1.0f
        );

        // Restore visibility
        restoreLayerVisibility(clothesModel);

        return true;
    }

    /**
     * Copy pose from HumanoidModel (NPC) to PlayerModel (clothes).
     * PlayerModel has extra parts (jacket, sleeves, pants) that HumanoidModel lacks,
     * so we copy those from the corresponding base parts.
     */
    private static void copyPoseFromHumanoid(
        HumanoidModel<?> source,
        PlayerModel<?> dest
    ) {
        // Base parts (exist in both models)
        dest.head.copyFrom(source.head);
        dest.hat.copyFrom(source.hat);
        dest.body.copyFrom(source.body);
        dest.rightArm.copyFrom(source.rightArm);
        dest.leftArm.copyFrom(source.leftArm);
        dest.rightLeg.copyFrom(source.rightLeg);
        dest.leftLeg.copyFrom(source.leftLeg);

        // PlayerModel-only parts: copy from corresponding base parts
        dest.jacket.copyFrom(source.body);
        dest.leftSleeve.copyFrom(source.leftArm);
        dest.rightSleeve.copyFrom(source.rightArm);
        dest.leftPants.copyFrom(source.leftLeg);
        dest.rightPants.copyFrom(source.rightLeg);

        // Animation flags
        dest.crouching = source.crouching;
        dest.riding = source.riding;
        dest.young = source.young;
    }

    /**
     * Copy pose from source PlayerModel to destination PlayerModel.
     * This ensures the clothes model matches the player's current pose/animation.
     */
    private static void copyPose(PlayerModel<?> source, PlayerModel<?> dest) {
        // Main body parts
        dest.head.copyFrom(source.head);
        dest.hat.copyFrom(source.hat);
        dest.body.copyFrom(source.body);
        dest.rightArm.copyFrom(source.rightArm);
        dest.leftArm.copyFrom(source.leftArm);
        dest.rightLeg.copyFrom(source.rightLeg);
        dest.leftLeg.copyFrom(source.leftLeg);

        // Outer layer parts (jacket, sleeves, pants)
        dest.jacket.copyFrom(source.jacket);
        dest.leftSleeve.copyFrom(source.leftSleeve);
        dest.rightSleeve.copyFrom(source.rightSleeve);
        dest.leftPants.copyFrom(source.leftPants);
        dest.rightPants.copyFrom(source.rightPants);

        // Copy other properties
        dest.crouching = source.crouching;
        dest.young = source.young;
    }

    /**
     * Determine if slim (Alex) arms should be used.
     * Priority: 1) Clothes force small arms, 2) Player's actual model type
     */
    private static boolean shouldUseSlimArms(
        ItemStack clothes,
        LivingEntity entity,
        ClothesProperties props
    ) {
        // 1. Check if clothes item forces small arms
        if (props.smallArms()) {
            return true;
        }

        // 2. Check local item setting
        if (clothes.getItem() instanceof GenericClothes gc) {
            if (gc.shouldForceSmallArms(clothes)) {
                return true;
            }
        }

        // 3. Follow player's actual model type
        if (entity instanceof AbstractClientPlayer player) {
            String modelName = player.getModelName();
            return "slim".equals(modelName);
        }

        return false; // Default: normal (Steve) arms
    }

    /**
     * Get clothes properties, checking both equipped item and remote player cache.
     */
    @Nullable
    private static ClothesProperties getClothesProperties(
        ItemStack clothes,
        LivingEntity entity
    ) {
        // First try to get from the equipped item directly
        ClothesProperties localProps = ClothesProperties.fromStack(clothes);

        // If entity is a player, also check remote cache (for other players' clothes)
        if (entity instanceof AbstractClientPlayer player) {
            UUID playerUUID = player.getUUID();
            ClothesClientCache.CachedClothesData cached =
                ClothesClientCache.getPlayerClothes(playerUUID);

            if (cached != null && cached.hasDynamicTexture()) {
                // Use cached data (synced from server)
                return new ClothesProperties(
                    cached.dynamicUrl(),
                    cached.fullSkin(),
                    cached.smallArms(),
                    cached.keepHead(),
                    cached.visibleLayers()
                );
            }
        }

        return localProps;
    }

    /**
     * Apply layer visibility from clothes properties.
     * Controls which parts of the outer layer (jacket, sleeves, pants) are visible.
     *
     * @param model The clothes PlayerModel
     * @param visible Which layers are enabled on the clothes
     * @param keepHead If true, hide the clothes head/hat (wearer's head shows instead)
     */
    private static void applyLayerVisibility(
        PlayerModel<?> model,
        EnumSet<ClothesProperties.LayerPart> visible,
        boolean keepHead
    ) {
        // Main body parts visibility
        // If keepHead is true, hide clothes head so wearer's head shows through
        model.head.visible = !keepHead;
        model.body.visible = true;
        model.rightArm.visible = true;
        model.leftArm.visible = true;
        model.rightLeg.visible = true;
        model.leftLeg.visible = true;

        // Outer layer parts controlled by settings
        // If keepHead is true, hide clothes hat so wearer's hat shows through
        model.hat.visible =
            !keepHead && visible.contains(ClothesProperties.LayerPart.HEAD);
        model.jacket.visible = visible.contains(
            ClothesProperties.LayerPart.BODY
        );
        model.leftSleeve.visible = visible.contains(
            ClothesProperties.LayerPart.LEFT_ARM
        );
        model.rightSleeve.visible = visible.contains(
            ClothesProperties.LayerPart.RIGHT_ARM
        );
        model.leftPants.visible = visible.contains(
            ClothesProperties.LayerPart.LEFT_LEG
        );
        model.rightPants.visible = visible.contains(
            ClothesProperties.LayerPart.RIGHT_LEG
        );
    }

    /**
     * Restore all layer visibility to default (all visible).
     * Important because the model is cached and shared.
     */
    private static void restoreLayerVisibility(PlayerModel<?> model) {
        model.head.visible = true;
        model.hat.visible = true;
        model.body.visible = true;
        model.jacket.visible = true;
        model.rightArm.visible = true;
        model.leftArm.visible = true;
        model.rightSleeve.visible = true;
        model.leftSleeve.visible = true;
        model.rightLeg.visible = true;
        model.leftLeg.visible = true;
        model.rightPants.visible = true;
        model.leftPants.visible = true;
    }

    /**
     * Check if clothes should force small arms rendering.
     * Public method for BondageItemRenderLayer.
     */
    public static boolean shouldUseSmallArms(
        ItemStack clothes,
        LivingEntity entity
    ) {
        if (
            clothes.isEmpty() ||
            !(clothes.getItem() instanceof GenericClothes gc)
        ) {
            return false;
        }

        // Check local item
        if (gc.shouldForceSmallArms(clothes)) {
            return true;
        }

        // Check remote cache
        if (entity instanceof AbstractClientPlayer player) {
            return ClothesClientCache.isSmallArmsForced(player.getUUID());
        }

        return false;
    }

    /**
     * Check if clothes have a dynamic texture (for deciding render path).
     */
    public static boolean hasDynamicTexture(
        ItemStack clothes,
        LivingEntity entity
    ) {
        if (
            clothes.isEmpty() ||
            !(clothes.getItem() instanceof GenericClothes gc)
        ) {
            return false;
        }

        // Check local item
        String localUrl = gc.getDynamicTextureUrl(clothes);
        if (localUrl != null && !localUrl.isEmpty()) {
            return true;
        }

        // Check remote cache
        if (entity instanceof AbstractClientPlayer player) {
            String cachedUrl = ClothesClientCache.getPlayerDynamicUrl(
                player.getUUID()
            );
            return cachedUrl != null && !cachedUrl.isEmpty();
        }

        return false;
    }

    /**
     * Check if full-skin mode is enabled for clothes.
     */
    public static boolean isFullSkinMode(
        ItemStack clothes,
        LivingEntity entity
    ) {
        if (
            clothes.isEmpty() ||
            !(clothes.getItem() instanceof GenericClothes gc)
        ) {
            return false;
        }

        // Check local item
        if (gc.isFullSkinEnabled(clothes)) {
            return true;
        }

        // Check remote cache
        if (entity instanceof AbstractClientPlayer player) {
            return ClothesClientCache.isFullSkinEnabled(player.getUUID());
        }

        return false;
    }

    // ==================== Wearer Layer Hiding ====================

    /**
     * Hide wearer's outer layers when clothes are equipped.
     * Called BEFORE rendering the base model.
     *
     * <p>Logic: When clothes are equipped, hide ALL wearer's outer layers.
     * The clothes will render their own layers on top.
     * Exception: If keepHead is enabled, the head/hat layers remain visible.
     *
     * @param model The wearer's PlayerModel
     * @param props Clothes properties (used to confirm clothes are valid)
     * @return Original visibility state for restoration (6 booleans)
     */
    public static boolean[] hideWearerLayers(
        PlayerModel<?> model,
        ClothesProperties props
    ) {
        if (props == null) {
            return null;
        }

        // Save original state
        boolean[] original = {
            model.hat.visible,
            model.jacket.visible,
            model.leftSleeve.visible,
            model.rightSleeve.visible,
            model.leftPants.visible,
            model.rightPants.visible,
        };

        // When wearing clothes, hide wearer's outer layers
        // Exception: if keepHead is true, don't hide head/hat
        if (!props.keepHead()) {
            model.hat.visible = false;
        }
        model.jacket.visible = false;
        model.leftSleeve.visible = false;
        model.rightSleeve.visible = false;
        model.leftPants.visible = false;
        model.rightPants.visible = false;

        return original;
    }

    /**
     * Restore wearer's layer visibility after rendering.
     *
     * @param model The wearer's PlayerModel
     * @param original Original visibility state from hideWearerLayers()
     */
    public static void restoreWearerLayers(
        PlayerModel<?> model,
        boolean[] original
    ) {
        if (original == null || original.length != 6) {
            return;
        }

        model.hat.visible = original[0];
        model.jacket.visible = original[1];
        model.leftSleeve.visible = original[2];
        model.rightSleeve.visible = original[3];
        model.leftPants.visible = original[4];
        model.rightPants.visible = original[5];
    }

    /**
     * Get clothes properties for wearer layer hiding.
     * Works for both players (with remote cache) and NPCs (direct from item).
     *
     * @param clothes The clothes ItemStack
     * @param entity The entity wearing clothes
     * @return ClothesProperties or null if not available
     */
    public static ClothesProperties getPropsForLayerHiding(
        ItemStack clothes,
        LivingEntity entity
    ) {
        if (
            clothes.isEmpty() || !(clothes.getItem() instanceof GenericClothes)
        ) {
            return null;
        }

        // For players, check remote cache first
        if (entity instanceof AbstractClientPlayer player) {
            ClothesClientCache.CachedClothesData cached =
                ClothesClientCache.getPlayerClothes(player.getUUID());
            if (cached != null) {
                return new ClothesProperties(
                    cached.dynamicUrl(),
                    cached.fullSkin(),
                    cached.smallArms(),
                    cached.keepHead(),
                    cached.visibleLayers()
                );
            }
        }

        // Fall back to direct item properties
        return ClothesProperties.fromStack(clothes);
    }
}
