package codechicken.lib.render.buffer;

import codechicken.lib.vec.Matrix4;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.datafixers.util.Pair;
import net.minecraft.client.renderer.BufferBuilder;
import net.minecraft.client.renderer.RenderState;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.vertex.VertexFormat;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL15;

import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.function.BiConsumer;

import static org.lwjgl.opengl.GL15.GL_ARRAY_BUFFER;
import static org.lwjgl.opengl.GL15.GL_STATIC_DRAW;

/**
 * A RenderType that is backed by a VertexBufferObject.
 * This has a few unique limited applications, mainly related to Item rendering,
 * although it _can_ be used for blocks.
 * Drawbacks:
 * - Doesn't support Overlay rendering. (block breaking)
 * - If LightMap support is required, it must be removed from the VertexFormat.
 * <p>
 * Created by covers1624 on 25/5/20.
 */
public class VBORenderType extends DelegateRenderType {

    private final BiConsumer<VertexFormat, BufferBuilder> factory;
    private int bufferId = -1;
    private int count;

    /**
     * Create a new VBORenderType, delegates render state setup
     * to the provided parent, also uses the parents VertexFormat.
     *
     * @param parent  The parent, for state setup and buffer VertexFormat.
     * @param factory The Factory used to fill the BufferBuilder with data.
     */
    public VBORenderType(RenderType parent, BiConsumer<VertexFormat, BufferBuilder> factory) {
        this(parent, parent.getVertexFormat(), factory);
    }

    /**
     * Create a new VBORenderType, delegates render state setup
     * to the provided parent, Uses the specified VertexFormat.
     *
     * @param parent       The parent, for state setup.
     * @param bufferFormat The VertexFormat to use.
     * @param factory      The Factory used to fill the BufferBuilder with data.
     */
    public VBORenderType(RenderType parent, VertexFormat bufferFormat, BiConsumer<VertexFormat, BufferBuilder> factory) {
        super(parent, bufferFormat);
        this.factory = factory;
    }

    /**
     * Can be called runtime to have the Buffer rebuilt,
     * doing so has very limited applications and is not recommended.
     */
    public void rebuild() {
        if (bufferId == -1) {
            bufferId = GL15.glGenBuffers();
        }

        BufferBuilder builder = new BufferBuilder(getBufferSize());
        builder.begin(getDrawMode(), getVertexFormat());
        factory.accept(getVertexFormat(), builder);
        builder.finishDrawing();
        Pair<BufferBuilder.DrawState, ByteBuffer> pair = builder.getNextBuffer();
        ByteBuffer buffer = pair.getSecond();
        count = buffer.remaining() / getVertexFormat().getSize();

        GL15.glBindBuffer(GL_ARRAY_BUFFER, bufferId);
        GL15.glBufferData(GL_ARRAY_BUFFER, buffer, GL_STATIC_DRAW);
        GL15.glBindBuffer(GL_ARRAY_BUFFER, 0);
    }

    /**
     * A soft clone of this VBORenderType, using the provided Matrix4.
     *
     * @param matrix The matrix.
     * @return The soft clone.
     */
    public MatrixVBORenderType withMatrix(Matrix4 matrix) {
        return new MatrixVBORenderType(this, matrix);
    }

    private void render() {
        if (bufferId == -1) {
            rebuild();
        }
        GL15.glBindBuffer(GL_ARRAY_BUFFER, bufferId);
        getVertexFormat().setupBufferState(0);
        GL15.glDrawArrays(getDrawMode(), 0, count);
        getVertexFormat().clearBufferState();
        GL15.glBindBuffer(GL_ARRAY_BUFFER, 0);
    }

    @Override
    public void finish(BufferBuilder buffer, int cameraX, int cameraY, int cameraZ) {
        buffer.finishDrawing();//We dont care about this, but we need to tell it to finish.
        buffer.getNextBuffer();

        setupRenderState();
        render();
        clearRenderState();
    }

    public static class MatrixVBORenderType extends DelegateRenderType {

        private final LinkedList<RenderState> states = new LinkedList<>();
        private final VBORenderType parent;
        private final Matrix4 matrix;
        private boolean hasLightMap = false;

        private int packedLight;

        public MatrixVBORenderType(VBORenderType parent, Matrix4 matrix) {
            super(parent);
            this.parent = parent;
            this.matrix = matrix.copy();
        }

        /**
         * Enables LightMap support.
         *
         * @param packedLight The PackedLightMap value.
         * @return The same RenderType.
         */
        public MatrixVBORenderType withLightMap(int packedLight) {
            hasLightMap = true;
            this.packedLight = packedLight;
            return this;
        }

        /**
         * An extra RenderState to be applied, may be a RenderType.
         *
         * @param state The state.
         * @return The same RenderType.
         */
        public MatrixVBORenderType withState(RenderState state) {
            states.add(state);
            return this;
        }

        @Override
        public void setupRenderState() {
            super.setupRenderState();
            states.forEach(RenderState::setupRenderState);
            RenderSystem.pushMatrix();
            matrix.glApply();
            if (hasLightMap) {
                RenderSystem.glMultiTexCoord2f(GL13.GL_TEXTURE2, packedLight & 0xFFFF, packedLight >>> 16);
            }
        }

        @Override
        public void finish(BufferBuilder buffer, int cameraX, int cameraY, int cameraZ) {
            buffer.finishDrawing();//We dont care about this, but we need to tell it to finish.
            buffer.getNextBuffer();

            setupRenderState();
            parent.render();
            clearRenderState();
        }

        @Override
        public void clearRenderState() {
            RenderSystem.popMatrix();
            states.forEach(RenderState::clearRenderState);
            super.clearRenderState();
        }

        @Override
        public boolean equals(Object other) {
            return other == this;
        }

        @Override
        public int hashCode() {
            return System.identityHashCode(this);
        }
    }
}