#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "nein_cursor.h"
#include <swc.h>
#include <wayland-server.h>
#include <wayland-util.h>
#include <xkbcommon/xkbcommon-keysyms.h>

#include "config.h"
#include "types.h"
#include "util.h"
#include "yawc.h"

static void focus(struct client* c);
static void apply_decor(struct client* c, bool active);
static struct client* first_client(struct screen* s);
static bool is_ws_client(const struct client* c, const struct screen* s);
static void on_screen_destroy(void* data);
static void on_win_destroy(void* data);
static void on_win_entered(void* data);
static void on_win_metadata_changed(void* data);
static void run_autostart(void);
static void setup(void);
static void setup_binds(void);
static void setup_cursor(void);
static void sync_window_visibility(void);

struct wm wm;
const struct swc_manager manager = {
	.new_screen = new_screen, .new_window = new_window, .new_device = new_device,
};
struct swc_window_handler window_handler = {
	.destroy = on_win_destroy,
	.title_changed = on_win_metadata_changed,
	.app_id_changed = on_win_metadata_changed,
	.entered = on_win_entered,
};
struct swc_screen_handler screen_handler = {
	.destroy = on_screen_destroy,
};

static void focus(struct client* c)
{
	if (wm.sel_client) {
		swc_window_set_border(
			wm.sel_client->win,
			cfg.border_col_normal, cfg.border_width,
			cfg.border_col_normal_outer, cfg.border_width_outer
		);
	}

	if (c) {
		swc_window_set_border(
			c->win,
			cfg.border_col_active, cfg.border_width,
			cfg.border_col_active_outer, cfg.border_width_outer
		);
	}

	swc_window_focus(c ? c->win : NULL);
	wm.sel_client = c;
}

static void apply_decor(struct client* c, bool active)
{
	(void)active;

	if (!c)
		return;
	swc_window_set_decor(c->win, NULL);
}

static struct client* first_client(struct screen* s)
{
	struct client* c;

	wl_list_for_each(c, &wm.clients, link) {
		if (is_ws_client(c, s))
			return c;
	}

	return NULL;
}

static bool is_ws_client(const struct client* c, const struct screen* s)
{
	return c && c->ws == wm.ws && (!s || c->scr == s);
}

static void on_screen_destroy(void* data)
{
	struct screen* s = data;

	if (!s)
		return;

	wl_list_remove(&s->link);

	if (wm.sel_screen == s) {
		if (wl_list_empty(&wm.screens))
			wm.sel_screen = NULL;
		else
			wm.sel_screen = wl_container_of(wm.screens.next, wm.sel_screen, link);
	}

	free(s);
}

static void on_win_destroy(void* data)
{
	struct client* c = data;
	struct client* next;

	if (!c)
		return;

	if (wm.grab.active && wm.grab.c == c) {
		wm.grab.active = false;
		wm.grab.c = NULL;
	}

	if (wm.sel_client == c) {
		wm.sel_client = NULL;
	}

	wl_list_remove(&c->link);
	free(c);

	next = first_client(wm.sel_screen);
	if (!next)
		next = first_client(NULL);
	focus(next);
}

static void on_win_entered(void* data)
{
	if (wm.grab.active)
		return;

	struct client* c = data;
	if (!is_ws_client(c, NULL))
		return;

	focus(c);
}

static void on_win_metadata_changed(void* data)
{
	struct client* c = data;

	if (!c)
		return;

	apply_decor(c, c == wm.sel_client);
}
static void setup_cursor(void)
{
	const struct nein_cursor_meta *arrow = &nein_cursor_metadata[NEIN_CURSOR_WHITEARROW];

	swc_set_cursor_mode(SWC_CURSOR_MODE_COMPOSITOR);
	swc_set_cursor_image(SWC_CURSOR_DEFAULT,
		&nein_cursor_data[arrow->offset],
		arrow->width, arrow->height,
		arrow->hotspot_x, arrow->hotspot_y);
}
static void setup(void)
{
	/* display */
	wm.dpy = wl_display_create();
	if (!wm.dpy)
		die(EXIT_FAILURE, "wl_display_create failed");

	/* variables */
	wl_list_init(&wm.screens);
	wl_list_init(&wm.clients);
	wm.sel_client = NULL;
	wm.sel_screen = NULL;
	wm.grab.active = false;
	wm.grab.resize = false;
	wm.grab.c = NULL;
	wm.ws = 1;

	/* event loop */
	wm.ev_loop = wl_display_get_event_loop(wm.dpy);
	if (!swc_initialize(wm.dpy, wm.ev_loop, &manager))
		die(EXIT_FAILURE, "swc_initialize failed\n");

	swc_repeat_rate = cfg.repeat_rate;
	swc_repeat_delay = cfg.repeat_delay;
	 setup_cursor();
	setup_binds();

	/* display socket */
	const char* sock;
	sock = wl_display_add_socket_auto(wm.dpy);
	if (!sock)
		die(EXIT_FAILURE, "wl_display_add_socket_auto failed\n");
	setenv("WAYLAND_DISPLAY", sock, 1);
	_log(stderr, "WAYLAND_DISPLAY=%s\n", sock);

	run_autostart();

	/* signals */
	signal(SIGINT,  sig_handler);
	signal(SIGTERM, sig_handler);
	signal(SIGQUIT, sig_handler);
}

static void run_autostart(void)
{
	const char* shell = getenv("SHELL");

	if (!shell || !shell[0])
		shell = "/bin/sh";

	for (size_t i = 0; i < LENGTH(autostart); i++) {
		const char* cmd = autostart[i];
		pid_t pid;

		if (!cmd || !cmd[0])
			continue;

		pid = fork();
		if (pid < 0) {
			_log(stderr, "autostart fork failed for: %s\n", cmd);
			continue;
		}

		if (pid == 0) {
			setsid();
			execl(shell, shell, "-c", cmd, (char*)NULL);
			_exit(127);
		}
	}
}

static void setup_binds(void)
{
	for (size_t i = 0; i < LENGTH(binds); i++) {
		const struct bind* b = &binds[i];
		swc_add_binding(b->type, b->mods, b->ksym, b->fn, (void*)&b->arg);
	}
}

static void sync_window_visibility(void)
{
	struct client* c;

	wl_list_for_each(c, &wm.clients, link) {
		if (c->ws == wm.ws)
			swc_window_show(c->win);
		else
			swc_window_hide(c->win);
	}
}

void focus_next(void* data, uint32_t time, uint32_t value, uint32_t state)
{
	(void)data;
	(void)time;
	(void)value;

	struct client* c = NULL;

	if (state != WL_KEYBOARD_KEY_STATE_PRESSED)
		return;

	if (wl_list_empty(&wm.clients))
		return;

	if (!wm.sel_client || !is_ws_client(wm.sel_client, wm.sel_screen)) {
		c = first_client(wm.sel_screen);
		if (!c)
			c = first_client(NULL);
		focus(c);
		return;
	}

	struct wl_list* start = wm.sel_client->link.next;
	struct wl_list* it = start;

	do {
		if (it == &wm.clients)
			it = wm.clients.next;
		if (it == &wm.clients)
			break;

		c = wl_container_of(it, c, link);
		if (is_ws_client(c, wm.sel_screen)) {
			focus(c);
			return;
		}
		it = it->next;
	} while (it != start);

	c = first_client(wm.sel_screen);
	if (!c)
		c = first_client(NULL);
	focus(c);
}

void focus_prev(void* data, uint32_t time, uint32_t value, uint32_t state)
{
	struct client* c = NULL;

	(void)data;
	(void)time;
	(void)value;

	if (state != WL_KEYBOARD_KEY_STATE_PRESSED)
		return;

	if (wl_list_empty(&wm.clients))
		return;

	if (!wm.sel_client || !is_ws_client(wm.sel_client, wm.sel_screen)) {
		c = first_client(wm.sel_screen);
		if (!c)
			c = first_client(NULL);
		focus(c);
		return;
	}

	struct wl_list* start = wm.sel_client->link.prev;
	struct wl_list* it = start;

	do {
		if (it == &wm.clients)
			it = wm.clients.prev;
		if (it == &wm.clients)
			break;

		c = wl_container_of(it, c, link);
		if (is_ws_client(c, wm.sel_screen)) {
			focus(c);
			return;
		}
		it = it->prev;
	} while (it != start);

	c = first_client(wm.sel_screen);
	if (!c)
		c = first_client(NULL);
	focus(c);
}

void kill_sel(void* data, uint32_t time, uint32_t value, uint32_t state)
{
	(void)data;
	(void)time;
	(void)value;

	if (state != WL_KEYBOARD_KEY_STATE_PRESSED)
		return;

	if (!wm.sel_client)
		return;

	swc_window_close(wm.sel_client->win);
}

void fullscreen(void* data, uint32_t time, uint32_t value, uint32_t state)
{
	struct swc_rectangle geom;

	(void)data;
	(void)time;
	(void)value;

	if (state != WL_KEYBOARD_KEY_STATE_PRESSED)
		return;

	if (!wm.sel_client)
		return;

	if (wm.sel_client->fullscreen) {
		wm.sel_client->fullscreen = false;
		swc_window_set_stacked(wm.sel_client->win);
		apply_decor(wm.sel_client, true);

		if (wm.sel_client->w > 0 && wm.sel_client->h > 0) {
			geom.x = wm.sel_client->x;
			geom.y = wm.sel_client->y;
			geom.width = wm.sel_client->w;
			geom.height = wm.sel_client->h;
			swc_window_set_geometry(wm.sel_client->win, &geom);
		}
		return;
	}

	if (!wm.sel_client->scr || !wm.sel_client->scr->scr)
		return;

	if (swc_window_get_geometry(wm.sel_client->win, &geom)) {
		wm.sel_client->x = geom.x;
		wm.sel_client->y = geom.y;
		wm.sel_client->w = geom.width;
		wm.sel_client->h = geom.height;
	}

	wm.sel_client->fullscreen = true;
	apply_decor(wm.sel_client, true);
	swc_window_set_fullscreen(wm.sel_client->win, wm.sel_client->scr->scr);
}

void mouse_move(void* data, uint32_t time, uint32_t value, uint32_t state)
{
	(void)data;
	(void)time;
	(void)value;

	if (state == WL_POINTER_BUTTON_STATE_PRESSED) {
		if (!wm.sel_client)
			return;

		if (!wm.sel_client->floating) {
			wm.sel_client->floating = true;
			swc_window_set_stacked(wm.sel_client->win);
		}

		wm.grab.active = true;
		wm.grab.resize = false;
		wm.grab.c = wm.sel_client;

		swc_window_begin_move(wm.grab.c->win);
	}
	else {
		if (!wm.grab.active || wm.grab.resize || !wm.grab.c)
			return;

		swc_window_end_move(wm.grab.c->win);

		wm.grab.active = false;
		wm.grab.c = NULL;
	}
}

void mouse_resize(void* data, uint32_t time, uint32_t value, uint32_t state)
{
	(void)data;
	(void)time;
	(void)value;

	if (state == WL_POINTER_BUTTON_STATE_PRESSED) {
		if (!wm.sel_client)
			return;

		if (!wm.sel_client->floating) {
			wm.sel_client->floating = true;
			swc_window_set_stacked(wm.sel_client->win);
		}

		wm.grab.active = true;
		wm.grab.resize = true;
		wm.grab.c = wm.sel_client;

		swc_window_begin_resize(
			wm.grab.c->win,
			SWC_WINDOW_EDGE_RIGHT | SWC_WINDOW_EDGE_BOTTOM
		);
	}
	else {
		if (!wm.grab.active || !wm.grab.resize || !wm.grab.c)
			return;

		swc_window_end_resize(wm.grab.c->win);

		wm.grab.active = false;
		wm.grab.c = NULL;
	}
}

void new_screen(struct swc_screen* scr)
{
	struct screen* s;

	s = malloc(sizeof(*s));
	if (!s)
		die(EXIT_FAILURE, "new screen calloc failed");

	s->scr = scr;

	s->x = 0;
	s->y = 0;
	s->w = 0;
	s->h = 0;

	wl_list_insert(&wm.screens, &s->link);

	if (!wm.sel_screen)
		wm.sel_screen = s;

	swc_screen_set_handler(scr, &screen_handler, s);

	_log(stderr, "new_screen=%p\n", (void*)scr);
}

void new_window(struct swc_window* win)
{
	struct client* c;

	c = malloc(sizeof(*c));
	if (!c)
		die(EXIT_FAILURE, "malloc client failed");

	win->motion_throttle_ms = 1000 / cfg.motion_throttle_hz;
	win->min_width = 1;
	win->min_height = 1;
	win->max_width = 0;
	win->max_height = 0;

	c->win = win;
	c->scr = wm.sel_screen;
	c->mapped = 0;
	c->floating = true;
	c->fullscreen = 0;
	c->ws = wm.ws;
	c->x = 0;
	c->y = 0;
	c->w = 0;
	c->h = 0;

	wl_list_insert(&wm.clients, &c->link);
	swc_window_set_handler(win, &window_handler, c);
	swc_window_set_stacked(win);
	apply_decor(c, false);
	{
		/* swc reports cursor coordinates in wl_fixed_t (24.8) units */
		int32_t cx_fixed = 0;
		int32_t cy_fixed = 0;

		if (swc_cursor_position(&cx_fixed, &cy_fixed)) {
			int32_t cx = cx_fixed / 256;
			int32_t cy = cy_fixed / 256;

			swc_window_set_position(
				win,
				cx - (int32_t)(cfg.spawn_w / 2),
				cy - (int32_t)(cfg.spawn_h / 2)
			);
		}
	}
	swc_window_show(win);
	focus(c);

	_log(stderr, "new_window=%p\n", (void*)win);
}

void new_device(struct libinput_device* dev)
{
	(void)dev;
}

void quit(void* data, uint32_t time, uint32_t value, uint32_t state)
{
	(void)data;
	(void)time;
	(void)value;
	(void)state;

	wl_display_terminate(wm.dpy);
}

void spawn(void* data, uint32_t time, uint32_t value, uint32_t state)
{
	union arg* a = data;
	char* const* cmd = (char* const*)a->v;

	(void)time;
	(void)value;

	if (state != WL_KEYBOARD_KEY_STATE_PRESSED)
		return;

	if (fork() == 0) {
		execvp(cmd[0], cmd);
		_exit(127);
	}
}

void workspace_goto(void* data, uint32_t time, uint32_t value, uint32_t state)
{
	union arg* a = data;
	struct client* c;

	(void)time;
	(void)value;

	if (state != WL_KEYBOARD_KEY_STATE_PRESSED)
		return;

	if (a->ui < 1 || a->ui > 9 || a->ui == wm.ws)
		return;

	wm.ws = a->ui;
	sync_window_visibility();

	c = first_client(wm.sel_screen);
	if (!c)
		c = first_client(NULL);
	focus(c);
}

void workspace_moveto(void* data, uint32_t time, uint32_t value, uint32_t state)
{
	union arg* a = data;
	struct client* c;
	struct client* next;

	(void)time;
	(void)value;

	if (state != WL_KEYBOARD_KEY_STATE_PRESSED)
		return;

	if (!wm.sel_client)
		return;

	if (a->ui < 1 || a->ui > 9)
		return;

	c = wm.sel_client;
	if (c->ws == a->ui)
		return;

	c->ws = a->ui;
	if (c->ws == wm.ws)
		swc_window_show(c->win);
	else
		swc_window_hide(c->win);

	next = first_client(wm.sel_screen);
	if (!next)
		next = first_client(NULL);
	focus(next);
}

int main(void)
{
	setup();
	wl_display_run(wm.dpy);
	swc_finalize();
	wl_display_destroy(wm.dpy);
	return EXIT_SUCCESS;
}
