/*
 * 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.
 */

/**
 * # Mathematical Functions
 *
 * The `math` module bundles various mathematical and trigonometrical functions.
 *
 * Functions can be individually imported and directly accessed using the
 * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#named_import named import}
 * syntax:
 *
 *   ```
 *   import { pow, rand } from 'math';
 *
 *   let x = pow(2, 5);
 *   let y = rand();
 *   ```
 *
 * Alternatively, the module namespace can be imported
 * using a wildcard import statement:
 *
 *   ```
 *   import * as math from 'math';
 *
 *   let x = math.pow(2, 5);
 *   let y = math.rand();
 *   ```
 *
 * Additionally, the math module namespace may also be imported by invoking the
 * `ucode` interpreter with the `-lmath` switch.
 *
 * @module math
 */

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

#include "ucode/module.h"

static bool srand_called = false;

/**
 * Returns the absolute value of the given numeric value.
 *
 * @function module:math#abs
 *
 * @param {*} number
 * The number to return the absolute value for.
 *
 * @returns {number}
 * Returns the absolute value or `NaN` if the given argument could
 * not be converted to a number.
 */
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;
}

/**
 * Calculates the principal value of the arc tangent of `y`/`x`,
 * using the signs of the two arguments to determine the quadrant
 * of the result.
 *
 * On success, this function returns the principal value of the arc
 * tangent of `y`/`x` in radians; the return value is in the range [-pi, pi].
 *
 *  - If `y` is +0 (-0) and `x` is less than 0, +pi (-pi) is returned.
 *  - If `y` is +0 (-0) and `x` is greater than 0, +0 (-0) is returned.
 *  - If `y` is less than 0 and `x` is +0 or -0, -pi/2 is returned.
 *  - If `y` is greater than 0 and `x` is +0 or -0, pi/2 is returned.
 *  - If either `x` or `y` is NaN, a NaN is returned.
 *  - If `y` is +0 (-0) and `x` is -0, +pi (-pi) is returned.
 *  - If `y` is +0 (-0) and `x` is +0, +0 (-0) is returned.
 *  - If `y` is a finite value greater (less) than 0, and `x` is negative
 *    infinity, +pi (-pi) is returned.
 *  - If `y` is a finite value greater (less) than 0, and `x` is positive
 *    infinity, +0 (-0) is returned.
 *  - If `y` is positive infinity (negative infinity), and `x` is finite,
 *    pi/2 (-pi/2) is returned.
 *  - If `y` is positive infinity (negative infinity) and `x` is negative
 *    infinity, +3*pi/4 (-3*pi/4) is returned.
 *  - If `y` is positive infinity (negative infinity) and `x` is positive
 *    infinity, +pi/4 (-pi/4) is returned.
 *
 * When either `x` or `y` can't be converted to a numeric value, `NaN` is
 * returned.
 *
 * @function module:math#atan2
 *
 * @param {*} y
 * The `y` value.
 *
 * @param {*} x
 * The `x` value.
 *
 * @returns {number}
 */
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));
}

/**
 * Calculates the cosine of `x`, where `x` is given in radians.
 *
 * Returns the resulting consine value.
 *
 * Returns `NaN` if the `x` value can't be converted to a number.
 *
 * @function module:math#cos
 *
 * @param {number} x
 * Radians value to calculate cosine for.
 *
 * @returns {number}
 */
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));
}

/**
 * Calculates the value of `e` (the base of natural logarithms)
 * raised to the power of `x`.
 *
 * On success, returns the exponential value of `x`.
 *
 *  - If `x` is positive infinity, positive infinity is returned.
 *  - If `x` is negative infinity, `+0` is returned.
 *  - If the result underflows, a range error occurs, and zero is returned.
 *  - If the result overflows, a range error occurs, and `Infinity` is returned.
 *
 * Returns `NaN` if the `x` value can't be converted to a number.
 *
 * @function module:math#exp
 *
 * @param {number} x
 * Power to raise `e` to.
 *
 * @returns {number}
 */
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));
}

/**
 * Calculates the natural logarithm of `x`.
 *
 * On success, returns the natural logarithm of `x`.
 *
 *  - If `x` is `1`, the result is `+0`.
 *  - If `x` is positive nfinity, positive infinity is returned.
 *  - If `x` is zero, then a pole error occurs, and the function
 *    returns negative infinity.
 *  - If `x` is negative (including negative infinity), then a domain
 *    error occurs, and `NaN` is returned.
 *
 * Returns `NaN` if the `x` value can't be converted to a number.
 *
 * @function module:math#log
 *
 * @param {number} x
 * Value to calulate natural logarithm of.
 *
 * @returns {number}
 */
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));
}

/**
 * Calculates the sine of `x`, where `x` is given in radians.
 *
 * Returns the resulting sine value.
 *
 *  - When `x` is positive or negative infinity, a domain error occurs
 *    and `NaN` is returned.
 *
 * Returns `NaN` if the `x` value can't be converted to a number.
 *
 * @function module:math#sin
 *
 * @param {number} x
 * Radians value to calculate sine for.
 *
 * @returns {number}
 */
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));
}

/**
 * Calculates the nonnegative square root of `x`.
 *
 * Returns the resulting square root value.
 *
 *  - If `x` is `+0` (`-0`) then `+0` (`-0`) is returned.
 *  - If `x` is positive infinity, positive infinity is returned.
 *  - If `x` is less than `-0`, a domain error occurs, and `NaN` is returned.
 *
 * Returns `NaN` if the `x` value can't be converted to a number.
 *
 * @function module:math#sqrt
 *
 * @param {number} x
 * Value to calculate square root for.
 *
 * @returns {number}
 */
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));
}

/**
 * Calculates the value of `x` raised to the power of `y`.
 *
 * On success, returns the value of `x` raised to the power of `y`.
 *
 *  - If the result overflows, a range error occurs, and the function
 *    returns `Infinity`.
 *  - If result underflows, and is not representable, a range error
 *    occurs, and `0.0` with the appropriate sign is returned.
 *  - If `x` is `+0` or `-0`, and `y` is an odd integer less than `0`,
 *    a pole error occurs `Infinity` is returned, with the same sign
 *    as `x`.
 *  - If `x` is `+0` or `-0`, and `y` is less than `0` and not an odd
 *    integer, a pole error occurs and `Infinity` is returned.
 *  - If `x` is `+0` (`-0`), and `y` is an odd integer greater than `0`,
 *    the result is `+0` (`-0`).
 *  - If `x` is `0`, and `y` greater than `0` and not an odd integer,
 *    the result is `+0`.
 *  - If `x` is `-1`, and `y` is positive infinity or negative infinity,
 *    the result is `1.0`.
 *  - If `x` is `+1`, the result is `1.0` (even if `y` is `NaN`).
 *  - If `y` is `0`, the result is `1.0` (even if `x` is `NaN`).
 *  - If `x` is a finite value less than `0`, and `y` is a finite
 *    noninteger, a domain error occurs, and `NaN` is returned.
 *  - If the absolute value of `x` is less than `1`, and `y` is negative
 *    infinity, the result is positive infinity.
 *  - If the absolute value of `x` is greater than `1`, and `y` is
 *    negative infinity, the result is `+0`.
 *  - If the absolute value of `x` is less than `1`, and `y` is positive
 *    infinity, the result is `+0`.
 *  - If the absolute value of `x` is greater than `1`, and `y` is positive
 *    infinity, the result is positive infinity.
 *  - If `x` is negative infinity, and `y` is an odd integer less than `0`,
 *    the result is `-0`.
 *  - If `x` is negative infinity, and `y` less than `0` and not an odd
 *    integer, the result is `+0`.
 *  - If `x` is negative infinity, and `y` is an odd integer greater than
 *    `0`, the result is negative infinity.
 *  - If `x` is negative infinity, and `y` greater than `0` and not an odd
 *    integer, the result is positive infinity.
 *  - If `x` is positive infinity, and `y` less than `0`, the result is `+0`.
 *  - If `x` is positive infinity, and `y` greater than `0`, the result is
 *    positive infinity.
 *
 * Returns `NaN` if either the `x` or `y` value can't be converted to a number.
 *
 * @function module:math#pow
 *
 * @param {number} x
 * The base value.
 *
 * @param {number} y
 * The power value.
 *
 * @returns {number}
 */
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));
}

/**
 * Produces a pseudo-random positive integer.
 *
 * Returns the calculated pseuo-random value. The value is within the range
 * `0` to `RAND_MAX` inclusive where `RAND_MAX` is a platform specific value
 * guaranteed to be at least `32767`.
 *
 * The {@link module:math~srand `srand()`} function sets its argument as the
 * seed for a new sequence of pseudo-random integers to be returned by `rand()`. These sequences are
 * repeatable by calling {@link module:math~srand `srand()`} with the same
 * seed value.
 *
 * If no seed value is explicitly set by calling
 * {@link module:math~srand `srand()`} prior to the first call to `rand()`,
 * the math module will automatically seed the PRNG once, using the current
 * time of day in milliseconds as seed value.
 *
 * @function module:math#rand
 *
 * @returns {number}
 */
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());
}

/**
 * Seeds the pseudo-random number generator.
 *
 * This functions seeds the PRNG with the given value and thus affects the
 * pseudo-random integer sequence produced by subsequent calls to
 * {@link module:math~rand `rand()`}.
 *
 * Setting the same seed value will result in the same pseudo-random numbers
 * produced by {@link module:math~rand `rand()`}.
 *
 * @function module:math#srand
 *
 * @param {number} seed
 * The seed value.
 */
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;
}

/**
 * Tests whether `x` is a `NaN` double.
 *
 * This functions checks whether the given argument is of type `double` with
 * a `NaN` (not a number) value.
 *
 * Returns `true` if the value is `NaN`, otherwise false.
 *
 * Note that a value can also be checked for `NaN` with the expression
 * `x !== x` which only evaluates to `true` if `x` is `NaN`.
 *
 * @function module:math#isnan
 *
 * @param {number} x
 * The value to test.
 *
 * @returns {boolean}
 */
static uc_value_t *
uc_isnan(uc_vm_t *vm, size_t nargs)
{
	uc_value_t *v = uc_fn_arg(0);

	return ucv_boolean_new(ucv_type(v) == UC_DOUBLE && isnan(ucv_double_get(v)));
}

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 },
	{ "isnan",	uc_isnan },
};

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