// TODO: Add all the error handling to the glx part

#include "openwindow.h"

#include <stdio.h>
#include <string.h>

int initOpenGL() {
    if (getOpenGLProcs() == 0) return -1;
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glEnable(GL_BLEND);

    return 0;
}

#define OPENWINDOW_OPENGL_VERSSION_MAJOR 3
#define OPENWINDOW_OPENGL_VERSSION_MINOR 0

#ifndef _WIN32

#include <unistd.h>

typedef GLXContext (*glXCreateContextAttribsARBProc)(Display*, GLXFBConfig, GLXContext, Bool, const int*);

void windowDefaultEventHandler(Window * w, XEvent xev) {
    switch (xev.type)
    {
        case KeyPress:
        case KeyRelease:
            XQueryKeymap(w->x.display, w->io.current);
            break;
    }
}

void windowEventHandler(Window * w, XEvent event) {
    windowDefaultEventHandler(w, event);
}

bool checkGLXVersion(Window window) {
    int glx_major, glx_minor;

    if (!glXQueryVersion(window.x.display, &glx_major, &glx_minor)) {
        return false;
    }

    return (glx_major == 1 && glx_minor >= 3) || ( glx_major > 1);
}

GLXFBConfig getBestFBConfig(Window window, GLXFBConfig * configs, size_t count) {
    int best = 0, best_samp = -1;

    for (size_t i = 0; i < count; ++i)
    {
        XVisualInfo * vi = glXGetVisualFromFBConfig(window.x.display, configs[i]);

        if (!vi) continue;

        int samp_buf, samples;

        glXGetFBConfigAttrib(window.x.display, configs[i], GLX_SAMPLE_BUFFERS, &samp_buf);
        glXGetFBConfigAttrib(window.x.display, configs[i], GLX_SAMPLES       , &samples);

        if (samp_buf && samples > best_samp)
          best = i, best_samp = samples;

        XFree(vi);
    }

    return configs[best];
}

int initGLX(Window * window)
{
    if (!checkGLXVersion(*window))
        return -1;

    int attribs[] = {
        GLX_X_RENDERABLE     , True,
        GLX_DRAWABLE_TYPE    , GLX_WINDOW_BIT,
        GLX_RENDER_TYPE      , GLX_RGBA_BIT,
        GLX_X_VISUAL_TYPE    , GLX_TRUE_COLOR,
        GLX_RED_SIZE         , 8,
        GLX_GREEN_SIZE      , 8,
        GLX_BLUE_SIZE       , 8,
        GLX_ALPHA_SIZE      , 8,
        GLX_DEPTH_SIZE      , 24,
        GLX_STENCIL_SIZE    , 8,
        GLX_DOUBLEBUFFER    , True,
        //GLX_SAMPLE_BUFFERS     , 1,
        //GLX_SAMPLES            , 4,
        None
    };

    int fbcount;
    GLXFBConfig * configs = glXChooseFBConfig(window->x.display, DefaultScreen(window->x.display), attribs, &fbcount);
    if (!configs || !fbcount) {
        fprintf(stderr, "Failed to retrieve a framebuffer config\n");
        return -1;
    }

    window->glx.config = getBestFBConfig(*window, configs, fbcount);

    XFree(configs);

    glXCreateContextAttribsARBProc glXCreateContextAttribsARB = 0;
    glXCreateContextAttribsARB = (glXCreateContextAttribsARBProc)
        glXGetProcAddressARB( (const GLubyte *) "glXCreateContextAttribsARB" );

    int context_attribs[] = {
      GLX_CONTEXT_MAJOR_VERSION_ARB, OPENWINDOW_OPENGL_VERSSION_MAJOR,
      GLX_CONTEXT_MINOR_VERSION_ARB, OPENWINDOW_OPENGL_VERSSION_MINOR,
      GLX_CONTEXT_PROFILE_MASK_ARB , GLX_CONTEXT_CORE_PROFILE_BIT_ARB,
      None
    };
    window->glx.context = glXCreateContextAttribsARB(window->x.display, window->glx.config, 0, True, context_attribs);

    return 0;
}

int init(Window * window, const char * name, size_t width, size_t height) {
    window->x.display = XOpenDisplay(NULL);

    if (window->x.display == NULL) {
        fprintf(stderr, "Failed to open xdisplay!\n");
        return -1;
    }

    if (initGLX(window))
        return -1;

    XVisualInfo * vi = glXGetVisualFromFBConfig(window->x.display, window->glx.config);

    XSetWindowAttributes wa;

    wa.border_pixel = 0;
    wa.colormap = window->x.cmap = XCreateColormap(window->x.display,
                                                   RootWindow(window->x.display, vi->screen),
                                                   vi->visual, AllocNone);

    window->x.eventMask = wa.event_mask =
        StructureNotifyMask |
        ExposureMask        |
        PointerMotionMask   |
        KeyPressMask        |
        KeyReleaseMask      |
        ButtonPressMask     |
        ButtonReleaseMask;

    X11Window root = RootWindow(window->x.display, vi->screen);

    window->x.window = XCreateWindow(
            window->x.display,
            root,
            0,
            0,
            width,
            height,
            0,
            vi->depth,
            InputOutput,
            vi->visual,
            CWBorderPixel|CWColormap|CWEventMask,
            &wa);

    if (window->x.window == None) {
        fprintf(stderr, "Failed to create x11 window!\n");
        return -1;
    }

    XFree(vi);

    XMapWindow(window->x.display, window->x.window);
    XStoreName(window->x.display, window->x.window, name);

    XSync(window->x.display, False);
    glXMakeCurrent(window->x.display, window->x.window, window->glx.context);

    return 0;
}

Window * openWindow(const char * name, size_t width, size_t height) {
    Window * w = calloc(1, sizeof(Window));
    w->fps = WINDOW_DEFAULT_FPS;

    if (init(w, name, width, height) < 0) {
        fprintf(stderr, "Failed to initialize window: X11 error!\n");
        exit(1);
    }

    if (initOpenGL() < 0) {
        fprintf(stderr, "Failed to initialize window: GLAD/OPENGL error!\n");
        exit(1);
    }

    return w;
}

void windowSetFps(Window * window, uint32_t fps) {
    window->fps = fps;
}

void windowDraw(Window * window) {
    glXSwapBuffers(window->x.display, window->x.window);
    usleep(1 / window->fps);
}

void resetIOState(Window * window) {
    memcpy(window->io.last, window->io.current, WINDOW_KEY_COUNT);
}

void windowHandleEvents(Window * window) {
    XEvent xev = {0};

    resetIOState(window);

    while (XPending(window->x.display)) {
        if (XCheckWindowEvent(window->x.display, window->x.window, window->x.eventMask, &xev)) {
            windowEventHandler(window, xev);
        }
    }
}

void closeWindow(Window * window) {
    glXMakeCurrent(window->x.display, None, None);
    glXDestroyContext(window->x.display, window->glx.context);

    XFreeColormap(window->x.display, window->x.cmap);
    XDestroyWindow(window->x.display, window->x.window);
    XCloseDisplay(window->x.display);
    free(window);
}

bool windowKeyPressed(Window * window, char * key) {
    KeySym keysym = XStringToKeysym(key);
    if (keysym == NoSymbol) return false;

    KeyCode keycode = XKeysymToKeycode(window->x.display, keysym);

    return !(window->io.last[keycode / 8] & (1 << (keycode % 8))) && (window->io.current[keycode / 8] & (1 << (keycode % 8)));
}

bool windowKeyHeld(Window * window, char * key) {
    KeySym keysym = XStringToKeysym(key);
    if (keysym == NoSymbol) return false;

    KeyCode keycode = XKeysymToKeycode(window->x.display, keysym);

    return (window->io.last[keycode / 8] & (1 << (keycode % 8))) && (window->io.current[keycode / 8] & (1 << (keycode % 8)));
}

bool windowKeyReleased(Window * window, char * key) {
    KeySym keysym = XStringToKeysym(key);
    if (keysym == NoSymbol) return false;

    KeyCode keycode = XKeysymToKeycode(window->x.display, keysym);

    return (window->io.last[keycode / 8] & (1 << (keycode % 8))) && !(window->io.current[keycode / 8] & (1 << (keycode % 8)));
}

#else

#include <windows.h>
#include <GL/wgl.h>

//TODO: Change this every time we open a window
#define WND_CLASS_NAME "OpenWindowWndClass"

void windowDefaultEventHandler(Window * window, unsigned int event, WPARAM wParam, LPARAM lParam) {
    (void) wParam;
    (void) lParam;

	switch (event)
	{
        case WM_KEYDOWN:
        case WM_KEYUP:
            GetKeyboardState(window->io.current);
            break;
        case WM_TIMER:
            InvalidateRect(window->wgl.window, NULL, FALSE);
            break;
	}
}

LRESULT CALLBACK windowEventHandler(HWND hwnd, unsigned int event, WPARAM wParam, LPARAM lParam) {
    Window * window = (Window *)GetWindowLongPtr(hwnd, GWLP_USERDATA);
    if (window == NULL) goto uninit;

    windowDefaultEventHandler(window, event, wParam, lParam);

uninit:
	return (DefWindowProc(hwnd, event, wParam, lParam));
}

int init(Window * window, const char * title, size_t width, size_t height) {
    window->wgl.instance = GetModuleHandle(NULL);
	WNDCLASSEX wcex;

	wcex.cbSize = sizeof(WNDCLASSEX);
	wcex.style = CS_CLASSDC;
	wcex.lpfnWndProc = windowEventHandler;
	wcex.cbClsExtra = 0;
	wcex.cbWndExtra = 0;
	wcex.hInstance = window->wgl.instance;
	wcex.hIcon = NULL;
	wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
	wcex.hbrBackground = 0;
	wcex.lpszMenuName = NULL;
	wcex.lpszClassName = WND_CLASS_NAME;
	wcex.hIconSm = NULL;

	RegisterClassEx(&wcex);

	window->wgl.window = CreateWindow(
            WND_CLASS_NAME,
            title,
            WS_OVERLAPPEDWINDOW | WS_VISIBLE,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            width,
            height,
            NULL,
            NULL,
            window->wgl.instance,
            NULL
    );

    if (window->wgl.window == NULL) {
        fprintf(stderr, "Failed to create window!\n");
        return -1;
    }

	ShowWindow(window->wgl.window, SW_SHOW);

    SetWindowLongPtr(window->wgl.window, GWLP_USERDATA, (LONG_PTR)window);

    window->wgl.display = GetDC(window->wgl.window);
    PIXELFORMATDESCRIPTOR pfd = {0};
    pfd.nSize           = sizeof(PIXELFORMATDESCRIPTOR);
    pfd.nVersion        = 1;
    pfd.dwFlags         = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
    pfd.iPixelType      = PFD_TYPE_RGBA;
    pfd.cColorBits      = 32;
    pfd.cRedBits        = 0;
    pfd.cRedShift       = 0;
    pfd.cGreenBits      = 0;
    pfd.cGreenShift     = 0;
    pfd.cBlueBits       = 0;
    pfd.cBlueShift      = 0;
    pfd.cAlphaBits      = 0;
    pfd.cAlphaShift     = 0;
    pfd.cAccumBits      = 0;
    pfd.cAccumRedBits   = 0;
    pfd.cAccumGreenBits = 0;
    pfd.cAccumBlueBits  = 0;
    pfd.cAccumAlphaBits = 0;
    pfd.cDepthBits      = 24;
    pfd.cStencilBits    = 8;
    pfd.cAuxBuffers     = 0;
    pfd.iLayerType      = PFD_MAIN_PLANE;
    pfd.bReserved       = 0;
    pfd.dwLayerMask     = 0;
    pfd.dwVisibleMask   = 0;
    pfd.dwDamageMask    = 0;

    int format = ChoosePixelFormat(window->wgl.display, &pfd);
    SetPixelFormat(window->wgl.display, format, &pfd);

    HGLRC tempContext = wglCreateContext(window->wgl.display);
    if (tempContext == NULL) return -1;

    if (!wglMakeCurrent(window->wgl.display, tempContext)) return -1;

    PFNWGLCREATECONTEXTATTRIBSARBPROC wglCreateContextAttribsARB = (PFNWGLCREATECONTEXTATTRIBSARBPROC)(void *)wglGetProcAddress("wglCreateContextAttribsARB");
    if (wglCreateContextAttribsARB == NULL) return -1;

    int attribs[] = {
        WGL_CONTEXT_MAJOR_VERSION_ARB, OPENWINDOW_OPENGL_VERSSION_MAJOR,
        WGL_CONTEXT_MINOR_VERSION_ARB, OPENWINDOW_OPENGL_VERSSION_MINOR,
        WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
        0
    };

    window->wgl.context = wglCreateContextAttribsARB(window->wgl.display, 0, attribs);
    if (window->wgl.context == NULL) {
        fprintf(stderr, "Failed to create wgl context!\n");
        return -1;
    }

    wglMakeCurrent(window->wgl.display, NULL);
    wglDeleteContext(tempContext);

    wglMakeCurrent(window->wgl.display, window->wgl.context);

    SetTimer(window->wgl.window, 1, (1 / window->fps) * 1000, NULL);

    return 0;
}

Window * openWindow(const char * name, size_t width, size_t height) {
    Window * w = calloc(1, sizeof(Window));
    w->fps = WINDOW_DEFAULT_FPS;

    if (init(w, name, width, height) < 0) {
        fprintf(stderr, "Failed to initialize wgl!\n");
        exit(1);
    }

    if (initOpenGL() < 0) {
        fprintf(stderr, "Failed to initialize window: GLAD/OPENGL error!\n");
        exit(1);
    }

    return w;
}

void windowDraw(Window * window) {
    SwapBuffers(window->wgl.display);
}

void resetIOState(Window * window) {
    memcpy(window->io.last, window->io.current, WINDOW_KEY_COUNT);
}

void windowHandleEvents(Window * window) {
    (void) window;

    resetIOState(window);

    MSG msg = {0};
    GetMessage(&msg, NULL, 0, 0);
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

void closeWindow(Window * window) {
    wglMakeCurrent(NULL, NULL);
    wglDeleteContext(window->wgl.context);
    ReleaseDC(window->wgl.window, window->wgl.display);

    DestroyWindow(window->wgl.window);
    UnregisterClass(WND_CLASS_NAME, window->wgl.instance);
    free(window);
}

bool windowKeyPressed(Window * window, int key) {
    return !(window->io.last[key] & 0x80) && (window->io.current[key] & 0x80);
}

bool windowKeyHeld(Window * window, int key) {
    return (window->io.last[key] & 0x80) && (window->io.current[key] & 0x80);
}

bool windowKeyReleased(Window * window, int key) {
    return (window->io.last[key] & 0x80) && !(window->io.current[key] & 0x80);
}

#endif // _WIN32

int getOpenGLProcs(void) {
    return gladLoadGL();
}
