#include <assert.h>
#include <stdlib.h>
#include "types/wlr_xdg_shell.h"

#define WM_BASE_VERSION 6

static const struct xdg_wm_base_interface xdg_shell_impl;

static struct wlr_xdg_client *xdg_client_from_resource(
		struct wl_resource *resource) {
	assert(wl_resource_instance_of(resource, &xdg_wm_base_interface,
		&xdg_shell_impl));
	return wl_resource_get_user_data(resource);
}

static void xdg_shell_handle_create_positioner(struct wl_client *wl_client,
		struct wl_resource *resource, uint32_t id) {
	struct wlr_xdg_client *client =
		xdg_client_from_resource(resource);
	create_xdg_positioner(client, id);
}

static void xdg_shell_handle_get_xdg_surface(struct wl_client *wl_client,
		struct wl_resource *client_resource, uint32_t id,
		struct wl_resource *surface_resource) {
	struct wlr_xdg_client *client =
		xdg_client_from_resource(client_resource);
	struct wlr_surface *surface = wlr_surface_from_resource(surface_resource);
	create_xdg_surface(client, surface, id);
}

static void xdg_shell_handle_pong(struct wl_client *wl_client,
		struct wl_resource *resource, uint32_t serial) {
	struct wlr_xdg_client *client = xdg_client_from_resource(resource);

	if (client->ping_serial != serial) {
		return;
	}

	wl_event_source_timer_update(client->ping_timer, 0);
	client->ping_serial = 0;
}

static void xdg_shell_handle_destroy(struct wl_client *wl_client,
		struct wl_resource *resource) {
	struct wlr_xdg_client *client = xdg_client_from_resource(resource);

	if (!wl_list_empty(&client->surfaces)) {
		wl_resource_post_error(client->resource,
			XDG_WM_BASE_ERROR_DEFUNCT_SURFACES,
			"xdg_wm_base was destroyed before children");
		return;
	}

	wl_resource_destroy(resource);
}

static const struct xdg_wm_base_interface xdg_shell_impl = {
	.destroy = xdg_shell_handle_destroy,
	.create_positioner = xdg_shell_handle_create_positioner,
	.get_xdg_surface = xdg_shell_handle_get_xdg_surface,
	.pong = xdg_shell_handle_pong,
};

static void xdg_client_handle_resource_destroy(struct wl_resource *resource) {
	struct wlr_xdg_client *client = xdg_client_from_resource(resource);

	struct wlr_xdg_surface *surface, *tmp = NULL;
	wl_list_for_each_safe(surface, tmp, &client->surfaces, link) {
		destroy_xdg_surface(surface);
	}

	if (client->ping_timer != NULL) {
		wl_event_source_remove(client->ping_timer);
	}

	wl_list_remove(&client->link);
	free(client);
}

static int xdg_client_ping_timeout(void *user_data) {
	struct wlr_xdg_client *client = user_data;

	struct wlr_xdg_surface *surface;
	wl_list_for_each(surface, &client->surfaces, link) {
		wl_signal_emit_mutable(&surface->events.ping_timeout, NULL);
	}

	client->ping_serial = 0;
	return 1;
}

static void xdg_shell_bind(struct wl_client *wl_client, void *data,
		uint32_t version, uint32_t id) {
	struct wlr_xdg_shell *xdg_shell = data;

	struct wlr_xdg_client *client = calloc(1, sizeof(*client));
	if (client == NULL) {
		wl_client_post_no_memory(wl_client);
		return;
	}

	wl_list_init(&client->surfaces);

	client->resource =
		wl_resource_create(wl_client, &xdg_wm_base_interface, version, id);
	if (client->resource == NULL) {
		free(client);
		wl_client_post_no_memory(wl_client);
		return;
	}
	client->client = wl_client;
	client->shell = xdg_shell;

	wl_resource_set_implementation(client->resource, &xdg_shell_impl, client,
		xdg_client_handle_resource_destroy);
	wl_list_insert(&xdg_shell->clients, &client->link);

	struct wl_display *display = wl_client_get_display(client->client);
	struct wl_event_loop *loop = wl_display_get_event_loop(display);
	client->ping_timer = wl_event_loop_add_timer(loop,
		xdg_client_ping_timeout, client);
	if (client->ping_timer == NULL) {
		wl_client_post_no_memory(client->client);
	}
}

static void handle_display_destroy(struct wl_listener *listener, void *data) {
	struct wlr_xdg_shell *xdg_shell =
		wl_container_of(listener, xdg_shell, display_destroy);
	wl_signal_emit_mutable(&xdg_shell->events.destroy, xdg_shell);
	wl_list_remove(&xdg_shell->display_destroy.link);
	wl_global_destroy(xdg_shell->global);
	free(xdg_shell);
}

struct wlr_xdg_shell *wlr_xdg_shell_create(struct wl_display *display,
		uint32_t version) {
	assert(version <= WM_BASE_VERSION);

	struct wlr_xdg_shell *xdg_shell = calloc(1, sizeof(*xdg_shell));
	if (!xdg_shell) {
		return NULL;
	}

	xdg_shell->version = version;
	xdg_shell->ping_timeout = 10000;

	wl_list_init(&xdg_shell->clients);
	wl_list_init(&xdg_shell->popup_grabs);

	struct wl_global *global = wl_global_create(display,
		&xdg_wm_base_interface, version, xdg_shell, xdg_shell_bind);
	if (!global) {
		free(xdg_shell);
		return NULL;
	}
	xdg_shell->global = global;

	wl_signal_init(&xdg_shell->events.new_surface);
	wl_signal_init(&xdg_shell->events.new_toplevel);
	wl_signal_init(&xdg_shell->events.new_popup);
	wl_signal_init(&xdg_shell->events.destroy);

	xdg_shell->display_destroy.notify = handle_display_destroy;
	wl_display_add_destroy_listener(display, &xdg_shell->display_destroy);

	return xdg_shell;
}
