package com.tiedup.remake.client.gltf;

import com.mojang.blaze3d.vertex.DefaultVertexFormat;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import com.mojang.blaze3d.vertex.VertexFormat;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderStateShard;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
import org.joml.Vector4f;

/**
 * Submits CPU-skinned glTF mesh vertices to Minecraft's rendering pipeline.
 * Uses TRIANGLES mode RenderType (same pattern as ObjModelRenderer).
 */
@OnlyIn(Dist.CLIENT)
public final class GltfMeshRenderer extends RenderStateShard {

    private static final ResourceLocation WHITE_TEXTURE =
        ResourceLocation.fromNamespaceAndPath(
            "tiedup",
            "models/obj/shared/white.png"
        );

    /** Cached default RenderType (white texture). Nulled by clearRenderTypeCache on reload. */
    private static volatile RenderType cachedDefaultRenderType;

    /** Cache for texture-specific RenderTypes, keyed by ResourceLocation. */
    private static final Map<ResourceLocation, RenderType> RENDER_TYPE_CACHE =
        new ConcurrentHashMap<>();

    private GltfMeshRenderer() {
        super("tiedup_gltf_renderer", () -> {}, () -> {});
    }

    /**
     * Get the default TRIANGLES-mode RenderType (white texture), creating it once if needed.
     */
    private static RenderType getDefaultRenderType() {
        if (cachedDefaultRenderType == null) {
            cachedDefaultRenderType = createTriangleRenderType(WHITE_TEXTURE);
        }
        return cachedDefaultRenderType;
    }

    /**
     * Public accessor for the default RenderType (white texture).
     * Used by external renderers that need the same RenderType for tinted rendering.
     */
    public static RenderType getRenderTypeForDefaultTexture() {
        return getDefaultRenderType();
    }

    /**
     * Get a RenderType for a specific texture, caching it for reuse.
     *
     * @param texture the texture ResourceLocation
     * @return the cached or newly created RenderType
     */
    private static RenderType getRenderTypeForTexture(
        ResourceLocation texture
    ) {
        return RENDER_TYPE_CACHE.computeIfAbsent(
            texture,
            GltfMeshRenderer::createTriangleRenderType
        );
    }

    /**
     * Create a TRIANGLES-mode RenderType for glTF mesh rendering with the given texture.
     */
    private static RenderType createTriangleRenderType(
        ResourceLocation texture
    ) {
        RenderType.CompositeState state = RenderType.CompositeState.builder()
            .setShaderState(RENDERTYPE_ENTITY_CUTOUT_NO_CULL_SHADER)
            .setTextureState(
                new RenderStateShard.TextureStateShard(texture, false, false)
            )
            .setTransparencyState(NO_TRANSPARENCY)
            .setCullState(NO_CULL)
            .setLightmapState(LIGHTMAP)
            .setOverlayState(OVERLAY)
            .createCompositeState(true);

        return RenderType.create(
            "tiedup_gltf_triangles",
            DefaultVertexFormat.NEW_ENTITY,
            VertexFormat.Mode.TRIANGLES,
            256 * 1024,
            true,
            false,
            state
        );
    }

    /**
     * Clear cached RenderTypes. Call on resource reload so that re-exported
     * textures are picked up without restarting the game.
     */
    public static void clearRenderTypeCache() {
        cachedDefaultRenderType = null;
        RENDER_TYPE_CACHE.clear();
    }

    /**
     * Render a skinned glTF mesh using the default white texture.
     *
     * @param data          parsed glTF data
     * @param jointMatrices computed joint matrices from skinning engine
     * @param poseStack     current pose stack
     * @param buffer        multi-buffer source
     * @param packedLight   packed light value
     * @param packedOverlay packed overlay value
     */
    public static void renderSkinned(
        GltfData data,
        Matrix4f[] jointMatrices,
        PoseStack poseStack,
        MultiBufferSource buffer,
        int packedLight,
        int packedOverlay
    ) {
        renderSkinnedInternal(
            data,
            jointMatrices,
            poseStack,
            buffer,
            packedLight,
            packedOverlay,
            getDefaultRenderType()
        );
    }

    /**
     * Render a skinned glTF mesh using a custom texture.
     *
     * @param data          parsed glTF data
     * @param jointMatrices computed joint matrices from skinning engine
     * @param poseStack     current pose stack
     * @param buffer        multi-buffer source
     * @param packedLight   packed light value
     * @param packedOverlay packed overlay value
     * @param texture       the texture to use for rendering
     */
    public static void renderSkinned(
        GltfData data,
        Matrix4f[] jointMatrices,
        PoseStack poseStack,
        MultiBufferSource buffer,
        int packedLight,
        int packedOverlay,
        ResourceLocation texture
    ) {
        renderSkinnedInternal(
            data,
            jointMatrices,
            poseStack,
            buffer,
            packedLight,
            packedOverlay,
            getRenderTypeForTexture(texture)
        );
    }

    /**
     * Internal rendering implementation shared by both overloads. Emits every
     * index with a single flat white color — no per-primitive metadata is read.
     */
    private static void renderSkinnedInternal(
        GltfData data,
        Matrix4f[] jointMatrices,
        PoseStack poseStack,
        MultiBufferSource buffer,
        int packedLight,
        int packedOverlay,
        RenderType renderType
    ) {
        Matrix4f pose = poseStack.last().pose();
        Matrix3f normalMat = poseStack.last().normal();
        VertexConsumer vc = buffer.getBuffer(renderType);
        float[] texCoords = data.texCoords();
        VertexScratch s = new VertexScratch();

        for (int idx : data.indices()) {
            emitVertex(
                vc, pose, normalMat, data, jointMatrices, idx, texCoords,
                255, 255, 255, packedLight, packedOverlay, s
            );
        }
    }

    /**
     * Scratch buffers reused across every vertex emission in a single render
     * call. Kept as a tiny value class so the two public loops share the same
     * pre-alloc pattern without duplicating four local variables.
     */
    private static final class VertexScratch {

        final float[] outPos = new float[3];
        final float[] outNormal = new float[3];
        final Vector4f tmpPos = new Vector4f();
        final Vector4f tmpNorm = new Vector4f();
    }

    /**
     * Skin {@code idx} and push one vertex into {@code vc}. Extracted from
     * {@link #renderSkinnedInternal} and {@link #renderSkinnedTinted} so both
     * loops share the vertex-format contract — if the format ever changes,
     * the edit happens in one place.
     */
    private static void emitVertex(
        VertexConsumer vc,
        Matrix4f pose,
        Matrix3f normalMat,
        GltfData data,
        Matrix4f[] jointMatrices,
        int idx,
        float[] texCoords,
        int r,
        int g,
        int b,
        int packedLight,
        int packedOverlay,
        VertexScratch s
    ) {
        GltfSkinningEngine.skinVertex(
            data,
            idx,
            jointMatrices,
            s.outPos,
            s.outNormal,
            s.tmpPos,
            s.tmpNorm
        );
        float u = texCoords[idx * 2];
        float v = texCoords[idx * 2 + 1];
        vc
            .vertex(pose, s.outPos[0], s.outPos[1], s.outPos[2])
            .color(r, g, b, 255)
            .uv(u, 1.0f - v)
            .overlayCoords(packedOverlay)
            .uv2(packedLight)
            .normal(
                normalMat,
                s.outNormal[0],
                s.outNormal[1],
                s.outNormal[2]
            )
            .endVertex();
    }

    /**
     * Render a skinned glTF mesh with per-primitive tint colors.
     *
     * <p>Each primitive in the mesh is checked against the tintColors map.
     * If a primitive is tintable and its channel is present in the map,
     * the corresponding RGB color is applied as vertex color (multiplied
     * against the texture by the {@code rendertype_entity_cutout_no_cull} shader).
     * Non-tintable primitives render with white (no tint).</p>
     *
     * <p>This is a single VertexConsumer stream — all primitives share the
     * same RenderType and draw call, only the vertex color differs per range.</p>
     *
     * @param data          parsed glTF data (must have primitives)
     * @param jointMatrices computed joint matrices from skinning engine
     * @param poseStack     current pose stack
     * @param buffer        multi-buffer source
     * @param packedLight   packed light value
     * @param packedOverlay packed overlay value
     * @param renderType    the RenderType to use
     * @param tintColors    channel name to RGB int (0xRRGGBB); empty map = white everywhere
     */
    public static void renderSkinnedTinted(
        GltfData data,
        Matrix4f[] jointMatrices,
        PoseStack poseStack,
        MultiBufferSource buffer,
        int packedLight,
        int packedOverlay,
        RenderType renderType,
        Map<String, Integer> tintColors
    ) {
        Matrix4f pose = poseStack.last().pose();
        Matrix3f normalMat = poseStack.last().normal();
        VertexConsumer vc = buffer.getBuffer(renderType);
        float[] texCoords = data.texCoords();
        VertexScratch s = new VertexScratch();

        for (GltfData.Primitive prim : data.primitives()) {
            int r = 255, g = 255, b = 255;
            if (prim.tintable() && prim.tintChannel() != null) {
                Integer colorInt = tintColors.get(prim.tintChannel());
                if (colorInt != null) {
                    r = (colorInt >> 16) & 0xFF;
                    g = (colorInt >> 8) & 0xFF;
                    b = colorInt & 0xFF;
                }
            }
            for (int idx : prim.indices()) {
                emitVertex(
                    vc, pose, normalMat, data, jointMatrices, idx, texCoords,
                    r, g, b, packedLight, packedOverlay, s
                );
            }
        }
    }
}
