/*
 * Copyright (c) 2014-2026, Yue Du
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright notice,
 *       this list of conditions and the following disclaimer in the documentation
 *       and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

/* This module provides an interface to xxhash, an extremely fast
 non-cryptographic hash algorithm */

#include <Python.h>

#include "xxhash.h"

/* ------------------------------------------------------------------ */
/*  Lock type & helpers                                               */
/* ------------------------------------------------------------------ */
#if PY_VERSION_HEX >= 0x030d0000 /* Python 3.13+: always-on PyMutex (3.15+ style) */
#  define XXHASH_LOCK_FIELD      PyMutex mutex;
#  define XXHASH_LOCK_INIT(o)    ((void)((o)->mutex = (PyMutex){0}))
#  define XXHASH_LOCK_IS_ACTIVE(o)  1
#  define XXHASH_LOCK_MAYBE_INIT(o, len)  ((void)0)
#  define XXHASH_LOCK_FINI(o)    ((void)0)
#  define XXHASH_LOCK_ACQUIRE(o)          PyMutex_Lock(&(o)->mutex)
#  define XXHASH_LOCK_ACQUIRE_BLOCKING(o) XXHASH_LOCK_ACQUIRE(o)
#  define XXHASH_LOCK_RELEASE(o)       PyMutex_Unlock(&(o)->mutex)
#else  /* Python 3.9-3.12: PyThread_type_lock */
#  define XXHASH_LOCK_FIELD      PyThread_type_lock lock;
#  define XXHASH_LOCK_INIT(o)    ((o)->lock = NULL)
#  define XXHASH_LOCK_IS_ACTIVE(o)  ((o)->lock != NULL)
/* Lazy allocation on first update large enough to release the GIL */
#  define XXHASH_LOCK_MAYBE_INIT(o, len)                                 \
    do {                                                                 \
        if ((o)->lock == NULL && (len) > XXHASH_GIL_MINSIZE) {           \
            (o)->lock = PyThread_allocate_lock();                        \
            /* fail? lock stays NULL, fall back to non-threaded code. */ \
        }                                                                \
    } while (0)
#  define XXHASH_LOCK_FINI(o)    do { if ((o)->lock)                 \
                                      PyThread_free_lock((o)->lock); \
                                  } while (0)
/* Acquire lock when GIL is already released — simple blocking acquire.
 * Only acquires if lock has been allocated (lazy init). */
#  define XXHASH_LOCK_ACQUIRE_BLOCKING(o)                \
    do {                                                 \
        if ((o)->lock) {                                 \
            PyThread_acquire_lock((o)->lock, WAIT_LOCK); \
        }                                                \
    } while (0)

/* Acquire lock with the GIL held — non-blocking try first, then release
 * GIL and block if contested (matches hashlib's ENTER_HASHLIB in 3.9-3.12).
 * Only acquires if lock has been allocated (lazy init). */
#  define XXHASH_LOCK_ACQUIRE(o)                                  \
    do {                                                          \
        if ((o)->lock) {                                          \
            if (!PyThread_acquire_lock((o)->lock, NOWAIT_LOCK)) { \
                /* Lock contested – release GIL while waiting. */ \
                Py_BEGIN_ALLOW_THREADS                            \
                PyThread_acquire_lock((o)->lock, WAIT_LOCK);      \
                Py_END_ALLOW_THREADS                              \
            }                                                     \
        }                                                         \
    } while (0)

#  define XXHASH_LOCK_RELEASE(o)              \
    do {                                      \
        if ((o)->lock) {                      \
            PyThread_release_lock((o)->lock); \
        }                                     \
    } while (0)
#endif

/* Data size threshold for releasing the GIL during hash. */
#define XXHASH_GIL_MINSIZE  65536

#define TOSTRING(x) #x
#define VALUE_TO_STRING(x) TOSTRING(x)
#define XXHASH_VERSION XXH_VERSION_MAJOR.XXH_VERSION_MINOR.XXH_VERSION_RELEASE

#define XXH32_DIGESTSIZE 4
#define XXH32_BLOCKSIZE 16
#define XXH64_DIGESTSIZE 8
#define XXH64_BLOCKSIZE 32
#define XXH128_DIGESTSIZE 16
#define XXH128_BLOCKSIZE 64

/* Get a buffer from an object. Rejects str with hashlib-compatible error. */
static inline int
_get_buffer_or_str(PyObject *obj, Py_buffer *buf)
{
    if (obj == Py_None) {
        PyErr_SetString(PyExc_TypeError,
            "object supporting the buffer API required");
        return -1;
    }
    if (PyUnicode_Check(obj)) {
        PyErr_SetString(PyExc_TypeError,
            "Strings must be encoded before hashing");
        return -1;
    }
    /* Fast path: bypass PyObject_GetBuffer dispatch for bytes objects.
     * For PyBUF_SIMPLE this is equivalent to PyBuffer_FillInfo(...).
     * PyBuffer_Release will DECREF buf->obj (bytes has no releaseproc). */
    if (PyBytes_Check(obj)) {
        buf->buf = PyBytes_AS_STRING(obj);
        buf->len = PyBytes_GET_SIZE(obj);
        buf->obj = (PyObject *)obj;
        Py_INCREF(obj);
        buf->readonly = 1;
        buf->itemsize = 1;
        buf->format = NULL;
        buf->ndim = 1;
        buf->shape = NULL;
        buf->strides = NULL;
        buf->suboffsets = NULL;
        buf->internal = NULL;
        return 0;
    }
    if (PyObject_GetBuffer(obj, buf, PyBUF_SIMPLE) < 0)
        return -1;
    return 0;
}

/* Parse data buffer and optional seed from fastcall arguments.
 * Handles: positional 'data', positional 'seed', keyword 'data',
 * keyword 'seed', with proper error reporting for unknown keywords,
 * duplicate arguments, and too many positional args.
 * Returns 0 on success, -1 on error with exception set. */
static inline int
_parse_fastcall_args(PyObject *const *args, Py_ssize_t nargs,
                     PyObject *kwnames, const char *funcname,
                     int data_required,
                     Py_buffer *buf,
                     unsigned long long *seed)
{
    int data_found = 0;
    int seed_found = 0;

    *seed = 0;
    buf->buf = NULL;
    buf->obj = NULL;

    /* positional args */
    if (nargs >= 1) {
        if (_get_buffer_or_str(args[0], buf) < 0)
            return -1;
        data_found = 1;
    }
    if (nargs >= 2) {
        *seed = PyLong_AsUnsignedLongLongMask(args[1]);
        if (PyErr_Occurred())
            goto error;
        seed_found = 1;
    }
    if (nargs > 2) {
        PyErr_Format(PyExc_TypeError,
            "%s() takes at most 2 positional arguments (%zd given)",
            funcname, nargs);
        goto error;
    }

    /* keyword args */
    if (kwnames) {
        Py_ssize_t nkw = PyTuple_GET_SIZE(kwnames);
        for (Py_ssize_t i = 0; i < nkw; i++) {
            PyObject *key = PyTuple_GET_ITEM(kwnames, i);
            PyObject *val = args[nargs + i];

            if (PyUnicode_CompareWithASCIIString(key, "data") == 0) {
                if (data_found) {
                    PyErr_Format(PyExc_TypeError,
                        "%s() got multiple values for argument 'data'",
                        funcname);
                    goto error;
                }
                if (_get_buffer_or_str(val, buf) < 0)
                    return -1;
                data_found = 1;
            } else if (PyUnicode_CompareWithASCIIString(key, "seed") == 0) {
                if (seed_found) {
                    PyErr_Format(PyExc_TypeError,
                        "%s() got multiple values for argument 'seed'",
                        funcname);
                    goto error;
                }
                *seed = PyLong_AsUnsignedLongLongMask(val);
                if (PyErr_Occurred())
                    goto error;
                seed_found = 1;
            } else {
                PyErr_Format(PyExc_TypeError,
                    "'%U' is an invalid keyword argument for '%s()'",
                    key, funcname);
                goto error;
            }
        }
    }

    if (!data_found && data_required) {
        PyErr_Format(PyExc_TypeError,
            "%s() missing required argument 'data'", funcname);
        return -1;
    }
    return 0;

error:
    if (data_found) {
        PyBuffer_Release(buf);
    }
    return -1;
}

/*****************************************************************************
 * Module Functions ***********************************************************
 ****************************************************************************/

/* XXH32 */
static PyObject *xxh32_digest(PyObject *self, PyObject *const *args,
                               Py_ssize_t nargs, PyObject *kwnames)
{
    XXH32_hash_t seed = 0;
    Py_buffer buf;
    unsigned long long raw_seed;
    if (_parse_fastcall_args(args, nargs, kwnames, "xxh32_digest", 1, &buf, &raw_seed) < 0)
        return NULL;
    seed = (XXH32_hash_t)raw_seed;

    XXH32_hash_t intdigest;
    if (buf.len > XXHASH_GIL_MINSIZE) {
        Py_BEGIN_ALLOW_THREADS
        intdigest = XXH32(buf.buf, buf.len, seed);
        Py_END_ALLOW_THREADS
    } else {
        intdigest = XXH32(buf.buf, buf.len, seed);
    }
    PyBuffer_Release(&buf);

    PyObject *ret = PyBytes_FromStringAndSize(NULL, XXH32_DIGESTSIZE);
    if (ret == NULL) return NULL;
    XXH32_canonicalFromHash((XXH32_canonical_t *)PyBytes_AS_STRING(ret), intdigest);
    return ret;
}
static PyObject *xxh32_intdigest(PyObject *self, PyObject *const *args,
                                  Py_ssize_t nargs, PyObject *kwnames)
{
    XXH32_hash_t seed = 0;
    Py_buffer buf;
    unsigned long long raw_seed;
    if (_parse_fastcall_args(args, nargs, kwnames, "xxh32_intdigest", 1, &buf, &raw_seed) < 0)
        return NULL;
    seed = (XXH32_hash_t)raw_seed;

    XXH32_hash_t intdigest;
    if (buf.len > XXHASH_GIL_MINSIZE) {
        Py_BEGIN_ALLOW_THREADS
        intdigest = XXH32(buf.buf, buf.len, seed);
        Py_END_ALLOW_THREADS
    } else {
        intdigest = XXH32(buf.buf, buf.len, seed);
    }
    PyBuffer_Release(&buf);

    return PyLong_FromUnsignedLong(intdigest);
}
static PyObject *xxh32_hexdigest(PyObject *self, PyObject *const *args,
                                  Py_ssize_t nargs, PyObject *kwnames)
{
    XXH32_hash_t seed = 0;
    Py_buffer buf;
    unsigned long long raw_seed;
    if (_parse_fastcall_args(args, nargs, kwnames, "xxh32_hexdigest", 1, &buf, &raw_seed) < 0)
        return NULL;
    seed = (XXH32_hash_t)raw_seed;

    XXH32_hash_t intdigest;
    if (buf.len > XXHASH_GIL_MINSIZE) {
        Py_BEGIN_ALLOW_THREADS
        intdigest = XXH32(buf.buf, buf.len, seed);
        Py_END_ALLOW_THREADS
    } else {
        intdigest = XXH32(buf.buf, buf.len, seed);
    }
    PyBuffer_Release(&buf);

    char digest[XXH32_DIGESTSIZE];
    XXH32_canonicalFromHash((XXH32_canonical_t *)digest, intdigest);

    PyObject *ret = PyUnicode_New(XXH32_DIGESTSIZE * 2, 127);
    if (ret == NULL) return NULL;
    Py_UCS1 *b = PyUnicode_1BYTE_DATA(ret);
    for (Py_ssize_t i = 0, j = 0; i < XXH32_DIGESTSIZE; i++) {
        unsigned char c;
        c = (digest[i] >> 4) & 0xf;
        c = (c > 9) ? c + 'a' - 10 : c + '0';
        b[j++] = c;
        c = (digest[i] & 0xf);
        c = (c > 9) ? c + 'a' - 10 : c + '0';
        b[j++] = c;
    }
    return ret;
}

/* XXH64 */
static PyObject *xxh64_digest(PyObject *self, PyObject *const *args,
                               Py_ssize_t nargs, PyObject *kwnames)
{
    XXH64_hash_t seed = 0;
    Py_buffer buf;
    unsigned long long raw_seed;
    if (_parse_fastcall_args(args, nargs, kwnames, "xxh64_digest", 1, &buf, &raw_seed) < 0)
        return NULL;
    seed = (XXH64_hash_t)raw_seed;

    XXH64_hash_t intdigest;
    if (buf.len > XXHASH_GIL_MINSIZE) {
        Py_BEGIN_ALLOW_THREADS
        intdigest = XXH64(buf.buf, buf.len, seed);
        Py_END_ALLOW_THREADS
    } else {
        intdigest = XXH64(buf.buf, buf.len, seed);
    }
    PyBuffer_Release(&buf);

    PyObject *ret = PyBytes_FromStringAndSize(NULL, XXH64_DIGESTSIZE);
    if (ret == NULL) return NULL;
    XXH64_canonicalFromHash((XXH64_canonical_t *)PyBytes_AS_STRING(ret), intdigest);
    return ret;
}
static PyObject *xxh64_intdigest(PyObject *self, PyObject *const *args,
                                  Py_ssize_t nargs, PyObject *kwnames)
{
    XXH64_hash_t seed = 0;
    Py_buffer buf;
    unsigned long long raw_seed;
    if (_parse_fastcall_args(args, nargs, kwnames, "xxh64_intdigest", 1, &buf, &raw_seed) < 0)
        return NULL;
    seed = (XXH64_hash_t)raw_seed;

    XXH64_hash_t intdigest;
    if (buf.len > XXHASH_GIL_MINSIZE) {
        Py_BEGIN_ALLOW_THREADS
        intdigest = XXH64(buf.buf, buf.len, seed);
        Py_END_ALLOW_THREADS
    } else {
        intdigest = XXH64(buf.buf, buf.len, seed);
    }
    PyBuffer_Release(&buf);

    return PyLong_FromUnsignedLongLong(intdigest);
}
static PyObject *xxh64_hexdigest(PyObject *self, PyObject *const *args,
                                  Py_ssize_t nargs, PyObject *kwnames)
{
    XXH64_hash_t seed = 0;
    Py_buffer buf;
    unsigned long long raw_seed;
    if (_parse_fastcall_args(args, nargs, kwnames, "xxh64_hexdigest", 1, &buf, &raw_seed) < 0)
        return NULL;
    seed = (XXH64_hash_t)raw_seed;

    XXH64_hash_t intdigest;
    if (buf.len > XXHASH_GIL_MINSIZE) {
        Py_BEGIN_ALLOW_THREADS
        intdigest = XXH64(buf.buf, buf.len, seed);
        Py_END_ALLOW_THREADS
    } else {
        intdigest = XXH64(buf.buf, buf.len, seed);
    }
    PyBuffer_Release(&buf);

    char digest[XXH64_DIGESTSIZE];
    XXH64_canonicalFromHash((XXH64_canonical_t *)digest, intdigest);

    PyObject *ret = PyUnicode_New(XXH64_DIGESTSIZE * 2, 127);
    if (ret == NULL) return NULL;
    Py_UCS1 *b = PyUnicode_1BYTE_DATA(ret);
    for (Py_ssize_t i = 0, j = 0; i < XXH64_DIGESTSIZE; i++) {
        unsigned char c;
        c = (digest[i] >> 4) & 0xf;
        c = (c > 9) ? c + 'a' - 10 : c + '0';
        b[j++] = c;
        c = (digest[i] & 0xf);
        c = (c > 9) ? c + 'a' - 10 : c + '0';
        b[j++] = c;
    }
    return ret;
}

/* XXH3_64 */
static PyObject *xxh3_64_digest(PyObject *self, PyObject *const *args,
                               Py_ssize_t nargs, PyObject *kwnames)
{
    XXH64_hash_t seed = 0;
    Py_buffer buf;
    unsigned long long raw_seed;
    if (_parse_fastcall_args(args, nargs, kwnames, "xxh3_64_digest", 1, &buf, &raw_seed) < 0)
        return NULL;
    seed = (XXH64_hash_t)raw_seed;

    XXH64_hash_t intdigest;
    if (buf.len > XXHASH_GIL_MINSIZE) {
        Py_BEGIN_ALLOW_THREADS
        intdigest = XXH3_64bits_withSeed(buf.buf, buf.len, seed);
        Py_END_ALLOW_THREADS
    } else {
        intdigest = XXH3_64bits_withSeed(buf.buf, buf.len, seed);
    }
    PyBuffer_Release(&buf);

    PyObject *ret = PyBytes_FromStringAndSize(NULL, XXH64_DIGESTSIZE);
    if (ret == NULL) return NULL;
    XXH64_canonicalFromHash((XXH64_canonical_t *)PyBytes_AS_STRING(ret), intdigest);
    return ret;
}
static PyObject *xxh3_64_intdigest(PyObject *self, PyObject *const *args,
                                  Py_ssize_t nargs, PyObject *kwnames)
{
    XXH64_hash_t seed = 0;
    Py_buffer buf;
    unsigned long long raw_seed;
    if (_parse_fastcall_args(args, nargs, kwnames, "xxh3_64_intdigest", 1, &buf, &raw_seed) < 0)
        return NULL;
    seed = (XXH64_hash_t)raw_seed;

    XXH64_hash_t intdigest;
    if (buf.len > XXHASH_GIL_MINSIZE) {
        Py_BEGIN_ALLOW_THREADS
        intdigest = XXH3_64bits_withSeed(buf.buf, buf.len, seed);
        Py_END_ALLOW_THREADS
    } else {
        intdigest = XXH3_64bits_withSeed(buf.buf, buf.len, seed);
    }
    PyBuffer_Release(&buf);

    return PyLong_FromUnsignedLongLong(intdigest);
}
static PyObject *xxh3_64_hexdigest(PyObject *self, PyObject *const *args,
                                  Py_ssize_t nargs, PyObject *kwnames)
{
    XXH64_hash_t seed = 0;
    Py_buffer buf;
    unsigned long long raw_seed;
    if (_parse_fastcall_args(args, nargs, kwnames, "xxh3_64_hexdigest", 1, &buf, &raw_seed) < 0)
        return NULL;
    seed = (XXH64_hash_t)raw_seed;

    XXH64_hash_t intdigest;
    if (buf.len > XXHASH_GIL_MINSIZE) {
        Py_BEGIN_ALLOW_THREADS
        intdigest = XXH3_64bits_withSeed(buf.buf, buf.len, seed);
        Py_END_ALLOW_THREADS
    } else {
        intdigest = XXH3_64bits_withSeed(buf.buf, buf.len, seed);
    }
    PyBuffer_Release(&buf);

    char digest[XXH64_DIGESTSIZE];
    XXH64_canonicalFromHash((XXH64_canonical_t *)digest, intdigest);

    PyObject *ret = PyUnicode_New(XXH64_DIGESTSIZE * 2, 127);
    if (ret == NULL) return NULL;
    Py_UCS1 *b = PyUnicode_1BYTE_DATA(ret);
    for (Py_ssize_t i = 0, j = 0; i < XXH64_DIGESTSIZE; i++) {
        unsigned char c;
        c = (digest[i] >> 4) & 0xf;
        c = (c > 9) ? c + 'a' - 10 : c + '0';
        b[j++] = c;
        c = (digest[i] & 0xf);
        c = (c > 9) ? c + 'a' - 10 : c + '0';
        b[j++] = c;
    }
    return ret;
}

/* XXH3_128 */
static PyObject *xxh3_128_digest(PyObject *self, PyObject *const *args,
                               Py_ssize_t nargs, PyObject *kwnames)
{
    XXH64_hash_t seed = 0;
    Py_buffer buf;
    unsigned long long raw_seed;
    if (_parse_fastcall_args(args, nargs, kwnames, "xxh3_128_digest", 1, &buf, &raw_seed) < 0)
        return NULL;
    seed = (XXH64_hash_t)raw_seed;

    XXH128_hash_t intdigest;
    if (buf.len > XXHASH_GIL_MINSIZE) {
        Py_BEGIN_ALLOW_THREADS
        intdigest = XXH3_128bits_withSeed(buf.buf, buf.len, seed);
        Py_END_ALLOW_THREADS
    } else {
        intdigest = XXH3_128bits_withSeed(buf.buf, buf.len, seed);
    }
    PyBuffer_Release(&buf);

    PyObject *ret = PyBytes_FromStringAndSize(NULL, XXH128_DIGESTSIZE);
    if (ret == NULL) return NULL;
    XXH128_canonicalFromHash((XXH128_canonical_t *)PyBytes_AS_STRING(ret), intdigest);
    return ret;
}
static PyObject *xxh3_128_intdigest(PyObject *self, PyObject *const *args,
                                     Py_ssize_t nargs, PyObject *kwnames)
{
    XXH64_hash_t seed = 0;
    Py_buffer buf;
    unsigned long long raw_seed;
    if (_parse_fastcall_args(args, nargs, kwnames, "xxh3_128_intdigest", 1, &buf, &raw_seed) < 0)
        return NULL;
    seed = (XXH64_hash_t)raw_seed;

    XXH128_hash_t intdigest;
    if (buf.len > XXHASH_GIL_MINSIZE) {
        Py_BEGIN_ALLOW_THREADS
        intdigest = XXH3_128bits_withSeed(buf.buf, buf.len, seed);
        Py_END_ALLOW_THREADS
    } else {
        intdigest = XXH3_128bits_withSeed(buf.buf, buf.len, seed);
    }
    PyBuffer_Release(&buf);

    PyObject *sixtyfour = PyLong_FromLong(64);
    PyObject *low = PyLong_FromUnsignedLongLong(intdigest.low64);
    PyObject *high = PyLong_FromUnsignedLongLong(intdigest.high64);
    PyObject *result = NULL;

    if (sixtyfour && low && high) {
        PyObject *shifted = PyNumber_Lshift(high, sixtyfour);
        if (shifted) {
            result = PyNumber_Add(shifted, low);
            Py_DECREF(shifted);
        }
    }
    Py_XDECREF(high);
    Py_XDECREF(low);
    Py_XDECREF(sixtyfour);
    return result;
}
static PyObject *xxh3_128_hexdigest(PyObject *self, PyObject *const *args,
                                  Py_ssize_t nargs, PyObject *kwnames)
{
    XXH64_hash_t seed = 0;
    Py_buffer buf;
    unsigned long long raw_seed;
    if (_parse_fastcall_args(args, nargs, kwnames, "xxh3_128_hexdigest", 1, &buf, &raw_seed) < 0)
        return NULL;
    seed = (XXH64_hash_t)raw_seed;

    XXH128_hash_t intdigest;
    if (buf.len > XXHASH_GIL_MINSIZE) {
        Py_BEGIN_ALLOW_THREADS
        intdigest = XXH3_128bits_withSeed(buf.buf, buf.len, seed);
        Py_END_ALLOW_THREADS
    } else {
        intdigest = XXH3_128bits_withSeed(buf.buf, buf.len, seed);
    }
    PyBuffer_Release(&buf);

    char digest[XXH128_DIGESTSIZE];
    XXH128_canonicalFromHash((XXH128_canonical_t *)digest, intdigest);

    PyObject *ret = PyUnicode_New(XXH128_DIGESTSIZE * 2, 127);
    if (ret == NULL) return NULL;
    Py_UCS1 *b = PyUnicode_1BYTE_DATA(ret);
    for (Py_ssize_t i = 0, j = 0; i < XXH128_DIGESTSIZE; i++) {
        unsigned char c;
        c = (digest[i] >> 4) & 0xf;
        c = (c > 9) ? c + 'a' - 10 : c + '0';
        b[j++] = c;
        c = (digest[i] & 0xf);
        c = (c > 9) ? c + 'a' - 10 : c + '0';
        b[j++] = c;
    }
    return ret;
}

/*****************************************************************************
 * Module Types ***************************************************************
 ****************************************************************************/

/* XXH32 */

typedef struct {
    PyObject_HEAD
    XXH32_state_t *xxhash_state;
    XXH32_hash_t seed;
    XXHASH_LOCK_FIELD
} PYXXH32Object;

static void PYXXH32_dealloc(PYXXH32Object *self)
{
    if (self->xxhash_state)
        XXH32_freeState(self->xxhash_state);
    XXHASH_LOCK_FINI(self);
    PyTypeObject *tp = Py_TYPE(self);
    tp->tp_free((PyObject *)self);
    Py_DECREF(tp);
}

/* Macro to generate _do_update for each hash type.
 * Matches CPython 3.9-3.12 md5 pattern: release GIL first (for large data),
 * then acquire lock, hash, release lock, re-acquire GIL.
 * For small data, acquire lock with GIL held (try-then-block if contested). */
#define XXHASH_DO_UPDATE(type, update_fn)                                     \
static inline void                                           \
PY##type##_do_update(PY##type##Object *self, Py_buffer *buf)                  \
{                                                                             \
    XXHASH_LOCK_MAYBE_INIT(self, buf->len);                                   \
    if (XXHASH_LOCK_IS_ACTIVE(self)) {                                        \
        if (buf->len > XXHASH_GIL_MINSIZE) {                                  \
            /* Release GIL first, then acquire lock. */                       \
            Py_BEGIN_ALLOW_THREADS                                            \
            XXHASH_LOCK_ACQUIRE_BLOCKING(self);                               \
            update_fn(self->xxhash_state, buf->buf, buf->len);                \
            XXHASH_LOCK_RELEASE(self);                                        \
            Py_END_ALLOW_THREADS                                              \
        } else {                                                              \
            /* Acquire lock with GIL held. */                                 \
            XXHASH_LOCK_ACQUIRE(self);                                        \
            update_fn(self->xxhash_state, buf->buf, buf->len);                \
            XXHASH_LOCK_RELEASE(self);                                        \
        }                                                                     \
    } else {                                                                  \
        /* No lock: hash directly, no GIL release. */                         \
        update_fn(self->xxhash_state, buf->buf, buf->len);                    \
    }                                                                         \
    PyBuffer_Release(buf);                                                    \
}

XXHASH_DO_UPDATE(XXH32, XXH32_update)

static PyObject *
PYXXH32_vectorcall(PyObject *type, PyObject *const *args,
                   size_t nargsf, PyObject *kwnames)
{
    Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
    XXH32_hash_t seed = 0;
    Py_buffer buf;
    unsigned long long raw_seed;

    if (_parse_fastcall_args(args, nargs, kwnames, "xxhash.xxh32", 0,
                             &buf, &raw_seed) < 0)
        return NULL;
    seed = (XXH32_hash_t)raw_seed;

    PYXXH32Object *self = (PYXXH32Object *)
        ((PyTypeObject *)type)->tp_alloc((PyTypeObject *)type, 0);
    if (self == NULL) {
        PyBuffer_Release(&buf);
        return NULL;
    }

    XXHASH_LOCK_INIT(self);

    self->xxhash_state = XXH32_createState();
    if (self->xxhash_state == NULL) {
        Py_DECREF(self);
        PyBuffer_Release(&buf);
        return PyErr_NoMemory();
    }
    self->seed = seed;
    XXH32_reset(self->xxhash_state, seed);

    if (buf.obj) {
        /* Constructor: no concurrent access possible, skip locking. */
        if (buf.len > XXHASH_GIL_MINSIZE) {
            Py_BEGIN_ALLOW_THREADS
            XXH32_update(self->xxhash_state, buf.buf, buf.len);
            Py_END_ALLOW_THREADS
        } else {
            XXH32_update(self->xxhash_state, buf.buf, buf.len);
        }
        PyBuffer_Release(&buf);
    }
    return (PyObject *)self;
}

/* XXH32 methods */

static PyObject *PYXXH32_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
{
    PYXXH32Object *self;

    if ((self = (PYXXH32Object *)type->tp_alloc(type, 0)) == NULL) {
        return NULL;
    }

    XXHASH_LOCK_INIT(self);

    if ((self->xxhash_state = XXH32_createState()) == NULL) {
        Py_DECREF(self);
        PyErr_NoMemory();
        return NULL;
    }

    self->seed = 0;
    XXH32_reset(self->xxhash_state, 0);

    return (PyObject *)self;
}

/* Shared helper for parsing __init__ arguments.
 * Handles positional and keyword 'data' and 'seed', validates keywords,
 * and detects duplicate/multiple values.
 * Returns 0 on success, -1 on error.
 * On success *data_obj is set (or NULL) and *seed is populated. */
static int
_parse_init_args(PyObject *args, PyObject *kwargs,
                 PyObject **data_obj, unsigned long long *seed,
                 const char *funcname)
{
    Py_ssize_t nargs = PyTuple_GET_SIZE(args);

    if (!kwargs) {
        /* fast path: no keywords */
    } else {
        Py_ssize_t pos = 0;
        PyObject *key, *val;
        while (PyDict_Next(kwargs, &pos, &key, &val)) {
            if (PyUnicode_CompareWithASCIIString(key, "data") == 0 ||
                PyUnicode_CompareWithASCIIString(key, "seed") == 0)
                continue;
            PyErr_Format(PyExc_TypeError,
                "'%U' is an invalid keyword argument for '%s()'",
                key, funcname);
            return -1;
        }
    }

    *data_obj = NULL;
    *seed = 0;

    if (nargs >= 1) {
        *data_obj = PyTuple_GET_ITEM(args, 0);
        if (kwargs && PyDict_GetItemString(kwargs, "data")) {
            PyErr_Format(PyExc_TypeError,
                "%s() got multiple values for argument 'data'", funcname);
            return -1;
        }
    }
    if (nargs >= 2) {
        *seed = PyLong_AsUnsignedLongLongMask(PyTuple_GET_ITEM(args, 1));
        if (PyErr_Occurred()) return -1;
        if (kwargs && PyDict_GetItemString(kwargs, "seed")) {
            PyErr_Format(PyExc_TypeError,
                "%s() got multiple values for argument 'seed'", funcname);
            return -1;
        }
    }
    if (nargs > 2) {
        PyErr_Format(PyExc_TypeError,
            "%s() takes at most 2 positional arguments (%zd given)",
            funcname, nargs);
        return -1;
    }

    if (kwargs) {
        PyObject *val = PyDict_GetItemString(kwargs, "data");
        if (val) {
            if (*data_obj) return -1; /* unreachable, caught above */
            *data_obj = val;
        }
        val = PyDict_GetItemString(kwargs, "seed");
        if (val) {
            *seed = PyLong_AsUnsignedLongLongMask(val);
            if (PyErr_Occurred()) return -1;
        }
    }
    return 0;
}

/* Macro to generate __init__ for each hash type. */
#define XXHASH_INIT(type, name, reset_fn, update_fn, seed_cast)               \
static int PY##type##_init(PY##type##Object *self, PyObject *args,            \
                           PyObject *kwargs)                                  \
{                                                                             \
    unsigned long long seed_val = 0;                                          \
    PyObject *data_obj = NULL;                                                \
    Py_buffer buf = {NULL, NULL};                                             \
                                                                              \
    if (_parse_init_args(args, kwargs, &data_obj, &seed_val,                  \
                         name) < 0)                                           \
        return -1;                                                            \
                                                                              \
    if (data_obj) {                                                           \
        if (_get_buffer_or_str(data_obj, &buf) < 0)                           \
            return -1;                                                        \
    }                                                                         \
                                                                              \
    XXHASH_LOCK_ACQUIRE(self);                                                \
    self->seed = (seed_cast)seed_val;                                         \
    reset_fn(self->xxhash_state, self->seed);                                 \
                                                                              \
    if (buf.obj) {                                                            \
        update_fn(self->xxhash_state, buf.buf, buf.len);                      \
        PyBuffer_Release(&buf);                                               \
    }                                                                         \
    XXHASH_LOCK_RELEASE(self);                                                \
    return 0;                                                                 \
}

XXHASH_INIT(XXH32, "xxhash.xxh32", XXH32_reset, XXH32_update, XXH32_hash_t)

PyDoc_STRVAR(
    PYXXH32_update_doc,
    "update (data)\n\n"
    "Update the xxh32 object with bytes-like data. Repeated calls are\n"
    "equivalent to a single call with the concatenation of all the arguments.");

static PyObject *PYXXH32_update(PYXXH32Object *self, PyObject *const *args,
                                 Py_ssize_t nargs, PyObject *kwnames)
{
    PyObject *arg = NULL;

    /* validate keywords first */
    if (kwnames) {
        Py_ssize_t nkw = PyTuple_GET_SIZE(kwnames);
        for (Py_ssize_t i = 0; i < nkw; i++) {
            PyObject *key = PyTuple_GET_ITEM(kwnames, i);
            if (PyUnicode_CompareWithASCIIString(key, "data") == 0) {
                if (nargs >= 1) {
                    PyErr_SetString(PyExc_TypeError,
                        "xxh32.update() got multiple values for argument 'data'");
                    return NULL;
                }
                arg = args[nargs + i];
            } else {
                PyErr_Format(PyExc_TypeError,
                    "'%U' is an invalid keyword argument for 'xxh32.update()'",
                    key);
                return NULL;
            }
        }
    }

    if (nargs >= 1) {
        if (nargs > 1) {
            PyErr_Format(PyExc_TypeError,
                "xxh32.update() takes at most 1 positional argument (%zd given)", nargs);
            return NULL;
        }
        arg = args[0];
    }

    if (!arg) {
        PyErr_SetString(PyExc_TypeError, "xxh32.update() missing required argument 'data'");
        return NULL;
    }

    Py_buffer buf;
    if (_get_buffer_or_str(arg, &buf) < 0)
        return NULL;
    PYXXH32_do_update(self, &buf);
    Py_RETURN_NONE;
}

PyDoc_STRVAR(
    PYXXH32_digest_doc,
    "digest() -> bytes\n\n"
    "Return the digest of the data passed to the update() method so\n"
    "far. This is a 4-byte value which may contain non-ASCII characters,\n"
    "including null bytes.");

static PyObject *PYXXH32_digest(PYXXH32Object *self)
{
    XXH32_hash_t intdigest;

    XXHASH_LOCK_ACQUIRE(self);
    intdigest = XXH32_digest(self->xxhash_state);
    XXHASH_LOCK_RELEASE(self);

    PyObject *ret = PyBytes_FromStringAndSize(NULL, XXH32_DIGESTSIZE);
    if (ret == NULL) return NULL;
    XXH32_canonicalFromHash((XXH32_canonical_t *)PyBytes_AS_STRING(ret), intdigest);
    return ret;
}

PyDoc_STRVAR(
    PYXXH32_hexdigest_doc,
    "hexdigest() -> string\n\n"
    "Like digest(), but returns the digest as a string of hexadecimal digits.");

static PyObject *PYXXH32_hexdigest(PYXXH32Object *self)
{
    XXH32_hash_t intdigest;
    char digest[XXH32_DIGESTSIZE];

    XXHASH_LOCK_ACQUIRE(self);
    intdigest = XXH32_digest(self->xxhash_state);
    XXHASH_LOCK_RELEASE(self);
    XXH32_canonicalFromHash((XXH32_canonical_t *)digest, intdigest);

    PyObject *ret = PyUnicode_New(XXH32_DIGESTSIZE * 2, 127);
    if (ret == NULL) return NULL;
    Py_UCS1 *b = PyUnicode_1BYTE_DATA(ret);
    for (Py_ssize_t i = 0, j = 0; i < XXH32_DIGESTSIZE; i++) {
        unsigned char c;
        c = (digest[i] >> 4) & 0xf;
        c = (c > 9) ? c + 'a' - 10 : c + '0';
        b[j++] = c;
        c = (digest[i] & 0xf);
        c = (c > 9) ? c + 'a' - 10 : c + '0';
        b[j++] = c;
    }
    return ret;
}

PyDoc_STRVAR(
    PYXXH32_intdigest_doc,
    "intdigest() -> int\n\n"
    "Like digest(), but returns the digest as an integer, which is the integer\n"
    "returned by the xxHash C API.");

static PyObject *PYXXH32_intdigest(PYXXH32Object *self)
{
    XXHASH_LOCK_ACQUIRE(self);
    XXH32_hash_t digest = XXH32_digest(self->xxhash_state);
    XXHASH_LOCK_RELEASE(self);
    return PyLong_FromUnsignedLong(digest);
}

PyDoc_STRVAR(
    PYXXH32_copy_doc,
    "copy() -> xxh32 object\n\n"
    "Return a copy of the xxh32 object.");

static PyObject *PYXXH32_copy(PYXXH32Object *self)
{
    PYXXH32Object *p;

    if ((p = (PYXXH32Object *)Py_TYPE(self)->tp_alloc(Py_TYPE(self), 0)) == NULL) {
        return NULL;
    }

    XXHASH_LOCK_INIT(p);

    if ((p->xxhash_state = XXH32_createState()) == NULL) {
        Py_DECREF(p);
        PyErr_NoMemory();
        return NULL;
    }

    XXHASH_LOCK_ACQUIRE(self);
    p->seed = self->seed;
    XXH32_copyState(p->xxhash_state, self->xxhash_state);
    XXHASH_LOCK_RELEASE(self);

    return (PyObject *)p;
}

PyDoc_STRVAR(
    PYXXH32_reset_doc,
    "reset()\n\n"
    "Reset state.");

static PyObject *PYXXH32_reset(PYXXH32Object *self)
{
    XXHASH_LOCK_ACQUIRE(self);
    XXH32_reset(self->xxhash_state, self->seed);
    XXHASH_LOCK_RELEASE(self);
    Py_RETURN_NONE;
}

static PyMethodDef PYXXH32_methods[] = {
    {"update", (PyCFunction)PYXXH32_update, METH_FASTCALL | METH_KEYWORDS, PYXXH32_update_doc},
    {"digest", (PyCFunction)PYXXH32_digest, METH_NOARGS, PYXXH32_digest_doc},
    {"hexdigest", (PyCFunction)PYXXH32_hexdigest, METH_NOARGS, PYXXH32_hexdigest_doc},
    {"intdigest", (PyCFunction)PYXXH32_intdigest, METH_NOARGS, PYXXH32_intdigest_doc},
    {"copy", (PyCFunction)PYXXH32_copy, METH_NOARGS, PYXXH32_copy_doc},
    {"reset", (PyCFunction)PYXXH32_reset, METH_NOARGS, PYXXH32_reset_doc},
    {NULL, NULL, 0, NULL}
};

static PyObject *PYXXH32_get_block_size(PYXXH32Object *self, void *closure)
{
    return PyLong_FromLong(XXH32_BLOCKSIZE);
}

static PyObject *
PYXXH32_get_digest_size(PYXXH32Object *self, void *closure)
{
    return PyLong_FromLong(XXH32_DIGESTSIZE);
}

static PyObject *
PYXXH32_get_name(PYXXH32Object *self, void *closure)
{
    return PyUnicode_FromString("XXH32");
}

static PyObject *
PYXXH32_get_seed(PYXXH32Object *self, void *closure)
{
    return PyLong_FromUnsignedLong(self->seed);
}

static PyGetSetDef PYXXH32_getseters[] = {
    {
        "digest_size",
        (getter)PYXXH32_get_digest_size, NULL,
        "Digest size.",
        NULL
    },
    {
        "block_size",
        (getter)PYXXH32_get_block_size, NULL,
        "Block size.",
        NULL
    },
    {
        "name",
        (getter)PYXXH32_get_name, NULL,
        "Name. Always XXH32.",
        NULL
    },
    {
        "digestsize",
        (getter)PYXXH32_get_digest_size, NULL,
        "Digest size.",
        NULL
    },
    {
        "seed",
        (getter)PYXXH32_get_seed, NULL,
        "Seed.",
        NULL
    },
    {NULL}  /* Sentinel */
};

PyDoc_STRVAR(
    PYXXH32Type_doc,
    "An xxh32 represents the object used to calculate the XXH32 hash of a\n"
    "bytes-like object.\n"
    "\n"
    "Methods:\n"
    "\n"
    "update(data) -- updates the current hash state with additional data\n"
    "digest() -- return the current digest value\n"
    "hexdigest() -- return the current digest as a string of hexadecimal digits\n"
    "intdigest() -- return the current digest as an integer\n"
    "copy() -- return a copy of the current xxh32 object");

static PyType_Slot XXH32Type_slots[] = {
    {Py_tp_dealloc, PYXXH32_dealloc},
    {Py_tp_doc, (void *)PYXXH32Type_doc},
    {Py_tp_methods, PYXXH32_methods},
    {Py_tp_getset, PYXXH32_getseters},
    {Py_tp_init, PYXXH32_init},
    {Py_tp_new, PYXXH32_new},
    {0, NULL},
};

static PyType_Spec XXH32Type_spec = {
    .name = "xxhash.xxh32",
    .basicsize = sizeof(PYXXH32Object),
    .flags = Py_TPFLAGS_DEFAULT
#if PY_VERSION_HEX >= 0x030c0000
           | Py_TPFLAGS_IMMUTABLETYPE
#endif
    ,
    .slots = XXH32Type_slots,
};

/* XXH64 */

typedef struct {
    PyObject_HEAD
    XXH64_state_t *xxhash_state;
    XXH64_hash_t seed;
    XXHASH_LOCK_FIELD
} PYXXH64Object;

static void PYXXH64_dealloc(PYXXH64Object *self)
{
    if (self->xxhash_state)
        XXH64_freeState(self->xxhash_state);
    XXHASH_LOCK_FINI(self);
    PyTypeObject *tp = Py_TYPE(self);
    tp->tp_free((PyObject *)self);
    Py_DECREF(tp);
}

XXHASH_DO_UPDATE(XXH64, XXH64_update)

static PyObject *
PYXXH64_vectorcall(PyObject *type, PyObject *const *args,
                   size_t nargsf, PyObject *kwnames)
{
    Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
    XXH64_hash_t seed = 0;
    Py_buffer buf;
    unsigned long long raw_seed;

    if (_parse_fastcall_args(args, nargs, kwnames, "xxhash.xxh64", 0,
                             &buf, &raw_seed) < 0)
        return NULL;
    seed = (XXH64_hash_t)raw_seed;

    PYXXH64Object *self = (PYXXH64Object *)
        ((PyTypeObject *)type)->tp_alloc((PyTypeObject *)type, 0);
    if (self == NULL) {
        PyBuffer_Release(&buf);
        return NULL;
    }

    XXHASH_LOCK_INIT(self);

    self->xxhash_state = XXH64_createState();
    if (self->xxhash_state == NULL) {
        Py_DECREF(self);
        PyBuffer_Release(&buf);
        return PyErr_NoMemory();
    }
    self->seed = seed;
    XXH64_reset(self->xxhash_state, seed);

    if (buf.obj) {
        /* Constructor: no concurrent access possible, skip locking. */
        if (buf.len > XXHASH_GIL_MINSIZE) {
            Py_BEGIN_ALLOW_THREADS
            XXH64_update(self->xxhash_state, buf.buf, buf.len);
            Py_END_ALLOW_THREADS
        } else {
            XXH64_update(self->xxhash_state, buf.buf, buf.len);
        }
        PyBuffer_Release(&buf);
    }
    return (PyObject *)self;
}
static PyObject *PYXXH64_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
{
    PYXXH64Object *self;

    if ((self = (PYXXH64Object *)type->tp_alloc(type, 0)) == NULL) {
        return NULL;
    }

    XXHASH_LOCK_INIT(self);

    if ((self->xxhash_state = XXH64_createState()) == NULL) {
        Py_DECREF(self);
        PyErr_NoMemory();
        return NULL;
    }

    self->seed = 0;
    XXH64_reset(self->xxhash_state, 0);

    return (PyObject *)self;
}

XXHASH_INIT(XXH64, "xxhash.xxh64", XXH64_reset, XXH64_update, XXH64_hash_t)

PyDoc_STRVAR(
    PYXXH64_update_doc,
    "update (data)\n\n"
    "Update the xxh64 object with bytes-like data. Repeated calls are\n"
    "equivalent to a single call with the concatenation of all the arguments.");

static PyObject *PYXXH64_update(PYXXH64Object *self, PyObject *const *args,
                                 Py_ssize_t nargs, PyObject *kwnames)
{
    PyObject *arg = NULL;

    /* validate keywords first */
    if (kwnames) {
        Py_ssize_t nkw = PyTuple_GET_SIZE(kwnames);
        for (Py_ssize_t i = 0; i < nkw; i++) {
            PyObject *key = PyTuple_GET_ITEM(kwnames, i);
            if (PyUnicode_CompareWithASCIIString(key, "data") == 0) {
                if (nargs >= 1) {
                    PyErr_SetString(PyExc_TypeError,
                        "xxh64.update() got multiple values for argument 'data'");
                    return NULL;
                }
                arg = args[nargs + i];
            } else {
                PyErr_Format(PyExc_TypeError,
                    "'%U' is an invalid keyword argument for 'xxh64.update()'",
                    key);
                return NULL;
            }
        }
    }

    if (nargs >= 1) {
        if (nargs > 1) {
            PyErr_Format(PyExc_TypeError,
                "xxh64.update() takes at most 1 positional argument (%zd given)", nargs);
            return NULL;
        }
        arg = args[0];
    }

    if (!arg) {
        PyErr_SetString(PyExc_TypeError, "xxh64.update() missing required argument 'data'");
        return NULL;
    }

    Py_buffer buf;
    if (_get_buffer_or_str(arg, &buf) < 0)
        return NULL;
    PYXXH64_do_update(self, &buf);
    Py_RETURN_NONE;
}

PyDoc_STRVAR(
    PYXXH64_digest_doc,
    "digest() -> bytes\n\n"
    "Return the digest of the data passed to the update() method so\n"
    "far. This is an 8-byte value which may contain non-ASCII characters,\n"
    "including null bytes.");

static PyObject *PYXXH64_digest(PYXXH64Object *self)
{
    XXH64_hash_t intdigest;

    XXHASH_LOCK_ACQUIRE(self);
    intdigest = XXH64_digest(self->xxhash_state);
    XXHASH_LOCK_RELEASE(self);

    PyObject *ret = PyBytes_FromStringAndSize(NULL, XXH64_DIGESTSIZE);
    if (ret == NULL) return NULL;
    XXH64_canonicalFromHash((XXH64_canonical_t *)PyBytes_AS_STRING(ret), intdigest);
    return ret;
}

PyDoc_STRVAR(
    PYXXH64_hexdigest_doc,
    "hexdigest() -> string\n\n"
    "Like digest(), but returns the digest as a string of hexadecimal digits.");

static PyObject *PYXXH64_hexdigest(PYXXH64Object *self)
{
    XXH64_hash_t intdigest;
    char digest[XXH64_DIGESTSIZE];

    XXHASH_LOCK_ACQUIRE(self);
    intdigest = XXH64_digest(self->xxhash_state);
    XXHASH_LOCK_RELEASE(self);
    XXH64_canonicalFromHash((XXH64_canonical_t *)digest, intdigest);

    PyObject *ret = PyUnicode_New(XXH64_DIGESTSIZE * 2, 127);
    if (ret == NULL) return NULL;
    Py_UCS1 *b = PyUnicode_1BYTE_DATA(ret);
    for (Py_ssize_t i = 0, j = 0; i < XXH64_DIGESTSIZE; i++) {
        unsigned char c;
        c = (digest[i] >> 4) & 0xf;
        c = (c > 9) ? c + 'a' - 10 : c + '0';
        b[j++] = c;
        c = (digest[i] & 0xf);
        c = (c > 9) ? c + 'a' - 10 : c + '0';
        b[j++] = c;
    }
    return ret;
}

PyDoc_STRVAR(
    PYXXH64_intdigest_doc,
    "intdigest() -> int\n\n"
    "Like digest(), but returns the digest as an integer, which is the integer\n"
    "returned by the xxHash C API.");

static PyObject *PYXXH64_intdigest(PYXXH64Object *self)
{
    XXHASH_LOCK_ACQUIRE(self);
    XXH64_hash_t digest = XXH64_digest(self->xxhash_state);
    XXHASH_LOCK_RELEASE(self);
    return PyLong_FromUnsignedLongLong(digest);
}

PyDoc_STRVAR(
    PYXXH64_copy_doc,
    "copy() -> xxh64 object\n\n"
    "Return a copy of the xxh64 object.");

static PyObject *PYXXH64_copy(PYXXH64Object *self)
{
    PYXXH64Object *p;

    if ((p = (PYXXH64Object *)Py_TYPE(self)->tp_alloc(Py_TYPE(self), 0)) == NULL) {
        return NULL;
    }

    XXHASH_LOCK_INIT(p);

    if ((p->xxhash_state = XXH64_createState()) == NULL) {
        Py_DECREF(p);
        PyErr_NoMemory();
        return NULL;
    }

    XXHASH_LOCK_ACQUIRE(self);
    p->seed = self->seed;
    XXH64_copyState(p->xxhash_state, self->xxhash_state);
    XXHASH_LOCK_RELEASE(self);

    return (PyObject *)p;
}

PyDoc_STRVAR(
    PYXXH64_reset_doc,
    "reset()\n\n"
    "Reset state.");

static PyObject *PYXXH64_reset(PYXXH64Object *self)
{
    XXHASH_LOCK_ACQUIRE(self);
    XXH64_reset(self->xxhash_state, self->seed);
    XXHASH_LOCK_RELEASE(self);
    Py_RETURN_NONE;
}

static PyMethodDef PYXXH64_methods[] = {
    {"update", (PyCFunction)PYXXH64_update, METH_FASTCALL | METH_KEYWORDS, PYXXH64_update_doc},
    {"digest", (PyCFunction)PYXXH64_digest, METH_NOARGS, PYXXH64_digest_doc},
    {"hexdigest", (PyCFunction)PYXXH64_hexdigest, METH_NOARGS, PYXXH64_hexdigest_doc},
    {"intdigest", (PyCFunction)PYXXH64_intdigest, METH_NOARGS, PYXXH64_intdigest_doc},
    {"copy", (PyCFunction)PYXXH64_copy, METH_NOARGS, PYXXH64_copy_doc},
    {"reset", (PyCFunction)PYXXH64_reset, METH_NOARGS, PYXXH64_reset_doc},
    {NULL, NULL, 0, NULL}
};

static PyObject *PYXXH64_get_block_size(PYXXH64Object *self, void *closure)
{
    return PyLong_FromLong(XXH64_BLOCKSIZE);
}

static PyObject *
PYXXH64_get_digest_size(PYXXH64Object *self, void *closure)
{
    return PyLong_FromLong(XXH64_DIGESTSIZE);
}

static PyObject *
PYXXH64_get_name(PYXXH64Object *self, void *closure)
{
    return PyUnicode_FromString("XXH64");
}

static PyObject *
PYXXH64_get_seed(PYXXH64Object *self, void *closure)
{
    return PyLong_FromUnsignedLongLong(self->seed);
}

static PyGetSetDef PYXXH64_getseters[] = {
    {
        "digest_size",
        (getter)PYXXH64_get_digest_size, NULL,
        "Digest size.",
        NULL
    },
    {
        "block_size",
        (getter)PYXXH64_get_block_size, NULL,
        "Block size.",
        NULL
    },
    {
        "name",
        (getter)PYXXH64_get_name, NULL,
        "Name. Always XXH64.",
        NULL
    },
    {
        "digestsize",
        (getter)PYXXH64_get_digest_size, NULL,
        "Digest size.",
        NULL
    },
    {
        "seed",
        (getter)PYXXH64_get_seed, NULL,
        "Seed.",
        NULL
    },
    {NULL}  /* Sentinel */
};

PyDoc_STRVAR(
    PYXXH64Type_doc,
    "An xxh64 represents the object used to calculate the XXH64 hash of a\n"
    "bytes-like object.\n"
    "\n"
    "Methods:\n"
    "\n"
    "update(data) -- updates the current hash state with additional data\n"
    "digest() -- return the current digest value\n"
    "hexdigest() -- return the current digest as a string of hexadecimal digits\n"
    "intdigest() -- return the current digest as an integer\n"
    "copy() -- return a copy of the current xxh64 object");

static PyType_Slot XXH64Type_slots[] = {
    {Py_tp_dealloc, PYXXH64_dealloc},
    {Py_tp_doc, (void *)PYXXH64Type_doc},
    {Py_tp_methods, PYXXH64_methods},
    {Py_tp_getset, PYXXH64_getseters},
    {Py_tp_init, PYXXH64_init},
    {Py_tp_new, PYXXH64_new},
    {0, NULL},
};

static PyType_Spec XXH64Type_spec = {
    .name = "xxhash.xxh64",
    .basicsize = sizeof(PYXXH64Object),
    .flags = Py_TPFLAGS_DEFAULT
#if PY_VERSION_HEX >= 0x030c0000
           | Py_TPFLAGS_IMMUTABLETYPE
#endif
    ,
    .slots = XXH64Type_slots,
};

/* XXH3_64 */

typedef struct {
    PyObject_HEAD
    XXH3_state_t *xxhash_state;
    XXH64_hash_t seed;
    XXHASH_LOCK_FIELD
} PYXXH3_64Object;

static void PYXXH3_64_dealloc(PYXXH3_64Object *self)
{
    if (self->xxhash_state)
        XXH3_freeState(self->xxhash_state);
    XXHASH_LOCK_FINI(self);
    PyTypeObject *tp = Py_TYPE(self);
    tp->tp_free((PyObject *)self);
    Py_DECREF(tp);
}

XXHASH_DO_UPDATE(XXH3_64, XXH3_64bits_update)

static PyObject *
PYXXH3_64_vectorcall(PyObject *type, PyObject *const *args,
                     size_t nargsf, PyObject *kwnames)
{
    Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
    XXH64_hash_t seed = 0;
    Py_buffer buf;
    unsigned long long raw_seed;

    if (_parse_fastcall_args(args, nargs, kwnames, "xxhash.xxh3_64", 0,
                             &buf, &raw_seed) < 0)
        return NULL;
    seed = (XXH64_hash_t)raw_seed;

    PYXXH3_64Object *self = (PYXXH3_64Object *)
        ((PyTypeObject *)type)->tp_alloc((PyTypeObject *)type, 0);
    if (self == NULL) {
        PyBuffer_Release(&buf);
        return NULL;
    }

    XXHASH_LOCK_INIT(self);

    self->xxhash_state = XXH3_createState();
    if (self->xxhash_state == NULL) {
        Py_DECREF(self);
        PyBuffer_Release(&buf);
        return PyErr_NoMemory();
    }
    self->seed = seed;
    XXH3_64bits_reset_withSeed(self->xxhash_state, seed);

    if (buf.obj) {
        /* Constructor: no concurrent access possible, skip locking. */
        if (buf.len > XXHASH_GIL_MINSIZE) {
            Py_BEGIN_ALLOW_THREADS
            XXH3_64bits_update(self->xxhash_state, buf.buf, buf.len);
            Py_END_ALLOW_THREADS
        } else {
            XXH3_64bits_update(self->xxhash_state, buf.buf, buf.len);
        }
        PyBuffer_Release(&buf);
    }
    return (PyObject *)self;
}
static PyObject *PYXXH3_64_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
{
    PYXXH3_64Object *self;

    if ((self = (PYXXH3_64Object *)type->tp_alloc(type, 0)) == NULL) {
        return NULL;
    }

    XXHASH_LOCK_INIT(self);

    if ((self->xxhash_state = XXH3_createState()) == NULL) {
        Py_DECREF(self);
        PyErr_NoMemory();
        return NULL;
    }

    self->seed = 0;
    XXH3_64bits_reset_withSeed(self->xxhash_state, 0);

    return (PyObject *)self;
}

XXHASH_INIT(XXH3_64, "xxhash.xxh3_64", XXH3_64bits_reset_withSeed, XXH3_64bits_update, XXH64_hash_t)

PyDoc_STRVAR(
    PYXXH3_64_update_doc,
    "update (data)\n\n"
    "Update the xxh3_64 object with bytes-like data. Repeated calls are\n"
    "equivalent to a single call with the concatenation of all the arguments.");

static PyObject *PYXXH3_64_update(PYXXH3_64Object *self, PyObject *const *args,
                                 Py_ssize_t nargs, PyObject *kwnames)
{
    PyObject *arg = NULL;

    /* validate keywords first */
    if (kwnames) {
        Py_ssize_t nkw = PyTuple_GET_SIZE(kwnames);
        for (Py_ssize_t i = 0; i < nkw; i++) {
            PyObject *key = PyTuple_GET_ITEM(kwnames, i);
            if (PyUnicode_CompareWithASCIIString(key, "data") == 0) {
                if (nargs >= 1) {
                    PyErr_SetString(PyExc_TypeError,
                        "xxh3_64.update() got multiple values for argument 'data'");
                    return NULL;
                }
                arg = args[nargs + i];
            } else {
                PyErr_Format(PyExc_TypeError,
                    "'%U' is an invalid keyword argument for 'xxh3_64.update()'",
                    key);
                return NULL;
            }
        }
    }

    if (nargs >= 1) {
        if (nargs > 1) {
            PyErr_Format(PyExc_TypeError,
                "xxh3_64.update() takes at most 1 positional argument (%zd given)", nargs);
            return NULL;
        }
        arg = args[0];
    }

    if (!arg) {
        PyErr_SetString(PyExc_TypeError, "xxh3_64.update() missing required argument 'data'");
        return NULL;
    }

    Py_buffer buf;
    if (_get_buffer_or_str(arg, &buf) < 0)
        return NULL;
    PYXXH3_64_do_update(self, &buf);
    Py_RETURN_NONE;
}

PyDoc_STRVAR(
    PYXXH3_64_digest_doc,
    "digest() -> bytes\n\n"
    "Return the digest of the data passed to the update() method so\n"
    "far. This is an 8-byte value which may contain non-ASCII characters,\n"
    "including null bytes.");

static PyObject *PYXXH3_64_digest(PYXXH3_64Object *self)
{
    XXH64_hash_t intdigest;

    XXHASH_LOCK_ACQUIRE(self);
    intdigest = XXH3_64bits_digest(self->xxhash_state);
    XXHASH_LOCK_RELEASE(self);

    PyObject *ret = PyBytes_FromStringAndSize(NULL, XXH64_DIGESTSIZE);
    if (ret == NULL) return NULL;
    XXH64_canonicalFromHash((XXH64_canonical_t *)PyBytes_AS_STRING(ret), intdigest);
    return ret;
}

PyDoc_STRVAR(
    PYXXH3_64_hexdigest_doc,
    "hexdigest() -> string\n\n"
    "Like digest(), but returns the digest as a string of hexadecimal digits.");

static PyObject *PYXXH3_64_hexdigest(PYXXH3_64Object *self)
{
    XXH64_hash_t intdigest;
    char digest[XXH64_DIGESTSIZE];

    XXHASH_LOCK_ACQUIRE(self);
    intdigest = XXH3_64bits_digest(self->xxhash_state);
    XXHASH_LOCK_RELEASE(self);
    XXH64_canonicalFromHash((XXH64_canonical_t *)digest, intdigest);

    PyObject *ret = PyUnicode_New(XXH64_DIGESTSIZE * 2, 127);
    if (ret == NULL) return NULL;
    Py_UCS1 *b = PyUnicode_1BYTE_DATA(ret);
    for (Py_ssize_t i = 0, j = 0; i < XXH64_DIGESTSIZE; i++) {
        unsigned char c;
        c = (digest[i] >> 4) & 0xf;
        c = (c > 9) ? c + 'a' - 10 : c + '0';
        b[j++] = c;
        c = (digest[i] & 0xf);
        c = (c > 9) ? c + 'a' - 10 : c + '0';
        b[j++] = c;
    }
    return ret;
}

PyDoc_STRVAR(
    PYXXH3_64_intdigest_doc,
    "intdigest() -> int\n\n"
    "Like digest(), but returns the digest as an integer, which is the integer\n"
    "returned by the xxHash C API.");

static PyObject *PYXXH3_64_intdigest(PYXXH3_64Object *self)
{
    XXHASH_LOCK_ACQUIRE(self);
    XXH64_hash_t intdigest = XXH3_64bits_digest(self->xxhash_state);
    XXHASH_LOCK_RELEASE(self);
    return PyLong_FromUnsignedLongLong(intdigest);
}

PyDoc_STRVAR(
    PYXXH3_64_copy_doc,
    "copy() -> xxh3_64 object\n\n"
    "Return a copy of the xxh3_64 object.");

static PyObject *PYXXH3_64_copy(PYXXH3_64Object *self)
{
    PYXXH3_64Object *p;

    if ((p = (PYXXH3_64Object *)Py_TYPE(self)->tp_alloc(Py_TYPE(self), 0)) == NULL) {
        return NULL;
    }

    XXHASH_LOCK_INIT(p);

    if ((p->xxhash_state = XXH3_createState()) == NULL) {
        Py_DECREF(p);
        PyErr_NoMemory();
        return NULL;
    }

    XXHASH_LOCK_ACQUIRE(self);
    p->seed = self->seed;
    XXH3_copyState(p->xxhash_state, self->xxhash_state);
#if XXH_VERSION_NUMBER < 704
    // v0.7.3 and earlier have a bug where states reset with a seed
    // will have a wild pointer to the original state when copied,
    // causing a use-after-free if the original is freed.
    if (p->xxhash_state->secret == &self->xxhash_state->customSecret[0])
        p->xxhash_state->secret = &p->xxhash_state->customSecret[0];
#endif
    XXHASH_LOCK_RELEASE(self);

    return (PyObject *)p;
}

PyDoc_STRVAR(
    PYXXH3_64_reset_doc,
    "reset()\n\n"
    "Reset state.");

static PyObject *PYXXH3_64_reset(PYXXH3_64Object *self)
{
    XXHASH_LOCK_ACQUIRE(self);
    XXH3_64bits_reset_withSeed(self->xxhash_state, self->seed);
    XXHASH_LOCK_RELEASE(self);
    Py_RETURN_NONE;
}

static PyMethodDef PYXXH3_64_methods[] = {
    {"update", (PyCFunction)PYXXH3_64_update, METH_FASTCALL | METH_KEYWORDS, PYXXH3_64_update_doc},
    {"digest", (PyCFunction)PYXXH3_64_digest, METH_NOARGS, PYXXH3_64_digest_doc},
    {"hexdigest", (PyCFunction)PYXXH3_64_hexdigest, METH_NOARGS, PYXXH3_64_hexdigest_doc},
    {"intdigest", (PyCFunction)PYXXH3_64_intdigest, METH_NOARGS, PYXXH3_64_intdigest_doc},
    {"copy", (PyCFunction)PYXXH3_64_copy, METH_NOARGS, PYXXH3_64_copy_doc},
    {"reset", (PyCFunction)PYXXH3_64_reset, METH_NOARGS, PYXXH3_64_reset_doc},
    {NULL, NULL, 0, NULL}
};

static PyObject *PYXXH3_64_get_block_size(PYXXH3_64Object *self, void *closure)
{
    return PyLong_FromLong(XXH64_BLOCKSIZE);
}

static PyObject *
PYXXH3_64_get_digest_size(PYXXH3_64Object *self, void *closure)
{
    return PyLong_FromLong(XXH64_DIGESTSIZE);
}

static PyObject *
PYXXH3_64_get_name(PYXXH3_64Object *self, void *closure)
{
    return PyUnicode_FromString("XXH3_64");
}

static PyObject *
PYXXH3_64_get_seed(PYXXH3_64Object *self, void *closure)
{
    return PyLong_FromUnsignedLongLong(self->seed);
}

static PyGetSetDef PYXXH3_64_getseters[] = {
    {
        "digest_size",
        (getter)PYXXH3_64_get_digest_size, NULL,
        "Digest size.",
        NULL
    },
    {
        "block_size",
        (getter)PYXXH3_64_get_block_size, NULL,
        "Block size.",
        NULL
    },
    {
        "name",
        (getter)PYXXH3_64_get_name, NULL,
        "Name. Always XXH3_64.",
        NULL
    },
    {
        "digestsize",
        (getter)PYXXH3_64_get_digest_size, NULL,
        "Digest size.",
        NULL
    },
    {
        "seed",
        (getter)PYXXH3_64_get_seed, NULL,
        "Seed.",
        NULL
    },
    {NULL}  /* Sentinel */
};

PyDoc_STRVAR(
    PYXXH3_64Type_doc,
    "An xxh3_64 represents the object used to calculate the XXH3_64 hash of a\n"
    "bytes-like object.\n"
    "\n"
    "Methods:\n"
    "\n"
    "update(data) -- updates the current hash state with additional data\n"
    "digest() -- return the current digest value\n"
    "hexdigest() -- return the current digest as a string of hexadecimal digits\n"
    "intdigest() -- return the current digest as an integer\n"
    "copy() -- return a copy of the current xxh3_64 object");

static PyType_Slot XXH3_64Type_slots[] = {
    {Py_tp_dealloc, PYXXH3_64_dealloc},
    {Py_tp_doc, (void *)PYXXH3_64Type_doc},
    {Py_tp_methods, PYXXH3_64_methods},
    {Py_tp_getset, PYXXH3_64_getseters},
    {Py_tp_init, PYXXH3_64_init},
    {Py_tp_new, PYXXH3_64_new},
    {0, NULL},
};

static PyType_Spec XXH3_64Type_spec = {
    .name = "xxhash.xxh3_64",
    .basicsize = sizeof(PYXXH3_64Object),
    .flags = Py_TPFLAGS_DEFAULT
#if PY_VERSION_HEX >= 0x030c0000
           | Py_TPFLAGS_IMMUTABLETYPE
#endif
    ,
    .slots = XXH3_64Type_slots,
};

/* XXH3_128 */

typedef struct {
    PyObject_HEAD
    XXH3_state_t *xxhash_state;
    XXH64_hash_t seed;
    XXHASH_LOCK_FIELD
} PYXXH3_128Object;

static void PYXXH3_128_dealloc(PYXXH3_128Object *self)
{
    if (self->xxhash_state)
        XXH3_freeState(self->xxhash_state);
    XXHASH_LOCK_FINI(self);
    PyTypeObject *tp = Py_TYPE(self);
    tp->tp_free((PyObject *)self);
    Py_DECREF(tp);
}

XXHASH_DO_UPDATE(XXH3_128, XXH3_128bits_update)

static PyObject *
PYXXH3_128_vectorcall(PyObject *type, PyObject *const *args,
                      size_t nargsf, PyObject *kwnames)
{
    Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
    XXH64_hash_t seed = 0;
    Py_buffer buf;
    unsigned long long raw_seed;

    if (_parse_fastcall_args(args, nargs, kwnames, "xxhash.xxh3_128", 0,
                             &buf, &raw_seed) < 0)
        return NULL;
    seed = (XXH64_hash_t)raw_seed;

    PYXXH3_128Object *self = (PYXXH3_128Object *)
        ((PyTypeObject *)type)->tp_alloc((PyTypeObject *)type, 0);
    if (self == NULL) {
        PyBuffer_Release(&buf);
        return NULL;
    }

    XXHASH_LOCK_INIT(self);

    self->xxhash_state = XXH3_createState();
    if (self->xxhash_state == NULL) {
        Py_DECREF(self);
        PyBuffer_Release(&buf);
        return PyErr_NoMemory();
    }
    self->seed = seed;
    XXH3_128bits_reset_withSeed(self->xxhash_state, seed);

    if (buf.obj) {
        /* Constructor: no concurrent access possible, skip locking. */
        if (buf.len > XXHASH_GIL_MINSIZE) {
            Py_BEGIN_ALLOW_THREADS
            XXH3_128bits_update(self->xxhash_state, buf.buf, buf.len);
            Py_END_ALLOW_THREADS
        } else {
            XXH3_128bits_update(self->xxhash_state, buf.buf, buf.len);
        }
        PyBuffer_Release(&buf);
    }
    return (PyObject *)self;
}
static PyObject *PYXXH3_128_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
{
    PYXXH3_128Object *self;

    if ((self = (PYXXH3_128Object *)type->tp_alloc(type, 0)) == NULL) {
        return NULL;
    }

    XXHASH_LOCK_INIT(self);

    if ((self->xxhash_state = XXH3_createState()) == NULL) {
        Py_DECREF(self);
        PyErr_NoMemory();
        return NULL;
    }

    self->seed = 0;
    XXH3_128bits_reset_withSeed(self->xxhash_state, 0);

    return (PyObject *)self;
}

XXHASH_INIT(XXH3_128, "xxhash.xxh3_128", XXH3_128bits_reset_withSeed, XXH3_128bits_update, XXH64_hash_t)

PyDoc_STRVAR(
    PYXXH3_128_update_doc,
    "update (data)\n\n"
    "Update the xxh3_128 object with bytes-like data. Repeated calls are\n"
    "equivalent to a single call with the concatenation of all the arguments.");

static PyObject *PYXXH3_128_update(PYXXH3_128Object *self, PyObject *const *args,
                                 Py_ssize_t nargs, PyObject *kwnames)
{
    PyObject *arg = NULL;

    /* validate keywords first */
    if (kwnames) {
        Py_ssize_t nkw = PyTuple_GET_SIZE(kwnames);
        for (Py_ssize_t i = 0; i < nkw; i++) {
            PyObject *key = PyTuple_GET_ITEM(kwnames, i);
            if (PyUnicode_CompareWithASCIIString(key, "data") == 0) {
                if (nargs >= 1) {
                    PyErr_SetString(PyExc_TypeError,
                        "xxh3_128.update() got multiple values for argument 'data'");
                    return NULL;
                }
                arg = args[nargs + i];
            } else {
                PyErr_Format(PyExc_TypeError,
                    "'%U' is an invalid keyword argument for 'xxh3_128.update()'",
                    key);
                return NULL;
            }
        }
    }

    if (nargs >= 1) {
        if (nargs > 1) {
            PyErr_Format(PyExc_TypeError,
                "xxh3_128.update() takes at most 1 positional argument (%zd given)", nargs);
            return NULL;
        }
        arg = args[0];
    }

    if (!arg) {
        PyErr_SetString(PyExc_TypeError, "xxh3_128.update() missing required argument 'data'");
        return NULL;
    }

    Py_buffer buf;
    if (_get_buffer_or_str(arg, &buf) < 0)
        return NULL;
    PYXXH3_128_do_update(self, &buf);
    Py_RETURN_NONE;
}

PyDoc_STRVAR(
    PYXXH3_128_digest_doc,
    "digest() -> bytes\n\n"
    "Return the digest of the data passed to the update() method so\n"
    "far. This is a 16-byte value which may contain non-ASCII characters,\n"
    "including null bytes.");

static PyObject *PYXXH3_128_digest(PYXXH3_128Object *self)
{
    XXH128_hash_t intdigest;

    XXHASH_LOCK_ACQUIRE(self);
    intdigest = XXH3_128bits_digest(self->xxhash_state);
    XXHASH_LOCK_RELEASE(self);

    PyObject *ret = PyBytes_FromStringAndSize(NULL, XXH128_DIGESTSIZE);
    if (ret == NULL) return NULL;
    XXH128_canonicalFromHash((XXH128_canonical_t *)PyBytes_AS_STRING(ret), intdigest);
    return ret;
}

PyDoc_STRVAR(
    PYXXH3_128_hexdigest_doc,
    "hexdigest() -> string\n\n"
    "Like digest(), but returns the digest as a string of hexadecimal digits.");

static PyObject *PYXXH3_128_hexdigest(PYXXH3_128Object *self)
{
    XXH128_hash_t intdigest;
    char digest[XXH128_DIGESTSIZE];

    XXHASH_LOCK_ACQUIRE(self);
    intdigest = XXH3_128bits_digest(self->xxhash_state);
    XXHASH_LOCK_RELEASE(self);
    XXH128_canonicalFromHash((XXH128_canonical_t *)digest, intdigest);

    PyObject *ret = PyUnicode_New(XXH128_DIGESTSIZE * 2, 127);
    if (ret == NULL) return NULL;
    Py_UCS1 *b = PyUnicode_1BYTE_DATA(ret);
    for (Py_ssize_t i = 0, j = 0; i < XXH128_DIGESTSIZE; i++) {
        unsigned char c;
        c = (digest[i] >> 4) & 0xf;
        c = (c > 9) ? c + 'a' - 10 : c + '0';
        b[j++] = c;
        c = (digest[i] & 0xf);
        c = (c > 9) ? c + 'a' - 10 : c + '0';
        b[j++] = c;
    }
    return ret;
}

PyDoc_STRVAR(
    PYXXH3_128_intdigest_doc,
    "intdigest() -> int\n\n"
    "Like digest(), but returns the digest as an integer, which is the integer\n"
    "returned by the xxHash C API.");

static PyObject *PYXXH3_128_intdigest(PYXXH3_128Object *self)
{
    XXH128_hash_t intdigest;
    PyObject *result, *high, *low, *sixtyfour;

    XXHASH_LOCK_ACQUIRE(self);
    intdigest = XXH3_128bits_digest(self->xxhash_state);
    XXHASH_LOCK_RELEASE(self);

    sixtyfour = PyLong_FromLong(64);
    low = PyLong_FromUnsignedLongLong(intdigest.low64);
    high = PyLong_FromUnsignedLongLong(intdigest.high64);
    result = NULL;

    if (sixtyfour && low && high) {
        PyObject *shifted = PyNumber_Lshift(high, sixtyfour);
        if (shifted) {
            result = PyNumber_Add(shifted, low);
            Py_DECREF(shifted);
        }
    }
    Py_XDECREF(high);
    Py_XDECREF(low);
    Py_XDECREF(sixtyfour);
    return result;
}

PyDoc_STRVAR(
    PYXXH3_128_copy_doc,
    "copy() -> xxh3_128 object\n\n"
    "Return a copy of the xxh3_128 object.");

static PyObject *PYXXH3_128_copy(PYXXH3_128Object *self)
{
    PYXXH3_128Object *p;

    if ((p = (PYXXH3_128Object *)Py_TYPE(self)->tp_alloc(Py_TYPE(self), 0)) == NULL) {
        return NULL;
    }

    XXHASH_LOCK_INIT(p);

    if ((p->xxhash_state = XXH3_createState()) == NULL) {
        Py_DECREF(p);
        PyErr_NoMemory();
        return NULL;
    }

    XXHASH_LOCK_ACQUIRE(self);
    p->seed = self->seed;
    XXH3_copyState(p->xxhash_state, self->xxhash_state);
#if XXH_VERSION_NUMBER < 704
    // v0.7.3 and earlier have a bug where states reset with a seed
    // will have a wild pointer to the original state when copied,
    // causing a use-after-free if the original is freed.
    if (p->xxhash_state->secret == &self->xxhash_state->customSecret[0])
        p->xxhash_state->secret = &p->xxhash_state->customSecret[0];
#endif
    XXHASH_LOCK_RELEASE(self);

    return (PyObject *)p;
}

PyDoc_STRVAR(
    PYXXH3_128_reset_doc,
    "reset()\n\n"
    "Reset state.");

static PyObject *PYXXH3_128_reset(PYXXH3_128Object *self)
{
    XXHASH_LOCK_ACQUIRE(self);
    XXH3_128bits_reset_withSeed(self->xxhash_state, self->seed);
    XXHASH_LOCK_RELEASE(self);
    Py_RETURN_NONE;
}

static PyMethodDef PYXXH3_128_methods[] = {
    {"update", (PyCFunction)PYXXH3_128_update, METH_FASTCALL | METH_KEYWORDS, PYXXH3_128_update_doc},
    {"digest", (PyCFunction)PYXXH3_128_digest, METH_NOARGS, PYXXH3_128_digest_doc},
    {"hexdigest", (PyCFunction)PYXXH3_128_hexdigest, METH_NOARGS, PYXXH3_128_hexdigest_doc},
    {"intdigest", (PyCFunction)PYXXH3_128_intdigest, METH_NOARGS, PYXXH3_128_intdigest_doc},
    {"copy", (PyCFunction)PYXXH3_128_copy, METH_NOARGS, PYXXH3_128_copy_doc},
    {"reset", (PyCFunction)PYXXH3_128_reset, METH_NOARGS, PYXXH3_128_reset_doc},
    {NULL, NULL, 0, NULL}
};

static PyObject *PYXXH3_128_get_block_size(PYXXH3_128Object *self, void *closure)
{
    return PyLong_FromLong(XXH128_BLOCKSIZE);
}

static PyObject *
PYXXH3_128_get_digest_size(PYXXH3_128Object *self, void *closure)
{
    return PyLong_FromLong(XXH128_DIGESTSIZE);
}

static PyObject *
PYXXH3_128_get_name(PYXXH3_128Object *self, void *closure)
{
    return PyUnicode_FromString("XXH3_128");
}

static PyObject *
PYXXH3_128_get_seed(PYXXH3_128Object *self, void *closure)
{
    return PyLong_FromUnsignedLongLong(self->seed);
}

static PyGetSetDef PYXXH3_128_getseters[] = {
    {
        "digest_size",
        (getter)PYXXH3_128_get_digest_size, NULL,
        "Digest size.",
        NULL
    },
    {
        "block_size",
        (getter)PYXXH3_128_get_block_size, NULL,
        "Block size.",
        NULL
    },
    {
        "name",
        (getter)PYXXH3_128_get_name, NULL,
        "Name. Always XXH3_128.",
        NULL
    },
    {
        "digestsize",
        (getter)PYXXH3_128_get_digest_size, NULL,
        "Digest size.",
        NULL
    },
    {
        "seed",
        (getter)PYXXH3_128_get_seed, NULL,
        "Seed.",
        NULL
    },
    {NULL}  /* Sentinel */
};

PyDoc_STRVAR(
    PYXXH3_128Type_doc,
    "An xxh3_128 represents the object used to calculate the XXH3_128 hash of a\n"
    "bytes-like object.\n"
    "\n"
    "Methods:\n"
    "\n"
    "update(data) -- updates the current hash state with additional data\n"
    "digest() -- return the current digest value\n"
    "hexdigest() -- return the current digest as a string of hexadecimal digits\n"
    "intdigest() -- return the current digest as an integer\n"
    "copy() -- return a copy of the current xxh3_128 object");

static PyType_Slot XXH3_128Type_slots[] = {
    {Py_tp_dealloc, PYXXH3_128_dealloc},
    {Py_tp_doc, (void *)PYXXH3_128Type_doc},
    {Py_tp_methods, PYXXH3_128_methods},
    {Py_tp_getset, PYXXH3_128_getseters},
    {Py_tp_init, PYXXH3_128_init},
    {Py_tp_new, PYXXH3_128_new},
    {0, NULL},
};

static PyType_Spec XXH3_128Type_spec = {
    .name = "xxhash.xxh3_128",
    .basicsize = sizeof(PYXXH3_128Object),
    .flags = Py_TPFLAGS_DEFAULT
#if PY_VERSION_HEX >= 0x030c0000
           | Py_TPFLAGS_IMMUTABLETYPE
#endif
    ,
    .slots = XXH3_128Type_slots,
};

/*****************************************************************************
 * Module Init ****************************************************************
 ****************************************************************************/

static int _exec(PyObject *module)
{
    /* Build heap types from specs (bound to module for sub-interpreter safety). */
    PyObject *xxh32_type = PyType_FromModuleAndSpec(module, &XXH32Type_spec, NULL);
    if (!xxh32_type) return -1;
    ((PyTypeObject *)xxh32_type)->tp_vectorcall = PYXXH32_vectorcall;
    if (PyModule_AddType(module, (PyTypeObject *)xxh32_type) < 0) {
        Py_DECREF(xxh32_type); return -1;
    }
    Py_DECREF(xxh32_type);

    PyObject *xxh64_type = PyType_FromModuleAndSpec(module, &XXH64Type_spec, NULL);
    if (!xxh64_type) return -1;
    ((PyTypeObject *)xxh64_type)->tp_vectorcall = PYXXH64_vectorcall;
    if (PyModule_AddType(module, (PyTypeObject *)xxh64_type) < 0) {
        Py_DECREF(xxh64_type); return -1;
    }
    Py_DECREF(xxh64_type);

    PyObject *xxh3_64_type = PyType_FromModuleAndSpec(module, &XXH3_64Type_spec, NULL);
    if (!xxh3_64_type) return -1;
    ((PyTypeObject *)xxh3_64_type)->tp_vectorcall = PYXXH3_64_vectorcall;
    if (PyModule_AddType(module, (PyTypeObject *)xxh3_64_type) < 0) {
        Py_DECREF(xxh3_64_type); return -1;
    }
    Py_DECREF(xxh3_64_type);

    PyObject *xxh3_128_type = PyType_FromModuleAndSpec(module, &XXH3_128Type_spec, NULL);
    if (!xxh3_128_type) return -1;
    ((PyTypeObject *)xxh3_128_type)->tp_vectorcall = PYXXH3_128_vectorcall;
    if (PyModule_AddType(module, (PyTypeObject *)xxh3_128_type) < 0) {
        Py_DECREF(xxh3_128_type); return -1;
    }
    Py_DECREF(xxh3_128_type);

    if (PyModule_AddStringConstant(module, "XXHASH_VERSION", VALUE_TO_STRING(XXHASH_VERSION)) < 0)
        return -1;

    if (PyModule_AddIntConstant(module, "_GIL_MINSIZE", XXHASH_GIL_MINSIZE) < 0)
        return -1;

    return 0;
}

static PyModuleDef_Slot slots[] = {
    {Py_mod_exec, _exec},
#if PY_VERSION_HEX >= 0x030c0000  /* Python 3.12+: sub-interpreter support */
    {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
#endif
#if PY_VERSION_HEX >= 0x030d0000
    /* Python 3.13+: module is thread-safe with per-object lock */
    {Py_mod_gil, Py_MOD_GIL_NOT_USED},
#endif
    {0, NULL}
};

static PyMethodDef methods[] = {
    {"xxh32_digest",       (PyCFunction)xxh32_digest,       METH_FASTCALL | METH_KEYWORDS, "xxh32_digest"},
    {"xxh32_intdigest",    (PyCFunction)xxh32_intdigest,    METH_FASTCALL | METH_KEYWORDS, "xxh32_intdigest"},
    {"xxh32_hexdigest",    (PyCFunction)xxh32_hexdigest,    METH_FASTCALL | METH_KEYWORDS, "xxh32_hexdigest"},
    {"xxh64_digest",       (PyCFunction)xxh64_digest,       METH_FASTCALL | METH_KEYWORDS, "xxh64_digest"},
    {"xxh64_intdigest",    (PyCFunction)xxh64_intdigest,    METH_FASTCALL | METH_KEYWORDS, "xxh64_intdigest"},
    {"xxh64_hexdigest",    (PyCFunction)xxh64_hexdigest,    METH_FASTCALL | METH_KEYWORDS, "xxh64_hexdigest"},
    {"xxh3_64_digest",     (PyCFunction)xxh3_64_digest,     METH_FASTCALL | METH_KEYWORDS, "xxh3_64_digest"},
    {"xxh3_64_intdigest",  (PyCFunction)xxh3_64_intdigest,  METH_FASTCALL | METH_KEYWORDS, "xxh3_64_intdigest"},
    {"xxh3_64_hexdigest",  (PyCFunction)xxh3_64_hexdigest,  METH_FASTCALL | METH_KEYWORDS, "xxh3_64_hexdigest"},
    {"xxh3_128_digest",    (PyCFunction)xxh3_128_digest,    METH_FASTCALL | METH_KEYWORDS, "xxh3_128_digest"},
    {"xxh3_128_intdigest", (PyCFunction)xxh3_128_intdigest, METH_FASTCALL | METH_KEYWORDS, "xxh3_128_intdigest"},
    {"xxh3_128_hexdigest", (PyCFunction)xxh3_128_hexdigest, METH_FASTCALL | METH_KEYWORDS, "xxh3_128_hexdigest"},
    {NULL, NULL, 0, NULL}
};

static struct PyModuleDef moduledef = {
    PyModuleDef_HEAD_INIT,
    "_xxhash",
    NULL,
    0,
    methods,
    slots,
    NULL,
    NULL,
    NULL
};

PyMODINIT_FUNC
PyInit__xxhash(void)
{
    return PyModuleDef_Init(&moduledef);
}
