/*
 * lsclocks(1) - display system clocks
 *
 * Copyright (C) 2023 Thomas Weißschuh <thomas@t-8ch.de>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it would be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <stdio.h>
#include <stdbool.h>
#include <time.h>
#include <inttypes.h>
#include <getopt.h>
#include <glob.h>
#include <sys/ioctl.h>

#include <libsmartcols.h>

#include <linux/rtc.h>

#include "c.h"
#include "nls.h"
#include "strutils.h"
#include "timeutils.h"
#include "closestream.h"
#include "xalloc.h"
#include "pathnames.h"
#include "all-io.h"
#include "list.h"

#define CLOCKFD 3

static inline clockid_t FD_TO_CLOCKID(int fd)
{
	return (~(unsigned int) fd << 3) | CLOCKFD;
}

#ifndef CLOCK_REALTIME
#define CLOCK_REALTIME			0
#endif

#ifndef CLOCK_MONOTONIC
#define CLOCK_MONOTONIC			1
#endif

#ifndef CLOCK_MONOTONIC_RAW
#define CLOCK_MONOTONIC_RAW		4
#endif

#ifndef CLOCK_REALTIME_COARSE
#define CLOCK_REALTIME_COARSE		5
#endif

#ifndef CLOCK_MONOTONIC_COARSE
#define CLOCK_MONOTONIC_COARSE		6
#endif

#ifndef CLOCK_BOOTTIME
#define CLOCK_BOOTTIME			7
#endif

#ifndef CLOCK_REALTIME_ALARM
#define CLOCK_REALTIME_ALARM		8
#endif

#ifndef CLOCK_BOOTTIME_ALARM
#define CLOCK_BOOTTIME_ALARM		9
#endif

#ifndef CLOCK_TAI
#define CLOCK_TAI			11
#endif

enum CLOCK_TYPE {
	CT_SYS,
	CT_PTP,
	CT_CPU,
	CT_RTC,
};

static const char *clock_type_name(enum CLOCK_TYPE type)
{
	switch (type) {
	case CT_SYS:
		return "sys";
	case CT_PTP:
		return "ptp";
	case CT_CPU:
		return "cpu";
	case CT_RTC:
		return "rtc";
	}
	errx(EXIT_FAILURE, _("Unknown clock type %d"), type);
}

struct clockinfo {
	enum CLOCK_TYPE type;
	clockid_t id;
	const char * const id_name;
	const char * const name;
	const char * const ns_offset_name;
	bool no_id;
};

static const struct clockinfo clocks[] = {
	{ CT_SYS, CLOCK_REALTIME,         "CLOCK_REALTIME",         "realtime"         },
	{ CT_SYS, CLOCK_MONOTONIC,        "CLOCK_MONOTONIC",        "monotonic",
	  .ns_offset_name = "monotonic"						       },
	{ CT_SYS, CLOCK_MONOTONIC_RAW,    "CLOCK_MONOTONIC_RAW",    "monotonic-raw"    },
	{ CT_SYS, CLOCK_REALTIME_COARSE,  "CLOCK_REALTIME_COARSE",  "realtime-coarse"  },
	{ CT_SYS, CLOCK_MONOTONIC_COARSE, "CLOCK_MONOTONIC_COARSE", "monotonic-coarse" },
	{ CT_SYS, CLOCK_BOOTTIME,         "CLOCK_BOOTTIME",         "boottime",
	  .ns_offset_name = "boottime"						       },
	{ CT_SYS, CLOCK_REALTIME_ALARM,   "CLOCK_REALTIME_ALARM",   "realtime-alarm"   },
	{ CT_SYS, CLOCK_BOOTTIME_ALARM,   "CLOCK_BOOTTIME_ALARM",   "boottime-alarm"   },
	{ CT_SYS, CLOCK_TAI,              "CLOCK_TAI",              "tai"              },
};

/* column IDs */
enum {
	COL_TYPE,
	COL_ID,
	COL_CLOCK,
	COL_NAME,
	COL_TIME,
	COL_ISO_TIME,
	COL_RESOL,
	COL_RESOL_RAW,
	COL_REL_TIME,
	COL_NS_OFFSET,
};

/* column names */
struct colinfo {
	const char * const	name; /* header */
	double			whint; /* width hint (N < 1 is in percent of termwidth) */
	int			flags; /* SCOLS_FL_* */
	int			json_type; /* SCOLS_JSON_* */
	const char * const	help;
};

/* columns descriptions */
static const struct colinfo infos[] = {
	[COL_TYPE]       = { "TYPE",       1, 0,              SCOLS_JSON_STRING, N_("type") },
	[COL_ID]         = { "ID",         1, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, N_("numeric id") },
	[COL_CLOCK]      = { "CLOCK",      1, 0,              SCOLS_JSON_STRING, N_("symbolic name") },
	[COL_NAME]       = { "NAME",       1, 0,              SCOLS_JSON_STRING, N_("readable name") },
	[COL_TIME]       = { "TIME",       1, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, N_("numeric time") },
	[COL_ISO_TIME]   = { "ISO_TIME",   1, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, N_("human readable ISO time") },
	[COL_RESOL]      = { "RESOL",      1, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, N_("human readable resolution") },
	[COL_RESOL_RAW]  = { "RESOL_RAW",  1, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, N_("resolution") },
	[COL_REL_TIME]   = { "REL_TIME",   1, SCOLS_FL_RIGHT, SCOLS_JSON_STRING, N_("human readable relative time") },
	[COL_NS_OFFSET]  = { "NS_OFFSET",  1, SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER, N_("namespace offset") },
};

static int column_name_to_id(const char *name, size_t namesz)
{
	size_t i;

	for (i = 0; i < ARRAY_SIZE(infos); i++) {
		const char *cn = infos[i].name;

		if (!strncasecmp(name, cn, namesz) && !*(cn + namesz))
			return i;
	}
	warnx(_("unknown column: %s"), name);

	return -1;
}

static void __attribute__((__noreturn__)) usage(void)
{
	FILE *out = stdout;
	size_t i;

	fputs(USAGE_HEADER, out);
	fprintf(out, _(" %s [options]\n"), program_invocation_short_name);

	fputs(USAGE_OPTIONS, out);
	fputs(_(" -J, --json                 use JSON output format\n"), out);
	fputs(_(" -n, --noheadings           don't print headings\n"), out);
	fputs(_(" -o, --output <list>        output columns\n"), out);
	fputs(_("     --output-all           output all columns\n"), out);
	fputs(_(" -r, --raw                  use raw output format\n"), out);
	fputs(_(" -t, --time <clock>         show current time of single clock\n"), out);
	fputs(_(" --no-discover-dynamic      do not try to discover dynamic clocks\n"), out);
	fputs(_(" -d, --dynamic-clock <path> also display specified dynamic clock\n"), out);
	fputs(_(" -c, --cpu-clock <pid>      also display CPU clock of specified process\n"), out);

	fputs(USAGE_SEPARATOR, out);
	fprintf(out, USAGE_HELP_OPTIONS(29));

	fputs(USAGE_COLUMNS, out);

	for (i = 0; i < ARRAY_SIZE(infos); i++)
		fprintf(out, " %16s  %-10s%s\n", infos[i].name,
			infos[i].json_type == SCOLS_JSON_STRING?  "<string>":
			infos[i].json_type == SCOLS_JSON_ARRAY_STRING?  "<string>":
			infos[i].json_type == SCOLS_JSON_ARRAY_NUMBER?  "<string>":
			infos[i].json_type == SCOLS_JSON_NUMBER?  "<number>":
			"<boolean>",
			_(infos[i].help));

	fprintf(out, USAGE_MAN_TAIL("lsclocks(1)"));

	exit(EXIT_SUCCESS);
}

static void scols_line_format_timespec(struct libscols_line *ln, size_t n, const struct timespec *ts)
{
	scols_line_sprintf(ln, n, "%ju.%09" PRId32, (uintmax_t) ts->tv_sec, (uint32_t) ts->tv_nsec);
}

static clockid_t parse_clock(const char *name)
{
	size_t i;
	uint32_t id = -1;
	int rc;

	rc = ul_strtou32(name, &id, 10);

	for (i = 0; i < ARRAY_SIZE(clocks); i++) {
		if (!strcmp(name, clocks[i].id_name)
		    || !strcmp(name, clocks[i].name))
			return clocks[i].id;
		if (rc == 0 && (clockid_t) id == clocks[i].id)
			return id;
	}

	errx(EXIT_FAILURE, _("Unknown clock: %s"), name);
}

static int64_t get_namespace_offset(const char *name)
{
	char *tokstr, *buf, *saveptr, *line, *space;
	uint64_t ret;
	int fd;

	fd = open(_PATH_PROC_TIMENS_OFF, O_RDONLY);
	if (fd == -1)
		err(EXIT_FAILURE, _("Could not open %s"), _PATH_PROC_TIMENS_OFF);

	read_all_alloc(fd, &buf);

	for (tokstr = buf; ; tokstr = NULL) {
		line = strtok_r(tokstr, "\n", &saveptr);
		if (!line)
			continue;
		line = (char *) startswith(line, name);
		if (!line || line[0] != ' ')
			continue;

		line = (char *) skip_blank(line);
		space = strchr(line, ' ');
		if (space)
			*space = '\0';
		ret = strtos64_or_err(line, _("Invalid offset"));
		break;
	}

	free(buf);
	close(fd);
	return ret;
}

static void add_clock_line(struct libscols_table *tb, const int *columns,
			   size_t ncolumns, const struct clockinfo *clockinfo,
			   const struct timespec *now, const struct timespec *resolution)
{
	char buf[FORMAT_TIMESTAMP_MAX];
	struct libscols_line *ln;
	size_t i;
	int rc;

	ln = scols_table_new_line(tb, NULL);
	if (!ln)
		errx(EXIT_FAILURE, _("failed to allocate output line"));

	for (i = 0; i < ncolumns; i++) {
		switch (columns[i]) {
			case COL_TYPE:
				scols_line_set_data(ln, i, clock_type_name(clockinfo->type));
				break;
			case COL_ID:
				if (!clockinfo->no_id)
					scols_line_sprintf(ln, i, "%ju", (uintmax_t) clockinfo->id);
				break;
			case COL_CLOCK:
				scols_line_set_data(ln, i, clockinfo->id_name);
				break;
			case COL_NAME:
				scols_line_set_data(ln, i, clockinfo->name);
				break;
			case COL_TIME:
				if (now->tv_nsec == -1)
					break;

				scols_line_format_timespec(ln, i, now);
				break;
			case COL_ISO_TIME:
				if (now->tv_nsec == -1)
					break;

				rc = strtimespec_iso(now,
						ISO_GMTIME | ISO_DATE | ISO_TIME | ISO_T | ISO_DOTNSEC | ISO_TIMEZONE,
						buf, sizeof(buf));
				if (rc)
					errx(EXIT_FAILURE, _("failed to format iso time"));
				scols_line_set_data(ln, i, buf);
				break;
			case COL_RESOL:
				if (resolution->tv_nsec == -1)
					break;

				rc = strtimespec_relative(resolution, buf, sizeof(buf));
				if (rc)
					errx(EXIT_FAILURE, _("failed to format relative time"));
				scols_line_set_data(ln, i, buf);
				break;
			case COL_RESOL_RAW:
				if (resolution->tv_nsec == -1)
					break;
				scols_line_format_timespec(ln, i, resolution);
				break;
			case COL_REL_TIME:
				if (now->tv_nsec == -1)
					break;
				rc = strtimespec_relative(now, buf, sizeof(buf));
				if (rc)
					errx(EXIT_FAILURE, _("failed to format relative time"));
				scols_line_set_data(ln, i, buf);
				break;
			case COL_NS_OFFSET:
				if (clockinfo->ns_offset_name)
					scols_line_sprintf(ln, i, "%"PRId64,
							   get_namespace_offset(clockinfo->ns_offset_name));
				break;
		}
	}
}

static void add_posix_clock_line(struct libscols_table *tb, const int *columns,
			         size_t ncolumns, const struct clockinfo *clockinfo)
{
	struct timespec resolution, now;
	int rc;

	rc = clock_gettime(clockinfo->id, &now);
	if (rc)
		now.tv_nsec = -1;

	rc = clock_getres(clockinfo->id, &resolution);
	if (rc)
		resolution.tv_nsec = -1;

	add_clock_line(tb, columns, ncolumns, clockinfo, &now, &resolution);
}

struct path_clock {
	struct list_head head;
	const char * path;
};

static void add_dynamic_clock_from_path(struct libscols_table *tb,
					const int *columns, size_t ncolumns,
					const char *path, bool explicit)
{
	int fd = open(path, O_RDONLY);
	if (fd == -1) {
		if (explicit)
			err(EXIT_FAILURE, _("Could not open %s"), path);
		else
			return;
	}

	struct clockinfo clockinfo = {
		.type = CT_PTP,
		.id = FD_TO_CLOCKID(fd),
		.no_id = true,
		.id_name = path,
		.name = path,
	};
	add_posix_clock_line(tb, columns, ncolumns, &clockinfo);
	close(fd);
}

static void add_dynamic_clocks_from_discovery(struct libscols_table *tb,
					      const int *columns, size_t ncolumns)
{
	int rc;
	size_t i;
	glob_t state;

	rc = glob("/dev/ptp*", 0, NULL, &state);
	if (rc == GLOB_NOMATCH)
		return;
	else if (rc)
		errx(EXIT_FAILURE, _("Could not glob: %d"), rc);

	for (i = 0; i < state.gl_pathc; i++)
		add_dynamic_clock_from_path(tb, columns, ncolumns,
					    state.gl_pathv[i], false);

	globfree(&state);
}

static void add_rtc_clock_from_path(struct libscols_table *tb,
				    const int *columns, size_t ncolumns,
				    const char *path, bool explicit)
{
	int fd, rc;
	struct rtc_time rtc_time;
	struct tm tm = { 0 };
	struct timespec now = { 0 }, resolution = { .tv_nsec = -1 };

	fd = open(path, O_RDONLY);
	if (fd == -1) {
		if (explicit)
			err(EXIT_FAILURE, _("Could not open %s"), path);
		else
			return;
	}

	rc = ioctl(fd, RTC_RD_TIME, &rtc_time);
	if (rc)
		err(EXIT_FAILURE,
		    _("ioctl(RTC_RD_NAME) to %s to read the time failed"), path);

	tm.tm_sec  = rtc_time.tm_sec;
	tm.tm_min  = rtc_time.tm_min;
	tm.tm_hour = rtc_time.tm_hour;
	tm.tm_mday = rtc_time.tm_mday;
	tm.tm_mon  = rtc_time.tm_mon;
	tm.tm_year = rtc_time.tm_year;
	tm.tm_wday = rtc_time.tm_wday;
	tm.tm_yday = rtc_time.tm_yday;

	now.tv_sec = mktime(&tm);

	struct clockinfo clockinfo = {
		.type = CT_RTC,
		.no_id = true,
		.id_name = path,
		.name = path,
	};
	add_clock_line(tb, columns, ncolumns, &clockinfo, &now, &resolution);

	close(fd);
}

static void add_rtc_clocks_from_discovery(struct libscols_table *tb,
					  const int *columns, size_t ncolumns)
{
	int rc;
	size_t i;
	glob_t state;

	rc = glob("/dev/rtc*", 0, NULL, &state);
	if (rc == GLOB_NOMATCH)
		return;
	if (rc)
		errx(EXIT_FAILURE, _("Could not glob: %d"), rc);

	for (i = 0; i < state.gl_pathc; i++)
		add_rtc_clock_from_path(tb, columns, ncolumns,
					    state.gl_pathv[i], false);

	globfree(&state);
}

struct cpu_clock {
	struct list_head head;
	pid_t pid;
	char name[sizeof(stringify_value(SINT_MAX(pid_t)))];
};

static void add_cpu_clock(struct libscols_table *tb,
			  const int *columns, size_t ncolumns,
			  pid_t pid, const char *name)
{
	int rc;
	clockid_t clockid;

	rc = clock_getcpuclockid(pid, &clockid);
	if (rc)
		errx(EXIT_FAILURE, _("Could not get CPU clock of process %jd: %s"),
				   (intmax_t) pid, strerror(rc));

	struct clockinfo clockinfo = {
		.type = CT_CPU,
		.id = clockid,
		.name = name,
		.no_id = true,
	};
	add_posix_clock_line(tb, columns, ncolumns, &clockinfo);
}


int main(int argc, char **argv)
{
	size_t i;
	int c, rc;
	const struct colinfo *colinfo;

	struct libscols_table *tb;
	struct libscols_column *col;

	bool noheadings = false, raw = false, json = false,
	     disc_dynamic = true, disc_rtc = true;
	const char *outarg = NULL;
	int columns[ARRAY_SIZE(infos) * 2];
	size_t ncolumns = 0;
	clockid_t clock = -1;
	struct path_clock *path_clock;
	struct cpu_clock *cpu_clock;
	struct list_head *current_path_clock, *current_cpu_clock;
	struct list_head dynamic_clocks, cpu_clocks, rtc_clocks;

	struct timespec now;

	enum {
		OPT_OUTPUT_ALL = CHAR_MAX + 1,
		OPT_NO_DISC_DYN,
		OPT_NO_DISC_RTC,
	};
	static const struct option longopts[] = {
		{ "noheadings",          no_argument,       NULL, 'n' },
		{ "output",              required_argument, NULL, 'o' },
		{ "output-all",	         no_argument,       NULL, OPT_OUTPUT_ALL },
		{ "version",             no_argument,       NULL, 'V' },
		{ "help",	         no_argument,       NULL, 'h' },
		{ "json",                no_argument,       NULL, 'J' },
		{ "raw",                 no_argument,       NULL, 'r' },
		{ "time",                required_argument, NULL, 't' },
		{ "no-discover-dynamic", no_argument,       NULL, OPT_NO_DISC_DYN },
		{ "dynamic-clock",       required_argument, NULL, 'd' },
		{ "cpu-clock",           required_argument, NULL, 'c' },
		{ "no-discover-rtc",     no_argument,       NULL, OPT_NO_DISC_RTC },
		{ "rtc",                 required_argument, NULL, 'x' },
		{ 0 }
	};

	setlocale(LC_ALL, "");
	bindtextdomain(PACKAGE, LOCALEDIR);
	textdomain(PACKAGE);
	close_stdout_atexit();

	INIT_LIST_HEAD(&dynamic_clocks);
	INIT_LIST_HEAD(&cpu_clocks);
	INIT_LIST_HEAD(&rtc_clocks);

	while ((c = getopt_long(argc, argv, "no:Jrt:d:c:x:Vh", longopts, NULL)) != -1) {
		switch (c) {
		case 'n':
			noheadings = true;
			break;
		case 'o':
			outarg = optarg;
			break;
		case OPT_OUTPUT_ALL:
			for (ncolumns = 0; ncolumns < ARRAY_SIZE(infos); ncolumns++)
				columns[ncolumns] = ncolumns;
			break;
		case 'J':
			json = true;
			break;
		case 'r':
			raw = true;
			break;
		case 't':
			clock = parse_clock(optarg);
			break;
		case 'd':
			path_clock = xmalloc(sizeof(*path_clock));
			path_clock->path = optarg;
			list_add(&path_clock->head, &dynamic_clocks);
			break;
		case OPT_NO_DISC_DYN:
			disc_dynamic = false;
			break;
		case 'c':
			cpu_clock = xmalloc(sizeof(*cpu_clock));
			cpu_clock->pid = strtopid_or_err(optarg, _("failed to parse pid"));
			snprintf(cpu_clock->name, sizeof(cpu_clock->name),
				 "%jd", (intmax_t) cpu_clock->pid);
			list_add(&cpu_clock->head, &cpu_clocks);
			break;
		case 'x':
			path_clock = xmalloc(sizeof(*path_clock));
			path_clock->path = optarg;
			list_add(&path_clock->head, &rtc_clocks);
			break;
		case OPT_NO_DISC_RTC:
			disc_rtc = false;
			break;
		case 'V':
			print_version(EXIT_SUCCESS);
		case 'h':
			usage();
		default:
			errtryhelp(EXIT_FAILURE);
		}
	}

	if (argv[optind])
		errtryhelp(EXIT_FAILURE);

	if (clock != -1) {
		rc = clock_gettime(clock, &now);
		if (rc)
			err(EXIT_FAILURE, _("failed to get time"));
		printf("%ju.%09"PRId32"\n", (uintmax_t) now.tv_sec, (uint32_t) now.tv_nsec);
		return EXIT_SUCCESS;
	}

	if (!ncolumns) {
		columns[ncolumns++] = COL_ID;
		columns[ncolumns++] = COL_NAME;
		columns[ncolumns++] = COL_TYPE;
		columns[ncolumns++] = COL_TIME;
		columns[ncolumns++] = COL_RESOL;
		columns[ncolumns++] = COL_ISO_TIME;
	}

	if (outarg && string_add_to_idarray(outarg, columns, ARRAY_SIZE(columns),
					    &ncolumns, column_name_to_id) < 0)
		return EXIT_FAILURE;

	scols_init_debug(0);

	tb = scols_new_table();
	if (!tb)
		errx(EXIT_FAILURE, _("failed to allocate output table"));
	scols_table_set_name(tb, "clocks");

	for (i = 0; i < ncolumns; i++) {
		colinfo = &infos[columns[i]];

		col = scols_table_new_column(tb, colinfo->name, colinfo->whint, colinfo->flags);
		if (!col)
			errx(EXIT_FAILURE, _("failed to allocate output column"));

		scols_column_set_json_type(col, colinfo->json_type);
	}

	for (i = 0; i < ARRAY_SIZE(clocks); i++)
		add_posix_clock_line(tb, columns, ncolumns, &clocks[i]);

	if (disc_dynamic)
		add_dynamic_clocks_from_discovery(tb, columns, ncolumns);

	list_for_each(current_path_clock, &dynamic_clocks) {
		path_clock = list_entry(current_path_clock, struct path_clock, head);
		add_dynamic_clock_from_path(tb, columns, ncolumns, path_clock->path, true);
	}

	list_free(&dynamic_clocks, struct path_clock, head, free);

	if (disc_rtc)
		add_rtc_clocks_from_discovery(tb, columns, ncolumns);

	list_for_each(current_path_clock, &rtc_clocks) {
		path_clock = list_entry(current_path_clock, struct path_clock, head);
		add_rtc_clock_from_path(tb, columns, ncolumns, path_clock->path, true);
	}

	list_free(&rtc_clocks, struct path_clock, head, free);

	list_for_each(current_cpu_clock, &cpu_clocks) {
		cpu_clock = list_entry(current_cpu_clock, struct cpu_clock, head);
		add_cpu_clock(tb, columns, ncolumns, cpu_clock->pid, cpu_clock->name);
	}

	list_free(&cpu_clocks, struct cpu_clock, head, free);

	scols_table_enable_json(tb, json);
	scols_table_enable_raw(tb, raw);
	scols_table_enable_noheadings(tb, noheadings);
	scols_print_table(tb);
	scols_unref_table(tb);
}
