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

import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis;
import com.tiedup.remake.client.gltf.GltfData;
import com.tiedup.remake.client.gltf.GltfMeshRenderer;
import com.tiedup.remake.client.gltf.GltfSkinningEngine;
import com.tiedup.remake.v2.furniture.EntityFurniture;
import com.tiedup.remake.v2.furniture.FurnitureDefinition;
import java.util.Map;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.entity.EntityRenderer;
import net.minecraft.client.renderer.entity.EntityRendererProvider;
import net.minecraft.client.renderer.texture.OverlayTexture;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.joml.Matrix4f;

/**
 * EntityRenderer for data-driven furniture entities.
 *
 * <p>Renders the furniture mesh from GLB data loaded via {@link FurnitureGltfCache},
 * using the existing {@link GltfMeshRenderer} pipeline for CPU-skinned GLTF rendering.
 * Supports both static rest-pose and animated rendering based on the entity's
 * synched animation state ({@link EntityFurniture#getAnimState()}).</p>
 *
 * <p>Tint channels from the {@link FurnitureDefinition} are applied via
 * {@link GltfMeshRenderer#renderSkinnedTinted} when the definition specifies
 * non-empty tint channel defaults and the mesh has multiple primitives.</p>
 *
 * <p>This is a non-living entity renderer (extends {@code EntityRenderer}, not
 * {@code LivingEntityRenderer}), so there is no hurt overlay or death animation.
 * Overlay coords use {@link OverlayTexture#NO_OVERLAY} instead of the
 * living-entity overlay that reads entity health (which would NPE).</p>
 *
 * @see EntityFurniture
 * @see FurnitureGltfCache
 * @see GltfMeshRenderer
 */
@OnlyIn(Dist.CLIENT)
public class FurnitureEntityRenderer extends EntityRenderer<EntityFurniture> {

    public FurnitureEntityRenderer(EntityRendererProvider.Context ctx) {
        super(ctx);
    }

    @Override
    public void render(
        EntityFurniture entity,
        float yaw,
        float partialTick,
        PoseStack poseStack,
        MultiBufferSource buffer,
        int packedLight
    ) {
        FurnitureDefinition def = entity.getDefinition();
        if (def == null) return;

        FurnitureGltfData data = FurnitureGltfCache.get(def.modelLocation());
        if (data == null || data.furnitureMesh() == null) return;

        GltfData meshData = data.furnitureMesh();

        // Compute joint matrices: animated if there is an active clip, static otherwise
        GltfData.AnimationClip activeClip = resolveActiveAnimation(
            entity,
            meshData
        );
        Matrix4f[] joints;
        if (activeClip != null) {
            float time = computeAnimationTime(entity, activeClip, partialTick);
            joints = GltfSkinningEngine.computeJointMatricesAnimated(
                meshData,
                activeClip,
                time
            );
        } else {
            joints = GltfSkinningEngine.computeJointMatrices(meshData);
        }

        poseStack.pushPose();

        // Apply entity yaw rotation around the Y axis.
        // The entity's yaw is set during placement and synced via IEntityAdditionalSpawnData.
        poseStack.mulPose(Axis.YP.rotationDegrees(-yaw));

        // Non-living entities use NO_OVERLAY (no red hurt flash, no death tint).
        // LivingEntityRenderer.getOverlayCoords(null, ...) would NPE because it
        // accesses entity health.
        int packedOverlay = OverlayTexture.NO_OVERLAY;

        // Render with tint support if the definition has tint channels and the mesh
        // has multiple primitives (tintable and non-tintable parts).
        Map<String, Integer> tintColors = def.tintChannels();
        if (!tintColors.isEmpty() && meshData.primitives().size() > 1) {
            RenderType renderType =
                GltfMeshRenderer.getRenderTypeForDefaultTexture();
            GltfMeshRenderer.renderSkinnedTinted(
                meshData,
                joints,
                poseStack,
                buffer,
                packedLight,
                packedOverlay,
                renderType,
                tintColors
            );
        } else {
            GltfMeshRenderer.renderSkinned(
                meshData,
                joints,
                poseStack,
                buffer,
                packedLight,
                packedOverlay
            );
        }

        poseStack.popPose();

        // super.render() handles debug hitbox rendering and name tag display
        super.render(entity, yaw, partialTick, poseStack, buffer, packedLight);
    }

    /**
     * Map the entity's synched animation state to a named animation clip from the GLB.
     *
     * <p>Falls back to "Idle" if the specific state animation is not found in the mesh.
     * Returns null if the mesh has no animations at all (static furniture).</p>
     *
     * @param entity   the furniture entity
     * @param meshData the parsed GLB mesh data
     * @return the resolved animation clip, or null for static rendering
     */
    private GltfData.AnimationClip resolveActiveAnimation(
        EntityFurniture entity,
        GltfData meshData
    ) {
        String animName = switch (entity.getAnimState()) {
            case EntityFurniture.STATE_OCCUPIED -> "Occupied";
            case EntityFurniture.STATE_LOCKING -> "LockClose";
            case EntityFurniture.STATE_STRUGGLE -> "Shake";
            case EntityFurniture.STATE_UNLOCKING -> "LockOpen";
            case EntityFurniture.STATE_ENTERING -> "Occupied"; // furniture plays Occupied during player enter transition
            case EntityFurniture.STATE_EXITING -> "Idle"; // furniture transitions to Idle during player exit
            default -> "Idle";
        };
        GltfData.AnimationClip clip = meshData.getAnimation(animName);
        if (
            clip == null && entity.getAnimState() != EntityFurniture.STATE_IDLE
        ) {
            // Specific state animation missing: fall back to Idle
            clip = meshData.getAnimation("Idle");
        }
        // Returns null if there are no animations at all (static mesh)
        return clip;
    }

    /**
     * Compute the animation time in frame-space for the skinning engine.
     *
     * <p>Uses the entity's tick count plus partial tick for smooth interpolation.
     * The time is looped via modulo against the clip's frame count. A playback speed
     * of 1.0 means one frame per tick (20 FPS, matching Minecraft's tick rate).</p>
     *
     * @param entity      the furniture entity (provides tick count)
     * @param clip        the active animation clip (provides frame count)
     * @param partialTick fractional tick for interpolation (0.0 to 1.0)
     * @return time in frame-space, suitable for {@link GltfSkinningEngine#computeJointMatricesAnimated}
     */
    private float computeAnimationTime(
        EntityFurniture entity,
        GltfData.AnimationClip clip,
        float partialTick
    ) {
        int frameCount = clip.frameCount();
        if (frameCount <= 1) return 0f;

        // 1 frame per tick = 20 FPS playback, matching Minecraft tick rate.
        // partialTick smooths between ticks for frame-rate-independent display.
        float time = (entity.tickCount + partialTick);

        // Loop within the valid frame range [0, frameCount - 1]
        return time % frameCount;
    }

    /**
     * GLB pipeline does not use the vanilla texture atlas system.
     * Textures are baked into the GLB file and applied via the custom RenderType
     * in {@link GltfMeshRenderer}.
     *
     * @return null because the GLB pipeline manages its own textures
     */
    @Override
    public ResourceLocation getTextureLocation(EntityFurniture entity) {
        return null;
    }
}
