#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdarg.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>

#include <wayland-client.h>
#include <wayland-util.h>

//#define RAND_DESYNC
//#define RAND_BELOW

struct surface {
	struct context *context;
	struct wl_surface *surface;
	struct wl_subsurface *subsurface;
	struct wl_buffer *buffer;

	int width;
	int height;
	int stride;
	int fd;
	uint32_t *data;
	uint32_t color;

	struct surface *parent;
	struct wl_list children;
	struct wl_list link;
};

struct context {
	struct wl_display *display;
	struct wl_registry *registry;
	struct wl_compositor *compositor;
	struct wl_subcompositor *subcompositor;
	struct wl_shell *shell;
	struct wl_shm *shm;
	struct surface *root_surface;
	struct wl_shell_surface *shell_surface;
};

static void
die(const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	vfprintf(stderr, fmt, ap);
	fprintf(stderr, "\n");
	va_end(ap);
	exit(EXIT_FAILURE);
}

static int
surface_alloc_data(struct surface *surface)
{
	char filename[] = "/tmp/wayland-shm-XXXXXX";
	uint32_t *data;
	int stride;
	int size;
	int fd;
	int i;

	fd = mkstemp(filename);
	if (fd < 0)
		die("failed to create file for SHM buffer: %m");

	unlink(filename);

	stride = surface->width * 4;
	size = surface->height * stride;

	if (fallocate(fd, 0, 0, size) < 0)
		die("failed to allocate %d bytes for SHM buffer: %m", size);

	data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	if (data == MAP_FAILED)
		die("failed to map SHM buffer data: %m");

	for (i = 0; i < surface->width * surface->height; i++)
		data[i] = surface->color;

	surface->fd = fd;
	surface->data = data;
	surface->stride = stride;

	return 0;
}

static struct surface *
surface_create(struct context *context, struct surface *parent,
	       int width, int height, int x, int y, uint32_t color)
{
	struct surface *surface;
	struct wl_shm_pool *pool;

	surface = calloc(1, sizeof *surface);
	if (!surface)
		return NULL;

	surface->fd = -1;
	surface->context = context;
	surface->width = width;
	surface->height = height;
	surface->color = color;
	surface->parent = parent;

	wl_list_init(&surface->children);
	wl_list_init(&surface->link);

	if (surface_alloc_data(surface)) {
		free(surface);
		return NULL;
	}

	pool = wl_shm_create_pool(context->shm, surface->fd,
				  surface->stride * surface->height);

	surface->buffer = wl_shm_pool_create_buffer(pool, 0,
						    surface->width,
						    surface->height,
						    surface->stride,
						    WL_SHM_FORMAT_ARGB8888);

	wl_shm_pool_destroy(pool);

	surface->surface = wl_compositor_create_surface(context->compositor);
	if (surface->parent) {
		surface->subsurface = wl_subcompositor_get_subsurface(
			context->subcompositor, surface->surface,
			parent->surface);

		wl_subsurface_set_position(surface->subsurface, x, y);

#ifdef RAND_DESYNC
		if (rand() % 2)
			wl_subsurface_set_desync(surface->subsurface);
#endif

#ifdef RAND_BELOW
		if (rand() % 2)
			wl_subsurface_place_below(surface->subsurface,
						  parent->surface);
#endif

		wl_list_insert(&parent->children, &surface->link);
	}

	wl_surface_attach(surface->surface, surface->buffer, 0, 0);
	wl_surface_damage(surface->surface, 0, 0,
			  surface->width, surface->height);

	if (surface->parent)
		wl_surface_commit(surface->surface);

	return surface;
}

static void
create_children(struct surface *surface)
{
	int lines, cols;
	struct surface *sub;
	int width, height;
	int i, j;

	lines = cols = 2;

	width = (surface->width - (cols + 1) * 4) / cols;
	height = (surface->height - (lines + 1) * 4) / lines;

	if (width < 4 || height < 4)
		return;

	for (i = 0; i < cols; i++) {
		for (j = 0; j < lines; j++) {
			sub = surface_create(surface->context, surface,
					     width, height,
					     4 + i * (width + 4),
					     4 + j * (height + 4),
					     (rand() & 0xffffff) | 0x80000000);
			create_children(sub);
		}
	}
}

static void
registry_handle_global(void *data, struct wl_registry *registry,
		       uint32_t id, const char *interface, uint32_t version)
{
	struct context *ctx = data;

	if (!strcmp(interface, "wl_compositor")) {
		ctx->compositor =
			wl_registry_bind(registry, id,
					 &wl_compositor_interface, 1);
	} else if (!strcmp(interface, "wl_subcompositor")) {
		ctx->subcompositor =
			wl_registry_bind(registry, id,
					 &wl_subcompositor_interface, 1);
	} else if (!strcmp(interface, "wl_shell")) {
		ctx->shell =
			wl_registry_bind(registry, id, &wl_shell_interface, 1);
	} else if (!strcmp(interface, "wl_shm")) {
		ctx->shm = wl_registry_bind(registry, id, &wl_shm_interface, 1);
	}
}

static void
registry_handle_global_remove(void *data, struct wl_registry *registry,
			      uint32_t id)
{
}

static const struct wl_registry_listener registry_listener = {
	registry_handle_global,
	registry_handle_global_remove
};

static struct context *
context_init(void)
{
	struct context *ctx;
	struct surface *root;

	ctx = calloc(1, sizeof (*ctx));
	ctx->display = wl_display_connect(NULL);
	if (!ctx->display)
		die("failed to init wayland display");

	ctx->registry = wl_display_get_registry(ctx->display);
	wl_registry_add_listener(ctx->registry, &registry_listener, ctx);
	wl_display_roundtrip(ctx->display);

	assert(ctx->compositor != NULL);
	assert(ctx->subcompositor != NULL);
	assert(ctx->shell != NULL);

	root = surface_create(ctx, NULL, 420, 420, 0, 0, 0xff000000);
	if (!root)
		die("failed to create root surface");

	ctx->root_surface = root;

	ctx->shell_surface = wl_shell_get_shell_surface(ctx->shell,
							root->surface);
	wl_shell_surface_set_toplevel(ctx->shell_surface);

	create_children(root);
	wl_surface_commit(root->surface);

	return ctx;
}

static void
context_destroy(struct context *ctx)
{
	wl_display_disconnect(ctx->display);

	free(ctx);
}

static void
seed(void)
{
	int fd = open("/dev/urandom", O_RDONLY);
	unsigned int s;

	if (fd < 0)
		return;

	read(fd, &s, sizeof (s));
	srand(s);
	close(fd);
}

int main(int argc, char *argv[])
{
	struct context *ctx;

	seed();

	ctx = context_init();

	while (wl_display_dispatch(ctx->display) >= 0)
		;

	context_destroy(ctx);

	return 0;
}
