/* -*- c -*- */

/*
 *****************************************************************************
 **                            INCLUDES                                     **
 *****************************************************************************
 */
#define NPY_NO_DEPRECATED_API NPY_API_VERSION

#include "Python.h"
#include "numpy/arrayobject.h"
#include "numpy/ufuncobject.h"
#include "numpy/npy_math.h"

#include "npy_pycompat.h"

#include "npy_config.h"

/*
 *****************************************************************************
 **                            BASICS                                       **
 *****************************************************************************
 */

#define INIT_OUTER_LOOP_1       \
    npy_intp dN = *dimensions++;\
    npy_intp N_;                \
    npy_intp s0 = *steps++;

#define INIT_OUTER_LOOP_2       \
    INIT_OUTER_LOOP_1           \
    npy_intp s1 = *steps++;

#define INIT_OUTER_LOOP_3       \
    INIT_OUTER_LOOP_2           \
    npy_intp s2 = *steps++;

#define INIT_OUTER_LOOP_4       \
    INIT_OUTER_LOOP_3           \
    npy_intp s3 = *steps++;

#define BEGIN_OUTER_LOOP_2      \
    for (N_ = 0; N_ < dN; N_++, args[0] += s0, args[1] += s1) {

#define BEGIN_OUTER_LOOP_3      \
    for (N_ = 0; N_ < dN; N_++, args[0] += s0, args[1] += s1, args[2] += s2) {

#define BEGIN_OUTER_LOOP_4      \
    for (N_ = 0; N_ < dN; N_++, args[0] += s0, args[1] += s1, args[2] += s2, args[3] += s3) {

#define END_OUTER_LOOP  }


/*
 *****************************************************************************
 **                             UFUNC LOOPS                                 **
 *****************************************************************************
 */

char *inner1d_signature = "(i),(i)->()";

/**begin repeat

   #TYPE=LONG,DOUBLE#
   #typ=npy_long,npy_double#
*/

/*
 *  This implements the function
 *        out[n] = sum_i { in1[n, i] * in2[n, i] }.
 */

static void
@TYPE@_inner1d(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func))
{
    INIT_OUTER_LOOP_3
    npy_intp di = dimensions[0];
    npy_intp i;
    npy_intp is1=steps[0], is2=steps[1];
    BEGIN_OUTER_LOOP_3
        char *ip1=args[0], *ip2=args[1], *op=args[2];
        @typ@ sum = 0;
        for (i = 0; i < di; i++) {
            sum += (*(@typ@ *)ip1) * (*(@typ@ *)ip2);
            ip1 += is1;
            ip2 += is2;
        }
        *(@typ@ *)op = sum;
    END_OUTER_LOOP
}

/**end repeat**/

char *innerwt_signature = "(i),(i),(i)->()";

/**begin repeat

   #TYPE=LONG,DOUBLE#
   #typ=npy_long,npy_double#
*/


/*
 *  This implements the function
 *        out[n] = sum_i { in1[n, i] * in2[n, i] * in3[n, i] }.
 */

static void
@TYPE@_innerwt(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func))
{
    INIT_OUTER_LOOP_4
    npy_intp di = dimensions[0];
    npy_intp i;
    npy_intp is1=steps[0], is2=steps[1], is3=steps[2];
    BEGIN_OUTER_LOOP_4
        char *ip1=args[0], *ip2=args[1], *ip3=args[2], *op=args[3];
        @typ@ sum = 0;
        for (i = 0; i < di; i++) {
            sum += (*(@typ@ *)ip1) * (*(@typ@ *)ip2) * (*(@typ@ *)ip3);
            ip1 += is1;
            ip2 += is2;
            ip3 += is3;
        }
        *(@typ@ *)op = sum;
    END_OUTER_LOOP
}

/**end repeat**/

char *matrix_multiply_signature = "(m,n),(n,p)->(m,p)";

/**begin repeat

   #TYPE=FLOAT,DOUBLE,LONG#
   #typ=npy_float,npy_double,npy_long#
*/

/*
 *  This implements the function
 *        out[k, m, p] = sum_n { in1[k, m, n] * in2[k, n, p] }.
 */

static void
@TYPE@_matrix_multiply(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func))
{
    /* no BLAS is available */
    INIT_OUTER_LOOP_3
    npy_intp dm = dimensions[0];
    npy_intp dn = dimensions[1];
    npy_intp dp = dimensions[2];
    npy_intp m,n,p;
    npy_intp is1_m=steps[0], is1_n=steps[1], is2_n=steps[2], is2_p=steps[3],
         os_m=steps[4], os_p=steps[5];
    npy_intp ib1_n = is1_n*dn;
    npy_intp ib2_n = is2_n*dn;
    npy_intp ib2_p = is2_p*dp;
    npy_intp ob_p  = os_p *dp;
    if (dn == 0) {
        /* No operand, need to zero the output */
        BEGIN_OUTER_LOOP_3
            char *op=args[2];
            for (m = 0; m < dm; m++) {
                for (p = 0; p < dp; p++) {
                    *(@typ@ *)op = 0;
                    op  +=  os_p;
                }
                op  +=  os_m - ob_p;
            }
        END_OUTER_LOOP
        return;
    }
    BEGIN_OUTER_LOOP_3
        char *ip1=args[0], *ip2=args[1], *op=args[2];
        for (m = 0; m < dm; m++) {
            for (n = 0; n < dn; n++) {
                @typ@ val1 = (*(@typ@ *)ip1);
                for (p = 0; p < dp; p++) {
                    if (n == 0) *(@typ@ *)op = 0;
                    *(@typ@ *)op += val1 * (*(@typ@ *)ip2);
                    ip2 += is2_p;
                    op  +=  os_p;
                }
                ip2 -= ib2_p;
                op  -=  ob_p;
                ip1 += is1_n;
                ip2 += is2_n;
            }
            ip1 -= ib1_n;
            ip2 -= ib2_n;
            ip1 += is1_m;
            op  +=  os_m;
        }
    END_OUTER_LOOP
}

/**end repeat**/

char *euclidean_pdist_signature = "(n,d)->(p)";

/**begin repeat

   #TYPE=FLOAT,DOUBLE#
   #typ=npy_float,npy_double#
   #sqrt_func=sqrtf,sqrt#
*/

/*
 *  This implements the function
 *        out[j*(2*n-3-j)+k-1] = sum_d { (in1[j, d] - in1[k, d])^2 }
 *  with 0 < k < j < n, i.e. computes all unique pairwise euclidean distances.
 */

static void
@TYPE@_euclidean_pdist(char **args, npy_intp *dimensions, npy_intp *steps,
                       void *NPY_UNUSED(func))
{
    INIT_OUTER_LOOP_2
    npy_intp len_n = *dimensions++;
    npy_intp len_d = *dimensions++;
    npy_intp stride_n = *steps++;
    npy_intp stride_d = *steps++;
    npy_intp stride_p = *steps;

    assert(len_n * (len_n - 1) / 2 == *dimensions);

    BEGIN_OUTER_LOOP_2
        const char *data_this = (const char *)args[0];
        char *data_out = args[1];
        npy_intp n;
        for (n = 0; n < len_n; ++n) {
            const char *data_that = data_this + stride_n;
            npy_intp nn;
            for (nn = n + 1; nn < len_n; ++nn) {
                const char *ptr_this = data_this;
                const char *ptr_that = data_that;
                @typ@ out = 0;
                npy_intp d;
                for (d = 0; d < len_d; ++d) {
                    const @typ@ delta = *(const @typ@ *)ptr_this -
                                        *(const @typ@ *)ptr_that;
                    out += delta * delta;
                    ptr_this += stride_d;
                    ptr_that += stride_d;
                }
                *(@typ@ *)data_out = npy_@sqrt_func@(out);
                data_that += stride_n;
                data_out += stride_p;
            }
            data_this += stride_n;
        }
    END_OUTER_LOOP
}

/**end repeat**/


static PyUFuncGenericFunction inner1d_functions[] = { LONG_inner1d, DOUBLE_inner1d };
static void * inner1d_data[] = { (void *)NULL, (void *)NULL };
static char inner1d_signatures[] = { NPY_LONG, NPY_LONG, NPY_LONG, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE };
static PyUFuncGenericFunction innerwt_functions[] = { LONG_innerwt, DOUBLE_innerwt };
static void * innerwt_data[] = { (void *)NULL, (void *)NULL };
static char innerwt_signatures[] = { NPY_LONG, NPY_LONG, NPY_LONG, NPY_LONG, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE };
static PyUFuncGenericFunction matrix_multiply_functions[] = { LONG_matrix_multiply, FLOAT_matrix_multiply, DOUBLE_matrix_multiply };
static void *matrix_multiply_data[] = { (void *)NULL, (void *)NULL, (void *)NULL };
static char matrix_multiply_signatures[] = { NPY_LONG, NPY_LONG, NPY_LONG,  NPY_FLOAT, NPY_FLOAT, NPY_FLOAT,  NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE };

static PyUFuncGenericFunction euclidean_pdist_functions[] =
                            { FLOAT_euclidean_pdist, DOUBLE_euclidean_pdist };
static void *eucldiean_pdist_data[] = { (void *)NULL, (void *)NULL };
static char euclidean_pdist_signatures[] = { NPY_FLOAT, NPY_FLOAT,
                                             NPY_DOUBLE, NPY_DOUBLE };


static void
addUfuncs(PyObject *dictionary) {
    PyObject *f;

    f = PyUFunc_FromFuncAndDataAndSignature(inner1d_functions, inner1d_data,
                    inner1d_signatures, 2, 2, 1, PyUFunc_None, "inner1d",
                    "inner on the last dimension and broadcast on the rest \n"
                    "     \"(i),(i)->()\" \n",
                    0, inner1d_signature);
    PyDict_SetItemString(dictionary, "inner1d", f);
    Py_DECREF(f);
    f = PyUFunc_FromFuncAndDataAndSignature(innerwt_functions, innerwt_data,
                    innerwt_signatures, 2, 3, 1, PyUFunc_None, "innerwt",
                    "inner1d with a weight argument \n"
                    "     \"(i),(i),(i)->()\" \n",
                    0, innerwt_signature);
    PyDict_SetItemString(dictionary, "innerwt", f);
    Py_DECREF(f);
    f = PyUFunc_FromFuncAndDataAndSignature(matrix_multiply_functions,
                    matrix_multiply_data, matrix_multiply_signatures,
                    3, 2, 1, PyUFunc_None, "matrix_multiply",
                    "matrix multiplication on last two dimensions \n"
                    "     \"(m,n),(n,p)->(m,p)\" \n",
                    0, matrix_multiply_signature);
    PyDict_SetItemString(dictionary, "matrix_multiply", f);
    Py_DECREF(f);
    f = PyUFunc_FromFuncAndDataAndSignature(euclidean_pdist_functions,
                    eucldiean_pdist_data, euclidean_pdist_signatures,
                    2, 1, 1, PyUFunc_None, "euclidean_pdist",
                    "pairwise euclidean distance on last two dimensions \n"
                    "     \"(n,d)->(p)\" \n",
                    0, euclidean_pdist_signature);
    PyDict_SetItemString(dictionary, "euclidean_pdist", f);
    Py_DECREF(f);
    f = PyUFunc_FromFuncAndDataAndSignature(inner1d_functions, inner1d_data,
                    inner1d_signatures, 2, 2, 1, PyUFunc_None, "inner1d_no_doc",
                    NULL,
                    0, inner1d_signature);
    PyDict_SetItemString(dictionary, "inner1d_no_doc", f);
    Py_DECREF(f);
}


static PyObject *
UMath_Tests_test_signature(PyObject *NPY_UNUSED(dummy), PyObject *args)
{
    int nin, nout;
    PyObject *signature, *sig_str;
    PyObject *f;
    int core_enabled;

    if (!PyArg_ParseTuple(args, "iiO", &nin, &nout, &signature)) return NULL;


    if (PyString_Check(signature)) {
        sig_str = signature;
    } else if (PyUnicode_Check(signature)) {
        sig_str = PyUnicode_AsUTF8String(signature);
    } else {
        PyErr_SetString(PyExc_ValueError, "signature should be a string");
        return NULL;
    }

    f = PyUFunc_FromFuncAndDataAndSignature(NULL, NULL, NULL,
        0, nin, nout, PyUFunc_None, "no name",
        "doc:none",
        1, PyString_AS_STRING(sig_str));
    if (sig_str != signature) {
        Py_DECREF(sig_str);
    }
    if (f == NULL) return NULL;
    core_enabled = ((PyUFuncObject*)f)->core_enabled;
    Py_DECREF(f);
    return Py_BuildValue("i", core_enabled);
}

static PyMethodDef UMath_TestsMethods[] = {
    {"test_signature",  UMath_Tests_test_signature, METH_VARARGS,
     "Test signature parsing of ufunc. \n"
     "Arguments: nin nout signature \n"
     "If fails, it returns NULL. Otherwise it will returns 0 for scalar ufunc "
     "and 1 for generalized ufunc. \n",
     },
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

#if defined(NPY_PY3K)
static struct PyModuleDef moduledef = {
        PyModuleDef_HEAD_INIT,
        "umath_tests",
        NULL,
        -1,
        UMath_TestsMethods,
        NULL,
        NULL,
        NULL,
        NULL
};
#endif

#if defined(NPY_PY3K)
#define RETVAL m
PyMODINIT_FUNC PyInit_umath_tests(void)
#else
#define RETVAL
PyMODINIT_FUNC
initumath_tests(void)
#endif
{
    PyObject *m;
    PyObject *d;
    PyObject *version;

#if defined(NPY_PY3K)
    m = PyModule_Create(&moduledef);
#else
    m = Py_InitModule("umath_tests", UMath_TestsMethods);
#endif
    if (m == NULL)
        return RETVAL;

    import_array();
    import_ufunc();

    d = PyModule_GetDict(m);

    version = PyString_FromString("0.1");
    PyDict_SetItemString(d, "__version__", version);
    Py_DECREF(version);

    /* Load the ufunc operators into the module's namespace */
    addUfuncs(d);

    if (PyErr_Occurred()) {
        PyErr_SetString(PyExc_RuntimeError,
                        "cannot load umath_tests module.");
    }

    return RETVAL;
}
