/**
 * @file apps/panel.c
 * @brief Panel with widgets. Main desktop interface.
 *
 * Provides the panel shown at the top of the screen, which
 * presents application windows, useful widgets, and a menu
 * for launching new apps.
 *
 * Also provides Alt-Tab app switching and a few other goodies.
 *
 * @copyright
 * This file is part of ToaruOS and is released under the terms
 * of the NCSA / University of Illinois License - see LICENSE.md
 * Copyright (C) 2013-2021 K. Lange
 */
#include <stdlib.h>
#include <assert.h>
#include <limits.h>
#include <math.h>
#include <time.h>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <sys/fswait.h>
#include <sys/shm.h>

/* auto-dep: export-dynamic */
#include <dlfcn.h>

#include <toaru/yutani.h>
#include <toaru/yutani-internal.h>
#include <toaru/graphics.h>
#include <toaru/hashmap.h>
#include <toaru/icon_cache.h>
#include <toaru/menu.h>
#include <toaru/text.h>

/* Several theming defines are in here */
#include <toaru/panel.h>

/* These are local to the core panel, so we don't need to put them in the header */
#define ALTTAB_WIDTH  250
#define ALTTAB_HEIGHT 200
#define ALTTAB_BACKGROUND premultiply(rgba(0,0,0,150))
#define ALTTAB_OFFSET 10
#define ALTTAB_WIN_SIZE 140

#define ALTF2_WIDTH 400
#define ALTF2_HEIGHT 200

static gfx_context_t * ctx = NULL;
static yutani_window_t * panel = NULL;

static gfx_context_t * actx = NULL;
static yutani_window_t * alttab = NULL;

static gfx_context_t * a2ctx = NULL;
static yutani_window_t * alt_f2 = NULL;

static size_t bg_size;
static char * bg_blob;

/* External interface for widgets */
list_t * window_list = NULL;
yutani_t * yctx;
int width;
int height;

struct TT_Font * font = NULL;
struct TT_Font * font_bold = NULL;
struct TT_Font * font_mono = NULL;
struct TT_Font * font_mono_bold = NULL;

list_t * widgets_enabled = NULL;

/* Windows, indexed by z-order */
struct window_ad * ads_by_z[MAX_WINDOW_COUNT+1] = {NULL};

int focused_app = -1;
int active_window = -1;

static int was_tabbing = 0;
static int new_focused = -1;

static void widgets_layout(void);

/**
 * Clip text and add ellipsis to fit a specified display width.
 */
char * ellipsify(char * input, int font_size, struct TT_Font * font, int max_width, int * out_width) {
	int len = strlen(input);
	char * out = malloc(len + 4);
	memcpy(out, input, len + 1);
	int width;
	tt_set_size(font, font_size);
	while ((width = tt_string_width(font, out)) > max_width) {
		len--;
		out[len+0] = '.';
		out[len+1] = '.';
		out[len+2] = '.';
		out[len+3] = '\0';
	}

	if (out_width) *out_width = width;

	return out;
}

static int _close_enough(struct yutani_msg_window_mouse_event * me) {
	if (me->command == YUTANI_MOUSE_EVENT_RAISE && sqrt(pow(me->new_x - me->old_x, 2) + pow(me->new_y - me->old_y, 2)) < 10) {
		return 1;
	}
	return 0;
}

static int center_x(int x) {
	return (width - x) / 2;
}

static int center_y(int y) {
	return (height - y) / 2;
}

static int center_x_a(int x) {
	return (alttab->width - x) / 2;
}

static int center_x_a2(int x) {
	return (ALTF2_WIDTH - x) / 2;
}

static volatile int _continue = 1;

static void toggle_hide_panel(void) {
	static int panel_hidden = 0;

	if (panel_hidden) {
		/* Unhide the panel */
		for (int i = PANEL_HEIGHT-1; i >= 0; i--) {
			yutani_window_move(yctx, panel, 0, -i);
			usleep(3000);
		}
		panel_hidden = 0;
	} else {
		/* Hide the panel */
		for (int i = 1; i <= PANEL_HEIGHT-1; i++) {
			yutani_window_move(yctx, panel, 0, -i);
			usleep(3000);
		}
		panel_hidden = 1;
	}
}

/* Handle SIGINT by telling other threads (clock) to shut down */
static void sig_int(int sig) {
	printf("Received shutdown signal in panel!\n");
	_continue = 0;
	signal(SIGINT, sig_int);
}

void launch_application(char * app) {
	if (!fork()) {
		printf("Starting %s\n", app);
		char * args[] = {"/bin/sh", "-c", app, NULL};
		execvp(args[0], args);
		exit(1);
	}
}

/* Callback for mouse events */
static void panel_check_click(struct yutani_msg_window_mouse_event * evt) {
	static struct PanelWidget * old_target = NULL;

	if (evt->wid == panel->wid) {

		/* Figure out what widget this belongs to */
		struct PanelWidget * target = NULL;
		if (evt->new_y >= Y_PAD && evt->new_y < PANEL_HEIGHT - Y_PAD) {
			foreach(widget_node, widgets_enabled) {
				struct PanelWidget * widget = widget_node->value;
				if (evt->new_x >= widget->left && evt->new_x < widget->left + widget->width) {
					target = widget;
					break;
				}
			}
		}

		int needs_redraw = 0;

		if (evt->command == YUTANI_MOUSE_EVENT_CLICK || _close_enough(evt)) {
			if (target) needs_redraw |= target->click(target, evt);
		} else if (evt->buttons & YUTANI_MOUSE_BUTTON_RIGHT) {
			if (target) needs_redraw |= target->right_click(target, evt);
		} else if (evt->command == YUTANI_MOUSE_EVENT_MOVE || evt->command == YUTANI_MOUSE_EVENT_ENTER) {
			if (old_target && target != old_target) needs_redraw |= old_target->leave(old_target, evt);
			if (target && target != old_target) needs_redraw |= target->enter(target, evt);
			if (target) needs_redraw |= target->move(target, evt);
			old_target = target;
		} else if (evt->command == YUTANI_MOUSE_EVENT_LEAVE) {
			if (old_target) needs_redraw |= old_target->leave(old_target, evt);
			old_target = NULL;
		}

		if (needs_redraw) redraw();
	}
}

static char altf2_buffer[1024] = {0};
static unsigned int altf2_collected = 0;

static void close_altf2(void) {
	free(a2ctx->backbuffer);
	free(a2ctx);

	altf2_buffer[0] = 0;
	altf2_collected = 0;

	yutani_close(yctx, alt_f2);
	alt_f2 = NULL;
}

static void redraw_altf2(void) {
	draw_fill(a2ctx, 0);
	draw_rounded_rectangle(a2ctx,0,0, ALTF2_WIDTH, ALTF2_HEIGHT, 11, premultiply(rgba(120,120,120,150)));
	draw_rounded_rectangle(a2ctx,1,1, ALTF2_WIDTH-2, ALTF2_HEIGHT-2, 10, ALTTAB_BACKGROUND);

	tt_set_size(font, 20);
	int t = tt_string_width(font, altf2_buffer);
	tt_draw_string(a2ctx, font, center_x_a2(t), 80, altf2_buffer, rgb(255,255,255));

	flip(a2ctx);
	yutani_flip(yctx, alt_f2);
}

static void redraw_alttab(void) {
	if (!actx) return;
	while (new_focused > -1 && !ads_by_z[new_focused]) {
		new_focused--;
	}
	if (new_focused == -1) {
		/* Stop tabbing */
		was_tabbing = 0;
		free(actx->backbuffer);
		free(actx);
		actx = NULL;
		yutani_close(yctx, alttab);
		return;
	}

	/* How many windows do we have? */
	unsigned int window_count = 0;
	while (ads_by_z[window_count]) window_count++;

#define ALTTAB_COLUMNS 5

	/* How many rows should that be? */
	int rows = (window_count - 1) / ALTTAB_COLUMNS + 1;

	/* How many columns? */
	int columns = (rows == 1) ? window_count : ALTTAB_COLUMNS;

	/* How much padding on the last row? */
	int last_row = (window_count % columns) ? ((ALTTAB_WIN_SIZE + 20) * (columns - (window_count % columns))) / 2  : 0;

	/* Is the window the right size? */
	unsigned int expected_width = columns * (ALTTAB_WIN_SIZE + 20) + 40;
	unsigned int expected_height = rows * (ALTTAB_WIN_SIZE + 20) + 60;

	if (alttab->width != expected_width || alttab->height != expected_height) {
		yutani_window_resize(yctx, alttab, expected_width, expected_height);
		return;
	}

	/* Draw the background, right now just a dark semi-transparent box */
	draw_fill(actx, 0);
	draw_rounded_rectangle(actx,0,0, alttab->width, alttab->height, 11, premultiply(rgba(120,120,120,150)));
	draw_rounded_rectangle(actx,1,1, alttab->width-2, alttab->height-2, 10, ALTTAB_BACKGROUND);

	for (unsigned int i = 0; i < window_count; ++i) {
		if (!ads_by_z[i]) continue;
		struct window_ad * ad = ads_by_z[i];

		/* Figure out grid alignment for this element */
		int pos_x = ((window_count - i - 1) % ALTTAB_COLUMNS) * (ALTTAB_WIN_SIZE + 20) + 20;
		int pos_y = ((window_count - i - 1) / ALTTAB_COLUMNS) * (ALTTAB_WIN_SIZE + 20) + 20;

		if ((window_count - i - 1) / ALTTAB_COLUMNS == (unsigned int)rows - 1) {
			pos_x += last_row;
		}

		if (i == (unsigned int)new_focused) {
			draw_rounded_rectangle(actx, pos_x, pos_y, ALTTAB_WIN_SIZE + 20, ALTTAB_WIN_SIZE + 20, 7, premultiply(rgba(170,170,170,150)));
		}

		/* try very hard to get a window texture */
		char key[1024];
		YUTANI_SHMKEY_EXP(yctx->server_ident, key, 1024, ad->bufid);
		size_t size;
		uint32_t * buf =  shm_obtain(key, &size);

		if (buf) {
			sprite_t tmp;
			tmp.width = ad->width;
			tmp.height = ad->height;
			tmp.bitmap = buf;

			int ox = 0;
			int oy = 0;
			int sw, sh;
			if (tmp.width > tmp.height) {
				sw = ALTTAB_WIN_SIZE;
				sh = tmp.height * ALTTAB_WIN_SIZE / tmp.width;
				oy = (ALTTAB_WIN_SIZE - sh) / 2;
			} else {
				sh = ALTTAB_WIN_SIZE;
				sw = tmp.width * ALTTAB_WIN_SIZE / tmp.height;
				ox = (ALTTAB_WIN_SIZE - sw) / 2;
			}
			draw_sprite_scaled(actx, &tmp,
				pos_x + ox + 10,
				pos_y + oy + 10,
				sw, sh);

			shm_release(key);

			sprite_t * icon = icon_get_48(ad->icon);
			draw_sprite(actx, icon, pos_x + 10 + ALTTAB_WIN_SIZE - 50, pos_y + 10 + ALTTAB_WIN_SIZE - 50);
		} else {
			sprite_t * icon = icon_get_48(ad->icon);
			draw_sprite(actx, icon, pos_x + 10 + (ALTTAB_WIN_SIZE - 48) / 2, pos_y + 10 + (ALTTAB_WIN_SIZE - 48) / 2);
		}

	}

	{
		struct window_ad * ad = ads_by_z[new_focused];
		int t;
		char * title = ellipsify(ad->name, 16, font, alttab->width - 20, &t);
		tt_set_size(font, 16);
		tt_draw_string(actx, font, center_x_a(t), rows * (ALTTAB_WIN_SIZE + 20) + 44, title, rgb(255,255,255));
		free(title);
	}

	flip(actx);
	yutani_window_move(yctx, alttab, center_x(alttab->width), center_y(alttab->height));
	yutani_flip(yctx, alttab);
}

static pthread_t _waiter_thread;
static void * logout_prompt_waiter(void * arg) {
	if (system("showdialog \"Log Out\" /usr/share/icons/48/exit.png \"Are you sure you want to log out?\"") == 0) {
		yutani_session_end(yctx);
		_continue = 0;
	}
	return NULL;
}

void launch_application_menu(struct MenuEntry * self) {
	struct MenuEntry_Normal * _self = (void *)self;

	if (!strcmp((char *)_self->action,"log-out")) {
		/* Spin off a thread for this */
		pthread_create(&_waiter_thread, NULL, logout_prompt_waiter, NULL);
	} else {
		launch_application((char *)_self->action);
	}
}

static void handle_key_event(struct yutani_msg_key_event * ke) {
	/**
	 * Is the Alt+F2 command entry window open?
	 * Then we should capture all typing and use
	 * direct it to the 'input box'.
	 */
	if (alt_f2 && ke->wid == alt_f2->wid) {
		if (ke->event.action == KEY_ACTION_DOWN) {
			/* Escape = cancel */
			if (ke->event.keycode == KEY_ESCAPE) {
				close_altf2();
				return;
			}

			/* Backspace */
			if (ke->event.key == '\b') {
				if (altf2_collected) {
					altf2_buffer[altf2_collected-1] = '\0';
					altf2_collected--;
					redraw_altf2();
				}
				return;
			}

			/* Enter */
			if (ke->event.key == '\n') {
				/* execute */
				launch_application(altf2_buffer);
				close_altf2();
				return;
			}

			/* Some other key */
			if (!ke->event.key) {
				return;
			}

			/* Try to add it */
			if (altf2_collected < sizeof(altf2_buffer) - 1) {
				altf2_buffer[altf2_collected] = ke->event.key;
				altf2_collected++;
				altf2_buffer[altf2_collected] = 0;
				redraw_altf2();
			}
		}
	}

	/* Ctrl-Alt-T = Open a new terminal */
	if ((ke->event.modifiers & KEY_MOD_LEFT_CTRL) &&
		(ke->event.modifiers & KEY_MOD_LEFT_ALT) &&
		(ke->event.keycode == 't') &&
		(ke->event.action == KEY_ACTION_DOWN)) {

		launch_application("exec terminal");
		return;
	}

	/* Ctrl-F11 = Toggle visibility of the panel */
	if ((ke->event.modifiers & KEY_MOD_LEFT_CTRL) &&
		(ke->event.keycode == KEY_F11) &&
		(ke->event.action == KEY_ACTION_DOWN)) {

		fprintf(stderr, "[panel] Toggling visibility.\n");
		toggle_hide_panel();
		return;
	}

	/* Alt-F2 = Show the command entry window */
	if ((ke->event.modifiers & KEY_MOD_LEFT_ALT) &&
		(ke->event.keycode == KEY_F2) &&
		(ke->event.action == KEY_ACTION_DOWN)) {
		/* show menu */
		if (!alt_f2) {
			alt_f2 = yutani_window_create(yctx, ALTF2_WIDTH, ALTF2_HEIGHT);
			yutani_window_move(yctx, alt_f2, center_x(ALTF2_WIDTH), center_y(ALTF2_HEIGHT));
			a2ctx = init_graphics_yutani_double_buffer(alt_f2);
			redraw_altf2();
		}
	}

	/* Maybe a plugin wants to handle this key bind */
	foreach(widget_node, widgets_enabled) {
		struct PanelWidget * widget = widget_node->value;
		widget->onkey(widget, ke);
	}

	/* Releasing Alt when the Alt-Tab switcher is visible */
	if ((was_tabbing) && (ke->event.keycode == 0 || ke->event.keycode == KEY_LEFT_ALT) &&
		(ke->event.modifiers == 0) && (ke->event.action == KEY_ACTION_UP)) {

		fprintf(stderr, "[panel] Stopping focus new_focused = %d\n", new_focused);

		struct window_ad * ad = ads_by_z[new_focused];

		if (!ad) return;

		yutani_focus_window(yctx, ad->wid);
		was_tabbing = 0;
		new_focused = -1;

		free(actx->backbuffer);
		free(actx);
		actx = NULL;

		yutani_close(yctx, alttab);

		return;
	}

	/* Alt-Tab = Switch windows */
	if ((ke->event.modifiers & KEY_MOD_LEFT_ALT) &&
		(ke->event.keycode == '\t') &&
		(ke->event.action == KEY_ACTION_DOWN)) {

		/* Alt-Tab and Alt-Shift-Tab should go in alternate directions */
		int direction = (ke->event.modifiers & KEY_MOD_LEFT_SHIFT) ? 1 : -1;

		/* Are there no windows to switch? */
		if (window_list->length < 1) return;

		/* Figure out the new focused window */
		if (was_tabbing) {
			new_focused = new_focused + direction;
		} else {
			new_focused = active_window + direction;
			/* Create tab window */
			alttab = yutani_window_create_flags(yctx, ALTTAB_WIDTH, ALTTAB_HEIGHT,
				YUTANI_WINDOW_FLAG_NO_STEAL_FOCUS | YUTANI_WINDOW_FLAG_NO_ANIMATION);

			yutani_set_stack(yctx, alttab, YUTANI_ZORDER_OVERLAY);

			/* Initialize graphics context against the window */
			actx = init_graphics_yutani_double_buffer(alttab);
		}

		/* Handle wraparound */
		if (new_focused < 0) {
			new_focused = 0;
			for (int i = 0; i < MAX_WINDOW_COUNT; i++) {
				if (ads_by_z[i+1] == NULL) {
					new_focused = i;
					break;
				}
			}
		} else if (ads_by_z[new_focused] == NULL) {
			if (ads_by_z[0]) {
				new_focused = 0;
			} else {
				new_focused = -1;
			}
		}

		was_tabbing = 1;
		redraw_alttab();
	}
}

void redraw(void) {
	memcpy(ctx->backbuffer, bg_blob, bg_size);

	foreach(widget_node, widgets_enabled) {
		struct PanelWidget * widget = widget_node->value;
		gfx_context_t * inner = init_graphics_subregion(ctx, widget->left, Y_PAD, widget->width, PANEL_HEIGHT - Y_PAD * 2);
		widget->draw(widget, inner);
		free(inner);
	}

	/* Flip */
	flip(ctx);
	yutani_flip(yctx, panel);
}

static void update_window_list(void) {
	yutani_query_windows(yctx);

	list_t * new_window_list = list_create();

	ads_by_z[0] = NULL;

	int i = 0;
	while (1) {
		/* We wait for a series of WINDOW_ADVERTISE messsages */
		yutani_msg_t * m = yutani_wait_for(yctx, YUTANI_MSG_WINDOW_ADVERTISE);
		struct yutani_msg_window_advertise * wa = (void*)m->data;

		if (wa->size == 0) {
			/* A sentinal at the end will have a size of 0 */
			free(m);
			break;
		}

		/* Store each window advertisement */
		struct window_ad * ad = malloc(sizeof(struct window_ad));

		char * s = malloc(wa->size);
		memcpy(s, wa->strings, wa->size);
		ad->name = &s[0];
		ad->icon = &s[wa->icon];
		ad->strings = s;
		ad->flags = wa->flags;
		ad->wid = wa->wid;
		ad->bufid = wa->bufid;
		ad->width = wa->width;
		ad->height = wa->height;

		ads_by_z[i] = ad;
		i++;
		ads_by_z[i] = NULL;

		node_t * next = NULL;

		/* And insert it, ordered by wid, into the window list */
		foreach(node, new_window_list) {
			struct window_ad * n = node->value;

			if (n->wid > ad->wid) {
				next = node;
				break;
			}
		}

		if (next) {
			list_insert_before(new_window_list, next, ad);
		} else {
			list_insert(new_window_list, ad);
		}
		free(m);
	}
	active_window = i-1;

	if (window_list) {
		foreach(node, window_list) {
			struct window_ad * ad = (void*)node->value;
			free(ad->strings);
			free(ad);
		}
		list_free(window_list);
		free(window_list);
	}
	window_list = new_window_list;

	/* And redraw the panel */
	redraw();
}

static void redraw_panel_background(gfx_context_t * ctx, int width, int height) {
	draw_fill(ctx, rgba(0,0,0,0));
	draw_rounded_rectangle(ctx, X_PAD, Y_PAD, width - X_PAD * 2, panel->height - Y_PAD * 2, 14, rgba(0,0,0,200));
}

static void resize_finish(int xwidth, int xheight) {
	yutani_window_resize_accept(yctx, panel, xwidth, xheight);

	reinit_graphics_yutani(ctx, panel);
	yutani_window_resize_done(yctx, panel);

	width = xwidth;

	redraw_panel_background(ctx, xwidth, xheight);

	/* Copy the prerendered background so we can redraw it quickly */
	bg_size = panel->width * panel->height * sizeof(uint32_t);
	bg_blob = realloc(bg_blob, bg_size);
	memcpy(bg_blob, ctx->backbuffer, bg_size);

	widgets_layout();
	update_window_list();
}

static void bind_keys(void) {

	/* Cltr-Alt-T = launch terminal */
	yutani_key_bind(yctx, 't', KEY_MOD_LEFT_CTRL | KEY_MOD_LEFT_ALT, YUTANI_BIND_STEAL);

	/* Alt+Tab = app switcher*/
	yutani_key_bind(yctx, '\t', KEY_MOD_LEFT_ALT, YUTANI_BIND_STEAL);
	yutani_key_bind(yctx, '\t', KEY_MOD_LEFT_ALT | KEY_MOD_LEFT_SHIFT, YUTANI_BIND_STEAL);

	/* Ctrl-F11 = toggle panel visibility */
	yutani_key_bind(yctx, KEY_F11, KEY_MOD_LEFT_CTRL, YUTANI_BIND_STEAL);

	/* Alt+F2 = show app runner */
	yutani_key_bind(yctx, KEY_F2, KEY_MOD_LEFT_ALT, YUTANI_BIND_STEAL);

	/* This lets us receive all just-modifier key releases */
	yutani_key_bind(yctx, KEY_LEFT_ALT, 0, YUTANI_BIND_PASSTHROUGH);

}

static void sig_usr2(int sig) {
	yutani_set_stack(yctx, panel, YUTANI_ZORDER_TOP);
	yutani_flip(yctx, panel);
	bind_keys();
	signal(SIGUSR2, sig_usr2);
}

static int mouse_event_ignore(struct PanelWidget * this, struct yutani_msg_window_mouse_event * evt) {
	return 0;
}

static int widget_enter_generic(struct PanelWidget * this, struct yutani_msg_window_mouse_event * evt) {
	this->highlighted = 1; /* Highlighted */
	return 1;
}

static int widget_leave_generic(struct PanelWidget * this, struct yutani_msg_window_mouse_event * evt) {
	this->highlighted = 0; /* Not highlighted */
	return 1;
}

static int widget_draw_generic(struct PanelWidget * this, gfx_context_t * ctx) {
	draw_rounded_rectangle(
		ctx, 0, 0, ctx->width, ctx->height, 7, premultiply(rgba(120,120,120,150)));
	if (this->highlighted) {
		draw_rounded_rectangle(
			ctx, 1, 1, ctx->width-2, ctx->height-2, 6, premultiply(rgba(120,160,230,220)));
	} else {
		draw_rounded_rectangle(
			ctx, 1, 1, ctx->width-2, ctx->height-2, 6, ALTTAB_BACKGROUND);
	}
	return 0;
}

static int widget_update_generic(struct PanelWidget * this) {
	return 0;
}

static int widget_onkey_generic(struct PanelWidget * this, struct yutani_msg_key_event * evt) {
	return 0;
}

struct PanelWidget * widget_new(void) {
	struct PanelWidget * out = calloc(1, sizeof(struct PanelWidget));
	out->draw = widget_draw_generic;
	out->click = mouse_event_ignore; /* click_generic */
	out->right_click = mouse_event_ignore; /* right_click_generic */
	out->leave = widget_leave_generic;
	out->enter = widget_enter_generic;
	out->update = widget_update_generic;
	out->onkey = widget_onkey_generic;
	out->move = mouse_event_ignore; /* move_generic */
	out->highlighted = 0;
	out->fill = 0;
	return out;
}

static void widgets_layout(void) {
	int total_width = 0;
	int flexible_widgets = 0;
	foreach(node, widgets_enabled) {
		struct PanelWidget * widget = node->value;
		if (widget->fill) {
			flexible_widgets++;
		} else {
			total_width += widget->width;
		}
	}

	/* Now lay out the widgets */
	int x = 16;
	int available = width - 32;
	foreach(node, widgets_enabled) {
		struct PanelWidget * widget = node->value;
		widget->left = x;
		if (widget->fill) {
			widget->width = (available - total_width) / flexible_widgets;
		}
		x += widget->width;
	}
}

static void update_periodic_widgets(void) {
	int needs_layout = 0;
	foreach(widget_node, widgets_enabled) {
		struct PanelWidget * widget = widget_node->value;
		needs_layout |= widget->update(widget);
	}
	if (needs_layout) widgets_layout();
	redraw();
}

int panel_menu_show_at(struct MenuList * menu, int x) {
	int mwidth, mheight, offset;

	/* Calculate the expected size of the menu window. */
	menu_calculate_dimensions(menu, &mheight, &mwidth);

	if (x - mwidth / 2 < X_PAD) {
		offset = X_PAD;
		menu->flags = (menu->flags & ~MENU_FLAG_BUBBLE) | MENU_FLAG_BUBBLE_LEFT;
	} else if (x + mwidth / 2 > width - X_PAD) {
		offset = width - X_PAD - mwidth;
		menu->flags = (menu->flags & ~MENU_FLAG_BUBBLE) | MENU_FLAG_BUBBLE_RIGHT;
	} else {
		offset = x - mwidth / 2;
		menu->flags = (menu->flags & ~MENU_FLAG_BUBBLE) | MENU_FLAG_BUBBLE_CENTER;
	}

	menu->flags |= MENU_FLAG_TAIL_POSITION;
	menu->tail_offset = x - offset;

	/* Prepare the menu, which creates the window. */
	menu_prepare(menu, yctx);

	/* If we succeeded, move it to the final offset and display it */
	if (menu->window) {
		yutani_window_move(yctx, menu->window, offset, DROPDOWN_OFFSET);
		yutani_flip(yctx,menu->window);
		return 0;
	}

	return 1;
}

int panel_menu_show(struct PanelWidget * this, struct MenuList * menu) {
	return panel_menu_show_at(menu, this->left + this->width / 2);
}

int main (int argc, char ** argv) {
	if (argc < 2 || strcmp(argv[1],"--really")) {
		fprintf(stderr,
				"%s: Desktop environment panel / dock\n"
				"\n"
				" Renders the application menu, window list, widgets,\n"
				" alt-tab window switcher, clock, etc.\n"
				" You probably don't want to run this directly - it is\n"
				" started automatically by the session manager.\n", argv[0]);
		return 1;
	}

	/* Connect to window server */
	yctx = yutani_init();

	/* Shared fonts */
	font           = tt_font_from_shm("sans-serif");
	font_bold      = tt_font_from_shm("sans-serif.bold");
	font_mono      = tt_font_from_shm("monospace");
	font_mono_bold = tt_font_from_shm("monospace.bold");

	/* For convenience, store the display size */
	width  = yctx->display_width;
	height = yctx->display_height;

	/* Create the panel window */
	panel = yutani_window_create_flags(yctx, width, PANEL_HEIGHT, YUTANI_WINDOW_FLAG_NO_STEAL_FOCUS | YUTANI_WINDOW_FLAG_ALT_ANIMATION);

	/* And move it to the top layer */
	yutani_set_stack(yctx, panel, YUTANI_ZORDER_TOP);
	yutani_window_update_shape(yctx, panel, YUTANI_SHAPE_THRESHOLD_CLEAR);

	/* Initialize graphics context against the window */
	ctx = init_graphics_yutani_double_buffer(panel);

	/* Draw the background */
	redraw_panel_background(ctx, panel->width, panel->height);

	/* Copy the prerendered background so we can redraw it quickly */
	bg_size = panel->width * panel->height * sizeof(uint32_t);
	bg_blob = malloc(bg_size);
	memcpy(bg_blob, ctx->backbuffer, bg_size);

	/* Catch SIGINT */
	signal(SIGINT, sig_int);
	signal(SIGUSR2, sig_usr2);

	widgets_enabled = list_create();

	/* Initialize requested widgets */
	const char * widgets_to_load[] = {
		"appmenu",
		"windowlist",
		"volume",
		"network",
		"weather",
		"date",
		"clock",
		"logout",
		NULL
	};

	for (const char ** widget = widgets_to_load; *widget; widget++) {
		char lib_name[200];
		char func_name[200];

		snprintf(lib_name, 200, "libtoaru_panel_%s.so", *widget);
		snprintf(func_name, 200, "widget_init_%s", *widget);

		void * lib = dlopen(lib_name, 0);
		if (!lib) {
			fprintf(stderr, "panel: failed to load widget '%s'\n", *widget);
		} else {
			void (*widget_init)(void) = dlsym(lib, func_name);
			if (!widget_init) {
				fprintf(stderr, "panel: failed to initialize widget '%s'\n", *widget);
			} else {
				widget_init();
			}
		}
	}

	/* Lay out the widgets */
	update_periodic_widgets();
	widgets_layout();

	/* Subscribe to window updates */
	yutani_subscribe_windows(yctx);

	/* Ask compositor for window list */
	update_window_list();

	/* Key bindings */
	bind_keys();

	time_t last_tick = 0;

	int fds[1] = {fileno(yctx->sock)};

	fprintf(stderr, "entering loop?\n");

	while (_continue) {

		int index = fswait2(1,fds,200);

		if (index == 0) {
			/* Respond to Yutani events */
			yutani_msg_t * m = yutani_poll(yctx);
			while (m) {
				menu_process_event(yctx, m);
				switch (m->type) {
					/* New window information is available */
					case YUTANI_MSG_NOTIFY:
						update_window_list();
						break;
					/* Mouse movement / click */
					case YUTANI_MSG_WINDOW_MOUSE_EVENT:
						panel_check_click((struct yutani_msg_window_mouse_event *)m->data);
						break;
					case YUTANI_MSG_KEY_EVENT:
						handle_key_event((struct yutani_msg_key_event *)m->data);
						break;
					case YUTANI_MSG_WELCOME:
						{
							struct yutani_msg_welcome * mw = (void*)m->data;
							width = mw->display_width;
							height = mw->display_height;
							yutani_window_resize(yctx, panel, mw->display_width, PANEL_HEIGHT);
						}
						break;
					case YUTANI_MSG_RESIZE_OFFER:
						{
							struct yutani_msg_window_resize * wr = (void*)m->data;
							if (wr->wid == panel->wid) {
								resize_finish(wr->width, wr->height);
							} else if (alttab && wr->wid == alttab->wid) {
								yutani_window_resize_accept(yctx, alttab, wr->width, wr->height);
								reinit_graphics_yutani(actx, alttab);
								redraw_alttab();
								yutani_window_resize_done(yctx, alttab);
							}
						}
						break;
					default:
						break;
				}
				free(m);
				m = yutani_poll_async(yctx);
			}
		}

		struct timeval now;
		gettimeofday(&now, NULL);
		if (now.tv_sec != last_tick) {
			last_tick = now.tv_sec;
			waitpid(-1, NULL, WNOHANG);
			update_periodic_widgets();
			if (was_tabbing) {
				redraw_alttab();
			}
		}
	}

	/* Close the panel window */
	yutani_close(yctx, panel);

	/* Stop notifying us of window changes */
	yutani_unsubscribe_windows(yctx);

	return 0;
}

