package electrosphere.renderer.meshgen;

import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

import org.joml.Vector2f;
import org.joml.Vector3f;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL40;

import electrosphere.client.block.BlockChunkData;
import electrosphere.engine.Globals;
import electrosphere.entity.state.collidable.MultiShapeTriGeomData;
import electrosphere.entity.state.collidable.TriGeomData;
import electrosphere.renderer.model.Material;
import electrosphere.renderer.model.Mesh;
import electrosphere.renderer.model.Model;

/**
 * Generates a model for a block
 */
public class BlockMeshgen {

    /**
     * The indices to draw faces on cubes
     */
    static final int[] CUBE_INDICES = new int[]{
        //Top
        2, 6, 7,
        2, 3, 7,

        //Bottom
        0, 4, 5,
        0, 1, 5,

        //Left
        0, 2, 6,
        0, 4, 6,

        //Right
        1, 3, 7,
        1, 5, 7,

        //Front
        0, 2, 3,
        0, 1, 3,

        //Back
        4, 6, 7,
        4, 5, 7
    };

    /**
     * Position of the sampler data in the buffer
     */
    static final int SAMPLER_SHADER_POSITION = 5;

    /**
     * The size of the sampler data per-vertex
     */
    static final int SAMPLER_DATA_SIZE = 1;


    /**
     * Calculates the quad meshes for the provided chunk data
     * @param quadMeshes The quad mesh list to fill
     * @param chunkData The chunk data
     */
    protected static void fillQuadMeshes(List<QuadMesh> quadMeshes, BlockChunkData chunkData){
        for(int z = 0; z < BlockChunkData.CHUNK_DATA_WIDTH; z++){
            for(int x = 0; x < BlockChunkData.CHUNK_DATA_WIDTH; x++){
                QuadMesh currentQuad = null;
                for(int y = 0; y < BlockChunkData.CHUNK_DATA_WIDTH; y++){
                    if(chunkData.isEmpty(x, y, z)){
                        if(currentQuad == null){
                            continue;
                        } else {
                            currentQuad.h = y - currentQuad.y;
                            //check if should merge with previous quad
                            for(QuadMesh prevMesh : quadMeshes){
                                if(prevMesh.x + prevMesh.w == currentQuad.x && prevMesh.y == currentQuad.y && prevMesh.h == currentQuad.h && prevMesh.z == currentQuad.z){
                                    prevMesh.w = prevMesh.w + 1;
                                    currentQuad = null;
                                    break;
                                }
                            }
                            if(currentQuad != null){
                                quadMeshes.add(currentQuad);
                            }
                            currentQuad = null;
                        }
                    } else {
                        if(currentQuad == null){
                            currentQuad = new QuadMesh();
                            currentQuad.x = x;
                            currentQuad.y = y;
                            currentQuad.z = z;
                            currentQuad.w = 1;
                            currentQuad.h = 1;
                            currentQuad.type = chunkData.getType(x, y, z);
                        } else {
                            continue;
                        }
                    }
                }
            }
        }
    }

    /**
     * Meshes a box
     * @param verts The list of verts to store into
     * @param normals The list of normals to store into
     * @param uvs The list of uvs to store into
     * @param indices The list of indices to store into
     * @param samplers The sampler for a given vertex
     * @param quad The quad
     * @param depth The depth of the box
     * @param blockType The type of block
     * @param indexOffset The offset for the indices that will be added (ie, if there are already vertices in the verts list, pass in the size of the vert list)
     */
    protected static void meshifyBox(List<Vector3f> verts, List<Vector3f> normals, List<Vector2f> uvs, List<Integer> indices, List<Integer> samplers, QuadMesh quad, int depth, int blockType, int indexOffset){
        //
        //face 1
        //
        int samplerIndex = Globals.blockTextureAtlas.getVoxelTypeOffset(blockType);

        //verts
        verts.add(new Vector3f(quad.x,            quad.y,            quad.z).mul(BlockChunkData.BLOCK_SIZE_MULTIPLIER));
        verts.add(new Vector3f(quad.x + quad.w,  quad.y,            quad.z).mul(BlockChunkData.BLOCK_SIZE_MULTIPLIER));
        verts.add(new Vector3f(quad.x,            quad.y + quad.h,  quad.z).mul(BlockChunkData.BLOCK_SIZE_MULTIPLIER));
        verts.add(new Vector3f(quad.x + quad.w,  quad.y + quad.h,  quad.z).mul(BlockChunkData.BLOCK_SIZE_MULTIPLIER));
        //indices
        indices.add(indexOffset + 0);
        indices.add(indexOffset + 2);
        indices.add(indexOffset + 3);
        indices.add(indexOffset + 0);
        indices.add(indexOffset + 3);
        indices.add(indexOffset + 1);
        //normals
        normals.add(new Vector3f(0,0,-1));
        normals.add(new Vector3f(0,0,-1));
        normals.add(new Vector3f(0,0,-1));
        normals.add(new Vector3f(0,0,-1));
        //uvs
        uvs.add(new Vector2f(     0,      0));
        uvs.add(new Vector2f(quad.w,      0));
        uvs.add(new Vector2f(     0, quad.h));
        uvs.add(new Vector2f(quad.w, quad.h));
        //samplers
        samplers.add(samplerIndex);
        samplers.add(samplerIndex);
        samplers.add(samplerIndex);
        samplers.add(samplerIndex);

        //
        //face 2
        //

        //verts
        verts.add(new Vector3f(quad.x,  quad.y,            quad.z        ).mul(BlockChunkData.BLOCK_SIZE_MULTIPLIER));
        verts.add(new Vector3f(quad.x,  quad.y,            quad.z + depth).mul(BlockChunkData.BLOCK_SIZE_MULTIPLIER));
        verts.add(new Vector3f(quad.x,  quad.y + quad.h,   quad.z        ).mul(BlockChunkData.BLOCK_SIZE_MULTIPLIER));
        verts.add(new Vector3f(quad.x,  quad.y + quad.h,   quad.z + depth).mul(BlockChunkData.BLOCK_SIZE_MULTIPLIER));
        //indices
        indices.add(indexOffset + 4);
        indices.add(indexOffset + 7);
        indices.add(indexOffset + 6);
        indices.add(indexOffset + 4);
        indices.add(indexOffset + 5);
        indices.add(indexOffset + 7);
        //normals
        normals.add(new Vector3f(-1,0,0));
        normals.add(new Vector3f(-1,0,0));
        normals.add(new Vector3f(-1,0,0));
        normals.add(new Vector3f(-1,0,0));
        //uvs
        uvs.add(new Vector2f(    0,      0));
        uvs.add(new Vector2f(depth,      0));
        uvs.add(new Vector2f(    0, quad.h));
        uvs.add(new Vector2f(depth, quad.h));
        //samplers
        samplers.add(samplerIndex);
        samplers.add(samplerIndex);
        samplers.add(samplerIndex);
        samplers.add(samplerIndex);

        //
        //face 3
        //

        //verts
        verts.add(new Vector3f(quad.x,           quad.y,   quad.z        ).mul(BlockChunkData.BLOCK_SIZE_MULTIPLIER));
        verts.add(new Vector3f(quad.x,           quad.y,   quad.z + depth).mul(BlockChunkData.BLOCK_SIZE_MULTIPLIER));
        verts.add(new Vector3f(quad.x + quad.w,  quad.y,   quad.z        ).mul(BlockChunkData.BLOCK_SIZE_MULTIPLIER));
        verts.add(new Vector3f(quad.x + quad.w,  quad.y,   quad.z + depth).mul(BlockChunkData.BLOCK_SIZE_MULTIPLIER));
        //indices
        indices.add(indexOffset +  8);
        indices.add(indexOffset + 10);
        indices.add(indexOffset + 11);
        indices.add(indexOffset +  9);
        indices.add(indexOffset +  8);
        indices.add(indexOffset + 11);
        //normals
        normals.add(new Vector3f(0,-1,0));
        normals.add(new Vector3f(0,-1,0));
        normals.add(new Vector3f(0,-1,0));
        normals.add(new Vector3f(0,-1,0));
        //uvs
        uvs.add(new Vector2f(    0,      0));
        uvs.add(new Vector2f(depth,      0));
        uvs.add(new Vector2f(    0, quad.w));
        uvs.add(new Vector2f(depth, quad.w));
        //samplers
        samplers.add(samplerIndex);
        samplers.add(samplerIndex);
        samplers.add(samplerIndex);
        samplers.add(samplerIndex);

        //
        //face 4
        //

        //verts
        verts.add(new Vector3f(quad.x,            quad.y,            quad.z + depth).mul(BlockChunkData.BLOCK_SIZE_MULTIPLIER));
        verts.add(new Vector3f(quad.x + quad.w,   quad.y,            quad.z + depth).mul(BlockChunkData.BLOCK_SIZE_MULTIPLIER));
        verts.add(new Vector3f(quad.x,            quad.y + quad.h,   quad.z + depth).mul(BlockChunkData.BLOCK_SIZE_MULTIPLIER));
        verts.add(new Vector3f(quad.x + quad.w,   quad.y + quad.h,   quad.z + depth).mul(BlockChunkData.BLOCK_SIZE_MULTIPLIER));
        //indices
        indices.add(indexOffset + 12);
        indices.add(indexOffset + 15);
        indices.add(indexOffset + 14);
        indices.add(indexOffset + 12);
        indices.add(indexOffset + 13);
        indices.add(indexOffset + 15);
        //normals
        normals.add(new Vector3f(0,0,1));
        normals.add(new Vector3f(0,0,1));
        normals.add(new Vector3f(0,0,1));
        normals.add(new Vector3f(0,0,1));
        //uvs
        uvs.add(new Vector2f(     0,      0));
        uvs.add(new Vector2f(quad.w,      0));
        uvs.add(new Vector2f(     0, quad.h));
        uvs.add(new Vector2f(quad.w, quad.h));
        //samplers
        samplers.add(samplerIndex);
        samplers.add(samplerIndex);
        samplers.add(samplerIndex);
        samplers.add(samplerIndex);


        //
        //face 5
        //

        //verts
        verts.add(new Vector3f(quad.x + quad.w,  quad.y,            quad.z        ).mul(BlockChunkData.BLOCK_SIZE_MULTIPLIER));
        verts.add(new Vector3f(quad.x + quad.w,  quad.y,            quad.z + depth).mul(BlockChunkData.BLOCK_SIZE_MULTIPLIER));
        verts.add(new Vector3f(quad.x + quad.w,  quad.y + quad.h,   quad.z        ).mul(BlockChunkData.BLOCK_SIZE_MULTIPLIER));
        verts.add(new Vector3f(quad.x + quad.w,  quad.y + quad.h,   quad.z + depth).mul(BlockChunkData.BLOCK_SIZE_MULTIPLIER));
        //indices
        indices.add(indexOffset + 16);
        indices.add(indexOffset + 18);
        indices.add(indexOffset + 19);
        indices.add(indexOffset + 16);
        indices.add(indexOffset + 19);
        indices.add(indexOffset + 17);
        //normals
        normals.add(new Vector3f(1,0,0));
        normals.add(new Vector3f(1,0,0));
        normals.add(new Vector3f(1,0,0));
        normals.add(new Vector3f(1,0,0));
        //uvs
        uvs.add(new Vector2f(    0,      0));
        uvs.add(new Vector2f(depth,      0));
        uvs.add(new Vector2f(    0, quad.h));
        uvs.add(new Vector2f(depth, quad.h));
        //samplers
        samplers.add(samplerIndex);
        samplers.add(samplerIndex);
        samplers.add(samplerIndex);
        samplers.add(samplerIndex);


        //
        //face 6
        //

        //verts
        verts.add(new Vector3f(quad.x,           quad.y + quad.h,   quad.z        ).mul(BlockChunkData.BLOCK_SIZE_MULTIPLIER));
        verts.add(new Vector3f(quad.x,           quad.y + quad.h,   quad.z + depth).mul(BlockChunkData.BLOCK_SIZE_MULTIPLIER));
        verts.add(new Vector3f(quad.x + quad.w,  quad.y + quad.h,   quad.z        ).mul(BlockChunkData.BLOCK_SIZE_MULTIPLIER));
        verts.add(new Vector3f(quad.x + quad.w,  quad.y + quad.h,   quad.z + depth).mul(BlockChunkData.BLOCK_SIZE_MULTIPLIER));
        //indicesindexOffset + 
        indices.add(indexOffset + 20);
        indices.add(indexOffset + 23);
        indices.add(indexOffset + 22);
        indices.add(indexOffset + 20);
        indices.add(indexOffset + 21);
        indices.add(indexOffset + 23);
        //normals
        normals.add(new Vector3f(0,1,0));
        normals.add(new Vector3f(0,1,0));
        normals.add(new Vector3f(0,1,0));
        normals.add(new Vector3f(0,1,0));
        //uvs
        uvs.add(new Vector2f(    0,      0));
        uvs.add(new Vector2f(depth,      0));
        uvs.add(new Vector2f(    0, quad.w));
        uvs.add(new Vector2f(depth, quad.w));
        //samplers
        samplers.add(samplerIndex);
        samplers.add(samplerIndex);
        samplers.add(samplerIndex);
        samplers.add(samplerIndex);
    }



    /**
     * Rasterizes a block chunk data into mesh data
     * @param chunkData The block chunk data
     * @return The mesh data
     */
    public static BlockMeshData rasterize(BlockChunkData chunkData){
        BlockMeshData rVal = new BlockMeshData();

        //calculate quad meshes
        List<QuadMesh> quadMeshes = new LinkedList<QuadMesh>();
        BlockMeshgen.fillQuadMeshes(quadMeshes, chunkData);

        //allocate lists to store mesh data in
        List<Vector3f> verts = new LinkedList<Vector3f>();
        List<Vector3f> normals = new LinkedList<Vector3f>();
        List<Vector2f> uvs = new LinkedList<Vector2f>();
        List<Integer> indices = new LinkedList<Integer>();
        List<Integer> samplers = new LinkedList<Integer>(); //the texture to sample for this quad

        //sort
        Collections.sort(quadMeshes);

        int lastVertexCount = 0;
        int lastFaceCount = 0;

        //generate volumes
        QuadMesh quad1 = null;
        QuadMesh quad2 = null;
        int zEnd = 0;
        for(int i = 0; i < quadMeshes.size();){
            quad1 = quadMeshes.get(i);
            zEnd = 1;
            for(int j = i + 1; j < quadMeshes.size(); j++){
                quad2 = quadMeshes.get(j);
                if(quad1.x == quad2.x && quad1.y == quad2.y && quad1.w == quad2.w && quad1.h == quad2.h && quad1.z + zEnd == quad2.z){
                    zEnd++;
                } else {
                    BlockMeshgen.meshifyBox(verts,normals,uvs,indices,samplers,quad1,zEnd,quad1.type,verts.size());
                    quad1 = quad2;

                    BlockSingleShape blockSingleShape = BlockMeshgen.copyDataToShape(verts,indices,lastVertexCount,lastFaceCount);
                    lastVertexCount = verts.size();
                    lastFaceCount = indices.size();
                    rVal.shapeData.add(blockSingleShape);
                    break;
                }
            }
            i = i + zEnd;
        }
        if(quad1 != null){
            BlockMeshgen.meshifyBox(verts,normals,uvs,indices,samplers,quad1,zEnd,quad1.type,verts.size());
            BlockSingleShape blockSingleShape = BlockMeshgen.copyDataToShape(verts,indices,lastVertexCount,lastFaceCount);
            lastVertexCount = verts.size();
            lastFaceCount = indices.size();
            rVal.shapeData.add(blockSingleShape);
        }
        
        //
        //store in flat arrays
        //

        //verts
        rVal.vertices = new float[verts.size() * 3];
        for(int i = 0; i < verts.size(); i++){
            Vector3f currentVert = verts.get(i);
            rVal.vertices[3 * i + 0] = currentVert.x;
            rVal.vertices[3 * i + 1] = currentVert.y;
            rVal.vertices[3 * i + 2] = currentVert.z;
        }
        rVal.vertBuffer = BufferUtils.createFloatBuffer(rVal.vertices.length);
        rVal.vertBuffer.put(rVal.vertices);

        //faces
        rVal.faceElements = new int[indices.size()];
        for(int i = 0; i < indices.size(); i++){
            rVal.faceElements[i] = indices.get(i);
        }
        rVal.faceBuffer = BufferUtils.createIntBuffer(rVal.faceElements.length);
        rVal.faceBuffer.put(rVal.faceElements);

        //normals
        rVal.normals = new float[normals.size() * 3];
        for(int i = 0; i < normals.size(); i++){
            Vector3f currentNormal = normals.get(i);
            rVal.normals[3 * i + 0] = currentNormal.x;
            rVal.normals[3 * i + 1] = currentNormal.y;
            rVal.normals[3 * i + 2] = currentNormal.z;
        }
        rVal.normalBuffer = BufferUtils.createFloatBuffer(rVal.normals.length);
        rVal.normalBuffer.put(rVal.normals);

        //uvs
        rVal.uvs = new float[uvs.size() * 2];
        for(int i = 0; i < uvs.size(); i++){
            Vector2f currentUV = uvs.get(i);
            rVal.uvs[2 * i + 0] = currentUV.x;
            rVal.uvs[2 * i + 1] = currentUV.y;
        }
        rVal.uvBuffer = BufferUtils.createFloatBuffer(rVal.uvs.length);
        rVal.uvBuffer.put(rVal.uvs);

        //samplers
        rVal.samplers = new int[samplers.size()];
        for(int i = 0; i < samplers.size(); i++){
            rVal.samplers[i] = samplers.get(i);
        }
        rVal.samplerBuffer = BufferUtils.createIntBuffer(rVal.samplers.length);
        rVal.samplerBuffer.put(rVal.samplers);

        return rVal;
    }

    /**
     * Copies vertex and index data from the combined array into a single shape
     * @param verts The list of vertices
     * @param indices The list of indices
     * @param lastVertCount The last vertex position that was copied
     * @param lastFaceCount The last index position that was copied
     * @return The data containing a single shape's worth of geometry data
     */
    private static BlockSingleShape copyDataToShape(List<Vector3f> verts, List<Integer> indices, int lastVertCount, int lastFaceCount){
        BlockSingleShape blockSingleShape = new BlockSingleShape((verts.size() - lastVertCount),(indices.size() - lastFaceCount));
        for(int i = lastVertCount; i < verts.size(); i++){
            Vector3f vert = verts.get(i);
            blockSingleShape.vertices[(i - lastVertCount) * 3 + 0] = vert.x;
            blockSingleShape.vertices[(i - lastVertCount) * 3 + 1] = vert.y;
            blockSingleShape.vertices[(i - lastVertCount) * 3 + 2] = vert.z;
        }
        for(int i = lastFaceCount; i < indices.size(); i++){
            blockSingleShape.faceElements[i - lastFaceCount] = indices.get(i) - lastVertCount;
        }
        return blockSingleShape;
    }

    /**
     * Generates a mesh based on a block mesh data object
     * @param data The block mesh data object
     * @return The mesh
     */
    protected static Mesh generateBlockMesh(BlockMeshData meshData){
        Mesh mesh = new Mesh("blockChunk");


        //
        //  VAO
        //
        mesh.generateVAO();
        
        
        
        
        FloatBuffer vertexArrayBufferData = meshData.vertBuffer;
        FloatBuffer normalArrayBufferData = meshData.normalBuffer;
        FloatBuffer textureArrayBufferData = meshData.uvBuffer;
        IntBuffer elementArrayBufferData = meshData.faceBuffer;
        IntBuffer samplerArrayBufferData = meshData.samplerBuffer;

        

        
        //
        //  Buffer data to GPU
        //
        int elementCount = meshData.faceElements.length;
        try {
            //actually buffer vertices
            if(vertexArrayBufferData.position() > 0){
                vertexArrayBufferData.flip();
                mesh.bufferVertices(vertexArrayBufferData, 3);
            }
            //actually buffer normals
            if(normalArrayBufferData != null && normalArrayBufferData.position() > 0){
                normalArrayBufferData.flip();
                mesh.bufferNormals(normalArrayBufferData, 3);
            }
            //actually buffer UVs
            if(textureArrayBufferData != null && textureArrayBufferData.position() > 0){
                textureArrayBufferData.flip();
                mesh.bufferTextureCoords(textureArrayBufferData, 2);
            }
            //buffer element indices
            if(elementArrayBufferData.position() > 0){
                elementArrayBufferData.flip();
                mesh.bufferFaces(elementArrayBufferData, elementCount);
            }
            //buffer sampler indices
            if(samplerArrayBufferData != null && samplerArrayBufferData.position() > 0){
                samplerArrayBufferData.flip();
                mesh.bufferCustomIntAttribArray(samplerArrayBufferData, SAMPLER_DATA_SIZE, SAMPLER_SHADER_POSITION);
            }
        } catch (NullPointerException ex){
            ex.printStackTrace();
        }

        //bounding sphere logic
        int distance = BlockChunkData.CHUNK_DATA_WIDTH / 2;
        mesh.updateBoundingSphere(
            distance,
            distance,
            distance,
            (float)Math.sqrt(
                distance * distance +
                distance * distance +
                distance * distance
            ));
        
        
        
        GL40.glBindVertexArray(0);
        return mesh;
    }

    /**
     * Generates the model for the block mesh
     * @param chunkData The mesh data
     * @return The model object
     */
    public static Model generateBlockModel(BlockMeshData meshData){
        Model rVal = new Model();
        Mesh m = BlockMeshgen.generateBlockMesh(meshData);
        
        //construct the material for the chunk
        Material groundMat = new Material();
        groundMat.setTexturePointer(Globals.blockTextureAtlas.getSpecular().getTexturePointer());
        groundMat.setNormalTexturePointer(Globals.blockTextureAtlas.getNormal().getTexturePointer());
        m.setMaterial(groundMat);

        //shader logic
        m.setShader(Globals.blockShader);
        m.setParent(rVal);
        
        rVal.getMeshes().add(m);
        rVal.setBoundingSphere(m.getBoundingSphere());

        return rVal;
    }
    
    /**
     * Contains the geom data for a single shape in the block mesh
     */
    public static class BlockSingleShape implements TriGeomData {

        /**
         * The verts
         */
        float[] vertices;
        
        /**
         * The faces
         */
        int[] faceElements;

        /**
         * Constructor
         * @param vertCount The number of verts
         * @param faceCount The number of faces
         */
        public BlockSingleShape(int vertCount, int faceCount){
            vertices = new float[vertCount * 3];
            faceElements = new int[faceCount];
        }

        @Override
        public float[] getVertices() {
            return vertices;
        }

        @Override
        public int[] getFaceElements() {
            return faceElements;
        }

    }

    /**
     * The final rasterization data that is emitted
     */
    public static class BlockMeshData implements TriGeomData, MultiShapeTriGeomData {

        /**
         * Vertex data in array form
         */
        float[] vertices;

        /**
         * Normal data in array form
         */
        float[] normals;

        /**
         * Face data in array form
         */
        int[] faceElements;

        /**
         * UV data in array form
         */
        float[] uvs;

        /**
         * Sampler data in array form
         */
        int[] samplers;

        /**
         * Data broken out by each shape
         */
        List<TriGeomData> shapeData = new LinkedList<TriGeomData>();

        /**
         * Buffer of vertex data
         */
        FloatBuffer vertBuffer;

        /**
         * Buffer of normal data
         */
        FloatBuffer normalBuffer;

        /**
         * Buffer of face data
         */
        IntBuffer faceBuffer;

        /**
         * Buffer of UV data
         */
        FloatBuffer uvBuffer;

        /**
         * Buffer of sampler data
         */
        IntBuffer samplerBuffer;

        @Override
        public float[] getVertices() {
            return vertices;
        }

        @Override
        public int[] getFaceElements() {
            return faceElements;
        }

        @Override
        public Collection<TriGeomData> getData() {
            return shapeData;
        }
    }
    
    /**
     * Intermediary structure used during rasterization
     */
    public static class QuadMesh implements Comparable<QuadMesh> {
        int x;
        int y;
        int z;
        int w;
        int h;
        int type;
        public QuadMesh(){}
        public QuadMesh(int x, int y, int z, int w, int h, int type){
            this.x = x;
            this.y = y;
            this.z = z;
            this.w = w;
            this.h = h;
            this.type = type;
        }

        @Override
        public int compareTo(QuadMesh other) {
            if(this.y != other.y){
                return this.y - other.y;
            }
            if(this.x != other.x){
                return this.x - other.x;
            }
            if(this.w != other.w){
                return other.w - this.w;
            }
            return other.h - this.h;
        }
    }
    
}
