/*
 * Copyright (C) 2020-2021 Jo-Philipp Wich <jo@mein.io>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <math.h>
#include <errno.h>
#include <sys/time.h>

#include "ucode/module.h"

static bool srand_called = false;

static uc_value_t *
uc_abs(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *v = uc_fn_arg(0), *nv, *res;
	int64_t n;
	double d;

	nv = v ? ucv_to_number(v) : NULL;

	switch (ucv_type(nv)) {
	case UC_INTEGER:
		n = ucv_int64_get(nv);

		if (n >= 0 || errno == ERANGE)
			res = ucv_get(nv);
		else if (n == INT64_MIN)
			res = ucv_uint64_new((uint64_t)INT64_MAX + 1);
		else
			res = ucv_uint64_new(-n);

		break;

	case UC_DOUBLE:
		d = ucv_double_get(nv);

		if (isnan(d) || d >= 0)
			res = ucv_get(nv);
		else
			res = ucv_double_new(-d);

		break;

	default:
		res = ucv_double_new(NAN);
		break;
	}

	ucv_put(nv);

	return res;
}

static uc_value_t *
uc_atan2(uc_vm_t *vm, size_t nargs)
{
	double d1 = ucv_to_double(uc_fn_arg(0));
	double d2 = ucv_to_double(uc_fn_arg(1));

	if (isnan(d1) || isnan(d2))
		return ucv_double_new(NAN);

	return ucv_double_new(atan2(d1, d2));
}

static uc_value_t *
uc_cos(uc_vm_t *vm, size_t nargs)
{
	double d = ucv_to_double(uc_fn_arg(0));

	if (isnan(d))
		return ucv_double_new(NAN);

	return ucv_double_new(cos(d));
}

static uc_value_t *
uc_exp(uc_vm_t *vm, size_t nargs)
{
	double d = ucv_to_double(uc_fn_arg(0));

	if (isnan(d))
		return ucv_double_new(NAN);

	return ucv_double_new(exp(d));
}

static uc_value_t *
uc_log(uc_vm_t *vm, size_t nargs)
{
	double d = ucv_to_double(uc_fn_arg(0));

	if (isnan(d))
		return ucv_double_new(NAN);

	return ucv_double_new(log(d));
}

static uc_value_t *
uc_sin(uc_vm_t *vm, size_t nargs)
{
	double d = ucv_to_double(uc_fn_arg(0));

	if (isnan(d))
		return ucv_double_new(NAN);

	return ucv_double_new(sin(d));
}

static uc_value_t *
uc_sqrt(uc_vm_t *vm, size_t nargs)
{
	double d = ucv_to_double(uc_fn_arg(0));

	if (isnan(d))
		return ucv_double_new(NAN);

	return ucv_double_new(sqrt(d));
}

static uc_value_t *
uc_pow(uc_vm_t *vm, size_t nargs)
{
	double x = ucv_to_double(uc_fn_arg(0));
	double y = ucv_to_double(uc_fn_arg(1));

	if (isnan(x) || isnan(y))
		return ucv_double_new(NAN);

	return ucv_double_new(pow(x, y));
}

static uc_value_t *
uc_rand(uc_vm_t *vm, size_t nargs)
{
	struct timeval tv;

	if (!srand_called) {
		gettimeofday(&tv, NULL);
		srand((tv.tv_sec * 1000) + (tv.tv_usec / 1000));

		srand_called = true;
	}

	return ucv_int64_new(rand());
}

static uc_value_t *
uc_srand(uc_vm_t *vm, size_t nargs)
{
	int64_t n = ucv_to_integer(uc_fn_arg(0));

	srand((unsigned int)n);
	srand_called = true;

	return NULL;
}

static const uc_function_list_t math_fns[] = {
	{ "abs",	uc_abs },
	{ "atan2",	uc_atan2 },
	{ "cos",	uc_cos },
	{ "exp",	uc_exp },
	{ "log",	uc_log },
	{ "sin",	uc_sin },
	{ "sqrt",	uc_sqrt },
	{ "pow",	uc_pow },
	{ "rand",	uc_rand },
	{ "srand",	uc_srand },
};

void uc_module_init(uc_vm_t *vm, uc_value_t *scope)
{
	uc_function_list_register(scope, math_fns);
}