// Float primitive operations
//
// These are registered in mypyc.primitives.float_ops.

#include <Python.h>
#include "CPy.h"


static double CPy_DomainError(void) {
    PyErr_SetString(PyExc_ValueError, "math domain error");
    return CPY_FLOAT_ERROR;
}

static double CPy_MathRangeError(void) {
    PyErr_SetString(PyExc_OverflowError, "math range error");
    return CPY_FLOAT_ERROR;
}

static double CPy_MathExpectedNonNegativeInputError(double x) {
    char *buf = PyOS_double_to_string(x, 'r', 0, Py_DTSF_ADD_DOT_0, NULL);
    if (buf) {
        PyErr_Format(PyExc_ValueError, "expected a nonnegative input, got %s", buf);
        PyMem_Free(buf);
    }
    return CPY_FLOAT_ERROR;
}

static double CPy_MathExpectedPositiveInputError(double x) {
    char *buf = PyOS_double_to_string(x, 'r', 0, Py_DTSF_ADD_DOT_0, NULL);
    if (buf) {
        PyErr_Format(PyExc_ValueError, "expected a positive input, got %s", buf);
        PyMem_Free(buf);
    }
    return CPY_FLOAT_ERROR;
}

static double CPy_MathExpectedFiniteInput(double x) {
    char *buf = PyOS_double_to_string(x, 'r', 0, Py_DTSF_ADD_DOT_0, NULL);
    if (buf) {
        PyErr_Format(PyExc_ValueError, "expected a finite input, got %s", buf);
        PyMem_Free(buf);
    }
    return CPY_FLOAT_ERROR;
}

double CPyFloat_FromTagged(CPyTagged x) {
    if (CPyTagged_CheckShort(x)) {
        return CPyTagged_ShortAsSsize_t(x);
    }
    double result = PyFloat_AsDouble(CPyTagged_LongAsObject(x));
    if (unlikely(result == -1.0) && PyErr_Occurred()) {
        return CPY_FLOAT_ERROR;
    }
    return result;
}

double CPyFloat_Sin(double x) {
    double v = sin(x);
    if (unlikely(isnan(v)) && !isnan(x)) {
#if CPY_3_14_FEATURES
        return CPy_MathExpectedFiniteInput(x);
#else
        return CPy_DomainError();
#endif
    }
    return v;
}

double CPyFloat_Cos(double x) {
    double v = cos(x);
    if (unlikely(isnan(v)) && !isnan(x)) {
#if CPY_3_14_FEATURES
        return CPy_MathExpectedFiniteInput(x);
#else
        return CPy_DomainError();
#endif
    }
    return v;
}

double CPyFloat_Tan(double x) {
    if (unlikely(isinf(x))) {
#if CPY_3_14_FEATURES
        return CPy_MathExpectedFiniteInput(x);
#else
        return CPy_DomainError();
#endif
    }
    return tan(x);
}

double CPyFloat_Sqrt(double x) {
    if (x < 0.0) {
#if CPY_3_14_FEATURES
        return CPy_MathExpectedNonNegativeInputError(x);
#else
        return CPy_DomainError();
#endif
    }
    return sqrt(x);
}

double CPyFloat_Exp(double x) {
    double v = exp(x);
    if (unlikely(v == INFINITY) && x != INFINITY) {
        return CPy_MathRangeError();
    }
    return v;
}

double CPyFloat_Log(double x) {
    if (x <= 0.0) {
#if CPY_3_14_FEATURES
        return CPy_MathExpectedPositiveInputError(x);
#else
        return CPy_DomainError();
#endif
    }
    return log(x);
}

CPyTagged CPyFloat_Floor(double x) {
    double v = floor(x);
    return CPyTagged_FromFloat(v);
}

CPyTagged CPyFloat_Ceil(double x) {
    double v = ceil(x);
    return CPyTagged_FromFloat(v);
}

bool CPyFloat_IsInf(double x) {
    return isinf(x) != 0;
}

bool CPyFloat_IsNaN(double x) {
    return isnan(x) != 0;
}

// From CPython 3.10.0, Objects/floatobject.c
static void
_float_div_mod(double vx, double wx, double *floordiv, double *mod)
{
    double div;
    *mod = fmod(vx, wx);
    /* fmod is typically exact, so vx-mod is *mathematically* an
       exact multiple of wx.  But this is fp arithmetic, and fp
       vx - mod is an approximation; the result is that div may
       not be an exact integral value after the division, although
       it will always be very close to one.
    */
    div = (vx - *mod) / wx;
    if (*mod) {
        /* ensure the remainder has the same sign as the denominator */
        if ((wx < 0) != (*mod < 0)) {
            *mod += wx;
            div -= 1.0;
        }
    }
    else {
        /* the remainder is zero, and in the presence of signed zeroes
           fmod returns different results across platforms; ensure
           it has the same sign as the denominator. */
        *mod = copysign(0.0, wx);
    }
    /* snap quotient to nearest integral value */
    if (div) {
        *floordiv = floor(div);
        if (div - *floordiv > 0.5) {
            *floordiv += 1.0;
        }
    }
    else {
        /* div is zero - get the same sign as the true quotient */
        *floordiv = copysign(0.0, vx / wx); /* zero w/ sign of vx/wx */
    }
}

double CPyFloat_FloorDivide(double x, double y) {
    double mod, floordiv;
    if (y == 0) {
        PyErr_SetString(PyExc_ZeroDivisionError, "float floor division by zero");
        return CPY_FLOAT_ERROR;
    }
    _float_div_mod(x, y, &floordiv, &mod);
    return floordiv;
}

// Adapted from CPython 3.10.7
double CPyFloat_Pow(double x, double y) {
    if (!isfinite(x) || !isfinite(y)) {
        if (isnan(x))
            return y == 0.0 ? 1.0 : x; /* NaN**0 = 1 */
        else if (isnan(y))
            return x == 1.0 ? 1.0 : y; /* 1**NaN = 1 */
        else if (isinf(x)) {
            int odd_y = isfinite(y) && fmod(fabs(y), 2.0) == 1.0;
            if (y > 0.0)
                return odd_y ? x : fabs(x);
            else if (y == 0.0)
                return 1.0;
            else /* y < 0. */
                return odd_y ? copysign(0.0, x) : 0.0;
        }
        else if (isinf(y)) {
            if (fabs(x) == 1.0)
                return 1.0;
            else if (y > 0.0 && fabs(x) > 1.0)
                return y;
            else if (y < 0.0 && fabs(x) < 1.0) {
                #if PY_VERSION_HEX < 0x030B0000
                if (x == 0.0) { /* 0**-inf: divide-by-zero */
                    return CPy_DomainError();
                }
                #endif
                return -y; /* result is +inf */
            } else
                return 0.0;
        }
    }
    double r = pow(x, y);
    if (!isfinite(r)) {
        if (isnan(r)) {
            return CPy_DomainError();
        }
        /*
           an infinite result here arises either from:
           (A) (+/-0.)**negative (-> divide-by-zero)
           (B) overflow of x**y with x and y finite
        */
        else if (isinf(r)) {
            if (x == 0.0)
                return CPy_DomainError();
            else
                return CPy_MathRangeError();
        }
    }
    return r;
}
