#include "platform-funcs.hpp"
#include "hotkey.hpp"

#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include <X11/extensions/scrnsaver.h>
#include <X11/extensions/XTest.h>
#undef Bool
#undef CursorShape
#undef Expose
#undef KeyPress
#undef KeyRelease
#undef FocusIn
#undef FocusOut
#undef FontChange
#undef None
#undef Status
#undef Unsorted
#include <util/platform.h>
#include <vector>
#include <thread>
#include <unordered_map>
#include <QStringList>
#include <QRegularExpression>
#include <QLibrary>
#ifdef USE_PROCPS
#include <proc/readproc.h>
#else
#include <libproc2/pids.h>
#endif
#include <fstream>
#include <sstream>

static Display *xdisplay = 0;

static QLibrary *libXtstHandle = nullptr;
typedef int (*keyPressFunc)(Display *, unsigned int, bool, unsigned long);
static keyPressFunc pressFunc = nullptr;
bool canSimulateKeyPresses = false;

static QLibrary *libXssHandle = nullptr;
typedef XScreenSaverInfo *(*XScreenSaverAllocInfoFunc)();
typedef int (*XScreenSaverQueryInfoFunc)(Display *, Window, XScreenSaverInfo *);
static XScreenSaverAllocInfoFunc allocSSFunc = nullptr;
static XScreenSaverQueryInfoFunc querySSFunc = nullptr;
bool canGetIdleTime = false;

std::chrono::high_resolution_clock::time_point lastMouseLeftClickTime{};
std::chrono::high_resolution_clock::time_point lastMouseMiddleClickTime{};
std::chrono::high_resolution_clock::time_point lastMouseRightClickTime{};

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

	return xdisplay;
}

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

	XCloseDisplay(xdisplay);
	xdisplay = 0;
}

static bool ewmhIsSupported()
{
	Display *display = disp();
	if (!display) {
		return false;
	}
	auto window = DefaultRootWindow(display);
	if (!window) {
		return false;
	}

	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 ewmhWindow = 0;

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

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

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

static QStringList getStates(Window window)
{
	QStringList states;
	if (!window || !ewmhIsSupported()) {
		return states;
	}

	Atom wmState = XInternAtom(disp(), "_NET_WM_STATE", true), type;
	int format;
	unsigned long num, bytes;
	unsigned char *data;

	int status = XGetWindowProperty(disp(), window, wmState, 0, ~0L, false,
					AnyPropertyType, &type, &format, &num,
					&bytes, &data);

	if (status == Success) {
		for (unsigned long i = 0; i < num; i++) {
			states.append(QString(
				XGetAtomName(disp(), ((Atom *)data)[i])));
		}
		XFree(data);
	}

	return states;
}

static std::vector<Window> getTopLevelWindows()
{
	std::vector<Window> res;
	if (!ewmhIsSupported()) {
		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 rootWindow = RootWindow(disp(), i);
		if (!rootWindow) {
			continue;
		}

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

		if (status != Success) {
			continue;
		}

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

		XFree(data);
	}

	return res;
}

std::string getWindowName(Window window)
{
	auto display = disp();
	if (!display || !window) {
		return "";
	}
	std::string windowTitle;
	char *name;
	int status = XFetchName(display, window, &name);
	if (status >= Success && name != nullptr) {
		std::string str(name);
		windowTitle = str;
		XFree(name);
	} else {
		XTextProperty xtp_new_name;
		if (XGetWMName(display, window, &xtp_new_name) != 0 &&
		    xtp_new_name.value != nullptr) {
			std::string str((const char *)xtp_new_name.value);
			windowTitle = str;
			XFree(xtp_new_name.value);
		}
	}

	return windowTitle;
}

void GetWindowList(std::vector<std::string> &windows)
{
	windows.resize(0);
	for (auto window : getTopLevelWindows()) {
		auto name = getWindowName(window);
		if (name.empty()) {
			continue;
		}
		windows.emplace_back(name);
	}
}

void GetWindowList(QStringList &windows)
{
	windows.clear();
	for (auto window : getTopLevelWindows()) {
		auto name = getWindowName(window);
		if (name.empty()) {
			continue;
		}
		windows << QString::fromStdString(name);
	}
}

int getActiveWindow(Window *&window)
{
	if (!ewmhIsSupported()) {
		return -1;
	}

	Atom active = XInternAtom(disp(), "_NET_ACTIVE_WINDOW", true);
	Atom actualType;
	int format;
	unsigned long num, bytes;

	auto rootWindow = DefaultRootWindow(disp());
	if (!rootWindow) {
		return -2;
	}

	return XGetWindowProperty(disp(), rootWindow, active, 0L, ~0L, false,
				  AnyPropertyType, &actualType, &format, &num,
				  &bytes, (uint8_t **)&window);
}

void GetCurrentWindowTitle(std::string &title)
{
	Window *data = 0;
	if (getActiveWindow(data) != Success || !data) {
		return;
	}
	if (!data[0]) {
		XFree(data);
		return;
	}

	auto name = getWindowName(data[0]);
	XFree(data);
	if (name.empty()) {
		return;
	}
	title = name;
}

bool windowStatesAreSet(const std::string &windowTitle,
			std::vector<QString> &expectedStates)
{
	if (!ewmhIsSupported()) {
		return false;
	}

	std::vector<Window> windows = getTopLevelWindows();
	for (auto &window : windows) {
		auto name = getWindowName(window);
		if (name.empty()) {
			continue;
		}

		bool equals = windowTitle == name;
		bool matches = QString::fromStdString(name).contains(
			QRegularExpression(
				QString::fromStdString(windowTitle)));

		if (!(equals || matches)) {
			continue;
		}

		QStringList states = getStates(window);
		if (states.isEmpty()) {
			if (expectedStates.empty()) {
				return true;
			}
			return false;
		}

		for (const auto &state : expectedStates) {
			if (!states.contains(state)) {
				return false;
			}
		}
		return true;
	}
	return false;
}

bool isMaximized(const std::string &title)
{
	std::vector<QString> states;
	states.emplace_back("_NET_WM_STATE_MAXIMIZED_VERT");
	states.emplace_back("_NET_WM_STATE_MAXIMIZED_HORZ");
	return windowStatesAreSet(title, states);
}

bool isFullscreen(const std::string &title)
{
	std::vector<QString> states;
	states.emplace_back("_NET_WM_STATE_FULLSCREEN");
	return windowStatesAreSet(title, states);
}

#ifdef USE_PROCPS
void GetProcessList(QStringList &processes)
{
	processes.clear();
	PROCTAB *proc = openproc(PROC_FILLSTAT);
	proc_t proc_info;
	memset(&proc_info, 0, sizeof(proc_info));
	while (readproc(proc, &proc_info) != NULL) {
		QString procName(proc_info.cmd);
		if (!procName.isEmpty() && !processes.contains(procName)) {
			processes << procName;
		}
	}
	closeproc(proc);
}
#else
void GetProcessList(QStringList &processes)
{
	processes.clear();
	struct pids_info *info = NULL;
	struct pids_stack *stack;
	enum pids_item Items[] = {
		PIDS_CMD,
	};

	if (procps_pids_new(&info, Items, sizeof(Items) / sizeof(Items[0])) <
	    0) {
		return;
	}

	while ((stack = procps_pids_get(info, PIDS_FETCH_TASKS_ONLY))) {
		auto cmd = PIDS_VAL(0, str, stack, info);
		QString procName(cmd);
		if (!procName.isEmpty() && !processes.contains(procName)) {
			processes << procName;
		}
	}
	procps_pids_unref(&info);
}
#endif

long getForegroundProcessPid()
{
	Window *window;
	if (getActiveWindow(window) != Success || !window || !*window) {
		return -1;
	}
	Atom atom, actual_type;
	int actual_format;
	unsigned long nitems;
	unsigned long bytes_after;
	unsigned char *prop;
	long long pid;
	atom = XInternAtom(disp(), "_NET_WM_PID", True);
	auto status = XGetWindowProperty(disp(), *window, atom, 0, 1024, False,
					 XA_CARDINAL, &actual_type,
					 &actual_format, &nitems, &bytes_after,
					 &prop);
	XFree(window);
	if (status != 0) {
		return -2;
	}
	if (!prop) {
		return -3;
	}

	pid = *((long *)prop);
	XFree(prop);
	return pid;
}

std::string getProcNameFromPid(long pid)
{
	std::string path = "/proc/" + std::to_string(pid) + "/comm";
	std::ifstream t(path);
	std::stringstream buffer;
	buffer << t.rdbuf();
	std::string name = buffer.str();
	if (!name.empty() && name[name.length() - 1] == '\n') {
		name.erase(name.length() - 1);
	}
	return name;
}

void GetForegroundProcessName(QString &proc)
{
	std::string temp;
	GetForegroundProcessName(temp);
	proc = QString::fromStdString(temp);
}

void GetForegroundProcessName(std::string &proc)
{
	proc.resize(0);
	auto pid = getForegroundProcessPid();
	proc = getProcNameFromPid(pid);
}

bool isInFocus(const QString &executable)
{
	std::string current;
	GetForegroundProcessName(current);

	// True if executable switch equals current window
	bool equals = (executable.toStdString() == current);
	// True if executable switch matches current window
	bool matches = QString::fromStdString(current).contains(
		QRegularExpression(executable));

	return (equals || matches);
}

int secondsSinceLastInput()
{
	if (!canGetIdleTime) {
		return 0;
	}

	auto display = disp();
	if (!display) {
		return 0;
	}

	auto window = DefaultRootWindow(display);
	if (!window) {
		return 0;
	}

	time_t idle_time;
	auto mit_info = allocSSFunc();

	auto status = querySSFunc(display, window, mit_info);
	idle_time = (mit_info->idle) / 1000;
	XFree(mit_info);

	return status != 0 ? idle_time : 0;
}

static std::unordered_map<HotkeyType, long> keyTable = {
	// Chars
	{HotkeyType::Key_A, XK_A},
	{HotkeyType::Key_B, XK_B},
	{HotkeyType::Key_C, XK_C},
	{HotkeyType::Key_D, XK_D},
	{HotkeyType::Key_E, XK_E},
	{HotkeyType::Key_F, XK_F},
	{HotkeyType::Key_G, XK_G},
	{HotkeyType::Key_H, XK_H},
	{HotkeyType::Key_I, XK_I},
	{HotkeyType::Key_J, XK_J},
	{HotkeyType::Key_K, XK_K},
	{HotkeyType::Key_L, XK_L},
	{HotkeyType::Key_M, XK_M},
	{HotkeyType::Key_N, XK_N},
	{HotkeyType::Key_O, XK_O},
	{HotkeyType::Key_P, XK_P},
	{HotkeyType::Key_Q, XK_Q},
	{HotkeyType::Key_R, XK_R},
	{HotkeyType::Key_S, XK_S},
	{HotkeyType::Key_T, XK_T},
	{HotkeyType::Key_U, XK_U},
	{HotkeyType::Key_V, XK_V},
	{HotkeyType::Key_W, XK_W},
	{HotkeyType::Key_X, XK_X},
	{HotkeyType::Key_Y, XK_Y},
	{HotkeyType::Key_Z, XK_Z},

	// Numbers
	{HotkeyType::Key_0, XK_0},
	{HotkeyType::Key_1, XK_1},
	{HotkeyType::Key_2, XK_2},
	{HotkeyType::Key_3, XK_3},
	{HotkeyType::Key_4, XK_4},
	{HotkeyType::Key_5, XK_5},
	{HotkeyType::Key_6, XK_6},
	{HotkeyType::Key_7, XK_7},
	{HotkeyType::Key_8, XK_8},
	{HotkeyType::Key_9, XK_9},

	{HotkeyType::Key_F1, XK_F1},
	{HotkeyType::Key_F2, XK_F2},
	{HotkeyType::Key_F3, XK_F3},
	{HotkeyType::Key_F4, XK_F4},
	{HotkeyType::Key_F5, XK_F5},
	{HotkeyType::Key_F6, XK_F6},
	{HotkeyType::Key_F7, XK_F7},
	{HotkeyType::Key_F8, XK_F8},
	{HotkeyType::Key_F9, XK_F9},
	{HotkeyType::Key_F10, XK_F10},
	{HotkeyType::Key_F11, XK_F11},
	{HotkeyType::Key_F12, XK_F12},
	{HotkeyType::Key_F13, XK_F13},
	{HotkeyType::Key_F14, XK_F14},
	{HotkeyType::Key_F15, XK_F15},
	{HotkeyType::Key_F16, XK_F16},
	{HotkeyType::Key_F17, XK_F17},
	{HotkeyType::Key_F18, XK_F18},
	{HotkeyType::Key_F19, XK_F19},
	{HotkeyType::Key_F20, XK_F20},
	{HotkeyType::Key_F21, XK_F21},
	{HotkeyType::Key_F22, XK_F22},
	{HotkeyType::Key_F23, XK_F23},
	{HotkeyType::Key_F24, XK_F24},

	{HotkeyType::Key_Escape, XK_Escape},
	{HotkeyType::Key_Space, XK_space},
	{HotkeyType::Key_Return, XK_Return},
	{HotkeyType::Key_Backspace, XK_BackSpace},
	{HotkeyType::Key_Tab, XK_Tab},

	{HotkeyType::Key_Shift_L, XK_Shift_L},
	{HotkeyType::Key_Shift_R, XK_Shift_R},
	{HotkeyType::Key_Control_L, XK_Control_L},
	{HotkeyType::Key_Control_R, XK_Control_R},
	{HotkeyType::Key_Alt_L, XK_Alt_L},
	{HotkeyType::Key_Alt_R, XK_Alt_R},
	{HotkeyType::Key_Win_L, XK_Super_L},
	{HotkeyType::Key_Win_R, XK_Super_R},
	{HotkeyType::Key_Apps, XK_Hyper_L},

	{HotkeyType::Key_CapsLock, XK_Caps_Lock},
	{HotkeyType::Key_NumLock, XK_Num_Lock},
	{HotkeyType::Key_ScrollLock, XK_Scroll_Lock},

	{HotkeyType::Key_PrintScreen, XK_Print},
	{HotkeyType::Key_Pause, XK_Pause},

	{HotkeyType::Key_Insert, XK_Insert},
	{HotkeyType::Key_Delete, XK_Delete},
	{HotkeyType::Key_PageUP, XK_Page_Up},
	{HotkeyType::Key_PageDown, XK_Page_Down},
	{HotkeyType::Key_Home, XK_Home},
	{HotkeyType::Key_End, XK_End},

	{HotkeyType::Key_Left, XK_Left},
	{HotkeyType::Key_Up, XK_Up},
	{HotkeyType::Key_Right, XK_Right},
	{HotkeyType::Key_Down, XK_Down},

	{HotkeyType::Key_Numpad0, XK_KP_0},
	{HotkeyType::Key_Numpad1, XK_KP_1},
	{HotkeyType::Key_Numpad2, XK_KP_2},
	{HotkeyType::Key_Numpad3, XK_KP_3},
	{HotkeyType::Key_Numpad4, XK_KP_4},
	{HotkeyType::Key_Numpad5, XK_KP_5},
	{HotkeyType::Key_Numpad6, XK_KP_6},
	{HotkeyType::Key_Numpad7, XK_KP_7},
	{HotkeyType::Key_Numpad8, XK_KP_8},
	{HotkeyType::Key_Numpad9, XK_KP_9},

	{HotkeyType::Key_NumpadAdd, XK_KP_Add},
	{HotkeyType::Key_NumpadSubtract, XK_KP_Subtract},
	{HotkeyType::Key_NumpadMultiply, XK_KP_Multiply},
	{HotkeyType::Key_NumpadDivide, XK_KP_Divide},
	{HotkeyType::Key_NumpadDecimal, XK_KP_Decimal},
	{HotkeyType::Key_NumpadEnter, XK_KP_Enter},
};

void PressKeys(const std::vector<HotkeyType> keys, int duration)
{
	if (!canSimulateKeyPresses) {
		return;
	}

	Display *display = disp();
	if (!display) {
		return;
	}

	// Press keys
	for (auto &key : keys) {
		auto it = keyTable.find(key);
		if (it == keyTable.end()) {
			continue;
		}
		pressFunc(display, XKeysymToKeycode(display, it->second), true,
			  CurrentTime);
	}
	XFlush(display);

	std::this_thread::sleep_for(std::chrono::milliseconds(duration));

	// Release keys
	for (auto &key : keys) {
		auto it = keyTable.find(key);
		if (it == keyTable.end()) {
			continue;
		}
		pressFunc(display, XKeysymToKeycode(display, it->second), false,
			  CurrentTime);
	}

	XFlush(display);
}

void PlatformInit()
{
	auto display = disp();
	if (!display) {
		return;
	}

	libXtstHandle = new QLibrary("libXtst", nullptr);
	pressFunc = (keyPressFunc)libXtstHandle->resolve("XTestFakeKeyEvent");
	int _;
	canSimulateKeyPresses = pressFunc &&
				XQueryExtension(disp(), "XTEST", &_, &_, &_);

	libXssHandle = new QLibrary("libXss", nullptr);
	allocSSFunc = (XScreenSaverAllocInfoFunc)libXssHandle->resolve(
		"XScreenSaverAllocInfo");
	querySSFunc = (XScreenSaverQueryInfoFunc)libXssHandle->resolve(
		"XScreenSaverQueryInfo");
	canGetIdleTime = allocSSFunc && querySSFunc &&
			 XQueryExtension(disp(), ScreenSaverName, &_, &_, &_);
}

void PlatformCleanup()
{
	if (libXtstHandle) {
		delete libXtstHandle;
		libXtstHandle = nullptr;
	}
	if (libXssHandle) {
		delete libXssHandle;
		libXssHandle = nullptr;
	}
	cleanupDisplay();
}
