#include <TerrainGenerator/TerrainGenerator.hpp>
#include <Utilities/JHash.hpp>
#include <Utilities/Utilities.hpp>
#include <WorldGenerator/SurvivalWorldGenerator/SurvivalWorldGenerator.hpp>
#include <Perlin2D/Perlin2D.hpp>
#include <World/LocalWorld/LocalWorld.hpp>
#include <Bitmap/Bitmap.hpp>
#include <cmath>

std::vector<std::string> TerrainGenerator::clayColorBands = {
    "brown", "red", "redOrange", "darkPink", "darkRed", "darkBrown",
};

TerrainGenerator::TerrainGrid::TerrainGrid() : data(256 * 256, Cell{}) {}

void TerrainGenerator::computeGridAt(TerrainGrid &grid, int regionX, int regionY)
{
    int x, y;
    for (x=regionX; x<regionX+256; x++)
    {
        for (y=regionY; y<regionY+256; y++)
        {
            float bumpiness = 0.5 * (bumpinessNoise.sample(x, y, 6) + 1.0);
            bumpiness = 0.1f + bumpiness * 3.5;

            float steepness = 0.5 * (steepnessNoise.sample(x, y, 1) + 1.0);
            steepness *= 4.0f;

            float subtleSteepness = 0.5 * (subtleSteepnessNoise.sample(x, y, 1) + 1.0);
            subtleSteepness *= 2.0f;

            float height = 8.0f * terrainNoise.sampleFractal(x, y, 6, 0.4f * bumpiness, bumpiness);

            height = erode(height, steepness, 64.0f);
            height = erode(height, subtleSteepness, 4.0f);

            if (height < 0.0)
            {
                height /= 5.0;
            }

            height += 4.0f;

            Cell &cell = grid.data[(y-regionY) * 256 + (x-regionX)];
            cell.height = (int) std::floor(height);
            cell.temperature = 0.5f * (temperatureNoise.sample(x, y, temperatureOrder) + 1.0f);
            cell.humidity = 0.5f * (humidityNoise.sample(x, y, humidityOrder) + 1.0f);
            cell.fertility = 0.5f * (fertilityNoise.sample(x, y, fertilityOrder) + 1.0f);
            cell.bumpiness = bumpiness;

            uint32_t biasSeed = permutationNode[jhash(x ^ 0x34eee633) ^ jhash(y ^ 0x5622eeff)];
            glm::vec3 noise = glm::vec3(biasSeed & 0xFF, (biasSeed >> 8) & 0xFF, (biasSeed >> 16) & 0xFF) / glm::vec3(128.0f) - glm::vec3(1.0f);
            float biasValue = 0.5f * (biasNoise.sample(x, y, biasOrder) + 1.0f);
            biasValue = 0.0025f + 0.008f * biasValue;
            glm::vec3 bias = noise * biasValue;

            cell.temperature += bias.x;
            cell.humidity += bias.y;
            cell.fertility += bias.z;

            cell.topID = worldgen.getWorld().getBlockId("main:grass");
            cell.groundID = worldgen.getWorld().getBlockId("main:dirt");
            cell.underLakeID = worldgen.getWorld().getBlockId("main:clay");
            cell.fluidBlockID = worldgen.getWorld().getBlockId("main:waterBlock");

            if (cell.fertility < 0.3)
            {
                cell.topID = cell.groundID;

                if (cell.humidity > 0.4)
                {
                    cell.topID = cell.groundID = worldgen.getWorld().getBlockId("main:clay");
                }
            }

            if (cell.height > 300)
            {
                cell.topID = cell.groundID = worldgen.getWorld().getBlockId("main:stone");
            }

            if (cell.height > 48)
            {
                if (cell.fertility < 0.5)
                {
                    if (steepness > 2.0 && subtleSteepness > 1.0 && cell.height < 128)
                    {
                        cell.topID = cell.groundID = worldgen.getWorld().getBlockId("main:stoneLimestone");
                    }
                    else
                    {
                        cell.topID = cell.groundID = worldgen.getWorld().getBlockId("main:stone");
                    }
                }
                
                if (cell.temperature > 0.5)
                {
                    cell.topID = cell.groundID = worldgen.getWorld().getBlockId("main:stoneBasalt");
                }
            }

            if (cell.height > 64 && cell.fertility < 0.5)
            {
                if (steepness < 2.0)
                {
                    cell.topID = cell.groundID = worldgen.getWorld().getBlockId("main:stoneGranite");
                }
            }

            bool isSand = cell.temperature > 0.5 && cell.fertility < 0.5 && (cell.height < 32 || cell.humidity > 0.3);
            isSand = isSand || (cell.height < 4 && cell.humidity < 0.3);

            if (isSand)
            {
                cell.topID = worldgen.getWorld().getBlockId("main:sand");
                if (cell.height > 32)
                {
                    cell.topID = -1;
                }

                cell.groundID = cell.topID;
                cell.underLakeID = cell.topID;
            }
        }
    }
}

TerrainGenerator::TerrainGrid& TerrainGenerator::gridAt(int regionX, int regionY)
{
    std::shared_lock<std::shared_mutex> shlock(gridCacheMutex);
    if (gridCache.count(regionX) && gridCache[regionX].count(regionY))
    {
        return *gridCache[regionX][regionY];
    }

    shlock.unlock();

    std::unique_lock<std::shared_mutex> ulock(gridCacheMutex);
    if (gridCache.count(regionX) && gridCache[regionX].count(regionY))
    {
        return *gridCache[regionX][regionY];
    }

    std::shared_ptr<TerrainGrid> grid = std::make_shared<TerrainGrid>();
    computeGridAt(*grid, regionX, regionY);
    gridCache[regionX][regionY] = grid;

    return *grid;
}

float TerrainGenerator::erode(float height, float steepness, float scale)
{
    height /= scale;
    float integerPart;
    float fractionalPart = std::modf(height, &integerPart);
    height = integerPart + std::pow(std::abs(fractionalPart), steepness) * ((fractionalPart > 0) - (fractionalPart < 0));
    height *= scale;
    return height;
}

TerrainGenerator::TerrainGenerator(SurvivalWorldGenerator &worldgen) :
    worldgen(worldgen),
    permutationNode(worldgen.getRootNode()[0x4245f12429374a80]),
    temperatureNoise(worldgen.getRootNode()[0x9a8b367741586806]),
    humidityNoise(worldgen.getRootNode()[0x819cf62ca76989de]),
    fertilityNoise(worldgen.getRootNode()[0xa5e6c11179f2f4a0]),
    bumpinessNoise(worldgen.getRootNode()[0x6ab53f2fca37a4f4]),
    steepnessNoise(worldgen.getRootNode()[0x0ef36bc6703dac38]),
    subtleSteepnessNoise(worldgen.getRootNode()[0xaf37c99bc2bd9603]),
    terrainNoise(permutationNode),
    biasNoise(worldgen.getRootNode()[0x8518f32e8d8bf450])
{
    if (envVars.dumpTerrainNoise)
    {
        const int width = 512;
        const int height = 512;

        Bitmap combinedBitmap(glm::uvec2(width, height));
        Bitmap temperatureBitmap(glm::uvec2(width, height));
        Bitmap fertilityBitmap(glm::uvec2(width, height));
        Bitmap humidityBitmap(glm::uvec2(width, height));
        Bitmap bumpinessBitmap(glm::uvec2(width, height));
        Bitmap heightBitmap(glm::uvec2(width, height));
        glm::uvec2 pos;

        for (pos.y = 0; pos.y < height; pos.y++)
        {
            for (pos.x = 0; pos.x < width; pos.x++)
            {
                int x = pos.x << 4;
                int y = pos.y << 4;

                Cell &cell = getCell(x, y);

                combinedBitmap.setLinearColor(pos, glm::vec4(cell.temperature, cell.fertility, cell.humidity, 1.0f));
                temperatureBitmap.setLinearColor(pos, glm::vec4(cell.temperature, 0.0f, 0.0f, 1.0f));
                fertilityBitmap.setLinearColor(pos, glm::vec4(0.0f, cell.fertility, 0.0f, 1.0f));
                humidityBitmap.setLinearColor(pos, glm::vec4(0.0f, 0.0f, cell.humidity, 1.0f));
                bumpinessBitmap.setLinearColor(pos, glm::vec4(cell.bumpiness / 2.5f, cell.bumpiness / 2.5f, cell.bumpiness / 2.5f, 1.0f));
                heightBitmap.setLinearColor(pos, glm::vec4((double) cell.height / 128.0f, (double) cell.height / 128.0f, (double) cell.height / 128.0f, 1.0f));
            }
        }

        combinedBitmap.writeToPNG("dumps/terrainNoise.png");
        temperatureBitmap.writeToPNG("dumps/temperatureNoise.png");
        fertilityBitmap.writeToPNG("dumps/fertilityNoise.png");
        humidityBitmap.writeToPNG("dumps/humidityNoise.png");
        bumpinessBitmap.writeToPNG("dumps/bumpinessNoise.png");
        heightBitmap.writeToPNG("dumps/heightNoise.png");
    }
}

TerrainGenerator::Cell& TerrainGenerator::getCell(int x, int y)
{
    TerrainGrid &grid = gridAt(x & ~0xFF, y & ~0xFF);

    int regionOffsetX = x & 0xFF;
    int regionOffsetY = y & 0xFF;

    return grid.data[regionOffsetY * 256 + regionOffsetX];
}

int TerrainGenerator::getTopZ(int x, int y)
{
    return getCell(x, y).height;
}

void TerrainGenerator::generate(Chunk &chunk)
{
    if (chunk.base.z < -16)
    {
        return;
    }

    int stoneID = worldgen.getWorld().getBlockId("main:stone");

    // Generate the terrain.
    glm::ivec3 offset;
    for (offset.x=0; offset.x<16; offset.x++)
    {
        for (offset.y=0; offset.y<16; offset.y++)
        {
            Cell &cell = getCell(chunk.base.x+offset.x, chunk.base.y+offset.y);

            for (offset.z=0; offset.z<16; offset.z++)
            {
                int absoluteZ = chunk.base.z + offset.z;
                int blockId = 0;
                uint8_t meta = 0;

                if (absoluteZ == cell.height)
                {
                    blockId = absoluteZ < -1 ? cell.underLakeID : cell.topID;
                }
                else if (absoluteZ < cell.height)
                {
                    blockId = absoluteZ < 0 ? stoneID : cell.groundID;
                }
                else if (absoluteZ < 0)
                {
                    blockId = cell.fluidBlockID;
                    meta = 0xFF;
                }

                if (blockId == -1)
                {
                    int height = chunk.base.z + offset.z;
                    std::string blockName = "main:sand";

                    if (height > 32)
                    {
                        int band = (height - 33) / clayBandHeight;
                        int colorIndex = band % clayColorBands.size();

                        blockName = formatString("main:$+Clay", clayColorBands[colorIndex]);
                    }

                    blockId = worldgen.getWorld().getBlockId(blockName);
                }

                chunk.setBlockAndMeta(offset, blockId, meta);
            }
        }
    }
}