#include "platform.h"

#include <assert.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include "unistd.h"

#include "X11/Xlib.h"
#include "X11/extensions/XInput2.h"
#include "X11/keysymdef.h"
#include "GL/glx.h"
#include "signal.h"
#include "pulse/pulseaudio.h"
#include "semaphore.h"

#include "lib/queue.h"
#include "debug/logger.h"

#ifndef SWAP_INTERVAL
#define SWAP_INTERVAL -1
#endif

// Platform state
static Display     *p_display = NULL;
static Window       p_root_window;
static Window       p_window;
static GLXDrawable  p_glx_drawable;


static u32 p_width;
static u32 p_height;

static bool p_mouse_grabbed;
static bool p_window_has_focus;
static bool p_cursor_is_inside_window;
static void p_mouse_grab_internal(bool grab);
static Cursor p_empty_cursor;
static int p_xi2_opcode;
static XIM          p_xim;
static XIC          p_xic;


static QUEUE_TYPE(Event) p_event_queue;

static void linux_signal_handler(s32 signal)
{
	switch(signal)
	{
		case SIGINT:
		{
			Event e;
			e.type = EVENT_QUIT;
			if(Queue_Size(p_event_queue) >= Queue_Capacity(p_event_queue))
				LOG(LOG_ERROR, "Event queue full. Dropping oldest event after receiving signal %d", signal);
			Queue_Push(p_event_queue, e);
		} break;
		default:
		{
			LOG(LOG_WARNING, "Unmanaged linux signal received: %d", signal);
		}
	}
}


static pa_threaded_mainloop *p_audio_mainloop;
static pa_mainloop_api *p_audio_mainloop_api;
static pa_context *p_audio_context;
static sem_t p_audio_context_ready;
static pa_context_state_t p_audio_context_state;
static pa_stream *p_audio_stream;
static void *p_audio_data;
static p_audio_callback p_audio_cb;
static u32 _p_audio_sample_rate;

static void pa_context_cb(pa_context *c, void *userdata)
{
	p_audio_context_state = pa_context_get_state(c);
	s32 status = sem_post(&p_audio_context_ready);
	assert(status == 0);
}

static pa_context_state_t wait_pa_context_state()
{
	s32 status;
	status = sem_wait(&p_audio_context_ready);
	assert(status == 0);
	sem_destroy(&p_audio_context_ready);
	return p_audio_context_state;
}

static void p_audio_write_cb(pa_stream *stream, long unsigned success, void *userdata)
{
	s32 status;
	void *data;
	u64 n_bytes;
	status = pa_stream_begin_write(stream, &data, &n_bytes);
	if(status != 0 || data == NULL)
	{
		// Pulseadio allocation failed. Alloc our own memory
		n_bytes = 16 * 1024;
		if(!p_audio_data)
			p_audio_data = p_alloc(n_bytes);
		data = p_audio_data;
	}

	p_audio_buffer buffer;
	buffer.samples = (p_audio_sample*)data;
	buffer.size = n_bytes / sizeof(p_audio_sample);

	if(p_audio_cb)
		p_audio_cb(&buffer);

	n_bytes = buffer.size * sizeof(p_audio_sample);
	pa_stream_write(stream, data, n_bytes, NULL, 0, PA_SEEK_RELATIVE);
}

// Generic platform initialization
void p_init(bool capture_os_signals)
{
	p_width = 1280;
	p_height = 720;

	p_mouse_grabbed = false;
	p_window_has_focus = false;
	p_cursor_is_inside_window = false;

	// Events
	p_event_queue = Queue_Alloc(p_alloc, Event, 32);

	if(capture_os_signals)
	{
		struct sigaction sa;
		memset(&sa, 0, sizeof(struct sigaction));
		sa.sa_handler = linux_signal_handler;
		int status = sigaction(SIGINT, &sa, NULL);
		assert(status == 0);
	}

	// Audio
	{
		_p_audio_sample_rate = 44100;
		p_audio_data = NULL;
		p_audio_cb = NULL;

		s32 status;
		p_audio_mainloop = pa_threaded_mainloop_new();
		assert(p_audio_mainloop != NULL);
		status = pa_threaded_mainloop_start(p_audio_mainloop);
		assert(status == 0);

		pa_threaded_mainloop_lock(p_audio_mainloop);

		p_audio_mainloop_api = pa_threaded_mainloop_get_api(p_audio_mainloop);
		assert(p_audio_mainloop_api != NULL);

		p_audio_context = pa_context_new(p_audio_mainloop_api, "Piuma");
		assert(p_audio_context != NULL);
		status = pa_context_connect(p_audio_context, NULL, PA_CONTEXT_NOFLAGS, NULL);
		assert(status == 0);
		status = sem_init(&p_audio_context_ready, 0, 0);
		assert(status == 0);
		pa_context_set_state_callback(p_audio_context, pa_context_cb, NULL);
		pa_threaded_mainloop_unlock(p_audio_mainloop);
		pa_context_state_t context_state = PA_CONTEXT_UNCONNECTED;
		while(context_state == PA_CONTEXT_UNCONNECTED || context_state == PA_CONTEXT_CONNECTING || context_state == PA_CONTEXT_AUTHORIZING || context_state == PA_CONTEXT_SETTING_NAME)
		{
			context_state = wait_pa_context_state();
		}
		pa_threaded_mainloop_lock(p_audio_mainloop);
		assert(context_state == PA_CONTEXT_READY);

		pa_sample_spec ss;
		ss.format = PA_SAMPLE_FLOAT32LE;
		ss.rate = _p_audio_sample_rate;
		ss.channels = 2;
		p_audio_stream = pa_stream_new(p_audio_context, "Piuma audio", &ss, NULL);
		assert(p_audio_stream != NULL);

		pa_stream_set_write_callback(p_audio_stream, p_audio_write_cb, NULL);
		status = pa_stream_connect_playback(p_audio_stream, NULL, NULL, PA_STREAM_NOFLAGS, NULL, NULL);

		pa_threaded_mainloop_unlock(p_audio_mainloop);
	}
}

void p_deinit()
{
	Queue_Free(p_free, p_event_queue);

	// Audio
	pa_stream_disconnect(p_audio_stream);
	pa_context_disconnect(p_audio_context);

	pa_threaded_mainloop_stop(p_audio_mainloop);
	pa_threaded_mainloop_free(p_audio_mainloop);

	if(p_audio_data)
		p_free(p_audio_data);
}


// Memory
void * p_alloc(u64 size)
{
	return malloc(size);
}

void * p_realloc(void *ptr, u64 new_size)
{
	return realloc(ptr, new_size);
}

void   p_free(void *ptr)
{
	free(ptr);
}


// File IO
bool p_file_init(p_file *file, const char *filename, b32 flags)
{
	bool create_flag_set = flags & P_FILE_CREATE_IF_NOT_EXISTS;
	bool readonly = flags & P_FILE_READONLY;

	const char *mode = "rb+";
	if(readonly)
		mode = "rb";
	file->handle = fopen(filename, mode);
	if(create_flag_set && file->handle == NULL) // @Robustness: check errno
	{
		file->handle = fopen(filename, "wb+");
	}

	if(file->handle == NULL)
		return false;
	return true;
}

u64  p_file_size(p_file *file)
{
	assert(file->handle != NULL);

	int status;
	u64 saved_pos = ftell(file->handle);

	status = fseek(file->handle, 0, SEEK_END);
	assert(status == 0);
	u64 size = ftell(file->handle);

	// Restore saved state
	status = fseek(file->handle, saved_pos, SEEK_SET);
	assert(status == 0);

	return size;
}

bool p_file_read(p_file *file, Buffer *buf, u64 max_size)
{
	assert(file->handle != NULL);

	int status;
	u64 read;

	status = fseek(file->handle, 0, SEEK_SET);
	assert(status == 0);

	read = fread(buf->data, 1, max_size, file->handle);
	buf->size = read;

	if(feof(file->handle) || read == max_size)
	{
		// File completely read successfully
		return true;
	}
	return false;
}

bool p_file_write(p_file *file, u8 *data, u64 size)
{
	assert(file->handle != NULL);

	int status;
	u64 written;

	status = fseek(file->handle, 0, SEEK_SET);
	assert(status == 0);

	written = fwrite(data, 1, size, file->handle);
	if(written == size)
	{
		return true;
	}
	return false;
}

void p_file_deinit(p_file *file)
{
	assert(file->handle != NULL);

	int res = fclose(file->handle);
	assert(res == 0);
}


// Timers
f64  p_time() // Returns seconds
{
	f64 result;
	timespec ts;

	clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
	result = ((f64)ts.tv_sec) + ((f64)ts.tv_nsec * 1e-9);

	return result;
}

void p_wait(f64 milliseconds)
{
	int status;

	u64 useconds = milliseconds * 1000;
	status = usleep(useconds);

	assert(status == 0);
}



// Windowing
/*
Related documentation available at:
	- X11: https://x.org/releases/current/doc/libX11/libX11/libX11.html
	- GLX: https://khronos.org/registry/OpenGL/specs/gl/glx1.4.pdf

Before starting to code, I suggest you take a look to the X11 documentation first
and then take a look to at least the GLX intro.
When you need to use a function from the X11 docs, look in the GLX spec to see if
a replacement function is provided.
*/

static int p_fb_config_attributes[] =
{
	// GLX_BUFFER_SIZE, 32, // RGBA 8-bit each
	GLX_DOUBLEBUFFER, True,
	GLX_DEPTH_SIZE, 24,
	GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT | GLX_PIXMAP_BIT,
	GLX_X_RENDERABLE, True,
	GLX_X_VISUAL_TYPE, GLX_TRUE_COLOR,
	None
};

static int p_gl_attributes[] =
{
	GLX_CONTEXT_MAJOR_VERSION_ARB, 4,
	GLX_CONTEXT_MINOR_VERSION_ARB, 3,
	GLX_CONTEXT_PROFILE_MASK_ARB, GLX_CONTEXT_CORE_PROFILE_BIT_ARB,
	None
};

static u32 p_x11_event_mask = ExposureMask | FocusChangeMask |
                              //ResizeRedirectMask |
                              KeyPressMask | KeyReleaseMask |
                              ButtonPressMask | ButtonReleaseMask |
                              PointerMotionMask;

void p_window_open()
{
	int          p_default_screen;
	XVisualInfo *p_visual_info;
	Colormap     p_color_map;

	GLXFBConfig  p_fb_config;
	GLXWindow    p_glx_window;
	GLXContext   p_glx_context;

	/* Open a connection to X11 server*/
	{
		p_display = XOpenDisplay(NULL);
		assert(p_display != NULL);

		p_default_screen = XDefaultScreen(p_display);
		p_root_window    = XDefaultRootWindow(p_display);
	}


	/* Get a FBConfig with the right parameters */
	{
		int n_fb_config;
		GLXFBConfig *fb_config_list;

		fb_config_list = glXChooseFBConfig(p_display, p_default_screen, p_fb_config_attributes, &n_fb_config);
		assert(fb_config_list != NULL);
		p_fb_config = *fb_config_list; // Get first matching FBConfig

		XFree(fb_config_list);
	}

	/* Create a window */
	{
		// @Robustness: manage errors for CreateWindow and MapWindow

		// Visual info struct
		p_visual_info = glXGetVisualFromFBConfig(p_display, p_fb_config);
		assert(p_visual_info != NULL);

		p_color_map = XCreateColormap(p_display, p_root_window, p_visual_info->visual, AllocNone);

		// X11 Window struct
		XSetWindowAttributes win_attr;
		win_attr.colormap   = p_color_map;
		win_attr.event_mask = ExposureMask | FocusChangeMask |
		                      //ResizeRedirectMask |
		                      KeyPressMask | KeyReleaseMask |
		                      ButtonPressMask | ButtonReleaseMask |
		                      PointerMotionMask |
		                      EnterWindowMask | LeaveWindowMask;

		p_window = XCreateWindow(p_display, p_root_window, 10, 10, p_width, p_height, 0, p_visual_info->depth, InputOutput, p_visual_info->visual, CWColormap | CWEventMask, &win_attr);

		// GLX Window struct
		p_glx_window = glXCreateWindow(p_display, p_fb_config, p_window, NULL);

		XMapWindow(p_display, p_window);

		XFree(p_visual_info);

		p_window_has_focus = true;
	}

	// Create empty cursor for the times when mouse is grabbed
	{
		char data[1] = {0};
		Pixmap pixmap;
		XColor color;

		pixmap = XCreateBitmapFromData(p_display, p_window, data, 1, 1);
		color.red = color.green = color.blue = 0;

		p_empty_cursor = XCreatePixmapCursor(p_display, pixmap, pixmap, &color, &color, 0, 0);

		XFreePixmap(p_display, pixmap);
	}

	// Initialize XInput2
	{
		// http://who-t.blogspot.com/2009/05/xi2-recipes-part-1.html
		int event, error;
		int major, minor;

		if(!XQueryExtension(p_display, "XInputExtension", &p_xi2_opcode, &event, &error))
		{
			LOG(LOG_WARNING, "X Input extension not available.");
		}

		major = 2;
		minor = 2;
		if(XIQueryVersion(p_display, &major, &minor) == BadRequest)
		{
			LOG(LOG_WARNING, "XInput2 not available. Server supports %d.%d", major, minor);
		}
	}

	// XIM (X Input Method)
	{
		p_xim = XOpenIM(p_display, NULL, NULL, NULL);
		if(p_xim == NULL)
			LOG(LOG_ERROR, "Cannot open XIM (input method)");
		p_xic = XCreateIC(p_xim, XNInputStyle, (XIMPreeditNothing | XIMStatusNothing), XNClientWindow, p_window, NULL);
		if(p_xic == NULL)
			LOG(LOG_ERROR, "Cannot create XIC (input context)");

		XSetICFocus(p_xic);
		//void XUnsetICFocus(XIC ic);
		//char *XmbResetIC(XIC ic);
	}

	/* GLX extensions */

	/* OpenGL Context and Drawable*/
	{
		// @Robustness: Check GLX extension string to see if this function is available.
		p_glx_context = glXCreateContextAttribsARB(p_display, p_fb_config, NULL, GL_TRUE, p_gl_attributes);

		glXMakeCurrent(p_display, p_window, p_glx_context);

		p_glx_drawable = glXGetCurrentDrawable();
		assert(p_glx_drawable != None);
	}

	/* VSync */
	{
		// @Robustness: Check GLX extension string to see if this function is available.
		glXSwapIntervalEXT(p_display, p_glx_drawable, SWAP_INTERVAL);
	}

	XFlush(p_display);
}

void p_window_name(char *name)
{
	XStoreName(p_display, p_window, name);
	XFlush(p_display);
}

void p_window_resize(u32 width, u32 height, bool fullscreen)
{
	// @Feature: fullscreen support
	XResizeWindow(p_display, p_window, width, height);
	XFlush(p_display); // Does not resize without flush
}

// You shouldn't call this for each frame. Cache the data or just look at system events.
void p_window_dimensions(u32 *width, u32 *height)
{
	Window root;
	s32 x, y;
	u32 border_w, border_h;
	// Other parameters cannot be null
	XGetGeometry(p_display, p_glx_drawable, &root, &x, &y, &p_width, &p_height, &border_w, &border_h);
	*width  = p_width;
	*height = p_height;
}

void p_window_close()
{
	XDestroyIC(p_xic);
	XCloseIM(p_xim);
	XCloseDisplay(p_display);
	p_display = NULL;
}


// Graphics
void p_graphics_swap_interval(s32 frames)
{
	// -1 for Adaptive Sync, 0 for no interval, >0 to wait 'frames' frames between each swap
	glXSwapIntervalEXT(p_display, p_glx_drawable, frames);
}

void p_graphics_swap()
{
	glXSwapBuffers(p_display, p_window);
}


#define CASE_STRING(str) case str: return #str;
static const char * x11_event_type_str(s32 type)
{
	switch(type)
	{
		CASE_STRING(MotionNotify)
		CASE_STRING(ButtonPress)
		CASE_STRING(ButtonRelease)
		CASE_STRING(ColormapNotify)
		CASE_STRING(EnterNotify)
		CASE_STRING(LeaveNotify)
		CASE_STRING(Expose)
		CASE_STRING(GraphicsExpose)
		CASE_STRING(NoExpose)
		CASE_STRING(FocusIn)
		CASE_STRING(FocusOut)
		CASE_STRING(KeymapNotify)
		CASE_STRING(KeyPress)
		CASE_STRING(KeyRelease)
		CASE_STRING(PropertyNotify)
		CASE_STRING(ResizeRequest)
		CASE_STRING(CirculateNotify)
		CASE_STRING(ConfigureNotify)
		CASE_STRING(DestroyNotify)
		CASE_STRING(GravityNotify)
		CASE_STRING(MapNotify)
		CASE_STRING(ReparentNotify)
		CASE_STRING(UnmapNotify)
		CASE_STRING(CirculateRequest)
		CASE_STRING(ConfigureRequest)
		CASE_STRING(MapRequest)
		CASE_STRING(ClientMessage)
		CASE_STRING(MappingNotify)
		CASE_STRING(SelectionClear)
		CASE_STRING(SelectionNotify)
		CASE_STRING(SelectionRequest)
		CASE_STRING(VisibilityNotify)
		default: return "Unknown X11 type";
	}
}

static Key_Code map_x11_button(u32 button)
{
	switch(button)
	{
		case 1: return KEY_MOUSE_LEFT;
		case 2: return KEY_MOUSE_MIDDLE;
		case 3: return KEY_MOUSE_RIGHT;
		case 4: return KEY_MOUSE_WHEEL_UP;
		case 5: return KEY_MOUSE_WHEEL_DOWN;
		case 8: return KEY_MOUSE_4;
		case 9: return KEY_MOUSE_5;
	}

	return KEY_UNKNOWN;
}

static Key_Code map_x11_keycode(u32 keycode)
{
	//LOG(LOG_DEBUG, "Keycode %u", keycode);
	// @Performance: a lot of this codes are sequential. Check with other OSs if that's the case. If it is, we can have faster mapping by subtracting and adding an offset.
	KeySym keysym = XKeycodeToKeysym(p_display, keycode, 0);
	switch(keysym)
	{
		case XK_Return:     return KEY_ENTER;
		case XK_ISO_Enter:  return KEY_ENTER;
		case XK_Escape:     return KEY_ESCAPE;
		case XK_BackSpace:  return KEY_BACKSPACE;
		case XK_Tab:        return KEY_TAB;
		case XK_space:      return KEY_SPACE;
		case XK_exclam:     return KEY_EXCLAMATION;
		case XK_quotedbl:   return KEY_DOUBLE_QUOTE;
		case XK_numbersign: return KEY_HASH;
		case XK_percent:    return KEY_PERCENT;
		case XK_dollar:     return KEY_DOLLAR;
		case XK_ampersand:  return KEY_AMPERSAND;
		case XK_apostrophe: return KEY_SINGLE_QUOTE;
		case XK_parenleft:  return KEY_LEFT_PARENTHESIS;
		case XK_parenright: return KEY_RIGHT_PARENTHESIS;
		case XK_asterisk:   return KEY_ASTERISK;
		case XK_plus:       return KEY_PLUS;
		case XK_comma:      return KEY_COMMA;
		case XK_minus:      return KEY_MINUS;
		case XK_period:     return KEY_PERIOD;
		case XK_slash:      return KEY_SLASH;
		case XK_0: return KEY_0;
		case XK_1: return KEY_1;
		case XK_2: return KEY_2;
		case XK_3: return KEY_3;
		case XK_4: return KEY_4;
		case XK_5: return KEY_5;
		case XK_6: return KEY_6;
		case XK_7: return KEY_7;
		case XK_8: return KEY_8;
		case XK_9: return KEY_9;
		case XK_colon:      return KEY_COLON;
		case XK_semicolon:  return KEY_SEMICOLON;
		case XK_less:       return KEY_LESS;
		case XK_equal:      return KEY_EQUALS;
		case XK_greater:    return KEY_GREATER;
		case XK_question:   return KEY_QUESTION;
		case XK_at:         return KEY_AT;
		case XK_bracketleft:  return KEY_LEFT_BRACKET;
		case XK_bracketright: return KEY_RIGHT_BRACKET;
		case XK_backslash:  return KEY_BACKSLASH;
		case XK_asciicircum:  return KEY_CARET;
		case XK_underscore: return KEY_UNDERSCORE;
		case XK_grave:      return KEY_BACKQUOTE;
		case XK_a: return KEY_A;
		case XK_b: return KEY_B;
		case XK_c: return KEY_C;
		case XK_d: return KEY_D;
		case XK_e: return KEY_E;
		case XK_f: return KEY_F;
		case XK_g: return KEY_G;
		case XK_h: return KEY_H;
		case XK_i: return KEY_I;
		case XK_j: return KEY_J;
		case XK_k: return KEY_K;
		case XK_l: return KEY_L;
		case XK_m: return KEY_M;
		case XK_n: return KEY_N;
		case XK_o: return KEY_O;
		case XK_p: return KEY_P;
		case XK_q: return KEY_Q;
		case XK_r: return KEY_R;
		case XK_s: return KEY_S;
		case XK_t: return KEY_T;
		case XK_u: return KEY_U;
		case XK_v: return KEY_V;
		case XK_w: return KEY_W;
		case XK_x: return KEY_X;
		case XK_y: return KEY_Y;
		case XK_z: return KEY_Z;

		case XK_Caps_Lock: return KEY_CAPSLOCK;
		case XK_F1:  return KEY_F1;
		case XK_F2:  return KEY_F2;
		case XK_F3:  return KEY_F3;
		case XK_F4:  return KEY_F4;
		case XK_F5:  return KEY_F5;
		case XK_F6:  return KEY_F6;
		case XK_F7:  return KEY_F7;
		case XK_F8:  return KEY_F8;
		case XK_F9:  return KEY_F9;
		case XK_F10: return KEY_F10;
		case XK_F11: return KEY_F11;
		case XK_F12: return KEY_F12;

		case XK_Print:       return KEY_PRINTSCREEN;
		case XK_Scroll_Lock: return KEY_SCROLLLOCK;
		case XK_Pause:       return KEY_PAUSE;
		case XK_Insert:      return KEY_INSERT;
		case XK_Delete:      return KEY_DELETE;

		case XK_Home:        return KEY_HOME;
		case XK_End:         return KEY_END;
		case XK_Page_Up:     return KEY_PAGEUP;
		case XK_Page_Down:   return KEY_PAGEDOWN;

		case XK_Up:    return KEY_ARROW_UP;
		case XK_Down:  return KEY_ARROW_DOWN;
		case XK_Left:  return KEY_ARROW_LEFT;
		case XK_Right: return KEY_ARROW_RIGHT;

		case XK_Num_Lock:    return KEY_NUMLOCK;
		case XK_KP_Divide:   return KEY_PAD_DIVIDE;
		case XK_KP_Multiply: return KEY_PAD_MULTIPLY;
		case XK_KP_Subtract: return KEY_PAD_MINUS;
		case XK_KP_Add:      return KEY_PAD_PLUS;
		case XK_KP_Enter:    return KEY_PAD_ENTER;
		case XK_KP_1: return KEY_PAD_1;
		case XK_KP_2: return KEY_PAD_2;
		case XK_KP_3: return KEY_PAD_3;
		case XK_KP_4: return KEY_PAD_4;
		case XK_KP_5: return KEY_PAD_5;
		case XK_KP_6: return KEY_PAD_6;
		case XK_KP_7: return KEY_PAD_7;
		case XK_KP_8: return KEY_PAD_8;
		case XK_KP_9: return KEY_PAD_9;
		case XK_KP_0: return KEY_PAD_0;
		case XK_KP_Separator: return KEY_PAD_PERIOD;
		case XK_KP_End:       return KEY_PAD_1;
		case XK_KP_Down:      return KEY_PAD_2;
		case XK_KP_Page_Down: return KEY_PAD_3;
		case XK_KP_Left:      return KEY_PAD_4;
		case XK_KP_Begin:     return KEY_PAD_5;
		case XK_KP_Right:     return KEY_PAD_6;
		case XK_KP_Home:      return KEY_PAD_7;
		case XK_KP_Up:        return KEY_PAD_8;
		case XK_KP_Page_Up:   return KEY_PAD_9;
		case XK_KP_Insert:    return KEY_PAD_0;
		case XK_KP_Delete:    return KEY_PAD_PERIOD;

		case XK_Control_L: return KEY_LEFT_CTRL;
		case XK_Control_R: return KEY_RIGHT_CTRL;
		case XK_Shift_L:   return KEY_LEFT_SHIFT;
		case XK_Shift_R:   return KEY_RIGHT_SHIFT;
		case XK_Alt_L:     return KEY_LEFT_ALT;
		case XK_Alt_R:     return KEY_RIGHT_ALT;
		default: {
			LOG(LOG_WARNING, "Keycode not mapped: %u 0x%X - sym %u 0x%X", keycode, keycode, keysym, keysym);
			return KEY_UNKNOWN;
		}
	}
}

static void p_events_process()
{
	while(Queue_Size(p_event_queue) < Queue_Capacity(p_event_queue) && XEventsQueued(p_display, QueuedAfterReading))
	{
		XEvent e;
		XNextEvent(p_display, &e);

		//if(e.type != 6) LOG(LOG_DEBUG, "X11 event: %d - %s", e.type, x11_event_type_str(e.type));
		// @Feature: xinput2, gamepads
		switch(e.type)
		{
			case MotionNotify:
			{
				XMotionEvent m = e.xmotion;
				Event event;

				event.type = EVENT_MOUSE_MOVE;
				event.mouse_move.relative = false;
				event.mouse_move.position.x = m.x;
				event.mouse_move.position.y = m.y;

				Queue_Push(p_event_queue, event);

				// Do things
				bool is_grab_position = (m.x == p_width/2) && (m.y == p_height/2);
				if(p_mouse_grabbed && p_window_has_focus && !is_grab_position)
					XWarpPointer(p_display, None, p_window, 0, 0, 0, 0, p_width / 2, p_height / 2);
			} break;

			case GenericEvent:
			{
				bool is_xi2_cookie = (e.xcookie.extension == p_xi2_opcode);
				if(is_xi2_cookie)
				{
					if(XGetEventData(p_display, &e.xcookie))
					{
						static int a = 0;
						switch(e.xcookie.evtype)
						{
							case XI_RawMotion:
							{
								if(p_mouse_grabbed && p_window_has_focus)
								{
									XIRawEvent *re = (XIRawEvent *)e.xcookie.data;
									if((1 < re->valuators.mask_len * 8) && XIMaskIsSet(re->valuators.mask, 0) && XIMaskIsSet(re->valuators.mask, 1))
									{
										Event event;

										event.type = EVENT_MOUSE_MOVE;
										event.mouse_move.relative = true;
										event.mouse_move.position.x = re->raw_values[0];
										event.mouse_move.position.y = re->raw_values[1];

										Queue_Push(p_event_queue, event);
									}
								}
							} break;
							case XI_RawButtonPress:
							{
							} break;
							case XI_RawButtonRelease:
							{
							} break;
						}
					}
					XFreeEventData(p_display, &e.xcookie);
				}
			} break;



			case ButtonPress:
			{
				XButtonEvent b = e.xbutton;

				Event event;

				event.type = EVENT_KEY;
				event.key.pressed = true;
				event.key.key_code = map_x11_button(b.button);

				Queue_Push(p_event_queue, event);
			} break;
			case ButtonRelease:
			{
				XButtonEvent b = e.xbutton;
				Event event;

				event.type = EVENT_KEY;
				event.key.pressed = false;
				event.key.key_code = map_x11_button(b.button);

				Queue_Push(p_event_queue, event);
			} break;


			case KeyPress:
			{
				XKeyEvent k = e.xkey;

				Event event;

				event.type = EVENT_KEY;
				event.key.pressed = true;
				event.key.key_code = map_x11_keycode(k.keycode);

				Queue_Push(p_event_queue, event);

				if(!XFilterEvent(&e, p_window))
				{
					char text[64];
					KeySym keysym;
					static Status status = 0;
					int text_length = Xutf8LookupString(p_xic, &k, text, 64, &keysym, &status);
					//LOG(LOG_DEBUG, "X11 UTF8 lookup (length %d): %.*s, (%u)", text_length, text_length, text, text[0]);
					if(text_length > 0)
					{
						Event event;

						event.type = EVENT_TEXT;
						memcpy(event.text.data, text, text_length);
						event.text.data[text_length] = '\0';

						if(event.text.data[0] == '\r')
							event.text.data[0] = '\n';

						Queue_Push(p_event_queue, event);
					}
				}
			} break;
			case KeyRelease:
			{
				XKeyEvent k = e.xkey;

				// Check for repeated keypress
				if(XEventsQueued(p_display, QueuedAfterReading))
				{
					XEvent next_e;
					XPeekEvent(p_display, &next_e);
					if(next_e.type == KeyPress && next_e.xkey.time == k.time && next_e.xkey.keycode == k.keycode)
					{
						// Repeated keypress. Key wasn't actually released.
						// It's the thing that the OS does when keep a key pressed and it prints multiple characters.
						// Repeated keypresses are sent as a KeyRelease, immediatly followed by a KeyPress with the same time.
						// So we eat both this KeyRelease and the next KeyPress
						XNextEvent(p_display, &e);

						// We still use this event for text input
						if(!XFilterEvent(&e, p_window))
						{
							XKeyEvent k = e.xkey;
							char text[64];
							KeySym keysym;
							static Status status = 0;
							int text_length = Xutf8LookupString(p_xic, &k, text, 64, &keysym, &status);
							//LOG(LOG_DEBUG, "X11 UTF8 lookup (length %d): %.*s", text_length, text_length, text);
							if(text_length > 0)
							{
								Event event;

								event.type = EVENT_TEXT;
								memcpy(event.text.data, text, text_length);
								event.text.data[text_length] = '\0';

								if(event.text.data[0] == '\r')
									event.text.data[0] = '\n';

								Queue_Push(p_event_queue, event);
							}
						}

						break;
					}
				}

				Event event;

				event.type = EVENT_KEY;
				event.key.pressed = false;
				event.key.key_code = map_x11_keycode(k.keycode);

				Queue_Push(p_event_queue, event);
			} break;



			case FocusIn:
			{
				//XFocusChangeEvent fc = e.xfocus;
				Event event;
				event.type = EVENT_FOCUS;
				Queue_Push(p_event_queue, event);

				// Grab pointer if it was grabbed before (before leaving the window, now we are coming back to it
				p_window_has_focus = true;
				if(p_mouse_grabbed && p_cursor_is_inside_window)
					p_mouse_grab_internal(p_mouse_grabbed);
			} break;
			case FocusOut:
			{
				//XFocusChangeEvent fc = e.xfocus;
				Event event;
				event.type = EVENT_UNFOCUS;
				Queue_Push(p_event_queue, event);

				// Ungrab pointer when leaving the window (minimize, alt+tab, ...)
				p_window_has_focus = false;
				if(p_mouse_grabbed)
					p_mouse_grab_internal(false);
			} break;

			case EnterNotify:
			{
				// Add to event queue -> no
				// Grab pointer if needed
				p_cursor_is_inside_window = true;
				if(p_mouse_grabbed && p_window_has_focus)
					p_mouse_grab_internal(p_mouse_grabbed);
			} break;
			case LeaveNotify:
			{
				// Add to event queue -> no
				// Ungrab pointer if needed
				p_cursor_is_inside_window = false;
				if(p_mouse_grabbed && p_window_has_focus)
					p_mouse_grab_internal(false);
			} break;


			case Expose:
			{
				XExposeEvent ex = e.xexpose;

				Event event;
				event.type = EVENT_RESIZE;
				u32 new_width  = ex.x + ex.width;
				u32 new_height = ex.y + ex.height;
				event.resize.width  = new_width;
				event.resize.height = new_height;
				p_width  = new_width;
				p_height = new_height;
				//LOG(LOG_DEBUG, "Expose event - x: %d  y: %d        width: %d  height: %d        count: %d", ex.x, ex.y, ex.width, ex.height, ex.count);

				Queue_Push(p_event_queue, event);
			} break;



			default:
				LOG(LOG_DEBUG, "Unrecognized X11 event: %d - %s", e.type, x11_event_type_str(e.type));
				// Discard unrecognized event
		}
	}
}


// Input
static void p_mouse_grab_internal(bool grab)
{
	if(grab)
	{
		// Set event mask
		XIEventMask xi2_event_mask;
		unsigned char mask[4] = { 0, 0, 0, 0 };

		xi2_event_mask.deviceid = XIAllMasterDevices;
		xi2_event_mask.mask_len = sizeof(mask);
		xi2_event_mask.mask = mask;
		XISetMask(mask, XI_RawMotion);
		XISetMask(mask, XI_RawButtonPress);
		XISetMask(mask, XI_RawButtonRelease);

		XISelectEvents(p_display, p_root_window, &xi2_event_mask, 1);

		// Cursor grabbing and hiding
		#if 1 // In XInput 2.0 we will not get raw events if we grab the pointer. In XInput 2.2 we get raw events even when we grab the pointer, as expected.
		unsigned event_mask = ButtonPressMask | ButtonReleaseMask | PointerMotionMask | FocusChangeMask | EnterWindowMask | LeaveWindowMask;

		XGrabPointer(p_display, p_window, False, event_mask, GrabModeAsync, GrabModeAsync, None, None, CurrentTime);
		#endif


		XDefineCursor(p_display, p_window, p_empty_cursor);

		XWarpPointer(p_display, None, p_window, 0, 0, 0, 0, p_width/2, p_height/2);
	}
	else
	{
		// Clear event mask
		XIEventMask event_mask;
		unsigned char mask[1] = { 0 };

		event_mask.deviceid = XIAllMasterDevices;
		event_mask.mask_len = 0;
		event_mask.mask = mask;

		XISelectEvents(p_display, p_root_window, &event_mask, 1);

		// Undo Cursor grabbing and hiding
		XUngrabPointer(p_display, CurrentTime);
		XDefineCursor(p_display, p_window, None);
	}
}

void p_mouse_grab(bool grab)
{
	if(grab != p_mouse_grabbed)
	{
		p_mouse_grabbed = grab;
		p_mouse_grab_internal(grab);
	}
}

bool p_next_event(Event *e)
{
	// If not in cache, process events until you find one
	if(Queue_Size(p_event_queue) == 0)
	{
		p_events_process();
	}

	// If an event was found, return it
	if(Queue_Size(p_event_queue) > 0)
	{
		*e = Queue_Pop(p_event_queue);
		return true;
	}

	return false;
}


// Audio
void p_audio_register_data_callback(p_audio_callback cb)
{
	p_audio_cb = cb;
}

u32 p_audio_sample_rate()
{
	return _p_audio_sample_rate;
}
