/**
 * @file lstab.c
 * @note Copyright (C) 2012 Richard Cochran <richardcochran@gmail.com>
 * @note SPDX-License-Identifier: GPL-2.0+
 */
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>

#include "lstab.h"

/*
 * Keep a history of the TAI - UTC offset in a lookup table.
 *
 * Each entry gives the NTP time when a new TAI offset came into
 * effect. This is always the second immediately after a leap second.
 *
 * The size of the table is the number of entries from the NIST table,
 * plus room for two hundred more entries to be added at run time.
 * Since there can be at most two leap seconds per year, this allows
 * for at least one hundred years.
 *
 * Information about leap seconds is published by the IERS in the
 * bulletin:
 * https://hpiers.obspm.fr/iers/bul/bulc/bulletinc.dat
 *
 * The table data is available from
 * https://hpiers.obspm.fr/iers/bul/bulc/ntp/leap-seconds.list
 *
 * Backup source:
 * https://data.iana.org/time-zones/tzdb/leapseconds
 *
 * When updating this table, do not forget to set N_HISTORICAL_LEAPS
 * and the expiration date.
 */

#define BASE_TAI_OFFSET		10
#define N_HISTORICAL_LEAPS	28
#define N_LEAPS			(N_HISTORICAL_LEAPS + 200)
#define NTP_UTC_OFFSET		2208988800ULL

struct epoch_marker {
	int offset;	/* TAI - UTC offset of epoch */
	uint64_t ntp;	/* NTP time of epoch */
	uint64_t tai;	/* TAI time of epoch */
	uint64_t utc;	/* UTC time of epoch */
};

struct lstab {
	struct epoch_marker lstab[N_LEAPS];
	uint64_t expiration_utc;
	const char *leapfile;
	time_t lsfile_mtime;
	int length;
};

static const uint64_t expiration_date_ntp = 3944332800ULL; /* 24 Dec 2024 */

static const uint64_t offset_table[N_LEAPS * 2] = {
	2272060800ULL,	10,	/* 1 Jan 1972 */
	2287785600ULL,	11,	/* 1 Jul 1972 */
	2303683200ULL,	12,	/* 1 Jan 1973 */
	2335219200ULL,	13,	/* 1 Jan 1974 */
	2366755200ULL,	14,	/* 1 Jan 1975 */
	2398291200ULL,	15,	/* 1 Jan 1976 */
	2429913600ULL,	16,	/* 1 Jan 1977 */
	2461449600ULL,	17,	/* 1 Jan 1978 */
	2492985600ULL,	18,	/* 1 Jan 1979 */
	2524521600ULL,	19,	/* 1 Jan 1980 */
	2571782400ULL,	20,	/* 1 Jul 1981 */
	2603318400ULL,	21,	/* 1 Jul 1982 */
	2634854400ULL,	22,	/* 1 Jul 1983 */
	2698012800ULL,	23,	/* 1 Jul 1985 */
	2776982400ULL,	24,	/* 1 Jan 1988 */
	2840140800ULL,	25,	/* 1 Jan 1990 */
	2871676800ULL,	26,	/* 1 Jan 1991 */
	2918937600ULL,	27,	/* 1 Jul 1992 */
	2950473600ULL,	28,	/* 1 Jul 1993 */
	2982009600ULL,	29,	/* 1 Jul 1994 */
	3029443200ULL,	30,	/* 1 Jan 1996 */
	3076704000ULL,	31,	/* 1 Jul 1997 */
	3124137600ULL,	32,	/* 1 Jan 1999 */
	3345062400ULL,	33,	/* 1 Jan 2006 */
	3439756800ULL,	34,	/* 1 Jan 2009 */
	3550089600ULL,	35,	/* 1 Jul 2012 */
	3644697600ULL,	36,	/* 1 Jul 2015 */
	3692217600ULL,	37,	/* 1 Jan 2017 */
};

static void epoch_marker_init(struct epoch_marker *ls, uint64_t val, int offset)
{
	ls->ntp = val;
	ls->utc = val - NTP_UTC_OFFSET;
	ls->tai = val - NTP_UTC_OFFSET + offset;
	ls->offset = offset;
}

static void lstab_init(struct lstab *lstab)
{
	struct epoch_marker *ls;
	uint64_t offset, val;
	int i;

	for (i = 0; i < N_HISTORICAL_LEAPS; i++) {
		ls = lstab->lstab + i;
		val = offset_table[2 * i];
		offset = offset_table[2 * i + 1];
		epoch_marker_init(ls, val, offset);
	}
	lstab->expiration_utc = expiration_date_ntp - NTP_UTC_OFFSET;
	lstab->length = i;
}

void lstab_print(struct lstab *lstab, FILE *fp)
{
	int i, len = lstab->length;

	fprintf(fp, "%3s%12s%12s%12s%4s\n", "idx", "NTP", "TAI", "UTC", "OFF");
	for (i = 0; i < len; i++) {
		fprintf(fp, "%3d" "%12" PRIu64 "%12" PRIu64 "%12" PRIu64 "%4d\n", i,
			lstab->lstab[i].ntp, lstab->lstab[i].tai,
			lstab->lstab[i].utc, lstab->lstab[i].offset);
	}
}

static int lstab_read(struct lstab *lstab, const char *name)
{
	uint64_t expiration, val;
	struct epoch_marker *ls;
	int index = 0, offset;
	char buf[1024];
	FILE *fp;

	fp = fopen(name, "r");
	if (!fp) {
		fprintf(stderr, "failed to open '%s' for reading: %m\n", name);
		return -1;
	}
	while (index < N_LEAPS) {
		if (!fgets(buf, sizeof(buf), fp)) {
			break;
		}
		if (1 == sscanf(buf, "#@ %" PRIu64, &expiration)) {
			lstab->expiration_utc = expiration - NTP_UTC_OFFSET;
			continue;
		}
		if (2 == sscanf(buf, "%" PRIu64 " %d", &val, &offset)) {
			ls = lstab->lstab + index;
			epoch_marker_init(ls, val, offset);
			index++;
		}
	}
	fclose(fp);
	if (!lstab->expiration_utc) {
		fprintf(stderr, "missing expiration date in '%s'\n", name);
		return -1;
	}
	lstab->length = index;

	return 0;
}

struct lstab *lstab_create(const char *filename)
{
	struct lstab *lstab = calloc(1, sizeof(*lstab));
	struct stat statbuf;
	int err;

	if (!lstab) {
		return NULL;
	}
	if (filename && filename[0]) {
		if (lstab_read(lstab, filename)) {
			free(lstab);
			return NULL;
		}
		lstab->leapfile = filename;

		err = stat(lstab->leapfile, &statbuf);
		if (err) {
			fprintf(stderr, "file status failed on %s: %m",
				lstab->leapfile);
			free(lstab);
			return NULL;
		}

		lstab->lsfile_mtime = statbuf.st_mtim.tv_sec;

	} else {
		lstab_init(lstab);
	}
	return lstab;
}

int update_leapsecond_table(struct lstab *lstab)
{
	struct stat statbuf;
	int err;

	if (!lstab->leapfile) {
		return 0;
	}
	err = stat(lstab->leapfile, &statbuf);
	if (err) {
		fprintf(stderr, "file status failed on %s: %m",
			lstab->leapfile);
		return -1;
	}
	if (lstab->lsfile_mtime == statbuf.st_mtim.tv_sec) {
		return 0;
	}
	printf("updating leap seconds file\n");

	if (lstab_read(lstab, lstab->leapfile)) {
		lstab->length = 0;
		return -1;
	}

	lstab->lsfile_mtime = statbuf.st_mtim.tv_sec;

	return 0;
}

void lstab_destroy(struct lstab *lstab)
{
	free(lstab);
}

enum lstab_result lstab_utc2tai(struct lstab *lstab, uint64_t utctime,
				int *tai_offset)
{
	int epoch = -1, index, next;

	if (update_leapsecond_table(lstab)) {
		fprintf(stderr, "Failed to update leap seconds table");
		return LSTAB_UNKNOWN;
	}

	for (index = lstab->length - 1; index > -1; index--) {
		if (utctime >= lstab->lstab[index].utc) {
			epoch = index;
			break;
		}
	}

	if (epoch == -1) {
		return LSTAB_UNKNOWN;
	}

	*tai_offset = lstab->lstab[epoch].offset;
	next = epoch + 1;

	if (next < lstab->length && utctime == lstab->lstab[next].utc - 1) {
		return LSTAB_AMBIGUOUS;
	}

	if (utctime > lstab->expiration_utc) {
		return LSTAB_EXPIRED;
	}

	return LSTAB_OK;
}
