/**
 * @file ts2phc_generic_pps_source.c
 * @note Copyright (C) 2019 Richard Cochran <richardcochran@gmail.com>
 * @note SPDX-License-Identifier: GPL-2.0+
 */
#include <stdlib.h>
#include <time.h>

#include "lstab.h"
#include "missing.h"
#include "print.h"
#include "ts2phc_generic_pps_source.h"
#include "ts2phc_pps_source_private.h"
#include "util.h"

struct ts2phc_generic_pps_source {
	struct ts2phc_pps_source pps_source;
	struct lstab *lstab;
};

static void ts2phc_generic_pps_source_destroy(struct ts2phc_pps_source *src)
{
	struct ts2phc_generic_pps_source *s =
		container_of(src, struct ts2phc_generic_pps_source, pps_source);

	if (s->lstab) {
		lstab_destroy(s->lstab);
	}

	free(s);
}

static int get_ntx(struct timex *ntx)
{
	int code;

	if (!ntx)
		return -1;

	memset(ntx, 0, sizeof(*ntx));
	code = adjtimex(ntx);
	if (code == -1) {
		pr_err("adjtimex failed: %m");
		return -1;
	}
	return 0;
}

/*
 * Returns the time on the PPS source device at which the most recent
 * PPS event was generated.  This implementation assumes that the
 * system time is approximately correct.
 */
static int ts2phc_generic_pps_source_getppstime(struct ts2phc_pps_source *src,
						struct timespec *ts)
{
	struct ts2phc_generic_pps_source *s =
		container_of(src, struct ts2phc_generic_pps_source, pps_source);
	enum lstab_result result;
	int64_t utc_time;
	struct timex ntx;
	int tai_offset;

	if (get_ntx(&ntx)) {
		return -1;
	}

	tai_offset = ntx.tai;

	/* When TAI offset is not set in system - try to get it from leapfile */
	if (tai_offset == 0) {
		if (!s->lstab) {
			return -1;
		}

		utc_time = ntx.time.tv_sec;
		result = lstab_utc2tai(s->lstab, utc_time, &tai_offset);

		switch (result) {
		case LSTAB_OK:
			break;
		case LSTAB_UNKNOWN:
			pr_err("Unable to find utc time in leap second table");
			return -1;
		case LSTAB_EXPIRED:
			pr_err("UTC time is past leap second table expiry date");
			return -1;
		case LSTAB_AMBIGUOUS:
			pr_err("UTC time stamp is ambiguous");
			return -1;
		}
	}

	ts->tv_sec  = ntx.time.tv_sec + tai_offset;
	if (ntx.status & STA_NANO)
		ts->tv_nsec = ntx.time.tv_usec;
	else
		ts->tv_nsec = ntx.time.tv_usec * 1000;

	return 0;
}

struct ts2phc_pps_source *ts2phc_generic_pps_source_create(struct ts2phc_private *priv,
							   const char *dev)
{
	struct ts2phc_generic_pps_source *src;
	const char *leapfile;
	struct timex ntx;

	if (get_ntx(&ntx)) {
		return NULL;
	}

	src = calloc(1, sizeof(*src));
	if (!src) {
		return NULL;
	}

	if (ntx.tai == 0) {
		pr_err("UTC-TAI offset not set in system! Trying to revert to leapfile");

		leapfile = config_get_string(priv->cfg, NULL, "leapfile");
		src->lstab = lstab_create(leapfile);
		if (!src->lstab) {
			free(src);
			return NULL;
		}
	}

	src->pps_source.destroy = ts2phc_generic_pps_source_destroy;
	src->pps_source.getppstime = ts2phc_generic_pps_source_getppstime;

	return &src->pps_source;
}
