#include <algorithm>
#include <fstream>
#include <iostream>
#include <math.h>
#include <random>
#include <cstdio>
#include <ctime>
#include <vector>

struct ufunc {
    std::string name;
    double (*f32func)(double);
    long double (*f64func)(long double);
    float f32ulp;
    float f64ulp;
};

template <typename T>
T
RandomFloat(T a, T b)
{
    T random = ((T)rand()) / (T)RAND_MAX;
    T diff = b - a;
    T r = random * diff;
    return a + r;
}

template <typename T>
void
append_random_array(std::vector<T> &arr, T min, T max, size_t N)
{
    for (size_t ii = 0; ii < N; ++ii)
        arr.emplace_back(RandomFloat<T>(min, max));
}

template <typename T1, typename T2>
std::vector<T1>
computeTrueVal(const std::vector<T1> &in, T2 (*mathfunc)(T2))
{
    std::vector<T1> out;
    for (T1 elem : in) {
        T2 elem_d = (T2)elem;
        T1 out_elem = (T1)mathfunc(elem_d);
        out.emplace_back(out_elem);
    }
    return out;
}

/*
 * FP range:
 * [-inf, -maxflt, -1., -minflt, -minden, 0., minden, minflt, 1., maxflt, inf]
 */

#define MINDEN std::numeric_limits<T>::denorm_min()
#define MINFLT std::numeric_limits<T>::min()
#define MAXFLT std::numeric_limits<T>::max()
#define INF std::numeric_limits<T>::infinity()
#define qNAN std::numeric_limits<T>::quiet_NaN()
#define sNAN std::numeric_limits<T>::signaling_NaN()

template <typename T>
std::vector<T>
generate_input_vector(std::string func)
{
    std::vector<T> input = {MINDEN,  -MINDEN, MINFLT, -MINFLT, MAXFLT,
                            -MAXFLT, INF,     -INF,   qNAN,    sNAN,
                            -1.0,    1.0,     0.0,    -0.0};

    // [-1.0, 1.0]
    if ((func == "arcsin") || (func == "arccos") || (func == "arctanh")) {
        append_random_array<T>(input, -1.0, 1.0, 700);
    }
    // (0.0, INF]
    else if ((func == "log2") || (func == "log10")) {
        append_random_array<T>(input, 0.0, 1.0, 200);
        append_random_array<T>(input, MINDEN, MINFLT, 200);
        append_random_array<T>(input, MINFLT, 1.0, 200);
        append_random_array<T>(input, 1.0, MAXFLT, 200);
    }
    // (-1.0, INF]
    else if (func == "log1p") {
        append_random_array<T>(input, -1.0, 1.0, 200);
        append_random_array<T>(input, -MINFLT, -MINDEN, 100);
        append_random_array<T>(input, -1.0, -MINFLT, 100);
        append_random_array<T>(input, MINDEN, MINFLT, 100);
        append_random_array<T>(input, MINFLT, 1.0, 100);
        append_random_array<T>(input, 1.0, MAXFLT, 100);
    }
    // [1.0, INF]
    else if (func == "arccosh") {
        append_random_array<T>(input, 1.0, 2.0, 400);
        append_random_array<T>(input, 2.0, MAXFLT, 300);
    }
    // [-INF, INF]
    else {
        append_random_array<T>(input, -1.0, 1.0, 100);
        append_random_array<T>(input, MINDEN, MINFLT, 100);
        append_random_array<T>(input, -MINFLT, -MINDEN, 100);
        append_random_array<T>(input, MINFLT, 1.0, 100);
        append_random_array<T>(input, -1.0, -MINFLT, 100);
        append_random_array<T>(input, 1.0, MAXFLT, 100);
        append_random_array<T>(input, -MAXFLT, -100.0, 100);
    }

    std::random_shuffle(input.begin(), input.end());
    return input;
}

int
main()
{
    srand(42);
    std::vector<struct ufunc> umathfunc = {
            {"sin", sin, sin, 1.49, 1.00},
            {"cos", cos, cos, 1.49, 1.00},
            {"tan", tan, tan, 3.91, 1.00},
            {"arcsin", asin, asin, 3.12, 1.00},
            {"arccos", acos, acos, 2.1, 1.00},
            {"arctan", atan, atan, 2.3, 1.00},
            {"sinh", sinh, sinh, 1.55, 1.00},
            {"cosh", cosh, cosh, 2.48, 1.00},
            {"tanh", tanh, tanh, 1.38, 2.00},
            {"arcsinh", asinh, asinh, 1.01, 1.00},
            {"arccosh", acosh, acosh, 1.16, 1.00},
            {"arctanh", atanh, atanh, 1.45, 1.00},
            {"cbrt", cbrt, cbrt, 1.94, 2.00},
            //{"exp",exp,exp,3.76,1.00},
            {"exp2", exp2, exp2, 1.01, 1.00},
            {"expm1", expm1, expm1, 2.62, 1.00},
            //{"log",log,log,1.84,1.00},
            {"log10", log10, log10, 3.5, 1.00},
            {"log1p", log1p, log1p, 1.96, 1.0},
            {"log2", log2, log2, 2.12, 1.00},
    };

    for (int ii = 0; ii < umathfunc.size(); ++ii) {
        // ignore sin/cos
        if ((umathfunc[ii].name != "sin") && (umathfunc[ii].name != "cos")) {
            std::string fileName =
                    "umath-validation-set-" + umathfunc[ii].name + ".csv";
            std::ofstream txtOut;
            txtOut.open(fileName, std::ofstream::trunc);
            txtOut << "dtype,input,output,ulperrortol" << std::endl;

            // Single Precision
            auto f32in = generate_input_vector<float>(umathfunc[ii].name);
            auto f32out = computeTrueVal<float, double>(f32in,
                                                        umathfunc[ii].f32func);
            for (int jj = 0; jj < f32in.size(); ++jj) {
                txtOut << "np.float32" << std::hex << ",0x"
                       << *reinterpret_cast<uint32_t *>(&f32in[jj]) << ",0x"
                       << *reinterpret_cast<uint32_t *>(&f32out[jj]) << ","
                       << ceil(umathfunc[ii].f32ulp) << std::endl;
            }

            // Double Precision
            auto f64in = generate_input_vector<double>(umathfunc[ii].name);
            auto f64out = computeTrueVal<double, long double>(
                    f64in, umathfunc[ii].f64func);
            for (int jj = 0; jj < f64in.size(); ++jj) {
                txtOut << "np.float64" << std::hex << ",0x"
                       << *reinterpret_cast<uint64_t *>(&f64in[jj]) << ",0x"
                       << *reinterpret_cast<uint64_t *>(&f64out[jj]) << ","
                       << ceil(umathfunc[ii].f64ulp) << std::endl;
            }
            txtOut.close();
        }
    }
    return 0;
}
