/* NOTE FOR GNOME BUG TEAM:
   SEARCH FOR "STUFF STARTS HERE" to skip all the junk (to make this file compile) */

#define global_variable static
#define persistent_value static

#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>

#include <time.h>
#include <linux/input.h>

#include <wayland-client.h>
#include <wayland-egl.h>
#include <wayland-cursor.h>
#include "pointer-constraints-unstable-v1-client-header.h"
#include "relative-pointer-unstable-v1-client-header.h"

#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES3/gl3.h>
#include <GLES2/gl2ext.h>
#include <GLES3/gl3ext.h>

#define WIDTH 800
#define HEIGHT 600

global_variable bool game_is_running = true;

#  define log(frmt, ...) fprintf(stderr, frmt "\n", ##__VA_ARGS__)

/* Wayland Listeners */
global_variable struct wl_registry_listener wayland_registry_listener;
global_variable struct wl_shell_surface_listener wayland_shell_surface_listener;
global_variable struct wl_seat_listener wayland_seat_listener;
global_variable struct wl_pointer_listener wayland_pointer_listener;
global_variable struct zwp_relative_pointer_v1_listener wayland_relative_pointer_listener;
global_variable struct zwp_confined_pointer_v1_listener wayland_confined_pointer_listener;
global_variable struct zwp_locked_pointer_v1_listener wayland_locked_pointer_listener;

/* Wayland Windowing State */
global_variable struct wl_display *wayland_display;

global_variable struct wl_compositor *wayland_compositor;
global_variable struct wl_shell *wayland_shell;
global_variable struct wl_seat *wayland_seat;
global_variable struct zwp_pointer_constraints_v1 *wayland_pointer_constraint;
global_variable struct zwp_relative_pointer_manager_v1 *wayland_relative_pointer_manager;

global_variable struct wl_egl_window *wayland_window;
global_variable struct wl_surface *wayland_surface;
global_variable struct wl_shell_surface *wayland_shell_surface;

/* Wayland Input State */
global_variable struct wl_pointer *wayland_pointer;
global_variable struct zwp_locked_pointer_v1 *wayland_locked_pointer;
global_variable struct zwp_confined_pointer_v1 *wayland_confined_pointer;
global_variable struct zwp_relative_pointer_v1 *wayland_relative_pointer;

/* Wayland Callbacks */

void waylandRegistryAddListener (void *data, struct wl_registry *registry, unsigned int id,
                                 const char *interface, unsigned int version)
{
    log("Add Registry Event: %s (id = %u)", interface, id);

    if (strcmp(interface, "wl_compositor") == 0) {
        wayland_compositor = wl_registry_bind(registry, id,
                                              &wl_compositor_interface, 1);
        log("Bound compositor");
    } else if (strcmp(interface, "wl_shell") == 0) {
        wayland_shell = wl_registry_bind(registry, id,
                                         &wl_shell_interface, 1);
        log("Bound shell");
    } else if (strcmp(interface, "wl_seat") == 0) {
        wayland_seat = wl_registry_bind(registry, id,
                                        &wl_seat_interface, 1);
        log("Bound seat device");
        wl_seat_add_listener(wayland_seat, &(wayland_seat_listener), NULL);
    } else if (strcmp(interface, "zwp_pointer_constraints_v1") == 0) {
        log("Bound pointer constraint");
        wayland_pointer_constraint = wl_registry_bind(registry, id, &zwp_pointer_constraints_v1_interface, 1);
    } else if (strcmp(interface, "zwp_relative_pointer_manager_v1") == 0) {
        log("Bound relative pointer");
        wayland_relative_pointer_manager = wl_registry_bind(registry, id, &zwp_relative_pointer_manager_v1_interface, 1);
    }
}

void waylandRegistryRemoveListener (void *data, struct wl_registry *registry, unsigned int id)
{
    log("Remove Registry Event: %u", id);
}

/* Wayland Shell Surface Callbacks */

void waylandShellSurfaceHandlePing (void *data, struct wl_shell_surface *shell_surface,
                                    unsigned int serial)
{
    log("PING!!!");
    wl_shell_surface_pong(shell_surface, serial);
}

void waylandShellSurfaceHandleConfigure (void *data, struct wl_shell_surface *shell_surface,
                                         unsigned int edges, int width, int height)
{
    log("Resize to %dx%d", width, height);
}

void waylandShellSurfaceHandlePopupDone (void *data, struct wl_shell_surface *shell_surface)
{
    log("Popup");
}

/* Wayland Seat Callbacks */

void waylandSeatHandleCapabilities (void *data, struct wl_seat *seat,
                                    enum wl_seat_capability caps)
{
    if ((caps & WL_SEAT_CAPABILITY_POINTER) && !(wayland_pointer)) {
        wayland_pointer = wl_seat_get_pointer(seat);
        wl_pointer_add_listener(wayland_pointer, &(wayland_pointer_listener), NULL);
        log("Display has pointer device");
    } else if (!(caps & WL_SEAT_CAPABILITY_POINTER) && (wayland_pointer)) {
        wl_pointer_destroy(wayland_pointer);
        wayland_pointer = NULL;
        log("Pointer device disconnected???");
    }

    if (caps & WL_SEAT_CAPABILITY_KEYBOARD) {
        log("Display has keyboard device");
    }

    if (caps & WL_SEAT_CAPABILITY_TOUCH) {
        log("Display has touch device");
    }
}


/* Wayland Pointer Callbacks */

void waylandPointerHandleEnter (void *data, struct wl_pointer *pointer,
                                unsigned int serial, struct wl_surface *surface,
                                wl_fixed_t sx, wl_fixed_t sy)
{
    log("Pointer entering at (%d, %d)", sx, sy);

    wl_pointer_set_cursor(pointer, serial, NULL, 0, 0);
}

void waylandPointerHandleLeave (void *data, struct wl_pointer *pointer,
                                unsigned int serial, struct wl_surface *surface)
{
    log("Pointer leaving");
}

void waylandPointerHandleMotion (void *data, struct wl_pointer *pointer,
                                 unsigned int time, wl_fixed_t sx, wl_fixed_t sy)
{
    log("Absolute Pointer movement");
}

void waylandPointerHandleButton (void *data, struct wl_pointer *pointer,
                                 unsigned int serial, unsigned int time, unsigned int button, unsigned int state)
{
    log("Pointer button");

    if (button == BTN_RIGHT && state == WL_POINTER_BUTTON_STATE_PRESSED) {
        game_is_running = false;
    }
}

void waylandPointerHandleAxis (void *data, struct wl_pointer *pointer,
                               unsigned int time, unsigned int axis, wl_fixed_t value)
{
    log("Pointer handle axis: %u", axis);
}

void waylandRelativePointerHandleMotion (void *data,
                                         struct zwp_relative_pointer_v1 *relative_pointer,
                                         uint32_t time_hi_bits,
                                         uint32_t time_lo_bits,
                                         wl_fixed_t dx_arg,
                                         wl_fixed_t dy_arg,
                                         wl_fixed_t dx_unaccel_arg,
                                         wl_fixed_t dy_unaccel_arg)
{
    double dx_unaccel = wl_fixed_to_double(dx_unaccel_arg);
    double dy_unaccel = wl_fixed_to_double(dy_unaccel_arg);

    log("Pointer moved by (%f %f)", dx_unaccel, dy_unaccel);
}

void waylandLockedPointerHandleLocked (void *data,
                                       struct zwp_locked_pointer_v1 *locked_pointer)
{
    log("Pointer Locked");
}

void waylandLockedPointerHandleUnlocked (void *data,
                                         struct zwp_locked_pointer_v1 *locked_pointer)
{
    log("Pointer Unlocked");
}

void waylandLockedPointerHandleConfined (void *data,
                                         struct zwp_confined_pointer_v1 *confined_pointer)
{
    log("Pointer Confined");
}

void waylandLockedPointerHandleUnconfined (void *data,
                                           struct zwp_confined_pointer_v1 *confined_pointer)
{
    log("Pointer Unconfined");
}


global_variable EGLDisplay egl_display;
global_variable EGLConfig egl_config;
global_variable EGLSurface egl_surface;
global_variable EGLContext egl_context;


int main (int argc, char *argv[])
{
    // Setup Wayland
    {
        wayland_registry_listener = (struct wl_registry_listener) {
            waylandRegistryAddListener,
            waylandRegistryRemoveListener
        };

        wayland_shell_surface_listener = (struct wl_shell_surface_listener) {
            waylandShellSurfaceHandlePing,
            waylandShellSurfaceHandleConfigure,
            waylandShellSurfaceHandlePopupDone
        };

        wayland_seat_listener = (struct wl_seat_listener) {
            waylandSeatHandleCapabilities
        };

        wayland_pointer_listener = (struct wl_pointer_listener) {
            waylandPointerHandleEnter,
            waylandPointerHandleLeave,
            waylandPointerHandleMotion,
            waylandPointerHandleButton,
            waylandPointerHandleAxis
        };

        wayland_relative_pointer_listener = (struct zwp_relative_pointer_v1_listener) {
            waylandRelativePointerHandleMotion
        };

        wayland_locked_pointer_listener = (struct zwp_locked_pointer_v1_listener) {
            waylandLockedPointerHandleLocked,
            waylandLockedPointerHandleUnlocked
        };

        wayland_confined_pointer_listener = (struct zwp_confined_pointer_v1_listener) {
            waylandLockedPointerHandleConfined,
            waylandLockedPointerHandleUnconfined
        };

        wayland_display = wl_display_connect(NULL);
        if (wayland_display == NULL) {
            log("Can't connect to server");
            goto nothing_done;
        }
        log("Connected to server");

        struct wl_registry *wayland_registry = wl_display_get_registry(wayland_display);
        wl_registry_add_listener(wayland_registry,
                                 &(wayland_registry_listener),
                                 NULL);

        wl_display_dispatch(wayland_display);
        wl_display_roundtrip(wayland_display);

        if (wayland_compositor == NULL) {
            log("Can't find compositor");
            goto wayland_display_connected;
        }
        log("Found compositor");

        if (wayland_shell == NULL) {
            log("Can't find shell");
            goto wayland_display_connected;
        }
        log("Found shell");

        wayland_surface = wl_compositor_create_surface(wayland_compositor);
        if (wayland_surface == NULL) {
            log("Can't create surface");
            goto wayland_display_connected;
        }
        log("Surface created");

        wayland_shell_surface = wl_shell_get_shell_surface(wayland_shell,
                                                           wayland_surface);
        if (wayland_shell_surface == NULL) {
            log("Can not create shell surface");
            goto wayland_surface_created;
        }
        log("Shell surface created");

        wl_shell_surface_set_toplevel(wayland_shell_surface);
        wl_shell_surface_add_listener(wayland_shell_surface,
                                      &(wayland_shell_surface_listener),
                                      NULL);

        wayland_window = wl_egl_window_create(wayland_surface,
                                              WIDTH, HEIGHT);
        if (wayland_window == EGL_NO_SURFACE) {
            log("Can't create EGL window");
            goto wayland_shell_surface_created;
        }
        log("EGL window created");

        wayland_locked_pointer = zwp_pointer_constraints_v1_lock_pointer(wayland_pointer_constraint,
                                                                         wayland_surface,
                                                                         wayland_pointer,
                                                                         NULL,
                                                                         ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ONESHOT);

        zwp_locked_pointer_v1_add_listener(wayland_locked_pointer,
                                           &wayland_locked_pointer_listener,
                                           NULL);

        wayland_relative_pointer = zwp_relative_pointer_manager_v1_get_relative_pointer(wayland_relative_pointer_manager,
                                                                                        wayland_pointer);
        zwp_relative_pointer_v1_add_listener(wayland_relative_pointer,
                                             &wayland_relative_pointer_listener,
                                             NULL);

    }


    // Initialize EGL
    {
        egl_display = eglGetDisplay((EGLNativeDisplayType) (wayland_display));
        if (egl_display == EGL_NO_DISPLAY) {
            log("Couldn't create display");
            exit(1);
        }
        log("display created");

        EGLint egl_major = 0, egl_minor = 0;
        if (eglInitialize(egl_display, &egl_major, &egl_minor) != EGL_TRUE) {
            log("Couldn't initialize");
            exit(1);
        }
        log("EGL initialized - Version %d.%d", egl_major, egl_minor);

        eglBindAPI(EGL_OPENGL_ES_API);

        EGLint egl_config_attribs[] = {
            EGL_RED_SIZE, 8,
            EGL_GREEN_SIZE, 8,
            EGL_BLUE_SIZE, 8,
            EGL_ALPHA_SIZE, 8,

            EGL_DEPTH_SIZE, 24,
            EGL_STENCIL_SIZE, 8,

            EGL_SAMPLE_BUFFERS, 1,
            EGL_SAMPLES, 4,

            EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
            EGL_CONFORMANT, EGL_OPENGL_ES3_BIT,

            EGL_NONE
        };

        EGLint egl_config_count = 0;

        eglGetConfigs(egl_display, NULL, 0, &egl_config_count);
        log("%d configs found", egl_config_count);

        // TODO: Proper bound checking below (in typecast), remove memory allocation,
        //       better way of choosing config than this ad-hoc system
        EGLConfig *egl_configs = calloc((unsigned int)egl_config_count, sizeof(*egl_configs));

        // QUESTION: What is egl_chosen_config_count?
        EGLint egl_chosen_config_count = 0;
        eglChooseConfig(egl_display, egl_config_attribs, egl_configs,
                        egl_config_count, &egl_chosen_config_count);

        egl_config = egl_configs[0];

        const EGLint egl_context_attribs[] = {
            EGL_CONTEXT_MAJOR_VERSION_KHR, 3,
            EGL_CONTEXT_MINOR_VERSION_KHR, 1,
            EGL_CONTEXT_FLAGS_KHR, EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR,
            EGL_NONE
        };

        egl_context = eglCreateContext(egl_display, egl_config,
                                       EGL_NO_CONTEXT, egl_context_attribs);

        egl_surface = eglCreateWindowSurface(egl_display,
                                             egl_config,
                                             wayland_window,
                                             NULL);

        if (eglMakeCurrent(egl_display,
                           egl_surface, egl_surface,
                           egl_context) == 0) {
            log("Make current failed");
            exit(1);
        }
        log("Made current");
    }

    // Initialize GLES 3
    {
        int gl_major_version = 0, gl_minor_version = 0;
        glGetIntegerv(GL_MAJOR_VERSION, &gl_major_version);
        glGetIntegerv(GL_MINOR_VERSION, &gl_minor_version);
        log("GLES Version: %d.%d",
            gl_major_version, gl_minor_version);
    }

    while (game_is_running) {
        {
            wl_display_dispatch_pending(wayland_display);
        }

        {
            glClearColor(1.0, 0.0, 0.0, 1.0);
            glClear(GL_COLOR_BUFFER_BIT);
            glFlush();
        }

        {
            eglSwapBuffers(egl_display, egl_surface);
        }

    }

  egl_surface_created:
    eglDestroySurface(egl_display, egl_surface);
  egl_context_created:
    eglDestroyContext(egl_display, egl_context);
  egl_initialised:
    eglTerminate(egl_display);
  wayland_egl_window_created:
    wl_egl_window_destroy(wayland_window);
  wayland_shell_surface_created:
    wl_shell_surface_destroy(wayland_shell_surface);
  wayland_surface_created:
    wl_surface_destroy(wayland_surface);
  wayland_display_connected:
    wl_display_disconnect(wayland_display);
  nothing_done:
    return 0;
}
