#include "chunkmesher.hpp"

#include <array>
#include <memory>

#include "block.hpp"
#include "chunk.hpp"
#include "chunkmanager.hpp"
#include "globals.hpp"
#include "renderer.hpp"
#include "spacefilling.hpp"
#include "utils.hpp"

namespace chunkmesher{

oneapi::tbb::concurrent_queue<MeshData*> MeshDataQueue;

oneapi::tbb::concurrent_queue<MeshData*>& getMeshDataQueue(){ return MeshDataQueue; }
    
void mesh(Chunk::Chunk* chunk)
{
    MeshData* mesh_data;
    if(!MeshDataQueue.try_pop(mesh_data)) return;

    /*
     * Taking inspiration from 0fps and the jme3 porting at
     * https://github.com/roboleary/GreedyMesh/blob/master/src/mygame/Main.java
     *
     * By carefully re-reading the code and the blog post, I've come to the
     * realization that I wrote something very similar yet a lot messier (and
     * uglier) back in
     * https://github.com/EmaMaker/voxel-engine-jme3/blob/master/src/voxelengine
     * /world/Chunk.java
     *
     * Reading roboleary's impl. I've learned how to optimize having to loop
     * across different planes everytime I change dimension without having to
     * write 3 separate 3-nested-for-loops
     */

    // Cleanup previous data
    mesh_data->numVertices = 0;
    mesh_data->chunk = chunk;
    mesh_data->vertices.clear();
    mesh_data->extents.clear();
    mesh_data->texinfo.clear();

    // Abort if chunk is empty
    if(chunk->getState(Chunk::CHUNK_STATE_EMPTY)){
	chunk->setState(Chunk::CHUNK_STATE_MESHED, true);
	renderer::getMeshDataQueue().push(mesh_data);
	return;
    }

    // convert tree to array since it is easier to work with it
    int length{0};
    std::unique_ptr<Block[]> blocks = chunk->getBlocksArray(&length);
    if(length == 0) {
	chunk->setState(Chunk::CHUNK_STATE_MESHED, true);
	renderer::getMeshDataQueue().push(mesh_data);
	return;
    }
    
    int k, l, u, v, w, h, n, j, i;
    int x[]{0, 0, 0};
    int q[]{0, 0, 0};
    int du[]{0, 0, 0};
    int dv[]{0, 0, 0};

    std::array<Block, CHUNK_SIZE * CHUNK_SIZE> mask;
    for (bool backFace = true, b = false; b != backFace; backFace = backFace && b, b = !b)
    {
        // iterate over 3 dimensions
        for (int dim = 0; dim < 3; dim++)
        {
            // offsets of other 2 axes
            u = (dim + 1) % 3;
            v = (dim + 2) % 3;

            x[0] = 0;
            x[1] = 0;
            x[2] = 0;

            q[0] = 0;
            q[1] = 0;
            q[2] = 0;
            q[dim] = 1; // easily mark which dimension we are comparing
                        // voxels
                        // on

            for (x[dim] = -1; x[dim] < CHUNK_SIZE;)
            {
                n = 0;

                for (x[v] = 0; x[v] < CHUNK_SIZE; x[v]++)
                {
                    for (x[u] = 0; x[u] < CHUNK_SIZE; x[u]++)
                    {
			Block b1, b2;
			if(x[dim] >= 0) b1 = blocks[HILBERT_XYZ_ENCODE[x[0]][x[1]][x[2]]];
			else{
			    int cx = chunk->getPosition().x*CHUNK_SIZE;
			    int cy = chunk->getPosition().y*CHUNK_SIZE;
			    int cz = chunk->getPosition().z*CHUNK_SIZE;

			    int bx = cx+x[0];
			    int by = cy+x[1];
			    int bz = cz+x[2];

			    b1 = chunkmanager::getBlockAtPos(bx, by, bz);
			}

			if(x[dim] < CHUNK_SIZE - 1) b2 = blocks[HILBERT_XYZ_ENCODE[x[0] + q[0]][x[1]
			    + q[1]][x[2] + q[2]]];
			else{
			    int cx = chunk->getPosition().x*CHUNK_SIZE;
			    int cy = chunk->getPosition().y*CHUNK_SIZE;
			    int cz = chunk->getPosition().z*CHUNK_SIZE;

			    int bx = cx+x[0] + q[0];
			    int by = cy+x[1] + q[1];
			    int bz = cz+x[2] + q[2];

			    b2 = chunkmanager::getBlockAtPos(bx, by, bz);
			}

			// Compute the mask
			// Checking if b1==b2 is needed to generate a single quad
			// The else case provides face culling for adjacent solid faces
			// Checking for NULLBLK avoids creating empty faces if nearby chunk was not
			// yet generated
			mask[n++] = b1 == b2 ? Block::NULLBLK
                                    : backFace ? b1 == Block::NULLBLK || b1 == Block::AIR ? b2 : Block::NULLBLK
                                    : b2 == Block::NULLBLK || b2 == Block::AIR ? b1 : Block::NULLBLK;
                    }
                }

                x[dim]++;
                n = 0;
                // Actually generate the mesh from the mask. This is the same thing I used in my old crappy voxel engine
                for (j = 0; j < CHUNK_SIZE; j++)
                {
                    for (i = 0; i < CHUNK_SIZE;)
                    {

                        if (mask[n] != Block::NULLBLK)
                        {
                            // First compute the width
                            for (w = 1; i + w < CHUNK_SIZE && mask[n + w] != Block::NULLBLK && mask[n] == mask[n + w]; w++)
                            {
                            }

                            bool done = false;
                            for (h = 1; j + h < CHUNK_SIZE; h++)
                            {
                                for (k = 0; k < w; k++)
                                {
                                    if (mask[n + k + h * CHUNK_SIZE] == Block::NULLBLK || mask[n + k + h * CHUNK_SIZE] != mask[n])
                                    {
                                        done = true;
                                        break;
                                    }
                                }
                                if (done)
                                    break;
                            }

                            if (mask[n] != Block::AIR)
                            {
                                x[u] = i;
                                x[v] = j;

                                du[0] = 0;
                                du[1] = 0;
                                du[2] = 0;
                                du[u] = w;

                                dv[0] = 0;
                                dv[1] = 0;
                                dv[2] = 0;
                                dv[v] = h;

				// bottom left
				mesh_data->vertices.push_back(x[0]); //bottomLeft.x
				mesh_data->vertices.push_back(x[1]); //bottomLeft.y
				mesh_data->vertices.push_back(x[2]); //bottomLeft.z

				// extents, use normals for now
				mesh_data->extents.push_back(du[0] + dv[0]);
				mesh_data->extents.push_back(du[1] + dv[1]);
				mesh_data->extents.push_back(du[2] + dv[2]);

				mesh_data->texinfo.push_back(backFace ? 0.0 : 1.0);
				mesh_data->texinfo.push_back((int)(mask[n]) - 2);
				mesh_data->numVertices++;
                            }

                            for (l = 0; l < h; ++l)
                            {
                                for (k = 0; k < w; ++k)
                                {
                                    mask[n + k + l * CHUNK_SIZE] = Block::NULLBLK;
                                }
                            }

                            /*
                             * And then finally increment the counters and
                             * continue
                             */
                            i += w;
                            n += w;
                        }
                        else
                        {
                            i++;
                            n++;
                        }
                    }
                }
            }
        }
    }

    chunk->setState(Chunk::CHUNK_STATE_MESHED, true);
    renderer::getMeshDataQueue().push(mesh_data);
}

void sendtogpu(MeshData* mesh_data)
{
    if (mesh_data->numVertices > 0)
    {
	if(mesh_data->chunk->VAO == 0) mesh_data->chunk->createBuffers();

	// bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
	glBindVertexArray(mesh_data->chunk->VAO);

	// position attribute
	glBindBuffer(GL_ARRAY_BUFFER, mesh_data->chunk->VBO);
	glBufferData(GL_ARRAY_BUFFER, mesh_data->vertices.size() * sizeof(GLfloat), &(mesh_data->vertices[0]), GL_STATIC_DRAW);
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *)0);
	glEnableVertexAttribArray(0);

	// normal attribute
	glBindBuffer(GL_ARRAY_BUFFER, mesh_data->chunk->extentsBuffer);
	glBufferData(GL_ARRAY_BUFFER, mesh_data->extents.size() * sizeof(GLfloat), &(mesh_data->extents[0]), GL_STATIC_DRAW);
	glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *)(0));
	glEnableVertexAttribArray(1);

	// texcoords attribute
	glBindBuffer(GL_ARRAY_BUFFER, mesh_data->chunk->texinfoBuffer);
	glBufferData(GL_ARRAY_BUFFER, mesh_data->texinfo.size() * sizeof(GLfloat), &(mesh_data->texinfo[0]), GL_STATIC_DRAW);
	glEnableVertexAttribArray(2);
	glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void *)0);

	glBindVertexArray(0);

	// save the number of indices of the mesh, it is needed later for drawing
	mesh_data->chunk->numVertices = (GLuint)(mesh_data->numVertices);

	// once data has been sent to the GPU, it can be cleared from system RAM
	mesh_data->vertices.clear();
	mesh_data->extents.clear();
	mesh_data->texinfo.clear();
    }
    
    // mark the chunk mesh has loaded on GPU
    mesh_data->chunk->setState(Chunk::CHUNK_STATE_MESH_LOADED, true);
}
};
