#define GL_GLEXT_PROTOTYPES
#define _USE_MATH_DEFINES

#include <uthash.h>

#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>
    #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 <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

#include "linmathv2.h"
//#include "geometry.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 "custom.h" // early declarations
#include "shaders.h" // 
#include "sky.h" // 
#include "terrain.h" // 
#include "player.h" // 

#define nullptr ((void*)0)

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

float waterTime = 0.0f; // frame consistent copy of updateCurrentTime

unsigned int depthFB, depthFBsample, depthFBdepth, depthFBstencil;
unsigned int cfd_state = 0;
int CreateFrameDepth() {
    if(cfd_state == 1) { // fu
	    glDeleteFramebuffers(1, &depthFB);
	    glDeleteTextures(1, &depthFBsample);
	    glDeleteTextures(1, &depthFBdepth); // reset next run
	    //glDeleteTextures(1, &depthFBstencil); // reset next run
	    //glDeleteRenderbuffers(1, &depthFBdepth); // reset next run
        //cfd_state = 0;
        //printf("deletingo old custom fbo--waterdepth\n");
    }
    cfd_state = 1;

    //printf("setting up new custom fbo--waterdepth\n");
    glGenFramebuffers(1, &depthFB);
	glBindFramebuffer(GL_FRAMEBUFFER, depthFB); //bind both read/write to the target framebuffer

    glGenTextures(1, &depthFBsample); // texture object
    glBindTexture(GL_TEXTURE_2D, depthFBsample);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RED, GL_UNSIGNED_BYTE, NULL);
	glBindTexture(GL_TEXTURE_2D, 0);



    glGenTextures(1, &depthFBdepth); // texture object can access in shader
    glBindTexture(GL_TEXTURE_2D, depthFBdepth);
    glTexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH_COMPONENT24, SCR_WIDTH, SCR_HEIGHT);
	glBindTexture(GL_TEXTURE_2D, 0);



    //glGenTextures(1, &depthFBstencil); // texture object can access in shader
    //glBindTexture(GL_TEXTURE_2D, depthFBstencil);
    //glTexStorage2D(GL_TEXTURE_2D, 1, GL_STENCIL_COMPONENTS, SCR_WIDTH, SCR_HEIGHT);
	//glBindTexture(GL_TEXTURE_2D, 0);
    //glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, SCR_WIDTH, SCR_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, NULL);
    //glTexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH_COMPONENT32F, SCR_WIDTH, SCR_HEIGHT);
    //glBindTexture(GL_TEXTURE_2D, 0);

    //glGenRenderbuffers(1, &depthFBdepth); // renderbuffer object cannot access in shader
    //glBindRenderbuffer(GL_RENDERBUFFER, depthFBdepth);
    //glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT,SCR_WIDTH, SCR_HEIGHT);
    //glBindRenderbuffer(GL_RENDERBUFFER, 0);
  
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, depthFBsample, 0);
//
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthFBdepth, 0);

    //glFramebufferTexture2D(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_TEXTURE_2D, depthFBstencil, 0);
    //glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthFBdepth);

	if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
	{
        printf("error on setup of custombuffer-waterdepth\n");
	}
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    //glBindTexture(GL_TEXTURE_2D, 0);

    return 0;
}

int WFO_WIDTH=128;
int WFO_HEIGHT=128;
unsigned int heightFB, heightFBsample;
unsigned int cfh_state = 0;
int CreateFrameHeight() {
    if(cfh_state == 1) { // fu
	    glDeleteFramebuffers(1, &heightFB);
	    glDeleteTextures(1, &heightFBsample);
    }
    cfh_state = 1;

    //printf("setting up new custom fbo--waterheight\n");
    glGenFramebuffers(1, &heightFB);
	glBindFramebuffer(GL_FRAMEBUFFER, heightFB); //bind both read/write to the target framebuffer

    glGenTextures(1, &heightFBsample); // texture object
    glBindTexture(GL_TEXTURE_2D, heightFBsample);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, WFO_WIDTH, WFO_HEIGHT, 0, GL_RED, GL_UNSIGNED_BYTE, NULL);
	glBindTexture(GL_TEXTURE_2D, 0);

    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, heightFBsample, 0);

	if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
	{
        printf("error on setup of custombuffer-waterheight\n");
	}
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    //glBindTexture(GL_TEXTURE_2D, 0);

    return 0;
}

unsigned int waterVAO,waterVBO,waterEBO;
int InitializeWater() {
    float quadVertices[] = {   // vertex attributes for a quad that fills the entire screen in Normalized Device Coordinates.
        // positions   // texCoords
        -1.0f,  1.0f,  0.0f, 1.0f,
        -1.0f, -1.0f,  0.0f, 0.0f,
         1.0f, -1.0f,  1.0f, 0.0f,
    
        -1.0f,  1.0f,  0.0f, 1.0f,
         1.0f, -1.0f,  1.0f, 0.0f,
         1.0f,  1.0f,  1.0f, 1.0f
    };

    // setup screen VAO
    glGenVertexArrays(1, &waterVAO);
    glGenBuffers(1, &waterVBO);
    glBindVertexArray(waterVAO);
    glBindBuffer(GL_ARRAY_BUFFER, waterVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), &quadVertices, GL_STATIC_DRAW); // static vertices
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
    
    return 0;
}
// Define grid resolution (e.g., 32x32 subdivisions)
#define GRID_SIZE 128
#define VERTEX_COUNT (GRID_SIZE + 1) * (GRID_SIZE + 1)
#define INDEX_COUNT (GRID_SIZE * GRID_SIZE * 6) // 6 indices per quad (2 triangles)

// Function to generate water grid mesh
void generateWaterMesh(GLuint* vao, GLuint* vbo, GLuint* ebo) {
    // Vertex data: position (x, z) and UV (u, v)
    float vertices[VERTEX_COUNT * 5]; // 5 floats per vertex: x, y (0 initially), z, u, v
    unsigned int indices[INDEX_COUNT];

    // Generate vertices
    unsigned int vertexIdx = 0;
    for (unsigned int z = 0; z <= GRID_SIZE; z++) {
        for (unsigned int x = 0; x <= GRID_SIZE; x++) {
            // Map x, z from [0, GRID_SIZE] to UV space [0, 1]
            float u = (float)x / GRID_SIZE;
            float v = (float)z / GRID_SIZE;
            // Position in XZ plane (Y will be set in shader)
            vertices[vertexIdx*5] = u;  // x
            vertices[vertexIdx*5+1] = 0.0f; // y (height added in shader)
            vertices[vertexIdx*5+2] = v;  // z
            vertices[vertexIdx*5+3] = u;  // u (UV)
            vertices[vertexIdx*5+4] = v;  // v (UV)
            vertexIdx++;
        }
    }

    // Generate indices for triangles
    unsigned int indexIdx = 0;
    for (unsigned int z = 0; z < GRID_SIZE; z++) {
        for (unsigned int x = 0; x < GRID_SIZE; x++) {
            int topLeft = z * (GRID_SIZE + 1) + x;
            int topRight = topLeft + 1;
            int bottomLeft = (z + 1) * (GRID_SIZE + 1) + x;
            int bottomRight = bottomLeft + 1;
            //if(indexIdx<300) {
            //printf("adding indices TL:%d, TR:%d, BL:%d, BR:%d\n",topLeft,topRight,bottomLeft,bottomRight);
            //}
            
            // Triangle 1: topLeft -> bottomLeft -> topRight
            indices[indexIdx*6] = topLeft;
            indices[indexIdx*6+1] = topRight;
            indices[indexIdx*6+2] = bottomLeft;

            // Triangle 2: t*6+1Right -> bottomLeft -> bottomRight
            indices[indexIdx*6+3] = bottomLeft;
            indices[indexIdx*6+4] = bottomRight;
            indices[indexIdx*6+5] = topRight;
            indexIdx++;
        }
    }

    // OpenGL setup
    glGenVertexArrays(1, vao);
    glGenBuffers(1, vbo);
    glGenBuffers(1, ebo);

    glBindVertexArray(*vao);

    // Vertex buffer
    glBindBuffer(GL_ARRAY_BUFFER, *vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(float)*(GRID_SIZE+1)*(GRID_SIZE+1)*5, &vertices[0], GL_STATIC_DRAW);

    // Index buffer
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, *ebo);
    //glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(unsigned int)*(GRID_SIZE*2)*(GRID_SIZE-1), &indices[0], GL_STATIC_DRAW);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(unsigned int)*GRID_SIZE*GRID_SIZE*6, &indices[0], GL_STATIC_DRAW);

    // Vertex attributes: position (x, y, z) + UV (u, v)
    int stride = 5 * sizeof(float);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, 0);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, stride, (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);

    glBindVertexArray(0);
}

int RenderWater() { // change realtime to heightmap use?
    glUseProgram(0);
    glUseProgram(programWater);
    //glUseProgram(programLava);

    glDisable(GL_CULL_FACE);
    glEnable(GL_DEPTH_TEST);
    glDepthMask(GL_TRUE);
    glDepthFunc(GL_LEQUAL);

//glEnable(GL_STENCIL_TEST);
//glStencilFunc(GL_EQUAL, 0, 0xFF);
//glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
//glStencilMask(0x00); // test stencil only


    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    //glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    //glBlendFunc(GL_DST_ALPHA, GL_ZERO);

    setRenderModePerspective();

    mat4x4_invert(ip,p); // invert projection for underwater frag post

    execRenderModePerspective(v);

    setUniform4m(&programWater,"MVP", mvp);
    setUniform3f(&programWater,"sunPos", sunpos.x, sunpos.y, sunpos.z);
    setUniform3f(&programWater,"moonPos", moonpos.x, moonpos.y,moonpos.z);
    setUniform3f(&programWater,"viewPos", camera->eye->position->x + player->transform->position->x, camera->eye->position->y + player->transform->position->y, camera->eye->position->z + player->transform->position->z);
    setUniform1f(&programWater,"daytime", daytime);
    setUniform4m(&programWater, "invProjection", ip);
    setUniform4m(&programWater, "invView", iv);

    setUniform3f(&programWater,"data", waterTime, -1.0f, 2.0f); // time,y-offset,scale
    setUniform1i(&programWater, "waterHeightTex", 0);

    //glBindTexture(GL_TEXTURE_2D, 0);
    
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, heightFBsample);

    glActiveTexture(GL_TEXTURE0+1);
    glBindTexture(GL_TEXTURE_2D, depthFBdepth);

    glActiveTexture(GL_TEXTURE0+2);
    glBindTexture(GL_TEXTURE_2D, mainFBdepth);

    glBindVertexArray(waterVAO);

    // single quad old
    //glDrawArrays(GL_TRIANGLES, 0, 6);
    //
    // multi quad new
    glDrawElements(GL_TRIANGLES, INDEX_COUNT, GL_UNSIGNED_INT, 0);

    glBindVertexArray(0);

    glBindTexture(GL_TEXTURE_2D, 0);

    glActiveTexture(GL_TEXTURE0+1);
    glBindTexture(GL_TEXTURE_2D, 0);

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

    RenderBoundBox(-1.0f,0.0f,-1.0f,1.0f,0.0f,1.0f,1,mvp);

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

//glStencilMask(0x00);
//glDisable(GL_STENCIL_TEST);

    glDepthFunc(GL_LESS);
    glDepthMask(GL_TRUE);
    glEnable(GL_DEPTH_TEST);
    glDisable(GL_CULL_FACE);

    glBindVertexArray(0);

    return 0;
}

float waterHeightSample = 0.0f;
float lastWaterHeightSample = 0.0f;
int RenderWaterHeight() {
    glUseProgram(0);
    glUseProgram(programWaterHeight);
    //glUseProgram(programLava);

    //glDisable(GL_CULL_FACE);
    //glEnable(GL_DEPTH_TEST);
    glDepthMask(GL_FALSE);
    //glDepthFunc(GL_LEQUAL);

    //setRenderModePerspective();
    //execRenderModePerspective(v);
    setRenderModeOrthographic();
    execRenderModeOrthographic(v2);

    setUniform4m(&programWaterHeight,"MVP", mvp);
    //setUniform3f(&programWater,"sunPos", sunpos.x, sunpos.y, sunpos.z);
    //setUniform3f(&programWater,"moonPos", moonpos.x, moonpos.y,moonpos.z);
    setUniform3f(&programWaterHeight,"data", waterTime, -1.0f, 2.0f); // time,y-offset,scale
    setUniform1i(&programWaterHeight, "viewport_width", WFO_WIDTH);
    setUniform1i(&programWaterHeight, "viewport_height", WFO_HEIGHT);


    glBindTexture(GL_TEXTURE_2D, 0);
    glBindVertexArray(quadVAO);

    glDrawArrays(GL_TRIANGLES, 0, 6);

    //glDepthFunc(GL_LESS);
    glDepthMask(GL_TRUE);
    glEnable(GL_DEPTH_TEST);
    glDisable(GL_CULL_FACE);

    // do water height check here?
    float pixel_color[3];
    unsigned int xclip=0;
    unsigned int yclip=0;
    // player coordinates within -256,-256 to 256,256 (512 map?)
    float posX=player->transform->position->x;
    float posZ=player->transform->position->z;
    posX=CLAMP(posX,256.0f,-256.0f);
    posZ=CLAMP(posZ,256.0f,-256.0f); // clamp
    xclip=(((unsigned int)posX+256))/10;
    yclip=(512-((unsigned int)posZ+256))/10; // offset 0-512

    //glClampColor(GL_CLAMP_READ_COLOR, GL_FIXED_ONLY);
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glReadBuffer(GL_COLOR_ATTACHMENT0);
    glReadPixels(xclip, yclip, 1, 1, GL_RED, GL_FLOAT, &pixel_color[0]);
    glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
    pixel_color[0]*=1.0f; // scale
    pixel_color[0]-=2.1f; // offset
    waterHeightSample = pixel_color[0];
    //printf("water height sample from texture = %f, pos=(%d,%d)\n",pixel_color[0],xclip,yclip);

    glBindVertexArray(0);

    return 0;
}

