
/* Included by _internal.c */
#include "_internal.h"

static int
get_string(PyObject *obj, const char **s, const char *type_error_message)
{
    if (!PyString_Check(obj) && obj != Py_None) {
        PyErr_SetString(PyExc_TypeError, type_error_message);
        return -1;
    }
    *s = NULL;
    if (obj != Py_None) {
        *s = PyString_AsString(obj);
        if (*s == NULL)
            return -1;
    }
    return 0;
}


PyObject *
ufunc_fromfunc(PyObject *NPY_UNUSED(dummy), PyObject *args)
{
    int nin, nout;
    int nfuncs, ntypes, ndata, n_writable_args;
    PyObject *func_list;
    PyObject *type_list;
    PyObject *data_list;
    PyObject *func_obj;
    PyObject *type_obj;
    PyObject *data_obj;
    PyObject *object; /* object to hold on to while ufunc is alive */
    PyObject *pyname, *pydoc;
    const char *name = NULL, *doc = NULL;
    char *signature = NULL;
    int identity;
    PyObject *writable_args_tuple = NULL;

    int i, j, idx, success;
    int custom_dtype = 0;
    PyUFuncGenericFunction *funcs;
    int *types;
    void **data;
    PyUFuncObject *ufunc;

    if (!PyArg_ParseTuple(args, "OOO!O!iiOOi|sO!",
                          &pyname, &pydoc,
                          &PyList_Type, &func_list,
                          &PyList_Type, &type_list,
                          &nin, &nout, &data_list,
                          &object, &identity, &signature,
                          &PyTuple_Type, &writable_args_tuple)) {
        return NULL;
    }
    if (get_string(pyname, &name, "name should be str or None"))
        return NULL;
    if (get_string(pydoc, &doc, "doc should be str or None"))
        return NULL;
    /* Ensure the pointers to C strings stay alive until the ufunc dies. */
    object = PyTuple_Pack(3, object, pyname, pydoc);
    if (!object)
        return NULL;

    nfuncs = PyList_Size(func_list);

    ntypes = PyList_Size(type_list);
    if (ntypes != nfuncs) {
        PyErr_SetString(PyExc_TypeError, "length of types list must be same as length of function pointer list");
        return NULL;
    }

    ndata = PyList_Size(data_list);
    if (ndata != nfuncs) {
        PyErr_SetString(PyExc_TypeError, "length of data pointer list must be same as length of function pointer list");
        return NULL;
    }

    funcs = PyArray_malloc(nfuncs * sizeof(PyUFuncGenericFunction));
    if (funcs == NULL) {
        return NULL;
    }

    /* build function pointer array */
    for (i = 0; i < nfuncs; i++) {
        func_obj = PyList_GetItem(func_list, i);
        /* Function pointers are passed in as long objects.
           Is there a better way to do this? */
        if (PyLong_Check(func_obj)) {
            funcs[i] = (PyUFuncGenericFunction)PyLong_AsVoidPtr(func_obj);
        }
        else {
            PyErr_SetString(PyExc_TypeError, "function pointer must be long object, or None");
            return NULL;
        }
    }

    types = PyArray_malloc(nfuncs * (nin+nout) * sizeof(int));
    if (types == NULL) {
        return NULL;
    }

    /* build function signatures array */
    for (i = 0; i < nfuncs; i++) {
        type_obj = PyList_GetItem(type_list, i);
        if (!type_obj)
            return NULL;

        for (j = 0; j < (nin+nout); j++) {
            int dtype_num;
            PyObject *dtype_num_obj = PyList_GetItem(type_obj, j);
            if (!dtype_num_obj)
                return NULL;

            SENTRY_VALID_LONG(
                types[i*(nin+nout) + j] = PyLong_AsLong(dtype_num_obj)
            );

            dtype_num = PyLong_AsLong(PyList_GetItem(type_obj, j));

            SENTRY_VALID_LONG(dtype_num);

            if (dtype_num >= NPY_USERDEF) {
                custom_dtype = dtype_num;
            }
        }
    }

    data = PyArray_malloc(nfuncs * sizeof(void *));
    if (data == NULL) {
        return NULL;
    }

    /* build function data pointers array */
    for (i = 0; i < nfuncs; i++) {
        if (PyList_Check(data_list)) {
            data_obj = PyList_GetItem(data_list, i);
            if (PyLong_Check(data_obj)) {
                data[i] = PyLong_AsVoidPtr(data_obj);
            }
            else if (data_obj == Py_None) {
                data[i] = NULL;
            }
            else {
                PyErr_SetString(PyExc_TypeError, "data pointer must be long object, or None");
                return NULL;
            }
        }
        else if (data_list == Py_None) {
            data[i] = NULL;
        }
        else {
            PyErr_SetString(PyExc_TypeError, "data pointers argument must be a list of void pointers, or None");
            return NULL;
        }
    }

    if (!custom_dtype) {
        char *char_types = PyArray_malloc(nfuncs * (nin+nout) * sizeof(char));
        for (i = 0; i < nfuncs; i++) {
            for (j = 0; j < (nin+nout); j++) {
                char_types[i*(nin+nout) + j] = (char)types[i*(nin+nout) + j];
            }
        }
        PyArray_free(types);
        ufunc = (PyUFuncObject *) PyUFunc_FromFuncAndDataAndSignature(
            (PyUFuncGenericFunction*) funcs, data, (char*) char_types,
            nfuncs, nin, nout,
            identity,
            name, doc,
            0 /* check_return */, signature);
        if (!ufunc) {
            PyArray_free(funcs);
            PyArray_free(data);
            Py_DECREF(object);
            return NULL;
        }
        /* XXX funcs, char_types and data won't be free'ed when the ufunc dies */
    }
    else {
        ufunc = (PyUFuncObject *) PyUFunc_FromFuncAndDataAndSignature(
            0, 0, 0, 0,
            nin,
            nout,
            identity,
            name, doc,
            0 /* check_return */, signature);
        if (!ufunc) {
            PyArray_free(funcs);
            PyArray_free(data);
            PyArray_free(types);
            Py_DECREF(object);
            return NULL;
        }

        PyUFunc_RegisterLoopForType(ufunc,
                                    custom_dtype,
                                    funcs[0],
                                    types,
                                    0);
        PyArray_free(funcs);
        PyArray_free(types);
        PyArray_free(data);
        funcs = NULL;
        data = NULL;
    }

    for (idx = 0; idx < nout; idx++) {
        ufunc->op_flags[idx + nin] |= NPY_ITER_READWRITE | NPY_ITER_UPDATEIFCOPY | NPY_ITER_ALLOCATE;
    }

    /* Create the sentinel object to clean up dynamically-allocated fields
       when the ufunc is destroyed. */
    ufunc->obj = cleaner_new(ufunc, object);
    Py_DECREF(object);
    if (ufunc->obj == NULL) {
        PyArray_free(funcs);
        PyArray_free(data);
        Py_DECREF(ufunc);
        return NULL;
    }

    /* update op_flags */
    if (writable_args_tuple != NULL) {
        n_writable_args = PyTuple_Size(writable_args_tuple);
        if (n_writable_args > nin) {
                PyErr_SetString(PyExc_ValueError, "Number of writable_args must be <= nin");
                Py_DECREF(ufunc);
                return NULL;
        }
        for (i = 0; i < n_writable_args; ++i) {
            data_obj = PyTuple_GET_ITEM(writable_args_tuple, i);
            success = 0;
            if (PyLong_Check(data_obj)) {
                j = PyLong_AsLong(data_obj);
                if ((j >= 0) && (j < nin)) {
                    ufunc->op_flags[j] |= NPY_ITER_READWRITE | NPY_ITER_UPDATEIFCOPY;
                    success = 1;
                }
            }
            if (!success) {
                PyErr_SetString(PyExc_ValueError,
                                "All values in writable_args must be integers between 0 and nin - 1");
                Py_DECREF(ufunc);
                return NULL;
            }
        }
    }

    return (PyObject *) ufunc;
}
