#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xutil.h>
#include <X11/extensions/Xcomposite.h>

#include <unordered_set>
#include <pthread.h>

#include <obs-module.h>
#include <util/platform.h>

#include "xcompcap-helper.hpp"

namespace XCompcap {
static Display *xdisplay = 0;

Display *disp()
{
	if (!xdisplay)
		xdisplay = XOpenDisplay(NULL);

	return xdisplay;
}

void cleanupDisplay()
{
	if (!xdisplay)
		return;

	XCloseDisplay(xdisplay);
	xdisplay = 0;
}

static void getAllWindows(Window parent, std::list<Window> &windows)
{
	UNUSED_PARAMETER(parent);
	UNUSED_PARAMETER(windows);
}

std::list<Window> getAllWindows()
{
	std::list<Window> res;

	for (int i = 0; i < ScreenCount(disp()); ++i)
		getAllWindows(RootWindow(disp(), i), res);

	return res;
}

// Specification for checking for ewmh support at
// http://standards.freedesktop.org/wm-spec/wm-spec-latest.html#idm140200472693600

bool ewmhIsSupported()
{
	Display *display = disp();
	Atom netSupportingWmCheck =
		XInternAtom(display, "_NET_SUPPORTING_WM_CHECK", true);
	Atom actualType;
	int format = 0;
	unsigned long num = 0, bytes = 0;
	unsigned char *data = NULL;
	Window ewmh_window = 0;

	int status = XGetWindowProperty(display, DefaultRootWindow(display),
					netSupportingWmCheck, 0L, 1L, false,
					XA_WINDOW, &actualType, &format, &num,
					&bytes, &data);

	if (status == Success) {
		if (num > 0) {
			ewmh_window = ((Window *)data)[0];
		}
		if (data) {
			XFree(data);
			data = NULL;
		}
	}

	if (ewmh_window) {
		status = XGetWindowProperty(display, ewmh_window,
					    netSupportingWmCheck, 0L, 1L, false,
					    XA_WINDOW, &actualType, &format,
					    &num, &bytes, &data);
		if (status != Success || num == 0 ||
		    ewmh_window != ((Window *)data)[0]) {
			ewmh_window = 0;
		}
		if (status == Success && data) {
			XFree(data);
		}
	}

	return ewmh_window != 0;
}

std::list<Window> getTopLevelWindows()
{
	std::list<Window> res;

	if (!ewmhIsSupported()) {
		blog(LOG_WARNING, "Unable to query window list "
				  "because window manager "
				  "does not support extended "
				  "window manager Hints");
		return res;
	}

	Atom netClList = XInternAtom(disp(), "_NET_CLIENT_LIST", true);
	Atom actualType;
	int format;
	unsigned long num, bytes;
	Window *data = 0;

	for (int i = 0; i < ScreenCount(disp()); ++i) {
		Window rootWin = RootWindow(disp(), i);

		int status = XGetWindowProperty(disp(), rootWin, netClList, 0L,
						~0L, false, AnyPropertyType,
						&actualType, &format, &num,
						&bytes, (uint8_t **)&data);

		if (status != Success) {
			blog(LOG_WARNING, "Failed getting root "
					  "window properties");
			continue;
		}

		for (unsigned long i = 0; i < num; ++i)
			res.push_back(data[i]);

		XFree(data);
	}

	return res;
}

int getRootWindowScreen(Window root)
{
	XWindowAttributes attr;

	if (!XGetWindowAttributes(disp(), root, &attr))
		return DefaultScreen(disp());

	return XScreenNumberOfScreen(attr.screen);
}

std::string getWindowAtom(Window win, const char *atom)
{
	Atom netWmName = XInternAtom(disp(), atom, false);
	int n;
	char **list = 0;
	XTextProperty tp;
	std::string res = "unknown";

	XGetTextProperty(disp(), win, &tp, netWmName);

	if (!tp.nitems)
		XGetWMName(disp(), win, &tp);

	if (!tp.nitems)
		return "error";

	if (tp.encoding == XA_STRING) {
		res = (char *)tp.value;
	} else {
		int ret = XmbTextPropertyToTextList(disp(), &tp, &list, &n);

		if (ret >= Success && n > 0 && *list) {
			res = *list;
			XFreeStringList(list);
		}
	}

	char *conv = nullptr;
	if (os_mbs_to_utf8_ptr(res.c_str(), 0, &conv))
		res = conv;
	bfree(conv);

	XFree(tp.value);

	return res;
}

std::string getWindowCommand(Window win)
{
	Atom xi = XInternAtom(disp(), "WM_COMMAND", false);
	int n;
	char **list = 0;
	XTextProperty tp;
	std::string res = "error";

	XGetTextProperty(disp(), win, &tp, xi);

	if (!tp.nitems)
		return std::string();

	if (tp.encoding == XA_STRING) {
		res = (char *)tp.value;
	} else {
		int ret = XmbTextPropertyToTextList(disp(), &tp, &list, &n);
		if (ret >= Success && n > 0 && *list) {
			res = *list;
			XFreeStringList(list);
		}
	}

	XFree(tp.value);

	return res;
}

int getWindowPid(Window win)
{
	UNUSED_PARAMETER(win);
	return 1234; //TODO
}

static std::unordered_set<Window> changedWindows;
static pthread_mutex_t changeLock = PTHREAD_MUTEX_INITIALIZER;
void processEvents()
{
	PLock lock(&changeLock);

	XLockDisplay(disp());

	while (XEventsQueued(disp(), QueuedAfterReading) > 0) {
		XEvent ev;

		XNextEvent(disp(), &ev);

		if (ev.type == ConfigureNotify)
			changedWindows.insert(ev.xconfigure.event);

		if (ev.type == MapNotify)
			changedWindows.insert(ev.xmap.event);

		if (ev.type == Expose)
			changedWindows.insert(ev.xexpose.window);

		if (ev.type == VisibilityNotify)
			changedWindows.insert(ev.xvisibility.window);

		if (ev.type == DestroyNotify)
			changedWindows.insert(ev.xdestroywindow.event);
	}

	XUnlockDisplay(disp());
}

bool windowWasReconfigured(Window win)
{
	PLock lock(&changeLock);

	auto it = changedWindows.find(win);

	if (it != changedWindows.end()) {
		changedWindows.erase(it);
		return true;
	}

	return false;
}

}

PLock::PLock(pthread_mutex_t *mtx, bool trylock) : m(mtx)
{
	if (trylock)
		islock = mtx && pthread_mutex_trylock(mtx) == 0;
	else
		islock = mtx && pthread_mutex_lock(mtx) == 0;
}

PLock::~PLock()
{
	if (islock) {
		pthread_mutex_unlock(m);
	}
}

bool PLock::isLocked()
{
	return islock;
}

void PLock::unlock()
{
	if (islock) {
		pthread_mutex_unlock(m);
		islock = false;
	}
}

void PLock::lock()
{
	if (!islock) {
		pthread_mutex_lock(m);
		islock = true;
	}
}

static bool *curErrorTarget = 0;
static char curErrorText[200];
static int xerrorlock_handler(Display *disp, XErrorEvent *err)
{

	if (curErrorTarget)
		*curErrorTarget = true;

	XGetErrorText(disp, err->error_code, curErrorText, 200);

	return 0;
}

XErrorLock::XErrorLock()
{
	goterr = false;
	islock = false;
	prevhandler = 0;

	lock();
}

XErrorLock::~XErrorLock()
{
	unlock();
}

bool XErrorLock::isLocked()
{
	return islock;
}

void XErrorLock::lock()
{
	if (!islock) {
		XLockDisplay(XCompcap::disp());
		XSync(XCompcap::disp(), 0);

		curErrorTarget = &goterr;
		curErrorText[0] = 0;
		prevhandler = XSetErrorHandler(xerrorlock_handler);

		islock = true;
	}
}

void XErrorLock::unlock()
{
	if (islock) {
		XSync(XCompcap::disp(), 0);

		curErrorTarget = 0;
		XSetErrorHandler(prevhandler);
		prevhandler = 0;
		XUnlockDisplay(XCompcap::disp());
		islock = false;
	}
}

bool XErrorLock::gotError()
{
	if (!islock)
		return false;

	XSync(XCompcap::disp(), 0);

	bool res = goterr;
	goterr = false;
	return res;
}

std::string XErrorLock::getErrorText()
{
	return curErrorText;
}

void XErrorLock::resetError()
{
	if (islock)
		XSync(XCompcap::disp(), 0);

	goterr = false;
	curErrorText[0] = 0;
}

XDisplayLock::XDisplayLock()
{
	islock = false;
	lock();
}

XDisplayLock::~XDisplayLock()
{
	unlock();
}

bool XDisplayLock::isLocked()
{
	return islock;
}

void XDisplayLock::lock()
{
	if (!islock) {
		XLockDisplay(XCompcap::disp());
		islock = true;
	}
}

void XDisplayLock::unlock()
{
	if (islock) {
		XSync(XCompcap::disp(), 0);
		XUnlockDisplay(XCompcap::disp());
		islock = false;
	}
}

ObsGsContextHolder::ObsGsContextHolder()
{
	obs_enter_graphics();
}

ObsGsContextHolder::~ObsGsContextHolder()
{
	obs_leave_graphics();
}