/* evilwm - minimalist window manager for X11
 * Copyright (C) 1999-2021 Ciaran Anscomb <evilwm@6809.org.uk>
 * see README for license and other details. */

// Miscellaneous utility functions

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/wait.h>
#include <unistd.h>

#include <X11/X.h>
#include <X11/Xlib.h>
#include <X11/Xproto.h>

#include "client.h"
#include "display.h"
#include "events.h"
#include "log.h"
#include "screen.h"
#include "util.h"

// For get_property()
#define MAXIMUM_PROPERTY_LENGTH 4096

// Error handler interaction
int ignore_xerror = 0;
volatile Window initialising = None;

// Spawn a subprocess by fork()ing twice so we don't have to worry about
// SIGCHLDs.

void spawn(const char *const cmd[]) {
	struct screen *current_screen = find_current_screen();
	pid_t pid;

	if (current_screen && current_screen->display)
		putenv(current_screen->display);
	if (!(pid = fork())) {
		// Put first fork in a new session
		setsid();
		switch (fork()) {
			// execvp()'s prototype is (char *const *) suggesting that it
			// modifies the contents of the strings.  The prototype is this
			// way due to SUS maintaining compatability with older code.
			// However, execvp guarantees not to modify argv, so the following
			// cast is valid.
			case 0: execvp(cmd[0], (char *const *)cmd); break;
			default: _exit(0);
		}
	}
	if (pid > 0)
		wait(NULL);
}

// When something we do raises an X error, we get sent here.  There are several
// specific types of error that we know we want to ignore, or that indicate a
// fatal error.  For the rest, cease managing the client, as it should indicate
// that the window has disappeared.

int handle_xerror(Display *dsply, XErrorEvent *e) {
	struct client *c;
	(void)dsply;  // unused

	LOG_ENTER("handle_xerror(error=%d, request=%d/%d, resourceid=%lx)", e->error_code, e->request_code, e->minor_code, e->resourceid);

	// Some parts of the code deliberately disable error checking.

	if (ignore_xerror) {
		LOG_DEBUG("ignoring...\n");
		LOG_LEAVE();
		return 0;
	}

	// client_manage_new() sets initialising to non-None to test if a
	// window still exists.  If we end up here, the test failed, so
	// indicate that by setting it back to None.

	if (initialising != None && e->resourceid == initialising) {
		LOG_DEBUG("error caught while initialising window=%lx\n", (unsigned long)initialising);
		initialising = None;
		LOG_LEAVE();
		return 0;
	}

	// This error is generally raised when trying to start evilwm while
	// another window manager is running.

	if (e->error_code == BadAccess && e->request_code == X_ChangeWindowAttributes) {
		LOG_ERROR("root window unavailable (maybe another wm is running?)\n");
		exit(1);
	}

	// Batching of events sometimes leads to us calling XSetInputFocus()
	// for enter events to windows that were subsequently deleted.  Ignore
	// these errors.

	if (e->request_code == X_SetInputFocus) {
		LOG_DEBUG("ignoring harmless error caused by possible race\n");
		LOG_LEAVE();
		return 0;
	}

	// For any other raised error, remove the client that triggered it from
	// management, as it's probably gone.

	c = find_client(e->resourceid);
	if (c) {
		LOG_DEBUG("flagging client for removal\n");
		c->remove = 1;
		need_client_tidy = 1;
	} else {
		LOG_DEBUG("unknown error: not handling\n");
	}

	LOG_LEAVE();
	return 0;
}

// Simplify calls to XQueryPointer(), and make destination pointers optional

Bool get_pointer_root_xy(Window w, int *x, int *y) {
	Window root_r, child_r;
	int root_x_r, root_y_r;
	int win_x_r, win_y_r;
	unsigned mask_r;
	if (!x)
		x = &root_x_r;
	if (!y)
		y = &root_y_r;
	return XQueryPointer(display.dpy, w, &root_r, &child_r, x, y, &win_x_r, &win_y_r, &mask_r);
}

// Wraps XGetWindowProperty()

void *get_property(Window w, Atom property, Atom req_type,
		   unsigned long *nitems_return) {
	Atom actual_type;
	int actual_format;
	unsigned long bytes_after;
	unsigned char *prop;
	if (XGetWindowProperty(display.dpy, w, property,
			       0L, MAXIMUM_PROPERTY_LENGTH / 4, False,
			       req_type, &actual_type, &actual_format,
			       nitems_return, &bytes_after, &prop) == Success) {
		if (actual_type == req_type)
			return (void *)prop;
		XFree(prop);
	}
	return NULL;
}

// interruptibleXNextEvent() is taken from the Blender source and comes with
// the following copyright notice:
//
// Copyright (c) Mark J. Kilgard, 1994, 1995, 1996.
//
// This program is freely distributable without licensing fees and is provided
// without guarantee or warrantee expressed or implied. This program is -not-
// in the public domain.

// Unlike XNextEvent, if a signal arrives, interruptibleXNextEvent will return
// zero.

int interruptibleXNextEvent(XEvent *event) {
	fd_set fds;
	int rc;
	int dpy_fd = ConnectionNumber(display.dpy);
	for (;;) {
		if (XPending(display.dpy)) {
			XNextEvent(display.dpy, event);
			return 1;
		}
		FD_ZERO(&fds);
		FD_SET(dpy_fd, &fds);
		rc = select(dpy_fd + 1, &fds, NULL, NULL, NULL);
		if (rc < 0) {
			if (errno == EINTR) {
				return 0;
			} else {
				LOG_ERROR("interruptibleXNextEvent(): select()\n");
			}
		}
	}
}

// Remove enter events from the queue, preserving only the last one
// corresponding to "except"s parent.

void discard_enter_events(struct client *except) {
	XEvent tmp, putback_ev;
	int putback = 0;
	XSync(display.dpy, False);
	while (XCheckMaskEvent(display.dpy, EnterWindowMask, &tmp)) {
		if (tmp.xcrossing.window == except->parent) {
			memcpy(&putback_ev, &tmp, sizeof(XEvent));
			putback = 1;
		}
	}
	if (putback) {
		XPutBackEvent(display.dpy, &putback_ev);
	}
}
