package com.straice.smoothdoors.client.anim;

import net.minecraft.block.BlockState;
import net.minecraft.block.FenceGateBlock;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.RenderLayers;
import net.minecraft.client.render.VertexConsumer;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.block.BlockModelRenderer;
import net.minecraft.client.render.model.BakedQuad;
import net.minecraft.client.render.model.BlockModelPart;
import net.minecraft.client.render.model.BlockStateModel;
import net.minecraft.client.texture.Sprite;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.RotationAxis;
import net.minecraft.util.math.random.Random;

import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;

final class FenceGateAnimation {

    private static final float POST_EDGE = 2.0f / 16.0f;
    private static final float EDGE_EPS = 1.0e-4f;

    private FenceGateAnimation() {}

    static void render(BlockState state, float angleDeg, MatrixStack matrices,
                       VertexConsumerProvider consumers, int light, int overlay) {
        MinecraftClient mc = MinecraftClient.getInstance();
        if (mc.world == null) return;

        Direction facing = state.get(FenceGateBlock.FACING);
        boolean leftRightIsX = facing.getAxis() == Direction.Axis.Z;

        BlockStateModel baseModel = mc.getBlockRenderManager().getModel(state);
        FenceGateModels models = splitFenceGateModels(baseModel, leftRightIsX);
        if (models.isEmpty()) return;

        int tint = mc.getBlockColors().getColor(state, null, null, 0);
        float r = ((tint >> 16) & 0xFF) / 255.0f;
        float g = ((tint >> 8) & 0xFF) / 255.0f;
        float b = (tint & 0xFF) / 255.0f;

        VertexConsumer consumer = consumers.getBuffer(RenderLayers.getEntityBlockLayer(state));

        if (models.posts != null) {
            BlockModelRenderer.render(matrices.peek(), consumer, models.posts, r, g, b, light, overlay);
        }

        float leftPivotX = leftRightIsX ? (1.0f / 16.0f) : 0.5f;
        float leftPivotZ = leftRightIsX ? 0.5f : (1.0f / 16.0f);
        float rightPivotX = leftRightIsX ? (15.0f / 16.0f) : 0.5f;
        float rightPivotZ = leftRightIsX ? 0.5f : (15.0f / 16.0f);

        float sign = (facing == Direction.SOUTH || facing == Direction.EAST) ? 1.0f : -1.0f;
        float signedAngle = angleDeg * sign;
        float leftAngle = leftRightIsX ? -signedAngle : signedAngle;
        float rightAngle = leftRightIsX ? signedAngle : -signedAngle;

        if (models.left != null) {
            matrices.push();
            matrices.translate(leftPivotX, 0.0f, leftPivotZ);
            matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(leftAngle));
            matrices.translate(-leftPivotX, 0.0f, -leftPivotZ);
            BlockModelRenderer.render(matrices.peek(), consumer, models.left, r, g, b, light, overlay);
            matrices.pop();
        }

        if (models.right != null) {
            matrices.push();
            matrices.translate(rightPivotX, 0.0f, rightPivotZ);
            matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(rightAngle));
            matrices.translate(-rightPivotX, 0.0f, -rightPivotZ);
            BlockModelRenderer.render(matrices.peek(), consumer, models.right, r, g, b, light, overlay);
            matrices.pop();
        }
    }

    private static FenceGateModels splitFenceGateModels(BlockStateModel model, boolean leftRightIsX) {
        List<BlockModelPart> parts = model.getParts(Random.create(42L));
        List<BlockModelPart> posts = new ArrayList<>();
        List<BlockModelPart> left = new ArrayList<>();
        List<BlockModelPart> right = new ArrayList<>();

        for (BlockModelPart part : parts) {
            FenceGateCollector postsCollector = new FenceGateCollector();
            FenceGateCollector leftCollector = new FenceGateCollector();
            FenceGateCollector rightCollector = new FenceGateCollector();

            for (Direction dir : Direction.values()) {
                addFenceGateQuads(part, dir, leftRightIsX, postsCollector, leftCollector, rightCollector);
            }
            addFenceGateQuads(part, null, leftRightIsX, postsCollector, leftCollector, rightCollector);

            if (!postsCollector.isEmpty()) posts.add(postsCollector.toPart(part));
            if (!leftCollector.isEmpty()) left.add(leftCollector.toPart(part));
            if (!rightCollector.isEmpty()) right.add(rightCollector.toPart(part));
        }

        return new FenceGateModels(
                posts.isEmpty() ? null : new StaticBlockStateModel(posts, model.particleSprite()),
                left.isEmpty() ? null : new StaticBlockStateModel(left, model.particleSprite()),
                right.isEmpty() ? null : new StaticBlockStateModel(right, model.particleSprite())
        );
    }

    private static void addFenceGateQuads(BlockModelPart part, Direction dir, boolean leftRightIsX,
                                          FenceGateCollector posts, FenceGateCollector left, FenceGateCollector right) {
        List<BakedQuad> quads = part.getQuads(dir);
        if (quads.isEmpty()) return;

        for (BakedQuad quad : quads) {
            FenceGateSection section = classifyFenceGateQuad(quad, leftRightIsX);
            switch (section) {
                case POSTS -> posts.add(dir, quad);
                case LEFT -> addDoubleSided(left, dir, quad);
                case RIGHT -> addDoubleSided(right, dir, quad);
            }
        }
    }

    private static FenceGateSection classifyFenceGateQuad(BakedQuad quad, boolean leftRightIsX) {
        int[] data = quad.vertexData();
        if (data.length < 8) return FenceGateSection.LEFT;

        int stride = data.length / 4;
        float min = Float.POSITIVE_INFINITY;
        float max = Float.NEGATIVE_INFINITY;
        int coordOffset = leftRightIsX ? 0 : 2;

        for (int i = 0; i < 4; i++) {
            int base = i * stride + coordOffset;
            float coord = Float.intBitsToFloat(data[base]);
            min = Math.min(min, coord);
            max = Math.max(max, coord);
        }

        if (max <= POST_EDGE + EDGE_EPS || min >= (1.0f - POST_EDGE) - EDGE_EPS) {
            return FenceGateSection.POSTS;
        }

        float center = (min + max) * 0.5f;
        return center <= 0.5f ? FenceGateSection.LEFT : FenceGateSection.RIGHT;
    }

    private enum FenceGateSection { POSTS, LEFT, RIGHT }

    private static void addDoubleSided(FenceGateCollector target, Direction dir, BakedQuad quad) {
        target.add(dir, quad);
        target.add(dir, reverseQuad(quad));
    }

    private static BakedQuad reverseQuad(BakedQuad quad) {
        int[] data = quad.vertexData();
        int stride = data.length / 4;
        int[] flipped = new int[data.length];

        for (int v = 0; v < 4; v++) {
            int src = (3 - v) * stride;
            int dst = v * stride;
            System.arraycopy(data, src, flipped, dst, stride);
        }

        Direction face = quad.face();
        Direction opposite = (face == null) ? null : face.getOpposite();
        return new BakedQuad(flipped, quad.tintIndex(), opposite, quad.sprite(), quad.shade(), quad.lightEmission());
    }

    private static final class FenceGateModels {
        final BlockStateModel posts;
        final BlockStateModel left;
        final BlockStateModel right;

        FenceGateModels(BlockStateModel posts, BlockStateModel left, BlockStateModel right) {
            this.posts = posts;
            this.left = left;
            this.right = right;
        }

        boolean isEmpty() {
            return posts == null && left == null && right == null;
        }
    }

    private static final class FenceGateCollector {
        private final EnumMap<Direction, List<BakedQuad>> faceQuads = new EnumMap<>(Direction.class);
        private final List<BakedQuad> unculled = new ArrayList<>();

        void add(Direction face, BakedQuad quad) {
            if (face == null) {
                unculled.add(quad);
                return;
            }
            faceQuads.computeIfAbsent(face, k -> new ArrayList<>()).add(quad);
        }

        boolean isEmpty() {
            return unculled.isEmpty() && faceQuads.isEmpty();
        }

        BlockModelPart toPart(BlockModelPart template) {
            return new StaticBlockModelPart(faceQuads, unculled, template.useAmbientOcclusion(), template.particleSprite());
        }
    }

    private static final class StaticBlockModelPart implements BlockModelPart {
        private final EnumMap<Direction, List<BakedQuad>> faceQuads;
        private final List<BakedQuad> unculled;
        private final boolean useAo;
        private final Sprite particleSprite;

        StaticBlockModelPart(EnumMap<Direction, List<BakedQuad>> faceQuads, List<BakedQuad> unculled,
                             boolean useAo, Sprite particleSprite) {
            this.faceQuads = faceQuads;
            this.unculled = unculled;
            this.useAo = useAo;
            this.particleSprite = particleSprite;
        }

        @Override
        public List<BakedQuad> getQuads(Direction face) {
            if (face == null) return unculled;
            List<BakedQuad> quads = faceQuads.get(face);
            return (quads == null) ? List.of() : quads;
        }

        @Override
        public boolean useAmbientOcclusion() {
            return useAo;
        }

        @Override
        public Sprite particleSprite() {
            return particleSprite;
        }
    }

    private static final class StaticBlockStateModel implements BlockStateModel {
        private final List<BlockModelPart> parts;
        private final Sprite particleSprite;

        StaticBlockStateModel(List<BlockModelPart> parts, Sprite particleSprite) {
            this.parts = parts;
            this.particleSprite = particleSprite;
        }

        @Override
        public void addParts(Random random, List<BlockModelPart> out) {
            out.addAll(parts);
        }

        @Override
        public Sprite particleSprite() {
            return particleSprite;
        }
    }
}
