#include <stdint.h>
#include <stdbool.h>
#include "common.hpp"
#include <cstring>
#include <list>
#include <fstream>
#include <vector>
#include <iostream>

#define HUMIDITY_OFFSET 12.3f
#define CAVE_OFFSET 45.6f
#define SCALE 0.01f
#define CAVE_COEFF 0.06
#define EDGES_FADE 64
#define CLOUD_SCALE 0.25f

#define BIOME_FOREST 0
#define BIOME_TAIGA 1

static const siv::PerlinNoise perlin;

static int world_type = 0;

const BlockDef BLOCKS[] = {
    BlockDef{ "air", R_Translucent, B_Stone, 0, 0, 0},
    BlockDef{ "stone", R_Normal, B_Stone, 7, 0, 3},
    BlockDef{ "grass", R_TriSide, B_Grass, 1, 0, 0, 1, 2},
    BlockDef{ "dirt", R_Normal, B_Dirt, 2, 0, 2},
    BlockDef{ "sand", R_Normal, B_Dirt, 2, 0, 4},
    BlockDef{ "water", R_Translucent, B_Stone, 0, 0, 14},
    BlockDef{ "planks", R_Normal, B_Wood, 3, 0, 11},
    BlockDef{ "gray_bricks", R_Normal, B_Stone, 5, 1, 16},
    BlockDef{ "red_bricks", R_Normal, B_Stone, 5, 1, 17},
    BlockDef{ "blue_bricks", R_Normal, B_Stone, 5, 1, 18},
    BlockDef{ "green_bricks", R_Normal, B_Stone, 5, 1, 19},
    BlockDef{ "glass", R_Translucent, B_Stone, 3, 1, 12},
    BlockDef{ "log", R_TriSide, B_Wood, 5, 0, 10, 9, 10},
    BlockDef{ "spruce_planks", R_Normal, B_Wood, 3, 0, 13},
    BlockDef{ "spruce_log", R_TriSide, B_Wood, 5, 0, 21, 20, 21},
    BlockDef{ "workbench", R_TranslucentAllSides, B_Stone, 3, 0, 23, 22, 23},
    BlockDef{ "blue_block", R_Normal, B_Stone, 5, 0, 22},
    BlockDef{ "yellow_block", R_Normal, B_Stone, 5, 0,  23},
    BlockDef{ "cyan_block", R_Normal, B_Stone, 5, 0, 24},
    BlockDef{ "magenta_block", R_Normal, B_Stone, 5, 0, 25},
    BlockDef{ "white_block", R_Normal, B_Stone, 5, 0, 26},
    BlockDef{ "black_block", R_Normal, B_Stone, 5, 0, 27},
    BlockDef{ "leaves", R_TranslucentAllSides, B_Grass, 1, 0, 28},
    BlockDef{ "copper_ore", R_Normal, B_Stone, 7, 1, 5},
    BlockDef{ "silver_ore", R_Normal, B_Stone, 7, 2, 6},
    BlockDef{ "gold_ore", R_Normal, B_Stone, 7, 3, 7},
    BlockDef{ "coal_ore", R_Normal, B_Stone, 7, 0, 8},
    BlockDef{ "unobtainium", R_Normal, B_Stone, 7, 999, 3}, // Bedrock?
};
int GetNumericIDFromString(std::string s) {
    int count = sizeof(BLOCKS) / sizeof(BLOCKS[0]);
    for (int i = 0; i < count; ++i) {
        if (BLOCKS[i].name == s) return i;
    }
    return -1;
}
static std::bitset<256> marked;

static int GetBlockRegularWorld(int x, int y, int z) {
    float fade_x = (fmax(EDGES_FADE - x, 0) + fmax(EDGES_FADE - WORLD_SIZE_BLOCKS + x, 0)) / (float)EDGES_FADE;
    float fade_z = (fmax(EDGES_FADE - z, 0) + fmax(EDGES_FADE - WORLD_SIZE_BLOCKS + z, 0)) / (float)EDGES_FADE;
    float fade = 1 - (fade_x + fade_z);

    const int SEA_HEIGHT = 32;     // sea surface y
    const int BEACH_DEPTH = 3;     // how many blocks below surface become sand

    float height0 = (perlin.noise2D((double)x * 1.5f * SCALE, (double)z * 1.5f * SCALE) + 1.0f ) / 2.0f * 16.0f;
    float mountains = pow((perlin.noise2D((double)x * 0.5f, (double)z * 0.5f * SCALE) + 1.0f ) / 2.0f, 16);
    float height1 = (perlin.noise2D((double)x * 4.5f * SCALE, (double)z * 4.5f * SCALE) + 1.0f ) / 2.0f * 24.0f * mountains;

    int height = floor((height0 + height1) * fade) + 26;

    float caveNoise = fabs(perlin.noise3D((double)x * 1.0f * SCALE + CAVE_OFFSET, (double)y * 4.0f * SCALE + CAVE_OFFSET, (double)z * 1.0f * SCALE + CAVE_OFFSET));

    if (y <= height) {
        if(caveNoise < 0.02f) return BLOCK_AIR;
        if (y == height) {
            if (height <= SEA_HEIGHT) {
                return BLOCK_SAND;
            }
            return BLOCK_GRASS;
        }


        if (y > height - BEACH_DEPTH && height <= SEA_HEIGHT + 2) {
            return BLOCK_SAND;
        }
        if(caveNoise < 0.2f || y < 4) return BLOCK_STONE;
        return BLOCK_DIRT;
    }

    if (y <= SEA_HEIGHT && height < SEA_HEIGHT) {
        return BLOCK_WATERS;
    }

    return BLOCK_AIR;
}
bool CanTick(int blockType) { // TODO: make smarter in case other blocks can tick too!
    return blockType == BLOCK_SAND;
}
bool GetCloud(int x, int z) {
    return (perlin.noise2D((x / 8) * 1.5f * CLOUD_SCALE, (z / 8) * 1.5f * CLOUD_SCALE) + 1.0f) / 2.0f > 0.5f;
}
static int GetBlockCaveWorld(int x, int y, int z) {
    float caveNoise = (perlin.noise3D((float)x * 3.0f * SCALE + CAVE_OFFSET, (float)y * 3.0f * SCALE + CAVE_OFFSET, (float)z * 3.0f * SCALE + CAVE_OFFSET) + 1.0f) / 2.0f;
    if(caveNoise > 0.5f) {
        return BLOCK_STONE;
    }
    return BLOCK_AIR;
}
static int has_block(int x, int y, int z) 
{
    switch (world_type)
    {
        case 0:
            return GetBlockRegularWorld(x, y, z);
            break;
        case 1:
            return GetBlockCaveWorld(x, y, z);
            break;
        case 254:
            if(y == 0 + x / 16) {
                return BLOCK_STONE;
            }
            break;
        case 255:
            if((y >= 63 || y == 61) && (x < 4 && z < 4)) {
                return BLOCK_STONE;
            }
            break;
        default:
            const int baseHeight = 32;
            if(y <= baseHeight / 3) {
                return BLOCK_STONE;
            }
            if (y > baseHeight / 3 && y < baseHeight)
            {
                return BLOCK_DIRT;
            }
            if (y == baseHeight)
            {
                return BLOCK_GRASS;
            }
            break;
    }
    return BLOCK_AIR;
}
bool IsTranslucent(int blockType) {
    return marked.test(blockType);
}

void SetBlockChunk(Chunk *c, int x, int y, int z, int t) {
    c->blocks[GetIndexChunk(x,y,z)] = t;
    if(y >= c->highestBlock[GetIndexChunk2D(x, z)]) {
        if(t != 0) {
            c->highestBlock[GetIndexChunk2D(x, z)] = y;
        }
        else {
            for (int _y = y-1; _y >= 0; _y--)
            {
                /*if(!IsTranslucent(c->blocks[GetIndexChunk(x,_y,z)])) {
                    c->highestBlock[GetIndexChunk2D(x, z)] = _y;
                    break;
                }*/
                if(c->blocks[GetIndexChunk(x,_y,z)] != 0) {
                    c->highestBlock[GetIndexChunk2D(x, z)] = _y;
                    break;
                }
            }
        }
    }
}
void RebuildHeightmap(Chunk *c) {
    for (int x = 0; x < CHUNK_SIZE; x++)
    {
        for (int z = 0; z < CHUNK_SIZE; z++)
        {
            for (int y = WORLD_HEIGHT-1; y >= 0; y--)
            {
                if(c->blocks[GetIndexChunk(x,y,z)] != 0) {
                    c->highestBlock[GetIndexChunk2D(x, z)] = y;
                    break;
                }
            }
        }
    }
}
int GetBiome(int sx, int sy) {
    return BIOME_TAIGA;
}
void SetBlockChunkFast(Chunk *c, int i, int t) {
    c->blocks[i] = t;
}
void SetBlock(Chunk world[], int x, int y, int z, int t) {
    if(CheckOOBWorld(x, y, z)) { return; }
    int locX = x % CHUNK_SIZE;
    int locZ = z % CHUNK_SIZE;
    int cx = x / CHUNK_SIZE;
    int cz = z / CHUNK_SIZE;
    SetBlockChunk(&world[GetIndexWorld(cx, cz)], locX, y, locZ, t);
}
void InitWorldgen(int worldType) {
    const siv::PerlinNoise::seed_type seed = 123456u;
	const siv::PerlinNoise perlin{ seed };
    constexpr size_t MAX = 128;           // allowed range: 0..MAX-1
    for (int i = 0; i < sizeof(BLOCKS) / sizeof(BlockDef); i++)
    {
        BlockDef def = BLOCKS[i];
        if(def.render == R_Translucent || def.render == R_TranslucentAllSides || def.render == R_TranslucentTriSide) {
            marked.set(i);
        }
    }
    world_type = worldType;
}
Vector3 WorldPosition(int cx, int cz, int localX, int y, int localZ) {
    return { (float)cx * CHUNK_SIZE + localX, (float)y, (float)cz * CHUNK_SIZE + localZ };
}

Chunk GenerateChunk(int _x, int _y) {
    Chunk new_chunk = { 0 };
    new_chunk.x = _x;
    new_chunk.z = _y;
    
    for (int x = 0; x < CHUNK_SIZE; x++)
    {
        for (int y = 0; y < WORLD_HEIGHT; y++)
        {
            for (int z = 0; z < CHUNK_SIZE; z++)
            {
                int id = has_block(_x * CHUNK_SIZE + x,y,_y * CHUNK_SIZE + z);
                SetBlockChunk(&new_chunk, x, y, z, id);
            }
        }
    }
    return new_chunk;
}
void GenerateWorldAdditional(Chunk world[]) {
    int biome = GetBiome(0, 0);
    SetRandomSeed(0);
    switch (world_type)
    {
        case 0:
            for (int x = 0; x < WORLD_SIZE_BLOCKS; x++)
            {
                for (int z = 0; z < WORLD_SIZE_BLOCKS; z++) {
                    Chunk c = world[GetIndexWorld(x / CHUNK_SIZE, z / CHUNK_SIZE)];
                    int highestY = c.highestBlock[GetIndexChunk2D(x % CHUNK_SIZE, z % CHUNK_SIZE)];
                    int b = fetch_block(c, x % CHUNK_SIZE, highestY, z % CHUNK_SIZE);
                    for (int _ = 0; _ < GetRandomValue(0, 6); _++)
                    {
                        if(GetRandomValue(0, 800) == 0) {
                            int max = GetRandomValue(10, 120);
                            Vector3 pos = {x, GetRandomValue(12, WORLD_HEIGHT-32), z};
                            Vector3 direction = {GetRandomValue(-100, 100) / 100.0f, GetRandomValue(-100, -5) / 100.0f, GetRandomValue(-100, 100) / 100.0f};
                            int radius = 2;
                            for (int _ = 0; _ < max; _++)
                            {
                                for (int x = -radius; x < radius; x++)
                                {
                                    for (int y = -radius; y < radius; y++)
                                    {
                                        for (int z = -radius; z < radius; z++)
                                        {
                                            if(Vector3Distance({pos.x + x, pos.y + y, pos.z + z}, pos) <= radius) {
                                                if(!CheckOOBWorld(pos.x + x, pos.y + y, pos.z + z)) {
                                                    SetBlock(world, pos.x + x, pos.y + y, pos.z + z, 0);
                                                }
                                            }
                                        }
                                    }
                                }
                                direction += {GetRandomValue(-100, 100) / 100.0f, GetRandomValue(-100, -5) / 100.0f, GetRandomValue(-100, 100) / 100.0f};
                                pos += direction;
                            }
                        }
                    }
                    for (int _ = 0; _ < GetRandomValue(8, 24); _++)
                    {
                        if(GetRandomValue(0, 100) == 0) {
                            Vector3 pos = {x, GetRandomValue(2, WORLD_HEIGHT-24), z};
                            int type = 23;
                            if(pos.y < 24)
                                type = 24;
                            if(pos.y < 12)
                                type = 25;
                            int radius = 2;
                            for (int x = -radius; x < radius; x++)
                            {
                                for (int y = -radius; y < radius; y++)
                                {
                                    for (int z = -radius; z < radius; z++)
                                    {
                                        if(Vector3Distance({pos.x + x, pos.y + y, pos.z + z}, pos) <= radius) {
                                            if(!CheckOOBWorld(pos.x + x, pos.y + y, pos.z + z) && GetBlock(world, pos.x + x, pos.y + y, pos.z + z) == BLOCK_STONE) {
                                                SetBlock(world, pos.x + x, pos.y + y, pos.z + z, type);
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
            break;
    }
    switch (world_type)
    {
        case 0:
            for (int x = 0; x < WORLD_SIZE_BLOCKS; x++)
            {
                for (int z = 0; z < WORLD_SIZE_BLOCKS; z++) {
                    Chunk c = world[GetIndexWorld(x / CHUNK_SIZE, z / CHUNK_SIZE)];
                    int highestY = c.highestBlock[GetIndexChunk2D(x % CHUNK_SIZE, z % CHUNK_SIZE)];
                    int b = fetch_block(c, x % CHUNK_SIZE, highestY, z % CHUNK_SIZE);
                    if(b == BLOCK_GRASS && GetRandomValue(0, 100) < 4 && biome == BIOME_FOREST) { // Regular trees
                        SetBlock(world, x, highestY, z, BLOCK_DIRT);
                        int maxY = GetRandomValue(5, 7);
                        for (int y = 1; y < maxY; y++)
                        {
                            int wy = highestY + y;
                            
                            if(y > 2) { //leaves
                                SetBlock(world, x-1, wy, z, 22);
                                SetBlock(world, x+1, wy, z, 22);
                                SetBlock(world, x, wy, z-1, 22);
                                SetBlock(world, x, wy, z+1, 22);
                                SetBlock(world, x, wy+1, z, 22);
                            }
                            SetBlock(world, x, wy, z, 12); // log
                        }
                    }
                    if(b == BLOCK_GRASS && GetRandomValue(0, 100) < 2 && biome == BIOME_TAIGA) { // Spruce
                        SetBlock(world, x, highestY, z, BLOCK_DIRT);
                        int maxY = 8;
                        for (int y = 1; y < maxY; y++)
                        {
                            int wy = highestY + y;
                            const int lower_size = 3;
                            const int upper_size = 2;
                            if(y > 2) { //leaves
                                if(y == 3) {
                                    for (int _x = -lower_size; _x <= lower_size; _x++)
                                    {
                                        for (int _z = -lower_size; _z <= lower_size; _z++)
                                        {
                                            if(
                                                !(fabs(_z) == lower_size && fabs(_x) == lower_size)
                                            ) SetBlock(world, x+_x, wy, z+_z, 22);
                                        }
                                    }
                                }
                                else {
                                    if(y == maxY-1) {
                                        SetBlock(world, x-1, wy+1, z, 22);
                                        SetBlock(world, x+1, wy+1, z, 22);
                                        SetBlock(world, x, wy+1, z-1, 22);
                                        SetBlock(world, x, wy+1, z+1, 22);
                                        SetBlock(world, x, wy+1, z, 22);
                                    }
                                    if(y == 4) {
                                        for (int _x = -upper_size; _x <= upper_size; _x++)
                                        {
                                            for (int _z = -upper_size; _z <= upper_size; _z++)
                                            {
                                                if(
                                                    !(fabs(_z) == upper_size && fabs(_x) == upper_size)
                                                ) SetBlock(world, x+_x, wy, z+_z, 22);
                                            }
                                        }
                                    }
                                    if(y > 4 && y%2 == 0) {
                                        SetBlock(world, x-1, wy, z, 22);
                                        SetBlock(world, x+1, wy, z, 22);
                                        SetBlock(world, x, wy, z-1, 22);
                                        SetBlock(world, x, wy, z+1, 22);
                                        SetBlock(world, x, wy, z, 22);
                                    }
                                }
                                
                            }
                            SetBlock(world, x, wy, z, 14); // log
                        }
                    }
                    if(!marked.test(b) && GetRandomValue(0, 800) == 0) {
                        int maxY = GetRandomValue(5, 7);
                        for (int _x = -1; _x <= 1; _x++)
                            for (int _z = -1; _z <= 1; _z++) {
                                int t = BLOCK_STONE;
                                if(GetRandomValue(0, 50) == 0) t = 23;
                                SetBlock(world, x+_x-1, highestY+1, z+_z-1, t);
                            }
                        
                        for (int _x = -1; _x <= 1; _x++)
                            for (int _z = -1; _z <= 1; _z++)
                                if(_x != _z) {
                                    int t = BLOCK_STONE;
                                    if(GetRandomValue(0, 80) == 0) t = 23;
                                    SetBlock(world, x+_x-1, highestY+2, z+_z-1, t);
                                }
                        SetBlock(world, x-1, highestY+2, z-1, BLOCK_STONE);
                        SetBlock(world, x-1, highestY+3, z-1, BLOCK_STONE);
                    }
                }
            }
            break;
    }
    switch (world_type)
    {
        case 0:
            for (int x = 0; x < WORLD_SIZE_BLOCKS; x++)
            {
                for (int z = 0; z < WORLD_SIZE_BLOCKS; z++) {
                    SetBlock(world, x, 1, z, 27);
                }
            }
            break;
    }
}
void TickBlock(Chunk world[], int wx, int wy, int wz) {
    if(CheckOOBWorld(wx, wy, wz)) return;
    int tile = GetBlock(world, wx, wy, wz);
    if(tile == BLOCK_SAND) {
        if(GetBlock(world, wx, wy-1, wz) == 0) {
            SetBlockNetwork(world, wx, wy, wz, 0);
            Entity temp = NewEntity();
            temp.type = 4;
            temp.position = (Vector3){wx, wy, wz};
            temp.velocity = {0, 0, 0};
            temp.data[1] = BLOCK_SAND;
            AddEntity(temp);
        }
    }
}
void TickChunk(Chunk *chunk, Chunk *world, int wx, int wz) {
    for (int y = WORLD_HEIGHT-1; y > 1; y--) {
        for (int x = 0; x < CHUNK_SIZE; x++)
        {
            for (int z = 0; z < CHUNK_SIZE; z++)
            {
                int tile = fetch_block_ptr(chunk, x, y, z);
                if(tile == BLOCK_SAND) {
                    if(fetch_block_ptr(chunk, x, y-1, z) == 0) {
                        SetBlockChunk(chunk, x, y, z, 0);
                        Entity temp = NewEntity();
                        temp.type = 4;
                        temp.position = (Vector3){((int)wx * CHUNK_SIZE + x), y, ((int)wz * CHUNK_SIZE + z)};
                        temp.velocity = {0, 0, 0};
                        temp.data[1] = BLOCK_SAND;
                        AddEntity(temp);
                        chunk->isDirty = true;
                    }
                }
                if(false) { // tile == BLOCK_WATERS
                    int rx = GetRandomValue(-1, 1);
                    int rz = GetRandomValue(-1, 1);
                    for (int _y = 0; _y >= -1; _y--)
                    {
                        if(GetBlock(world, wx+x+rx, y+_y, wz+z+rz) == 0) {
                            SetBlockChunk(chunk, x, y+_y, z, 0);
                            SetBlock(world, wx+x+rx, y+_y, wz+z+rz, BLOCK_WATERS);
                            chunk->isDirty = true;
                            continue;
                        }
                    }
                }
            }
        } 
    }
}
void RebuildOpaqueMask(Chunk *chunk) {
    const int GROUP = 16;
    const int GROUPS = CHUNK_DATA_SIZE / GROUP;

    uint16_t oMask[GROUPS] = { 0 };
    uint16_t aMask[GROUPS] = { 0 };
    uint8_t blocks[CHUNK_DATA_SIZE];
    memcpy(blocks, chunk->blocks, sizeof(blocks));

    for (int i = 0; i < CHUNK_DATA_SIZE; ++i) {
        int g = i / GROUP;
        int bit = i % GROUP;
        if (chunk->blocks[i] == 0) {
            aMask[g] |= 1 << bit;
        }
        if (!IsTranslucent(chunk->blocks[i])) {
            oMask[g] |= 1 << bit;
        }
    }

    memcpy(chunk->opaqueMask, oMask, sizeof(oMask));
    memcpy(chunk->airMask, aMask, sizeof(aMask));
}

#ifndef SERVER
//Generates an opaque CRD (ChunkRenderData). Outputs the CRD.
ChunkRenderData GenerateCRDOpaque(const Chunk &chunk, Chunk world[]) {
    struct ChunkRenderData new_chunk = { 0 };
    uint16_t mask[CHUNK_DATA_SIZE/16];
    memcpy(mask, chunk.opaqueMask, sizeof(chunk.opaqueMask));

    const int cxw = chunk.x * CHUNK_SIZE;
    const int cxz = chunk.z * CHUNK_SIZE;
    const int chunkMaskSize = CHUNK_DATA_SIZE/16;
    for (int step = 0; step < chunkMaskSize; ++step) {
        uint16_t m = mask[step];
        if(m == 0) continue;

        while (m) { 
            int bit = __builtin_ctz(m); 
            m &= m - 1; 
            int i2 = (step<<4) + bit;
            int _x = bit;
            int y = step >> 4;
            int _z = step & 0xF;
            //int _x, y, _z;
            //GetXYZFromIndex(i2, &_x, &y, &_z);
            if (!TestMask(mask, i2)) continue; // Skip if this is translucent
            
            int x = _x + cxw;
            int z = _z + cxz;

            bool l = (_x > 0) ? TestMask(mask, i2-1) : TestOpaqueMaskWorld(world, x - 1, y, z);
            bool r = (_x < CHUNK_SIZE-1) ? TestMask(mask, i2+1) : TestOpaqueMaskWorld(world, x + 1, y, z);
            bool u = (y==WORLD_HEIGHT-1) ? false : TestMask(mask, i2+CHUNK_SIZE_SQR);
            bool d = (y==0) ? true : TestMask(mask, i2-CHUNK_SIZE_SQR);
            bool b = (_z==0) ? TestOpaqueMaskWorld(world, x, y, z - 1) : TestMask(mask, i2-CHUNK_SIZE);
            bool f = (_z==CHUNK_SIZE-1) ? TestOpaqueMaskWorld(world, x, y, z + 1) : TestMask(mask, i2+CHUNK_SIZE);
            new_chunk.sides[i2] = pack6(
                !l,
                !r,
                !u,
                !d,
                !b,
                !f
            );
        }
    }
    return new_chunk;
}

//Generates a translucent CRD (ChunkRenderData). Outputs the CRD.
ChunkRenderData GenerateCRDTranslucent(Chunk chunk, Chunk world[]) {
    struct ChunkRenderData new_chunk = { 0 };
    uint16_t *mask0= chunk.opaqueMask;
    uint16_t *mask = chunk.airMask;
    for (int step = 0; step < CHUNK_DATA_SIZE/16; ++step)
    {
        uint16_t m = ~(mask[step]);
        if(m == 0) continue;

        while (m) { 
            int bit = __builtin_ctz(m); 
            m &= m - 1; 
            int i2 = (step<<4) + bit;
            int _x = bit;
            int y = step >> 4;
            int _z = step & 0xF;
            
            if (TestMask(mask0, i2)) {
                new_chunk.sides[i2] = 0;
                continue;
            };
            if(BLOCKS[fetch_block(chunk, _x, y, _z)].render == R_TranslucentAllSides) { 
                new_chunk.sides[i2] = 0xFF;
                continue;
            } 
            int x = _x + chunk.x * CHUNK_SIZE;
            int z = _z + chunk.z * CHUNK_SIZE;
            bool l = (_x > 0) ? !TestMask(mask, i2-1) : !TestAirMaskWorld(world, x - 1, y, z);
            bool r = (_x < CHUNK_SIZE-1) ? !TestMask(mask, i2+1) : !TestAirMaskWorld(world, x + 1, y, z);
            bool u = (y==WORLD_HEIGHT-1) ? false : !TestMask(mask, i2+CHUNK_SIZE_SQR);
            bool d = (y==0) ? true : !TestMask(mask, i2-CHUNK_SIZE_SQR);
            bool b = (_z==0) ? !TestAirMaskWorld(world, x, y, z - 1) : !TestMask(mask, i2-CHUNK_SIZE);
            bool f = (_z==CHUNK_SIZE-1) ? !TestAirMaskWorld(world, x, y, z + 1) : !TestMask(mask, i2+CHUNK_SIZE);
            new_chunk.sides[i2] = pack6(
                !l,
                !r,
                !u,
                !d,
                !b,
                !f
            );
        }
    }
    return new_chunk;
}
#endif
#define SAVE_FORMAT 0

void SaveWorld(Chunk world[], const char* path) {
    std::ofstream data(path, std::ios::binary);
    if (!data) return;

    data.write("WFF", 3);
    char fmt = static_cast<char>(SAVE_FORMAT);
    data.write(&fmt, 1);

    for (int i = 0; i < MAX_WORLD_AREA; ++i) {
        const Chunk &c = world[i];

        std::vector<uint8_t> temp;
        int last_type = -1;
        uint8_t count = 0;

        for (int j = 0; j < CHUNK_DATA_SIZE; ++j) {
            uint8_t b = c.blocks[j];

            if (last_type == -1) {
                last_type = b;
                count = 1;
            } else if (b == last_type && count < 255) {
                ++count;
            } else {
                temp.push_back(count);
                temp.push_back(static_cast<uint8_t>(last_type));
                last_type = b;
                count = 1;
            }
        }

        if (last_type != -1) {
            temp.push_back(count);
            temp.push_back(static_cast<uint8_t>(last_type));
        }

        uint16_t size = static_cast<uint16_t>(temp.size());
        data.write(reinterpret_cast<const char*>(&size), sizeof(size));
        if (!temp.empty()) {
            data.write(reinterpret_cast<const char*>(temp.data()), temp.size());
        }
    }

    data.close();
}

void Debug_Write(const std::string &stuff);
bool LoadWorld(Chunk world[], const char* path) {
    std::ifstream data(path, std::ios::binary);
    if (!data) return false;

    char header[3];
    if (!data.read(header, 3)) return false;
    if (std::memcmp(header, "WFF", 3) != 0) return false;

    char fmt;
    if (!data.read(&fmt, 1)) return false;

    uint8_t* temp = (uint8_t*)malloc(65535);
    for (int i = 0; i < MAX_WORLD_AREA; ++i) {
        printf("[PRINTF] Chunk %d\n", i);

        uint16_t size = 0;
        Debug_Write("Trying to read size");
        if (!data.read(reinterpret_cast<char*>(&size), sizeof(size))) {
            free(temp);
            return false;
        };
        printf("[PRINTF] Size %d\n", size);

        if (size > 0) {
            if (!data.read(reinterpret_cast<char*>(temp), size)) {
                free(temp);
                return false;
            }
        }
        Chunk *c = &world[i];
        int outIndex = 0;
        for (size_t p = 0; p + 1 < size; p += 2) {
            uint8_t count = temp[p];
            uint8_t value = temp[p + 1];
            for (uint8_t k = 0; k < count; ++k) {
                SetBlockChunkFast(c, outIndex++, value);
            }
        }
        while (outIndex < CHUNK_DATA_SIZE) {
            SetBlockChunkFast(c, outIndex++, 0);
        }
        RebuildHeightmap(c);
    }
    free(temp);
    return true;
}
