// OpenGL Setup Code
// Written by Sjur Julin 2023.
// License: CC BY 4.0 https://creativecommons.org/licenses/by/4.0/
// Read the 2023 article here: https://www.ignorantus.com/pages/nano_fractals/
#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define _USE_MATH_DEFINES
#include <math.h>

#define GL_GLEXT_PROTOTYPES
#define EGL_EGLEXT_PROTOTYPES
#define GLX_GLXEXT_PROTOTYPES
#define GLEW_STATIC
#include <GL/glew.h>
#include <GL/gl.h>
#include <GL/glx.h>
#include <GL/glu.h>

#include <X11/X.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <X11/XKBlib.h>

#include "nanofrax_gl.h"

#define PI M_PI
#define TAU M_2_PI

typedef struct XWin_t {
    Display *dpy;
    Window win;
    XVisualInfo *vi;
    XEvent xev;
    Atom wm_delete_window;
    GLXContext glx;
} XWin;

static XWin xw;

int quit;

char keys[256];
char mouse;

// https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html


int XWinCreate(char *title)
{

    xw.dpy = XOpenDisplay(0);
    if(!xw.dpy) {
        perror("No connect X server.");
        return -1;
    }

//    int scr = DefaultScreen(xw.dpy);
//    int w = DisplayWidth(xw.dpy, scr) / 8;
//    int h = DisplayHeight(xw.dpy, scr) / 8;
//    int x = w / 2;
//    int y = h / 2;
    int w = 1920, h = 1080, x =  0, y =   0;

    GLint att[] = { GLX_RGBA, GLX_DEPTH_SIZE, 24, GLX_DOUBLEBUFFER, None };
    xw.vi = glXChooseVisual(xw.dpy, 0, att);
    if(!xw.vi) { perror("No choose glX visual."); return -1; }

    Window root = RootWindow(xw.dpy, xw.vi->screen);

    XSetWindowAttributes swa;
    swa.colormap = XCreateColormap(xw.dpy, root, xw.vi->visual, AllocNone);
    swa.event_mask = ExposureMask | KeyPressMask | KeyReleaseMask | ButtonPressMask | StructureNotifyMask | SubstructureNotifyMask | SubstructureRedirectMask | VisibilityChangeMask | PropertyChangeMask;
    swa.override_redirect = True;
    swa.border_pixel = 0;

    xw.win = XCreateWindow(xw.dpy, root, x, y, w, h, 0, xw.vi->depth, InputOutput, xw.vi->visual, CWColormap | CWEventMask, &swa);
    if(!xw.win) { perror("No create X window."); return -1; }

    xw.wm_delete_window = XInternAtom(xw.dpy, "WM_DELETE_WINDOW", 0);
    XSetWMProtocols(xw.dpy, xw.win, &xw.wm_delete_window, 1);
    XSetWindowBackground(xw.dpy, xw.win, 0x002F0000); // ARGB

    XClearWindow(xw.dpy, xw.win);
    XStoreName(xw.dpy, xw.win, title);

    xw.glx = glXCreateContext(xw.dpy, xw.vi, 0, GL_TRUE);
    glXMakeCurrent(xw.dpy, xw.win, xw.glx);

    XSizeHints xsh = {0};

    xsh.flags = USSize|PAspect|PPosition;
    xsh.x = x;
    xsh.y = y;
    xsh.min_aspect.x = xsh.max_aspect.x = w;
    xsh.min_aspect.y = xsh.max_aspect.y = h;
    XSetWMSizeHints(xw.dpy, xw.win, &xsh, XA_WM_NORMAL_HINTS);

    XMapWindow(xw.dpy, xw.win);
    XRaiseWindow(xw.dpy, xw.win);
    XFlush(xw.dpy);
    XSync(xw.dpy, 0);

    return 0;
}

#define _NET_WM_STATE_REMOVE 0 //
#define _NET_WM_STATE_ADD    1 //
#define _NET_WM_STATE_TOGGLE 2 //

void XWinFullscreen(int fullscreen)
{
    XEvent xev;
    xev.xclient.type = ClientMessage;
    xev.xclient.serial = 0;
    xev.xclient.send_event = True;
    xev.xclient.window = xw.win;
    xev.xclient.message_type = XInternAtom(xw.dpy, "_NET_WM_STATE", 0);
    xev.xclient.format = 32;
    xev.xclient.data.l[0] = (fullscreen ? _NET_WM_STATE_ADD : _NET_WM_STATE_REMOVE);
    xev.xclient.data.l[1] = XInternAtom(xw.dpy, "_NET_WM_STATE_FULLSCREEN", 0);
    xev.xclient.data.l[2] = 0;
    XSendEvent(xw.dpy, DefaultRootWindow(xw.dpy), False, SubstructureRedirectMask | SubstructureNotifyMask, &xev);
}

void XWinRestore()
{
    XClientMessageEvent ev = {};
    ev.type = ClientMessage;
    ev.window = xw.win;
    ev.message_type = XInternAtom(xw.dpy, "_NET_ACTIVE_WINDOW", True);
    ev.format = 32;
    ev.data.l[0] = 1;
    ev.data.l[1] = CurrentTime;
    ev.data.l[2] = ev.data.l[3] = ev.data.l[4] = 0;
    XSendEvent( xw.dpy, RootWindow(xw.dpy, XDefaultScreen(xw.dpy)), False, SubstructureRedirectMask | SubstructureNotifyMask, (XEvent*)&ev );
    XFlush( xw.dpy );
}

void XWinProc(void)
{

    while(XPending(xw.dpy)) {
        XNextEvent(xw.dpy, &xw.xev);
        switch(xw.xev.type) {
        case VisibilityNotify:
            XRaiseWindow(xw.dpy, xw.win);
            XFlush(xw.dpy);
            break;
        case Expose:
        {
            XWindowAttributes gwa;
            XGetWindowAttributes(xw.dpy, xw.win, &gwa);
            glViewport(0, 0, gwa.width, gwa.height);
        }
        break;
        case KeyPress:
        case KeyRelease:
        {
            XID key = ((XKeyEvent*)&xw.xev)->keycode;
            keys[key&0xFF] = xw.xev.type==KeyPress?1:0;
        }
        break;
        case ButtonPress: break;
        case MapNotify:   break;
        case ClientMessage:
            if((Atom)xw.xev.xclient.data.l[0] == xw.wm_delete_window)
            quit = 1;
            break;	
        }
    }
}

void XWinDestroy(void)
{
    XDestroyWindow(xw.dpy, xw.win);
    XCloseDisplay(xw.dpy);
}

const char *vs_txt;
const char *fs_txt;
const char *tx_txt;

GLuint pr1, pr2;
static GLuint vb1, vb2;

Texture *NewTexture(int w, int h)
{
    Texture *tex = malloc(sizeof(Texture));
    if(!tex) exit(-1);
    tex->img = malloc(sizeof(Image));
    if(!tex->img) exit(-1);

    tex->img->w = w;
    tex->img->h = h;
    tex->img->datasize = w*h*4;
    tex->img->data = malloc(tex->img->datasize);
    tex->internalformat = GL_RGBA;

    glGenTextures(1, &tex->id);
    glBindTexture(GL_TEXTURE_2D, tex->id);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
    glPixelStorei(GL_UNPACK_ROW_LENGTH, tex->img->w);
	
    return tex;
}

void UploadTexture(Texture *tex)
{
    glBindTexture(GL_TEXTURE_2D, tex->id);
    glTexImage2D(GL_TEXTURE_2D, 0, tex->internalformat, tex->img->w, tex->img->h, 0, GL_RGBA, GL_UNSIGNED_BYTE, tex->img->data);
//    glTexSubImage2D(GL_TEXTURE_2D, 0, dx, dy, w, h, img->format, img->type, img->data);

    if(tex->mipmap) glGenerateMipmap(GL_TEXTURE_2D);
}


void CompileShader(GLuint id)
{
    glCompileShader(id);
    GLint ok;
    glGetShaderiv(id, GL_COMPILE_STATUS, &ok);
    if(!ok) {
        GLchar msg[1024];
        glGetShaderInfoLog(id, sizeof(msg), 0, msg);
        printf("Shader compilation error:\n%s", msg);
        exit(-1);
    }
}

void LinkProgram(GLuint id)
{
    glLinkProgram(id);
    GLint ok;
    glGetProgramiv(id, GL_LINK_STATUS, &ok);
    if(!ok) { // print errors
        GLchar msg[1024];
        glGetProgramInfoLog(id, sizeof(msg), 0, msg);
        printf("Program link error:\n%s", msg);
        exit(-1);
    }
}

int InitGL()
{
    GLint res = glewInit();
    if(res != GLEW_OK) {
        printf("GLEW initialization failed: %s\n", glewGetErrorString(res));
        return -1;
    }

    glXSwapIntervalEXT(xw.dpy, xw.win, 1); // vsync

    glViewport(0, 0, 640, 480);

    glBlendEquation(GL_BLEND_EQUATION_ALPHA);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glEnable(GL_BLEND);

    GLuint vs = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vs, 1, &vs_txt, NULL);
    CompileShader(vs);

    GLuint vs2 = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vs2, 1, &vs_txt, NULL);
    CompileShader(vs2);

    GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fs, 1, &fs_txt, NULL);
    CompileShader(fs);

    GLuint tx = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(tx, 1, &tx_txt, NULL);
    CompileShader(tx);

    pr1 = glCreateProgram();
    glAttachShader(pr1, vs);
    glAttachShader(pr1, fs);
    LinkProgram(pr1);

    pr2 = glCreateProgram();
    glAttachShader(pr2, vs2);
    glAttachShader(pr2, tx);
    LinkProgram(pr2);

    Vtx v1[6] = {
        { 1.f, 1.f, 0.f, 1.f, 0.f, },
        {-1.f, 1.f, 0.f, 0.f, 0.f, },
        {-1.f,-1.f, 0.f, 0.f, 1.f, },
        { 1.f, 1.f, 0.f, 1.f, 0.f, },
        {-1.f,-1.f, 0.f, 0.f, 1.f, },
        { 1.f,-1.f, 0.f, 1.f, 1.f, },
    };

    glGenBuffers(1, &vb1);
    glBindBuffer(GL_ARRAY_BUFFER, vb1);
    glBufferData(GL_ARRAY_BUFFER, sizeof(v1), v1, GL_STATIC_DRAW);

    setTexBottom();
    return 0;
}

void setTexCenter( void )
{
    Vtx v2[6] = {
        { 1.f,-0.8519f + 0.9f + 30/1080.0f, 0.f, 1.f, 0.f, },
        {-1.f,-0.8519f + 0.9f + 30/1080.0f, 0.f, 0.f, 0.f, },
        {-1.f,-1.0000f + 0.9f + 30/1080.0f, 0.f, 0.f, 1.f, },
        { 1.f,-0.8519f + 0.9f + 30/1080.0f, 0.f, 1.f, 0.f, },
        {-1.f,-1.0000f + 0.9f + 30/1080.0f, 0.f, 0.f, 1.f, },
        { 1.f,-1.0000f + 0.9f + 30/1080.0f, 0.f, 1.f, 1.f, },
    };
    glGenBuffers(1, &vb2);
    glBindBuffer(GL_ARRAY_BUFFER, vb2);
    glBufferData(GL_ARRAY_BUFFER, sizeof(v2), v2, GL_STATIC_DRAW);
}

void setTexBottom( void )
{
    Vtx v2[6] = {
        { 1.f,-.8519f, 0.f, 1.f, 0.f, },
        {-1.f,-.8519f, 0.f, 0.f, 0.f, },
        {-1.f,-1.f, 0.f, 0.f, 1.f, },
        { 1.f,-.8519f, 0.f, 1.f, 0.f, },
        {-1.f,-1.f, 0.f, 0.f, 1.f, },
        { 1.f,-1.f, 0.f, 1.f, 1.f, },
    };
    glGenBuffers(1, &vb2);
    glBindBuffer(GL_ARRAY_BUFFER, vb2);
    glBufferData(GL_ARRAY_BUFFER, sizeof(v2), v2, GL_STATIC_DRAW);
}

static GLuint dataSSbo;

void initUniformBuffer( void *src, int size )
{
    glGenBuffers( 1, &dataSSbo );
    glBindBuffer( GL_UNIFORM_BUFFER, dataSSbo );
    glBufferData( GL_UNIFORM_BUFFER, size, src, GL_DYNAMIC_DRAW );
    glUnmapBuffer( GL_UNIFORM_BUFFER );
    glBindBufferBase( GL_UNIFORM_BUFFER, 0, dataSSbo );

}

void copyToUniformBuffer( void *src, int size )
{
    glBindBuffer( GL_UNIFORM_BUFFER, dataSSbo );
    void *dst = glMapBufferRange( GL_UNIFORM_BUFFER, 0, size, GL_MAP_WRITE_BIT );
    memcpy( dst, src, size );
    glUnmapBuffer( GL_UNIFORM_BUFFER );
}

void Draw(void)
{
    static int loops;

    glClear(GL_COLOR_BUFFER_BIT);

    GLfloat mvp[4*4] = { 1, 0, 0, 0,
                         0, 1, 0, 0,
                         0, 0,-1, 0,
                         0, 0, 0, 1 };
    GLint loc;

    glUseProgram(pr1);
    loc = glGetUniformLocation(pr1, "uMVP");
    glUniformMatrix4fv(loc, 1, GL_FALSE, mvp);

    glBindBuffer(GL_ARRAY_BUFFER, vb1);

    loc = glGetAttribLocation(pr1, "aPos");
    glVertexAttribPointer(loc, 3, GL_FLOAT, GL_FALSE, sizeof(Vtx), (void*)0);
    glEnableVertexAttribArray(loc);

    loc = glGetAttribLocation(pr1, "aUV1");
    glVertexAttribPointer(loc, 2, GL_FLOAT, GL_FALSE, sizeof(Vtx), (void*)(sizeof(float) * 3));
    glEnableVertexAttribArray(loc);

    glDrawArrays(GL_TRIANGLES, 0, 6);

    glUseProgram(pr2);
    loc = glGetUniformLocation(pr2, "uMVP");
    glUniformMatrix4fv(loc, 1, GL_FALSE, mvp);

    glBindBuffer(GL_ARRAY_BUFFER, vb2);

    loc = glGetAttribLocation(pr2, "aPos");
    glVertexAttribPointer(loc, 3, GL_FLOAT, GL_FALSE, sizeof(Vtx), (void*)0);
    glEnableVertexAttribArray(loc);

    loc = glGetAttribLocation(pr2, "aUV1");
    glVertexAttribPointer(loc, 2, GL_FLOAT, GL_FALSE, sizeof(Vtx), (void*)(sizeof(float) * 3));

    glEnableVertexAttribArray(loc);
//    loc = glGetUniformLocation(pr2, "uTex");

    glDrawArrays(GL_TRIANGLES, 0, 6);

    glUseProgram(pr1);

    glXSwapBuffers(xw.dpy, xw.win);

    loops++;
}
