/*
 * bufsum -- zero-copy bulk data plus correct interpreter release.
 *
 * Demonstrates:
 *   - acquiring, validating and releasing a Py_buffer on every path
 *   - Py_BEGIN_ALLOW_THREADS around pure-C work, with the error recorded
 *     in a plain C variable and raised only after re-attaching
 *   - a writable buffer request (in-place mutation)
 *   - the y* argument-parsing shortcut
 *
 * Build:  pip install -e . --no-build-isolation
 * Lessons: 06, 10, 12
 */

#include <Python.h>
#include <math.h>
#include <string.h>

/* Shared validation: a 1-D contiguous buffer of C doubles. */
static int
get_double_view(PyObject *obj, Py_buffer *view, int writable)
{
    int flags = PyBUF_C_CONTIGUOUS | PyBUF_FORMAT;
    if (writable) {
        flags |= PyBUF_WRITABLE;
    }
    if (PyObject_GetBuffer(obj, view, flags) < 0) {
        return -1;                  /* BufferError already set */
    }
    if (view->ndim != 1) {
        PyErr_Format(PyExc_ValueError, "expected a 1-D buffer, got %d dimensions",
                     view->ndim);
        goto error;
    }
    if (view->format == NULL || strcmp(view->format, "d") != 0) {
        PyErr_Format(PyExc_TypeError, "expected a buffer of C doubles ('d'), got '%s'",
                     view->format ? view->format : "B");
        goto error;
    }
    if (view->itemsize != (Py_ssize_t)sizeof(double)) {
        PyErr_SetString(PyExc_TypeError, "unexpected item size");
        goto error;
    }
    return 0;

error:
    PyBuffer_Release(view);
    return -1;
}

/* ------------------------------------------------------------------ *
 * sum_doubles(buf, /) -> float
 * ------------------------------------------------------------------ */
static PyObject *
bufsum_sum_doubles(PyObject *module, PyObject *obj)
{
    Py_buffer view;
    if (get_double_view(obj, &view, 0) < 0) {
        return NULL;
    }

    const double *p = (const double *)view.buf;   /* pinned by the view */
    Py_ssize_t n = view.shape[0];
    double total = 0.0;
    int not_finite = 0;                            /* error flag in plain C */

    Py_BEGIN_ALLOW_THREADS
    for (Py_ssize_t i = 0; i < n; i++) {
        total += p[i];
    }
    if (!isfinite(total)) {
        not_finite = 1;
    }
    Py_END_ALLOW_THREADS

    PyBuffer_Release(&view);

    if (not_finite) {
        PyErr_SetString(PyExc_OverflowError, "sum is not finite");
        return NULL;
    }
    return PyFloat_FromDouble(total);
}

/* ------------------------------------------------------------------ *
 * scale(buf, k, /) -> None      (in place, requires a writable buffer)
 * ------------------------------------------------------------------ */
static PyObject *
bufsum_scale(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
{
    if (nargs != 2) {
        PyErr_SetString(PyExc_TypeError, "scale() takes exactly 2 arguments");
        return NULL;
    }
    double k = PyFloat_AsDouble(args[1]);
    if (k == -1.0 && PyErr_Occurred()) {
        return NULL;
    }

    Py_buffer view;
    if (get_double_view(args[0], &view, 1) < 0) {
        return NULL;
    }

    double *p = (double *)view.buf;
    Py_ssize_t n = view.shape[0];

    Py_BEGIN_ALLOW_THREADS
    for (Py_ssize_t i = 0; i < n; i++) {
        p[i] *= k;
    }
    Py_END_ALLOW_THREADS

    PyBuffer_Release(&view);
    Py_RETURN_NONE;
}

/* ------------------------------------------------------------------ *
 * checksum(data, /) -> int
 *
 * Uses the y* format unit, which acquires a Py_buffer for you. Note that
 * it must still be released on every path, including the error path.
 * ------------------------------------------------------------------ */
static PyObject *
bufsum_checksum(PyObject *module, PyObject *args)
{
    Py_buffer data;
    if (!PyArg_ParseTuple(args, "y*:checksum", &data)) {
        return NULL;
    }

    const unsigned char *p = (const unsigned char *)data.buf;
    Py_ssize_t n = data.len;
    unsigned long long h = 1469598103934665603ULL;   /* FNV-1a offset basis */

    Py_BEGIN_ALLOW_THREADS
    for (Py_ssize_t i = 0; i < n; i++) {
        h ^= (unsigned long long)p[i];
        h *= 1099511628211ULL;
    }
    Py_END_ALLOW_THREADS

    PyBuffer_Release(&data);
    return PyLong_FromUnsignedLongLong(h);
}

/* ------------------------------------------------------------------ */

static PyMethodDef bufsum_methods[] = {
    {"sum_doubles", bufsum_sum_doubles, METH_O,
     PyDoc_STR("sum_doubles(buf, /)\n--\n\nSum a 1-D contiguous buffer of doubles.")},
    {"scale", (PyCFunction)(void (*)(void))bufsum_scale, METH_FASTCALL,
     PyDoc_STR("scale(buf, k, /)\n--\n\nMultiply every element of a writable buffer by k.")},
    {"checksum", bufsum_checksum, METH_VARARGS,
     PyDoc_STR("checksum(data, /)\n--\n\nFNV-1a hash of any bytes-like object.")},
    {NULL}
};

static PyModuleDef_Slot bufsum_slots[] = {
    {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
#if PY_VERSION_HEX >= 0x030D0000
    {Py_mod_gil, Py_MOD_GIL_NOT_USED},
#endif
    {0, NULL}
};

static PyModuleDef bufsum_module = {
    .m_base    = PyModuleDef_HEAD_INIT,
    .m_name    = "bufsum",
    .m_doc     = PyDoc_STR("Buffer protocol and interpreter release."),
    .m_size    = 0,
    .m_methods = bufsum_methods,
    .m_slots   = bufsum_slots,
};

PyMODINIT_FUNC
PyInit_bufsum(void)
{
    return PyModuleDef_Init(&bufsum_module);
}
