/*
 * Copyright 2022 Collabora, Ltd.
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice (including the
 * next paragraph) shall be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 */

#include "config.h"

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <stddef.h>
#include <unistd.h>
#include <poll.h>

#include <time.h>

#include <wayland-client.h>
#include "tests/test-config.h"
#include "shared/helpers.h"
#include "shared/xalloc.h"
#include "shared/xcb-xwayland.h"
#include <libweston/zalloc.h>
#include "xcb-client-helper.h"

#define DEBUG

#ifdef DEBUG
#define printfd(fmt, args...) do {	\
	fprintf(stderr, fmt, ##args);	\
} while (0)
#else
#define printfd(fmt, args...)	{}
#endif

struct event_response {
    uint8_t response_type;
    bool (*eventcb)(xcb_generic_event_t *e, struct window_x11 *win);
    const char *name;
};

const char *to_event_name(uint8_t event);

static xcb_drawable_t
handle_event_to_wid(xcb_generic_event_t *ev)
{
	xcb_drawable_t wid;

	switch (EVENT_TYPE(ev)) {
	case XCB_CREATE_NOTIFY: {
		xcb_create_notify_event_t *ce = (xcb_create_notify_event_t *) ev;
		wid = ce->window;
		break;
	}
	case XCB_DESTROY_NOTIFY: {
		xcb_destroy_notify_event_t *de = (xcb_destroy_notify_event_t *) ev;
		wid = de->window;
		break;
	}
	case XCB_MAP_NOTIFY: {
		xcb_map_notify_event_t *mn = (xcb_map_notify_event_t *) ev;
		wid = mn->window;
		break;
	}
	case XCB_UNMAP_NOTIFY: {
		xcb_unmap_notify_event_t *un = (xcb_unmap_notify_event_t *) ev;
		wid = un->window;
		break;
	}
	case XCB_PROPERTY_NOTIFY: {
		xcb_property_notify_event_t *pn = (xcb_property_notify_event_t *) ev;
		wid = pn->window;
		break;
	}
	case XCB_CONFIGURE_NOTIFY: {
		xcb_configure_notify_event_t *cn = (xcb_configure_notify_event_t *) ev;
		wid = cn->window;
		break;
	}
	case XCB_EXPOSE: {
		xcb_expose_event_t *ep = (xcb_expose_event_t *) ev;
		wid = ep->window;
		break;
	}
	case XCB_REPARENT_NOTIFY: {
		xcb_reparent_notify_event_t *re = (xcb_reparent_notify_event_t *) ev;
		wid = re->window;
		break;
	}
	default:
		wid = 0;
	}

	return wid;
}

void
handle_event_remove_pending(struct window_state *wstate)
{
	wl_list_remove(&wstate->link);
	free(wstate);
}

/* returns true if all events in the pending_list has been accounted for */
static bool
handle_event_check_pending(struct window_x11 *window, xcb_generic_event_t *ev)
{

	struct window_state *wstate, *wstate_next;
	struct wl_list *pending_events =
		&window->tentative_state.pending_events_list;
	xcb_drawable_t wid;
	uint8_t event;
	bool found = false;

	event = EVENT_TYPE(ev);
	wid = handle_event_to_wid(ev);

	wl_list_for_each_safe(wstate, wstate_next, pending_events, link) {
		if (wstate->event != event)
			continue;

		if (wstate->wid == wid) {
			handle_event_remove_pending(wstate);
			found = true;
			printfd("%s: removed event %d - %s\n", __func__,
				event, to_event_name(event));
			break;
		}
	}

	if (!found) {
		printfd("%s(): event id %d, name %s not found\n", __func__,
				event, to_event_name(event));
		return false;
	}

	/* still need to get events? -> wait one more round */
	if (!wl_list_empty(pending_events)) {
		printfd("%s(): still have %d events to handle!\n", __func__,
				wl_list_length(pending_events));
		return false;
	}

	return true;
}

/** In case you need to wait for a notify event call use this function to do so
 * and then call handle_events_x11() at the end, to wait for the events to be
 * delivered/handled. Note that handle_events_x11() will wait forever if
 * handle_event_set_pending() was called for an event which never arrives.
 *
 * You can call this function multiple times for the same wid, in case you
 * expect multiple events to be delivered (i.e., a map notify event and a
 * expose one). All functions in this XCB wrapper library calls this function,
 * with the user only needing to call handle_events_x11() at the end. This
 * function is only needed if the test itself requires to wait for additional
 * events, or when expanding this library with other states changes
 * (max/fullscreen).
 *
 * \param window the window_x11 in question
 * \param event the event to wait for, like XCB_MAP_NOTIFY, XCB_EXPOSE, etc.
 * \param pending_state the pending event to wait for, similar to the XCB ones
 *  but defined in enum w_state
 * \param wid the window id, could be different than that of the window itself
 */
void
handle_event_set_pending(struct window_x11 *window, uint8_t event,
			 enum w_state pending_state, xcb_drawable_t wid)
{
	struct window_state *wstate = xzalloc(sizeof(*wstate));

	wstate->event = event;
	wstate->wid = wid;
	wstate->pending_state = pending_state;
	wl_list_insert(&window->tentative_state.pending_events_list,
		       &wstate->link);

	printfd("%s: Added pending event id %d - name %s, wid %d\n",
		__func__, event, to_event_name(event), wid);
}

static bool
handle_map_notify(xcb_generic_event_t *e, struct window_x11 *window)
{
	xcb_map_notify_event_t *ce = (xcb_map_notify_event_t *) e;

	if (ce->window != window->win_id)
		return false;

	window_state_set_flag(window, MAPPED);

	return true;
}

static bool
handle_unmap_notify(xcb_generic_event_t *e, struct window_x11 *window)
{
	xcb_unmap_notify_event_t *ce = (xcb_unmap_notify_event_t*) e;

	if (ce->window != window->win_id && ce->window != window->frame_id)
		return false;

	test_assert_true(window_state_has_flag(window, MAPPED));
	window_state_set_flag(window, UNMAPPED);

	return true;
}

static bool
handle_create_notify(xcb_generic_event_t *e, struct window_x11 *window)
{
	xcb_create_notify_event_t *ce = (xcb_create_notify_event_t*) e;

	if (ce->window != window->win_id)
		return false;

	window_state_set_flag(window, CREATED);

	return true;
}


static bool
handle_destroy_notify(xcb_generic_event_t *e, struct window_x11 *window)
{
	xcb_destroy_notify_event_t *dn = (xcb_destroy_notify_event_t*) e;

	if (window->win_id != dn->window)
		return false;

	test_assert_true(window_state_has_flag(window, CREATED));
	window_state_set_flag(window, DESTROYED);

	return true;
}

static bool
handle_property_notify(xcb_generic_event_t *e, struct window_x11 *window)
{
	xcb_property_notify_event_t *pn = (xcb_property_notify_event_t *) e;
	struct atom_x11 *atoms = window->conn->atoms;

	if (pn->window != window->win_id)
		return false;

	if (pn->atom == atoms->net_wm_name) {
		window_state_set_flag(window, PROPERTY_NAME);
		return true;
	}

	return false;
}

static bool
handle_expose(xcb_generic_event_t *e, struct window_x11 *window)
{
	xcb_expose_event_t *ep = (xcb_expose_event_t *) e;

	if (ep->window != window->win_id)
		return false;

	window_state_set_flag(window, EXPOSE);
	return true;
}

static bool
handle_configure_notify(xcb_generic_event_t *e, struct window_x11 *window)
{
	xcb_configure_notify_event_t *cn = (xcb_configure_notify_event_t*) e;

	/* we're not interested into other's windows */
	if (cn->window != window->win_id)
		return false;

	return true;
}

static bool
handle_reparent_notify(xcb_generic_event_t *e, struct window_x11 *window)
{
	xcb_reparent_notify_event_t *re = (xcb_reparent_notify_event_t *) e;

	if (re->window == window->win_id && window->frame_id == 0) {
		window->frame_id = re->parent;
		window_state_set_flag(window, REPARENT);
		printfd("Window reparent frame id %d\n", window->frame_id);
		return true;
	}

	return false;
}

/* the event handlers should return a boolean that denotes that fact
 * they've been handled. One can customize that behaviour such that
 * it forces handle_events_x11() to wait for additional events, in case
 * that's needed. */
static const struct event_response events[] = {
	{ XCB_CREATE_NOTIFY, handle_create_notify, "CREATE_NOTIFY" },
	{ XCB_MAP_NOTIFY, handle_map_notify, "MAP_NOTIFY" },
	{ XCB_UNMAP_NOTIFY, handle_unmap_notify, "UNMAP_NOTIFY" },
	{ XCB_EXPOSE, handle_expose, "EXPOSE_NOTIFY" },
	{ XCB_PROPERTY_NOTIFY, handle_property_notify, "PROPERTY_NOTIFY" },
	{ XCB_CONFIGURE_NOTIFY, handle_configure_notify, "CONFIGURE_NOTIFY" },
	{ XCB_DESTROY_NOTIFY, handle_destroy_notify, "DESTROY_NOTIFY" },
	{ XCB_REPARENT_NOTIFY, handle_reparent_notify, "REPARENT_NOTIFY" },
};

const char *
to_event_name(uint8_t event)
{
	size_t i;
	for (i = 0; i < ARRAY_LENGTH(events); i++)
		if (events[i].response_type == event)
			return events[i].name;

	return "(unknown event)";
}

/**
 * Tells the X server to display the window. Call handle_events_x11() after
 * calling this function to wait for events to be delivered. Note there's no
 * need to include additional events, they're already added.
 *
 * \sa handle_events_x11().
 *
 * \param window the window in question
 */
void
window_x11_map(struct window_x11 *window)
{
	handle_event_set_pending(window, XCB_MAP_NOTIFY, MAPPED, window->win_id);
	handle_event_set_pending(window, XCB_EXPOSE, EXPOSE, window->win_id);

	/* doing a synchronization for the frame wid helps with other potential
	 * states changes like max/fullscreen or if we try to map the window
	 * from the beginning with a max/fullscreen state rather than normal
	 * state (!max && !fullscreen)
	 */
	handle_event_set_pending(window, XCB_REPARENT_NOTIFY, REPARENT, window->win_id);

	xcb_map_window(window->conn->connection, window->win_id);
	xcb_flush(window->conn->connection);
}

/**
 * \sa window_x11_map, handle_events_x11.  Tells the X server to unmap the
 * window. Call handle_events_x11() to wait for the events to be delivered.
 *
 * \param window the window in question
 */
void
window_x11_unmap(struct window_x11 *window)
{
	handle_event_set_pending(window, XCB_UNMAP_NOTIFY, UNMAPPED, window->win_id);

	xcb_unmap_window(window->conn->connection, window->win_id);
	xcb_flush(window->conn->connection);
}

static void
window_x11_set_cursor(struct window_x11 *window, const char *cursor_name)
{
	test_assert_ptr_not_null(window);
	test_assert_ptr_null(window->ctx);

	if (xcb_cursor_context_new(window->conn->connection,
				   window->screen, &window->ctx) < 0) {
		fprintf(stderr, "Error creating context!\n");
		return;
	}

	window->cursor = xcb_cursor_load_cursor(window->ctx, cursor_name);

	xcb_change_window_attributes(window->conn->connection,
				     window->root_win_id, XCB_CW_CURSOR,
				     &window->cursor);
	xcb_flush(window->conn->connection);
}

static bool
handle_event(xcb_generic_event_t *ev, struct window_x11 *window)
{
	uint8_t event = EVENT_TYPE(ev);
	bool events_handled = false;
	size_t i;

	for (i = 0; i < ARRAY_LENGTH(events); i++) {
		if (event == events[i].response_type) {
			bool ev_cb_handled = events[i].eventcb(ev, window);
			if (!ev_cb_handled)
				return false;

			events_handled =
				handle_event_check_pending(window, ev);
		}
	}

	return events_handled;
}

/**
 * Each operation on 'window_x11' requires calling handle_events_x11() to a) flush
 * out the connection, b) poll for a xcb_generic_event_t and c) to call the
 * appropriate event handler;
 *
 * This function should never block to allow a programmatic way of applying
 * different operations/states to the window. If that happens, running it
 * under meson test will cause a test fail (with a timeout).
 *
 * Before calling this function, one *shall* use handle_event_set_pending() to
 * explicitly set which events to wait for. Not doing so will effectively
 * deadlock the test, as it will wait for pending events never being set.
 *
 * Note that all state change functions, including map and unmap, would
 * implicitly, if not otherwise stated, set the pending events to wait for.
 *
 * \sa handle_event_set_pending()
 * \param window the X11 window in question
 */
int
handle_events_x11(struct window_x11 *window)
{
	bool running = true;
	xcb_generic_event_t *ev;
	int ret = 0;

	test_assert_false(window->handle_in_progress);
	window->handle_in_progress = true;

	do {
		bool events_handled = false;

		xcb_flush(window->conn->connection);

		if (xcb_connection_has_error(window->conn->connection)) {
			fprintf(stderr, "X11 connection got interrupted\n");
			ret = -1;
			break;
		}

		ev = xcb_wait_for_event(window->conn->connection);
		if (!ev) {
			fprintf(stderr, "Error, no event received, "
					"although we requested for one!\n");
			break;
		}

		events_handled = handle_event(ev, window);

		/* signals that we've done processing all the pending events */
		if (events_handled) {
			running = false;
		}

		free(ev);
	} while (running);

	window->handle_in_progress = false;
	return ret;
}

/* Might be useful in case you'd want to receive create notify for our own window. */
void
window_x11_notify_for_root_events(struct window_x11 *window)
{
	int mask_values =
		XCB_EVENT_MASK_STRUCTURE_NOTIFY |
		XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY;

	xcb_change_window_attributes(window->conn->connection,
				     window->root_win_id,
				     XCB_CW_EVENT_MASK, &mask_values);
	xcb_flush(window->conn->connection);
}

/**
 * Sets the x11 window a property name.  Call handle_events_x11() to
 * wait for the events to be delivered.
 *
 * \param window the window in question
 * \param name the name, as string
 *
 */
void
window_x11_set_win_name(struct window_x11 *window, const char *name)
{
	struct atom_x11 *atoms = window->conn->atoms;

	handle_event_set_pending(window, XCB_PROPERTY_NOTIFY, PROPERTY_NAME, window->win_id);

	xcb_change_property(window->conn->connection, XCB_PROP_MODE_REPLACE,
			    window->win_id, atoms->net_wm_name,
			    atoms->string, 8,
			    strlen(name), name);
	xcb_flush(window->conn->connection);
}

/** Create a x11 connection.
 *
 * \sa window_get_connection() to retrieve it in the same tests to avoid
 * creating a new connection
 * \sa create_x11_window(), where you need to pass this connection_x11 object.
 *
 * \return a struct connection_x11 which defines a x11 connection.
 */
struct connection_x11 *
create_x11_connection(void)
{
	struct connection_x11 *conn;

	if (access(XSERVER_PATH, X_OK) != 0)
		return NULL;

	conn = xzalloc(sizeof(*conn));
	conn->connection = xcb_connect(NULL, NULL);
	if (!conn->connection)
		return NULL;

	conn->atoms = xzalloc(sizeof(struct atom_x11));

	/* retrieve atoms */
	x11_get_atoms(conn->connection, conn->atoms);

	return conn;
}

/** Destroys a x11 connection. Use this at the end (of the test) to destroy the
 * x11 connection.
 *
 * \param conn the x11 connection in question.
 */
void
destroy_x11_connection(struct connection_x11 *conn)
{
	xcb_disconnect(conn->connection);

	free(conn->atoms);
	free(conn);
}


/**
 * creates a X window, based on the initial supplied values. All operations
 * performed will work on this window_x11 object.
 *
 * The creation and destruction of the window_x11 object is handled implictly
 * so there's no need wait for (additional) events, like it is required for
 * all other change state operations.
 *
 * The window is not mapped/displayed so that needs to happen explictly, by
 * calling window_x11_map() and then waiting for events using
 * handle_events_x11().
 *
 * \param width initial size, width value
 * \param height initial size, height value
 * \param pos_x initial position, x value
 * \param pos_y initial position, y value
 * \param conn X11 connection
 * \param bg_color a background color
 * \param parent the window_x11 parent
 * \return a pointer to window_x11, which gets destroyed with destroy_x11_window()
 */
struct window_x11 *
create_x11_window(int width, int height, int pos_x, int pos_y,
		  struct connection_x11 *conn, pixman_color_t bg_color,
		  struct window_x11 *parent)
{
	uint32_t colorpixel = 0x0;
	uint32_t values[2];
	uint32_t mask = 0;

	xcb_colormap_t colormap;
	struct window_x11 *window;
	xcb_window_t parent_win_id;
	xcb_alloc_color_cookie_t cookie;
	xcb_alloc_color_reply_t *reply;
	xcb_void_cookie_t cookie_create;
	xcb_generic_error_t *error_create;
	const struct xcb_setup_t *xcb_setup;

	test_assert_ptr_not_null(conn);
	window = xzalloc(sizeof(*window));

	window->conn = conn;
	xcb_setup = xcb_get_setup(window->conn->connection);
	window->screen = xcb_setup_roots_iterator(xcb_setup).data;

	wl_list_init(&window->window_list);
	wl_list_init(&window->tentative_state.pending_events_list);

	window->root_win_id = window->screen->root;
	window->parent = parent;
	if (window->parent) {
		parent_win_id = window->parent->win_id;
		wl_list_insert(&parent->window_list, &window->window_link);
	} else {
		parent_win_id = window->root_win_id;
	}

	colormap = window->screen->default_colormap;
	cookie = xcb_alloc_color(window->conn->connection, colormap,
				 bg_color.red, bg_color.blue, bg_color.green);
	reply = xcb_alloc_color_reply(window->conn->connection, cookie, NULL);
	test_assert_ptr_not_null(reply);

	colorpixel = reply->pixel;
	free(reply);

	window->background = xcb_generate_id(window->conn->connection);
	mask = XCB_GC_FOREGROUND | XCB_GC_GRAPHICS_EXPOSURES;
	values[0]  = colorpixel;
	values[1] = 0;
	window->bg_color = bg_color;

	xcb_create_gc(window->conn->connection, window->background,
		      window->root_win_id, mask, values);

	/* create the window */
	window->win_id = xcb_generate_id(window->conn->connection);
	mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK;
	values[0] = colorpixel;
	values[1] = XCB_EVENT_MASK_EXPOSURE |
		    XCB_EVENT_MASK_KEY_PRESS |
		    XCB_EVENT_MASK_VISIBILITY_CHANGE |
		    XCB_EVENT_MASK_STRUCTURE_NOTIFY |
		    XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT |
		    XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY |
		    XCB_EVENT_MASK_PROPERTY_CHANGE;

	window->pos_x = pos_x;
	window->pos_y = pos_y;

	window->width = width;
	window->height = height;

	cookie_create = xcb_create_window_checked(window->conn->connection,
						  XCB_COPY_FROM_PARENT,
						  window->win_id, parent_win_id,
						  window->pos_x, window->pos_y,
						  window->width, window->height,
						  0,
						  XCB_WINDOW_CLASS_INPUT_OUTPUT,
						  window->screen->root_visual,
						  mask, values);
	error_create = xcb_request_check(window->conn->connection, cookie_create);
	test_assert_ptr_null(error_create);

	window_state_set_flag(window, CREATED);
	window_x11_set_cursor(window, "left_ptr");

	return window;
}

static void
kill_window(struct window_x11 *window)
{
	handle_event_set_pending(window, XCB_DESTROY_NOTIFY, DESTROYED, window->win_id);

	xcb_destroy_window(window->conn->connection, window->win_id);
	xcb_flush(window->conn->connection);
}

/**
 * \sa create_x11_window(). The creation and destruction of the window_x11 is
 * handled implicitly so there's no wait for (additional) events.
 *
 * This function would wait for destroy notify event and will disconnect from
 * the server. No further operation can happen on the window_x11, except
 * for destroying the x11 connection using destroy_x11_connection().
 *
 * \param window the window in question
 */
void
destroy_x11_window(struct window_x11 *window)
{
	struct window_state *wstate, *wstate_next;

	xcb_free_cursor(window->conn->connection, window->cursor);
	xcb_cursor_context_free(window->ctx);
	xcb_flush(window->conn->connection);

	kill_window(window);
	handle_events_x11(window);

	/* in case we're called before any events have been handled */
	wl_list_for_each_safe(wstate, wstate_next,
			      &window->tentative_state.pending_events_list, link)
		handle_event_remove_pending(wstate);

	free(window);
}

/**
 * Return the reply_t for an atom
 *
 * \param window the window in question
 * \param win the handle for the window; could be different from the window itself!
 * \param atom the atom in question
 *
 */
xcb_get_property_reply_t *
window_x11_dump_prop(struct window_x11 *window, xcb_drawable_t win, xcb_atom_t atom)
{
	xcb_get_property_cookie_t prop_cookie;
	xcb_get_property_reply_t *prop_reply;

	prop_cookie = xcb_get_property(window->conn->connection, 0, win, atom,
				      XCB_GET_PROPERTY_TYPE_ANY, 0, 2048);

	prop_reply = xcb_get_property_reply(window->conn->connection, prop_cookie, NULL);

	/* callers needs to free it */
	return prop_reply;
}

/** Retrieve the atoms
 *
 * \param win the window in question from which to retrieve the atoms
 *
 */
struct atom_x11 *
window_get_atoms(struct window_x11 *win)
{
	return win->conn->atoms;
}

/** Retrive the connection_x11 from the window_x11
 *
 * \param win the window in question from which to retrieve the connection
 *
 */
struct xcb_connection_t *
window_get_connection(struct window_x11 *win)
{
	return win->conn->connection;
}
