#ifdef __linux__
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <X11/X.h>
#include <X11/Xlib.h>
#include <GL/glx.h>
#include <X11/extensions/Xrandr.h>
#include <X11/XKBlib.h>
#include <X11/Xatom.h>

#include "gfx_window_manager_api.h"
#include "gfx_screen_config.h"

#include "src/pc/controller/controller_keyboard.h"

#include "../configfile.h"

#ifdef VERSION_EU
#define FRAME_INTERVAL_US_NUMERATOR 40000
#define FRAME_INTERVAL_US_DENOMINATOR 1
#else
#define FRAME_INTERVAL_US_NUMERATOR 100000
#define FRAME_INTERVAL_US_DENOMINATOR 3
#endif

const struct {
    const char *name;
    int scancode;
} keymap_name_to_scancode[] = {
    {"ESC", 0x01},
    {"AE01", 0x02 },
    {"AE02", 0x03 },
    {"AE03", 0x04 },
    {"AE04", 0x05 },
    {"AE05", 0x06 },
    {"AE06", 0x07 },
    {"AE07", 0x08 },
    {"AE08", 0x09 },
    {"AE09", 0x0a },
    {"AE10", 0x0b },
    {"AE11", 0x0c },
    {"AE12", 0x0d },
    {"BKSP", 0x0e },
    {"TAB", 0x0f },
    {"AD01", 0x10 },
    {"AD02", 0x11 },
    {"AD03", 0x12 },
    {"AD04", 0x13 },
    {"AD05", 0x14 },
    {"AD06", 0x15 },
    {"AD07", 0x16 },
    {"AD08", 0x17 },
    {"AD09", 0x18 },
    {"AD10", 0x19 },
    {"AD11", 0x1a },
    {"AD12", 0x1b },
    {"RTRN", 0x1c },
    {"LCTL", 0x1d },
    {"AC01", 0x1e },
    {"AC02", 0x1f },
    {"AC03", 0x20 },
    {"AC04", 0x21 },
    {"AC05", 0x22 },
    {"AC06", 0x23 },
    {"AC07", 0x24 },
    {"AC08", 0x25 },
    {"AC09", 0x26 },
    {"AC10", 0x27 },
    {"AC11", 0x28 },
    {"TLDE", 0x29 },
    {"LFSH", 0x2a },
    {"BKSL", 0x2b },
    {"AB01", 0x2c },
    {"AB02", 0x2d },
    {"AB03", 0x2e },
    {"AB04", 0x2f },
    {"AB05", 0x30 },
    {"AB06", 0x31 },
    {"AB07", 0x32 },
    {"AB08", 0x33 },
    {"AB09", 0x34 },
    {"AB10", 0x35 },
    {"RTSH", 0x36 },
    {"KPMU", 0x37 },
    {"LALT", 0x38 },
    {"SPCE", 0x39 },
    {"CAPS", 0x3a },
    {"FK01", 0x3b },
    {"FK02", 0x3c },
    {"FK03", 0x3d },
    {"FK04", 0x3e },
    {"FK05", 0x3f },
    {"FK06", 0x40 },
    {"FK07", 0x41 },
    {"FK08", 0x42 },
    {"FK09", 0x43 },
    {"FK10", 0x44 },
    {"NMLK", 0x45 },
    {"SCLK", 0x46 },
    {"KP7", 0x47 },
    {"KP8", 0x48 },
    {"KP9", 0x49 },
    {"KPSU", 0x4a },
    {"KP4", 0x4b },
    {"KP5", 0x4c },
    {"KP6", 0x4d },
    {"KPAD", 0x4e },
    {"KP1", 0x4f },
    {"KP2", 0x50 },
    {"KP3", 0x51 },
    {"KP0", 0x52 },
    {"KPDL", 0x53 },
    {"LVL3", 0x54 }, // correct?
    {"", 0x55 }, // not mapped?
    {"LSGT", 0x56 },
    {"FK11", 0x57 },
    {"FK12", 0x58 },
    {"AB11", 0x59 },
    {"KATA", 0 },
    {"HIRA", 0 },
    {"HENK", 0 },
    {"HKTG", 0 },
    {"MUHE", 0 },
    {"JPCM", 0 },
    {"KPEN", 0x11c },
    {"RCTL", 0x11d },
    {"KPDV", 0x135 },
    {"PRSC", 0x54 }, // ?
    {"RALT", 0x138 },
    {"LNFD", 0 },
    {"HOME", 0x147 },
    {"UP", 0x148 },
    {"PGUP", 0x149 },
    {"LEFT", 0x14b },
    {"RGHT", 0x14d },
    {"END", 0x14f },
    {"DOWN", 0x150 },
    {"PGDN", 0x151 },
    {"INS", 0x152 },
    {"DELE", 0x153 },
    {"PAUS", 0x21d },
    {"LWIN", 0x15b },
    {"RWIN", 0x15c },
    {"COMP", 0x15d },
};

static struct {
    Display *dpy;
    Window root;
    Window win;
    
    Atom atom_wm_state;
    Atom atom_wm_state_fullscreen;
    
    bool is_fullscreen;
    
    int keymap[256];
    
    PFNGLXGETSYNCVALUESOMLPROC glXGetSyncValuesOML;
    PFNGLXSWAPBUFFERSMSCOMLPROC glXSwapBuffersMscOML;
    PFNGLXWAITFORSBCOMLPROC glXWaitForSbcOML;
    
    PFNGLXSWAPINTERVALEXTPROC glXSwapIntervalEXT;
    PFNGLXSWAPINTERVALSGIPROC glXSwapIntervalSGI;
    
    PFNGLXGETVIDEOSYNCSGIPROC glXGetVideoSyncSGI;
    PFNGLXWAITVIDEOSYNCSGIPROC glXWaitVideoSyncSGI;
    
    bool has_oml_sync_control;
    uint64_t ust0;
    int64_t last_msc;
    uint64_t wanted_ust; // multiplied by FRAME_INTERVAL_US_DENOMINATOR
    uint64_t vsync_interval;
    uint64_t last_ust;
    int64_t target_msc;
    bool dropped_frame;
    
    bool has_sgi_video_sync;
    uint64_t last_sync_counter;
    int64_t this_msc;
    int64_t this_ust;
} glx;

static int64_t get_time(void) {
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return (int64_t)ts.tv_sec * 1000000 + ts.tv_nsec / 1000;
}

static int64_t adjust_sync_counter(uint32_t counter) {
    uint32_t hi = glx.last_sync_counter >> 32;
    uint32_t lo = (uint32_t)glx.last_sync_counter;
    if (lo >= 0x80000000U && counter < 0x80000000U) {
        // Wrapped
        ++hi;
    }
    glx.last_sync_counter = ((uint64_t)hi << 32) | counter;
    return glx.last_sync_counter;
}

static int64_t glXWaitVideoSyncSGI_wrapper(void) {
    unsigned int counter = 0;
    glx.glXWaitVideoSyncSGI(1, 0, &counter);
    return adjust_sync_counter(counter);
}

static int64_t glXGetVideoSyncSGI_wrapper(void) {
    unsigned int counter = 0;
    glx.glXGetVideoSyncSGI(&counter);
    return adjust_sync_counter(counter);
}

static void init_keymap(void) {
    XkbDescPtr desc = XkbGetMap(glx.dpy, 0, XkbUseCoreKbd);
    XkbGetNames(glx.dpy, XkbKeyNamesMask, desc);
    
    for (int i = desc->min_key_code; i <= desc->max_key_code && i < 256; i++) {
        char name[XkbKeyNameLength + 1];
        memcpy(name, desc->names->keys[i].name, XkbKeyNameLength);
        name[XkbKeyNameLength] = '\0';
        for (size_t j = 0; j < sizeof(keymap_name_to_scancode) / sizeof(keymap_name_to_scancode[0]); j++) {
            if (strcmp(keymap_name_to_scancode[j].name, name) == 0) {
                glx.keymap[i] = keymap_name_to_scancode[j].scancode;
                break;
            }
        }
    }
    
    XkbFreeNames(desc, XkbKeyNamesMask, True);
    XkbFreeKeyboard(desc, 0, True);
}

static void gfx_glx_set_fullscreen(bool on);

static bool gfx_glx_check_extension(const char *extensions, const char *extension) {
    size_t len = strlen(extension);
    const char *pos = extensions;
    while ((pos = strstr(pos, extension)) != NULL) {
        if ((pos[len] == ' ' || pos[len] == '\0') && (pos == extensions || pos[-1] == ' ')) {
            return true;
        }
        if (pos[len] == '\0') {
            break;
        }
        pos += len + 1;
    }
    return false;
}

static void gfx_glx_init(void) {
    // On NVIDIA proprietary driver, make the driver queue up to two frames on glXSwapBuffers,
    // which means that glXSwapBuffers should be non-blocking,
    // if we are sure to wait at least one vsync interval between calls.
    setenv("__GL_MaxFramesAllowed", "2", true);
    
    glx.dpy = XOpenDisplay(NULL);
    if (glx.dpy == NULL) {
        fprintf(stderr, "Cannot connect to X server\n");
        exit(1);
    }
    int screen = DefaultScreen(glx.dpy);
    glx.root = RootWindow(glx.dpy, screen);
    
    GLint att[] = { GLX_RGBA, GLX_DEPTH_SIZE, 24, GLX_DOUBLEBUFFER, None };
    XVisualInfo *vi = glXChooseVisual(glx.dpy, 0, att);
    if (vi == NULL) {
        fprintf(stderr, "No appropriate GLX visual found\n");
        exit(1);
    }
    Colormap cmap = XCreateColormap(glx.dpy, glx.root, vi->visual, AllocNone);
    XSetWindowAttributes swa;
    swa.colormap = cmap;
    swa.event_mask = ExposureMask | KeyPressMask | KeyReleaseMask | FocusChangeMask;
    glx.win = XCreateWindow(glx.dpy, glx.root, 0, 0, DESIRED_SCREEN_WIDTH, DESIRED_SCREEN_HEIGHT, 0, vi->depth, InputOutput, vi->visual, CWColormap | CWEventMask, &swa);
    
    glx.atom_wm_state = XInternAtom(glx.dpy, "_NET_WM_STATE", False);
    glx.atom_wm_state_fullscreen = XInternAtom(glx.dpy, "_NET_WM_STATE_FULLSCREEN", False);
    XMapWindow(glx.dpy, glx.win);

    if (configFullscreen) {
        glx.is_fullscreen = !glx.is_fullscreen;
        gfx_glx_set_fullscreen(true);
    }

    XStoreName(glx.dpy, glx.win, "Super Mario 64 PC-port GLX");
    GLXContext glc = glXCreateContext(glx.dpy, vi, NULL, GL_TRUE);
    glXMakeCurrent(glx.dpy, glx.win, glc);
    
    init_keymap();
    
    const char *extensions = glXQueryExtensionsString(glx.dpy, screen);
    
    if (gfx_glx_check_extension(extensions, "GLX_OML_sync_control")) {
        glx.glXGetSyncValuesOML = (PFNGLXGETSYNCVALUESOMLPROC)glXGetProcAddressARB((const GLubyte *)"glXGetSyncValuesOML");
        glx.glXSwapBuffersMscOML = (PFNGLXSWAPBUFFERSMSCOMLPROC)glXGetProcAddressARB((const GLubyte *)"glXSwapBuffersMscOML");
        glx.glXWaitForSbcOML = (PFNGLXWAITFORSBCOMLPROC)glXGetProcAddressARB((const GLubyte *)"glXWaitForSbcOML");
    }
    if (gfx_glx_check_extension(extensions, "GLX_EXT_swap_control")) {
        glx.glXSwapIntervalEXT = (PFNGLXSWAPINTERVALEXTPROC)glXGetProcAddressARB((const GLubyte *)"glXSwapIntervalEXT");
    }
    if (gfx_glx_check_extension(extensions, "GLX_SGI_swap_control")) {
        glx.glXSwapIntervalSGI = (PFNGLXSWAPINTERVALSGIPROC)glXGetProcAddressARB((const GLubyte *)"glXSwapIntervalSGI");
    }
    if (gfx_glx_check_extension(extensions, "GLX_SGI_video_sync")) {
        glx.glXGetVideoSyncSGI = (PFNGLXGETVIDEOSYNCSGIPROC)glXGetProcAddressARB((const GLubyte *)"glXGetVideoSyncSGI");
        glx.glXWaitVideoSyncSGI = (PFNGLXWAITVIDEOSYNCSGIPROC)glXGetProcAddressARB((const GLubyte *)"glXWaitVideoSyncSGI");
    }
    
    int64_t ust, msc, sbc;
    if (glx.glXGetSyncValuesOML != NULL && glx.glXGetSyncValuesOML(glx.dpy, glx.win, &ust, &msc, &sbc)) {
        glx.has_oml_sync_control = true;
        glx.ust0 = (uint64_t)ust;
    } else {
        glx.ust0 = get_time();
        if (glx.glXSwapIntervalEXT != NULL) {
            glx.glXSwapIntervalEXT(glx.dpy, glx.win, 1);
        } else if (glx.glXSwapIntervalSGI != NULL) {
            glx.glXSwapIntervalSGI(1);
        }
        if (glx.glXGetVideoSyncSGI != NULL) {
            // Try if it really works
            unsigned int count;
            if (glx.glXGetVideoSyncSGI(&count) == 0) {
                glx.last_sync_counter = count;
                glx.has_sgi_video_sync = true;
            }
        }
    }
    glx.vsync_interval = 16666;
}

static void gfx_glx_main_loop(void (*run_one_game_iter)(void)) {
    while (1) {
        run_one_game_iter();
    }
}

static void gfx_glx_get_dimensions(uint32_t *width, uint32_t *height) {
    XWindowAttributes attributes;
    XGetWindowAttributes(glx.dpy, glx.win, &attributes);
    *width = attributes.width;
    *height = attributes.height;
}

static void gfx_glx_hide_mouse(bool hide) {
    // Removes distracting mouse cursor during fullscreen play
    if (hide) {
        Cursor hideCursor;
        Pixmap bitmapNoData;
        XColor black;
        static char noData[] = { 0,0,0,0,0,0,0,0 };
        black.red = black.green = black.blue = 0;

        bitmapNoData = XCreateBitmapFromData(glx.dpy, glx.win, noData, 8, 8);
        hideCursor = XCreatePixmapCursor(glx.dpy, bitmapNoData, bitmapNoData,
                                       &black, &black, 0, 0);
        XDefineCursor(glx.dpy, glx.win, hideCursor);
        XSync(glx.dpy, False);
        XFreeCursor(glx.dpy, hideCursor);
        XFreePixmap(glx.dpy, bitmapNoData);
    } else {
        XUndefineCursor(glx.dpy, glx.win);
        XSync(glx.dpy, False);
    }

}

static void gfx_glx_set_fullscreen(bool on) {
    XEvent xev;
    configFullscreen = on;
    xev.xany.type = ClientMessage;
    xev.xclient.message_type = glx.atom_wm_state;
    xev.xclient.format = 32;
    xev.xclient.window = glx.win;
    xev.xclient.data.l[0] = on;
    xev.xclient.data.l[1] = glx.atom_wm_state_fullscreen;
    xev.xclient.data.l[2] = 0;
    xev.xclient.data.l[3] = 0;
    XSendEvent(glx.dpy, glx.root, 0, SubstructureNotifyMask | SubstructureRedirectMask, &xev);
    gfx_glx_hide_mouse(on);
}

static void gfx_glx_handle_events(void) {
    while (XPending(glx.dpy)) {
        XEvent xev;
        XNextEvent(glx.dpy, &xev);
        if (xev.type == FocusOut) {
            keyboard_on_all_keys_up();
        }
        if (xev.type == KeyPress || xev.type == KeyRelease) {
            if (xev.xkey.keycode < 256) {
                int scancode = glx.keymap[xev.xkey.keycode];
                if (scancode != 0) {
                    if (xev.type == KeyPress) {
                        if (scancode == 0x44) { // F10
                            glx.is_fullscreen = !glx.is_fullscreen;
                            gfx_glx_set_fullscreen(glx.is_fullscreen);
                        }
                        keyboard_on_key_down(scancode);
                    } else {
                        keyboard_on_key_up(scancode);
                    }
                }
            }
        }
    }
}

static bool gfx_glx_start_frame(void) {
    return true;
}

static void gfx_glx_swap_buffers_begin(void) {
    glx.wanted_ust += FRAME_INTERVAL_US_NUMERATOR; // advance 1/30 seconds on JP/US or 1/25 seconds on EU
    
    if (!glx.has_oml_sync_control && !glx.has_sgi_video_sync) {
        glFlush();
        
        uint64_t target = glx.wanted_ust / FRAME_INTERVAL_US_DENOMINATOR;
        uint64_t now;
        while (target > (now = (uint64_t)get_time() - glx.ust0)) {
            struct timespec ts = {(target - now) / 1000000, ((target - now) % 1000000) * 1000};
            if (nanosleep(&ts, NULL) == 0) {
                break;
            }
        }
        
        if (target + 2 * FRAME_INTERVAL_US_NUMERATOR / FRAME_INTERVAL_US_DENOMINATOR < now) {
            if (target + 32 * FRAME_INTERVAL_US_NUMERATOR / FRAME_INTERVAL_US_DENOMINATOR >= now) {
                printf("Dropping frame\n");
                glx.dropped_frame = true;
                return;
            } else {
                // Reset timer since we are way out of sync
                glx.wanted_ust = now * FRAME_INTERVAL_US_DENOMINATOR;
            }
        }
        glXSwapBuffers(glx.dpy, glx.win);
        glx.dropped_frame = false;
        
        return;
    }
    
    double vsyncs_to_wait = (int64_t)(glx.wanted_ust / FRAME_INTERVAL_US_DENOMINATOR - glx.last_ust) / (double)glx.vsync_interval;
    if (vsyncs_to_wait <= 0) {
        printf("Dropping frame\n");
        // Drop frame
        glx.dropped_frame = true;
        return;
    }
    if (floor(vsyncs_to_wait) != vsyncs_to_wait) {
        uint64_t left_ust = glx.last_ust + floor(vsyncs_to_wait) * glx.vsync_interval;
        uint64_t right_ust = glx.last_ust + ceil(vsyncs_to_wait) * glx.vsync_interval;
        uint64_t adjusted_wanted_ust = glx.wanted_ust / FRAME_INTERVAL_US_DENOMINATOR + (glx.last_ust + FRAME_INTERVAL_US_NUMERATOR / FRAME_INTERVAL_US_DENOMINATOR > glx.wanted_ust / FRAME_INTERVAL_US_DENOMINATOR ? 2000 : -2000);
        int64_t diff_left = adjusted_wanted_ust - left_ust;
        int64_t diff_right = right_ust - adjusted_wanted_ust;
        if (diff_left < 0) {
            diff_left = -diff_left;
        }
        if (diff_right < 0) {
            diff_right = -diff_right;
        }
        if (diff_left < diff_right) {
            vsyncs_to_wait = floor(vsyncs_to_wait);
        } else {
            vsyncs_to_wait = ceil(vsyncs_to_wait);
        }
        if (vsyncs_to_wait <= -4) {
            printf("vsyncs_to_wait became -4 or less so dropping frame\n");
            glx.dropped_frame = true;
            return;
        } else if (vsyncs_to_wait < 1) {
            vsyncs_to_wait = 1;
        }
    }
    glx.dropped_frame = false;
    //printf("Vsyncs to wait: %d, diff: %d\n", (int)vsyncs_to_wait, (int)(glx.last_ust + (int64_t)vsyncs_to_wait * glx.vsync_interval - glx.wanted_ust / 3));
    if (vsyncs_to_wait > 30) {
        // Unreasonable, so change to 2
        vsyncs_to_wait = 2;
    }
    glx.target_msc = glx.last_msc + vsyncs_to_wait;
    
    if (glx.has_oml_sync_control) {
        glx.glXSwapBuffersMscOML(glx.dpy, glx.win, glx.target_msc, 0, 0);
    } else if (glx.has_sgi_video_sync) {
        glFlush(); // Try to submit pending work. Don't use glFinish since that busy loops on NVIDIA proprietary driver.
        
        //uint64_t counter0;
        uint64_t counter1, counter2;
        
        //uint64_t before_wait = get_time();
        
        counter1 = glXGetVideoSyncSGI_wrapper();
        //counter0 = counter1;
        //int waits = 0;
        while (counter1 < (uint64_t)glx.target_msc - 1) {
            counter1 = glXWaitVideoSyncSGI_wrapper();
            //++waits;
        }
        
        //uint64_t before = get_time();
        glXSwapBuffers(glx.dpy, glx.win);
        
        
        counter2 = glXGetVideoSyncSGI_wrapper();
        while (counter2 < (uint64_t)glx.target_msc) {
            counter2 = glXWaitVideoSyncSGI_wrapper();
        }
        uint64_t after = get_time();
        
        //printf("%.3f %.3f %.3f\t%.3f\t%u %d %.2f %u %d\n", before_wait * 0.000060, before * 0.000060, after * 0.000060, (after - before) * 0.000060, counter0, counter2 - counter0, vsyncs_to_wait, (unsigned int)glx.target_msc, waits);
        glx.this_msc = counter2;
        glx.this_ust = after;
    }
}

static void gfx_glx_swap_buffers_end(void) {
    if (glx.dropped_frame || (!glx.has_oml_sync_control && !glx.has_sgi_video_sync)) {
        return;
    }
    
    int64_t ust, msc, sbc;
    if (glx.has_oml_sync_control) {
        if (!glx.glXWaitForSbcOML(glx.dpy, glx.win, 0, &ust, &msc, &sbc)) {
            // X connection broke or something?
            glx.last_ust += (glx.target_msc - glx.last_msc) * glx.vsync_interval;
            glx.last_msc = glx.target_msc;
            return;
        }
    } else {
        ust = glx.this_ust;
        msc = glx.this_msc;
    }
    uint64_t this_ust = ust - glx.ust0;
    uint64_t vsyncs_passed = msc - glx.last_msc;
    bool bad_vsync_interval = false;
    if (glx.last_ust != 0 && vsyncs_passed != 0) {
        uint64_t new_vsync_interval = (this_ust - glx.last_ust) / vsyncs_passed;
        if (new_vsync_interval <= 500000) {
            // Should be less than 0.5 seconds to be trusted
            glx.vsync_interval = new_vsync_interval;
        } else {
            bad_vsync_interval = true;
        }
        //printf("glx.vsync_interval: %d\n", (int)glx.vsync_interval);
    }
    glx.last_ust = this_ust;
    glx.last_msc = msc;
    if (msc != glx.target_msc) {
        printf("Frame too late by %d vsyncs\n", (int)(msc - glx.target_msc));
    }
    if (msc - glx.target_msc >= 8 || bad_vsync_interval) {
        // Frame arrived way too late, so reset timer from here
        printf("Reseting timer\n");
        glx.wanted_ust = this_ust * FRAME_INTERVAL_US_DENOMINATOR;
    }
}

static double gfx_glx_get_time(void) {
    return 0.0;
}

struct GfxWindowManagerAPI gfx_glx = {
    gfx_glx_init,
    gfx_glx_main_loop,
    gfx_glx_get_dimensions,
    gfx_glx_handle_events,
    gfx_glx_start_frame,
    gfx_glx_swap_buffers_begin,
    gfx_glx_swap_buffers_end,
    gfx_glx_get_time
};

#endif
