/***************************************************************************

  x11.c

  (c) 2000-2017 Benoît Minisini <g4mba5@gmail.com>

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2, or (at your option)
  any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
  MA 02110-1301, USA.

***************************************************************************/

#define __X11_C

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>

#include "main.h"
#include "x11.h"
#include "c_x11.h"
#include "systray/systray.h"

#define MAX_WINDOW_PROP 16

Atom X11_atom_net_wm_state;
Atom X11_atom_net_wm_state_above;
Atom X11_atom_net_wm_state_below;
Atom X11_atom_net_wm_state_stays_on_top;
Atom X11_atom_net_wm_state_skip_taskbar;
Atom X11_atom_net_wm_window_type;
Atom X11_atom_net_wm_window_type_normal;
Atom X11_atom_net_wm_window_type_utility;
Atom X11_atom_net_wm_desktop;
Atom X11_atom_net_wm_user_time;
Atom X11_atom_net_current_desktop;
Atom X11_atom_net_workarea = None;
Atom X11_atom_motif_wm_hints = None;
Atom X11_atom_net_system_tray = None;

Atom X11_UTF8_STRING;

bool X11_ready = FALSE;

Display *X11_display = NULL;
#define _display X11_display
Window X11_root = 0;
#define _root X11_root
bool X11_event_filter_enabled = FALSE;

static bool _atom_init = FALSE;

static bool _has_test_extension = FALSE;
static bool _init_keycode = FALSE;

static int _min_keycode, _max_keycode, _keysyms_per_keycode;
static KeySym *_keycode_map = NULL;

static XModifierKeymap *_modifier_map = NULL;
static KeyCode *_shift_keycode = NULL;
static KeyCode *_alt_gr_keycode = NULL;

typedef
	struct {
		int count;
		Atom atoms[MAX_WINDOW_PROP];
		bool changed;
		}
	X11_PROPERTY;

static X11_PROPERTY _window_prop;
static X11_PROPERTY _window_save[2];

static char *_property_value = NULL;

static GB_FUNCTION _x11_property_notify_func;
static GB_FUNCTION _x11_configure_notify_func;

static void init_atoms()
{
	if (_atom_init)
		return;

	//fprintf(stderr, "init_atom: display = %p\n", _display);
	X11_atom_net_current_desktop = XInternAtom(_display, "_NET_CURRENT_DESKTOP", True);
	//fprintf(stderr, "init_atom: #1\n");
	X11_atom_net_wm_state = XInternAtom(_display, "_NET_WM_STATE", True);
	X11_atom_net_wm_state_above = XInternAtom(_display, "_NET_WM_STATE_ABOVE", True);
	X11_atom_net_wm_state_below = XInternAtom(_display, "_NET_WM_STATE_BELOW", True);
	X11_atom_net_wm_state_stays_on_top = XInternAtom(_display, "_NET_WM_STATE_STAYS_ON_TOP", True);
	X11_atom_net_wm_state_skip_taskbar = XInternAtom(_display, "_NET_WM_STATE_SKIP_TASKBAR", True);

	X11_atom_net_wm_desktop = XInternAtom(_display, "_NET_WM_DESKTOP", True);
	X11_atom_net_wm_window_type = XInternAtom(_display, "_NET_WM_WINDOW_TYPE", True);
	X11_atom_net_wm_window_type_normal = XInternAtom(_display, "_NET_WM_WINDOW_TYPE_NORMAL", True);
	X11_atom_net_wm_window_type_utility = XInternAtom(_display, "_NET_WM_WINDOW_TYPE_UTILITY", True);
	X11_atom_net_wm_user_time = XInternAtom(_display, "_NET_WM_USER_TIME", True);

	X11_UTF8_STRING = XInternAtom(X11_display, "UTF8_STRING", True);

	_atom_init = TRUE;
}

#define PROPERTY_START_READ 1024
#define PROPERTY_NEXT_READ 1024

#if 0
	// On 64 bits OS, format 32 are actually long, i.e. int padded to 64 bits!
	#if OS_64BITS
	if (format == 32)
	{
		int *p = (int *)_property_value;
		for (i = 0; i < count; i++)
			p[i] = *((long *)data + i);
	}
	else
	#endif
	{
		memcpy(_property_value, (char *)data, count * size);
	}
#endif

static size_t sizeof_format(int format)
{
	return format == 32 ? sizeof(long) : (format == 16 ? sizeof(short) : sizeof(char));
}

char *X11_get_property(Window wid, Atom prop, Atom *type, int *format, int *pcount)
{
	uchar *data;
  unsigned long count;
  unsigned long after;
	unsigned long offset;
	int size, offset_size;

	*pcount = 0;

	if (XGetWindowProperty(_display, wid, prop, 0, PROPERTY_START_READ / sizeof(int32_t),
			False, AnyPropertyType, type, format,
			&count, &after, &data) != Success)
		return NULL;

	*pcount += count;

	size = sizeof_format(*format);
	offset_size = *format == 32 ? sizeof(int32_t) : ( *format == 16 ? sizeof(short) : 1 );

	//fprintf(stderr, "X11_get_property: format = %d size = %d count = %ld after = %ld\n", *format, size, count, after);

	GB.FreeString(&_property_value);
	_property_value = GB.NewString((char *)data, count * size);
	XFree(data);

	offset = count * offset_size / sizeof(int32_t);

	while (after)
	{
		//fprintf(stderr, "X11_get_property: offset = %ld read = %ld\n", offset, Min(after, PROPERTY_NEXT_READ) / sizeof(int32_t));

		if (XGetWindowProperty(_display, wid, prop, offset, Min(after, PROPERTY_NEXT_READ) / sizeof(int32_t),
				False, AnyPropertyType, type, format,
				&count, &after, &data) != Success)
			return NULL;

		*pcount += count;
		offset += count * offset_size / sizeof(int32_t);

		//fprintf(stderr, "X11_get_property: format = %d size = %d count = %ld after = %ld next offset = %ld\n", *format, size, count, after, offset);

		_property_value = GB.AddString(_property_value, (char *)data, count * size);
		XFree(data);
	}

	return _property_value;
}

#if 0
static void get_property(Window wid, Atom prop, int maxlength, unsigned char **data, unsigned long *count)
{
	Atom type;
	int format;
	unsigned long after;

	XGetWindowProperty(_display, wid, prop, 0, maxlength / 4,
		False, AnyPropertyType, &type, &format,
		count, &after, data);
}
#endif

static char *get_property(Window wid, Atom prop, int *count)
{
	Atom type;
	int format;

	return X11_get_property(wid, prop, &type, &format, count);
}

Atom X11_get_property_type(Window wid, Atom prop, int *format)
{
	uchar *data = NULL;
	unsigned long count;
	unsigned long after;
	Atom type;

	if (XGetWindowProperty(X11_display, wid, prop, 0, PROPERTY_START_READ / sizeof(int32_t),
			False, AnyPropertyType, &type, format,
			&count, &after, &data) != Success)
		return None;

	XFree(data);
	return type;
}

#if 0
	#if OS_64BITS
	long padded_data[count];
	int i;

	if (format == 32)
	{
		for (i = 0; i < count; i++)
			padded_data[i] = ((int *)data)[i];
	}
	data = padded_data;
	#endif
#endif

void X11_set_property(Window wid, Atom prop, Atom type, int format, void *data, int count)
{
	XChangeProperty(X11_display, wid, prop, type, format, PropModeReplace, (uchar *)data, count);
}

Atom X11_intern_atom(const char *name, bool create)
{
	int val = atoi(name);

	if (val)
		return (Atom)val;
	else
		return XInternAtom(X11_display, name, !create);
}

static void load_window_state(Window win, Atom prop)
{
	int length;
	char *data;

	_window_prop.count = 0;
	_window_prop.changed = FALSE;

	data = get_property(win, prop, &length);

	if (length > MAX_WINDOW_PROP)
		length = MAX_WINDOW_PROP;

	_window_prop.count = length;
	if (data)
		memcpy(_window_prop.atoms, data, length * sizeof(Atom));
}

static void save_window_state(Window win, Atom prop)
{
	if (_window_prop.changed)
	{
		//fprintf(stderr, "XChangeProperty: %ld %ld\n", (long)win, (long)prop);
		XChangeProperty(_display, win, prop, XA_ATOM, 32, PropModeReplace, (unsigned char *)_window_prop.atoms, _window_prop.count);
	}
}

static bool has_window_state(Atom prop)
{
	int i;

	for (i = 0; i < _window_prop.count; i++)
	{
		if (_window_prop.atoms[i] == prop)
			return TRUE;
	}

	return FALSE;
}

static void set_window_state(Atom prop)
{
	if (has_window_state(prop))
		return;

	if (_window_prop.count == MAX_WINDOW_PROP)
	{
		fprintf(stderr, "X11: set_window_state: Too many properties in window\n");
		return;
	}

	_window_prop.atoms[_window_prop.count++] = prop;
	_window_prop.changed = TRUE;
}

static void clear_window_state(Atom prop)
{
	int i;

	for (i = 0; i < _window_prop.count; i++)
	{
		if (_window_prop.atoms[i] == prop)
		{
			_window_prop.count--;

			for (; i < _window_prop.count; i++)
				_window_prop.atoms[i] = _window_prop.atoms[i + 1];

			_window_prop.changed = TRUE;
			return;
		}
	}
}


bool X11_do_init()
{
	int event_base, error_base, major_version, minor_version;

	if (X11_ready)
		return FALSE;

	GB.Component.GetInfo("DISPLAY", POINTER(&_display));

	_root = DefaultRootWindow(_display);

	X11_ready = _display != NULL;

	if (!X11_ready)
	{
		fprintf(stderr, "WARNING: X11_init() has failed\n");
		return TRUE;
	}

	init_atoms();

	_has_test_extension = XTestQueryExtension(_display, &event_base, &error_base, &major_version, &minor_version);

	return FALSE;
}

void X11_exit()
{
	if (_keycode_map)
		XFree(_keycode_map);
	if (_modifier_map)
		XFreeModifiermap(_modifier_map);
	if (_property_value)
		GB.FreeString(&_property_value);
}

void X11_send_client_message(Window dest, Window window, Atom message, char *data, int format, int count)
{
	XEvent e;
	int mask = (SubstructureRedirectMask | SubstructureNotifyMask);

	//fprintf(stderr, "X11_send_client_message: dest = %ld window = %ld message = %ld format = %d count = %d\n", dest, window, message, format, count);

	e.xclient.type = ClientMessage;
	e.xclient.message_type = message;
	e.xclient.display = X11_display;
	e.xclient.window = window;
	e.xclient.format = format;

	memset(&e.xclient.data.l[0], 0, sizeof(long) * 5);
	if (data)
	{
		count *= sizeof_format(format);
		if (count > (sizeof(long) * 5))
			count = sizeof(long) * 5;
		memcpy(&e.xclient.data.l[0], data, count);
		//fprintf(stderr, "%ld %ld %ld %ld %ld\n", e.xclient.data.l[0], e.xclient.data.l[1], e.xclient.data.l[2], e.xclient.data.l[3], e.xclient.data.l[4]);
	}

	XSendEvent(X11_display, dest, False, mask, &e);
	XFlush(X11_display);
}

void X11_window_change_property(Window window, bool visible, Atom property, bool set)
{
	XEvent e;
	long mask = (SubstructureRedirectMask | SubstructureNotifyMask);

	if (visible)
	{
		e.xclient.type = ClientMessage;
		e.xclient.message_type = X11_atom_net_wm_state;
		e.xclient.display = _display;
		e.xclient.window = window;
		e.xclient.format = 32;
		e.xclient.data.l[0] = set ? 1 : 0;
		e.xclient.data.l[1] = property;
		e.xclient.data.l[2] = 0;
		e.xclient.data.l[3] = 0;
		e.xclient.data.l[4] = 0;

		XSendEvent(_display, _root, False, mask, &e);
		XFlush(X11_display);
	}
	else
	{
		load_window_state(window, X11_atom_net_wm_state);

		if (set)
			set_window_state(property);
		else
			clear_window_state(property);

		save_window_state(window, X11_atom_net_wm_state);
	}
}


bool X11_window_has_property(Window window, Atom property)
{
	load_window_state(window, X11_atom_net_wm_state);
	return has_window_state(property);
}

void X11_sync(void)
{
	XSync(_display, False);
}

void X11_window_save_properties(Window window)
{
	load_window_state(window, X11_atom_net_wm_state);
	_window_save[0] = _window_prop;
	//load_window_state(window, X11_atom_net_wm_window_type);
	//_window_save[1] = _window_prop;
}

void X11_window_restore_properties(Window window)
{
	_window_prop = _window_save[0];
	save_window_state(window, X11_atom_net_wm_state);
	//_window_prop = _window_save[1];
	//save_window_state(window, X11_atom_net_wm_window_type);
}

#define SYSTEM_TRAY_REQUEST_DOCK    0
#define SYSTEM_TRAY_BEGIN_MESSAGE   1
#define SYSTEM_TRAY_CANCEL_MESSAGE  2

#define OPCODE "_NET_SYSTEM_TRAY_OPCODE"

void X11_window_dock(Window window)
{
	Window xmanager=None;
	XClientMessageEvent ev;
	Atom OpCodeAtom;
	Screen *xscreen;
	char buf[256];
	Atom selection_atom;

	buf[0]=0;

	xscreen = DefaultScreenOfDisplay(_display);
	sprintf(buf,"_NET_SYSTEM_TRAY_S%d",XScreenNumberOfScreen(xscreen));
	selection_atom = XInternAtom(_display, buf, 0);

	XGrabServer(_display);

	xmanager = XGetSelectionOwner(_display, selection_atom);
	if (xmanager != None)
		XSelectInput(_display, xmanager, StructureNotifyMask);

	XUngrabServer(_display);
	XFlush(_display);

	/***********************************************
		Dock Tray Icon
	************************************************/

	OpCodeAtom = XInternAtom(_display, OPCODE, 0);

	ev.type = ClientMessage;
	ev.window = xmanager;
	ev.message_type = OpCodeAtom;
	ev.format = 32;
	ev.data.l[0] = 0;
	ev.data.l[1] = SYSTEM_TRAY_REQUEST_DOCK;
	ev.data.l[2] = window;
	ev.data.l[3] = 0;
	ev.data.l[4] = 0;

	XSendEvent(_display, xmanager, 0, NoEventMask, (XEvent *)&ev);
	XSync(_display, 0);
}

void X11_window_startup(Window window, int x, int y, int w, int h)
{
	XSizeHints s;

	s.flags = USPosition | PPosition | USSize | PSize;

	s.x = x;
	s.y = y;
	s.width = w;
	s.height = h;

	XSetWMNormalHints(_display, window, &s);
}

void X11_find_windows(Window **window_list, int *count)
{
	static Atom _net_client_list = 0;

	if (!_net_client_list)
		_net_client_list = XInternAtom(_display, "_NET_CLIENT_LIST", True);

	*window_list = (Window *)get_property(_root, _net_client_list, count);
}

void X11_get_window_title(Window window, char **result, int *length)
{
	static Atom _net_wm_name = 0;

	if (!_net_wm_name)
		_net_wm_name = XInternAtom(_display, "_NET_WM_NAME", True);

	*result = get_property(window, _net_wm_name, length);
}

void X11_get_window_class(Window window, char **result, int *length)
{
	*result = get_property(window, XA_WM_CLASS, length);
}

void X11_get_window_role(Window window, char **result, int *length)
{
	static Atom wm_window_role = (Atom)0;

	if (!wm_window_role)
		wm_window_role = XInternAtom(_display, "WM_WINDOW_ROLE", True);

	*result = get_property(window, wm_window_role, length);
}


// Makes a tool window

void X11_set_window_tool(Window window, int tool, Window parent)
{
	load_window_state(window, X11_atom_net_wm_window_type);

	if (tool)
	{
		set_window_state(X11_atom_net_wm_window_type_utility);
		clear_window_state(X11_atom_net_wm_window_type_normal);
		if (parent)
			XSetTransientForHint(_display, window, parent);
	}
	else
	{
		clear_window_state(X11_atom_net_wm_window_type_utility);
		set_window_state(X11_atom_net_wm_window_type_normal);
	}

	save_window_state(window, X11_atom_net_wm_window_type);
}

int X11_get_window_tool(Window window)
{
	load_window_state(window, X11_atom_net_wm_window_type);
	return has_window_state(X11_atom_net_wm_window_type_utility);
}


// Set window desktop

void X11_window_set_desktop(Window window, bool visible, int desktop)
{
	XEvent e;
	long mask = (SubstructureRedirectMask | SubstructureNotifyMask);

	if (visible)
	{
		e.xclient.type = ClientMessage;
		e.xclient.message_type = X11_atom_net_wm_desktop;
		e.xclient.display = _display;
		e.xclient.window = window;
		e.xclient.format = 32;
		e.xclient.data.l[0] = desktop;
		e.xclient.data.l[1] = 1;
		e.xclient.data.l[2] = 0;
		e.xclient.data.l[3] = 0;
		e.xclient.data.l[4] = 0;

		XSendEvent(_display, _root, False, mask, &e);
		XFlush(_display);
	}
	else
	{
		XChangeProperty(_display, window, X11_atom_net_wm_desktop, XA_CARDINAL, 32, PropModeReplace,
				(unsigned char *)&desktop, 1);
		XFlush(_display);
	}
}


int X11_window_get_desktop(Window window)
{
	int length;
	char *data = NULL;
	int desktop = 0;

	data = get_property(window, X11_atom_net_wm_desktop, &length);
	if (data)
		desktop = *((int *)data);

	return desktop;
}

int X11_get_current_desktop()
{
	int length;
	char *data;
	int desktop = 0;

	data = get_property(_root, X11_atom_net_current_desktop, &length);
	if (data)
		desktop = *((int *)data);

	return desktop;
}

static void init_keycode()
{
	int i, j; //, k;
	KeyCode *pm, *p;

	if (_init_keycode)
		return;

	XDisplayKeycodes(_display, &_min_keycode, &_max_keycode);

	_keycode_map = XGetKeyboardMapping(_display, _min_keycode, _max_keycode - _min_keycode + 1, &_keysyms_per_keycode);
	_modifier_map = XGetModifierMapping(_display);

	p = _modifier_map->modifiermap;
	for (i = 0; i < 8; i++)
	{
		pm = p;
		for (j = 0; j < _modifier_map->max_keypermod; j++)
		{
			//for (k = 0; k < 3; k++)
				switch (XkbKeycodeToKeysym(_display, *p, 0, 0))
				{
					case XK_Shift_L: _shift_keycode = pm; break;
					case XK_Mode_switch: _alt_gr_keycode = pm; break;
				}
			p++;
		}
	}

	//fprintf(stderr, "SHIFT: %d  ALTGR: %d\n", _shift_keycode[0], _alt_gr_keycode[0]);

	_init_keycode = TRUE;
}

static void send_modifiers(KeyCode *codes, bool press)
{
	int i;

	for (i = 0; i < _modifier_map->max_keypermod; i++)
	{
		if (codes[i])
			XTestFakeKeyEvent(_display, codes[i], press, CurrentTime);
	}
}

static void handle_modifier(KeyCode code, KeySym keysym, bool press)
{
	KeySym *sym;
	int i;

	sym = &_keycode_map[(code - _min_keycode) * _keysyms_per_keycode];

	for (i = 0; i < _keysyms_per_keycode; i++)
	{
		if (keysym == sym[i])
		{
			//fprintf(stderr, "0x%04X: %d\n", keysym, i);
			break;
		}
	}

	switch(i)
	{
		case 1:
			send_modifiers(_shift_keycode, press);
			break;
		case 2:
			send_modifiers(_alt_gr_keycode, press);
			break;
		case 3:
			send_modifiers(_shift_keycode, press);
			send_modifiers(_alt_gr_keycode, press);
			break;
		//default:
		//	fprintf(stderr, "0x%04X: ?\n", keysym);
	}
}

char *X11_send_key(char *key, bool press)
{
	KeySym keysym;
	KeyCode code;

	if (!_has_test_extension)
		return "No XTEST extension";

	if (!_init_keycode)
		init_keycode();

	//fprintf(stderr, "X11_send_key: '%s' %d\n", key, XStringToKeysym(key));

	if (strlen(key) == 1)
	{
		if (*key == '\n')
			keysym = XK_Return;
		else if (*key == '\t')
			keysym = XK_Tab;
		else if (*key >= ' ' || *key < 0)
			keysym = (int)*key & 0xFF;
		else
			keysym = NoSymbol;
	}
	else
		keysym = XStringToKeysym(key);

	if (keysym == NoSymbol)
		return "Unknown key";

	code = XKeysymToKeycode(_display, keysym);
	if (code)
	{
		if (press)
			handle_modifier(code, keysym, TRUE);

		XTestFakeKeyEvent(_display, code, press, CurrentTime);

		if (press)
			handle_modifier(code, keysym, FALSE);
	}

	usleep(1000);

	return NULL;
}

void X11_enable_event_filter(bool enable)
{
	static int count = 0;

	void (*set_event_filter)(void *) = NULL;

	if (enable)
		count++;
	else
		count--;

	GB.Component.GetInfo("SET_EVENT_FILTER", POINTER(&set_event_filter));
	if (set_event_filter)
		(*set_event_filter)(count ? X11_event_filter : NULL);
}

int X11_event_filter(XEvent *e)
{
	static bool init = FALSE;

	if (!init)
	{
		GB_CLASS startup = GB.Application.StartupClass();
		GB.GetFunction(&_x11_property_notify_func, (void *)startup, "X11_PropertyNotify", "ii", "");
		GB.GetFunction(&_x11_configure_notify_func, (void *)startup, "X11_ConfigureNotify", "iiiii", "");
		init = TRUE;
	}

	if (e->type == PropertyNotify && GB_FUNCTION_IS_VALID(&_x11_property_notify_func))
	{
		GB.Push(2, GB_T_INTEGER, e->xany.window, GB_T_INTEGER, e->xproperty.atom);
		GB.Call(&_x11_property_notify_func, 2, TRUE);
	}
	else if (e->type == ConfigureNotify && GB_FUNCTION_IS_VALID(&_x11_configure_notify_func))
	{
		GB.Push(5, GB_T_INTEGER, e->xany.window,
			GB_T_INTEGER, e->xconfigure.x,
			GB_T_INTEGER, e->xconfigure.y,
			GB_T_INTEGER, e->xconfigure.width,
			GB_T_INTEGER, e->xconfigure.height);

		GB.Call(&_x11_configure_notify_func, 5, TRUE);
	}

	WATCHER_event_filter(e);
	return SYSTRAY_event_filter(e);
}

void X11_get_window_geometry(Window win, int *wx, int *wy, int *ww, int *wh)
{
	Window p;
	int transx, transy;
	XWindowAttributes wattr;

	*wx = *wy = *ww = *wh = 0;

	if (!XTranslateCoordinates(_display, win, _root, 0, 0, &transx, &transy, &p))
		return;

	if (!XGetWindowAttributes(_display, win, &wattr))
		return;

	*wx = transx - wattr.border_width;
	*wy = transy - wattr.border_width;
	*ww = wattr.width + wattr.border_width * 2;
	*wh = wattr.height + wattr.border_width * 2;
}
