/* #include  */
#include 
#include 

#include 
#include 
#include 
#include 
#include 
// i686-w64-mingw32-gcc main.c -O0 -lopengl32 -lglut && ./a.exe

// extensions
PFNGLLOCKARRAYSEXTPROC glLockArraysEXT;
PFNGLUNLOCKARRAYSEXTPROC glUnlockArraysEXT;

float randFloat() { return ((float)(rand() % 1000000)/1000000.0f); }

int last_time = 0;

#define CHUNK_H 16
#define CHUNK_V 128
#define RENDER_DISTANCE 16

// Camera state
float camX = 1000, camY = CHUNK_V/2, camZ = 1000; // position
float camYaw = 0.0f, camPitch = 0.0f;       // angles in degrees

float moveSpeed = 0.1f;
float mouseSensitivity = 0.2f;

// Mouse previous position
int lastMouseX = -1, lastMouseY = -1;

// Key state
int keys[256] = {0};

void error(char* s) {
    printf(s);
    exit(1);
}

GLuint load_ppm_texture(const char *path)
{
    #define PPM_HEADER_LIMIT 128
    
    FILE *f = fopen(path, "rb");
    if (!f) {
        error("Error: failed to load texture\n");
    }

    char header[PPM_HEADER_LIMIT];
    int hlen = 0;

    int spaces = 0;
    while (hlen < PPM_HEADER_LIMIT-1 && spaces < 4) {
        int c = fgetc(f);
        if (c == EOF) { fclose(f); return 0; }
        header[hlen++] = c;
        if (c == ' ' || c == '\n' || c == '\t') spaces++;
    }

    if (hlen >= PPM_HEADER_LIMIT-1) {
        fclose(f);
        return 0;
    }

    header[hlen] = 0;

    char magic[3];
    int width, height, maxval;

    if (sscanf(header, "%2s %d %d %d", magic, &width, &height, &maxval) != 4) {
        fclose(f);
        return 0;
    }

    if (strcmp(magic, "P6") != 0 || maxval != 255) {
        fclose(f);
        return 0;
    }

    size_t size = width * height * 3;

    unsigned char *rgb = malloc(size);
    if (!rgb) {
        fclose(f);
        return 0;
    }

    if (fread(rgb, 1, size, f) != size) {
        free(rgb);
        fclose(f);
        return 0;
    }

    fclose(f);

    /* expand to RGBA with white colorkey */
    size_t pixels = width * height;
    unsigned char *rgba = malloc(pixels * 4);

    for (size_t i = 0; i < pixels; i++) {
        unsigned char r = rgb[i*3+0];
        unsigned char g = rgb[i*3+1];
        unsigned char b = rgb[i*3+2];

        rgba[i*4+0] = r;
        rgba[i*4+1] = g;
        rgba[i*4+2] = b;

        if (r==0 && g==0 && b==0)
            rgba[i*4+3] = 0;   /* transparent */
        else
            rgba[i*4+3] = 255; /* opaque */
    }

    free(rgb);

    GLuint tex;
    glGenTextures(1, &tex);
    glBindTexture(GL_TEXTURE_2D, tex);

    glPixelStorei(GL_UNPACK_ALIGNMENT,1);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

    glTexImage2D(
        GL_TEXTURE_2D,
        0,
        GL_RGBA,
        width,
        height,
        0,
        GL_RGBA,
        GL_UNSIGNED_BYTE,
        rgba
    );

    free(rgba);

    return tex;
}


typedef struct { int x, y; } ivec2;
typedef unsigned char block;

typedef struct {
    block       b[CHUNK_H][CHUNK_V][CHUNK_H];   /* block ids */

    char        is_vector_buffer_valid;
    int         vector_buffer_len;              /* last element in vector buffer */
    /* some extra positions in the buffer are dedicated to drawing lights */
    int         vector_buffer_light_len;
    
    int         vector_buffer_allocated;        /* size of vector buffer's backing array */
    
    GLfloat*    vector_buffer;
        
    unsigned    random;                         /* used for graphic effects */
    
    char        visible;
} chunk;

typedef struct {
    chunk* chunk[RENDER_DISTANCE][RENDER_DISTANCE];
    ivec2 pivot;
} world;

world* world_new() {
    world* new_world = calloc(1, sizeof(world));
    return new_world;
}

void world_move_xp(world* w) { }
void world_move_xn(world* w) { }
void world_move_zp(world* w) { }
void world_move_zn(world* w) { }

#define static_range(A, arr) for (int A = 0; A < sizeof(arr)/sizeof((arr)[0]); A++)

enum block_id {
    block_id_null,
    block_id_air,
    block_id_stone,
    block_id_grass,
    block_id_water,
    block_id_cloud,
    block_id_light,
    block_ids,
};

chunk* chunk_new() {
    chunk* newchunk = malloc(sizeof(chunk));
    newchunk->vector_buffer = 0;
    newchunk->is_vector_buffer_valid = 0;
    /* properly initialized chunks are guaranteed to be full of air */
    memset(newchunk->b, block_id_air, sizeof(newchunk->b));
    return newchunk;
}

void chunk_free(chunk* c) {
    if (!c) return;
    if (c->vector_buffer) free(c->vector_buffer);
    free(c);
}

static int floorf_to_int(float f) {
    int i = (int)f;
    if (f < 0.0f && (i != f)) {
        return i - 1;
    }
    return i;
}

unsigned seed = 0;

static uint32_t hash(int x, int y) {
    uint32_t h = seed + (uint32_t)x * 374761393 + (uint32_t)y * 668265263;
    h = (h ^ (h >> 13)) * 1274126177;
    return h ^ (h >> 16);
}

float noise2d(float x, float y) {
    int xi = floorf_to_int(x);
    int yi = floorf_to_int(y);
    float xf = x - (float)xi;
    float yf = y - (float)yi;
    float v00 = (float)hash(xi, yi) / 4294967296.0f;
    float v10 = (float)hash(xi + 1, yi) / 4294967296.0f;
    float v01 = (float)hash(xi, yi + 1) / 4294967296.0f;
    float v11 = (float)hash(xi + 1, yi + 1) / 4294967296.0f;
    // smoothstep
    float u = xf * xf * (3.0f - 2.0f * xf);
    float v = yf * yf * (3.0f - 2.0f * yf);
    float x1 = v00 + u * (v10 - v00);
    float x2 = v01 + u * (v11 - v01);
    return x1 + v * (x2 - x1);
}

void chunk_terrain(chunk* c, int x, int y) {

    c->random = noise2d(x, y);
    
    static_range(i, c->b) {
        static_range(j, c->b[i]) {
            static_range(k, c->b[i][j]) {
                if (j < (CHUNK_V/3)) {
                    c->b[i][j][k] = block_id_stone;
                    continue;
                }
                float n = (noise2d((x + i)/50.0f, (y + k)/50.0f)) * (CHUNK_V/2) + (CHUNK_V/3);
                if ( n > j ) {
                    c->b[i][j][k] = block_id_stone;
                } else if ((n + 1.0f) > j) {
                    c->b[i][j][k] = block_id_grass;
                    if ((rand() % 1000) == 0)
                        c->b[i][j+1][k] = block_id_light;
                }
                if ( (j < (CHUNK_V/2)) && (n < j) ) {
                    c->b[i][j][k] = block_id_water;
                }
            }
        }
    }

}


block get_block(world* w, int x, int y, int z) {
    
    if (x < w->pivot.x) error("requested out of bounds block in negative x\n");
    if (x > ((w->pivot.x)+(CHUNK_H*RENDER_DISTANCE))) error("requested out of bounds block in positive x\n");
    if (z < w->pivot.y) error("requested out of bounds block in z\n");
    if (z > ((w->pivot.y)+(CHUNK_H*RENDER_DISTANCE))) error("requested out of bounds block in positive z\n");
    if ((y < 0) || (y >= CHUNK_V)) error("requested out of bounds block in y\n");
    
    /* todo look at replacing the divisions and modulus with & and % */
    ivec2 world_index = (ivec2){
        (x - w->pivot.x)/CHUNK_H,
        (z - w->pivot.y)/CHUNK_H
    };
    
    block b = w->chunk[world_index.x][world_index.y]->b[x%CHUNK_H][y][z%CHUNK_H];
    
    if (b == block_id_null) error("attempted to read null block\n");
    
    return b;
}
#define LIGHT_LEVELS 16

float world_daylight = 0.5f;
void daylight_update(int tick) {
    
    
}

/* p represents the absolute position of the chunk */
void chunk_update_vector_array(world* w, chunk* c, int x, int y) {

    const float TILE = 16.0f/256.0f;
    
    float texU0(unsigned face) { return face * TILE; }
    float texU1(unsigned face) { return face * TILE + TILE; }

    float texV0(block id) { return id * TILE; }
    float texV1(block id) { return id * TILE + TILE; }
    
    #define VECTOR_BUCKET 4096

    void vector_buffer_push(chunk *c, float v)
    {
        if (c->vector_buffer_allocated == 0)
        {
            c->vector_buffer_allocated = VECTOR_BUCKET;
            c->vector_buffer_len = 0;
            c->vector_buffer_light_len = 0;
            c->vector_buffer = malloc(sizeof(GLfloat) * VECTOR_BUCKET);
        }

        if (c->vector_buffer_len >= c->vector_buffer_allocated)
        {
            size_t new_cap = c->vector_buffer_allocated + VECTOR_BUCKET;

            GLfloat *tmp = realloc(c->vector_buffer, sizeof(GLfloat) * new_cap);
            if (!tmp) return; /* or abort */

            c->vector_buffer = tmp;
            c->vector_buffer_allocated = new_cap;
        }

        c->vector_buffer[c->vector_buffer_len++] = v;
    }
    
    void vector_buffer_light_push(chunk *c, float v)
    {
        if (c->vector_buffer_allocated == 0)
        {
            c->vector_buffer_allocated = VECTOR_BUCKET;
            c->vector_buffer_len = 0;
            c->vector_buffer_light_len = 0;
            c->vector_buffer = malloc(sizeof(GLfloat) * VECTOR_BUCKET);
        }

        int index = c->vector_buffer_len + c->vector_buffer_light_len;

        if (index >= c->vector_buffer_allocated)
        {
            size_t new_cap = c->vector_buffer_allocated + VECTOR_BUCKET;

            GLfloat *tmp = realloc(c->vector_buffer, sizeof(GLfloat) * new_cap);
            if (!tmp) return;

            c->vector_buffer = tmp;
            c->vector_buffer_allocated = new_cap;
        }
        
        c->vector_buffer[index] = v;
        c->vector_buffer_light_len++;
    }
    
    int light_sources_count = 0;
    static int light_sources[CHUNK_H*CHUNK_H*CHUNK_V][3];
    

    for(int i = -CHUNK_H/2; i < (3*CHUNK_H/2); i++) {
        for(int j = 0; j < CHUNK_V; j++) {
            for(int k = -CHUNK_H/2; k < (3*CHUNK_H/2); k++) {
                block b = get_block(w, x + i, j, y + k);
                if (b == block_id_light) {
                    light_sources[light_sources_count][0] = x + i;
                    light_sources[light_sources_count][1] = j;
                    light_sources[light_sources_count][2] = y + k;
                    light_sources_count++;
                }
            }
        }
    }
    
    
    float lighting(int x, int y, int z) {
        float artificial_lighting = 0.0f;
        
        for(int i = 0; i < light_sources_count; i++) {
            int d = abs(x - light_sources[i][0]) + abs(y - light_sources[i][1]) + abs(z - light_sources[i][2]);
            if (d < LIGHT_LEVELS) {                
                if (d == 0) {
                    artificial_lighting = 1.0f;
                    break;
                }
                if ((1.0f / (float)d) > artificial_lighting)
                    artificial_lighting = (1.0f / (float)d);
            }
        }

        if (y > (CHUNK_V/2)) {
            return (world_daylight + artificial_lighting);
        }
        float f = (float)world_daylight / (float)(CHUNK_V/2);
        return f*f*f + artificial_lighting;
    }
    
    /* emit a quad with interleaved xyzuv */
    void emit_face_run(
        chunk* c,
        const GLfloat *cube,
        int i, int j, int k, int run,
        float r, float g, float b,
        const float uv[8],
        int *acc
    ){        
        for(int v=0; v<4; v++) {
            
            float z = cube[v*3+2];
            if (z > 0) z += (run - 1);
            
            vector_buffer_push(c, cube[v*3+0] + i + x);
            vector_buffer_push(c, cube[v*3+1] + j);
            vector_buffer_push(c,  z + k + y);
            vector_buffer_push(c, uv[v*2+0]);
            vector_buffer_push(c, uv[v*2+1]);
            vector_buffer_push(c, r);
            vector_buffer_push(c, g);
            vector_buffer_push(c, b);
        }
    }
    
    
    void emit_face(
        chunk* c,
        const GLfloat *cube,
        int i, int j, int k,
        float r, float g, float b,
        unsigned face,
        block id,
        int *acc
    ){
        
        float u0 = texU0(face);
        float u1 = texU1(face);
        float v0 = texV0(id);
        float v1 = texV1(id);
        const float uv[8] = {
            u0,v0,
            u1,v0,
            u1,v1,
            u0,v1
        };
        
        for(int v=0; v<4; v++) {
            vector_buffer_push(c, cube[v*3+0] + i + x);
            vector_buffer_push(c, cube[v*3+1] + j);
            vector_buffer_push(c, cube[v*3+2] + k + y);
            vector_buffer_push(c, uv[v*2+0]);
            vector_buffer_push(c, uv[v*2+1]);
            vector_buffer_push(c, r);
            vector_buffer_push(c, g);
            vector_buffer_push(c, b);
        }
    }
    
    const static GLfloat CubeVertices_ZP[] = {
        -0.5f,-0.5f, 0.5f,
         0.5f,-0.5f, 0.5f,
         0.5f, 0.5f, 0.5f,
        -0.5f, 0.5f, 0.5f
    };
    const static GLfloat CubeVertices_ZN[] = {
         0.5f,-0.5f,-0.5f,
        -0.5f,-0.5f,-0.5f,
        -0.5f, 0.5f,-0.5f,
         0.5f, 0.5f,-0.5f
    };
    const static GLfloat CubeVertices_XP[] = {
         0.5f,-0.5f, 0.5f,
         0.5f,-0.5f,-0.5f,
         0.5f, 0.5f,-0.5f,
         0.5f, 0.5f, 0.5f
    };
    const static GLfloat CubeVertices_XN[] = {
        -0.5f,-0.5f,-0.5f,
        -0.5f,-0.5f, 0.5f,
        -0.5f, 0.5f, 0.5f,
        -0.5f, 0.5f,-0.5f
    };

    const static GLfloat CubeVertices_YP[] = {
        -0.5f, 0.5f, 0.5f,
         0.5f, 0.5f, 0.5f,
         0.5f, 0.5f,-0.5f,
        -0.5f, 0.5f,-0.5f
    };
    const static GLfloat CubeVertices_YN[] = {
        -0.5f,-0.5f,-0.5f,
         0.5f,-0.5f,-0.5f,
         0.5f,-0.5f, 0.5f,
        -0.5f,-0.5f, 0.5f
    };

    int acc = 0;
    const static char is_transparent[block_ids] = {
        [block_id_air] = 1,
        [block_id_water] = 1,
    };

    static_range(i, c->b) {
        static_range(j, c->b[i]) {
            if (j <= 1) continue;
            if (j >= (CHUNK_V-2)) continue;
            
            for (int k = 0; k < CHUNK_H; )
            {
                block id = get_block(w,i+x,j,k+y);

                if (id == block_id_air) {
                    k++;
                    continue;
                }
                block n0 = get_block(w,i+x+1,j,k+y);
                if (!((n0 == block_id_air) || (!is_transparent[id] && is_transparent[n0]))) {
                    k++;
                    continue;
                }
                int run_len = 1;
                while ((k + run_len) < CHUNK_H)
                {
                    block id2 = get_block(w,i+x,j,k+y+run_len);
                    block n02 = get_block(w,i+x+1,j,k+y+run_len);
                    if (id2 != id)
                        break;
                    if (!((n02 == block_id_air) || (!is_transparent[id2] && is_transparent[n02])))
                        break;
                    run_len++;
                }
                float l = lighting(i+x,j,k+y);

                float u0 = texU0(0);
                float u1 = texU1(0);
                float v0 = texV0(id);
                float v1 = texV1(id);
                
                const float UV_XP[8] = {
                    u1*run_len, v0,
                    u0        , v0,
                    u0        , v1,
                    u1*run_len, v1
                };

                emit_face_run(c, CubeVertices_XP,
                    i,j,k,run_len,
                    l,l,l,
                    UV_XP,&acc);
                k += run_len;
                
            }
            for (int k = 0; k < CHUNK_H; )
            {
                block id = get_block(w,i+x,j,k+y);
                if (id == block_id_air) {
                    k++;
                    continue;
                }
                block n1 = get_block(w,i+x-1,j,k+y);
                if (!((n1 == block_id_air) || (!is_transparent[id] && is_transparent[n1]))) {
                    k++;
                    continue;
                }
                int run_len = 1;
                while ((k + run_len) < CHUNK_H)
                {
                    block id2 = get_block(w,i+x,j,k+y+run_len);
                    block n12 = get_block(w,i+x-1,j,k+y+run_len);
                    if (id2 != id)
                        break;
                    if (!((n12 == block_id_air) || (!is_transparent[id2] && is_transparent[n12])))
                        break;

                    run_len++;
                }
                float l = lighting(i+x,j,k+y);

                float u0 = texU0(0);
                float u1 = texU1(0);
                float v0 = texV0(id);
                float v1 = texV1(id);

                const float UV_XN[8] = {
                    u0, v0,
                    u1*run_len, v0,
                    u1*run_len, v1,
                    u0, v1
                };
    
                emit_face_run(c, CubeVertices_XN,
                    i,j,k,run_len,
                    l,l,l,
                    UV_XN,&acc);
                k += run_len;
            }
            for (int k = 0; k < CHUNK_H; )
            {
                block id = get_block(w,i+x,j,k+y);
                if (id == block_id_air) {
                    k++;
                    continue;
                }
                block n4 = get_block(w,i+x,j+1,k+y);
                if (!((n4 == block_id_air) || (!is_transparent[id] && is_transparent[n4]))) {
                    k++;
                    continue;
                }
                int run_len = 1;
                while ((k + run_len) < CHUNK_H)
                {
                    block id2 = get_block(w,i+x,j,k+y+run_len);
                    block n42 = get_block(w,i+x,j+1,k+y+run_len);
                    if (id2 != id)
                        break;
                    if (!((n42 == block_id_air) || (!is_transparent[id2] && is_transparent[n42])))
                        break;

                    run_len++;
                }
                float l = lighting(i+x,j,k+y);

                float u0 = texU0(0);
                float u1 = texU1(0);
                float v0 = texV0(id);
                float v1 = texV1(id);

                const float UV_YP[8] = {
                    u0, v0,
                    u0, v1,
                    u1*run_len, v1,
                    u1*run_len, v0,

                };

                emit_face_run(c, CubeVertices_YP,
                    i,j,k,run_len,
                    l,l,l,
                    UV_YP,&acc);
                k += run_len;
            }
            for (int k = 0; k < CHUNK_H; )
            {
                block id = get_block(w,i+x,j,k+y);
                if (id == block_id_air) {
                    k++;
                    continue;
                }
                block n5 = get_block(w,i+x,j-1,k+y);
                if (!((n5 == block_id_air) || (!is_transparent[id] && is_transparent[n5]))) {
                    k++;
                    continue;
                }
                int run_len = 1;
                while ((k + run_len) < CHUNK_H)
                {
                    block id2 = get_block(w,i+x,j,k+y+run_len);
                    block n52 = get_block(w,i+x,j-1,k+y+run_len);
                    if (id2 != id)
                        break;
                    if (!((n52 == block_id_air) || (!is_transparent[id2] && is_transparent[n52])))
                        break;
                    run_len++;
                }
                float l = lighting(i+x,j,k+y);
                
                float u0 = texU0(0);
                float u1 = texU1(0);
                float v0 = texV0(id);
                float v1 = texV1(id);

                const float UV_YN[8] = {
                    u1*run_len, v0,
                    u1*run_len, v1,
                    u0, v1,
                    u0, v0
                };
                
                emit_face_run(c, CubeVertices_YN,
                    i,j,k,run_len,
                    l,l,l,
                    UV_YN,&acc);

                k += run_len;
            }
            for (int k = 0; k < CHUNK_H; k++)
            {
                block id = get_block(w,i+x,j,k+y);
                if (id == block_id_air)
                    continue;
                float l = lighting(i+x,j,k+y);
                block n2 = get_block(w,i+x,j,k+y+1);
                if ((n2 == block_id_air) || (!is_transparent[id] && is_transparent[n2]))
                    emit_face(c, CubeVertices_ZP,
                        i,j,k,
                        l,l,l,
                        0,id,&acc);
                block n3 = get_block(w,i+x,j,k+y-1);
                if ((n3 == block_id_air) || (!is_transparent[id] && is_transparent[n3]))
                    emit_face(c, CubeVertices_ZN,
                        i,j,k,
                        l,l,l,
                        0,id,&acc);
            }
        }
    }

    printf("after occlusion: %d %d\n", c->vector_buffer_len, c->vector_buffer_allocated);
}

world* world_current;
void world_logic() {

    if (!world_current) {
        world_current = world_new();
        world_current->pivot.x = (int)camX - (int)camX % CHUNK_H;
        world_current->pivot.y = (int)camZ - (int)camZ % CHUNK_H;
    }
    
    void world_shift(world *w, int shift_i, int shift_j)
    {
        if (shift_i == 0 && shift_j == 0) return;
        chunk* newgrid[RENDER_DISTANCE][RENDER_DISTANCE] = {0};
        static_range(i, world_current->chunk)
        static_range(j, world_current->chunk[i]) {
            int ni = i - shift_i;
            int nj = j - shift_j;
            if (ni >= 0 && ni < RENDER_DISTANCE &&
                nj >= 0 && nj < RENDER_DISTANCE) {
                newgrid[ni][nj] = w->chunk[i][j];
            } else {
                chunk_free(w->chunk[i][j]);
                w->chunk[i][j] = NULL;
            }
        }
        memcpy(w->chunk, newgrid, sizeof(newgrid));
        // mark all existing chunks as needing vector buffer regen
/*
        static_range(i, world_current->chunk)
        static_range(j, world_current->chunk[i])
            if (w->chunk[i][j]) w->chunk[i][j]->is_vector_buffer_valid = 0;
*/
    }
    
    if (world_current) {
        int center = (RENDER_DISTANCE * CHUNK_H) / 2;
        int dx = camX - (world_current->pivot.x + center);
        int dz = camZ - (world_current->pivot.y + center);
        int hysteresis = 24;
        int shift_i = 0;
        int shift_j = 0;
        if (dx > hysteresis)
            shift_i = (dx - hysteresis) / CHUNK_H + 1;
        else if (dx < -hysteresis)
            shift_i = (dx + hysteresis) / CHUNK_H - 1;
        if (dz > hysteresis)
            shift_j = (dz - hysteresis) / CHUNK_H + 1;
        else if (dz < -hysteresis)
            shift_j = (dz + hysteresis) / CHUNK_H - 1;
        if (shift_i >  RENDER_DISTANCE) shift_i =  RENDER_DISTANCE;
        if (shift_i < -RENDER_DISTANCE) shift_i = -RENDER_DISTANCE;
        if (shift_j >  RENDER_DISTANCE) shift_j =  RENDER_DISTANCE;
        if (shift_j < -RENDER_DISTANCE) shift_j = -RENDER_DISTANCE;
        if (shift_i || shift_j) {
            world_shift(world_current, shift_i, shift_j);
            world_current->pivot.x += shift_i * CHUNK_H;
            world_current->pivot.y += shift_j * CHUNK_H;
        }
    }

    if (world_current) {
        static_range(i, world_current->chunk) {
            static_range(j, world_current->chunk[i]) {
                if (!world_current->chunk[i][j]) {
                    chunk* newchunk = chunk_new();
                    chunk_terrain(
                        newchunk,
                        world_current->pivot.x + i*CHUNK_H,
                        world_current->pivot.y + j*CHUNK_H
                    );
                    world_current->chunk[i][j] = newchunk;
                }
            }
        }
        printf("%d %d\n",world_current->pivot.x, world_current->pivot.y);
    }
    
    if (world_current) {
        static_range(i, world_current->chunk) {
            static_range(j, world_current->chunk[i]) {
                if (!world_current->chunk[i][j]->is_vector_buffer_valid) {
                    /* guarantee every chunk in the world is generated before updating vector arrays */
                    /* avoid updating vertex arrays on chunks with uninitialized neighbors */
                    if ((i > 0) && (i < (RENDER_DISTANCE-1)))
                    if ((j > 0) && (j < (RENDER_DISTANCE-1))) {
                        chunk_update_vector_array(
                            world_current,
                            world_current->chunk[i][j],
                            i*CHUNK_H + world_current->pivot.x,
                            j*CHUNK_H + world_current->pivot.y
                        );
                        world_current->chunk[i][j]->is_vector_buffer_valid = 1;
                    }
                }            
            }
        }
    }
    
    
}


/*
GLuint atlas[LIGHT_LEVELS];
*/
GLuint atlas;

typedef struct {
    chunk* c;
    int p;
    int i, j;
} chunk_sorting;    
int chunk_dist(const void* a, const void* b) {
    const chunk_sorting* A = a;
    const chunk_sorting* B = b;
    if (A->p < B->p) return -1;
    if (A->p > B->p) return 1;
    return 0;
}

unsigned tick = 0;

char do_occlusion = 1;

void display(void)
{
    tick++;
    int start = glutGet(GLUT_ELAPSED_TIME);

    if(!atlas) {
/*
    if (!atlas[0]) {
        char name[64];
        for(int i = 0; i < LIGHT_LEVELS; i++) {
            sprintf(name, "atlas%d.ppm", i);
            printf("loaded atlas %d %s\n", i, name); 
            atlas[i] = load_ppm_texture(name);
        }
*/
        atlas = load_ppm_texture("atlas.ppm");
    }

    world_logic();

    float cosPitch = cosf(camPitch * M_PI / 180.0f);
    float sinPitch = sinf(camPitch * M_PI / 180.0f);
    float cosYaw = cosf(camYaw * M_PI / 180.0f);
    float sinYaw = sinf(camYaw * M_PI / 180.0f);
    float dirX = cosPitch * sinYaw;
    float dirY = sinPitch;
    float dirZ = -cosPitch * cosYaw;
    glClearColor(
        0.0f/255.0f,
        0.0f/255.0f,
        0.0f/255.0f,
        0.0f/255.0f
    );
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(70.0,640.0/480.0,0.1,1000.0);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    gluLookAt(
        camX, camY, camZ,
        camX + dirX, camY + dirY, camZ + dirZ,
        0.0f, 1.0f, 0.0f
    );
    
/*
    glEnable(GL_FOG);
    glFogi(GL_FOG_MODE, GL_LINEAR);
    GLfloat fogColor[4] = {
        50.0f/255.0f,
        209.0f/255.0f,
        231.0f/255.0f,
        255.0f/255.0f
    };
    
    glFogi(GL_FOG_END, (RENDER_DISTANCE-2)*CHUNK_H);
    
    glFogfv(GL_FOG_COLOR, fogColor);
    glHint(GL_FOG_HINT, GL_NICEST);
*/



    glEnableClientState(GL_VERTEX_ARRAY);
/*
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    glEnableClientState(GL_COLOR_ARRAY);
*/
    
    float dirLen = sqrtf(dirX*dirX + dirZ*dirZ);

    int chunk_order_len = 0;
    
    chunk_sorting chunk_order[RENDER_DISTANCE * RENDER_DISTANCE];

    if (world_current) {
        static_range(i, world_current->chunk) {
            static_range(j, world_current->chunk[i]) {
                chunk* c = world_current->chunk[i][j];
                if (!c) continue;
                if (!c->vector_buffer) continue;
                if (!c->is_vector_buffer_valid) continue;

                chunk_order[chunk_order_len].c = c;
                int cx = (camX - (world_current->pivot.x + i*CHUNK_H + CHUNK_H/2));
                int cy = (camZ - (world_current->pivot.y + j*CHUNK_H + CHUNK_H/2));
                chunk_order[chunk_order_len].p = cx*cx + cy*cy;
                
                chunk_order[chunk_order_len].i = i;
                chunk_order[chunk_order_len].j = j;
                chunk_order_len++;
            }
        }
        qsort(chunk_order, chunk_order_len, sizeof(chunk_sorting), chunk_dist);
    }
    int occluded_counter  = 0;

    if ((tick % 4) == 0) {
        glDisable(GL_TEXTURE_2D);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        
        #define OCCLUSION_RES 128
        #define OCCLUSION_SIZE (OCCLUSION_RES * OCCLUSION_RES * 3)
        
        GLint old_viewport[4];
        glGetIntegerv(GL_VIEWPORT, old_viewport);
        
        glViewport(0, 0, OCCLUSION_RES, OCCLUSION_RES);

        for(int k = 0; k < chunk_order_len; k++) {

            int i = chunk_order[k].i;
            int j = chunk_order[k].j;
            chunk* c = world_current->chunk[i][j];
            
            c->visible = 0; /* make all chunks invisible */
            
            if (!c) continue;
            if (!c->vector_buffer) continue;
            if (!c->is_vector_buffer_valid) continue;
            
            int cx = (camX - (world_current->pivot.x + i*CHUNK_H + CHUNK_H/2));
            int cy = (camZ - (world_current->pivot.y + j*CHUNK_H + CHUNK_H/2));
            float c_len = sqrtf((cx * cx) + (cy * cy));
            float dot_dir_chunk = (cx / c_len) * (dirX / dirLen) + (cy / c_len) * (dirZ / dirLen);
            if (dot_dir_chunk > 0.5) continue;


            int num_vertices = c->vector_buffer_len / 8;
            GLfloat *buf = c->vector_buffer;

            const int stride = 8*sizeof(GLfloat);

            glVertexPointer(3, GL_FLOAT, stride, buf);
            glTexCoordPointer(2, GL_FLOAT, stride, buf+3);
            glColorPointer(3, GL_FLOAT, stride, buf+5);

            /* never set render distance to >255 chunks */
            /* color given an offset to not break in a black background */
            unsigned char g = i; 
            unsigned char b = j;
            glColor3ub(0,g,b);
            if (glLockArraysEXT && num_vertices) glLockArraysEXT(0, num_vertices);
            glDrawArrays(GL_QUADS, 0, num_vertices);
            if (glLockArraysEXT) glUnlockArraysEXT();

        }
        
       unsigned char buffer[OCCLUSION_SIZE];
        glReadPixels(
            0, 0,                         // x,y
            OCCLUSION_RES, OCCLUSION_RES, // width,height
            GL_RGB,                       // format
            GL_UNSIGNED_BYTE,             // type
            buffer
        );

        GLfloat curr_clear_color[4];
        glGetFloatv(GL_COLOR_CLEAR_VALUE, curr_clear_color);
        
        for(int i = 0; i < OCCLUSION_SIZE; i += 3) {
            unsigned char r = buffer[i+0];
            unsigned char g = buffer[i+1];
            unsigned char b = buffer[i+2];
            if (g != 0 || b != 0) {
                world_current->chunk[g][b]->visible = 1;
            }
        }
        
        glViewport(
            old_viewport[0],
            old_viewport[1],
            old_viewport[2],
            old_viewport[3]
        );
    }

    glDisableClientState(GL_VERTEX_ARRAY);

    glClearColor(
        50.0f/255.0f,
        209.0f/255.0f,
        231.0f/255.0f,
        255.0f/255.0f
    );
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    glEnableClientState(GL_COLOR_ARRAY);

    glEnable(GL_TEXTURE_2D);
/*
    glBindTexture(GL_TEXTURE_2D, atlas[1]);
*/
    glBindTexture(GL_TEXTURE_2D, atlas);

    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
    glColor3f(0, 0, 0);

    glEnable(GL_ALPHA_TEST);
    glAlphaFunc(GL_GREATER, 0.5f);
    
    
    if (world_current)
    for(int k = 0; k < chunk_order_len; k++) {
        int i = chunk_order[k].i;
        int j = chunk_order[k].j;
        chunk* c = chunk_order[k].c;
        if (!c) continue;
        if (!c->vector_buffer) continue;
        if (!c->is_vector_buffer_valid) continue;
        
        if (!chunk_order[k].c->visible) { occluded_counter++; continue; }
        

        int cx = (camX - (world_current->pivot.x + i*CHUNK_H + CHUNK_H/2));
        int cy = (camZ - (world_current->pivot.y + j*CHUNK_H + CHUNK_H/2));
        float c_len = sqrtf((cx * cx) + (cy * cy));
        float dot_dir_chunk = (cx / c_len) * (dirX / dirLen) + (cy / c_len) * (dirZ / dirLen);
        if (dot_dir_chunk > 0.5) continue;

        int num_vertices = c->vector_buffer_len / 8;
        GLfloat *buf = c->vector_buffer;

        const int stride = 8*sizeof(GLfloat);

        glVertexPointer(3, GL_FLOAT, stride, buf);
        glTexCoordPointer(2, GL_FLOAT, stride, buf+3);
        glColorPointer(3, GL_FLOAT, stride, buf+5);

        if (glLockArraysEXT && num_vertices) glLockArraysEXT(0, num_vertices);
        glDrawArrays(GL_QUADS, 0, num_vertices);
        if (glLockArraysEXT) glUnlockArraysEXT();
            
    }

    glDisableClientState(GL_COLOR_ARRAY);
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);
    glDisableClientState(GL_VERTEX_ARRAY);


    int end = glutGet(GLUT_ELAPSED_TIME);

    printf("time: %d ms\n", end - start);
    printf("occluded %d chunks\n", occluded_counter);

    glutSwapBuffers();

    float radYaw = camYaw * M_PI / 180.0f;
    float radPitch = camPitch * M_PI / 180.0f;
    float forwardX = sinf(radYaw);
    float forwardZ = -cosf(radYaw);
    if (keys['w']) { camX += forwardX * moveSpeed; camZ += forwardZ * moveSpeed; }
    if (keys['s']) { camX -= forwardX * moveSpeed; camZ -= forwardZ * moveSpeed; }
    if (keys['d']) { camX -= forwardZ * moveSpeed; camZ += forwardX * moveSpeed; }
    if (keys['a']) { camX += forwardZ * moveSpeed; camZ -= forwardX * moveSpeed; }
    if (keys[' ']) { camY += 0.1f; }
    if (keys['z']) camY -= 0.1f;

}

int windowWidth, windowHeight;
void reshape(int w, int h) {
    windowWidth = w;
    windowHeight = h;

	glViewport(0, 0, w, h);

    glScissor(0,0,w,h);
    glEnable(GL_SCISSOR_TEST);

    glEnable(GL_CULL_FACE);
    glCullFace(GL_BACK);

    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LESS);

	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glOrtho(-1,1,-1,1,-1,1);
	glMatrixMode(GL_MODELVIEW);
}

void timer(int v) {
	glutPostRedisplay();
	glutTimerFunc(16,timer,0);
}

void keyDown(unsigned char key, int x, int y) {
    keys[key] = 1;
	if (key == 'q') {
		exit(0);
	}
    if (do_occlusion == 'p') do_occlusion = !do_occlusion;
    
    if (key == 'u') {
        static_range(i, world_current->chunk) {
            static_range(j, world_current->chunk[i]) {
                free(world_current->chunk[i][j]);
                world_current->chunk[i][j] = 0;
            }
        }
        world_current->pivot.x+=16;
    }
}
void keyUp(unsigned char key, int x, int y) { keys[key] = 0; }

void mouseMotion(int x, int y) {
    int centerX = windowWidth / 2;
    int centerY = windowHeight / 2;
    int dx = x - centerX;
    int dy = y - centerY;
    if (dx == 0 && dy == 0) return; // ignore warp events
    camYaw   += dx * mouseSensitivity;
    camPitch += -dy * mouseSensitivity;
    if (camPitch > 89.0f) camPitch = 89.0f;
    if (camPitch < -89.0f) camPitch = -89.0f;
    glutWarpPointer(centerX, centerY);
}

int main(int argc, char** argv) {
    srand(time(NULL));   // seed random generator
    seed = rand();      // random number
    
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
	glutInitWindowSize(640,480);
	glutInitWindowPosition(100,100);
	glutCreateWindow("Opengl Window");
    glutSetCursor(GLUT_CURSOR_NONE);
    
    glLockArraysEXT = (void*)glutGetProcAddress("glLockArraysEXT");
    glUnlockArraysEXT = (void*)glutGetProcAddress("glUnlockArraysEXT");
    glEnable(GL_DEPTH_TEST);
    
	glutDisplayFunc(display);
	glutReshapeFunc(reshape);
    
    glutKeyboardFunc(keyDown);
    glutKeyboardUpFunc(keyUp);
    glutPassiveMotionFunc(mouseMotion);
    
	glutTimerFunc(16,timer,0);
	glutMainLoop();
}