#include<errno.h>
#include<getopt.h>
#include<poll.h>
#include<stdbool.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<signal.h>

#ifdef __linux__
#include<features.h>
#ifdef __GLIBC__
#include<execinfo.h>
#endif
#endif

#include<wayland-server.h>
#include<wayland-client.h>
#include<wayland-client-protocol.h>

#include"wlr-layer-shell-unstable-v1-protocol.h"
#include"xdg-output-unstable-v1-protocol.h"
#include"xdg-shell-protocol.h"

#include"colour.h"
#include"misc.h"
#include"output.h"
#include"render.h"
#include"surface.h"
#include"wlclock.h"

struct Wlclock_context context = {0};

/**
 * Intercept user signals so that they don't kill us.
 */
static void handle_user_signal (int signum)
{
	clocklog(0, "ERROR: wlclock does not handle user signals.\n");
}

/**
 * Intercept error signals (like SIGSEGV and SIGFPE) so that we can try to
 * print a fancy error message and a backtracke before letting the system kill us.
 */
static void handle_error (int signum)
{
	const char *msg =
		"\n"
		"┌──────────────────────────────────────────┐\n"
		"│                                          │\n"
		"│         wlclock has crashed.             │\n"
		"│                                          │\n"
		"│    This is likely a bug, so please       │\n"
		"│    report this to the mailing list.      │\n"
		"│                                          │\n"
		"│  ~leon_plickat/public-inbox@lists.sr.ht  │\n"
		"│                                          │\n"
		"└──────────────────────────────────────────┘\n"
		"\n";
	fputs(msg, stderr);

#ifdef __linux__
#ifdef __GLIBC__
	fputs("Attempting to get backtrace:\n", stderr);

	/* In some rare cases, getting a backtrace can also cause a segfault.
	 * There is nothing we can or should do about that. All hope is lost at
	 * that point.
	 */
	void *buffer[255];
	const int calls = backtrace(buffer, sizeof(buffer) / sizeof(void *));
	backtrace_symbols_fd(buffer, calls, fileno(stderr));
	fputs("\n", stderr);
#endif
#endif

	/* Let the default handlers deal with the rest. */
	signal(signum, SIG_DFL);
	kill(getpid(), signum);
}

/**
 * Intercept soft kills (like SIGINT and SIGTERM) so we can attempt to clean up
 * and exit gracefully.
 */
static void handle_term (int signum)
{
	clocklog(1, "[signal] Soft kill received.\n");

	/* If cleanup fails or hangs and causes this signal to be recieved again,
	 * let the default signal handler kill us.
	 */
	signal(signum, SIG_DFL);

	context.loop = false;
}

/**
 * Set up signal handlers.
 */
static void init_signals (void)
{
	signal(SIGSEGV, handle_error);
	signal(SIGFPE, handle_error);

	signal(SIGINT, handle_term);
	signal(SIGTERM, handle_term);

	signal(SIGUSR1, handle_user_signal);
	signal(SIGUSR2, handle_user_signal);
}

static void registry_handle_global (void *data, struct wl_registry *registry,
		uint32_t name, const char *interface, uint32_t version)
{
	if (! strcmp(interface, wl_compositor_interface.name))
	{
		clocklog(2, "[main] Get wl_compositor.\n");
		context.compositor = wl_registry_bind(registry, name, &wl_compositor_interface, 4);
	}
	if (! strcmp(interface, wl_subcompositor_interface.name))
	{
		clocklog(2, "[main] Get wl_subcompositor.\n");
		context.subcompositor = wl_registry_bind(registry, name,
				&wl_subcompositor_interface, 1);
	}
	else if (! strcmp(interface, wl_shm_interface.name))
	{
		clocklog(2, "[main] Get wl_shm.\n");
		context.shm = wl_registry_bind(registry, name, &wl_shm_interface, 1);
	}
	else if (! strcmp(interface, zwlr_layer_shell_v1_interface.name))
	{
		clocklog(2, "[main] Get zwlr_layer_shell_v1.\n");
		context.layer_shell = wl_registry_bind(registry, name, &zwlr_layer_shell_v1_interface, 1);
	}
	else if (! strcmp(interface, zxdg_output_manager_v1_interface.name))
	{
		clocklog(2, "[main] Get zxdg_output_manager_v1.\n");
		context.xdg_output_manager = wl_registry_bind(registry, name, &zxdg_output_manager_v1_interface, 3);
	}
	else if (! strcmp(interface, wl_output_interface.name))
	{
		if (! create_output(registry, name, interface, version))
			goto error;
	}

	return;
error:
	context.loop = false;
	context.ret  = EXIT_FAILURE;
}

static void registry_handle_global_remove (void *data,
		struct wl_registry *registry, uint32_t name)
{
	clocklog(1, "[main] Global remove.\n");
	destroy_output(get_output_from_global_name(name));
}

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

/* Helper function for capability support error message. */
static bool capability_test (void *ptr, const char *name)
{
	if ( ptr != NULL )
		return true;
	clocklog(0, "ERROR: Wayland compositor does not support %s.\n", name);
	return false;
}

static bool init_wayland (void)
{
	clocklog(1, "[main] Init Wayland.\n");

	/* Connect to Wayland server. */
	clocklog(2, "[main] Connecting to server.\n");
	if ( NULL == (context.display = wl_display_connect(NULL)) )
	{
		clocklog(0, "ERROR: Can not connect to a Wayland server.\n");
		return false;
	}

	/* Get registry and add listeners. */
	clocklog(2, "[main] Get wl_registry.\n");
	context.registry = wl_display_get_registry(context.display);

	wl_registry_add_listener(context.registry, &registry_listener, NULL);

	/* Allow registry listeners to catch up. */
	// TODO use sync instead
	if ( wl_display_roundtrip(context.display) == -1 )
	{
		clocklog(0, "ERROR: Roundtrip failed.\n");
		return false;
	}

	/* Testing compatibilities. */
	if (! capability_test(context.compositor, "wl_compositor"))
		return false;
	if (! capability_test(context.shm, "wl_shm"))
		return false;
	if (! capability_test(context.layer_shell, "zwlr_layer_shell"))
		return false;
	if (! capability_test(context.xdg_output_manager, "xdg_output_manager"))
		return false;

	clocklog(2, "[main] Catching up on output configuration.\n");
	struct Wlclock_output *op;
	wl_list_for_each(op, &context.outputs, link)
		if ( ! op->configured && ! configure_output(op) )
			return false;

	return true;
}

/* Finish him! */
static void finish_wayland (void)
{
	if ( context.display == NULL )
		return;

	clocklog(1, "[main] Finish Wayland.\n");

	destroy_all_outputs();

	clocklog(2, "[main] Destroying Wayland objects.\n");
	if ( context.layer_shell != NULL )
		zwlr_layer_shell_v1_destroy(context.layer_shell);
	if ( context.compositor != NULL )
		wl_compositor_destroy(context.compositor);
	if ( context.shm != NULL )
		wl_shm_destroy(context.shm);
	if ( context.registry != NULL )
		wl_registry_destroy(context.registry);

	clocklog(2, "[main] Diconnecting from server.\n");
	wl_display_disconnect(context.display);
}

static int count_args (int index, int argc, char *argv[])
{
	index--;
	int args = 0;
	while ( index < argc )
	{
		if ( *argv[index] == '-' )
			break;
		args++;
		index++;
	}
	return args;
}

static bool handle_command_flags (int argc, char *argv[])
{
	enum
	{
		BACKGROUND_COLOUR,
		BORDER_COLOUR,
		BORDER_SIZE,
		CLOCK_COLOUR,
		CORNER_RADIUS,
		EXCLUSIVE_ZONE,
		HAND_WIDTH,
		LAYER,
		MARGIN,
		MARKING_WIDTH,
		NAMEPSACE,
		NO_INPUT,
		OUTPUT,
		POSITION,
		SIZE,
		SNAP
	};

	static struct option opts[] = {
		{"help",     no_argument,       NULL, 'h'},
		{"verbose",  no_argument,       NULL, 'v'},
		{"version",  no_argument,       NULL, 'V'},

		{"background-colour", required_argument, NULL, BACKGROUND_COLOUR},
		{"border-colour",     required_argument, NULL, BORDER_COLOUR},
		{"border-size",       required_argument, NULL, BORDER_SIZE},
		{"clock-colour",      required_argument, NULL, CLOCK_COLOUR},
		{"corner-radius",     required_argument, NULL, CORNER_RADIUS},
		{"exclusive-zone",    required_argument, NULL, EXCLUSIVE_ZONE},
		{"hand-width",        required_argument, NULL, HAND_WIDTH},
		{"layer",             required_argument, NULL, LAYER},
		{"margin",            required_argument, NULL, MARGIN},
		{"marking-width",     required_argument, NULL, MARKING_WIDTH},
		{"namespace",         required_argument, NULL, NAMEPSACE},
		{"no-input",          no_argument,       NULL, NO_INPUT},
		{"output",            required_argument, NULL, OUTPUT},
		{"position",          required_argument, NULL, POSITION},
		{"size",              required_argument, NULL, SIZE},
		{"snap",              no_argument,       NULL, SNAP},
		{NULL,                0,                 NULL, 0}
	};

	const char *usage =
		"Usage: wlclock [options]\n"
		"\n"
		"  -h, --help               Show this help text.\n"
		"  -v, --verbose            Increase verbosity of output.\n"
		"  -V, --version            Show the version.\n"
		"      --background-colour  Background colour.\n"
		"      --border-colour      Border colour.\n"
		"      --border-size        Size of the border.\n"
		"      --clock-colour       Colour of the clock elements.\n"
		"      --corner-radius      Corner radii.\n"
		"      --exclusive-zone     Exclusive zone of the layer surface.\n"
		"      --hand-width         Width of the clock hands.\n"
		"      --layer              Layer of the layer surface.\n"
		"      --margin             Directional margins.\n"
		"      --marking-width      Width of the markings on the clock face.\n"
		"      --namespace          Namespace of the layer surface.\n"
		"      --no-input           Let inputs surface pass trough the layer surface.\n"
		"      --output             The output which the clock will be displayed.\n"
		"      --position           Set the position of the context.\n"
		"      --size               Size of the context.\n"
		"      --snap               Let the hour hand snap to the next position instead of slowly progressing.\n"
		"\n";

	int opt, args;
	extern int optind;
	extern char *optarg;
	while ( (opt = getopt_long(argc, argv, "hvV", opts, &optind)) != -1 ) switch (opt)
	{
		case 'h':
			fputs(usage, stderr);
			context.ret = EXIT_SUCCESS;
			return false;

		case 'v':
			context.verbosity++;
			break;

		case 'V':
			fputs("wlclock version " VERSION "\n", stderr);
			context.ret = EXIT_SUCCESS;
			return false;

		case POSITION:
			if (! strcmp(optarg, "center"))
				context.anchor = 0;
			else if (! strcmp(optarg, "top"))
				context.anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP;
			else if (! strcmp(optarg, "right"))
				context.anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT;
			else if (! strcmp(optarg, "bottom"))
				context.anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM;
			else if (! strcmp(optarg, "left"))
				context.anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT;
			else if (! strcmp(optarg, "top-left"))
				context.anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP
					| ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT;
			else if (! strcmp(optarg, "top-right"))
				context.anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP
					| ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT;
			else if (! strcmp(optarg, "bottom-left"))
				context.anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM
					| ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT;
			else if (! strcmp(optarg, "bottom-right"))
				context.anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM
					| ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT;
			else
			{
				clocklog(0, "ERROR: Unrecognized position \"%s\".\n"
						"INFO: Possible positisions are 'center', "
						"'top', 'right', 'bottom', 'left', "
						"'top-right', 'top-left', 'bottom-right', 'bottom-left'.\n",
						optarg);
				return false;
			}
			break;
			break;

		case BACKGROUND_COLOUR:
			if (! colour_from_string(&context.background_colour, optarg))
				return false;
			break;

		case BORDER_COLOUR:
			if (! colour_from_string(&context.border_colour, optarg))
				return false;
			break;

		case BORDER_SIZE:
			args = count_args(optind, argc, argv);
			if ( args == 1 )
				context.border_top = context.border_right =
					context.border_bottom = context.border_left =
					atoi(optarg);
			else if ( args == 4 )
			{
				context.border_top    = atoi(argv[optind-1]);
				context.border_right  = atoi(argv[optind]);
				context.border_bottom = atoi(argv[optind+1]);
				context.border_left   = atoi(argv[optind+2]);
				optind += 3; /* Tell getopt() to skip three argv fields. */
			}
			else
			{
				clocklog(0, "ERROR: Border configuration "
						"requires one or four arguments.\n");
				return false;
			}
			if ( context.border_top < 0 || context.border_right < 0
					|| context.border_bottom < 0 || context.border_left < 0 )
			{
				clocklog(0, "ERROR: Borders may not be smaller than zero.\n");
				return false;
			}
			break;

		case CLOCK_COLOUR:
			if (! colour_from_string(&context.clock_colour, optarg))
				return false;
			break;

		case MARKING_WIDTH:
			context.marking_width = atoi(optarg);
			if ( context.marking_width < 0 )
			{
				clocklog(0, "ERROR: Marking width may not be smaller than zero.\n");
				return false;
			}
			break;

		case HAND_WIDTH:
			context.hand_width = atoi(optarg);
			if ( context.hand_width < 0 )
			{
				clocklog(0, "ERROR: Hand width may not be smaller than zero.\n");
				return false;
			}
			break;

		case EXCLUSIVE_ZONE:
			if (is_boolean_true(optarg))
				context.exclusive_zone = 1;
			else if (is_boolean_false(optarg))
				context.exclusive_zone = 0;
			else if (! strcmp(optarg, "stationary"))
				context.exclusive_zone = -1;
			else
			{
				clocklog(0, "ERROR: Unrecognized exclusive zone option \"%s\".\n"
						"INFO: Possible options are 'true', "
						"'false' and 'stationary'.\n", optarg);
				return false;
			}
			break;

		case LAYER:
			if (! strcmp(optarg, "overlay"))
				context.layer = ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY;
			else if (! strcmp(optarg, "top"))
				context.layer = ZWLR_LAYER_SHELL_V1_LAYER_TOP;
			else if (! strcmp(optarg, "bottom"))
				context.layer = ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM;
			else if (! strcmp(optarg, "background"))
				context.layer = ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND;
			else
			{
				clocklog(0, "ERROR: Unrecognized layer \"%s\".\n"
						"INFO: Possible layers are 'overlay', "
						"'top', 'bottom', and 'background'.\n", optarg);
				return false;
			}
			break;

		case MARGIN:
			args = count_args(optind, argc, argv);
			if ( args == 1 )
				context.margin_top = context.margin_right =
					context.margin_bottom = context.margin_left =
					atoi(optarg);
			else if ( args == 4 )
			{
				context.margin_top    = atoi(argv[optind-1]);
				context.margin_right  = atoi(argv[optind]);
				context.margin_bottom = atoi(argv[optind+1]);
				context.margin_left   = atoi(argv[optind+2]);
				optind += 3; /* Tell getopt() to skip three argv fields. */
			}
			else
			{
				clocklog(0, "ERROR: Margin configuration "
						"requires one or four arguments.\n");
				return false;
			}
			if ( context.margin_top < 0 || context.margin_right < 0
					|| context.margin_bottom < 0 || context.margin_left < 0 )
			{
				clocklog(0, "ERROR: Margins may not be smaller than zero.\n");
				return false;
			}
			break;

		case NAMEPSACE:
			set_string(&context.namespace, optarg);
			break;

		case NO_INPUT:
			context.input = false;
			break;

		case SNAP:
			context.snap = true;
			break;

		case OUTPUT:
			if ( ! strcmp("all", optarg) || ! strcmp("*", optarg) )
				free_if_set(context.output);
			else
				set_string(&context.output, optarg);
			break;

		case CORNER_RADIUS:
			args = count_args(optind, argc, argv);
			if ( args == 1 )
				context.radius_top_left = context.radius_top_right =
					context.radius_bottom_right = context.radius_bottom_left =
					atoi(optarg);
			else if ( args == 4 )
			{
				context.radius_top_left     = atoi(argv[optind-1]);
				context.radius_top_right    = atoi(argv[optind]);
				context.radius_bottom_right = atoi(argv[optind+1]);
				context.radius_bottom_left  = atoi(argv[optind+2]);
				optind += 3; /* Tell getopt() to skip three argv fields. */
			}
			else
			{
				clocklog(0, "ERROR: Radius configuration "
						"requires one or four arguments.\n");
				return false;
			}
			if ( context.radius_top_left < 0 || context.radius_top_right < 0
					|| context.radius_bottom_right < 0 || context.radius_bottom_left < 0 )
			{
				clocklog(0, "ERROR: Radii may not be smaller than zero.\n");
				return false;
			}
			break;

		case SIZE:
			context.dimensions.center_size = atoi(optarg);
			if ( context.dimensions.center_size <= 10 )
			{
				clocklog(0, "ERROR: Unreasonably small size \"%d\".\n",
						context.dimensions.center_size);
				return false;
			}
			break;

		default:
			return false;
	}

	return true;
}

/* Timeout until next minute. */
static time_t get_timeout (void)
{
	time_t now = time(NULL);
	return ((now / 60 * 60 ) + 60 - now) * 1000;
}

static void clock_run ()
{
	clocklog(1, "[main] Starting loop.\n");
	context.ret = EXIT_SUCCESS;

	struct pollfd fds[1] = {0};
	size_t wayland_fd = 0;

	fds[wayland_fd].events = POLLIN;
	if ( -1 ==  (fds[wayland_fd].fd = wl_display_get_fd(context.display)) )
	{
		clocklog(0, "ERROR: Unable to open Wayland display fd.\n");
		context.ret = EXIT_FAILURE;
		goto exit;
	}

	while (context.loop)
	{
		/* Flush pending Wayland events/requests. */
		int ret = 1;
		while ( ret > 0 )
		{
			ret = wl_display_dispatch_pending(context.display);
			wl_display_flush(context.display);
		}
		if ( ret < 0 )
		{
			clocklog(0, "ERROR: wl_display_dispatch_pending: %s\n", strerror(errno));
			context.ret = EXIT_FAILURE;
			goto exit;
		}

		ret = poll(fds, 1, get_timeout());

		if ( ret == 0 ) /* Timeout -> update clock hands. */
		{
			clocklog(1, "[surface] Updating all hands.\n");
			struct Wlclock_output *op, *tmp;
			wl_list_for_each_safe(op, tmp, &context.outputs, link)
				if ( op->surface != NULL )
				{
					render_hands_frame(op->surface);
					wl_surface_commit(op->surface->hands_surface);
					wl_surface_commit(op->surface->background_surface);
				}
			continue;
		}
		else if ( ret < 0 )
		{
			if ( errno == EINTR )
				clocklog(1, "[loop] Interrupted by signal.\n");
			else
				clocklog(0, "ERROR: poll: %s\n", strerror(errno));
			continue;
		}
	
		/* Wayland events */
		if ( fds[wayland_fd].revents & POLLIN && wl_display_dispatch(context.display) == -1 )
		{
			clocklog(0, "ERROR: wl_display_flush: %s\n", strerror(errno));
			context.ret = EXIT_FAILURE;
			break;
		}
		if ( fds[wayland_fd].revents & POLLOUT && wl_display_flush(context.display) == -1 )
		{
			clocklog(0, "ERROR: wl_display_flush: %s\n", strerror(errno));
			context.ret = EXIT_FAILURE;
			break;
		}
	}

exit:
	if ( fds[wayland_fd].fd != -1 )
		close(fds[wayland_fd].fd);
	return;
}

int main (int argc, char *argv[])
{
	init_signals();

	context.ret = EXIT_FAILURE;
	context.loop = true;
	context.verbosity = 0;
	context.dimensions.center_size = 165; /* About the size of xclock, at least on my machine. */
	context.exclusive_zone = -1;
	context.input = true;
	context.snap = false;
	context.layer = ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY;
	context.anchor = 0; /* Center */
	context.border_bottom = context.border_top
		= context.border_left = context.border_right = 1;
	context.radius_bottom_left = context.radius_bottom_right
		= context.radius_top_left = context.radius_top_right = 0;
	context.margin_bottom = context.margin_top
		= context.margin_left = context.margin_right = 0;
	context.marking_width = 1;
	context.hand_width = 0;

	wl_list_init(&context.outputs);
	set_string(&context.namespace, "wlclock");

	colour_from_string(&context.background_colour, "#FFFFFF");
	colour_from_string(&context.border_colour,     "#000000");
	colour_from_string(&context.clock_colour,      "#000000");

	if (! handle_command_flags(argc, argv))
		goto exit;

	context.dimensions.w = context.dimensions.center_size
		+ context.border_left + context.border_right;
	context.dimensions.h = context.dimensions.center_size
		+ context.border_top + context.border_bottom;
	context.dimensions.center_x = context.border_left;
	context.dimensions.center_y = context.border_top;

	clocklog(1, "[main] wlclock: version=%s\n"
			"[main] Default dimensions: size=%d cx=%d cy=%d w=%d h=%d\n",
			VERSION, context.dimensions.center_size,
			context.dimensions.center_x, context.dimensions.center_y,
			context.dimensions.w, context.dimensions.h);

	if (! init_wayland())
		goto exit;

	clock_run();

exit:
	finish_wayland();
	free_if_set(context.output);
	free_if_set(context.namespace);
	clocklog(1, "[main] Bye!\n");
	return context.ret;
}

