// SPDX-License-Identifier: MIT
#include <assert.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <libHX/defs.h>
#include <libHX/init.h>
#include <libHX/misc.h>
#include "internal.h"

typedef struct timespec *(*add_func_t)(struct timespec *,
	const struct timespec *, const struct timespec *);
typedef struct timespec *(*mul_func_t)(struct timespec *,
	const struct timespec *, int);
typedef struct timespec *(*mulf_func_t)(struct timespec *,
	const struct timespec *, double);

static const int NANOSECOND = 1000000000;
static const long long NANOSECOND_LL = 1000000000;
static const unsigned int clock_id = CLOCK_THREAD_CPUTIME_ID;
static const unsigned int step = 1000;
static const long step_mul = 10000000;

static const struct timespec pairs[] = {
	{-1, 700000000}, {-1, 400000000}, {-1, 0},
	{0, -700000000}, {0, -400000000}, {0, 0},
	{0, 400000000}, {0, 700000000},
	{1, 0}, {1, 400000000}, {1, 700000000},
};

/*
 * The playing fields it not level at all!
 * HX_timespec_add_DIVQ benefits from inlining and near jumps,
 * while HX_timespec_add has to go via PLT, and also has the PIC tax.
 * It is actually worse by 7% on i7-8250U/-m64.
 */
static struct timespec *HX_timespec_add_DIVQ(struct timespec *r,
    const struct timespec *a, const struct timespec *b)
{
	long long p, q;

	p  = a->tv_sec * NANOSECOND_LL +
	     ((a->tv_sec >= 0) ? a->tv_nsec : -a->tv_nsec);
	q  = b->tv_sec * NANOSECOND_LL +
	     ((b->tv_sec >= 0) ? b->tv_nsec : -b->tv_nsec);

	p += q;
	r->tv_sec  = p / NANOSECOND;
	r->tv_nsec = p % NANOSECOND;
	if (r->tv_sec < 0 && r->tv_nsec < 0)
		r->tv_nsec = -r->tv_nsec;
	return r;
}

/*
 * Variant that does split multiplication.
 */
static struct timespec *
HX_timespec_mul_SPL(struct timespec *r, const struct timespec *a, int f)
{
	long long nsec;
	bool neg = HX_timespec_isneg(a);

	if (neg)
		HX_timespec_neg(r, a);
	else
		*r = *a;
	if (f < 0) {
		f = -f;
		neg = !neg;
	}

	r->tv_sec *= f;
	nsec = static_cast(long long, r->tv_nsec) * f;
	r->tv_sec += nsec / NANOSECOND;
	r->tv_nsec = nsec % NANOSECOND;
	if (neg)
		HX_timespec_neg(r, r);
	return r;
}

/*
 * Variant for mulf that uses seconds rather than nanosecond as working base.
 * This shows itself to be detrimental to precision (on IEEE-754).
 */
static struct timespec *
HX_timespec_mulf_S(struct timespec *r, const struct timespec *a, double f)
{
	double i, t;

	t  = ((a->tv_sec >= 0) ? a->tv_nsec : -a->tv_nsec) /
	     static_cast(double, NANOSECOND);
	t += a->tv_sec;
	t *= f;
	t  = modf(t, &i);
	r->tv_nsec = t * NANOSECOND;
	r->tv_sec  = i;
	if (r->tv_sec < 0 && r->tv_nsec < 0)
		r->tv_nsec = -r->tv_nsec;
	return r;
}

static void test_basic(void)
{
	struct timeval a = {1, 769298}, b = {2, 430520}, c;
	struct timespec p = {1, 769298000}, q = {2, 430520000}, r;
	HX_timeval_sub(&c, &b, &a);
	HX_timespec_sub(&r, &q, &p);
	printf(HX_TIMEVAL_FMT "\n", HX_TIMEVAL_EXP(&c));
	printf(HX_TIMESPEC_FMT "\n", HX_TIMESPEC_EXP(&r));
	assert(c.tv_sec == 0);
	assert(c.tv_usec == 661222);
	assert(r.tv_sec == 0);
	assert(r.tv_nsec == 661222000);
}

static void test_same(void)
{
	static const struct timespec zero = {0, 0};
	struct timespec r;
	unsigned int i;

	printf("# Test src==dst operand behavior\n");

	/* 1s */
	for (i = 0; i < ARRAY_SIZE(pairs); ++i) {
		r = pairs[i];
		printf("-(" HX_TIMESPEC_FMT ") = ", HX_TIMESPEC_EXP(&r));
		HX_timespec_neg(&r, &r);
		printf(HX_TIMESPEC_FMT "\n", HX_TIMESPEC_EXP(&r));
	}
	printf("\n");

	for (i = 0; i < ARRAY_SIZE(pairs); ++i) {
		r = pairs[i];
		printf(HX_TIMESPEC_FMT " + 0 = ", HX_TIMESPEC_EXP(&r));
		HX_timespec_add(&r, &r, &zero);
		printf(HX_TIMESPEC_FMT "\n", HX_TIMESPEC_EXP(&r));
	}
	printf("\n");

	for (i = 0; i < ARRAY_SIZE(pairs); ++i) {
		r = pairs[i];
		printf(HX_TIMESPEC_FMT " - 0 = ", HX_TIMESPEC_EXP(&r));
		HX_timespec_sub(&r, &r, &zero);
		printf(HX_TIMESPEC_FMT "\n", HX_TIMESPEC_EXP(&r));
	}
	printf("\n");

	for (i = 0; i < ARRAY_SIZE(pairs); ++i) {
		r = pairs[i];
		printf(HX_TIMESPEC_FMT " * 1 = ", HX_TIMESPEC_EXP(&r));
		HX_timespec_mul(&r, &r, 1);
		printf(HX_TIMESPEC_FMT "\n", HX_TIMESPEC_EXP(&r));
	}
	printf("\n");

	/* 2s */
	for (i = 0; i < ARRAY_SIZE(pairs); ++i) {
		r = pairs[i];
		printf(HX_TIMESPEC_FMT " + " HX_TIMESPEC_FMT " = ",
		       HX_TIMESPEC_EXP(&r), HX_TIMESPEC_EXP(&r));
		HX_timespec_add(&r, &r, &r);
		printf(HX_TIMESPEC_FMT "\n", HX_TIMESPEC_EXP(&r));
	}
	printf("\n");

	for (i = 0; i < ARRAY_SIZE(pairs); ++i) {
		r = pairs[i];
		printf(HX_TIMESPEC_FMT " - " HX_TIMESPEC_FMT " = ",
		       HX_TIMESPEC_EXP(&r), HX_TIMESPEC_EXP(&r));
		HX_timespec_sub(&r, &r, &r);
		printf(HX_TIMESPEC_FMT "\n", HX_TIMESPEC_EXP(&r));
	}
	printf("\n");
}

static void print_sgn(const struct timespec *a)
{
	printf(HX_timespec_isneg(a) ? "[-]" : "[+]");
}

static void test_neg(void)
{
	const struct timespec *now;
	struct timespec then;

	printf("# Negation\n");
	for (now = pairs; now < pairs + ARRAY_SIZE(pairs); ++now) {
		HX_timespec_neg(&then, now);

		print_sgn(now);
		printf(HX_TIMESPEC_FMT " -> ", HX_TIMESPEC_EXP(now));
		print_sgn(&then);
		printf(HX_TIMESPEC_FMT "\n", HX_TIMESPEC_EXP(&then));
	}
	printf("\n");
}

static void print_op2(const struct timespec *r, const struct timespec *a,
    const char *op, const struct timespec *b)
{
	printf(HX_TIMESPEC_FMT " %s ", HX_TIMESPEC_EXP(a), op);
	printf(HX_TIMESPEC_FMT " = ", HX_TIMESPEC_EXP(b));
	printf(HX_TIMESPEC_FMT "\n", HX_TIMESPEC_EXP(r));
}

static void test_add(void)
{
	const struct timespec *a, *b;
	struct timespec r, s;

	printf("# Test addition behavior\n");
	for (a = pairs; a < pairs + ARRAY_SIZE(pairs); ++a) {
		for (b = pairs; b < pairs + ARRAY_SIZE(pairs); ++b) {
			HX_timespec_add(&r, a, b);
			print_op2(&r, a, "+L", b);
			HX_timespec_add_DIVQ(&s, a, b);
			print_op2(&r, a, "+Q", b);
			if (r.tv_sec != s.tv_sec || r.tv_nsec != s.tv_nsec)
				abort();
			HX_timespec_sub(&r, a, b);
			print_op2(&r, a, "- ", b);
			printf("----------\n");
		}
	}
	printf("\n");
}

static void test_adds_nz(time_t s, add_func_t fn)
{
	struct timespec a, b, r;

	a.tv_sec = s;
	for (a.tv_nsec = 0; a.tv_nsec < NANOSECOND;
	     a.tv_nsec += NANOSECOND / step)
	{
		b.tv_sec = -1;
		for (b.tv_nsec = 0; b.tv_nsec < NANOSECOND;
		     b.tv_nsec += NANOSECOND / step)
			(*fn)(&r, &a, &b);

		b.tv_sec = 0;
		for (b.tv_nsec = -NANOSECOND + NANOSECOND / step;
		     b.tv_nsec < NANOSECOND; b.tv_nsec += NANOSECOND / step)
			(*fn)(&r, &a, &b);

		b.tv_sec = 1;
		for (b.tv_nsec = 0; b.tv_nsec < NANOSECOND;
		     b.tv_nsec += NANOSECOND / step)
			(*fn)(&r, &a, &b);
	}
}

static void test_adds_z(add_func_t fn)
{
	struct timespec a, b, r;

	a.tv_sec = 0;
	for (a.tv_nsec = -NANOSECOND + NANOSECOND / step;
	     a.tv_nsec < NANOSECOND; a.tv_nsec += NANOSECOND / step)
	{
		b.tv_sec = -1;
		for (b.tv_nsec = 0; b.tv_nsec < NANOSECOND;
		     b.tv_nsec += NANOSECOND / step)
			(*fn)(&r, &a, &b);

		b.tv_sec = 0;
		for (b.tv_nsec = -NANOSECOND + NANOSECOND / step;
		     b.tv_nsec < NANOSECOND; b.tv_nsec += NANOSECOND / step)
			(*fn)(&r, &a, &b);

		b.tv_sec = 1;
		for (b.tv_nsec = 0; b.tv_nsec < NANOSECOND;
		     b.tv_nsec += NANOSECOND / step)
			(*fn)(&r, &a, &b);
	}
}

static void test_adds_1(const char *text, add_func_t fn)
{
	struct timespec start, delta;

	printf("%s", text);
	clock_gettime(clock_id, &start);
	test_adds_nz(-2, fn);
	test_adds_nz(-1, fn);
	test_adds_z(fn);
	test_adds_nz(1, fn);
	test_adds_nz(2, fn);
	clock_gettime(clock_id, &delta);
	HX_timespec_sub(&delta, &delta, &start);
	printf(HX_TIMESPEC_FMT "\n", HX_TIMESPEC_EXP(&delta));
}

static void test_adds(void)
{
	printf("# Test addition speed\n");
	test_adds_1("normal:  ", HX_timespec_add);
	test_adds_1("div64:   ", HX_timespec_add_DIVQ);
	printf("\n");
}

static void test_mul(void)
{
	struct timespec r, s;
	unsigned int i;
	double k;
	int j;

	printf("# Test multiplication behavior\n");
	for (i = 0; i < ARRAY_SIZE(pairs); ++i) {
		for (j = -3; j <= 3; ++j) {
			printf(HX_TIMESPEC_FMT " *N %d = ",
			       HX_TIMESPEC_EXP(&pairs[i]), j);
			HX_timespec_mul(&r, &pairs[i], j);
			printf(HX_TIMESPEC_FMT "\n", HX_TIMESPEC_EXP(&r));

			printf(HX_TIMESPEC_FMT " *S %d = ",
			       HX_TIMESPEC_EXP(&pairs[i]), j);
			HX_timespec_mul_SPL(&s, &pairs[i], j);
			printf(HX_TIMESPEC_FMT "\n", HX_TIMESPEC_EXP(&s));

			if (r.tv_sec != s.tv_sec || r.tv_nsec != s.tv_nsec)
				abort();
		}

		for (k = -3; k <= 3; k += 0.1) {
			printf(HX_TIMESPEC_FMT " *fN %f = ",
			       HX_TIMESPEC_EXP(&pairs[i]), k);
			HX_timespec_mulf(&r, &pairs[i], k);
			printf(HX_TIMESPEC_FMT "\n", HX_TIMESPEC_EXP(&r));

			printf(HX_TIMESPEC_FMT " *fS %f = ",
			       HX_TIMESPEC_EXP(&pairs[i]), k);
			HX_timespec_mulf_S(&s, &pairs[i], k);
			printf(HX_TIMESPEC_FMT "\n", HX_TIMESPEC_EXP(&s));
		}
	}

	printf("\n");
}

static void test_muls_1i(const char *text, mul_func_t fn)
{
	struct timespec r, s, start, delta;

	printf("%s", text);
	clock_gettime(clock_id, &start);
	for (time_t i = 0; i < step_mul; ++i) {
		r.tv_sec  = -i;
		r.tv_nsec = -i / 4;
		(*fn)(&s, &r, 7);
	}
	clock_gettime(clock_id, &delta);
	HX_timespec_sub(&delta, &delta, &start);
	printf(HX_TIMESPEC_FMT "\n", HX_TIMESPEC_EXP(&delta));
}

static void test_muls_1f(const char *text, mulf_func_t fn)
{
	struct timespec r, s, start, delta;

	printf("%s", text);
	clock_gettime(clock_id, &start);
	for (time_t i = 0; i < step_mul; ++i) {
		r.tv_sec  = -i;
		r.tv_nsec = -i / 4;
		(*fn)(&s, &r, 7);
	}
	clock_gettime(clock_id, &delta);
	HX_timespec_sub(&delta, &delta, &start);
	printf(HX_TIMESPEC_FMT "\n", HX_TIMESPEC_EXP(&delta));
}

static void test_muls(void)
{
	printf("# Test multiplication speed\n");
	test_muls_1i("normal: ", HX_timespec_mul);
	test_muls_1i("split:  ", HX_timespec_mul_SPL);

	test_muls_1f("float:  ", HX_timespec_mulf);
	test_muls_1f("flt-S:  ", HX_timespec_mulf_S);
	printf("\n");
}

int main(void)
{
	if (HX_init() <= 0)
		abort();

	test_basic();
	test_same();
	test_neg();
	test_add();
	test_mul();
	test_adds();
	test_muls();
	HX_exit();
	return EXIT_SUCCESS;
}
