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

#include <stdint.h>
typedef int            Sint;
typedef int8_t         Sint8;
typedef int16_t        Sint16;
typedef int32_t        Sint32;
typedef int64_t        Sint64;
typedef intptr_t       Sptr;

typedef unsigned int   Uint;
typedef uint8_t        Uint8;
typedef uint16_t       Uint16;
typedef uint32_t       Uint32;
typedef uint64_t       Uint64;
typedef uintptr_t      Uptr;

typedef float          Float;
typedef float          Float32;
typedef double         Float64;

typedef char           Char;

typedef char           Byte;

typedef _Bool          Bool;
typedef Uint32         Bool32;
#define true 1
#define false 0



#define global_variable static
#define persistent_value static



#include <stdio.h>
#include <string.h>
#include <stdlib.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;

enum Log_Channel {
    LOGC_WAYLAND = 0,
    LOGC_EGL,
    LOGC_GLES,
    LOGC_LOG,

    LOGC_COUNT
};

enum Log_Level {
    LOGL_CRITICAL = 0, // Fatal error, quit application
    LOGL_ERROR, // Recoverable error
    LOGL_WARN, // Generic warning (weird stuff going on)
    LOGL_INFO, // Information logging for user's sake, this will be default shipping level
    LOGL_DEBUG, // Temporary debug information for printf debugging
    LOGL_VERBOSE, // More logging, shipping binary will have cmd line switch to turn this ON
    LOGL_CHATTER, // Stuff that might be happening multiple times in a frame, cmd line spam

    LOGL_COUNT
};


#  define LOG(ctgr, lvl, frmt, ...) fprintf(stderr, frmt "\n", ##__VA_ARGS__)
#  define LOG_SET_LEVEL(ctgr, lvl)
#  define LOG_SET_ALL_LEVELS(lvl)



/******************************************************************
/******************************************************************
STUFF STARTS HERE
*****************************************************************/


/**************************************************************************************
 * DISPLAY
 *************************************************************************************/

/*****************************************
 * Wayland
 */

/* 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, Uint32 id,
                                 const Char *interface, Uint32 version)
{
    LOG(LOGC_WAYLAND, LOGL_VERBOSE,
        "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(LOGC_WAYLAND, LOGL_VERBOSE,
            "Bound compositor");
    } else if (strcmp(interface, "wl_shell") == 0) {
        wayland_shell = wl_registry_bind(registry, id,
                                         &wl_shell_interface, 1);
        LOG(LOGC_WAYLAND, LOGL_VERBOSE,
            "Bound shell");
    } else if (strcmp(interface, "wl_seat") == 0) {
        wayland_seat = wl_registry_bind(registry, id,
                                        &wl_seat_interface, 1);
        LOG(LOGC_WAYLAND, LOGL_VERBOSE,
            "Bound seat device");
        wl_seat_add_listener(wayland_seat, &(wayland_seat_listener), NULL);
    } else if (strcmp(interface, "zwp_pointer_constraints_v1") == 0) {
        LOG(LOGC_WAYLAND, LOGL_VERBOSE,
            "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(LOGC_WAYLAND, LOGL_VERBOSE,
            "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, Uint32 id)
{
    LOG(LOGC_WAYLAND, LOGL_VERBOSE,
        "Remove Registry Event: %u", id);
}

/* Wayland Shell Surface Callbacks */

void waylandShellSurfaceHandlePing (void *data, struct wl_shell_surface *shell_surface,
                                    Uint32 serial)
{
    LOG(LOGC_WAYLAND, LOGL_CHATTER,
        "PING!!!");
    wl_shell_surface_pong(shell_surface, serial);
}

void waylandShellSurfaceHandleConfigure (void *data, struct wl_shell_surface *shell_surface,
                                         Uint32 edges, Sint32 width, Sint32 height)
{
    LOG(LOGC_WAYLAND, LOGL_CHATTER,
        "Resize to %dx%d", width, height);
}

void waylandShellSurfaceHandlePopupDone (void *data, struct wl_shell_surface *shell_surface)
{
    LOG(LOGC_WAYLAND, LOGL_CHATTER,
        "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(LOGC_WAYLAND, LOGL_VERBOSE,
            "Display has pointer device");
    } else if (!(caps & WL_SEAT_CAPABILITY_POINTER) && (wayland_pointer)) {
        wl_pointer_destroy(wayland_pointer);
        wayland_pointer = NULL;
        LOG(LOGC_WAYLAND, LOGL_VERBOSE,
            "Pointer device disconnected???");
    }

    if (caps & WL_SEAT_CAPABILITY_KEYBOARD) {
        LOG(LOGC_WAYLAND, LOGL_VERBOSE,
            "Display has keyboard device");
    }

    if (caps & WL_SEAT_CAPABILITY_TOUCH) {
        LOG(LOGC_WAYLAND, LOGL_VERBOSE,
            "Display has touch device");
    }
}


/* Wayland Pointer Callbacks */

void waylandPointerHandleEnter (void *data, struct wl_pointer *pointer,
                                Uint32 serial, struct wl_surface *surface,
                                wl_fixed_t sx, wl_fixed_t sy)
{
    LOG(LOGC_WAYLAND, LOGL_CHATTER,
        "Pointer entering at (%d, %d)", sx, sy);

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

void waylandPointerHandleLeave (void *data, struct wl_pointer *pointer,
                                Uint32 serial, struct wl_surface *surface)
{
    LOG(LOGC_WAYLAND, LOGL_CHATTER,
        "Pointer leaving");
}

void waylandPointerHandleMotion (void *data, struct wl_pointer *pointer,
                                 Uint32 time, wl_fixed_t sx, wl_fixed_t sy)
{
    LOG(LOGC_WAYLAND, LOGL_WARN,
        "Absolute Pointer movement");
}

void waylandPointerHandleButton (void *data, struct wl_pointer *pointer,
                                 Uint32 serial, Uint32 time, Uint32 button, Uint32 state)
{
    LOG(LOGC_WAYLAND, LOGL_CHATTER,
        "Pointer button");

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

void waylandPointerHandleAxis (void *data, struct wl_pointer *pointer,
                               Uint32 time, Uint32 axis, wl_fixed_t value)
{
    LOG(LOGC_WAYLAND, LOGL_CHATTER,
        "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)
{
    Float64 dx_unaccel = wl_fixed_to_double(dx_unaccel_arg);
    Float64 dy_unaccel = wl_fixed_to_double(dy_unaccel_arg);

    LOG(LOGC_WAYLAND, LOGL_CHATTER,
        "Pointer moved by (%f %f)", dx_unaccel, dy_unaccel);
}

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

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

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

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

/******************************************
 * EGL
 */

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


/*****************************************
 * MAIN
 */

int main (Sint argc, Char *argv[])
{
    LOG_SET_ALL_LEVELS(LOGL_CHATTER);

    // TODO: ERROR HANDLING FTW!!! Check every error condition (epecially in EGL)

    // 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(LOGC_WAYLAND, LOGL_CRITICAL,
                "Can't connect to server");
            goto nothing_done;
        }
        LOG(LOGC_WAYLAND, LOGL_VERBOSE,
            "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(LOGC_WAYLAND, LOGL_CRITICAL,
                "Can't find compositor");
            goto wayland_display_connected;
        }
        LOG(LOGC_WAYLAND, LOGL_VERBOSE,
            "Found compositor");

        if (wayland_shell == NULL) {
            LOG(LOGC_WAYLAND, LOGL_CRITICAL,
                "Can't find shell");
            goto wayland_display_connected;
        }
        LOG(LOGC_WAYLAND, LOGL_VERBOSE,
            "Found shell");

        wayland_surface = wl_compositor_create_surface(wayland_compositor);
        if (wayland_surface == NULL) {
            LOG(LOGC_WAYLAND, LOGL_CRITICAL,
                "Can't create surface");
            goto wayland_display_connected;
        }
        LOG(LOGC_WAYLAND, LOGL_VERBOSE,
            "Surface created");

        wayland_shell_surface = wl_shell_get_shell_surface(wayland_shell,
                                                           wayland_surface);
        if (wayland_shell_surface == NULL) {
            LOG(LOGC_WAYLAND, LOGL_ERROR,
                "Can not create shell surface");
            goto wayland_surface_created;
        }
        LOG(LOGC_WAYLAND, LOGL_VERBOSE,
            "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(LOGC_WAYLAND, LOGL_CRITICAL,
                "Can't create EGL window");
            goto wayland_shell_surface_created;
        }
        LOG(LOGC_WAYLAND, LOGL_VERBOSE,
            "EGL window created");

        /* UNCOMMENT THIS TO CONFINE POINTER */
/*
        wayland_confined_pointer = zwp_pointer_constraints_v1_confine_pointer(wayland_pointer_constraint,
                                                                              wayland_surface,
                                                                              wayland_pointer,
                                                                              NULL,
                                                                              ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT);

        zwp_confined_pointer_v1_add_listener(wayland_confined_pointer,
                                           &wayland_confined_pointer_listener,
                                           NULL);
*/

        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(LOGC_EGL, LOGL_CRITICAL,
                "Couldn't create display");
            exit(1);
        }
        LOG(LOGC_EGL, LOGL_VERBOSE,
            "display created");

        EGLint egl_major = 0, egl_minor = 0;
        if (eglInitialize(egl_display, &egl_major, &egl_minor) != EGL_TRUE) {
            LOG(LOGC_EGL, LOGL_CRITICAL,
                "Couldn't initialize");
            exit(1);
        }
        LOG(LOGC_EGL, LOGL_INFO,
            "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(LOGC_EGL, LOGL_VERBOSE,
            "%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((Uint)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,
#if defined(POTP_DEBUG)
            EGL_CONTEXT_FLAGS_KHR, EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR,
#endif // POTP_DEBUG
            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(LOGC_EGL, LOGL_CRITICAL,
                "Make current failed");
            exit(1);
        }
        LOG(LOGC_EGL, LOGL_VERBOSE,
            "Made current");
    }

    // Initialize GLES 3
    {
        Sint gl_major_version = 0, gl_minor_version = 0;
        glGetIntegerv(GL_MAJOR_VERSION, &gl_major_version);
        glGetIntegerv(GL_MINOR_VERSION, &gl_minor_version);
        LOG(LOGC_GLES, LOGL_INFO,
            "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;
}
