#define GL_GLEXT_PROTOTYPES
#define _USE_MATH_DEFINES

#include <uthash.h>
#include <ft2build.h>
#include FT_FREETYPE_H

//#if defined(__IPHONE__) || defined(__ANDROID__)
//    //#include "SDL_opengles2.h"
//    //#include "SDL_opengles2_gl2.h"
//    //#include "SDL_opengles2_gl2ext.h"
//#elif defined(__APPLE__)
//    //#include <OpenGL/gl3.h>
//    //#include <SDL2/SDL_opengl.h>
//#elif defined(_WIN32) ?
typedef __PTRDIFF_TYPE__ ptrdiff_t;
#if defined(_WIN32)
    #if defined(__GNUC__)
        #include <cpuid.h>
    #elif defined(_MSC_VER)
        #include <intrin.h>
    #endif
    //#define APIENTRY __stdcall
    #include <windows.h>
    #include <errno.h>
    #include <pthread.h>
    #define GLEW_STATIC
    #include <GL/glew.h>
    //#include "glew/glew.h"
    //#include <GL/gl.h>
    //#include <GLES2/gl2.h>
    //#include <GLES2/gl2ext.h>
    //#define GLFW_INCLUDE_ES3
    #define GLFW_EXPOSE_NATIVE_WGL
    #define GLFW_EXPOSE_NATIVE_WIN32
#else
    #include <pthread.h>
    #include <GL/gl.h>
    //#include <GL/glext.h>
    #define GLFW_EXPOSE_NATIVE_X11
    #define GLFW_EXPOSE_NATIVE_GLX
#endif

#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>
#include <GLFW/glfw3native.h>

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
//#include "lodepng.h" // alternative to stb_image, may be slower

//#define STB_IMAGE_WRITE_IMPLEMENTATION
//#include "stb_image_write.h" //Just for test purposes
 
#include "linmathv2.h"

#include <math.h>
#ifndef M_PI
#define M_PI 3.14159265358979323846264338327950288
#endif
#define degToRad(angleInDegrees) ((angleInDegrees) * M_PI / 180.0)
#define radToDeg(angleInRadians) ((angleInRadians) * 180.0 / M_PI)

#define MIN(a,b) (((a)<(b))?(a):(b))
#define MAX(a,b) (((a)>(b))?(a):(b))
#define CLAMP(x, upper, lower) (MIN(upper, MAX(x, lower)))

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdarg.h>
#include <time.h>

#include "custom.h" // early declarations

#include "cjson/cJSON.h" // https://github.com/DaveGamble/cJSON
#include "ufbx/ufbx.h" // https://github.com/ufbx/ufbx

#include "input.h" // input glfw callbacks/mouse
#include "shaders.h" // shaders before text/sky/terrain etc
#include "text.h" // 
#include "geometry.h" // geometry before sky/terrain/player etc
#include "sky.h" //
#include "terrain.h" // 
#include "water.h" // 
#include "fire.h" // 
#include "player.h" // 
#include "network.h" // 

// includes for Sleep POSIX etc
#ifdef _WIN32
#include <Windows.h>
#else
#include <unistd.h>
#endif

#define COMPARE(a, b) (((a) > (b)) - ((a) < (b)))

#define nullptr ((void*)0)

#define vstr(s) str(s)
#define str(s) #s

//--void GLAPIENTRY
//--MessageCallback( GLenum source,
//--                 GLenum type,
//--                 GLuint id,
//--                 GLenum severity,
//--                 GLsizei length,
//--                 const GLchar* message,
//--                 const void* userParam )
//--{
//--  fprintf( stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
//--           ( type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : "" ),
//--            type, severity, message );
//--}
 
static void error_callback(int x, const char* description)
{
    fprintf(stderr, "Error (%d): %s\n",x,description);
}

// Global variables
GLFWwindow* window;
pthread_cond_t ready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;
int should_render = 0; // Start 0 or segfault createframebuffer
uint8_t more = 1;
unsigned int smallest_side = 0;


// Renderer function
void render() {
    // all render stuff here
    int camMode = 2;

    //double deltaLapsed = 0;
    //waterTime = updateCurrentTime;
    double phase = fmod(updateCurrentTime / 86400.0 * 2.0 * M_PI, 2.0 * M_PI);
    waterTime = (float)phase;

    renderCurrentTime = glfwGetTime();
    if(renderLastTime != 0) {
        renderDeltaTime = renderCurrentTime - renderLastTime;
        //deltaLapsed += renderDeltaTime / (1.0f / limitFPS); // game loop limit not vsync

        avgfps=renderDeltaTime;
        //avgupdates=1.0f/fixedDeltaTime;
                                                      //
        if(camMode == 2) { // both 3rd and 1st person zoomDist 0.01f
            
            while (camera->theta > 180.0) { camera->theta -= 360.0; }
            while (camera->theta < -180.0) { camera->theta += 360.0; }
            camera->phi = CLAMP(camera->phi, 179.0f, 1.0f);
            player->phi = CLAMP(player->phi, 179.0f, 1.0f);

            getDirection(camera->dir,camera->theta,camera->phi); // player.c
            while (camera->xOffset > 180.0) { camera->xOffset -= 360.0; }
            while (camera->xOffset < -180.0) { camera->xOffset += 360.0; }

            camera->eye->position->x=camera->dir->forward->x * zoomDist; // camera dir
            camera->eye->position->y=camera->dir->forward->y * zoomDist; // to player/
            camera->eye->position->z=camera->dir->forward->z * zoomDist; // camera center

            //getDirection(player->dir,player->theta,player->phi); // player.c
            //
            //Quaternion qrot=directionToQuaternion(player=>dir->forward);
            //Vec3 erot=ToEulerAngles(qrot);
            //quaternion_rotation(qrot, up, anglerad);
        }
        vec3 eye={
            camera->eye->position->x + player->transform->position->x,
            camera->eye->position->y + player->transform->position->y,
            camera->eye->position->z + player->transform->position->z
        };
        vec3 center={
            camera->center->x + player->transform->position->x,
            camera->center->y + player->transform->position->y,
            camera->center->z + player->transform->position->z
        };
        // set up "view" (v)
        // p and mvp is updated throughout
        mat4x4_look_at(v,eye,center,up);

        // inverted view et only once
        mat4x4_invert(iv,v);

        glColorMask (true, true, true, true);

        Vec3 pdir={0,0,0};
        float diffXoffset = camera->theta+camera->xOffset;
        while (diffXoffset > 180.0) { diffXoffset -= 360.0; }
        while (diffXoffset < -180.0) { diffXoffset += 360.0; }
        float usePhi=camera->phi;
        if(moveMode==1) usePhi=player->phi;

        pdir=getDirection(player->dir,diffXoffset,usePhi); // player.c
        
        if(readyXoffset && applyXoffset!=0) { // if not aliged keep offset
            if(applyXoffset==1) { // camera to player
                camera->theta += camera->xOffset;
                camera->xOffset = 0;
                applyXoffset = 0;
                readyXoffset = false;
            } else if(applyXoffset==2) { // player to camera
                camera->xOffset = 0;
                applyXoffset = 0;
                readyXoffset = false;
            }
        }
        if(moveMode==1 && isSwimming) {
            pdir.x = player->dir->forward->x;
            pdir.y = player->dir->forward->y;
            pdir.z = player->dir->forward->z;
        }
        player->transform->rotation->x = pdir.x;
        player->transform->rotation->y = pdir.y;
        player->transform->rotation->z = pdir.z;
        //printf("pdir.x=%f, pdir.y=%f, pdir.z=%f, theta=%f, phi=%f, xOffset=%f\n",pdir.x,pdir.y,pdir.z,camera->theta,camera->phi,camera->xOffset);

    
        glBindFramebuffer(GL_FRAMEBUFFER, 0);

        glDepthMask(GL_FALSE);
        glClear(GL_COLOR_BUFFER_BIT);
        glClearColor(0.5f, 0.0f, 0.5f, 1.0f); // pink bg / outside scope but within final fbo


        //CreateFrameText();
        glBindFramebuffer(GL_FRAMEBUFFER, textFB); //bind both read/write to the target framebuffer
        glViewport(0, 0, TXT_WIDTH, TXT_HEIGHT); // Update viewport

        // --- Sentences are constructed into reusable images
        PrepareAllText(); // upscale ortho mode, linebreaks not implemented

        glBindFramebuffer(GL_FRAMEBUFFER, 0);
    
        if(hiddenWater==0) {
            glViewport(0, 0, WFO_WIDTH, WFO_HEIGHT); // Update viewport
            glBindFramebuffer(GL_FRAMEBUFFER, heightFB);

            glClear(GL_COLOR_BUFFER_BIT);
            glClearColor(0.0, 0.0, 0.0, 0.0);

            RenderWaterHeight(); // custom buffer in water.c

            glBindFramebuffer(GL_FRAMEBUFFER, 0);
        }
    
        glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT); // Update viewport

        //glBindFramebuffer(GL_FRAMEBUFFER, msaaFB);
   
        glDepthMask(GL_TRUE);
        glEnable(GL_DEPTH_TEST); // enable death before death clear
        glEnable(GL_CULL_FACE);

        //glEnable(GL_STENCIL_TEST);
        //glStencilMask(0x00); // == false;false,false,false

        glBlendFunc(GL_ONE, GL_ZERO);
        glDisable(GL_BLEND);

        if(click_trace!=0) { // 1 = lMB, 2 = RMB
            //glDepthMask(GL_TRUE); // true before glClear or glClear will fail clearing
            //glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
            //glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // background framebuffer
            //glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

            // draw GL_FLAT for click target with glReadPixel
            // custom flat color shader
            printf("click_trace=%d mouseX=%f, mouseY=%f\n",click_trace,lockMouseX,lockMouseY);
            click_trace=0;
        }

        // -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        // render stuff to mainFB (before msaa antialias prepared framebuffer)
        glBindFramebuffer(GL_FRAMEBUFFER, msaaFB);
        glClearColor(0.1f, 0.11f, 0.17f, 1.0f); // background framebuffer dim blue grey
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        
        //if(!wireframeOn && gridOn) {
        if(gridOn) {
            // WORLD 
            RenderGridLines(); // as a static backdrop, conforms to camera
            RenderGridLines2(); // as a static backdrop, conforms to camera
            RenderGridLines3(); // as a static backdrop, conforms to camera
        }
        
        if(wireframeOn) { // on
            glPolygonMode( GL_FRONT_AND_BACK, GL_LINE );
            //glPolygonMode( GL_FRONT_AND_BACK, GL_FILL );
        }

        if(!hiddenTerrain) {
            RenderTerrain();
        }
        if(enableTracking) {
            setRenderModePerspective();
            execRenderModePerspective(v);
            RenderLine(0,0,0,player->transform->position->x,player->transform->position->y,player->transform->position->z,1,mvp);
        }
        if(hiddenSky==1) { // sphere sky
            if(skytexloaded==1) {
                unloadSkyImages();
            }
            RenderSky();
        } else if(hiddenSky==2) { // box sky
            if(skytexloaded!=1) {
                loadSkyImages();
            }
            drawSkyBox();
        } else {
            if(skytexloaded==1) {
                unloadSkyImages();
            }
        }
        if(hiddenCloud==1) {
            RenderClouds();
        }

        RenderSpinningQuad(); // as a dynamic object, conforms to camera

        RenderCube(); // player

        RenderStair(); // blocks
        

        glBindFramebuffer(GL_FRAMEBUFFER, 0);
        glBindFramebuffer(GL_FRAMEBUFFER, mainFB);
        glDepthMask(GL_TRUE);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
        // blit depth here
        glColorMask(false, false, false, false);
        glBindFramebuffer(GL_READ_FRAMEBUFFER, msaaFB);
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mainFB); // -water
        //glReadBuffer(GL_DEPTH_ATTACHMENT);
        //glDrawBuffer(GL_DEPTH_ATTACHMENT);
        glBlitFramebuffer(0,0,SCR_WIDTH,SCR_HEIGHT,0,0,SCR_WIDTH,SCR_HEIGHT,GL_DEPTH_BUFFER_BIT,GL_NEAREST);
        //glBindFramebuffer(GL_FRAMEBUFFER, 0);
        glColorMask (true, true, true, true);
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
        
        glBindFramebuffer(GL_FRAMEBUFFER, msaaFB); // cont.
                      //
        // render 3D text
        RenderAllText3D(0); // render text behind water
        RenderFireQuad(0);
        RenderFireBall(0); // pre render transparent on transparent
        RenderVortexQuad(0);
        if(hiddenWater==0) {
            RenderWater(); // water intersects with text now works
        }

        // copy depth of main before water is rendered,
        // have two depth terrain without water and with to compare
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
        glBindFramebuffer(GL_FRAMEBUFFER, depthFB);
        glDepthMask(GL_TRUE);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
        // blit depth here
        glColorMask(false, false, false, false);
        glBindFramebuffer(GL_READ_FRAMEBUFFER, msaaFB);
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, depthFB); // +water
        //glReadBuffer(GL_DEPTH_ATTACHMENT);
        //glDrawBuffer(GL_DEPTH_ATTACHMENT);
        glBlitFramebuffer(0,0,SCR_WIDTH,SCR_HEIGHT,0,0,SCR_WIDTH,SCR_HEIGHT,GL_DEPTH_BUFFER_BIT,GL_NEAREST);
        //glBindFramebuffer(GL_FRAMEBUFFER, 0);
        glColorMask (true, true, true, true);
        glBindFramebuffer(GL_FRAMEBUFFER, 0);

        // custom stencil to depthFB color sample in red
        glBindFramebuffer(GL_FRAMEBUFFER, depthFB);

        RenderAllText3D(-1); // render text in depthFBsample
        RenderFireQuad(-1); // depth
        RenderFireBall(-1); // depth
        RenderVortexQuad(-1); // depth
        RenderAllLines(-1); // render text in depthFBsample
        
        glBindFramebuffer(GL_FRAMEBUFFER, 0);

        glBindFramebuffer(GL_FRAMEBUFFER, msaaFB); // cont.


        RenderAllText3D(0); // render text front of water?, no stencil needed
        RenderFireQuad(0);
        RenderFireBall(0);
        RenderVortexQuad(0);

        if(wireframeOn) { // off - opengl is a "state machine"
            //glPolygonMode( GL_FRONT_AND_BACK, GL_LINE );
            glPolygonMode( GL_FRONT_AND_BACK, GL_FILL );
        }

        if(boundsTog) {
            RenderAllBounds();
            bidx=0; // reset bound list index
        }

        RenderAllLines(0);
        lidx=0; // reset all single line draw
        
        glBindFramebuffer(GL_FRAMEBUFFER, 0);


        glBindFramebuffer(GL_FRAMEBUFFER, uiFB);
        glDepthMask(GL_FALSE);
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // no bg and transparent ui
        glClear(GL_COLOR_BUFFER_BIT);


        if(waterHeightShow) {
            //smallest_side = MAX(SCR_HEIGHT,SCR_WIDTH);
            smallest_side = MIN(SCR_HEIGHT,SCR_WIDTH);
            //smallest_side = smallest_side / 3;
            unsigned int centerleft = 0;
            unsigned int centerbot = 0;
            if(SCR_WIDTH>SCR_HEIGHT) {
                centerleft = (SCR_WIDTH - smallest_side) / 2;
            } else {
                centerbot = (SCR_HEIGHT - smallest_side) / 2;
            }
            //
            //
            // preview water framebuffer overlay
            glColorMask(false, false, false, false);
            glBindFramebuffer(GL_FRAMEBUFFER, 0);
            glBindFramebuffer(GL_FRAMEBUFFER, uiFB);
            glDepthMask(GL_FALSE);
            // blit depth here
            glBindFramebuffer(GL_READ_FRAMEBUFFER, heightFB);
            glBindFramebuffer(GL_DRAW_FRAMEBUFFER, uiFB);
            glReadBuffer(GL_COLOR_ATTACHMENT0);
            glDrawBuffer(GL_COLOR_ATTACHMENT0);
            glBlitFramebuffer(0,0,WFO_WIDTH,WFO_HEIGHT,centerleft,centerbot,smallest_side+centerleft,smallest_side+centerbot,GL_COLOR_BUFFER_BIT,GL_NEAREST);
            glColorMask (true, true, true, true);
            glBindFramebuffer(GL_FRAMEBUFFER, 0);
            glBindFramebuffer(GL_FRAMEBUFFER, uiFB);
        }


        // UI
        RenderAllText2D(0); // as a 2d interface, conforms to screen(buffer)
        
        
        //
        //
        // all post processing start here
        // ---
        glBindFramebuffer(GL_FRAMEBUFFER, 0); // to screen buffer

        glDisable(GL_CULL_FACE);
        glDisable(GL_DEPTH_TEST);
        glDepthMask(GL_FALSE);
        
        glUseProgram(0);
        glUseProgram(programPost);

        glBindVertexArray(quadVAO); // bound here until the end~

        setUniform1i(&programPost, "viewport_width", SCR_WIDTH);
        setUniform1i(&programPost, "viewport_height", SCR_HEIGHT);
        setUniform1i(&programPost, "samples", samples);
        //setUniform1i(&programPost, "waterDepthTex", depthFBsample);
        setUniform1i(&programPost, "screenTexture", 0);
        setUniform1i(&programPost, "waterDepthTex", 1);
        setUniform1i(&programPost, "sceneDepthTex", 2);
        setUniform1i(&programPost, "sceneStencilTex", 3);
        setUniform1i(&programPost, "waterHeightTex", 4);
        setUniform1i(&programPost, "rendMode", rendMode);
        setUniform1f(&programPost, "time", waterTime);
        setUniform1f(&programPost,"daytime", daytime);

        setUniform4m(&programPost, "invProjection", ip);
        setUniform4m(&programPost, "invView", iv);
        setUniform3f(&programPost, "cameraPosition", eye[0], eye[1], eye[2]);
        setUniform1f(&programPost, "fogDistance", rendMode);
        setUniform3f(&programPost, "fogColor", 0.0, 0.2, 0.3);

        setUniform3f(&programPost,"data", (float)hiddenWater, -1.0f, 2.0f); // time,y-offset,scale


        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, msaaSample); // custom.c

        glActiveTexture(GL_TEXTURE0 + 1);
        glBindTexture(GL_TEXTURE_2D, mainFBdepth); // water.c (-water)

        glActiveTexture(GL_TEXTURE0 + 2);
        glBindTexture(GL_TEXTURE_2D, depthFBdepth); // water.c (+water)
        
        glActiveTexture(GL_TEXTURE0 + 3);
        glBindTexture(GL_TEXTURE_2D, depthFBsample); // water.c (stencil)

        glActiveTexture(GL_TEXTURE0 + 4);
        glBindTexture(GL_TEXTURE_2D, heightFBsample); // water.c

        glDrawArrays(GL_TRIANGLES, 0, 6); // final draw frame to screen, conforms to screen

        //glBindVertexArray(0);

        glBindTexture(GL_TEXTURE_2D, 0); // unbind 3
        
        glActiveTexture(GL_TEXTURE0 + 3);
        glBindTexture(GL_TEXTURE_2D, 0); // unbind 2
                                         //
        glActiveTexture(GL_TEXTURE0 + 2);
        glBindTexture(GL_TEXTURE_2D, 0); // unbind 2
        
        glActiveTexture(GL_TEXTURE0 + 1);
        glBindTexture(GL_TEXTURE_2D, 0); // unbind 1
                                         //
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0); // unbind 0
        


        // draw full text/ui quad, cant use blit with transparency
        glUseProgram(0);
        glUseProgram(programUI); // as spinning quad? simple tex or UI

        //glBindVertexArray(quadVAO); // quadVAO rebound from here and out
        
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

        //setUniform1i(&programMsaa, "samples", samples);
        //setUniform1i(&program, "texture1", 0);
        setUniform1i(&programUI, "sampler", 0);
        setUniform1f(&programUI, "ris", 0.0f);
        setUniform1f(&programUI, "alp", 1.0f);


        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, uiFBsample);

        glDrawArrays(GL_TRIANGLES, 0, 6); // final draw frame to screen, conforms to screen

        glBindVertexArray(0);

        glBindTexture(GL_TEXTURE_2D, 0);

        glBlendFunc(GL_ONE, GL_ZERO);
        glDisable(GL_BLEND);

        // -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        //return 0;
    }
    renderLastTime = renderCurrentTime;
}

int update() { // render section below
    handle_move(); // player.c

    //if(!readyZoomTog) {
    //    readyZoomTog = true;
    //}
    // game mechanics here
    //
    if(reloadShaders) {
        cleanupAllShaders(1);
        reloadShaders=false;
    }
    
    if(reloadFont) {
        cleanupFonts(1);
        reloadFont=false;
    }
    return 0;
}

void* thread_render(__attribute__((unused)) void* arg) {
    //GLFWwindow* window = (GLFWwindow*)arg;

    while(more) {
        // Acquire the mutex lock
        pthread_mutex_lock(&glock);
        while(!should_render) {
            pthread_cond_wait(&ready, &glock);
        }
        should_render = 0;

        // Make the OpenGL context current for this thread
        glfwMakeContextCurrent(window);
        //more=!glfwWindowShouldClose(window);

        render();
        frames++;
        // Swap buffers to display the rendered content
        glfwSwapBuffers(window);

        // Release the OpenGL context
        glfwMakeContextCurrent(NULL);
        // Release the mutex lock
        pthread_mutex_unlock(&glock);

        // Optional: sleep to reduce CPU usage (adjust as needed)
        //usleep(16000); // ~60 FPS    
    }
    // ending
    pthread_mutex_unlock(&glock);

    return NULL;
}



float readyResize=0.0f;
bool resized = false;
void* thread_main(__attribute__((unused)) void *arg) {
    //GLFWwindow* window = (GLFWwindow*)arg;

    #if defined(_WIN32)
        glewExperimental = true; // Needed for core profile, comment out if not using glew?
    #endif
    if (!glfwInit())
    {
        fprintf(stderr, "Failed to initialize GLFW\n");
        exit(EXIT_FAILURE);
    }

    window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "AnaTwi Development LeanOpenGL", NULL, NULL); // start window
                                                                                                            //
    ratio = SCR_WIDTH / (float) SCR_HEIGHT;

    TXT_HEIGHT=SCR_HEIGHT * txt_quality;
    TXT_WIDTH=SCR_WIDTH * txt_quality;

    //TXT_HEIGHT=to_nearest_pow2(TXT_HEIGHT);
    //TXT_WIDTH=to_nearest_pow2(TXT_WIDTH);

    glfwSetErrorCallback(error_callback);

    //glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    glfwSetScrollCallback(window, scroll_callback);

    glfwSetMouseButtonCallback(window, mouse_btn_callback);
    glfwSetCursorPosCallback(window, mouse_mov_callback);

    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);

    #if defined(__APPLE__)
        glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
    #endif

    glfwWindowHint(GLFW_SAMPLES, samples);
    glEnable(GL_MULTISAMPLE);
    glEnable(GL_LINE_SMOOTH);
	glHint( GL_GENERATE_MIPMAP_HINT, GL_NICEST);

    monitor = glfwGetPrimaryMonitor();
    vmode = glfwGetVideoMode(monitor);
    //GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", monitor, NULL); // start fullscreen
    //
    if (!window)
    {
        glfwTerminate();
        fprintf(stderr, "Failed to create GLFW window\n");
        exit(EXIT_FAILURE);
    }
    
    //glfwSetWindowSize(window, 1920, 1080); // set window size for fullscreen
    //glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
    glfwSetInputMode(window, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE);
    //
    //glfwSetWindowCloseCallback(window, window_close_callback);
    
    glfwMakeContextCurrent(window); //initgl
    
    // Initialize the mutex
    if (pthread_mutex_init(&glock, NULL) != 0) {
        fprintf(stderr, "Failed to initialize mutex\n");
        glfwDestroyWindow(window);
        glfwTerminate();
        exit(EXIT_FAILURE);
    } 
    
    #if defined(_WIN32)
    if (glewInit() != GLEW_OK) // comment out if not using glew ?
    {
        glfwTerminate();
        exit(EXIT_FAILURE);
    }
    #endif

    printf("GL_VENDOR=%s\n",glGetString(GL_VENDOR));
    printf("GL_RENDERER=%s\n",glGetString(GL_RENDERER));
    printf("GL_VERSION=%s\n\n",glGetString(GL_VERSION));
 
    if(vsync == 2) { // adaptive
        glfwSwapInterval(-1);
    } else if(vsync == 1) { // on
        glfwSwapInterval(1);
    } else { // off
        glfwSwapInterval(0);
    }

    // --- Scene Frame and Shader Initialization
    printf("AAA\n");
    loadAllShaders();
    InitializeShadersAll();

    InitializeTextures();

    InitializeSpinningQuad();
    InitializeCube(); // used for player, can re use for other~
    InitializeText();

    InitializeFireBall();
    
    CreateFrameHeight(); // fixed frame does not recreate

    CreateFrameText();
    CreateFrameUI();
    CreateFrameDepth();
    CreateFrameMain();
    CreateFrameMsaa();

    InitializeSky();
    InitializeTerrain();
    //InitializeWater();
    generateWaterMesh(&waterVAO,&waterVBO,&waterEBO);
    
    printKeys();
    // ---
    
    glEnable(GL_CULL_FACE);
    glBlendFunc(GL_ONE, GL_ZERO);
    glDisable(GL_BLEND);

    fovrad=degToRad(FOV);
    radian=degToRad(1);
    glActiveTexture(GL_TEXTURE0); // all only uses _0_ currently
    glColorMask (true, true, true, false); // avoid alpga on main buffer


    struct Vec3 *pos = malloc(sizeof(struct Vec3));
    struct Vec3 *rot = malloc(sizeof(struct Vec3));

    int width,height;
    // --------- runtime loop
    while(more) // thread_main
    {
        //glfwPollEvents();
        glfwWaitEventsTimeout(0.5/limitFPS); // split 60 between update and render max?

        // Acquire the mutex lock
        pthread_mutex_lock(&glock);
        // Make the OpenGL context current
        glfwMakeContextCurrent(window);
        more=!glfwWindowShouldClose(window);
       

        if(readyFullScreen==1) {
            if(fullScreen==1) {
                SCR_WIDTH=lastSW;
                SCR_HEIGHT=lastSH;

                glfwSetWindowMonitor(window, NULL, 0, 0, SCR_WIDTH,SCR_HEIGHT, vmode->refreshRate); // set window
                //
                
                fullScreen=0;
            } else {
                lastSW=SCR_WIDTH;
                lastSH=SCR_HEIGHT;

                glfwGetMonitorWorkarea(monitor,&mX,&mY,&mW,&mH);
                printf("mX=%d mY=%d mW=%d mH=%d\n",mX,mY,mW,mH);

                glfwSetWindowMonitor(window, monitor, mX, mY, mW, mH,GLFW_REFRESH_RATE); // set fullscreen
                //
                
                fullScreen=1;
            }
            if(readyResize<=0.0f) {
                readyResize=.01f;
            }
            resized=true;
            readyFullScreen=2;
            spamonce=0;
        }

        // Check for window resize
        glfwGetWindowSize(window, &width, &height);
        if ((width != (int)SCR_WIDTH || height != (int)SCR_HEIGHT)) {
            
            SCR_WIDTH = (unsigned int)width;
            SCR_HEIGHT = (unsigned int)height;
            ratio = SCR_WIDTH / (float) SCR_HEIGHT;

            TXT_HEIGHT=SCR_HEIGHT * txt_quality;
            TXT_WIDTH=SCR_WIDTH * txt_quality;

            //TXT_HEIGHT=to_nearest_pow2(TXT_HEIGHT);
            //TXT_WIDTH=to_nearest_pow2(TXT_WIDTH);

            if(readyResize<=0.0f) {
                readyResize=.01f;
            }
            resized=true;
        }

        if(resized && readyResize<=0.0f) {
            glViewport(0, 0, width, height); // Update viewport
            
            // recreate size dependent framebuffers
            CreateFrameText();
            CreateFrameUI();
            CreateFrameDepth();
            CreateFrameMain();
            CreateFrameMsaa();

            rerenderText=1;

            render();

            resized=false;
            readyResize = 0.0f;

            // resize down may appear instant because SCR_WIDTH/HEIGHT is set instant and frame buffer it renders to remain larger than needed, second resize function is for things that should be controlled in not bein re-recreated to often, keep the readyResize belo 0.1f, as low as 0.01f, can be tested at 1f to 10f
            printf("resize\n");
        } else if(readyResize>0.0f) {
            readyResize-=updateDeltaTime;
        }

        updateCurrentTime = glfwGetTime();
        if(updateLastTime != 0) {
            updateDeltaTime = updateCurrentTime - updateLastTime;
            updateDeltaLapsed += updateDeltaTime / (1.0f / limitFPS); // game loop limit not vsync

            avgupdates=updateDeltaTime;
            sprintf(fpstext, "Ums: %f, Rms: %f, Fms:%f, CT:%f", avgupdates, avgfps,avgfixed,updateCurrentTime);

            rot->x=0.0f; rot->y=0.0f; rot->z=0.0f;
            pos->x=0.0f; pos->y=0.0f; pos->z=0.0f;
            UpdateText(1, pos, rot, 1.0f, Align.topleft);

            while(updateDeltaLapsed >= 1.0f) {
                fixedDeltaTime = updateCurrentTime - lastFixedFrameTime;
                avgfixed=fixedDeltaTime;

                if(lastFixedFrameTime != 0) {
                    update();  // - Update function use fixeddelta
                    updates++;



                    updateDeltaLapsed--;
                    lastFixedFrameTime=updateCurrentTime;
                } else {
                    lastFixedFrameTime=updateCurrentTime;
                }
            }
            processInput(window); // do not calc movement here --

            totalTime += updateDeltaTime * timeSpeed;

            if(hiddenSky==1) {
                //daytime=fabs((86400*0.5f)-totalTime)/43200; // 1-0-1 night-day-night
                daytime=((86400*0.5f)-totalTime)/43200; // 1-0-1 night-day-night
                //printf("daytime = %f, totalTime=%f\n",daytime,totalTime);
            } else {
                daytime=0.0f; // 1-0-1 night-day-night
            }
            if(hiddenCloud==0) {
                cloudAmp=0.0; // cloud disabled, terrain shader amp light
            }
            saveCount-=updateDeltaTime;
            if(updateCurrentTime - timer > 1.0) {
                timer++;

                while(totalTime>86400) {
                    totalTime-=86400;
                    ++lapsedDays;
                }

                float hhT = totalTime / 60 / 60;
                hh = floor(hhT);
                hhT -= hh;
                float mmT = hhT * 60;
                mm = floor(mmT);
                mmT -= mm;
                ss = mmT * 60;

                sprintf(timertext, "Localtime: %d:%02d:%02d, Day: %d", hh,mm,ss,lapsedDays);
                rot->x=0.0f; rot->y=0.0f; rot->z=0.0f;
                pos->x=0.0f; pos->y=0.0f; pos->z=0.0f;
                UpdateText(2, pos, rot, 1.0f, Align.topright);

                if(saveCount<=0.0f) {
                    saveGame();
                    saveCount=saveTime;
                    printf("Game saved. (Game local time=%d:%02d:%02d\n",hh,mm,ss);
                //} else {
                //    printf("next save in %02f\n",saveCount);
                }
            }
        } else {
            timer = glfwGetTime(); // start time
        }
        updateLastTime = updateCurrentTime;
        //continue;
        
        // Release the context
        glfwMakeContextCurrent(NULL);
        // Release the mutex lock
        should_render = 1;
        pthread_cond_signal(&ready);
        pthread_mutex_unlock(&glock);
    }

    return NULL;
}

// custom list sorting
int compare(const void *a, const void *b) {
    float fa = *(const float*) a;
    float fb = *(const float*) b;
    return COMPARE(fa, fb);
}

// strcmp default compare ascii values, use custom str_cmp
bool str_cmp(char *str1, char *str2, int len) {
    bool match=true;
    for(int i=0;i<len;i++) { // matches char to char
        if(str1[i]!=str2[i]) { // but require input len
            match=false;
            break;
        }
    }
    return match;
}

// custom string split
char *str_split(char *str, char *d, int *rows, int *chunk) { // return rows and chunk
    int N=1;
    int chunks=100; // max input string
    char *sub=malloc(sizeof(char)*100*N); // chunk is 100 = 1 line
    int splits=0;
    int c=0;
    bool endofstring=false;
    for(int i=0;i<chunks;++i) {
        if(str[i]==*d) { // delimiter
            sub[(chunks*splits)+c]='\0'; // add null terminator
            ++splits;
            ++N;
            c=0;
            sub=realloc(sub,sizeof(char)*100*N); // increase
            //printf("found 1 split, adding N:%d\n",N);
        } else {
            if(str[i]!='\0') {
                sub[(chunks*splits)+c]=str[i]; // add char
                //printf("adding char:%c, pos:%d\n",str[i],c);
            } else {
                endofstring=true;
            }
            ++c;
            if(endofstring) break;
        }
    }
    if(N>1) {
        sub[(chunks*splits)+c]='\0'; // add null terminator
    }
    *rows=N;
    *chunk=chunks;
    //printf("splits:%d, chunk size: %d\n",*rows,*chunk);
    return sub;
}

int main(int argc, char **argv)
//int main()
{
    totalTime = adjustStartTime;

    if(argc>1) { // cmd argument handler
        char *data=malloc(sizeof(char)*100);
        for(int i=1;i<argc;++i) {
            //printf("arg%d: %s\n",i,argv[i]);
            if(str_cmp(argv[i],"--server",8)) {
                if(i==argc-1) {
                    printf("correct argument is --server <port>\n");
                } else {
                    //data=argv[i+1];
                    memcpy(data,argv[i+1],100);
                    printf("starting as server --server %s\n\n",data);
                }
            } else if(str_cmp(argv[i],"--connect",9)) {
                if(i==argc-1) {
                    printf("correct argument is --connect <client:port>\n");
                } else {
                    //data=argv[i+1];
                    memcpy(data,argv[i+1],100);
                    int chunk;
                    int rows;
                    char *list = str_split(data,":",&rows,&chunk);
                    for(int j=0;j<rows;++j) {
                        //printf("split string j=%d, str=%s\n",j,list[j]);
                        printf("split string j=%d, str=%s\n",j,&list[chunk*j]);
                    }
                    free(list);
                    printf("starting as client --client %s\n\n",data);
                }
            } else if(str_cmp(argv[i],"--help",6)) {
                printf("--help: valid arguments are\n  --server <port>\n  --connect <client:port>\n  --help\n\n");
            } else {
                printf("unknown arg %s, try --help\n",argv[i]);
            }
        }
        free(data);
    }

    // just some example stuff
    unsigned int sizevec3=sizeof(Camera);
    printf("size = %d\n",sizevec3);

    //FreeImage_Initialise();

    time_t mytime = time(NULL);
    char *time_str = ctime(&mytime);
    time_str[strlen(time_str)-1]='\0';
    printf("Current Time: %s\n", time_str);

    int xs[] = {420, 69, 1337, 4, 1, 3, 2};
    size_t n = sizeof(xs)/sizeof(xs[0]);
    qsort(xs, n, sizeof(xs[0]), compare);
    for(size_t i = 0; i < n; ++i) printf("%d ",xs[i]);
    printf("\n\n");
    //--char **words;
    //--size_t tlen=0;
    //--long unsigned int i;
    //--words=split(time_str," ",&tlen);
    //--*time_str='\0';
    //--for(i=0;i<tlen;++i) {
    //--    strcat(time_str,words[i]);
    //--    strcat(time_str," ");
    //--}
    //--strcat(time_str, words[i]);
    //--printf("\"%s\"\n",time_str);
    //--free4split(words);

    // prep main memory things (all malloc must freed
    // glGenTex etc has internal malloc, glDelete to clear~
    
    mX = 24; // fixed window coordiates in window mode
    mY = 64; // to avoid titlebar out of screen on windows
    //
    player = malloc(sizeof(Player));

    player->transform = malloc(sizeof(Transform));

    player->dir = malloc(sizeof(Direction));

    player->transform->position = malloc(sizeof(Vec3));
    player->transform->rotation = malloc(sizeof(Vec3));
    player->transform->scale = malloc(sizeof(Vec3));
    
    player->dir->right = malloc(sizeof(Vec3));
    player->dir->forward = malloc(sizeof(Vec3));
    player->dir->up = malloc(sizeof(Vec3));

    camera = malloc(sizeof(Camera));

    camera->eye = malloc(sizeof(Transform));
    
    camera->dir = malloc(sizeof(Direction));
    
    camera->eye->position = malloc(sizeof(Vec3));
    camera->eye->rotation = malloc(sizeof(Vec3));
    camera->eye->scale = malloc(sizeof(Vec3));

    camera->dir->right = malloc(sizeof(Vec3));
    camera->dir->forward = malloc(sizeof(Vec3));
    camera->dir->up = malloc(sizeof(Vec3));
    
    camera->center = malloc(sizeof(Vec3));

    player->camera = camera;

    InitializePlayer();
    
    // init save file
    FILE *fp = fopen("data.json", "r");
    int savemissing=false;
    if(fp==NULL) {
        printf("error: unable to open the file.\n");
        savemissing=true;
    } else {
        printf("found and loading existing game data\n");
        loadGame(fp);
    }
    if(savemissing) {
        printf("creating new initial save file cjson\n");
        saveGame();
    }

    // saving 
    vec3 ui_eye={camera->eye->position->x,camera->eye->position->y,camera->eye->position->z};
    //vec3 ui_center={camera->center->position->x,camera->center->position->y,camera->center->position->z};
    vec3 ui_center={camera->center->x,camera->center->y,camera->center->z};
    mat4x4_look_at(v2,ui_eye,ui_center,up);

    //double pollingDelay = 1000/limitFPS;

    if(FOV>=180) invertControl=0;

    // live updating texts updates with UpdateText()
    // is executed in render loop with RenderText()
    fpstext=calloc(1,sizeof(float)*80);
    timertext=calloc(1,sizeof(float)*80);

    // just a test rounding calculation
    float test=0.43254654342358773647365834573;
    float test2 = test * 100;
    printf("\n---\n%f * 100 = %f\n",test,test2);
    float test3=round(test2 * 100) / 100;
    printf("round(%f * 100)/100 = %f\n",test2,test3);
    char *buffer = (char *)malloc(sizeof(char) * 80);
    sprintf(buffer, "with printf rounding = %.2f", test3);
    printf("%s\n---\n\n",buffer);

    free(buffer);
    //output linux:
    //0.432547 * 100 = 43.254654
    //round(43.254654 * 100)/100 = 43.250000
    
    //printf("early test 2");
    //GLint mvp_location, vpos_location, vcol_location; 

    // moving on to threads~
    pthread_t main_thread, render_thread;
    pthread_create(&main_thread, NULL, thread_main, NULL);
    pthread_create(&render_thread, NULL, thread_render, NULL);

    //while(more) { // no need to loop here as join will block/wait
    //    // keep alive?
    //    more=!glfwWindowShouldClose(window);

    //    #if defined(_WIN32)
    //        Sleep(16); // ~60 fps mcs
    //    #else
    //        usleep(16000); // ~60fps ns
    //    #endif
    //}
    printf("main closed?\n");
   
    // Wait for the thread to finish
    pthread_join(main_thread, NULL);
    pthread_join(render_thread, NULL);
    // ---------------------------------------------------------------
    // Clean up
    pthread_mutex_destroy(&glock);
    
    free(fpstext);
    free(timertext);
    //
    // buffers
    glDeleteBuffers(1, &skyVBO);
    glDeleteBuffers(1, &skyEBO);
    glDeleteBuffers(1, &terrainVBO);
    glDeleteBuffers(1, &terrainEBO);
    glDeleteBuffers(1, &waterVBO);
    glDeleteBuffers(1, &waterEBO);
    glDeleteBuffers(1, &plane_elements);
    glDeleteBuffers(1, &plane_buffer);
    glDeleteBuffers(1, &cube_elements);
    glDeleteBuffers(1, &cube_buffer);
    glDeleteBuffers(1, &quadVBO);
    glDeleteBuffers(1, &VBO);
    // arrays
    //
    glDeleteVertexArrays(1, &skyVAO);  // sphere/sky
    glDeleteVertexArrays(1, &terrainVAO); //terrain
    glDeleteVertexArrays(1, &waterVAO); //water
    glDeleteVertexArrays(1, &cube_array); // cube/player
    glDeleteVertexArrays(1, &plane_array); // spinning quad
    glDeleteVertexArrays(1, &quadVAO); // framebuffer quad
    glDeleteVertexArrays(1, &VAO); // text quad
    //framebuffer
    //
    if(cft_state == 1) {
        glDeleteFramebuffers(1, &textFB);
        glDeleteTextures(1, &textFBsample);
        //glDeleteRenderbuffers(1, &textFBbuffer);
    }
    if(cfd_state == 1) {
        glDeleteFramebuffers(1, &depthFB);
        glDeleteTextures(1, &depthFBsample);
        glDeleteTextures(1, &depthFBdepth);
        //glDeleteTextures(1, &depthFBstencil);
    }
    if(cfm_state == 1) {
        glDeleteFramebuffers(1, &msaaFB);
        glDeleteTextures(1, &msaaSample);
        glDeleteTextures(1, &msaaDepth);
    }
    if(cfa_state == 1) {
        glDeleteFramebuffers(1, &mainFB);
        glDeleteTextures(1, &mainFBsample);
        glDeleteTextures(1, &mainFBdepth);
    }
    if(cfu_state == 1) {
        glDeleteFramebuffers(1, &uiFB);
        glDeleteTextures(1, &uiFBsample);
    }
    if(cfh_state == 1) {
        glDeleteFramebuffers(1, &heightFB);
        glDeleteTextures(1, &heightFBsample);
    }
        
    unloadTerrainTextures();

    unloadSkyImages();
    cleanupAllShaders(0); // shaders.c

    while(highestTidx>0) { // cleanup sentences
        Sentences *sts;
        if((sts = find_sts(highestTidx))) {
            if(sts != NULL) { // rerender(all) or refresh(specific)
                glDeleteTextures(1,&sts->textureid);
                
                free(sts->transform->position);
                free(sts->transform->rotation);
                free(sts->transform->scale);
                free(sts->transform);
                
                delete_sts(sts);
            }
        }
        highestTidx--;
    }

    cleanupFonts(0); // glyph cleanup text.c
    
    if(initStairs==1) {
        free(block_colliders);
    }
    free(terrainCollider);

    // mallocs in C, opengl uses it's own malloc behind ~glGenTexture etc, delete with glDeleteTexture
    // uthash will free it's own "known" data, but pointer within struct malloced must be freed here
    free(keepLines);
    free(camera->dir->right);
    free(camera->dir->forward);
    free(camera->dir->up);
    free(camera->dir);
    free(camera->eye->position);
    free(camera->eye->rotation);
    free(camera->eye->scale);
    free(camera->eye);
    free(camera->center);
    free(camera);
    free(player->transform->position);
    free(player->transform->rotation);
    free(player->transform->scale);
    free(player->transform);
    free(player->dir->right);
    free(player->dir->forward);
    free(player->dir->up);
    free(player->dir);
    free(player);
 
    glfwDestroyWindow(window);
 
    glfwTerminate();
    pthread_exit(NULL);
    exit(EXIT_SUCCESS);
}
