module dlib.platform;

import dlib.aliases;

static if(NativeTarget)
{

import dlib.alloc : Reset;
import dlib.alloc;
import dlib.util;
import dlib.math;

import dlibincludes;

import std.typecons;
import std.stdio;
import core.memory;
import core.thread.osthread;
import core.time;
import core.volatile;
import core.atomic;

import core.stdc.stdio : FilePtr = FILE, FRead = fread;

const WINDOW_EDGE_BUFFER = 50;

enum Input : u32
{
	None, 
	Backspace   = 0x08,
	Tab         = 0x09,
	Enter       = 0x0A,
	Escape      = 0x1B,
	Space       = 0x20,
	Exclamation = 0x21,
	DoubleQuote = 0x22,
	Hash        = 0x23,
	Dollar      = 0x24,
	Percent     = 0x25,
	Ampersand   = 0x26,
	SingleQuote = 0x27,
	LeftParen   = 0x28,
	RightParent = 0x29,
	Asterisk    = 0x2A,
	Plus        = 0x2B,
	Comma       = 0x2C,
	Minus       = 0x2D,
	Period      = 0x2E,
	Slash       = 0x2F,
	Zero        = 0x30,
	One         = 0x31,
	Two         = 0x32,
	Three       = 0x33,
	Four        = 0x34,
	Five        = 0x35,
	Six         = 0x36,
	Seven       = 0x37,
	Eight       = 0x38,
	Nine        = 0x39,
	Colon       = 0x3A,
	Semicolon   = 0x3B,
	LessThan    = 0x3C,
	Equals      = 0x3D,
	GreaterThan = 0x3E,
	Question    = 0x3F,
	At          = 0x40,
	A = 0x41, B = 0x42,	C = 0x43,	D = 0x44,	E = 0x45,	F = 0x46,	G = 0x47,	H = 0x48,	I = 0x49,	J = 0x4A,	K = 0x4B,	L = 0x4C,	M = 0x4D,	
	N = 0x4E,	O = 0x4F, P = 0x50, Q = 0x51, R = 0x52, S = 0x53, T = 0x54, U = 0x55, V = 0x56, W = 0x57, X = 0x58, Y = 0x59, Z = 0x5A,
	LeftBracket = 0x5B,
	BackSlash    = 0x5C,
	RightBracket = 0x5D,
	Caret        = 0x5E,
	Underscore   = 0x5F,
	Grave        = 0x60,
	a = I.A+32, b = I.B+32, c = I.C+32, d = I.D+32, e = I.E+32, f = I.F+32, g = I.G+32, h = I.H+32, i = I.I+32, j = I.J+32, k = I.K+32, l = I.L+32, m = I.M+32,
	n = I.N+32, o = I.O+32, p = I.P+32, q = I.Q+32, r = I.R+32, s = I.S+32, t = I.T+32, u = I.U+32, v = I.V+32, w = I.W+32, x = I.X+32, y = I.Y+32, z = I.Z+32,
	LeftBrace   = 0x7B,
	VerticalBar = 0x7C,
	RightBrace  = 0x7D,
	Tilde       = 0x7E,
	Delete      = 0x7F,

	F1  = 0x150,
	F2  = 0x151,
	F3  = 0x152,
	F4  = 0x153,
	F5  = 0x154,
	F6  = 0x155,
	F7  = 0x156,
	F8  = 0x157,
	F9  = 0x158,
	F10 = 0x159,
	F11 = 0x15A,
	F12 = 0x15B,

	PrintScreen = 0x10C,
	ScrollLock  = 0x10D,
	Pause       = 0x10E,
	Insert      = 0x10F,
	Home        = 0x110,
	End         = 0x111,
	PageUp      = 0x112,
	PageDown    = 0x113,

	NumLock     = 0x114,
	NumEnter    = 0x10A,
	NumAsterisk = 0x12A,
	NumPlus     = 0x12B,
	NumMinus    = 0x12D,
	NumPeriod   = 0x12E,
	NumSlash    = 0x12F,
	NumZero     = 0x130,
	NumOne      = 0x131,
	NumTwo      = 0x132,
	NumThree    = 0x133,
	NumFour     = 0x134,
	NumFive     = 0x135,
	NumSix      = 0x136,
	NumSeven    = 0x137,
	NumEight    = 0x138,
	NumNine     = 0x139,

	LeftCtrl   = 0x13A,
	RightCtrl  = 0x13B,
	LeftShift  = 0x13C,
	RightShift = 0x13D,
	LeftSuper  = 0x13E,
	RightSuper = 0x13F,
	LeftAlt    = 0x140,
	RightAlt   = 0x141,
	CapsLock   = 0x142,

	Left  = 0x143,
	Right = 0x144,
	Up    = 0x145,
	Down  = 0x146,

	// Mouse
	MouseMotion = 0x147,
	LeftClick   = 0x148, 
	MiddleClick = 0x149, 
	RightClick  = 0x14A, 
	ScrollUp    = 0x14B, 
	ScrollDown  = 0x14C,
};

alias I = Input;

enum Modifier : u32
{
	None       = 0x00,
	LeftShift  = 0x01,
	RightShift = 0x02,
	LeftCtrl   = 0x04,
	RightCtrl  = 0x08,
	LeftAlt    = 0x10,
	RightAlt   = 0x20,
}

alias MD = Modifier;

struct InputEvent
{
	Input       key;
	Modifier    md;
	InputEvent* next, prev;
	string      text;
	bool       	pressed;
	i32         x;
	i32         y;
	i32         rel_x;
	i32         rel_y;
}

enum ClipboardMode
{
	Clipboard,
	Primary,
	Selection = Primary,
	Secondary,
	Max
}

alias CBM = ClipboardMode;

struct Inputs
{
	InputEvent* first, last;
	Arena       arena;
}

pragma(inline) bool
Shift(Modifier md)
{
	return cast(bool)(md & (MD.LeftShift | MD.RightShift));
}

pragma(inline) bool
Ctrl(Modifier md)
{
	return cast(bool)(md & (MD.LeftCtrl | MD.RightCtrl));
}

pragma(inline) bool
Alt(Modifier md)
{
	return cast(bool)(md & (MD.LeftAlt | MD.RightAlt));
}

Inputs*
GetEvents(PlatformWindow* window)
{
	Lock(&window.input_mutex);

	Inputs* inputs   = &window.inputs[window.input_idx];
	window.input_idx = (window.input_idx + 1) % 2;
	ResetInputs(&window.inputs[window.input_idx]);

	Unlock(&window.input_mutex);

	return inputs;
}

Inputs*
GetInputs(PlatformWindow* window)
{
	Lock(&window.input_mutex);

	Inputs* inputs = window.inputs.ptr + window.input_idx;

	return inputs;
}

void
ReturnInputs(PlatformWindow* window)
{
	Unlock(&window.input_mutex);
}

enum SysMessageType
{
	None,
	Quit,
	LockCursor,
	UnlockCursor,
	HideCursor,
	ShowCursor,
}

alias SMT = SysMessageType;

struct Mut
{
	bool locked;
}

struct TicketMut
{
	u64 ticket;
	u64 next_ticket;
}

Mut
CreateMut()
{
	return Mut(locked: false);
}

bool
TryLock(Mut* mut)
{
	return cas!(MemoryOrder.acq, MemoryOrder.raw)(&mut.locked, false, true);
}

void
Unlock(Mut* mut)
{
	atomicStore!(MemoryOrder.rel, bool)(mut.locked, false);
}

TicketMut
CreateTicketMut()
{
	return TicketMut(ticket: 0, next_ticket: 0);
}

void
Lock(TicketMut* mut)
{
	u64 ticket = atomicFetchAdd!(MemoryOrder.acq, u64)(mut.ticket, 1);
	while (ticket != volatileLoad(&mut.next_ticket)) {}
}

u64
Unlock(TicketMut* mut)
{
	return atomicFetchAdd!(MemoryOrder.rel, u64)(mut.next_ticket, 1);
}

__gshared const SysMessage  g_sys_message;
__gshared       SysMessage* g_NIL_MSG;

struct SysMessage
{
	SysMessageType type;
	SysMessage*    next, prev;
}

struct MessageQueue
{
	TicketMut               mut;
	Arena                   arena;
	SysMessage*             first, last;
	LinkedList!(SysMessage) free_list;
}

MessageQueue
CreateMessageQueue()
{
	if(g_NIL_MSG == null)
	{
		g_NIL_MSG = cast(SysMessage*)&g_sys_message;
	}

	MessageQueue queue = {
		first: g_NIL_MSG,
		last:  g_NIL_MSG,
		mut:   CreateTicketMut(),
		arena: CreateArena(MB(1)),
		free_list: {
			first: g_NIL_MSG,
		 	last: g_NIL_MSG,
		},
	};

	return queue;
}

pragma(inline) bool
Nil(SysMessage* msg)
{
	return msg == null || msg == g_NIL_MSG;
}

SysMessage*
Pop(MessageQueue* queue)
{
	Lock(&queue.mut);

	SysMessage* node = DLLPop(queue, g_NIL_MSG);

	Unlock(&queue.mut);

	return node;
}

void
Push(PlatformWindow* w, SysMessageType msg)
{
	Lock(&w.msg_queue.mut);
	
	SysMessage* node = g_NIL_MSG;
	if(Nil(w.msg_queue.free_list.first))
	{
		node = Alloc!(SysMessage)(&w.msg_queue.arena);
	}
	else
	{
		node = DLLPop(&w.msg_queue.free_list, g_NIL_MSG);
	}

	node.type = msg;
	DLLPush(&w.msg_queue, node, g_NIL_MSG);

	Unlock(&w.msg_queue.mut);
}

void
PushFree(PlatformWindow* w, SysMessage* node)
{
	DLLPush(&w.msg_queue, node, g_NIL_MSG);
}

version(linux)
{
import core.sys.posix.dlfcn;
import core.sys.posix.sys.mman;
import core.sys.linux.sys.inotify;
import core.sys.linux.fcntl;
import core.sys.posix.sys.time : timespec, timeval, gettimeofday;
import core.sys.posix.unistd;
import core.sys.posix.pthread : PThread              = pthread_t, 
			 													PThreadCond          = pthread_cond_t, 
																PThreadMutex         = pthread_mutex_t, 
																PThreadMutexInit     = pthread_mutex_init, 
																PThreadCondInit      = pthread_cond_init, 
																PThreadCreate        = pthread_create,
																PThreadMutexLock     = pthread_mutex_lock,
																PThreadCondWait      = pthread_cond_wait,
																PThreadMutexUnlock   = pthread_mutex_unlock,
																PThreadCondSignal    = pthread_cond_signal,
																PThreadExit          = pthread_exit,
																PThreadCondTimedWait = pthread_cond_timedwait;

import core.stdc.string : strlen;

const u32 X11_CB_TRANSFER_SIZE_DEFAULT = 1048576;
const u32 X11_TIMEOUT_DEFAULT = 1500;

struct SysThread
{
	PThread      handle;
	PThreadCond  cond;
	PThreadMutex mut;
}

struct Selection
{
	bool owned;
	u8[] data;
	Atom target;
	Atom xmode;
}

enum Atoms
{
	Targets,
	Multiple,
	Timestamp,
	Incr,
	Clipboard,
	Utf8String,
	WMProtocols,
	DeleteWindow,
	StateHidden,
	XselData,
	Max,
}

const char[][] ATOM_STRS = [
	Atoms.Targets:      CastStr!(char)("TARGETS"),
	Atoms.Multiple:     CastStr!(char)("MULTIPLE"),
	Atoms.Timestamp:    CastStr!(char)("TIMESTAMP"),
	Atoms.Incr:         CastStr!(char)("INCR"),
	Atoms.Clipboard:    CastStr!(char)("CLIPBOARD"),
	Atoms.Utf8String:   CastStr!(char)("UTF8_STRING"),
	Atoms.WMProtocols:  CastStr!(char)("WM_PROTOCOLS"),
	Atoms.DeleteWindow: CastStr!(char)("WM_DELETE_WINDOW"),
	Atoms.StateHidden:  CastStr!(char)("_NET_WM_STATE_HIDDEN"),
	Atoms.XselData:     CastStr!(char)("XSEL_DATA"),
];

alias PThreadProc = extern (C) void* function(void*);

auto
ExecLocation(T)()
{
	static char[512] buf;

	u64 size = readlink("/proc/self/exe", buf.ptr, buf.length);

	char[] str;
	for(u64 i = size-1; i64(i) >= 0; i -= 1)
	{
		if(buf[i] == '/')
		{
			str = buf[0 .. i];
			break;
		}
	}

	static if(is(T: u8) || is(T: char) || is(T: i8))
	{
		return (cast(T[])str);
	}
	else static if(is(T: string))
	{
		string s = (cast(immutable(char)*)str.ptr)[0 .. str.length];
		return s;
	}
	else static assert(false);
}

auto
Cwd(T = string)()
{
	static char[512] buf;

	getcwd(buf.ptr, buf.length);

	static if(is(T: u8) || is(T: i8) || is(T: char))
	{
		return (cast(T*)buf.ptr)[0 .. strlen(buf.ptr)];
	}
	else static if(is(T: string))
	{
		return ConvToStr(buf[0 .. strlen(buf.ptr)]);
	}
}

bool
ChDir(u8[] dir)
{
	char[512] buf;
	
	buf[0 .. dir.length] = (cast(char*)dir)[0 .. dir.length];
	buf[dir.length] = '\0';

	return chdir(buf.ptr) == 0;
}

SysThread
CreateThread(void* proc, void* param)
{
	SysThread thread;
	bool result;

	result  = cast(bool)(!PThreadMutexInit(&thread.mut, null));
	result &= cast(bool)(!PThreadCondInit(&thread.cond, null));
	result &= cast(bool)(!PThreadCreate(&thread.handle, null, cast(PThreadProc)proc, param));
	assert(result);

	return thread;
}

void
Suspend(SysThread* thread)
{
	PThreadMutexLock(&thread.mut);
	PThreadCondWait(&thread.cond, &thread.mut);
	PThreadMutexUnlock(&thread.mut);
}

void
Wake(SysThread* thread)
{
	PThreadCondSignal(&thread.cond);
}

void
Yield()
{
	version(linux)
	{
		import core.sys.posix.sched;
		sched_yield();
	}
}

void
Kill()
{
	PThreadExit(null);
}

void
ResetInputs(Inputs* inputs)
{
	inputs.first = inputs.last = null;
	Reset(&inputs.arena);
}

void
Push(Inputs* inputs, Input input, i32 x, i32 y, bool pressed, Modifier md, char[] text = [])
{
	InputEvent* node = Alloc!(InputEvent)(&inputs.arena);

	node.key     = input;
	node.pressed = pressed;
	node.x       = x;
	node.y       = y;
	node.md      = md;
	node.text    = text.length ? ConvToStr(Alloc!(char)(&inputs.arena, text)) : [];

	DLLPushFront(inputs, node, null);
}

void
PushMotion(Inputs* inputs, i32 rel_x, i32 rel_y, i32 x, i32 y)
{
	InputEvent* node = Alloc!(InputEvent)(&inputs.arena);
	node.key = Input.MouseMotion;
	node.rel_x = rel_x;
	node.rel_y = rel_y;
	node.x = x;
	node.y = y;
	node.pressed = false;

	DLLPushFront(inputs, node, null);
}

struct PlatformWindow
{
	Atom[Atoms.max]    atoms;
	Display*           display;
	Window             window;
	Window             root_window;
	i32                screen_id;
	u32								 w;
	u32								 h;
	i32								 mouse_prev_x;
	i32								 mouse_prev_y;
	bool							 locked_cursor;
	bool							 close;
	Modifier           modifier;

	SysThread          thread;
	MessageQueue       msg_queue;
	u32                input_idx;
	Inputs[2]					 inputs;
	TicketMut          input_mutex;

	Mut                cb_msg_mut;
	TicketMut          cb_mut;
	Selection[CBM.max] selections;
	version(linux) u32 cb_transfer_size;
	static if(__traits(compiles, { GLXContext context; }))
	{
		GLXContext       glx_context;
	}
};

struct Library
{
	void* ptr;
};

struct Function
{
	void* ptr;
};

__gshared string WINDOW_ERR_MSG = null;

string
WindowError()
{
	return WINDOW_ERR_MSG;
}

enum i32 GL_CORE_PROFILE = 0x00000001;

bool
CreateWindow(PlatformWindow* window, string name, u32 width, u32 height)
{
	PlatformWindow wnd = {
		w: width,
		h: height,
		input_mutex: CreateTicketMut(),
		msg_queue:   CreateMessageQueue(),
		cb_mut:      CreateTicketMut(),
		cb_msg_mut:  CreateMut(),
		inputs: [
			{ arena: CreateArena(MB(1)) },
			{ arena: CreateArena(MB(1)) },
		],
	};

	*window = wnd;

	window.cb_transfer_size = X11_CB_TRANSFER_SIZE_DEFAULT;
	
	window.display = XOpenDisplay(null);
	if(!window.display)
	{
		WINDOW_ERR_MSG = "Unable to open X11 display";
		return false;
	}

	window.root_window = DefaultRootWindow(window.display);
	if(window.root_window == None)
	{
		WINDOW_ERR_MSG = "Unable to retrieve X11 root window";
		return false;
	}
	
	window.screen_id = XDefaultScreen(window.display);

	const i64 value_mask = CWBackPixmap | CWBackPixel;
	const i64 event_mask = KeyPressMask        | 
											   KeyReleaseMask      | 
											   ExposureMask        | 
											   ButtonPressMask     | 
											   ButtonReleaseMask   |
									   	   PointerMotionMask   | 
									 		   StructureNotifyMask | 
											   PropertyChangeMask;

	window.window = XCreateSimpleWindow(
		window.display,
		window.root_window,
		0,
		0,
		width,
		height,
		0,
		XBlackPixel(window.display, window.screen_id),
		XBlackPixel(window.display, window.screen_id)
	);

	if(window.window == None)
	{
		WINDOW_ERR_MSG = "Failed to create X11 window";
		return false;
	}

	XSelectInput(window.display, window.window, event_mask);

	foreach(atom; Atoms.min .. Atoms.max)
	{
		window.atoms[atom] = XInternAtom(window.display, ATOM_STRS[atom].ptr, false);
	}

	XSetWMProtocols(window.display, window.window, window.atoms.ptr, window.atoms.length);

	window.selections[CBM.Clipboard].xmode = window.atoms[Atoms.Clipboard];
	window.selections[CBM.Primary].xmode   = XA_PRIMARY;
	window.selections[CBM.Secondary].xmode = XA_SECONDARY;

	XChangeProperty(
		window.display,
		window.window,
		XA_WM_NAME,
		XA_STRING,
		8,
		PropModeReplace,
		cast(const(u8)*)name.ptr,
		cast(u32)name.length
	);
	XStoreName(window.display, window.window, name.ptr);

	XChangeProperty(
		window.display,
		window.window,
		window.atoms[Atoms.WMProtocols],
		XA_ATOM,
		32,
		PropModeReplace,
		cast(const(u8)*)&window.atoms[Atoms.DeleteWindow],
		1
	);

	int major, minor;
	XFixesQueryVersion(window.display, &major, &minor);

	XMapWindow(window.display, window.window);

	XFlush(window.display);

	return true;
}

void
StartPlatformThread(PlatformWindow* window)
{
	window.thread = CreateThread(&HandleEvents, window);
}

bool
LockCursor(PlatformWindow* window)
{
	bool result = window.locked_cursor;

	if(!window.locked_cursor)
	{
		u32 counter = 0;
		for(;;)
		{
			i32 grab_res = XGrabPointer(window.display, window.window, true, 0, GrabModeAsync, GrabModeAsync, window.window, None, CurrentTime);

			if(grab_res == None)
			{
				result = true;
				break;
			}

			if(counter > 5)
			{
				break;
			}

			counter += 1;
			Thread.sleep(dur!("msecs")(50));
		}

		HideCursor(window);
		window.locked_cursor = result;
	}

	return result;
}

void
UnlockCursor(PlatformWindow* window)
{
	if(window.locked_cursor)
	{
		XUngrabPointer(window.display, CurrentTime);
		ShowCursor(window);
		window.locked_cursor = false;
	}
}

bool
HideCursor(PlatformWindow* window)
{
	XFixesHideCursor(window.display, window.window);
	return true;
}

bool
ShowCursor(PlatformWindow* window)
{
	XFixesShowCursor(window.display, window.window);
	return true;
}

void
ClearEvents(PlatformWindow* window)
{
	for(;;)
	{
		XEvent ev;
		u32 count = XPending(window.display);
		for(u32 i = 0; i < count; i += 1)
		{
			XNextEvent(window.display, &ev);
		}

		if(count == 0)
		{
			break;
		}
	}
}

bool
TransmitSelection(PlatformWindow* w, XSelectionRequestEvent* ev)
{
	bool result;
	if(ev.property == None)
	{
		ev.property = ev.target;
	}

	if(ev.target == w.atoms[Atoms.Targets])
	{
		Atom[3] targets = [
			w.atoms[Atoms.Timestamp],
			w.atoms[Atoms.Targets],
			w.atoms[Atoms.Utf8String]
		];

		XChangeProperty(
			w.display,
			ev.requestor,
			ev.property,
			XA_ATOM,
			Atom.sizeof * 8,
			PropModeReplace,
			cast(const(u8)*)targets.ptr,
			targets.length
		);

		result = true;
	}
	else if(ev.target == w.atoms[Atoms.Timestamp])
	{
		Time cur = CurrentTime;
		XChangeProperty(
			w.display,
			ev.requestor,
			ev.property,
			XA_INTEGER,
			cur.sizeof * 8,
			PropModeReplace,
			cast(const(u8)*)&cur,
			1
		);

		result = true;
	}
	else if(ev.target == w.atoms[Atoms.Utf8String])
	{
		Selection* sel = null;
		Lock(&w.cb_mut);

		foreach(i; CBM.min .. CBM.max)
		{
			if(w.selections[i].xmode == ev.selection)
			{
				sel = &w.selections[i];
				break;
			}
		}

		if(sel != null && sel.owned && sel.data.length > 0 && sel.target == ev.target)
		{
			XChangeProperty(
				w.display,
				ev.requestor,
				ev.property,
				ev.target,
				8,
				PropModeReplace,
				cast(const(u8)*)sel.data.ptr,
				cast(u32)sel.data.length
			);

			result = true;
		}

		Unlock(&w.cb_mut);
	}

	return result;
}

bool
ClipboardOwns(PlatformWindow* w, ClipboardMode mode)
{
	bool result;

	Lock(&w.cb_mut);

	result = w.selections[mode].owned;
	Unlock(&w.cb_mut);

	return result;
}

u8[]
ClipboardText(PlatformWindow* w, ClipboardMode mode)
{
	u8[] buf;

	Lock(&w.cb_mut);

	Selection* sel = &w.selections[mode];

	if(sel.owned)
	{
		buf = GetClipboardSelection(w, sel);
		Unlock(&w.cb_mut);
	}
	else
	{
		Free(sel.data);
		sel.data = [];

		u64 ticket = Unlock(&w.cb_mut)+1;

		sel.target = w.atoms[Atoms.Utf8String];
		XConvertSelection(w.display, sel.xmode, sel.target, sel.xmode, w.window, CurrentTime);
		XFlush(w.display);

		while(ticket == w.cb_mut.next_ticket) {}

		buf = GetClipboardSelection(w, sel);
	}

	return buf;
}

u8[]
ClipboardText(PlatformWindow* w)
{
	return ClipboardText(w, CBM.Clipboard);
}

bool
SetClipboard(PlatformWindow* w, u8[] data, ClipboardMode mode)
{
	bool result = true;

	if(data.length > 0)
	{
		Lock(&w.cb_mut);
		scope(exit) Unlock(&w.cb_mut);

		Selection* sel = &w.selections[mode];
		if(sel.data.length > 0)
		{
			Free(sel.data);
			sel.data = [];
		}

		sel.data = Alloc!(u8)(data.length+1);
		if(sel.data.length > 0)
		{
			MemCpy(sel.data.ptr, data.ptr, data.length);

			sel.data[sel.data.length-1] = '\0';
			sel.owned = true;
			sel.target = w.atoms[Atoms.Utf8String];

			XSetSelectionOwner(w.display, sel.xmode, w.window, CurrentTime);
			XFlush(w.display);
		}
		else
		{
			result = false;
		}
	}

	return result;
}

bool
SetClipboard(PlatformWindow* w, u8[] data)
{
	return SetClipboard(w, data, CBM.Clipboard);
}

u8[]
GetClipboardSelection(PlatformWindow* w, Selection* sel)
{
	u8[] buf;

	if(sel.data.length > 0 && sel.target == w.atoms[Atoms.Utf8String])
	{
		buf = ScratchAlloc(sel.data);
	}

	return buf;
}

void
RetrieveSelection(PlatformWindow* w, XSelectionEvent* ev)
{
	Lock(&w.cb_mut);
	scope(exit) Unlock(&w.cb_mut);

	u8[] buf;
	u64  buf_size;
	u64  bytes_after = 1;
	Atom actual_type;
	i32  actual_format;
	u64  nitems;
	u8*  prop_data;

	if(ev.property == XA_PRIMARY || ev.property == XA_SECONDARY || ev.property == w.atoms[Atoms.Clipboard])
	{
		while(bytes_after > 0)
		{
			scope(exit) XFree(prop_data);

			i32 get_res = XGetWindowProperty(
				w.display,
				w.window,
				ev.property,
				cast(u32)(buf_size/4), 
				cast(u32)(w.cb_transfer_size/4),
				true,
				AnyPropertyType,
				&actual_type,
				&actual_format,
				&nitems,
				&bytes_after,
				&prop_data
			);

			if(nitems > 0)
			{
				if(buf_size%4 != 0)
				{
					Errf("RetrieveSelection failure: Data size is not a multiple of 4");
					break;
				}

				u64 unit_size = actual_format/8;
				u64 data_len  = unit_size * nitems;
				buf = Realloc!(u8)(buf, data_len + buf_size);

				MemCpy(buf.ptr + buf_size, prop_data, data_len);
				buf_size += data_len;
			}
		}
	}

	if(buf != null)
	{
		Selection* sel;
		foreach(i; CBM.min .. CBM.max)
		{
			if(w.selections[i].xmode == ev.property)
			{
				sel = &w.selections[i];
				break;
			}
		}

		if(sel != null && sel.target == actual_type)
		{
			Free(sel.data);
			sel.data = buf;
			buf = [];
		} 
		else
		{
			Errf("RetrieveSelection failure: mismatched selection actual_type: %s", actual_type);
		}
	}
	else
	{
		Free(buf);
	}
}

void
ClearSelection(PlatformWindow* w, XSelectionClearEvent* ev)
{
	if(ev.window == w.window)
	{
		foreach(i; CBM.min .. CBM.max)
		{
			Selection* sel = &w.selections[i];
			if(sel.xmode == ev.selection)
			{
				Lock(&w.cb_mut);

				Free(sel.data);
				sel.data = [];
				sel.owned = false;
				sel.target = XCB_NONE;

				Unlock(&w.cb_mut);

				break;
			}
		}
	}
}

void
HandleEvents(void* window_ptr)
{
	PlatformWindow* w = cast(PlatformWindow*)window_ptr;

	SysMessage* sys_msg = g_NIL_MSG;
	XEvent e;

	bool ignore_mouse_events = false;
	
	u64 no_ev_count;
	for(;;)
	{
		if(w.close)
		{
			Kill();
		}

		sys_msg = Pop(&w.msg_queue);

		if(!Nil(sys_msg))
		{
			switch (sys_msg.type)
			{
				case SMT.Quit:
					{
						w.close = true;
					} break;
				case SMT.LockCursor:
					{
						for(u64 i = 0; i < 5; i += 1)
						{
							if(LockCursor(w))
							{
								break;
							}
						}
					} break;
				case SMT.UnlockCursor:
					{
						UnlockCursor(w);
					} break;
				case SMT.HideCursor:
					{
						for (u64 i = 0; i < 5; i += 1)
						{
							if(HideCursor(w))
							{
								break;
							}
						}
					} break;
				case SMT.ShowCursor:
					{
						for(u64 i = 0; i < 5; i += 1)
						{
							if(ShowCursor(w))
							{
								break;
							}
						}
					} break;
				default: break;
			}
		}

		if(XPending(w.display))
		{
			XNextEvent(w.display, &e);
			Inputs* inputs = GetInputs(w);

			switch (e.type)
			{
			case ClientMessage:
				{
					XClientMessageEvent* msg = &e.xclient;
					if(msg.window != w.window)
					{
						break;
					}

					if(msg.data.l[0] == w.atoms[Atoms.DeleteWindow])
					{
						w.close = true;
					}
				} break;
			case KeyRelease:
			case KeyPress:
				{
					XKeyEvent* kb_ev = &e.xkey;

					char[32] chars;
					i32 text_length = XLookupString(kb_ev, chars.ptr, chars.length, null, null);
					char[] text     = text_length ? chars[0 .. text_length] : [];
					bool pressed    = e.type == KeyPress;
					u32 code        = kb_ev.keycode;
					KeySym key_sym  = XkbKeycodeToKeysym(w.display, cast(KeyCode)code, 0, 0);
					Input input     = ConvertInput(key_sym);

					enum modifier_inputs = [Input.LeftShift, Input.RightShift, Input.LeftCtrl, Input.RightCtrl, Input.LeftAlt, Input.RightAlt];
					enum modifiers       = [MD.LeftShift, MD.RightShift, MD.LeftCtrl, MD.RightCtrl, MD.LeftAlt, MD.RightAlt];

					static foreach(i, md; modifier_inputs)
					{
						if(input == md)
						{
							w.modifier = cast(Modifier)(pressed ? (w.modifier | modifiers[i]) : (w.modifier & ~modifiers[i]));
						}
					}

					if(input != Input.None)
					{
						Push(inputs, input, kb_ev.x, kb_ev.y, pressed, w.modifier, text);
					}
				} break;
			case ButtonPress:
			case ButtonRelease:
				{
					XButtonEvent* mouse_event = &e.xbutton;

					bool pressed = e.type == ButtonPress;
					Input input  = Input.None;

					switch (mouse_event.button)
					{
						case Button1: input = Input.LeftClick;   break;
						case Button2: input = Input.MiddleClick; break;
						case Button3: input = Input.RightClick;  break;
						case Button4: input = Input.ScrollUp;    break;
						case Button5: input = Input.ScrollDown;  break;
						default: break;
					}

					if(input != Input.None)
					{
						Push(inputs, input, mouse_event.x, mouse_event.y, pressed, w.modifier);
					}					
				} break;
			case MotionNotify:
				{
					if(ignore_mouse_events) continue;

					XMotionEvent* move_event = &e.xmotion;

					i32 x = move_event.x;
					i32 y = move_event.y;

					static bool first = true;
					if(first)
					{
						w.mouse_prev_x = x;
						w.mouse_prev_y = y;
						first = false;
					}

					if(x > 0 || y > 0)
					{
						PushMotion(inputs, w.mouse_prev_x-x, w.mouse_prev_y-y, x, y);
					}

					w.mouse_prev_x = x;
					w.mouse_prev_y = y;

					if(w.locked_cursor && 
							(x < WINDOW_EDGE_BUFFER || y < WINDOW_EDGE_BUFFER || x > w.w - WINDOW_EDGE_BUFFER || y > w.h - WINDOW_EDGE_BUFFER))
					{
						i32 new_x = cast(i32)(w.w / 2);
						i32 new_y = cast(i32)(w.h / 2);
						XWarpPointer(w.display, w.window, w.window, 0, 0, cast(i16)w.w, cast(i16)w.h, new_x, new_y);
						w.mouse_prev_x = new_x;
						w.mouse_prev_y = new_y;
						ignore_mouse_events = true;
					}
				} break;
			case ConfigureNotify:
				{
					XConfigureEvent* config_event = &e.xconfigure;
					if(w.w != config_event.width || w.h != config_event.height)
					{
						w.w = config_event.width;
						w.h = config_event.height;
					}
				} break;
			case SelectionClear:
				{
					ClearSelection(w, &e.xselectionclear);
				} break;
			case SelectionNotify:
				{
					RetrieveSelection(w, &e.xselection);
				} break;
			case SelectionRequest:
				{
					auto req = &e.xselectionrequest;
					XEvent notify = {
						xselection:	{
							type:      SelectionNotify,
							time:      CurrentTime,
							display:   w.display,
							requestor: req.requestor,
							selection: req.selection,
							target:    req.target,
							property:  TransmitSelection(w, req) ? req.property : None,
						},
					};

					XSendEvent(w.display, req.requestor, false, PropertyChangeMask, &notify);
					XFlush(w.display);
				} break;
			default: break;
			}

			ReturnInputs(w);
		}
		else
		{
			no_ev_count += 1;
			if(no_ev_count >= 5)
			{
				no_ev_count = 0;
				Yield();
			}
		}
	} 
}

Library 
LoadLibrary(string name)
{
	Library lib = {
		ptr: null,
	};

	lib.ptr = dlopen(name.ptr, RTLD_NOW);
	
	return lib;
};

Function 
LoadFunction(Library lib, string name)
{
	Function fn = {
		ptr: null,
	};

	fn.ptr = dlsym(lib.ptr, name.ptr);

	return fn;
}

u8
InputToChar(Input input, bool modified = false)
{
	switch(input)
	{
		case Input.Tab:
		case Input.Enter:
		case Input.Space:
			{
				return cast(u8)(input);
			}
		case Input.LeftBracket:
		case Input.RightBracket:
		case Input.BackSlash:
		case Input.Minus:
		case Input.Equals:
		case Input.Grave:
		case Input.Semicolon:
		case Input.SingleQuote:
		case Input.Comma:
		case Input.Period:
		case Input.Slash:
		case Input.Zero: .. case Input.Nine:
			{
				u32 code = cast(u32)(0xFF & input);
				if(!modified)
				{
					return cast(u8)code;
				}

				switch(cast(Input)code)
				{
					case Input.Zero: return ')';
					case Input.One: return '!';
					case Input.Two: return '@';
					case Input.Three: return '#';
					case Input.Four: return '$';
					case Input.Five: return '%';
					case Input.Six: return '^';
					case Input.Seven: return '&';
					case Input.Eight: return '*';
					case Input.Nine: return '(';
					case Input.LeftBracket: return '{';
					case Input.RightBracket: return '}';
					case Input.BackSlash: return '|';
					case Input.Minus: return '_';
					case Input.Equals: return '+';
					case Input.Grave: return '~';
					case Input.Semicolon: return ':';
					case Input.SingleQuote: return '"';
					case Input.Comma: return '<';
					case Input.Period: return '>';
					case Input.Slash: return '?';
					default: return 0;
				}
			}
		case Input.a: .. case Input.z:
			{
				return !modified ? cast(u8)input : cast(u8)(input-32);
			}
		case Input.NumEnter:
		case Input.NumAsterisk:
		case Input.NumPlus:
		case Input.NumMinus:
		case Input.NumPeriod:
		case Input.NumSlash:
		case Input.NumZero: .. case Input.NumNine:
			{
				u32 code = cast(u32)(input & 0xFF);
				return cast(u8)(code);
			}
		default:
			{
				u32 code = cast(u32)input;
				return code >= 0x20 && code <= 0x7E ? cast(u8)code : 0;
			}
	}
}

Input
ConvertInput(u64 x_key)
{
	switch (x_key)
	{
		case XK_space: .. case XK_asciitilde:
			{
				return cast(Input)(x_key);
			}
		case XK_BackSpace: 		return Input.Backspace;
		case XK_Return: 			return Input.Enter;
		case XK_Tab: 					return Input.Tab;
		case XK_Pause:				return Input.Pause;
		case XK_Caps_Lock: 		return Input.CapsLock;
		case XK_Escape:				return Input.Escape;
		case XK_Prior:				return Input.PageUp;
		case XK_Next:					return Input.PageDown;
		case XK_End:					return Input.End;
		case XK_Home:					return Input.Home;
		case XK_Left:					return Input.Left;
		case XK_Up:						return Input.Up;
		case XK_Right:				return Input.Right;
		case XK_Down:					return Input.Down;
		case XK_Print:				return Input.PrintScreen;
		case XK_Insert:				return Input.Insert;
		case XK_Delete: 			return Input.Delete;
		case XK_Meta_L:
		case XK_Super_L:			return Input.LeftSuper;
		case XK_Meta_R:
		case XK_Super_R: 			return Input.RightSuper;
		case XK_KP_0:					return Input.NumZero;
		case XK_KP_1: 				return Input.NumOne;
		case XK_KP_2: 				return Input.NumTwo;
		case XK_KP_3: 				return Input.NumThree;
		case XK_KP_4: 				return Input.NumFour;
		case XK_KP_5: 				return Input.NumFive;
		case XK_KP_6: 				return Input.NumSix;
		case XK_KP_7: 				return Input.NumSeven;
		case XK_KP_8: 				return Input.NumEight;
		case XK_KP_9: 				return Input.NumNine;
		case XK_KP_Multiply:	return Input.NumAsterisk;
		case XK_KP_Subtract:	return Input.NumMinus;
		case XK_KP_Decimal:		return Input.NumPeriod;
		case XK_KP_Divide:		return Input.NumSlash;
		case XK_KP_Add:				return Input.NumPlus;
		case XK_F1:						return Input.F1;
		case XK_F2:						return Input.F2;
		case XK_F3:						return Input.F3;
		case XK_F4:						return Input.F4;
		case XK_F5:						return Input.F5;
		case XK_F6:						return Input.F6;
		case XK_F7:						return Input.F7;
		case XK_F8:						return Input.F8;
		case XK_F9:						return Input.F9;
		case XK_F10:					return Input.F10;
		case XK_F11:					return Input.F11;
		case XK_F12:					return Input.F12;
		case XK_Num_Lock:			return Input.NumLock;
		case XK_Scroll_Lock:	return Input.ScrollLock;
		case XK_Shift_L:			return Input.LeftShift;
		case XK_Shift_R: 			return Input.RightShift;
		case XK_Control_L:		return Input.LeftCtrl;
		case XK_Control_R:		return Input.RightCtrl;
		case XK_Alt_L: 				return Input.LeftAlt;
		case XK_Alt_R:				return Input.RightAlt;
		default: 							return Input.None;
	}
}

void*
MemAlloc(u64 size)
{
	return mmap(null, size, PROT_READ|PROT_WRITE, MAP_ANON|MAP_PRIVATE, -1, 0);
}

void
MemFree(void* ptr, u64 size)
{
	auto result = munmap(ptr, size);
	assert(result == 0, "MemFree failure");
}

struct Watcher
{
	Arena arena;
	u8[] buffer;
	WatcherH handle;
	WatchH dir_handle;
	u8[] watched_dir;
	bool blocking;
}

alias WatcherH = int;
alias WatchH = int;

enum WatchType 
{
	None,
	Access = IN_ACCESS,
	Metadata = IN_ATTRIB,
	Create = IN_CREATE,
	Delete = IN_DELETE,
	Modify = IN_MODIFY,
	Moved = IN_MOVED_FROM | IN_MOVED_TO,
}

alias WT = WatchType;

Watcher
WatchDirectory(string dir, WatchType type, bool blocking = false)
{
	assert(dir.length > 0);

	Watcher watcher = {
		arena: CreateArena(MB(4)),
		buffer: Alloc!(u8)(MB(1)),
		blocking: blocking,
		watched_dir: (cast(u8*)dir.ptr)[0 .. dir.length],
	};

	watcher.handle = inotify_init();
	assert(watcher.dir_handle >= 0, "WatchDirectory failure: unable to initialize");

	if(!blocking)
	{
		fcntl(watcher.handle, F_SETFL, fcntl(watcher.handle, F_GETFL) | O_NONBLOCK);
	}

	watcher.dir_handle = inotify_add_watch(watcher.handle, dir.ptr, type);

	return watcher;
}

WatchEvent[]
ViewChanges(Watcher* watcher)
{
	assert(watcher.handle >= 0 && watcher.dir_handle >= 0, "ViewChanges failure: handles are not valid");

	Reset(&watcher.arena);

	WatchEvent[] events;

	i64 length = read(watcher.handle, watcher.buffer.ptr, watcher.buffer.length);
	if(length > 0)
	{
		i64 count = 0;
		i64 i = 0;
		while(i < length)
		{
			inotify_event* event = (cast(inotify_event*)(watcher.buffer.ptr + i));
			count += 1;

			assert(event.wd == watcher.dir_handle);

			i += inotify_event.sizeof + event.len;
		}

		if(count > 0)
		{
			struct Moved
			{
				u32 cookie;
				i64 to;
				i64 from;
			}

			Moved[] moved = Alloc!(Moved)(&watcher.arena, (count/2)+1);
			i64 m_count = 0;
			events = Alloc!(WatchEvent)(&watcher.arena, count);
			count = 0;
			i = 0;
			while (i < length)
			{
				inotify_event* event = (cast(inotify_event*)(watcher.buffer.ptr + i));
				if(event.len > 0)
				{
					u8[] file_name = (cast(u8*)event.name)[0 .. strlen(event.name.ptr)];

					if(event.mask & IN_MOVED_FROM || event.mask & IN_MOVED_TO)
					{
						bool from = (event.mask & IN_MOVED_FROM) > 0;

						Moved* m;
						foreach(j; 0 .. m_count)
						{
							if(moved[j].cookie == event.cookie)
							{
								m = moved.ptr + j;
							}
						}

						if(m != null)
						{
							if(from && m.to >= 0)
							{
								events[m.to].names[0] = file_name;

								if(watcher.watched_dir == file_name)
								{
									events[m.to].type &= ~(WET.File | WET.Dir);
									events[m.to].type |= WET.Self;
								}
							}
							else if(!from && m.from >= 0)
							{
								events[m.from].names[1] = file_name;
							}
						}
						else 
						{
							WatchEvent* ev = events.ptr + count;
							ev.type = (event.mask & IN_ISDIR) ? WET.Dir : WET.File;
							ev.type |= WET.Moved;

							moved[m_count].cookie = event.cookie;

							if(from)
							{
								ev.names[0] = file_name;
								moved[m_count].from = count;
								moved[m_count].to = -1;

								if(watcher.watched_dir == file_name)
								{
									ev.type &= ~(WET.File | WET.Dir);
									ev.type |= WET.Self;
								}
							}
							else
							{
								ev.names[1] = file_name;
								moved[m_count].to = count;
								moved[m_count].from = -1;
							}

							count += 1;
							m_count += 1;
						}
					}
					else
					{
						WatchEvent* ev = events.ptr + count;
						ev.type = (event.mask & IN_ISDIR) ? WET.Dir : WET.File;

						ev.names[0] = file_name;
						if(ev.names[0] == watcher.watched_dir)
						{
							ev.type = WET.Self;
						}

						SetEventType(ev, event.mask);

						count += 1;
					}
				}
				else
				{
					WatchEvent* ev = events.ptr + count;
					ev.type = (event.mask & IN_ISDIR) ? WET.Dir : WET.File;

					SetEventType(ev, event.mask);

					count += 1;
				}

				i += inotify_event.sizeof + event.len;
			}
		}
	}

	return events;
}

void
SetEventType(WatchEvent* ev, u32 mask)
{
		if(mask & IN_ACCESS)
		{
			ev.type |= WET.Accessed;
		}
		else if(mask & IN_ATTRIB)
		{
			ev.type |= WET.Metadata;
		}
		else if(mask & IN_CREATE)
		{
			ev.type |= WET.Created;
		}
		else if(mask & IN_MODIFY)
		{
			ev.type |= WET.Modified;
		}
		else if(mask & IN_DELETE)
		{
			ev.type |= WET.Deleted;
		}
}

version(DLIB_TEST) unittest
{
	import std.stdio;
	import std.file : Remove = remove;

	Watcher fw = WatchDirectory("./", WT.Create | WT.Delete | WT.Moved | WT.Modify | WT.Access | WT.Metadata | WT.Access | WT.Metadata);

	auto f = File("test_file.txt", "wb");

	f.write("test");
	f.sync();
	f.close();

	Remove("./test_file.txt");

	WatchEvent[] events = ViewChanges(&fw);

	assert(events.length == 3);

	assert(events[0].type == WET.FileCreated);
	assert(events[0].names[0] == r"test_file.txt");

	assert(events[1].type == WET.FileModified);
	assert(events[1].names[0] == r"test_file.txt");

	assert(events[2].type == WET.FileDeleted);
	assert(events[2].names[0] == r"test_file.txt");
}

}

enum WatchEventType 
{
	None = 0x0000,
	File = 0x0001,
	Dir = 0x0002,
	Self = 0x0004,
	Created = 0x0008,
	Modified = 0x0010,
	Deleted = 0x0020,
	Moved = 0x0040,
	Metadata = 0x0080,
	Accessed = 0x0100,
	FileCreated = WET.File | WET.Created,
	FileModified = WET.File | WET.Modified,
	FileDeleted = WET.File | WET.Deleted,
	FileMoved = WET.File | WET.Moved,
	FileMetadata = WET.File | WET.Metadata,
	FileAccessed = WET.File | WET.Accessed,
	DirCreated = WET.Dir | WET.Created,
	DirModified = WET.Dir | WET.Modified,
	DirDeleted = WET.Dir | WET.Deleted,
	DirMoved = WET.Dir | WET.Moved,
	DirMetadata = WET.Dir | WET.Metadata,
	DirAccessed = WET.Dir | WET.Accessed,
	SelfCreated = WET.Self | WET.Created,
	SelfModified = WET.Self | WET.Modified,
	SelfDeleted = WET.Self | WET.Deleted,
	SelfMoved = WET.Self | WET.Moved,
	SelfMetadata = WET.Self | WET.Metadata,
	SelfAccessed = WET.Self | WET.Accessed,
}

alias WET = WatchEventType;

struct WatchEvent
{
	WatchEventType type;
	u8[][2]   names;
}

version(Windows)
{
import core.sys.windows.windows;

struct SysThread
{
	HANDLE handle;
}

alias Win32ThreadProc = extern (C) u32 function(void*);

SysThread
CreateThread(void* proc, void* param)
{
	SysThread thread;
	CreateThread(null, 0, cast(Win32ThreadProc)proc, param, 0, null);
	return thread;
}

void
Suspend(SysThread* thread)
{
	SuspendThread(thread.handle);
}

void
Wake(SysThread* thread)
{
	ResumeThread(thread.handle);
}

void
Kill()
{
	ExitThread(0);
}

}

// TODO: replace this with proper OS stuff
u8[]
LoadFile(Arena* arena, string file_name)
{
	u8[] data;

	const u64 buf_size = MB(8);
	static u8[] buf;
	if(!buf) 
	{
		buf = Alloc!(u8)(buf_size);
	}

	File f;
	try
	{
		f = File(file_name, "rb");
	}
	catch(Exception e)
	{
		
	}

	if(f.isOpen)
	{
		u64 size = f.size();
		data = Alloc!(u8)(arena, size);

		auto fp = f.getFP();
		for(u64 pos = 0; pos < size;)
		{
			u64 length = Min(size-pos, buf_size);
			fread(data.ptr+pos, 1, length, fp);
			pos -= length;
		}

		f.flush();
		f.close();
	}

	return data;
}

version(DLIB_TEST) unittest
{
	{ // Keys
		u8 ch = InputToChar(Input.NumEight);
		assert(ch == '8');
		ch = InputToChar(Input.b, true);
		assert(ch == 'B');

		version(linux)
		{
			ch = InputToChar(ConvertInput(XK_asciitilde));
			assert(ch == '~');
			ch = InputToChar(ConvertInput(XK_grave));
			assert(ch == '`');
		}
	}

	{
		string s = ExecLocation!(string)();
		u8[]   b = ExecLocation!(u8)();

		assert(ChDir(b));

		string wd = Cwd!(string)();

		assert(wd == s);
	}

	{ // File Reads
		Arena arena = CreateArena(MB(2));

		u8[] data_valid, data_test;

		File f = File("../dlib/platform.d", "rb");

		data_valid = f.rawRead(new u8[f.size()]);
		data_test  = LoadFile(&arena, "../dlib/platform.d");

		assert(data_valid == data_test);
	}
}


pragma(inline) u64
RDTSC()
{
	union u64_split
	{
		u64 full;
		struct 
		{
			u32 lower;
			u32 upper;
		};
	};

	u64_split val;

	u64_split* valp = &val;
	asm
	{
		cpuid;
		rdtsc;
		mov R8, valp;
		mov valp.upper.offsetof[R8], EDX;
		mov valp.lower.offsetof[R8], EAX;
	}

	return val.full;
}

pragma(inline) u64
OSTimeFreq()
{
	u64 freq;

	version (linux)
	{
		freq = 1000000;
	}
	else static assert(false, NO_IMPL);

	return freq;
}

pragma(inline) u64
OSTime()
{
	version(linux)
	{
		import core.sys.linux.sys.time;
		timeval value;
		gettimeofday(&value, null);

		u64 time = OSTimeFreq() * cast(u64)(value.tv_sec) + cast(u64)(value.tv_usec);
	}
	else static assert(false, NO_IMPL);

	return time;
}

}
