#include <UndergroundGenerator/UndergroundGenerator.hpp>
#include <WorldGenerator/SurvivalWorldGenerator/SurvivalWorldGenerator.hpp>
#include <World/LocalWorld/LocalWorld.hpp>
#include <Utilities/JHash.hpp>

void UndergroundGenerator::getClustersBasedInChunk(glm::ivec3 chunkBase, std::vector<Cluster> &clusters)
{
    for (Ore &ore : ores)
    {
        if (chunkBase.z <= ore.depth)
        {
            uint64_t key = (uint64_t) jhash(chunkBase.x);
            key <<= 32;
            key |= jhash(chunkBase.y);
            key ^= jhash(chunkBase.z);

            GeneratorNode oreNode = ore.node(key);
            int blockID = worldgen.getWorld().getBlockId(ore.blockName);
            int clustersInChunk = ore.minClusterCount + oreNode[0] % (ore.maxClusterCount-ore.minClusterCount+1);

            int i;
            for (i=0; i<clustersInChunk; i++)
            {
                GeneratorNode clusterNode = oreNode(i);
                int clusterSize = ore.minClusterSize + clusterNode.seed % (ore.maxClusterSize-ore.minClusterSize + 1);

                if (clusterSize == 0)
                {
                    continue;
                }

                glm::ivec3 offset;
                offset.x = (clusterNode.seed >> 32) & 0xF;
                offset.y = (clusterNode.seed >> 36) & 0xF;
                offset.z = (clusterNode.seed >> 40) & 0xF;

                clusters.push_back({blockID, clusterSize, chunkBase + offset, clusterNode});
            }
        }
    }
}

void UndergroundGenerator::generateOres(Chunk &chunk)
{
    std::vector<Cluster> clusters;

    // Get the ores based in all neighbouring chunks, and the current one.
    glm::ivec3 offset;
    for (offset.x = -16; offset.x <= 16; offset.x+=16)
    {
        for (offset.y = -16; offset.y <= 16; offset.y+=16)
        {
            for (offset.z = -16; offset.z <= 16; offset.z+=16)
            {
                getClustersBasedInChunk(chunk.base + offset, clusters);
            }
        }
    }

    // Generate the clusters.
    for (const Cluster &cluster : clusters)
    {
        generateCluster(chunk, cluster);
    }
}

bool UndergroundGenerator::isOccupiedByOre(Chunk &chunk, glm::ivec3 pos, ClusterBuildState &state)
{
    glm::ivec3 offset = pos - chunk.base + glm::ivec3(32);
    size_t index = offset.z * 80 * 80 + offset.y * 80 + offset.x;

    return state.occupyBitmap.test(index);
}

void UndergroundGenerator::markOccupiedByOre(Chunk &chunk, glm::ivec3 pos, ClusterBuildState &state)
{
    glm::ivec3 offset = pos - chunk.base + glm::ivec3(32);
    size_t index = offset.z * 80 * 80 + offset.y * 80 + offset.x;

    state.occupyBitmap.set(index);
}

void UndergroundGenerator::generateClusterLayer(Chunk &chunk, const Cluster &cluster, ClusterBuildState &state, int radius)
{
    int radiusSquared = radius * radius;
    int stoneID = worldgen.getWorld().getBlockId("main:stone");

    glm::ivec3 offset;
    for (offset.z = -radius; offset.z <= radius; offset.z++)
    {
        for (offset.y = -radius; offset.y <= radius; offset.y++)
        {
            for (offset.x = -radius; offset.x <= radius; offset.x++)
            {
                glm::ivec3 offsetSquared = offset * offset;
                if (offsetSquared.x + offsetSquared.y + offsetSquared.z > radiusSquared)
                {
                    continue;
                }

                if (isOccupiedByOre(chunk, cluster.base + offset, state))
                {
                    continue;
                }

                markOccupiedByOre(chunk, cluster.base + offset, state);

                glm::ivec3 chunkOffset = cluster.base + offset - chunk.base;
                if ((chunkOffset & glm::ivec3(~0xF)) == glm::ivec3(0))
                {
                    if (chunk.base.z < -64 || std::get<0>(chunk.getBlockAndMeta(chunkOffset)) == stoneID)
                    {
                        chunk.setBlockAndMeta(chunkOffset, cluster.blockID, 0);
                    }
                }

                if (--state.blocksLeft == 0)
                {
                    return;
                }
            }
        }
    }
}

void UndergroundGenerator::generateCluster(Chunk &chunk, const Cluster &cluster)
{
    ClusterBuildState state;
    state.blocksLeft = cluster.size;

    for (int radius = 0; state.blocksLeft != 0; radius++)
    {
        generateClusterLayer(chunk, cluster, state, radius);
    }
}

void UndergroundGenerator::registerOre(
    GeneratorNode::ID oreID,
    int depth,
    int minClusterCount, int maxClusterCount,
    int minClusterSize, int maxClusterSize,
    std::string blockName
)
{
    ores.push_back(Ore());
    Ore &ore = ores.back();

    ore.node = oreNode(oreID);
    ore.depth = depth;
    ore.minClusterCount = minClusterCount;
    ore.maxClusterCount = maxClusterCount;
    ore.minClusterSize = minClusterSize;
    ore.maxClusterSize = maxClusterSize;
    ore.blockName = blockName;
}

UndergroundGenerator::UndergroundGenerator(SurvivalWorldGenerator &worldgen) :
    worldgen(worldgen),
    oreNode(worldgen.getRootNode()(0xc954536927ee68cf))
{
    registerOre(
        0x57fae252efd2007b,                             // nodeID
        0,                                              // depth
        2,                                              // minClusterCount
        3,                                              // maxClusterCount
        8,                                              // minClusterSize
        16,                                             // maxClusterSize
        "main:stoneCopperOre"
    );

    registerOre(
        0xddde98dd47cf8d8b,                             // nodeID
        -32,                                            // depth
        3,                                              // minClusterCount
        3,                                              // maxClusterCount
        10,                                             // minClusterSize
        16,                                             // maxClusterSize
        "main:ironOre"
    );

    registerOre(
        0xe964e0d120e63d6b,                             // nodeID
        0,                                              // depth
        2,                                              // minClusterCount
        2,                                              // maxClusterCount
        2,                                              // minClusterSize
        5,                                              // maxClusterSize
        "main:dirt"
    );

    registerOre(
        0x829dfb9681febf5b,                             // nodeID
        0,                                              // depth
        2,                                              // minClusterCount
        2,                                              // maxClusterCount
        8,                                              // minClusterSize
        16,                                             // maxClusterSize
        "main:coalOre"
    );

    registerOre(
        0x7c25e8d64e138fc1,                             // nodeID
        -48,                                            // depth
        1,                                              // minClusterCount
        1,                                              // maxClusterCount
        4,                                              // minClusterSize
        6,                                              // maxClusterSize
        "main:leadOre"
    );

    registerOre(
        0xdcb107753ad48ce1,                             // nodeID
        0,                                              // depth
        3,                                              // minClusterCount
        3,                                              // maxClusterCount
        10,                                             // minClusterSize
        16,                                             // maxClusterSize
        "main:stoneTinOre"
    );

    registerOre(
        0x315b3e80343c8298,                             // nodeID
        -64,                                            // depth
        3,                                              // minClusterCount
        3,                                              // maxClusterCount
        3,                                              // minClusterSize
        6,                                              // maxClusterSize
        "main:siliconOre"
    );

    registerOre(
        0x066167eca0da8a40,                             // nodeID
        -64,                                            // depth
        1,                                              // minClusterCount
        2,                                              // maxClusterCount
        2,                                              // minClusterSize
        4,                                              // maxClusterSize
        "main:silverOre"
    );

    registerOre(
        0x6c30a45baae396a9,                             // nodeID
        -144,                                           // depth
        1,                                              // minClusterCount
        1,                                              // maxClusterCount
        3,                                              // minClusterSize
        10,                                              // maxClusterSize
        "main:uraniumOreMantle"
    );

    registerOre(
        0xf1f3418b51c891cb,                             // nodeID
        -80,                                            // depth
        0,                                              // minClusterCount
        2,                                              // maxClusterCount
        1,                                              // minClusterSize
        4,                                              // maxClusterSize
        "main:diamondOreX"
    );

    registerOre(
        0x904dda25662cec5e,                             // nodeID
        -48,                                            // depth
        2,                                              // minClusterCount
        2,                                              // maxClusterCount
        4,                                              // minClusterSize
        8,                                              // maxClusterSize
        "main:nickelOre"
    );

    registerOre(
        0xe175f1d390b4d436,                             // nodeID
        -64,                                            // depth
        1,                                              // minClusterCount
        1,                                              // maxClusterCount
        2,                                              // minClusterSize
        6,                                              // maxClusterSize
        "main:goldOre"
    );
}

void UndergroundGenerator::generate(Chunk &chunk)
{
    int stoneID = worldgen.getWorld().getBlockId("main:stone");
    int marbleID = worldgen.getWorld().getBlockId("main:stoneMarble");

    if (chunk.base.z > -16)
    {
        return;
    }

    uint64_t key1 = ((uint64_t) (uint32_t) (chunk.base.x >> 5) << 32) | (uint64_t) (uint32_t) (chunk.base.y >> 5);
    uint64_t key2 = (uint64_t) (uint32_t) (chunk.base.z >> 5);

    bool isMarble = chunk.base.z >= -128 && chunk.base.z < -16 &&
        (oreNode(key1)[key2] % 100) < 20;

    glm::ivec3 pos;
    if (chunk.base.z != -16)
    {
        for (pos.x=0; pos.x<16; pos.x++)
        {
            for (pos.y=0; pos.y<16; pos.y++)
            {
                for (pos.z=0; pos.z<16; pos.z++)
                {
                    chunk.setBlockAndMeta(pos, isMarble ? marbleID : stoneID, 0);
                }
            }
        }
    }

    // Generate ores on non-marble chunks.
    if (!isMarble)
    {
        generateOres(chunk);
    }
}