// SPDX-License-Identifier: LGPL-2.1-or-later
/*
 * This file is part of libgpiod.
 *
 * Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
 */

#include <gpiod.h>
#include "tools-common.h"

#include <stdio.h>
#include <string.h>
#include <getopt.h>
#include <signal.h>
#include <sys/signalfd.h>
#include <limits.h>
#include <poll.h>
#include <errno.h>
#include <unistd.h>

static const struct option longopts[] = {
	{ "help",		no_argument,		NULL,	'h' },
	{ "version",		no_argument,		NULL,	'v' },
	{ "active-low",		no_argument,		NULL,	'l' },
	{ "num-events",		required_argument,	NULL,	'n' },
	{ "silent",		no_argument,		NULL,	's' },
	{ "rising-edge",	no_argument,		NULL,	'r' },
	{ "falling-edge",	no_argument,		NULL,	'f' },
	{ "format",		required_argument,	NULL,	'F' },
	{ GETOPT_NULL_LONGOPT },
};

static const char *const shortopts = "+hvln:srfF:";

static void print_help(void)
{
	printf("Usage: %s [OPTIONS] <chip name/number> <offset 1> <offset 2> ...\n",
	       get_progname());
	printf("Wait for events on GPIO lines\n");
	printf("\n");
	printf("Options:\n");
	printf("  -h, --help:\t\tdisplay this message and exit\n");
	printf("  -v, --version:\tdisplay the version and exit\n");
	printf("  -l, --active-low:\tset the line active state to low\n");
	printf("  -n, --num-events=NUM:\texit after processing NUM events\n");
	printf("  -s, --silent:\t\tdon't print event info\n");
	printf("  -r, --rising-edge:\tonly process rising edge events\n");
	printf("  -f, --falling-edge:\tonly process falling edge events\n");
	printf("  -F, --format=FMT\tspecify custom output format\n");
	printf("\n");
	printf("Format specifiers:\n");
	printf("  %%o:  GPIO line offset\n");
	printf("  %%e:  event type (0 - falling edge, 1 rising edge)\n");
	printf("  %%s:  seconds part of the event timestamp\n");
	printf("  %%n:  nanoseconds part of the event timestamp\n");
}

struct mon_ctx {
	unsigned int offset;
	unsigned int events_wanted;
	unsigned int events_done;

	bool silent;
	char *fmt;

	int sigfd;
};

static void event_print_custom(unsigned int offset,
			       const struct timespec *ts,
			       int event_type,
			       struct mon_ctx *ctx)
{
	char *prev, *curr, fmt;

	for (prev = curr = ctx->fmt;;) {
		curr = strchr(curr, '%');
		if (!curr) {
			fputs(prev, stdout);
			break;
		}

		if (prev != curr)
			fwrite(prev, curr - prev, 1, stdout);

		fmt = *(curr + 1);

		switch (fmt) {
		case 'o':
			printf("%u", offset);
			break;
		case 'e':
			if (event_type == GPIOD_CTXLESS_EVENT_CB_RISING_EDGE)
				fputc('1', stdout);
			else
				fputc('0', stdout);
			break;
		case 's':
			printf("%ld", ts->tv_sec);
			break;
		case 'n':
			printf("%ld", ts->tv_nsec);
			break;
		case '%':
			fputc('%', stdout);
			break;
		case '\0':
			fputc('%', stdout);
			goto end;
		default:
			fwrite(curr, 2, 1, stdout);
			break;
		}

		curr += 2;
		prev = curr;
	}

end:
	fputc('\n', stdout);
}

static void event_print_human_readable(unsigned int offset,
				       const struct timespec *ts,
				       int event_type)
{
	char *evname;

	if (event_type == GPIOD_CTXLESS_EVENT_CB_RISING_EDGE)
		evname = " RISING EDGE";
	else
		evname = "FALLING EDGE";

	printf("event: %s offset: %u timestamp: [%8ld.%09ld]\n",
	       evname, offset, ts->tv_sec, ts->tv_nsec);
}

static int poll_callback(unsigned int num_lines,
			 struct gpiod_ctxless_event_poll_fd *fds,
			 const struct timespec *timeout, void *data)
{
	struct pollfd pfds[GPIOD_LINE_BULK_MAX_LINES + 1];
	struct mon_ctx *ctx = data;
	int cnt, ts, ret;
	unsigned int i;

	for (i = 0; i < num_lines; i++) {
		pfds[i].fd = fds[i].fd;
		pfds[i].events = POLLIN | POLLPRI;
	}

	pfds[i].fd = ctx->sigfd;
	pfds[i].events = POLLIN | POLLPRI;

	ts = timeout->tv_sec * 1000 + timeout->tv_nsec / 1000000;

	cnt = poll(pfds, num_lines + 1, ts);
	if (cnt < 0)
		return GPIOD_CTXLESS_EVENT_POLL_RET_ERR;
	else if (cnt == 0)
		return GPIOD_CTXLESS_EVENT_POLL_RET_TIMEOUT;

	ret = cnt;
	for (i = 0; i < num_lines; i++) {
		if (pfds[i].revents) {
			fds[i].event = true;
			if (!--cnt)
				return ret;
		}
	}

	/*
	 * If we're here, then there's a signal pending. No need to read it,
	 * we know we should quit now.
	 */
	close(ctx->sigfd);

	return GPIOD_CTXLESS_EVENT_POLL_RET_STOP;
}

static void handle_event(struct mon_ctx *ctx, int event_type,
			 unsigned int line_offset,
			 const struct timespec *timestamp)
{
	if (!ctx->silent) {
		if (ctx->fmt)
			event_print_custom(line_offset, timestamp,
					   event_type, ctx);
		else
			event_print_human_readable(line_offset,
						   timestamp, event_type);
	}

	ctx->events_done++;
}

static int event_callback(int event_type, unsigned int line_offset,
			  const struct timespec *timestamp, void *data)
{
	struct mon_ctx *ctx = data;

	switch (event_type) {
	case GPIOD_CTXLESS_EVENT_CB_RISING_EDGE:
	case GPIOD_CTXLESS_EVENT_CB_FALLING_EDGE:
		handle_event(ctx, event_type, line_offset, timestamp);
		break;
	default:
		/*
		 * REVISIT: This happening would indicate a problem in the
		 * library.
		 */
		return GPIOD_CTXLESS_EVENT_CB_RET_OK;
	}

	if (ctx->events_wanted && ctx->events_done >= ctx->events_wanted)
		return GPIOD_CTXLESS_EVENT_CB_RET_STOP;

	return GPIOD_CTXLESS_EVENT_CB_RET_OK;
}

static int make_signalfd(void)
{
	sigset_t sigmask;
	int sigfd, rv;

	sigemptyset(&sigmask);
	sigaddset(&sigmask, SIGTERM);
	sigaddset(&sigmask, SIGINT);

	rv = sigprocmask(SIG_BLOCK, &sigmask, NULL);
	if (rv < 0)
		die("error masking signals: %s", strerror(errno));

	sigfd = signalfd(-1, &sigmask, 0);
	if (sigfd < 0)
		die("error creating signalfd: %s", strerror(errno));

	return sigfd;
}

int main(int argc, char **argv)
{
	unsigned int offsets[GPIOD_LINE_BULK_MAX_LINES], num_lines = 0, offset;
	bool active_low = false, watch_rising = false, watch_falling = false;
	struct timespec timeout = { 10, 0 };
	int optc, opti, ret, i, event_type;
	struct mon_ctx ctx;
	char *end;

	memset(&ctx, 0, sizeof(ctx));

	for (;;) {
		optc = getopt_long(argc, argv, shortopts, longopts, &opti);
		if (optc < 0)
			break;

		switch (optc) {
		case 'h':
			print_help();
			return EXIT_SUCCESS;
		case 'v':
			print_version();
			return EXIT_SUCCESS;
		case 'l':
			active_low = true;
			break;
		case 'n':
			ctx.events_wanted = strtoul(optarg, &end, 10);
			if (*end != '\0')
				die("invalid number: %s", optarg);
			break;
		case 's':
			ctx.silent = true;
			break;
		case 'r':
			watch_rising = true;
			break;
		case 'f':
			watch_falling = true;
			break;
		case 'F':
			ctx.fmt = optarg;
			break;
		case '?':
			die("try %s --help", get_progname());
		default:
			abort();
		}
	}

	argc -= optind;
	argv += optind;

	if (watch_rising && !watch_falling)
		event_type = GPIOD_CTXLESS_EVENT_RISING_EDGE;
	else if (watch_falling && !watch_rising)
		event_type = GPIOD_CTXLESS_EVENT_FALLING_EDGE;
	else
		event_type = GPIOD_CTXLESS_EVENT_BOTH_EDGES;

	if (argc < 1)
		die("gpiochip must be specified");

	if (argc < 2)
		die("at least one GPIO line offset must be specified");

	for (i = 1; i < argc; i++) {
		offset = strtoul(argv[i], &end, 10);
		if (*end != '\0' || offset > INT_MAX)
			die("invalid GPIO offset: %s", argv[i]);

		offsets[i - 1] = offset;
		num_lines++;
	}

	ctx.sigfd = make_signalfd();

	ret = gpiod_ctxless_event_monitor_multiple(argv[0], event_type,
						   offsets, num_lines,
						   active_low, "gpiomon",
						   &timeout, poll_callback,
						   event_callback, &ctx);
	if (ret)
		die_perror("error waiting for events");

	return EXIT_SUCCESS;
}