#define GMMODULE
#include "GarrysMod/Lua/Interface.h"

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <sys/wait.h>
#include <unistd.h>
#include <dlfcn.h>

#if defined(__x86_64__)
__asm__(".symver pthread_create,pthread_create@GLIBC_2.2.5");
__asm__(".symver dlopen,dlopen@GLIBC_2.2.5");
__asm__(".symver dlsym,dlsym@GLIBC_2.2.5");
__asm__(".symver dlclose,dlclose@GLIBC_2.2.5");
#elif defined(__i386__)
__asm__(".symver pthread_create,pthread_create@GLIBC_2.1");
__asm__(".symver dlopen,dlopen@GLIBC_2.1");
__asm__(".symver dlsym,dlsym@GLIBC_2.0");
__asm__(".symver dlclose,dlclose@GLIBC_2.0");
#endif
#include <time.h>
#include <pthread.h>
#include <poll.h>

#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/extensions/Xfixes.h>

static const size_t MAX_IMAGE_SIZE = 32u * 1024u * 1024u;

struct Buf {
	char* data;
	size_t size;
	size_t cap;
	Buf() : data(nullptr), size(0), cap(0) {}
	~Buf() { free(data); }
	bool append(const char* p, size_t n) {
		if (size + n > MAX_IMAGE_SIZE) return false;
		if (size + n > cap) {
			size_t ncap = cap ? cap * 2 : 65536;
			while (ncap < size + n) ncap *= 2;
			char* nd = (char*)realloc(data, ncap);
			if (!nd) return false;
			data = nd; cap = ncap;
		}
		memcpy(data + size, p, n);
		size += n;
		return true;
	}
	void clear() { size = 0; }
};

static bool looks_like_png(const Buf& b)
{
	static const unsigned char magic[8] = { 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A };
	return b.size > 8 && memcmp(b.data, magic, 8) == 0;
}

typedef Display*    (*fn_XOpenDisplay)(const char*);
typedef int         (*fn_XCloseDisplay)(Display*);
typedef Window      (*fn_XDefaultRootWindow)(Display*);
typedef Window      (*fn_XCreateSimpleWindow)(Display*, Window, int, int,
                        unsigned int, unsigned int, unsigned int,
                        unsigned long, unsigned long);
typedef int         (*fn_XDestroyWindow)(Display*, Window);
typedef Atom        (*fn_XInternAtom)(Display*, const char*, Bool);
typedef int         (*fn_XConvertSelection)(Display*, Atom, Atom, Atom, Window, Time);
typedef int         (*fn_XSelectInput)(Display*, Window, long);
typedef int         (*fn_XPending)(Display*);
typedef int         (*fn_XNextEvent)(Display*, XEvent*);
typedef int         (*fn_XGetWindowProperty)(Display*, Window, Atom, long, long,
                        Bool, Atom, Atom*, int*, unsigned long*, unsigned long*,
                        unsigned char**);
typedef int         (*fn_XDeleteProperty)(Display*, Window, Atom);
typedef int         (*fn_XFree)(void*);
typedef int         (*fn_XFlush)(Display*);
typedef Window      (*fn_XGetSelectionOwner)(Display*, Atom);
typedef int         (*fn_XSync)(Display*, Bool);
typedef XErrorHandler (*fn_XSetErrorHandler)(XErrorHandler);
typedef Status      (*fn_XSendEvent)(Display*, Window, Bool, long, XEvent*);
typedef int         (*fn_XSetSelectionOwner)(Display*, Atom, Window, Time);
typedef int         (*fn_XChangeProperty)(Display*, Window, Atom, Atom, int, int,
                        const unsigned char*, int);
typedef int         (*fn_XConnectionNumber)(Display*);

static struct {
	void* lib;
	fn_XOpenDisplay        XOpenDisplay;
	fn_XCloseDisplay       XCloseDisplay;
	fn_XDefaultRootWindow  XDefaultRootWindow;
	fn_XCreateSimpleWindow XCreateSimpleWindow;
	fn_XDestroyWindow      XDestroyWindow;
	fn_XInternAtom         XInternAtom;
	fn_XConvertSelection   XConvertSelection;
	fn_XSelectInput        XSelectInput;
	fn_XPending            XPending;
	fn_XNextEvent          XNextEvent;
	fn_XGetWindowProperty  XGetWindowProperty;
	fn_XDeleteProperty     XDeleteProperty;
	fn_XFree               XFree;
	fn_XFlush              XFlush;
	fn_XGetSelectionOwner  XGetSelectionOwner;
	fn_XSync               XSync;
	fn_XSetErrorHandler    XSetErrorHandler;
	fn_XSendEvent          XSendEvent;
	fn_XSetSelectionOwner  XSetSelectionOwner;
	fn_XChangeProperty     XChangeProperty;
	fn_XConnectionNumber   XConnectionNumber;
} X = {};

static void install_error_handler_once();

static bool load_xlib()
{
	if (X.lib) return true;

	void* lib = dlopen("libX11.so.6", RTLD_LAZY | RTLD_LOCAL);
	if (!lib) lib = dlopen("libX11.so", RTLD_LAZY | RTLD_LOCAL);
	if (!lib) return false;

	#define XSYM(name) \
		X.name = (fn_##name)dlsym(lib, #name); \
		if (!X.name) { dlclose(lib); return false; }

	XSYM(XOpenDisplay)
	XSYM(XCloseDisplay)
	XSYM(XDefaultRootWindow)
	XSYM(XCreateSimpleWindow)
	XSYM(XDestroyWindow)
	XSYM(XInternAtom)
	XSYM(XConvertSelection)
	XSYM(XSelectInput)
	XSYM(XPending)
	XSYM(XNextEvent)
	XSYM(XGetWindowProperty)
	XSYM(XDeleteProperty)
	XSYM(XFree)
	XSYM(XFlush)
	XSYM(XGetSelectionOwner)
	XSYM(XSync)
	XSYM(XSetErrorHandler)
	XSYM(XSendEvent)
	XSYM(XSetSelectionOwner)
	XSYM(XChangeProperty)
	XSYM(XConnectionNumber)
	#undef XSYM

	X.lib = lib;
	install_error_handler_once();
	return true;
}

static pthread_mutex_t g_disp_mutex = PTHREAD_MUTEX_INITIALIZER;
static Display* g_our_displays[8] = {};
static XErrorHandler g_prev_error_handler = nullptr;
static bool g_error_handler_installed = false;

static void register_display(Display* d)
{
	pthread_mutex_lock(&g_disp_mutex);
	for (int i = 0; i < 8; i++)
		if (!g_our_displays[i]) { g_our_displays[i] = d; break; }
	pthread_mutex_unlock(&g_disp_mutex);
}

static void unregister_display(Display* d)
{
	pthread_mutex_lock(&g_disp_mutex);
	for (int i = 0; i < 8; i++)
		if (g_our_displays[i] == d) g_our_displays[i] = nullptr;
	pthread_mutex_unlock(&g_disp_mutex);
}

static int ecpaste_x_error_handler(Display* d, XErrorEvent* e)
{
	pthread_mutex_lock(&g_disp_mutex);
	bool ours = false;
	for (int i = 0; i < 8; i++)
		if (g_our_displays[i] == d) { ours = true; break; }
	pthread_mutex_unlock(&g_disp_mutex);

	if (ours) return 0;

	if ((e->error_code == BadWindow || e->error_code == BadDrawable)
		&& (e->request_code == 25 || e->request_code == 18
			|| e->request_code == 20 || e->request_code == 24))
		return 0;

	if (g_prev_error_handler) return g_prev_error_handler(d, e);
	return 0;
}

static void install_error_handler_once()
{

	if (g_error_handler_installed) return;
	g_prev_error_handler = X.XSetErrorHandler(ecpaste_x_error_handler);
	g_error_handler_installed = true;
}

static double now_seconds()
{
	struct timespec ts;
	clock_gettime(CLOCK_MONOTONIC, &ts);
	return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
}

static bool wait_event(Display* dpy, Window win, int type, Atom prop_filter,
                       int prop_state, XEvent* out, double timeout_s)
{
	double deadline = now_seconds() + timeout_s;
	for (;;) {
		while (X.XPending(dpy) > 0) {
			XEvent ev;
			X.XNextEvent(dpy, &ev);
			if (ev.type != type) continue;

			if (type == SelectionNotify) {
				if (ev.xselection.requestor == win) { *out = ev; return true; }
			} else if (type == PropertyNotify) {
				if (ev.xproperty.window == win
					&& ev.xproperty.atom == prop_filter
					&& ev.xproperty.state == prop_state) { *out = ev; return true; }
			}
		}
		if (now_seconds() >= deadline) return false;
		usleep(5000);
	}
}

static bool read_property(Display* dpy, Window win, Atom prop, Atom incr_atom,
                          Buf& out, const char** err)
{
	Atom actual_type = None;
	int fmt = 0;
	unsigned long nitems = 0, remaining = 0;
	unsigned char* data = nullptr;

	if (X.XGetWindowProperty(dpy, win, prop, 0, 0x1FFFFFFF, True, AnyPropertyType,
			&actual_type, &fmt, &nitems, &remaining, &data) != Success) {
		*err = "XGetWindowProperty failed";
		return false;
	}

	if (actual_type == incr_atom) {

		if (data) X.XFree(data);
		X.XFlush(dpy);

		for (;;) {
			XEvent ev;
			if (!wait_event(dpy, win, PropertyNotify, prop, PropertyNewValue, &ev, 2.0)) {
				*err = "INCR transfer timed out";
				return false;
			}

			data = nullptr;
			if (X.XGetWindowProperty(dpy, win, prop, 0, 0x1FFFFFFF, True, AnyPropertyType,
					&actual_type, &fmt, &nitems, &remaining, &data) != Success) {
				*err = "XGetWindowProperty failed during INCR";
				return false;
			}
			X.XFlush(dpy);

			unsigned long nbytes = nitems * (unsigned long)(fmt / 8);
			if (nbytes == 0) {
				if (data) X.XFree(data);
				return true;
			}
			bool ok = out.append((const char*)data, nbytes);
			if (data) X.XFree(data);
			if (!ok) { *err = "clipboard image too large (>32MB)"; return false; }
		}
	}

	if (data) {
		unsigned long nbytes = nitems * (unsigned long)(fmt / 8);
		bool ok = nbytes == 0 || out.append((const char*)data, nbytes);
		X.XFree(data);
		if (!ok) { *err = "clipboard image too large (>32MB)"; return false; }
	}
	return true;
}

enum X11Result { X11_IMAGE, X11_NO_IMAGE, X11_UNAVAILABLE };

static X11Result get_image_x11(Buf& out, const char** err)
{
	if (!load_xlib()) { *err = "libX11 not available"; return X11_UNAVAILABLE; }

	Display* dpy = X.XOpenDisplay(nullptr);
	if (!dpy) { *err = "cannot open X display"; return X11_UNAVAILABLE; }

	register_display(dpy);

	X11Result result = X11_NO_IMAGE;
	Window win = 0;

	Atom clipboard = X.XInternAtom(dpy, "CLIPBOARD", False);
	Atom png       = X.XInternAtom(dpy, "image/png", False);
	Atom incr      = X.XInternAtom(dpy, "INCR", False);
	Atom prop      = X.XInternAtom(dpy, "ECPASTE_DATA", False);

	{
		Window owner = X.XGetSelectionOwner(dpy, clipboard);
		if (owner == None) {
			*err = "clipboard is empty";
			goto cleanup;
		}

		Atom net_wm_pid = X.XInternAtom(dpy, "_NET_WM_PID", False);
		Atom actual_type = None;
		int fmt = 0;
		unsigned long nitems = 0, remaining = 0;
		unsigned char* pid_data = nullptr;
		if (X.XGetWindowProperty(dpy, owner, net_wm_pid, 0, 1, False, XA_CARDINAL,
				&actual_type, &fmt, &nitems, &remaining, &pid_data) == Success
			&& pid_data) {
			unsigned long owner_pid = (nitems > 0 && fmt == 32)
				? *(unsigned long*)pid_data : 0;
			X.XFree(pid_data);
			if (owner_pid != 0 && owner_pid == (unsigned long)getpid()) {
				*err = "clipboard is owned by the game itself (text copied in gmod)";
				goto cleanup;
			}
		}
	}

	win = X.XCreateSimpleWindow(dpy, X.XDefaultRootWindow(dpy), -10, -10, 1, 1, 0, 0, 0);
	if (!win) { *err = "cannot create X window"; result = X11_UNAVAILABLE; goto cleanup; }
	X.XSelectInput(dpy, win, PropertyChangeMask);

	X.XConvertSelection(dpy, clipboard, png, prop, win, CurrentTime);
	X.XFlush(dpy);

	{
		XEvent ev;
		if (!wait_event(dpy, win, SelectionNotify, None, 0, &ev, 0.4)) {

			*err = "clipboard owner did not respond";
			goto cleanup;
		}
		if (ev.xselection.property == None) {
			*err = "no image/png in clipboard";
			goto cleanup;
		}
		if (!read_property(dpy, win, ev.xselection.property, incr, out, err))
			goto cleanup;
	}

	if (!looks_like_png(out)) { *err = "clipboard data is not a PNG"; out.clear(); goto cleanup; }
	result = X11_IMAGE;

cleanup:
	if (win) X.XDestroyWindow(dpy, win);
	X.XSync(dpy, True);
	unregister_display(dpy);
	X.XCloseDisplay(dpy);
	return result;
}

static const char* const TOOL_COMMANDS[] = {
	"( timeout 2 wl-paste --type image/png ) 2>/dev/null",
	"( timeout 2 xclip -selection clipboard -t image/png -o ) 2>/dev/null",
	"( timeout 2 /run/host/usr/bin/wl-paste --type image/png ) 2>/dev/null",
	"( timeout 2 /run/host/usr/bin/xclip -selection clipboard -t image/png -o ) 2>/dev/null",
	"( timeout 3 flatpak-spawn --host wl-paste --type image/png ) 2>/dev/null",
	"( timeout 3 flatpak-spawn --host xclip -selection clipboard -t image/png -o ) 2>/dev/null",
	nullptr
};

static bool run_capture(const char* cmd, Buf& out)
{
	out.clear();
	FILE* pipe = popen(cmd, "r");
	if (!pipe) return false;

	char buf[65536];
	size_t n;
	bool overflow = false;
	while ((n = fread(buf, 1, sizeof(buf), pipe)) > 0) {
		if (!out.append(buf, n)) { overflow = true; break; }
	}
	int status = pclose(pipe);
	if (overflow) return false;
	if (status == -1 || !WIFEXITED(status) || WEXITSTATUS(status) != 0) return false;
	return out.size > 0;
}

static bool get_image_tools(Buf& out, const char** err)
{
	for (int i = 0; TOOL_COMMANDS[i]; i++) {
		if (run_capture(TOOL_COMMANDS[i], out) && looks_like_png(out))
			return true;
	}
	out.clear();
	*err = "external clipboard tools failed too";
	return false;
}

static bool get_clipboard_image(Buf& out, const char** err)
{
	const char* x11_err = "";
	X11Result res = get_image_x11(out, &x11_err);
	if (res == X11_IMAGE) return true;

	out.clear();
	if (res == X11_NO_IMAGE) {

		*err = x11_err;
		return false;
	}

	if (get_image_tools(out, err)) return true;
	*err = x11_err;
	return false;
}

static pthread_mutex_t g_clip_mutex = PTHREAD_MUTEX_INITIALIZER;
static char*           g_clip_text = nullptr;
static size_t          g_clip_len = 0;
static pthread_t       g_clip_thread;
static bool            g_clip_thread_running = false;
static int             g_clip_pipe[2] = { -1, -1 };

static void* clip_thread_main(void*)
{
	Display* dpy = X.XOpenDisplay(nullptr);
	if (!dpy) goto done;
	register_display(dpy);

	{
		Atom clipboard   = X.XInternAtom(dpy, "CLIPBOARD", False);
		Atom targets     = X.XInternAtom(dpy, "TARGETS", False);
		Atom utf8_string = X.XInternAtom(dpy, "UTF8_STRING", False);
		Atom text_plain  = X.XInternAtom(dpy, "text/plain;charset=utf-8", False);

		Window win = X.XCreateSimpleWindow(dpy, X.XDefaultRootWindow(dpy),
			-10, -10, 1, 1, 0, 0, 0);
		if (!win) { X.XCloseDisplay(dpy); goto done; }

		X.XSetSelectionOwner(dpy, clipboard, win, CurrentTime);
		X.XFlush(dpy);
		if (X.XGetSelectionOwner(dpy, clipboard) != win) {
			X.XDestroyWindow(dpy, win);
			unregister_display(dpy);
			X.XCloseDisplay(dpy);
			goto done;
		}

		int xfd = X.XConnectionNumber(dpy);
		for (;;) {

			while (X.XPending(dpy) > 0) {
				XEvent ev;
				X.XNextEvent(dpy, &ev);

				if (ev.type == SelectionClear) {

					X.XDestroyWindow(dpy, win);
					unregister_display(dpy);
					X.XCloseDisplay(dpy);
					goto done;
				}

				if (ev.type != SelectionRequest) continue;

				XSelectionRequestEvent* req = &ev.xselectionrequest;
				XEvent reply;
				memset(&reply, 0, sizeof(reply));
				reply.xselection.type      = SelectionNotify;
				reply.xselection.display   = req->display;
				reply.xselection.requestor = req->requestor;
				reply.xselection.selection = req->selection;
				reply.xselection.target    = req->target;
				reply.xselection.time      = req->time;
				reply.xselection.property  = None;

				Atom prop = req->property != None ? req->property : req->target;

				if (req->target == targets) {
					Atom offer[4] = { targets, utf8_string, text_plain, XA_STRING };
					X.XChangeProperty(dpy, req->requestor, prop, XA_ATOM, 32,
						PropModeReplace, (unsigned char*)offer, 4);
					reply.xselection.property = prop;
				} else if (req->target == utf8_string || req->target == XA_STRING
						|| req->target == text_plain) {
					pthread_mutex_lock(&g_clip_mutex);
					X.XChangeProperty(dpy, req->requestor, prop, req->target, 8,
						PropModeReplace,
						(const unsigned char*)(g_clip_text ? g_clip_text : ""),
						(int)g_clip_len);
					pthread_mutex_unlock(&g_clip_mutex);
					reply.xselection.property = prop;
				}

				X.XSendEvent(dpy, req->requestor, False, 0, &reply);
				X.XFlush(dpy);
			}

			struct pollfd fds[2] = {
				{ xfd,            POLLIN, 0 },
				{ g_clip_pipe[0], POLLIN, 0 },
			};
			poll(fds, 2, -1);

			if (fds[1].revents & POLLIN) {
				char cmd = 0;
				ssize_t rd = read(g_clip_pipe[0], &cmd, 1);
				if (rd == 1 && cmd == 'q') {
					X.XDestroyWindow(dpy, win);
					unregister_display(dpy);
					X.XCloseDisplay(dpy);
					goto done;
				}

			}
		}
	}

done:
	pthread_mutex_lock(&g_clip_mutex);
	g_clip_thread_running = false;
	pthread_mutex_unlock(&g_clip_mutex);
	return nullptr;
}

static bool clip_set_text(const char* text, size_t len)
{
	if (!load_xlib()) return false;
	if (len > 1024u * 1024u) len = 1024u * 1024u;

	pthread_mutex_lock(&g_clip_mutex);
	char* copy = (char*)malloc(len + 1);
	if (!copy) { pthread_mutex_unlock(&g_clip_mutex); return false; }
	memcpy(copy, text, len);
	copy[len] = '\0';
	free(g_clip_text);
	g_clip_text = copy;
	g_clip_len = len;

	bool need_thread = !g_clip_thread_running;
	if (need_thread) {
		if (g_clip_pipe[0] == -1 && pipe(g_clip_pipe) != 0) {
			pthread_mutex_unlock(&g_clip_mutex);
			return false;
		}
		pthread_attr_t attr;
		pthread_attr_init(&attr);
		pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
		if (pthread_create(&g_clip_thread, &attr, clip_thread_main, nullptr) == 0)
			g_clip_thread_running = true;
		pthread_attr_destroy(&attr);
	}
	bool ok = g_clip_thread_running;
	pthread_mutex_unlock(&g_clip_mutex);

	if (!need_thread && ok && g_clip_pipe[1] != -1) {
		char cmd = 'u';
		ssize_t wr = write(g_clip_pipe[1], &cmd, 1);
		(void)wr;
	}
	return ok;
}

typedef Bool (*fn_XFixesQueryExtension)(Display*, int*, int*);
typedef void (*fn_XFixesSelectSelectionInput)(Display*, Window, Atom, unsigned long);

static struct {
	Window owner;
	Buf*   text;
	bool   has_png;
	bool   valid;
} g_harvest = { None, nullptr, false, false };

static pthread_t g_watch_thread;
static bool      g_watch_running = false;
static int       g_watch_pipe[2] = { -1, -1 };

static bool targets_have_png(Display* dpy, Window win, Atom prop, Atom png_atom)
{
	Atom actual_type = None;
	int fmt = 0;
	unsigned long nitems = 0, remaining = 0;
	unsigned char* data = nullptr;
	bool has = false;
	if (X.XGetWindowProperty(dpy, win, prop, 0, 65536, True, AnyPropertyType,
			&actual_type, &fmt, &nitems, &remaining, &data) == Success && data) {
		if (fmt == 32) {
			long* atoms = (long*)data;
			for (unsigned long i = 0; i < nitems; i++)
				if ((Atom)atoms[i] == png_atom) { has = true; break; }
		}
		X.XFree(data);
	}
	return has;
}

static void harvest_clipboard(Display* dpy, Window win, Window owner,
	Atom clipboard, Atom targets, Atom utf8, Atom png_atom, Atom incr, Atom prop)
{
	bool has_png = false;
	Buf* text = new Buf();
	const char* err = "";
	XEvent ev;

	X.XConvertSelection(dpy, clipboard, targets, prop, win, CurrentTime);
	X.XFlush(dpy);
	if (wait_event(dpy, win, SelectionNotify, None, 0, &ev, 1.5)
		&& ev.xselection.property != None)
		has_png = targets_have_png(dpy, win, ev.xselection.property, png_atom);

	X.XConvertSelection(dpy, clipboard, utf8, prop, win, CurrentTime);
	X.XFlush(dpy);
	if (wait_event(dpy, win, SelectionNotify, None, 0, &ev, 1.5)
		&& ev.xselection.property != None)
		read_property(dpy, win, ev.xselection.property, incr, *text, &err);

	if (text->size == 0) {
		Buf latin1;
		X.XConvertSelection(dpy, clipboard, XA_STRING, prop, win, CurrentTime);
		X.XFlush(dpy);
		if (wait_event(dpy, win, SelectionNotify, None, 0, &ev, 1.0)
			&& ev.xselection.property != None)
			read_property(dpy, win, ev.xselection.property, incr, latin1, &err);

		for (size_t i = 0; i < latin1.size; i++) {
			unsigned char b = (unsigned char)latin1.data[i];
			if (b < 0x80) {
				text->append((const char*)&b, 1);
			} else {
				char two[2] = { (char)(0xC0 | (b >> 6)), (char)(0x80 | (b & 0x3F)) };
				text->append(two, 2);
			}
		}
	}

	pthread_mutex_lock(&g_clip_mutex);
	delete g_harvest.text;
	g_harvest.text = text;
	g_harvest.owner = owner;
	g_harvest.has_png = has_png;
	g_harvest.valid = true;
	pthread_mutex_unlock(&g_clip_mutex);
}

static void* watch_thread_main(void*)
{
	Display* dpy = X.XOpenDisplay(nullptr);
	if (!dpy) goto stopped;
	register_display(dpy);

	{
		Atom clipboard = X.XInternAtom(dpy, "CLIPBOARD", False);
		Atom targets   = X.XInternAtom(dpy, "TARGETS", False);
		Atom utf8      = X.XInternAtom(dpy, "UTF8_STRING", False);
		Atom png_atom  = X.XInternAtom(dpy, "image/png", False);
		Atom incr      = X.XInternAtom(dpy, "INCR", False);
		Atom prop      = X.XInternAtom(dpy, "ECPASTE_WATCH", False);

		Window win = X.XCreateSimpleWindow(dpy, X.XDefaultRootWindow(dpy),
			-10, -10, 1, 1, 0, 0, 0);
		if (!win) { unregister_display(dpy); X.XCloseDisplay(dpy); goto stopped; }
		X.XSelectInput(dpy, win, PropertyChangeMask);

		bool xfixes = false;
		int xfixes_evbase = 0;
		void* xflib = dlopen("libXfixes.so.3", RTLD_LAZY | RTLD_LOCAL);
		if (xflib) {
			fn_XFixesQueryExtension qe =
				(fn_XFixesQueryExtension)dlsym(xflib, "XFixesQueryExtension");
			fn_XFixesSelectSelectionInput sel =
				(fn_XFixesSelectSelectionInput)dlsym(xflib, "XFixesSelectSelectionInput");
			int errbase = 0;
			if (qe && sel && qe(dpy, &xfixes_evbase, &errbase)) {
				sel(dpy, win, clipboard, XFixesSetSelectionOwnerNotifyMask);
				X.XFlush(dpy);
				xfixes = true;
			}
		}

		int xfd = X.XConnectionNumber(dpy);
		Window last_owner = (Window)-1;
		bool force_harvest = false;

		for (;;) {

			while (X.XPending(dpy) > 0) {
				XEvent ev;
				X.XNextEvent(dpy, &ev);
				if (xfixes && ev.type == xfixes_evbase + XFixesSelectionNotify)
					force_harvest = true;
			}

			Window owner = X.XGetSelectionOwner(dpy, clipboard);
			if (owner != last_owner || force_harvest) {
				last_owner = owner;
				force_harvest = false;
				if (owner != None) {
					usleep(60000);
					harvest_clipboard(dpy, win, owner, clipboard, targets,
						utf8, png_atom, incr, prop);
				} else {
					pthread_mutex_lock(&g_clip_mutex);
					g_harvest.valid = false;
					g_harvest.owner = None;
					pthread_mutex_unlock(&g_clip_mutex);
				}
			}

			struct pollfd fds[2] = {
				{ xfd,             POLLIN, 0 },
				{ g_watch_pipe[0], POLLIN, 0 },
			};
			poll(fds, 2, xfixes ? 5000 : 300);

			if (fds[1].revents & POLLIN) {
				char cmd = 0;
				ssize_t rd = read(g_watch_pipe[0], &cmd, 1);
				if (rd == 1 && cmd == 'q') {
					X.XDestroyWindow(dpy, win);
					unregister_display(dpy);
					X.XCloseDisplay(dpy);
					goto stopped;
				}
			}
		}
	}

stopped:
	pthread_mutex_lock(&g_clip_mutex);
	g_watch_running = false;
	pthread_mutex_unlock(&g_clip_mutex);
	return nullptr;
}

static void start_watch()
{
	if (!load_xlib()) return;
	pthread_mutex_lock(&g_clip_mutex);
	if (!g_watch_running) {
		if (g_watch_pipe[0] == -1 && pipe(g_watch_pipe) != 0) {
			pthread_mutex_unlock(&g_clip_mutex);
			return;
		}
		pthread_attr_t attr;
		pthread_attr_init(&attr);
		pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
		if (pthread_create(&g_watch_thread, &attr, watch_thread_main, nullptr) == 0)
			g_watch_running = true;
		pthread_attr_destroy(&attr);
	}
	pthread_mutex_unlock(&g_clip_mutex);
}

static bool get_self_text(Buf& out)
{

	if (load_xlib()) {
		Display* dpy = X.XOpenDisplay(nullptr);
		if (dpy) {
			register_display(dpy);
			Atom clipboard = X.XInternAtom(dpy, "CLIPBOARD", False);
			Window owner = X.XGetSelectionOwner(dpy, clipboard);
			unregister_display(dpy);
			X.XCloseDisplay(dpy);

			pthread_mutex_lock(&g_clip_mutex);
			bool hit = g_harvest.valid && owner != None
				&& owner == g_harvest.owner
				&& g_harvest.text && g_harvest.text->size > 0;
			if (hit) out.append(g_harvest.text->data, g_harvest.text->size);
			pthread_mutex_unlock(&g_clip_mutex);
			if (hit) return true;
		}
	}

	if (load_xlib()) {
		Display* dpy = X.XOpenDisplay(nullptr);
		if (dpy) {
			register_display(dpy);
			Atom actual_type = None;
			int fmt = 0;
			unsigned long nitems = 0, remaining = 0;
			unsigned char* data = nullptr;

			if (X.XGetWindowProperty(dpy, X.XDefaultRootWindow(dpy), (Atom)9,
					0, 262144 , False, AnyPropertyType,
					&actual_type, &fmt, &nitems, &remaining, &data) == Success
				&& data) {
				if (fmt == 8 && nitems > 0)
					out.append((const char*)data, (size_t)nitems);
				X.XFree(data);
			}
			X.XSync(dpy, True);
			unregister_display(dpy);
			X.XCloseDisplay(dpy);
			if (out.size > 0) return true;
		}
	}

	typedef char* (*fn_SDL_GetClipboardText)(void);
	typedef void  (*fn_SDL_free)(void*);
	fn_SDL_GetClipboardText sdl_get =
		(fn_SDL_GetClipboardText)dlsym(RTLD_DEFAULT, "SDL_GetClipboardText");
	fn_SDL_free sdl_free = (fn_SDL_free)dlsym(RTLD_DEFAULT, "SDL_free");
	if (sdl_get) {
		char* text = sdl_get();
		if (text) {
			size_t len = strlen(text);
			if (len > 0) out.append(text, len);
			if (sdl_free) sdl_free(text);
			if (out.size > 0) return true;
		}
	}
	return false;
}

LUA_FUNCTION(GetImage)
{
	start_watch();
	Buf data;
	const char* err = "unknown error";
	if (get_clipboard_image(data, &err)) {
		LUA->PushString(data.data, (unsigned int)data.size);
		return 1;
	}
	LUA->PushNil();
	LUA->PushString(err);
	return 2;
}

LUA_FUNCTION(HasImage)
{
	Buf data;
	const char* err = "";
	LUA->PushBool(get_clipboard_image(data, &err));
	return 1;
}

LUA_FUNCTION(SetText)
{
	unsigned int len = 0;
	const char* text = LUA->GetString(1, &len);
	if (!text) { LUA->PushBool(false); return 1; }
	LUA->PushBool(clip_set_text(text, (size_t)len));
	return 1;
}

LUA_FUNCTION(StartWatch)
{
	start_watch();
	LUA->PushBool(true);
	return 1;
}

LUA_FUNCTION(GetSelfText)
{
	Buf out;
	if (get_self_text(out)) {
		LUA->PushString(out.data, (unsigned int)out.size);
		return 1;
	}
	LUA->PushNil();
	return 1;
}

LUA_FUNCTION(GetBackend)
{

	const char* disp = getenv("DISPLAY");
	if (load_xlib() && disp && disp[0]) {
		LUA->PushString("x11");
		return 1;
	}
	LUA->PushString("tools-fallback");
	return 1;
}

static const double ECPASTE_VERSION = 11;

GMOD_MODULE_OPEN()
{
	LUA->PushSpecial(GarrysMod::Lua::SPECIAL_GLOB);
		LUA->CreateTable();
			LUA->PushNumber(ECPASTE_VERSION); LUA->SetField(-2, "VERSION");
			LUA->PushCFunction(GetImage);   LUA->SetField(-2, "GetImage");
			LUA->PushCFunction(HasImage);   LUA->SetField(-2, "HasImage");
			LUA->PushCFunction(GetBackend); LUA->SetField(-2, "GetBackend");
			LUA->PushCFunction(SetText);    LUA->SetField(-2, "SetText");
			LUA->PushCFunction(GetSelfText); LUA->SetField(-2, "GetSelfText");
			LUA->PushCFunction(StartWatch);  LUA->SetField(-2, "StartWatch");
		LUA->SetField(-2, "ecpaste");
	LUA->Pop();
	return 0;
}

GMOD_MODULE_CLOSE()
{
	pthread_mutex_lock(&g_clip_mutex);
	bool running = g_clip_thread_running;
	pthread_mutex_unlock(&g_clip_mutex);
	if (running && g_clip_pipe[1] != -1) {
		char cmd = 'q';
		ssize_t wr = write(g_clip_pipe[1], &cmd, 1);
		(void)wr;
	}
	pthread_mutex_lock(&g_clip_mutex);
	bool watching = g_watch_running;
	pthread_mutex_unlock(&g_clip_mutex);
	if (watching && g_watch_pipe[1] != -1) {
		char cmd = 'q';
		ssize_t wr = write(g_watch_pipe[1], &cmd, 1);
		(void)wr;
	}
	return 0;
}
