/*
 * chr  - Andrew Chronister &
 * inso - Alex Baines
 *
 * 12.19.2019
 *
 * Updated linux layer for 4coder
 *
 */

// TOP

#include <stdio.h>

#define FPS 60
#define frame_useconds (Million(1) / FPS)
#define frame_nseconds (Billion(1) / FPS)
#define SLASH '/'
#define DLL "so"

#include "4coder_base_types.h"
#include "4coder_version.h"
#include "4coder_events.h"

#include "4coder_table.h"
#include "4coder_types.h"
#include "4coder_default_colors.h"
#include "4coder_system_types.h"
#include "4ed_font_interface.h"

#define STATIC_LINK_API
#include "generated/system_api.h"

#define STATIC_LINK_API
#include "generated/graphics_api.h"

#define STATIC_LINK_API
#include "generated/font_api.h"

#include "4ed_font_set.h"
#include "4ed_render_target.h"
#include "4coder_search_list.h"
#include "4ed.h"

#include "generated/system_api.cpp"
#include "generated/graphics_api.cpp"
#include "generated/font_api.cpp"

#include "4coder_base_types.cpp"
#include "4coder_stringf.cpp"
#include "4coder_events.cpp"
#include "4coder_hash_functions.cpp"
#include "4coder_table.cpp"
#include "4coder_log.cpp"

#include "4coder_hash_functions.cpp"
#include "4coder_system_allocator.cpp"
#include "4coder_malloc_allocator.cpp"
#include "4coder_codepoint_map.cpp"

#include "4ed_mem.cpp"
#include "4ed_font_set.cpp"
#include "4coder_search_list.cpp"
#include "4ed_font_provider_freetype.h"
#include "4ed_font_provider_freetype.cpp"

#include <dirent.h>
#include <dlfcn.h>
#include <fcntl.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <locale.h>
#include <errno.h>
#include <pthread.h>

#include <sys/epoll.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/timerfd.h>
#include <sys/eventfd.h>
#include <sys/syscall.h>

#define Cursor XCursor
#undef function
#undef internal
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/cursorfont.h>
#include <X11/Xatom.h>
#include <X11/extensions/Xfixes.h>
#include <X11/XKBlib.h>
#include <X11/keysym.h>
#define function static
#undef Cursor

//#include <fontconfig/fontconfig.h>
#define internal static

#include <GL/glx.h>
#include <GL/glext.h>

#ifdef INSO_DEBUG
#define LINUX_FN_DEBUG(fmt, ...) do { \
fprintf(stderr, "%s: " fmt "\n", __func__, ##__VA_ARGS__);\
} while (0)

// I want to see a message
#undef AssertBreak
#define AssertBreak(m) ({\
fprintf(stderr, "\n** ASSERTION FAILURE: %s:%d: %s\n\n", __FILE__, __LINE__, #m);\
*((volatile u64*)0) = 0xba771e70ad5;\
})
#else
#define LINUX_FN_DEBUG(...)
#endif

////////////////////////////

struct Linux_Input_Chunk_Transient {
    Input_List event_list;
    b8 mouse_l_press;
    b8 mouse_l_release;
    b8 mouse_r_press;
    b8 mouse_r_release;
    i8 mouse_wheel;
    b8 trying_to_kill;
};

struct Linux_Input_Chunk_Persistent {
    Vec2_i32 mouse;
    Input_Modifier_Set_Fixed modifiers;
    b8 mouse_l;
    b8 mouse_r;
    b8 mouse_out_of_window;
};

struct Linux_Input_Chunk {
    Linux_Input_Chunk_Transient trans;
    Linux_Input_Chunk_Persistent pers;
};

struct Linux_Memory_Tracker_Node {
    Linux_Memory_Tracker_Node* prev;
    Linux_Memory_Tracker_Node* next;
    String_Const_u8 location;
    u64 size;
};

struct Linux_Vars {
    Thread_Context tctx;
    Arena frame_arena;
    
    Display* dpy;
    Window win;
    
    b32 has_xfixes;
    int xfixes_selection_event;
    XIM xim;
    XIC xic;
    //FcConfig* fontconfig;
    XkbDescPtr xkb;
    
    Linux_Input_Chunk input;
    int xkb_event;
    int xkb_group; // active keyboard layout (0-3)
    KeyCode prev_filtered_key;
    
    Key_Mode key_mode;
    
    int epoll;
    int step_timer_fd;
    u64 last_step_time;
    
    XCursor xcursors[APP_MOUSE_CURSOR_COUNT];
    Application_Mouse_Cursor cursor;
    XCursor hidden_cursor;
    i32 cursor_show;
    i32 prev_cursor_show;
    
    Node free_linux_objects;
    Node timer_objects;
    
    System_Mutex global_frame_mutex;
    pthread_mutex_t memory_tracker_mutex;
    Linux_Memory_Tracker_Node* memory_tracker_head;
    Linux_Memory_Tracker_Node* memory_tracker_tail;
    int memory_tracker_count;
    
    Arena clipboard_arena;
    String_Const_u8 clipboard_contents;
    b32 received_new_clipboard;
    b32 clipboard_catch_all;
    
    pthread_mutex_t audio_mutex;
    pthread_cond_t audio_cond;
    void* audio_ctx;
    Audio_Mix_Sources_Function* audio_src_func;
    Audio_Mix_Destination_Function* audio_dst_func;
    System_Thread audio_thread;
    
    Atom atom_TARGETS;
    Atom atom_CLIPBOARD;
    Atom atom_UTF8_STRING;
    Atom atom__NET_WM_STATE;
    Atom atom__NET_WM_STATE_MAXIMIZED_HORZ;
    Atom atom__NET_WM_STATE_MAXIMIZED_VERT;
    Atom atom__NET_WM_STATE_FULLSCREEN;
    Atom atom__NET_WM_PING;
    Atom atom__NET_WM_WINDOW_TYPE;
    Atom atom__NET_WM_WINDOW_TYPE_NORMAL;
    Atom atom__NET_WM_PID;
    Atom atom_WM_DELETE_WINDOW;
};

global Linux_Vars linuxvars;
global Render_Target render_target;

////////////////////////////

// Defererencing an epoll_event's .data.ptr will always give one of these event types.

typedef i32 Epoll_Kind;
enum {
    EPOLL_STEP_TIMER,
    EPOLL_X11,
    EPOLL_X11_INTERNAL,
    EPOLL_CLI_PIPE,
    EPOLL_USER_TIMER,
};

// Where per-event epoll data is not needed, .data.ptr will point to one of
// these static vars below.
// If per-event data is needed, container_of can be used on data.ptr
// to access the containing struct and all its other members.

internal Epoll_Kind epoll_tag_step_timer = EPOLL_STEP_TIMER;
internal Epoll_Kind epoll_tag_x11 = EPOLL_X11;
internal Epoll_Kind epoll_tag_x11_internal = EPOLL_X11_INTERNAL;
internal Epoll_Kind epoll_tag_cli_pipe = EPOLL_CLI_PIPE;

////////////////////////////

typedef i32 Linux_Object_Kind;
enum {
    LinuxObjectKind_ERROR = 0,
    LinuxObjectKind_Timer = 1,
    LinuxObjectKind_Thread = 2,
    LinuxObjectKind_Mutex = 3,
    LinuxObjectKind_ConditionVariable = 4,
};

struct Linux_Object {
    Linux_Object_Kind kind;
    Node node;
    union {
        struct {
            int fd;
            Epoll_Kind epoll_tag;
        } timer;
        struct {
            pthread_t pthread;
            Thread_Function* proc;
            void* ptr;
        } thread;
        pthread_mutex_t mutex;
        pthread_cond_t condition_variable;
    };
};

Linux_Object*
handle_to_object(Plat_Handle ph){
    return *(Linux_Object**)&ph;
}

Plat_Handle
object_to_handle(Linux_Object* obj) {
    return *(Plat_Handle*)&obj;
}

internal Linux_Object*
linux_alloc_object(Linux_Object_Kind kind){
    Linux_Object* result = NULL;
    
    if (linuxvars.free_linux_objects.next != &linuxvars.free_linux_objects) {
        result = CastFromMember(Linux_Object, node, linuxvars.free_linux_objects.next);
    }
    
    if (result == NULL) {
        i32 count = 512;
        
        Linux_Object* objects = (Linux_Object*)system_memory_allocate(
                                                                      sizeof(Linux_Object) * count,
                                                                      file_name_line_number_lit_u8
                                                                      );
        
        objects[0].node.prev = &linuxvars.free_linux_objects;
        linuxvars.free_linux_objects.next = &objects[0].node;
        for (i32 i = 1; i < count; ++i) {
            objects[i - 1].node.next = &objects[i].node;
            objects[i].node.prev = &objects[i - 1].node;
        }
        objects[count - 1].node.next = &linuxvars.free_linux_objects;
        linuxvars.free_linux_objects.prev = &objects[count - 1].node;
        
        result = CastFromMember(Linux_Object, node, linuxvars.free_linux_objects.next);
    }
    
    Assert(result != 0);
    dll_remove(&result->node);
    block_zero_struct(result);
    result->kind = kind;
    return result;
}

internal void
linux_free_object(Linux_Object *object){
    if (object->node.next != 0){
        dll_remove(&object->node);
    }
    dll_insert(&linuxvars.free_linux_objects, &object->node);
}

////////////////////////////

internal int
linux_compare_file_infos(File_Info** a, File_Info** b) {
    b32 a_hidden = (*a)->file_name.str[0] == '.';
    b32 b_hidden = (*b)->file_name.str[0] == '.';
    
    // hidden files lower in list
    if(a_hidden != b_hidden) {
        return a_hidden - b_hidden;
    }
    
    // push_stringf seems to null terminate
    return strcoll((char*)(*a)->file_name.str, (char*)(*b)->file_name.str);
}

internal int
linux_system_get_file_list_filter(const struct dirent *dirent) {
    String_Const_u8 file_name = SCu8((u8*)dirent->d_name);
    if (string_match(file_name, string_u8_litexpr("."))) {
        return 0;
    }
    else if (string_match(file_name, string_u8_litexpr(".."))) {
        return 0;
    }
    return 1;
}

internal u64
linux_us_from_timespec(const struct timespec timespec) {
    return timespec.tv_nsec/Thousand(1) + Million(1) * timespec.tv_sec;
}

internal File_Attribute_Flag
linux_convert_file_attribute_flags(int mode) {
    File_Attribute_Flag result = {};
    MovFlag(mode, S_IFDIR, result, FileAttribute_IsDirectory);
    return result;
}

internal File_Attributes
linux_file_attributes_from_struct_stat(struct stat* file_stat) {
    File_Attributes result = {};
    result.size = file_stat->st_size;
    result.last_write_time = linux_us_from_timespec(file_stat->st_mtim);
    result.flags = linux_convert_file_attribute_flags(file_stat->st_mode);
    return(result);
}

internal void
linux_schedule_step(){
    u64 now  = system_now_time();
    u64 diff = (now - linuxvars.last_step_time);
    
    struct itimerspec its = {};
    timerfd_gettime(linuxvars.step_timer_fd, &its);
    
    if (diff > frame_useconds) {
        its.it_value.tv_nsec = 1;
        timerfd_settime(linuxvars.step_timer_fd, 0, &its, NULL);
    } else {
        if (its.it_value.tv_sec == 0 && its.it_value.tv_nsec == 0){
            its.it_value.tv_nsec = (frame_useconds - diff) * 1000UL;
            timerfd_settime(linuxvars.step_timer_fd, 0, &its, NULL);
        }
    }
}

enum wm_state_mode {
    WM_STATE_DEL = 0,
    WM_STATE_ADD = 1,
    WM_STATE_TOGGLE = 2,
};

internal void
linux_set_wm_state(Atom one, Atom two, enum wm_state_mode mode){
    //NOTE(inso): this will only work after the window has been mapped
    
    XEvent e = {};
    e.xany.type = ClientMessage;
    e.xclient.message_type = linuxvars.atom__NET_WM_STATE;
    e.xclient.format = 32;
    e.xclient.window = linuxvars.win;
    e.xclient.data.l[0] = mode;
    e.xclient.data.l[1] = one;
    e.xclient.data.l[2] = two;
    e.xclient.data.l[3] = 1L;
    
    XSendEvent(linuxvars.dpy,
               RootWindow(linuxvars.dpy, 0),
               0, SubstructureNotifyMask | SubstructureRedirectMask, &e);
}

internal void
linux_window_maximize(enum wm_state_mode mode){
    linux_set_wm_state(linuxvars.atom__NET_WM_STATE_MAXIMIZED_HORZ, linuxvars.atom__NET_WM_STATE_MAXIMIZED_VERT, mode);
}

internal void
linux_window_fullscreen(enum wm_state_mode mode) {
    linux_set_wm_state(linuxvars.atom__NET_WM_STATE_FULLSCREEN, 0, mode);
}

internal int
linux_get_xsettings_dpi(Display* dpy, int screen){
    struct XSettingHeader {
        u8 type;
        u8 pad0;
        u16 name_len;
        char name[0];
    };
    
    struct XSettings {
        u8 byte_order;
        u8 pad[3];
        u32 serial;
        u32 num_settings;
    };
    
    enum { XSettingsTypeInt, XSettingsTypeString, XSettingsTypeColor };
    
    int dpi = -1;
    unsigned char* prop = NULL;
    char sel_buffer[64];
    struct XSettings* xs;
    const char* p;
    
    snprintf(sel_buffer, sizeof(sel_buffer), "_XSETTINGS_S%d", screen);
    
    Atom XSET_SEL = XInternAtom(dpy, sel_buffer, True);
    Atom XSET_SET = XInternAtom(dpy, "_XSETTINGS_SETTINGS", True);
    
    if (XSET_SEL == None || XSET_SET == None){
        //LOG("XSETTINGS unavailable.\n");
        return(dpi);
    }
    
    Window xset_win = XGetSelectionOwner(dpy, XSET_SEL);
    if (xset_win == None){
        // TODO(inso): listen for the ClientMessage about it becoming available?
        //             there's not much point atm if DPI scaling is only done at startup
        goto out;
    }
    
    {
        Atom type;
        int fmt;
        unsigned long pad, num;
        
        if (XGetWindowProperty(dpy, xset_win, XSET_SET, 0, 1024, False, XSET_SET, &type, &fmt, &num, &pad, &prop) != Success){
            //LOG("XSETTINGS: GetWindowProperty failed.\n");
            goto out;
        }
        
        if (fmt != 8){
            //LOG("XSETTINGS: Wrong format.\n");
            goto out;
        }
    }
    
    xs = (struct XSettings*)prop;
    p  = (char*)(xs + 1);
    
    if (xs->byte_order != 0){
        //LOG("FIXME: XSETTINGS not host byte order?\n");
        goto out;
    }
    
    for (int i = 0; i < xs->num_settings; ++i){
        struct XSettingHeader* h = (struct XSettingHeader*)p;
        
        p += sizeof(struct XSettingHeader);
        p += h->name_len;
        p += ((4 - (h->name_len & 3)) & 3);
        p += 4; // serial
        
        switch (h->type){
            case XSettingsTypeInt: {
                if (strncmp(h->name, "Xft/DPI", h->name_len) == 0){
                    dpi = *(i32*)p;
                    if (dpi != -1) dpi /= 1024;
                }
                p += 4;
            } break;
            
            case XSettingsTypeString: {
                u32 len = *(u32*)p;
                p += 4;
                p += len;
                p += ((4 - (len & 3)) & 3);
            } break;
            
            case XSettingsTypeColor: {
                p += 8;
            } break;
            
            default: {
                //LOG("XSETTINGS: Got invalid type...\n");
                goto out;
            } break;
        }
    }
    
    out:
    if (prop){
        XFree(prop);
    }
    
    return dpi;
}

internal void*
linux_thread_proc_start(void* arg) {
    Linux_Object* info = (Linux_Object*)arg;
    Assert(info->kind == LinuxObjectKind_Thread);
    info->thread.proc(info->thread.ptr);
    return NULL;
}

#include "linux_icon.h"
internal void
linux_set_icon(Display* d, Window w){
    Atom WM_ICON = XInternAtom(d, "_NET_WM_ICON", False);
    XChangeProperty(d, w, WM_ICON, XA_CARDINAL, 32, PropModeReplace, (unsigned char*)linux_icon, sizeof(linux_icon) / sizeof(long));
}

#include "linux_error_box.cpp"

function void
os_popup_error(char *title, char *message){
    system_error_box(message);
    exit(1);
}

////////////////////////////

#include "linux_4ed_functions.cpp"
#include "linux_4ed_audio.cpp"

////////////////////////////

#include <GL/gl.h>
#include "opengl/4ed_opengl_defines.h"
#define GL_FUNC(N,R,P) typedef R (CALL_CONVENTION N##_Function)P; N##_Function *N = 0;
#include "opengl/4ed_opengl_funcs.h"
#include "opengl/4ed_opengl_render.cpp"

internal
graphics_get_texture_sig(){
    return(gl__get_texture(dim, texture_kind));
}

internal
graphics_fill_texture_sig(){
    return(gl__fill_texture(texture_kind, texture, p, dim, data));
}

////////////////////////////

internal Face*
font_make_face(Arena* arena, Face_Description* description, f32 scale_factor) {
    
    Face_Description local_description = *description;
    String_Const_u8* name = &local_description.font.file_name;
    
    // if description->font.file_name is a relative path, prepend the font directory.
    if(string_get_character(*name, 0) != '/') {
        String_Const_u8 binary = system_get_path(arena, SystemPath_Binary);
        *name = push_u8_stringf(arena, "%.*sfonts/%.*s", string_expand(binary), string_expand(*name));
    }
    
    Face* result = ft__font_make_face(arena, &local_description, scale_factor);
    
    if(!result) {
        // is this fatal? 4ed.cpp:277 (caller) does not check for null.
        char msg[4096];
        snprintf(msg, sizeof(msg), "Unable to load font: %.*s", string_expand(*name));
        system_error_box(msg);
    }
    
    return(result);
}

////////////////////////////

internal b32
glx_init(void) {
    int glx_maj, glx_min;
    
    if(!glXQueryVersion(linuxvars.dpy, &glx_maj, &glx_min)) {
        return false;
    }
    
    return glx_maj > 1 || (glx_maj == 1 && glx_min >= 3);
}

internal b32
glx_get_config(GLXFBConfig* fb_config, XVisualInfo* vi) {
    
    static const int attrs[] = {
        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,
        None
    };
    
    int conf_count = 0;
    GLXFBConfig* conf_list = glXChooseFBConfig(linuxvars.dpy, DefaultScreen(linuxvars.dpy), attrs, &conf_count);
    if(!conf_count || conf_count <= 0) {
        return false;
    }
    
    *fb_config = *conf_list;
    XFree(conf_list);
    
    XVisualInfo* xvi = glXGetVisualFromFBConfig(linuxvars.dpy, *fb_config);
    if(!xvi) {
        return false;
    }
    
    *vi = *xvi;
    XFree(xvi);
    
    return true;
}

internal b32 glx_ctx_error;

internal int
glx_error_handler(Display* dpy, XErrorEvent* ev){
    glx_ctx_error = true;
    return 0;
}

typedef GLXContext (glXCreateContextAttribsARB_Function)(Display*, GLXFBConfig, GLXContext, Bool, const int*);
typedef void       (glXSwapIntervalEXT_Function)        (Display *dpy, GLXDrawable drawable, int interval);
typedef int        (glXSwapIntervalMESA_Function)       (unsigned int interval);
typedef int        (glXGetSwapIntervalMESA_Function)    (void);
typedef int        (glXSwapIntervalSGI_Function)        (int interval);

internal b32
glx_create_context(GLXFBConfig fb_config){
    const char *glx_exts = glXQueryExtensionsString(linuxvars.dpy, DefaultScreen(linuxvars.dpy));
    
    glXCreateContextAttribsARB_Function *glXCreateContextAttribsARB = 0;
    glXSwapIntervalEXT_Function         *glXSwapIntervalEXT = 0;
    glXSwapIntervalMESA_Function        *glXSwapIntervalMESA = 0;
    glXGetSwapIntervalMESA_Function     *glXGetSwapIntervalMESA = 0;
    glXSwapIntervalSGI_Function         *glXSwapIntervalSGI = 0;
    
#define GLXLOAD(f) f = (f##_Function*) glXGetProcAddressARB((const GLubyte*) #f);
    GLXLOAD(glXCreateContextAttribsARB);
    
    GLXContext ctx = NULL;
    int (*old_handler)(Display*, XErrorEvent*) = XSetErrorHandler(&glx_error_handler);
    
    if (glXCreateContextAttribsARB == NULL){
        //LOG("glXCreateContextAttribsARB() not found, using old-style GLX context\n" );
        ctx = glXCreateNewContext(linuxvars.dpy, fb_config, GLX_RGBA_TYPE, 0, True);
    } else {
        static const int context_attribs[] = {
            GLX_CONTEXT_MAJOR_VERSION_ARB, 2,
            GLX_CONTEXT_MINOR_VERSION_ARB, 1,
            GLX_CONTEXT_PROFILE_MASK_ARB , GLX_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB,
#if GL_DEBUG_MODE
            GLX_CONTEXT_FLAGS_ARB        , GLX_CONTEXT_DEBUG_BIT_ARB,
#endif
            None
        };
        
        //LOG("Creating GL 2.1 context... ");
        ctx = glXCreateContextAttribsARB(linuxvars.dpy, fb_config, 0, True, context_attribs);
    }
    
    XSync(linuxvars.dpy, False);
    if(glx_ctx_error || !ctx) {
        return false;
    }
    
    XSync(linuxvars.dpy, False);
    XSetErrorHandler(old_handler);
    
    //b32 direct = glXIsDirect(linuxvars.dpy, ctx);
    
    //LOG("Making context current\n");
    glXMakeCurrent(linuxvars.dpy, linuxvars.win, ctx);
    
    //glx_enable_vsync();
    
    // NOTE(allen): Load gl functions
#define GL_FUNC(f,R,P) GLXLOAD(f)
#include "opengl/4ed_opengl_funcs.h"
    
#undef GLXLOAD
    
    return true;
}

////////////////////////////

internal void
linux_x11_init(int argc, char** argv, Plat_Settings* settings) {
    
    Display* dpy = XOpenDisplay(0);
    if (!dpy){
        fprintf(stderr, "FATAL: Cannot open X11 Display!\n");
        exit(1);
    }
    
    linuxvars.dpy = dpy;
    
#define LOAD_ATOM(x) linuxvars.atom_##x = XInternAtom(linuxvars.dpy, #x, False);
    
    LOAD_ATOM(TARGETS);
    LOAD_ATOM(CLIPBOARD);
    LOAD_ATOM(UTF8_STRING);
    LOAD_ATOM(_NET_WM_STATE);
    LOAD_ATOM(_NET_WM_STATE_MAXIMIZED_HORZ);
    LOAD_ATOM(_NET_WM_STATE_MAXIMIZED_VERT);
    LOAD_ATOM(_NET_WM_STATE_FULLSCREEN);
    LOAD_ATOM(_NET_WM_PING);
    LOAD_ATOM(_NET_WM_WINDOW_TYPE);
    LOAD_ATOM(_NET_WM_WINDOW_TYPE_NORMAL);
    LOAD_ATOM(_NET_WM_PID);
    LOAD_ATOM(WM_DELETE_WINDOW);
    
#undef LOAD_ATOM
    
    if (!glx_init()){
        system_error_box("Your XServer's GLX version is too old. GLX 1.3+ is required.");
    }
    
    GLXFBConfig fb_config;
    XVisualInfo vi;
    if (!glx_get_config(&fb_config, &vi)){
        system_error_box("Could not get a matching GLX FBConfig. Check your OpenGL drivers are installed correctly.");
    }
    
    // TODO: window size
#define WINDOW_W_DEFAULT 800
#define WINDOW_H_DEFAULT 600
    int w = WINDOW_W_DEFAULT;
    int h = WINDOW_H_DEFAULT;
    
    // TEMP
    render_target.width = w;
    render_target.height = h;
    
    XSetWindowAttributes swa = {};
    swa.backing_store = WhenMapped;
    swa.event_mask = StructureNotifyMask;
    swa.bit_gravity = NorthWestGravity;
    swa.colormap = XCreateColormap(dpy, RootWindow(dpy, vi.screen), vi.visual, AllocNone);
    
    u32 CWflags = CWBackingStore|CWBitGravity|CWBackPixel|CWBorderPixel|CWColormap|CWEventMask;
    linuxvars.win = XCreateWindow(dpy, RootWindow(dpy, vi.screen), 0, 0, w, h, 0, vi.depth, InputOutput, vi.visual, CWflags, &swa);
    
    if (!linuxvars.win){
        system_error_box("XCreateWindow failed. Make sure your display is set up correctly.");
    }
    
    //NOTE(inso): Set the window's type to normal
    XChangeProperty(linuxvars.dpy, linuxvars.win, linuxvars.atom__NET_WM_WINDOW_TYPE, XA_ATOM, 32, PropModeReplace, (unsigned char*)&linuxvars.atom__NET_WM_WINDOW_TYPE_NORMAL, 1);
    
    //NOTE(inso): window managers want the PID as a window property for some reason.
    pid_t pid = getpid();
    XChangeProperty(linuxvars.dpy, linuxvars.win, linuxvars.atom__NET_WM_PID, XA_CARDINAL, 32, PropModeReplace, (unsigned char*)&pid, 1);
    
    //NOTE(inso): set wm properties
    XStoreName(linuxvars.dpy, linuxvars.win, WINDOW_NAME);
    
    XSizeHints *sz_hints = XAllocSizeHints();
    XWMHints   *wm_hints = XAllocWMHints();
    XClassHint *cl_hints = XAllocClassHint();
    
    sz_hints->flags = PMinSize | PMaxSize | PWinGravity;
    
    sz_hints->min_width = 50;
    sz_hints->min_height = 50;
    
    sz_hints->max_width = sz_hints->max_height = (1UL << 16UL);
    sz_hints->win_gravity = NorthWestGravity;
    
    if (settings->set_window_pos){
        sz_hints->flags |= USPosition;
        sz_hints->x = settings->window_x;
        sz_hints->y = settings->window_y;
    }
    
    wm_hints->flags |= InputHint | StateHint;
    wm_hints->input = True;
    wm_hints->initial_state = NormalState;
    
    cl_hints->res_name = "4coder";
    cl_hints->res_class = "4coder";
    
    char* win_name_list[] = { WINDOW_NAME };
    XTextProperty win_name;
    XStringListToTextProperty(win_name_list, 1, &win_name);
    
    XSetWMProperties(linuxvars.dpy, linuxvars.win, &win_name, NULL, argv, argc, sz_hints, wm_hints, cl_hints);
    
    XFree(win_name.value);
    XFree(sz_hints);
    XFree(wm_hints);
    XFree(cl_hints);
    
    linux_set_icon(linuxvars.dpy, linuxvars.win);
    
    // NOTE(inso): make the window visible
    XMapWindow(linuxvars.dpy, linuxvars.win);
    
    if(!glx_create_context(fb_config)) {
        system_error_box("Unable to create GLX context.");
    }
    
    XRaiseWindow(linuxvars.dpy, linuxvars.win);
    
    if (settings->set_window_pos){
        XMoveWindow(linuxvars.dpy, linuxvars.win, settings->window_x, settings->window_y);
    }
    
    if (settings->maximize_window){
        linux_set_wm_state(linuxvars.atom__NET_WM_STATE_MAXIMIZED_HORZ, linuxvars.atom__NET_WM_STATE_MAXIMIZED_VERT, WM_STATE_ADD);
    } else if (settings->fullscreen_window){
        linux_set_wm_state(linuxvars.atom__NET_WM_STATE_FULLSCREEN, 0, WM_STATE_ADD);
    }
    
    XSync(linuxvars.dpy, False);
    
    Atom wm_protos[] = {
        linuxvars.atom_WM_DELETE_WINDOW,
        linuxvars.atom__NET_WM_PING
    };
    
    XSetWMProtocols(linuxvars.dpy, linuxvars.win, wm_protos, 2);
    
    // XFixes extension for clipboard notification.
    {
        int xfixes_version_unused, xfixes_err_unused;
        Bool has_xfixes = XQueryExtension(linuxvars.dpy, "XFIXES", &xfixes_version_unused, &linuxvars.xfixes_selection_event, &xfixes_err_unused);
        linuxvars.has_xfixes = (has_xfixes == True);
        
        // request notifications for CLIPBOARD updates.
        if(has_xfixes) {
            XFixesSelectSelectionInput(linuxvars.dpy, linuxvars.win, linuxvars.atom_CLIPBOARD, XFixesSetSelectionOwnerNotifyMask);
        }
    }
    
    // Input handling init
    
    setlocale(LC_ALL, "");
    XSetLocaleModifiers("");
    b32 locale_supported = XSupportsLocale();
    
    if (!locale_supported){
        setlocale(LC_ALL, "C");
    }
    
    linuxvars.xim = XOpenIM(dpy, 0, 0, 0);
    if (!linuxvars.xim){
        // NOTE(inso): Try falling back to the internal XIM implementation that
        // should in theory always exist.
        XSetLocaleModifiers("@im=none");
        linuxvars.xim = XOpenIM(dpy, 0, 0, 0);
    }
    
    // If it still isn't there we're screwed.
    if (!linuxvars.xim){
        system_error_box("Could not initialize X Input.");
    }
    
    XIMStyles *styles = NULL;
    const XIMStyle style_want = (XIMPreeditNothing | XIMStatusNothing);
    b32 found_style = false;
    
    if (!XGetIMValues(linuxvars.xim, XNQueryInputStyle, &styles, NULL) && styles){
        for (i32 i = 0; i < styles->count_styles; ++i){
            XIMStyle style = styles->supported_styles[i];
            if (style == style_want) {
                found_style = true;
                break;
            }
        }
    }
    
    if(!found_style) {
        system_error_box("Could not find supported X Input style.");
    }
    
    XFree(styles);
    
    linuxvars.xic = XCreateIC(linuxvars.xim,
                              XNInputStyle, style_want,
                              XNClientWindow, linuxvars.win,
                              XNFocusWindow, linuxvars.win,
                              NULL);
    
    if(!linuxvars.xic) {
        system_error_box("Error creating X Input context.");
    }
    
    int xim_event_mask;
    if (XGetICValues(linuxvars.xic, XNFilterEvents, &xim_event_mask, NULL)){
        xim_event_mask = 0;
    }
    
    u32 event_mask = ExposureMask
        | KeyPressMask | KeyReleaseMask
        | ButtonPressMask | ButtonReleaseMask
        | EnterWindowMask | LeaveWindowMask
        | PointerMotionMask
        | FocusChangeMask
        | StructureNotifyMask
        | ExposureMask | VisibilityChangeMask
        | xim_event_mask;
    
    XSelectInput(linuxvars.dpy, linuxvars.win, event_mask);
    
    // init XKB keyboard extension
    
    if(!XkbQueryExtension(linuxvars.dpy, 0, &linuxvars.xkb_event, 0, 0, 0)) {
        system_error_box("XKB Extension not available.");
    }
    
    XkbSelectEvents(linuxvars.dpy, XkbUseCoreKbd, XkbAllEventsMask, XkbAllEventsMask);
    linuxvars.xkb = XkbGetMap(linuxvars.dpy, XkbKeyTypesMask | XkbKeySymsMask, XkbUseCoreKbd);
    if(!linuxvars.xkb) {
        system_error_box("Error getting XKB keyboard map.");
    }
    
    if(XkbGetNames(linuxvars.dpy, XkbKeyNamesMask, linuxvars.xkb) != Success) {
        system_error_box("Error getting XKB key names.");
    }
    
    // closer to windows behaviour (holding key doesn't generate release events)
    XkbSetDetectableAutoRepeat(linuxvars.dpy, True, NULL);
    
    XCursor cursors[APP_MOUSE_CURSOR_COUNT] = {
        None,
        None,
        XCreateFontCursor(linuxvars.dpy, XC_xterm),
        XCreateFontCursor(linuxvars.dpy, XC_sb_h_double_arrow),
        XCreateFontCursor(linuxvars.dpy, XC_sb_v_double_arrow)
    };
    block_copy(linuxvars.xcursors, cursors, sizeof(cursors));
    
    // sneaky invisible cursor
    {
        char data = 0;
        XColor c  = {};
        Pixmap p  = XCreateBitmapFromData(linuxvars.dpy, linuxvars.win, &data, 1, 1);
        
        linuxvars.hidden_cursor = XCreatePixmapCursor(linuxvars.dpy, p, p, &c, &c, 0, 0);
        
        XFreePixmap(linuxvars.dpy, p);
    }
}

global Key_Code keycode_lookup_table_physical[255];
global Key_Code keycode_lookup_table_language[255];

struct SymCode {
    KeySym sym;
    Key_Code code;
};

internal void
linux_keycode_init_common(Display* dpy, Key_Code* keycode_lookup_table, SymCode* sym_table, SymCode* p, size_t sym_table_size){
    
    *p++ = { XK_space, KeyCode_Space };
    *p++ = { XK_Tab, KeyCode_Tab };
    *p++ = { XK_Escape, KeyCode_Escape };
    *p++ = { XK_Pause, KeyCode_Pause };
    *p++ = { XK_Up, KeyCode_Up };
    *p++ = { XK_Down, KeyCode_Down };
    *p++ = { XK_Left, KeyCode_Left };
    *p++ = { XK_Right, KeyCode_Right };
    *p++ = { XK_BackSpace, KeyCode_Backspace };
    *p++ = { XK_Return, KeyCode_Return };
    *p++ = { XK_Delete, KeyCode_Delete };
    *p++ = { XK_Insert, KeyCode_Insert };
    *p++ = { XK_Home, KeyCode_Home };
    *p++ = { XK_End, KeyCode_End };
    *p++ = { XK_Page_Up, KeyCode_PageUp };
    *p++ = { XK_Page_Down, KeyCode_PageDown };
    *p++ = { XK_Caps_Lock, KeyCode_CapsLock };
    *p++ = { XK_Num_Lock, KeyCode_NumLock };
    *p++ = { XK_Scroll_Lock, KeyCode_ScrollLock };
    *p++ = { XK_Menu, KeyCode_Menu };
    *p++ = { XK_Shift_L, KeyCode_Shift };
    *p++ = { XK_Shift_R, KeyCode_Shift };
    *p++ = { XK_Control_L, KeyCode_Control };
    *p++ = { XK_Control_R, KeyCode_Control };
    *p++ = { XK_Alt_L, KeyCode_Alt };
    *p++ = { XK_Alt_R, KeyCode_Alt };
    *p++ = { XK_Super_L, KeyCode_Command };
    *p++ = { XK_Super_R, KeyCode_Command };
    
    for (Key_Code k = KeyCode_F1; k <= KeyCode_F24; ++k){
        *p++ = { XK_F1 + (k - KeyCode_F1), k };
    }
    
    for (Key_Code k = KeyCode_NumPad0; k <= KeyCode_NumPad9; ++k){
        *p++ = { XK_KP_0 + (k - KeyCode_NumPad0), k };
    }
    
    *p++ = { XK_KP_Multiply, KeyCode_NumPadStar };
    *p++ = { XK_KP_Add, KeyCode_NumPadPlus };
    *p++ = { XK_KP_Subtract, KeyCode_NumPadMinus };
    *p++ = { XK_KP_Decimal, KeyCode_NumPadDot };
    *p++ = { XK_KP_Delete, KeyCode_NumPadDot }; // seems to take precedence over Decimal...
    *p++ = { XK_KP_Divide, KeyCode_NumPadSlash };
    *p++ = { XK_KP_Enter, KeyCode_Return }; // NumPadEnter?
    
    const int table_size = p - sym_table;
    Assert(table_size < sym_table_size);
    
    Key_Code next_extra = KeyCode_Ex1;
    const Key_Code max_extra = KeyCode_Ex29;
    
    for(int i = XkbMinLegalKeyCode; i <= XkbMaxLegalKeyCode; ++i) {
        KeySym sym = NoSymbol;
        
        // lookup key in current layout with no modifiers held (0)
        if(!XkbTranslateKeyCode(linuxvars.xkb, i, XkbBuildCoreState(0, linuxvars.xkb_group), NULL, &sym)) {
            continue;
        }
        
        int j;
        for(j = 0; j < table_size; ++j) {
            if(sym_table[j].sym == sym) {
                keycode_lookup_table[i] = sym_table[j].code;
                //printf("lookup %s = %d\n", key_code_name[sym_table[j].code], i);
                break;
            }
        }
        
        if(j != table_size){
            continue;
        }
        
        // nothing found - try with shift held (needed for e.g. belgian numbers to bind).
        KeySym shift_sym = NoSymbol;
        
        if(!XkbTranslateKeyCode(linuxvars.xkb, i, XkbBuildCoreState(ShiftMask, linuxvars.xkb_group), NULL, &shift_sym)) {
            continue;
        }
        
        for(j = 0; j < table_size; ++j) {
            if(sym_table[j].sym == shift_sym) {
                keycode_lookup_table[i] = sym_table[j].code;
                //printf("lookup %s = %d\n", key_code_name[sym_table[j].code], i);
                break;
            }
        }
        
        // something unknown bound, put it in extra
        if(j == table_size && sym != NoSymbol && next_extra <= max_extra && keycode_lookup_table[i] == 0) {
            keycode_lookup_table[i] = next_extra++;
        }
    }
    
}

internal void
linux_keycode_init_language(Display* dpy, Key_Code* keycode_lookup_table){
    SymCode sym_table[300];
    SymCode* p = sym_table;
    
    for(unsigned int i = 0; i < 26; ++i) {
        *p++ = { XK_a + i, KeyCode_A + i};
    }
    
    for(unsigned int i = 0; i < 26; ++i) {
        *p++ = { XK_A + i, KeyCode_A + i};
    }
    
    for(unsigned int i = 0; i <= 9; ++i) {
        *p++ = { XK_0 + i, KeyCode_0 + i};
    }
    
    *p++ = { XK_grave, KeyCode_Tick };
    *p++ = { XK_minus, KeyCode_Minus };
    *p++ = { XK_equal, KeyCode_Equal };
    *p++ = { XK_bracketleft, KeyCode_LeftBracket };
    *p++ = { XK_bracketright, KeyCode_RightBracket };
    *p++ = { XK_semicolon, KeyCode_Semicolon };
    *p++ = { XK_apostrophe, KeyCode_Quote };
    *p++ = { XK_comma, KeyCode_Comma };
    *p++ = { XK_period, KeyCode_Period };
    *p++ = { XK_slash, KeyCode_ForwardSlash };
    *p++ = { XK_backslash, KeyCode_BackwardSlash };
    
    linux_keycode_init_common(dpy, keycode_lookup_table, sym_table, p, ArrayCount(sym_table));
}

internal void
linux_keycode_init_physical(Display* dpy, Key_Code* keycode_lookup_table){
    
    // Find common keys by their key label
    SymCode sym_table[100];
    linux_keycode_init_common(dpy, keycode_lookup_table, sym_table, sym_table, ArrayCount(sym_table));
    
    // Find these keys by physical position, and map to QWERTY KeyCodes
#define K(k) glue(KeyCode_, k)
    static const u8 positional_keys[] = {
        K(1), K(2), K(3), K(4), K(5), K(6), K(7), K(8), K(9), K(0), K(Minus), K(Equal),
        K(Q), K(W), K(E), K(R), K(T), K(Y), K(U), K(I), K(O), K(P), K(LeftBracket), K(RightBracket),
        K(A), K(S), K(D), K(F), K(G), K(H), K(J), K(K), K(L), K(Semicolon), K(Quote), /*uk hash*/0,
        K(Z), K(X), K(C), K(V), K(B), K(N), K(M), K(Comma), K(Period), K(ForwardSlash), 0, 0
    };
#undef K
    
    // XKB gives the alphanumeric keys names like AE01 -> E is the row (from B-E), 01 is the column (01-12).
    // to get key names in .ps file: setxkbmap -print | xkbcomp - - | xkbprint -label name - out.ps
    
    static const int ncols = 12;
    static const int nrows = 4;
    
    for(int i = XkbMinLegalKeyCode; i <= XkbMaxLegalKeyCode; ++i) {
        const char* name = linuxvars.xkb->names->keys[i].name;
        
        // alphanumeric keys
        
        if(name[0] == 'A' && name[1] >= 'B' && name[1] <= 'E') {
            int row = (nrows - 1) - (name[1] - 'B');
            int col = (name[2] - '0') * 10 + (name[3] - '0') - 1;
            
            if(row >= 0 && row < nrows && col >= 0 && col < ncols) {
                keycode_lookup_table[i] = positional_keys[row * ncols + col];
            }
        }
        
        // numpad
        
        else if(name[0] == 'K' && name[1] == 'P' && name[2] >= '0' && name[2] <= '9' && !name[3]) {
            
            // don't overwrite - for e.g. laptops with numpad keys embedded in the normal ones, toggling with numlock
            if(keycode_lookup_table[i] == 0) {
                keycode_lookup_table[i] = KeyCode_NumPad0 + name[2] - '0';
            }
        }
        
        // a few special cases:
        
        else if(memcmp(name, "TLDE", XkbKeyNameLength) == 0) {
            keycode_lookup_table[i] = KeyCode_Tick;
        } else if(memcmp(name, "BKSL", XkbKeyNameLength) == 0) {
            keycode_lookup_table[i] = KeyCode_BackwardSlash;
        } else if(memcmp(name, "LSGT", XkbKeyNameLength) == 0) {
            // UK extra key between left shift and Z
            // it prints \ and | with shift. KeyCode_Backslash will be where UK # is.
            keycode_lookup_table[i] = KeyCode_Ex0;
        }
    }
}

internal void
linux_keycode_init(Display* dpy){
    block_zero_array(keycode_lookup_table_physical);
    block_zero_array(keycode_lookup_table_language);
    
    linux_keycode_init_physical(dpy, keycode_lookup_table_physical);
    linux_keycode_init_language(dpy, keycode_lookup_table_language);
}

internal void
linux_epoll_init(void) {
    struct epoll_event e = {};
    e.events = EPOLLIN | EPOLLET;
    
    linuxvars.step_timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
    linuxvars.epoll = epoll_create(16);
    
    e.data.ptr = &epoll_tag_x11;
    epoll_ctl(linuxvars.epoll, EPOLL_CTL_ADD, ConnectionNumber(linuxvars.dpy), &e);
    
    e.data.ptr = &epoll_tag_step_timer;
    epoll_ctl(linuxvars.epoll, EPOLL_CTL_ADD, linuxvars.step_timer_fd, &e);
}

internal void
linux_clipboard_send(XSelectionRequestEvent* req) {
    
    XSelectionEvent rsp = {};
    rsp.type = SelectionNotify;
    rsp.requestor = req->requestor;
    rsp.selection = req->selection;
    rsp.target = req->target;
    rsp.time = req->time;
    rsp.property = None;
    
    Atom formats[] = {
        linuxvars.atom_UTF8_STRING,
        XA_STRING,
    };
    
    if(linuxvars.clipboard_contents.size == 0) {
        goto done;
    }
    
    if(req->selection != linuxvars.atom_CLIPBOARD || req->property == None) {
        goto done;
    }
    
    if (req->target == linuxvars.atom_TARGETS){
        
        XChangeProperty(
                        req->display,
                        req->requestor,
                        req->property,
                        XA_ATOM,
                        32,
                        PropModeReplace,
                        (u8*)formats,
                        ArrayCount(formats));
        
        rsp.property = req->property;
        
    } else {
        
        int i;
        for(i = 0; i < ArrayCount(formats); ++i){
            if (req->target == formats[i]){
                break;
            }
        }
        
        if (i != ArrayCount(formats)){
            XChangeProperty(
                            req->display,
                            req->requestor,
                            req->property,
                            req->target,
                            8,
                            PropModeReplace,
                            linuxvars.clipboard_contents.str,
                            linuxvars.clipboard_contents.size
                            );
            
            rsp.property = req->property;
        }
    }
    
    done:
    XSendEvent(req->display, req->requestor, True, 0, (XEvent*)&rsp);
}

internal String_Const_u8
linux_clipboard_recv(Arena *arena){
    Atom type;
    int fmt;
    unsigned long nitems;
    unsigned long bytes_left;
    u8 *data;
    
    int result = XGetWindowProperty(linuxvars.dpy,
                                    linuxvars.win,
                                    linuxvars.atom_CLIPBOARD,
                                    0L, 0x20000000L, False,
                                    linuxvars.atom_UTF8_STRING,
                                    &type, &fmt, &nitems,
                                    &bytes_left, &data);
    
    String_Const_u8 clip = {};
    if(result == Success && fmt == 8){
        clip= push_string_copy(arena, SCu8(data, nitems));
        XFree(data);
        XDeleteProperty(linuxvars.dpy, linuxvars.win, linuxvars.atom_CLIPBOARD);
    }
    
    return(clip);
}

internal void
linux_clipboard_recv(XSelectionEvent* ev) {
    
    if(ev->selection != linuxvars.atom_CLIPBOARD ||
       ev->target != linuxvars.atom_UTF8_STRING ||
       ev->property == None) {
        return;
    }
    
    Scratch_Block scratch(&linuxvars.tctx);
    String_Const_u8 clip = linux_clipboard_recv(scratch);
    if (clip.size > 0){
        linalloc_clear(&linuxvars.clipboard_arena);
        linuxvars.clipboard_contents = push_string_copy(&linuxvars.clipboard_arena, clip);
        linuxvars.received_new_clipboard = true;
        linux_schedule_step();
    }
}

internal
system_get_clipboard_sig(){
    // TODO(inso): index?
    return(push_string_copy(arena, linuxvars.clipboard_contents));
}

internal void
system_post_clipboard(String_Const_u8 str, i32 index){
    // TODO(inso): index?
    //LINUX_FN_DEBUG("%.*s", string_expand(str));
    linalloc_clear(&linuxvars.clipboard_arena);
    linuxvars.clipboard_contents = push_u8_stringf(&linuxvars.clipboard_arena, "%.*s", string_expand(str));
    XSetSelectionOwner(linuxvars.dpy, linuxvars.atom_CLIPBOARD, linuxvars.win, CurrentTime);
}

internal void
system_set_clipboard_catch_all(b32 enabled){
    LINUX_FN_DEBUG("%d", enabled);
    linuxvars.clipboard_catch_all = !!enabled;
}

internal b32
system_get_clipboard_catch_all(void){
    return linuxvars.clipboard_catch_all;
}

internal String_Const_u8
linux_filter_text(Arena* arena, u8* buf, int len) {
    u8* const result = push_array(arena, u8, len);
    u8* outp = result;
    
    for(int i = 0; i < len; ++i) {
        u8 c = buf[i];
        
        if(c == '\r') {
            *outp++ = '\n';
        } else if(c > 127 || (' ' <= c && c <= '~') || c == '\t') {
            *outp++ = c;
        }
    }
    
    return SCu8(result, outp - result);
}

internal KeyCode
linux_numlock_convert(KeyCode in){
    static const KeyCode lookup[] = {
        KeyCode_Insert,
        KeyCode_End,
        KeyCode_Down,
        KeyCode_PageDown,
        KeyCode_Left,
        0,
        KeyCode_Right,
        KeyCode_Home,
        KeyCode_Up,
        KeyCode_PageUp,
        0, 0, 0,
        KeyCode_Delete,
    };
    
    if(in >= KeyCode_NumPad0 && in <= KeyCode_NumPadDot) {
        KeyCode ret = lookup[in - KeyCode_NumPad0];
        if(ret != 0) {
            return ret;
        }
    }
    
    return in;
}

internal void
linux_handle_x11_events() {
    static XEvent prev_event = {};
    b32 should_step = false;
    
    while (XPending(linuxvars.dpy)) {
        XEvent event;
        XNextEvent(linuxvars.dpy, &event);
        
        b32 filtered = false;
        if (XFilterEvent(&event, None) == True){
            filtered = true;
            if(event.type != KeyPress && event.type != KeyRelease) {
                continue;
            }
        }
        
        u64 event_id = (u64)event.xkey.serial << 32 | event.xkey.time;
        
        switch(event.type) {
            case KeyPress: {
                should_step = true;
                
                Input_Modifier_Set_Fixed* mods = &linuxvars.input.pers.modifiers;
                
                int state = event.xkey.state;
                set_modifier(mods, KeyCode_Shift, state & ShiftMask);
                set_modifier(mods, KeyCode_Control, state & ControlMask);
                set_modifier(mods, KeyCode_CapsLock, state & LockMask);
                set_modifier(mods, KeyCode_Alt, state & Mod1Mask);
                
                event.xkey.state &= ~(ControlMask);
                
                Status status;
                KeySym keysym = NoSymbol;
                u8 buf[256] = {};
                
                int len = Xutf8LookupString(linuxvars.xic, &event.xkey, (char*)buf, sizeof(buf) - 1, &keysym, &status);
                
                if (status == XBufferOverflow){
                    Xutf8ResetIC(linuxvars.xic);
                    XSetICFocus(linuxvars.xic);
                }
                
                if (keysym == XK_ISO_Left_Tab){
                    add_modifier(mods, KeyCode_Shift);
                }
                
                Key_Code key;
                if(linuxvars.key_mode == KeyMode_Physical) {
                    key = keycode_lookup_table_physical[(u8)event.xkey.keycode];
                } else {
                    key = keycode_lookup_table_language[(u8)event.xkey.keycode];
                }
                
                if(!(state & Mod2Mask)) {
                    key = linux_numlock_convert(key);
                }
                
                //printf("key %d = %s (f:%d)\n", event.xkey.keycode, key_code_name[key], filtered);
                
                b32 is_dead = false;
                if (keysym >= XK_dead_grave && keysym <= XK_dead_greek && len == 0) {
                    is_dead = true;
                }
                
                if(!is_dead && filtered) {
                    linuxvars.prev_filtered_key = key;
                    break;
                }
                
                // send a keycode for the key after the dead key
                if(!key && linuxvars.prev_filtered_key) {
                    key = linuxvars.prev_filtered_key;
                    linuxvars.prev_filtered_key = 0;
                }
                
                Input_Event* key_event = NULL;
                if(key) {
                    add_modifier(mods, key);
                    // printf(" push key %d\n", key);
                    
                    key_event = push_input_event(&linuxvars.frame_arena, &linuxvars.input.trans.event_list);
                    key_event->kind = InputEventKind_KeyStroke;
                    key_event->key.code = key;
                    key_event->key.modifiers = copy_modifier_set(&linuxvars.frame_arena, mods);
                    key_event->key.flags = 0;
                    if (is_dead){
                        key_event->key.flags |= KeyFlag_IsDeadKey;
                    }
                }
                
                Input_Event* text_event = NULL;
                if(status == XLookupChars || status == XLookupBoth) {
                    String_Const_u8 str = linux_filter_text(&linuxvars.frame_arena, buf, len);
                    if(str.size) {
                        // printf(" push txt %d\n", key);
                        text_event = push_input_event(&linuxvars.frame_arena, &linuxvars.input.trans.event_list);
                        text_event->kind = InputEventKind_TextInsert;
                        text_event->text.string = str;
                    }
                }
                
                if(key_event && text_event) {
                    key_event->key.first_dependent_text = text_event;
                }
            } break;
            
            case KeyRelease: {
                should_step = true;
                
                Input_Modifier_Set_Fixed* mods = &linuxvars.input.pers.modifiers;
                
                int state = event.xkey.state;
                set_modifier(mods, KeyCode_Shift, state & ShiftMask);
                set_modifier(mods, KeyCode_Control, state & ControlMask);
                set_modifier(mods, KeyCode_CapsLock, state & LockMask);
                set_modifier(mods, KeyCode_Alt, state & Mod1Mask);
                
                Key_Code key;
                if(linuxvars.key_mode == KeyMode_Physical) {
                    key = keycode_lookup_table_physical[(u8)event.xkey.keycode];
                } else {
                    key = keycode_lookup_table_language[(u8)event.xkey.keycode];
                }
                
                // num lock off -> convert KP keys to Insert, Home, End etc.
                if(!(state & Mod2Mask)) {
                    key = linux_numlock_convert(key);
                }
                
                Input_Event* key_event = NULL;
                if(key) {
                    remove_modifier(mods, key);
                    key_event = push_input_event(&linuxvars.frame_arena, &linuxvars.input.trans.event_list);
                    key_event->kind = InputEventKind_KeyRelease;
                    key_event->key.code = key;
                    key_event->key.modifiers = copy_modifier_set(&linuxvars.frame_arena, mods);
                }
            } break;
            
            case MotionNotify: {
                int x = clamp(0, event.xmotion.x, render_target.width - 1);
                int y = clamp(0, event.xmotion.y, render_target.height - 1);
                linuxvars.input.pers.mouse = { x, y };
                should_step = true;
            } break;
            
            case ButtonPress: {
                should_step = true;
                switch(event.xbutton.button) {
                    case Button1: {
                        linuxvars.input.trans.mouse_l_press = true;
                        linuxvars.input.pers.mouse_l = true;
                        
                        // NOTE(inso): improves selection dragging (especially in notepad-like mode).
                        // we will still get mouse events when the pointer leaves the window if it's dragging.
                        XGrabPointer(
                                     linuxvars.dpy,
                                     linuxvars.win,
                                     True, PointerMotionMask | ButtonPressMask | ButtonReleaseMask,
                                     GrabModeAsync, GrabModeAsync,
                                     None, None, CurrentTime);
                        
                    } break;
                    
                    case Button3: {
                        linuxvars.input.trans.mouse_r_press = true;
                        linuxvars.input.pers.mouse_r = true;
                    } break;
                    
                    case Button4: {
                        linuxvars.input.trans.mouse_wheel = -100;
                    } break;
                    
                    case Button5: {
                        linuxvars.input.trans.mouse_wheel = +100;
                    } break;
                }
            } break;
            
            case ButtonRelease: {
                should_step = true;
                switch(event.xbutton.button) {
                    case Button1: {
                        linuxvars.input.trans.mouse_l_release = true;
                        linuxvars.input.pers.mouse_l = false;
                        
                        XUngrabPointer(linuxvars.dpy, CurrentTime);
                    } break;
                    
                    case Button3: {
                        linuxvars.input.trans.mouse_r_release = true;
                        linuxvars.input.pers.mouse_r = false;
                    } break;
                }
            } break;
            
            case FocusIn:
            case FocusOut: {
                linuxvars.input.pers.mouse_l = false;
                linuxvars.input.pers.mouse_r = false;
                block_zero_struct(&linuxvars.input.pers.modifiers);
            } break;
            
            case EnterNotify: {
                linuxvars.input.pers.mouse_out_of_window = 0;
            } break;
            
            case LeaveNotify: {
                linuxvars.input.pers.mouse_out_of_window = 1;
            } break;
            
            case ConfigureNotify: {
                i32 w = event.xconfigure.width;
                i32 h = event.xconfigure.height;
                
                if (w != render_target.width || h != render_target.height){
                    should_step = true;
                    render_target.width = w;
                    render_target.height = h;
                }
            } break;
            
            case ClientMessage: {
                Atom atom = event.xclient.data.l[0];
                
                // Window X button clicked
                if(atom == linuxvars.atom_WM_DELETE_WINDOW) {
                    should_step = true;
                    linuxvars.input.trans.trying_to_kill = true;
                }
                
                // Notify WM that we're still responding (don't grey our window out).
                else if(atom == linuxvars.atom__NET_WM_PING) {
                    event.xclient.window = DefaultRootWindow(linuxvars.dpy);
                    XSendEvent(linuxvars.dpy,
                               event.xclient.window,
                               False,
                               SubstructureRedirectMask | SubstructureNotifyMask,
                               &event);
                }
            } break;
            
            case SelectionRequest: {
                linux_clipboard_send((XSelectionRequestEvent*)&event);
            } break;
            
            case SelectionNotify: {
                linux_clipboard_recv((XSelectionEvent*)&event);
            } break;
            
            case SelectionClear: {
                if(event.xselectionclear.selection == linuxvars.atom_CLIPBOARD) {
                    linalloc_clear(&linuxvars.clipboard_arena);
                    block_zero_struct(&linuxvars.clipboard_contents);
                }
            } break;
            
            case Expose:
            case VisibilityNotify: {
                should_step = true;
            } break;
            
            default: {
                // clipboard update notification - ask for the new content
                if (event.type == linuxvars.xfixes_selection_event) {
                    XFixesSelectionNotifyEvent* sne = (XFixesSelectionNotifyEvent*)&event;
                    if (sne->subtype == XFixesSelectionNotify && sne->owner != linuxvars.win){
                        XConvertSelection(linuxvars.dpy,
                                          linuxvars.atom_CLIPBOARD,
                                          linuxvars.atom_UTF8_STRING,
                                          linuxvars.atom_CLIPBOARD,
                                          linuxvars.win,
                                          CurrentTime);
                    }
                }
                
                else if(event.type == linuxvars.xkb_event) {
                    XkbEvent* kb = (XkbEvent*)&event;
                    
                    // Keyboard layout changed, refresh lookup table.
                    if(kb->any.xkb_type == XkbStateNotify && kb->state.group != linuxvars.xkb_group) {
                        linuxvars.xkb_group = kb->state.group;
                        XkbRefreshKeyboardMapping((XkbMapNotifyEvent*)kb);
                        linux_keycode_init(linuxvars.dpy);
                    }
                }
            } break;
        }
    }
    
    if(should_step) {
        linux_schedule_step();
    }
}

internal b32
linux_epoll_process(struct epoll_event* events, int num_events) {
    b32 do_step = false;
    
    for (int i = 0; i < num_events; ++i){
        struct epoll_event* ev = events + i;
        Epoll_Kind* tag = (Epoll_Kind*)ev->data.ptr;
        
        switch (*tag){
            case EPOLL_X11: {
                linux_handle_x11_events();
            } break;
            
            case EPOLL_X11_INTERNAL: {
                //XProcessInternalConnection(linuxvars.dpy, fd);
            } break;
            
            case EPOLL_STEP_TIMER: {
                u64 count;
                int ret;
                do {
                    ret = read(linuxvars.step_timer_fd, &count, 8);
                } while (ret != -1 || errno != EAGAIN);
                do_step = true;
            } break;
            
            case EPOLL_CLI_PIPE: {
                linux_schedule_step();
            } break;
            
            case EPOLL_USER_TIMER: {
                Linux_Object* obj = CastFromMember(Linux_Object, timer.epoll_tag, tag);
                close(obj->timer.fd);
                obj->timer.fd = -1;
                linux_schedule_step();
            } break;
        }
    }
    
    return do_step;
}

int
main(int argc, char **argv){
    
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init(&linuxvars.memory_tracker_mutex, &attr);
    
    pthread_mutex_init(&linuxvars.audio_mutex, &attr);
    pthread_cond_init(&linuxvars.audio_cond, NULL);
    
    // NOTE(allen): context setup
    {
        Base_Allocator* alloc = get_base_allocator_system();
        thread_ctx_init(&linuxvars.tctx, ThreadKind_Main, alloc, alloc);
    }
    
    API_VTable_system system_vtable = {};
    system_api_fill_vtable(&system_vtable);
    
    API_VTable_graphics graphics_vtable = {};
    graphics_api_fill_vtable(&graphics_vtable);
    
    API_VTable_font font_vtable = {};
    font_api_fill_vtable(&font_vtable);
    
    // NOTE(allen): memory
    linuxvars.frame_arena = make_arena_system();
    linuxvars.clipboard_arena = make_arena_system();
    render_target.arena = make_arena_system(KB(256));
    
    //linuxvars.fontconfig = FcInitLoadConfigAndFonts();
    
    linuxvars.cursor_show = MouseCursorShow_Always;
    linuxvars.prev_cursor_show = MouseCursorShow_Always;
    
    dll_init_sentinel(&linuxvars.free_linux_objects);
    dll_init_sentinel(&linuxvars.timer_objects);
    
    //InitializeCriticalSection(&win32vars.thread_launch_mutex);
    //InitializeConditionVariable(&win32vars.thread_launch_cv);
    
    linuxvars.clipboard_catch_all = false;
    
    // NOTE(allen): load core
    System_Library core_library = {};
    App_Functions app = {};
    {
        App_Get_Functions *get_funcs = 0;
        Scratch_Block scratch(&linuxvars.tctx);
        List_String_Const_u8 search_list = {};
        def_search_list_add_system_path(scratch, &search_list, SystemPath_Binary);
        
        String_Const_u8 core_path = def_search_get_full_path(scratch, &search_list, SCu8("4ed_app.so"));
        if (system_load_library(scratch, core_path, &core_library)){
            get_funcs = (App_Get_Functions*)system_get_proc(core_library, "app_get_functions");
            if (get_funcs != 0){
                app = get_funcs();
            }
            else{
                char msg[] = "Failed to get application code from '4ed_app.so'.";
                system_error_box(msg);
            }
        }
        else{
            char msg[] = "Could not load '4ed_app.so'. This file should be in the same directory as the main '4ed' executable.";
            system_error_box(msg);
        }
    }
    
    // NOTE(allen): send system vtable to core
    app.load_vtables(&system_vtable, &font_vtable, &graphics_vtable);
    // get_logger calls log_init which is needed.
    app.get_logger();
    //linuxvars.log_string = app.get_logger();
    
    // NOTE(allen): init & command line parameters
    Plat_Settings plat_settings = {};
    void *base_ptr = 0;
    {
        Scratch_Block scratch(&linuxvars.tctx);
        String_Const_u8 curdir = system_get_path(scratch, SystemPath_CurrentDirectory);
        
        char **files = 0;
        i32 *file_count = 0;
        base_ptr = app.read_command_line(&linuxvars.tctx, curdir, &plat_settings, &files, &file_count, argc, argv);
        /* TODO(inso): what is this doing?
        {
            i32 end = *file_count;
            i32 i = 0, j = 0;
            for (; i < end; ++i){
                if (system_file_can_be_made(scratch, (u8*)files[i])){
                    files[j] = files[i];
                    ++j;
                }
            }
            *file_count = j;
        }*/
    }
    
    // NOTE(allen): setup user directory override
    if (plat_settings.user_directory != 0){
        lnx_override_user_directory = plat_settings.user_directory;
    }
    
    // NOTE(allen): load custom layer
    System_Library custom_library = {};
    Custom_API custom = {};
    {
        char custom_not_found_msg[] = "Did not find a library for the custom layer.";
        char custom_fail_load_msg[] = "Failed to load custom code due to missing version information.  Try rebuilding with buildsuper.";
        char custom_fail_version_msg[] = "Failed to load custom code due to a version mismatch.  Try rebuilding with buildsuper.";
        char custom_fail_init_apis[] = "Failed to load custom code due to missing 'init_apis' symbol.  Try rebuilding with buildsuper";
        
        Scratch_Block scratch(&linuxvars.tctx);
        String_Const_u8 default_file_name = string_u8_litexpr("custom_4coder.so");
        List_String_Const_u8 search_list = {};
        def_search_list_add_system_path(scratch, &search_list, SystemPath_CurrentDirectory);
        def_search_list_add_system_path(scratch, &search_list, SystemPath_UserDirectory);
        def_search_list_add_system_path(scratch, &search_list, SystemPath_Binary);
        String_Const_u8 custom_file_names[2] = {};
        i32 custom_file_count = 1;
        if (plat_settings.custom_dll != 0){
            custom_file_names[0] = SCu8(plat_settings.custom_dll);
            if (!plat_settings.custom_dll_is_strict){
                custom_file_names[1] = default_file_name;
                custom_file_count += 1;
            }
        }
        else{
            custom_file_names[0] = default_file_name;
        }
        String_Const_u8 custom_file_name = {};
        for (i32 i = 0; i < custom_file_count; i += 1){
            custom_file_name = def_search_get_full_path(scratch, &search_list, custom_file_names[i]);
            if (custom_file_name.size > 0){
                break;
            }
        }
        b32 has_library = false;
        if (custom_file_name.size > 0){
            if (system_load_library(scratch, custom_file_name, &custom_library)){
                has_library = true;
            }
        }
        
        if (!has_library){
            system_error_box(custom_not_found_msg);
        }
        custom.get_version = (_Get_Version_Type*)system_get_proc(custom_library, "get_version");
        if (custom.get_version == 0){
            system_error_box(custom_fail_load_msg);
        }
        else if (custom.get_version(MAJOR, MINOR, PATCH) == 0){
            system_error_box(custom_fail_version_msg);
        }
        custom.init_apis = (_Init_APIs_Type*)system_get_proc(custom_library, "init_apis");
        if (custom.init_apis == 0){
            system_error_box(custom_fail_init_apis);
        }
    }
    
    linux_x11_init(argc, argv, &plat_settings);
    linux_keycode_init(linuxvars.dpy);
    linux_epoll_init();
    
    linuxvars.audio_thread = system_thread_launch(&linux_audio_main, NULL);
    
    
    // app init
    {
        Scratch_Block scratch(&linuxvars.tctx);
        String_Const_u8 curdir = system_get_path(scratch, SystemPath_CurrentDirectory);
        app.init(&linuxvars.tctx, &render_target, base_ptr, curdir, custom);
    }
    
    linuxvars.global_frame_mutex = system_mutex_make();
    system_mutex_acquire(linuxvars.global_frame_mutex);
    
    linux_schedule_step();
    b32 first_step = true;
    u64 timer_start = system_now_time();
    
    for (;;) {
        
        if (XEventsQueued(linuxvars.dpy, QueuedAlready)){
            linux_handle_x11_events();
        }
        
        system_mutex_release(linuxvars.global_frame_mutex);
        
        struct epoll_event events[16];
        int num_events = epoll_wait(linuxvars.epoll, events, ArrayCount(events), -1);
        
        system_mutex_acquire(linuxvars.global_frame_mutex);
        
        if (num_events == -1){
            if (errno != EINTR){
                perror("epoll_wait");
                //LOG("epoll_wait\n");
            }
            continue;
        }
        
        if(!linux_epoll_process(events, num_events)) {
            continue;
        }
        
        linuxvars.last_step_time = system_now_time();
        
        // NOTE(allen): Frame Clipboard Input
        // Request clipboard contents from X11 on first step, or every step if they don't have XFixes notification ability.
        if (first_step || (!linuxvars.has_xfixes && linuxvars.clipboard_catch_all)){
            XConvertSelection(linuxvars.dpy, linuxvars.atom_CLIPBOARD, linuxvars.atom_UTF8_STRING, linuxvars.atom_CLIPBOARD, linuxvars.win, CurrentTime);
        }
        
        Application_Step_Input input = {};
        
        if (linuxvars.received_new_clipboard && linuxvars.clipboard_catch_all){
            input.clipboard = linuxvars.clipboard_contents;
        }
        linuxvars.received_new_clipboard = false;
        
        input.first_step = first_step;
        input.dt = frame_useconds/1000000.f; // variable?
        input.events = linuxvars.input.trans.event_list;
        input.trying_to_kill = linuxvars.input.trans.trying_to_kill;
        
        input.mouse.out_of_window = linuxvars.input.pers.mouse_out_of_window;
        input.mouse.p = linuxvars.input.pers.mouse;
        input.mouse.l = linuxvars.input.pers.mouse_l;
        input.mouse.r = linuxvars.input.pers.mouse_r;
        input.mouse.press_l = linuxvars.input.trans.mouse_l_press;
        input.mouse.release_l = linuxvars.input.trans.mouse_l_release;
        input.mouse.press_r = linuxvars.input.trans.mouse_r_press;
        input.mouse.release_r = linuxvars.input.trans.mouse_r_release;
        input.mouse.wheel = linuxvars.input.trans.mouse_wheel;
        
        // NOTE(allen): Application Core Update
        Application_Step_Result result = {};
        if (app.step != 0){
            result = app.step(&linuxvars.tctx, &render_target, base_ptr, &input);
        }
        
        // NOTE(allen): Finish the Loop
        if (result.perform_kill){
            break;
        }
        
        // NOTE(NAME): Switch to New Title
        if (result.has_new_title){
            XStoreName(linuxvars.dpy, linuxvars.win, result.title_string);
        }
        
        // NOTE(allen): Switch to New Cursor
        if (result.mouse_cursor_type != linuxvars.cursor && !linuxvars.input.pers.mouse_l){
            XCursor c = linuxvars.xcursors[result.mouse_cursor_type];
            if (linuxvars.cursor_show){
                XDefineCursor(linuxvars.dpy, linuxvars.win, c);
            }
            linuxvars.cursor = result.mouse_cursor_type;
        }
        
        gl_render(&render_target);
        glXSwapBuffers(linuxvars.dpy, linuxvars.win);
        
        // TODO(allen): don't let the screen size change until HERE after the render
        
        // NOTE(allen): Schedule a step if necessary
        if (result.animating){
            linux_schedule_step();
        }
        
        first_step = false;
        
        linalloc_clear(&linuxvars.frame_arena);
        block_zero_struct(&linuxvars.input.trans);
    }
    
    return 0;
}

// NOTE(inso): to prevent me continuously messing up indentation
// vim: et:ts=4:sts=4:sw=4
