"""
Hash implementations for Numba types
"""

import math
import numpy as np
import sys
import ctypes
import warnings
from collections import namedtuple

import llvmlite.binding as ll
from llvmlite import ir

from numba import literal_unroll
from numba.core.extending import (
    overload, overload_method, intrinsic, register_jitable)
from numba.core import errors
from numba.core import types, utils
from numba.core.unsafe.bytes import grab_byte, grab_uint64_t
from numba.cpython.randomimpl import (const_int, get_next_int, get_next_int32,
                                      get_state_ptr)

_py310_or_later = utils.PYVERSION >= (3, 10)

# This is Py_hash_t, which is a Py_ssize_t, which has sizeof(size_t):
# https://github.com/python/cpython/blob/d1dd6be613381b996b9071443ef081de8e5f3aff/Include/pyport.h#L91-L96    # noqa: E501
_hash_width = sys.hash_info.width
_Py_hash_t = getattr(types, 'int%s' % _hash_width)
_Py_uhash_t = getattr(types, 'uint%s' % _hash_width)

# Constants from CPython source, obtained by various means:
# https://github.com/python/cpython/blob/d1dd6be613381b996b9071443ef081de8e5f3aff/Include/pyhash.h    # noqa: E501
_PyHASH_INF = sys.hash_info.inf
_PyHASH_NAN = sys.hash_info.nan
_PyHASH_MODULUS = _Py_uhash_t(sys.hash_info.modulus)
_PyHASH_BITS = 31 if types.intp.bitwidth == 32 else 61  # mersenne primes
_PyHASH_MULTIPLIER = 0xf4243  # 1000003UL
_PyHASH_IMAG = _PyHASH_MULTIPLIER
_PyLong_SHIFT = sys.int_info.bits_per_digit
_Py_HASH_CUTOFF = sys.hash_info.cutoff
_Py_hashfunc_name = sys.hash_info.algorithm


# This stub/overload pair are used to force branch pruning to remove the dead
# branch based on the potential `None` type of the hash_func which works better
# if the predicate for the prune in an ir.Arg. The obj is an arg to allow for
# a custom error message.
def _defer_hash(hash_func):
    pass


@overload(_defer_hash)
def ol_defer_hash(obj, hash_func):
    err_msg = f"unhashable type: '{obj}'"

    def impl(obj, hash_func):
        if hash_func is None:
            raise TypeError(err_msg)
        else:
            return hash_func()
    return impl


# hash(obj) is implemented by calling obj.__hash__()
@overload(hash)
def hash_overload(obj):
    attempt_generic_msg = ("No __hash__ is defined for object of type "
                           f"'{obj}' and a generic hash() cannot be "
                           "performed as there is no suitable object "
                           "represention in Numba compiled code!")

    def impl(obj):
        if hasattr(obj, '__hash__'):
            return _defer_hash(obj, getattr(obj, '__hash__'))
        else:
            raise TypeError(attempt_generic_msg)
    return impl


@register_jitable
def process_return(val):
    asint = _Py_hash_t(val)
    if (asint == int(-1)):
        asint = int(-2)
    return asint


# This is a translation of CPython's _Py_HashDouble:
# https://github.com/python/cpython/blob/d1dd6be613381b996b9071443ef081de8e5f3aff/Python/pyhash.c#L34-L129   # noqa: E501
# NOTE: In Python 3.10 hash of nan is now hash of the pointer to the PyObject
# containing said nan. Numba cannot replicate this as there is no object, so it
# elects to replicate the behaviour i.e. hash of nan is something "unique" which
# satisfies https://bugs.python.org/issue43475.

@register_jitable(locals={'x': _Py_uhash_t,
                          'y': _Py_uhash_t,
                          'm': types.double,
                          'e': types.intc,
                          'sign': types.intc,
                          '_PyHASH_MODULUS': _Py_uhash_t,
                          '_PyHASH_BITS': types.intc})
def _Py_HashDouble(v):
    if not np.isfinite(v):
        if (np.isinf(v)):
            if (v > 0):
                return _PyHASH_INF
            else:
                return -_PyHASH_INF
        else:
            # Python 3.10 does not use `_PyHASH_NAN`.
            # https://github.com/python/cpython/blob/2c4792264f9218692a1bd87398a60591f756b171/Python/pyhash.c#L102   # noqa: E501
            # Numba returns a pseudo-random number to reflect the spirit of the
            # change.
            if _py310_or_later:
                x = _prng_random_hash()
                return process_return(x)
            else:
                return _PyHASH_NAN

    m, e = math.frexp(v)

    sign = 1
    if (m < 0):
        sign = -1
        m = -m

    # process 28 bits at a time;  this should work well both for binary
    #  and hexadecimal floating point.
    x = 0
    while (m):
        x = ((x << 28) & _PyHASH_MODULUS) | x >> (_PyHASH_BITS - 28)
        m *= 268435456.0  # /* 2**28 */
        e -= 28
        y = int(m)  # /* pull out integer part */
        m -= y
        x += y
        if x >= _PyHASH_MODULUS:
            x -= _PyHASH_MODULUS
    # /* adjust for the exponent;  first reduce it modulo _PyHASH_BITS */
    if e >= 0:
        e = e % _PyHASH_BITS
    else:
        e = _PyHASH_BITS - 1 - ((-1 - e) % _PyHASH_BITS)

    x = ((x << e) & _PyHASH_MODULUS) | x >> (_PyHASH_BITS - e)

    x = x * sign
    return process_return(x)


@intrinsic
def _fpext(tyctx, val):
    def impl(cgctx, builder, signature, args):
        val = args[0]
        return builder.fpext(val, ir.DoubleType())
    sig = types.float64(types.float32)
    return sig, impl


@intrinsic
def _prng_random_hash(tyctx):

    def impl(cgctx, builder, signature, args):
        state_ptr = get_state_ptr(cgctx, builder, "internal")
        bits = const_int(_hash_width)

        # Why not just use get_next_int() with the correct bitwidth?
        # get_next_int() always returns an i64, because the bitwidth it is
        # passed may not be a compile-time constant, so it needs to allocate
        # the largest unit of storage that may be required. Therefore, if the
        # hash width is 32, then we need to use get_next_int32() to ensure we
        # don't return a wider-than-expected hash, even if everything above
        # the low 32 bits would have been zero.
        if _hash_width == 32:
            value = get_next_int32(cgctx, builder, state_ptr)
        else:
            value = get_next_int(cgctx, builder, state_ptr, bits, False)

        return value

    sig = _Py_hash_t()
    return sig, impl


# This is a translation of CPython's long_hash, but restricted to the numerical
# domain reachable by int64/uint64 (i.e. no BigInt like support):
# https://github.com/python/cpython/blob/d1dd6be613381b996b9071443ef081de8e5f3aff/Objects/longobject.c#L2934-L2989    # noqa: E501
# obdigit is a uint32_t which is typedef'd to digit
# int32_t is typedef'd to sdigit


@register_jitable(locals={'x': _Py_uhash_t,
                          'p1': _Py_uhash_t,
                          'p2': _Py_uhash_t,
                          'p3': _Py_uhash_t,
                          'p4': _Py_uhash_t,
                          '_PyHASH_MODULUS': _Py_uhash_t,
                          '_PyHASH_BITS': types.int32,
                          '_PyLong_SHIFT': types.int32,})
def _long_impl(val):
    # This function assumes val came from a long int repr with val being a
    # uint64_t this means having to split the input into PyLong_SHIFT size
    # chunks in an unsigned hash wide type, max numba can handle is a 64bit int

    # mask to select low _PyLong_SHIFT bits
    _tmp_shift = 32 - _PyLong_SHIFT
    mask_shift = (~types.uint32(0x0)) >> _tmp_shift

    # a 64bit wide max means Numba only needs 3 x 30 bit values max,
    # or 5 x 15 bit values max on 32bit platforms
    i = (64 // _PyLong_SHIFT) + 1

    # alg as per hash_long
    x = 0
    p3 = (_PyHASH_BITS - _PyLong_SHIFT)
    for idx in range(i - 1, -1, -1):
        p1 = x << _PyLong_SHIFT
        p2 = p1 & _PyHASH_MODULUS
        p4 = x >> p3
        x = p2 | p4
        # the shift and mask splits out the `ob_digit` parts of a Long repr
        x += types.uint32((val >> idx * _PyLong_SHIFT) & mask_shift)
        if x >= _PyHASH_MODULUS:
            x -= _PyHASH_MODULUS
    return _Py_hash_t(x)


# This has no CPython equivalent, CPython uses long_hash.
@overload_method(types.Integer, '__hash__')
@overload_method(types.Boolean, '__hash__')
def int_hash(val):

    _HASH_I64_MIN = -2 if sys.maxsize <= 2 ** 32 else -4
    _SIGNED_MIN = types.int64(-0x8000000000000000)

    # Find a suitable type to hold a "big" value, i.e. iinfo(ty).min/max
    # this is to ensure e.g. int32.min is handled ok as it's abs() is its value
    _BIG = types.int64 if getattr(val, 'signed', False) else types.uint64

    # this is a bit involved due to the CPython repr of ints
    def impl(val):
        # If the magnitude is under PyHASH_MODULUS, just return the
        # value val as the hash, couple of special cases if val == val:
        # 1. it's 0, in which case return 0
        # 2. it's signed int minimum value, return the value CPython computes
        # but Numba cannot as there's no type wide enough to hold the shifts.
        #
        # If the magnitude is greater than PyHASH_MODULUS then... if the value
        # is negative then negate it switch the sign on the hash once computed
        # and use the standard wide unsigned hash implementation
        val = _BIG(val)
        mag = abs(val)
        if mag < _PyHASH_MODULUS:
            if val == 0:
                ret = 0
            elif val == _SIGNED_MIN:  # e.g. int64 min, -0x8000000000000000
                ret = _Py_hash_t(_HASH_I64_MIN)
            else:
                ret = _Py_hash_t(val)
        else:
            needs_negate = False
            if val < 0:
                val = -val
                needs_negate = True
            ret = _long_impl(val)
            if needs_negate:
                ret = -ret
        return process_return(ret)
    return impl

# This is a translation of CPython's float_hash:
# https://github.com/python/cpython/blob/d1dd6be613381b996b9071443ef081de8e5f3aff/Objects/floatobject.c#L528-L532    # noqa: E501


@overload_method(types.Float, '__hash__')
def float_hash(val):
    if val.bitwidth == 64:
        def impl(val):
            hashed = _Py_HashDouble(val)
            return hashed
    else:
        def impl(val):
            # widen the 32bit float to 64bit
            fpextended = np.float64(_fpext(val))
            hashed = _Py_HashDouble(fpextended)
            return hashed
    return impl

# This is a translation of CPython's complex_hash:
# https://github.com/python/cpython/blob/d1dd6be613381b996b9071443ef081de8e5f3aff/Objects/complexobject.c#L408-L428    # noqa: E501


@overload_method(types.Complex, '__hash__')
def complex_hash(val):
    def impl(val):
        hashreal = hash(val.real)
        hashimag = hash(val.imag)
        # Note:  if the imaginary part is 0, hashimag is 0 now,
        # so the following returns hashreal unchanged.  This is
        # important because numbers of different types that
        # compare equal must have the same hash value, so that
        # hash(x + 0*j) must equal hash(x).
        combined = hashreal + _PyHASH_IMAG * hashimag
        return process_return(combined)
    return impl


# Python 3.8 strengthened its hash alg for tuples.
# This is a translation of CPython's tuplehash for Python >=3.8
# https://github.com/python/cpython/blob/b738237d6792acba85b1f6e6c8993a812c7fd815/Objects/tupleobject.c#L338-L391    # noqa: E501

# These consts are needed for this alg variant, they are from:
# https://github.com/python/cpython/blob/b738237d6792acba85b1f6e6c8993a812c7fd815/Objects/tupleobject.c#L353-L363    # noqa: E501
if _Py_uhash_t.bitwidth // 8 > 4:
    _PyHASH_XXPRIME_1 = _Py_uhash_t(11400714785074694791)
    _PyHASH_XXPRIME_2 = _Py_uhash_t(14029467366897019727)
    _PyHASH_XXPRIME_5 = _Py_uhash_t(2870177450012600261)

    @register_jitable(locals={'x': types.uint64})
    def _PyHASH_XXROTATE(x):
        # Rotate left 31 bits
        return ((x << types.uint64(31)) | (x >> types.uint64(33)))
else:
    _PyHASH_XXPRIME_1 = _Py_uhash_t(2654435761)
    _PyHASH_XXPRIME_2 = _Py_uhash_t(2246822519)
    _PyHASH_XXPRIME_5 = _Py_uhash_t(374761393)

    @register_jitable(locals={'x': types.uint64})
    def _PyHASH_XXROTATE(x):
        # Rotate left 13 bits
        return ((x << types.uint64(13)) | (x >> types.uint64(19)))


@register_jitable(locals={'acc': _Py_uhash_t, 'lane': _Py_uhash_t,
                          '_PyHASH_XXPRIME_5': _Py_uhash_t,
                          '_PyHASH_XXPRIME_1': _Py_uhash_t,
                          'tl': _Py_uhash_t})
def _tuple_hash(tup):
    tl = len(tup)
    acc = _PyHASH_XXPRIME_5
    for x in literal_unroll(tup):
        lane = hash(x)
        if lane == _Py_uhash_t(-1):
            return -1
        acc += lane * _PyHASH_XXPRIME_2
        acc = _PyHASH_XXROTATE(acc)
        acc *= _PyHASH_XXPRIME_1

    acc += tl ^ (_PyHASH_XXPRIME_5 ^ _Py_uhash_t(3527539))

    if acc == _Py_uhash_t(-1):
        return process_return(1546275796)

    return process_return(acc)


@overload_method(types.BaseTuple, '__hash__')
def tuple_hash(val):
    def impl(val):
        return _tuple_hash(val)
    return impl


# ------------------------------------------------------------------------------
# String/bytes hashing needs hashseed info, this is from:
# https://stackoverflow.com/a/41088757
# with thanks to Martijn Pieters
#
# Developer note:
# CPython makes use of an internal "hashsecret" which is essentially a struct
# containing some state that is set on CPython initialization and contains magic
# numbers used particularly in unicode/string hashing. This code binds to the
# Python runtime libraries in use by the current process and reads the
# "hashsecret" state so that it can be used by Numba. As this is done at runtime
# the behaviour and influence of the PYTHONHASHSEED environment variable is
# accommodated.

from ctypes import (  # noqa
    c_size_t,
    c_ubyte,
    c_uint64,
    pythonapi,
    Structure,
    Union,
)  # noqa


class FNV(Structure):
    _fields_ = [
        ('prefix', c_size_t),
        ('suffix', c_size_t)
    ]


class SIPHASH(Structure):
    _fields_ = [
        ('k0', c_uint64),
        ('k1', c_uint64),
    ]


class DJBX33A(Structure):
    _fields_ = [
        ('padding', c_ubyte * 16),
        ('suffix', c_size_t),
    ]


class EXPAT(Structure):
    _fields_ = [
        ('padding', c_ubyte * 16),
        ('hashsalt', c_size_t),
    ]


class _Py_HashSecret_t(Union):
    _fields_ = [
        # ensure 24 bytes
        ('uc', c_ubyte * 24),
        # two Py_hash_t for FNV
        ('fnv', FNV),
        # two uint64 for SipHash24
        ('siphash', SIPHASH),
        # a different (!) Py_hash_t for small string optimization
        ('djbx33a', DJBX33A),
        ('expat', EXPAT),
    ]


_hashsecret_entry = namedtuple('_hashsecret_entry', ['symbol', 'value'])


# Only a few members are needed at present
def _build_hashsecret():
    """Read hash secret from the Python process

    Returns
    -------
    info : dict
        - keys are "djbx33a_suffix", "siphash_k0", siphash_k1".
        - values are the namedtuple[symbol:str, value:int]
    """
    # Read hashsecret and inject it into the LLVM symbol map under the
    # prefix `_numba_hashsecret_`.
    pyhashsecret = _Py_HashSecret_t.in_dll(pythonapi, '_Py_HashSecret')
    info = {}

    def inject(name, val):
        symbol_name = "_numba_hashsecret_{}".format(name)
        val = ctypes.c_uint64(val)
        addr = ctypes.addressof(val)
        ll.add_symbol(symbol_name, addr)
        info[name] = _hashsecret_entry(symbol=symbol_name, value=val)

    inject('djbx33a_suffix', pyhashsecret.djbx33a.suffix)
    inject('siphash_k0', pyhashsecret.siphash.k0)
    inject('siphash_k1', pyhashsecret.siphash.k1)
    return info


_hashsecret = _build_hashsecret()


# ------------------------------------------------------------------------------


if _Py_hashfunc_name in ('siphash13', 'siphash24', 'fnv'):

    # Check for use of the FNV hashing alg, warn users that it's not implemented
    # and functionality relying of properties derived from hashing will be fine
    # but hash values themselves are likely to be different.
    if _Py_hashfunc_name == 'fnv':
        msg = ("FNV hashing is not implemented in Numba. See PEP 456 "
               "https://www.python.org/dev/peps/pep-0456/ "
               "for rationale over not using FNV. Numba will continue to work, "
               "but hashes for built in types will be computed using "
               "siphash24. This will permit e.g. dictionaries to continue to "
               "behave as expected, however anything relying on the value of "
               "the hash opposed to hash as a derived property is likely to "
               "not work as expected.")
        warnings.warn(msg)

    # This is a translation of CPython's siphash24 function:
    # https://github.com/python/cpython/blob/d1dd6be613381b996b9071443ef081de8e5f3aff/Python/pyhash.c#L287-L413    # noqa: E501
    # and also, since Py 3.11, a translation of CPython's siphash13 function:
    # https://github.com/python/cpython/blob/9dda9020abcf0d51d59b283a89c58c8e1fb0f574/Python/pyhash.c#L376-L424
    # the only differences are in the use of SINGLE_ROUND in siphash13 vs.
    # DOUBLE_ROUND in siphash24, and that siphash13 has an extra "ROUND" applied
    # just before the final XORing of components to create the return value.

    # /* *********************************************************************
    # <MIT License>
    # Copyright (c) 2013  Marek Majkowski <marek@popcount.org>

    # Permission is hereby granted, free of charge, to any person obtaining a
    # copy of this software and associated documentation files (the "Software"),
    # to deal in the Software without restriction, including without limitation
    # the rights to use, copy, modify, merge, publish, distribute, sublicense,
    # and/or sell copies of the Software, and to permit persons to whom the
    # Software is furnished to do so, subject to the following conditions:

    # The above copyright notice and this permission notice shall be included in
    # all copies or substantial portions of the Software.

    # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
    # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
    # DEALINGS IN THE SOFTWARE.
    # </MIT License>

    # Original location:
    # https://github.com/majek/csiphash/

    # Solution inspired by code from:
    # Samuel Neves (supercop/crypto_auth/siphash24/little)
    #djb (supercop/crypto_auth/siphash24/little2)
    # Jean-Philippe Aumasson (https://131002.net/siphash/siphash24.c)

    # Modified for Python by Christian Heimes:
    # - C89 / MSVC compatibility
    # - _rotl64() on Windows
    # - letoh64() fallback
    # */

    @register_jitable(locals={'x': types.uint64,
                              'b': types.uint64, })
    def _ROTATE(x, b):
        return types.uint64(((x) << (b)) | ((x) >> (types.uint64(64) - (b))))

    @register_jitable(locals={'a': types.uint64,
                              'b': types.uint64,
                              'c': types.uint64,
                              'd': types.uint64,
                              's': types.uint64,
                              't': types.uint64, })
    def _HALF_ROUND(a, b, c, d, s, t):
        a += b
        c += d
        b = _ROTATE(b, s) ^ a
        d = _ROTATE(d, t) ^ c
        a = _ROTATE(a, 32)
        return a, b, c, d

    @register_jitable(locals={'v0': types.uint64,
                              'v1': types.uint64,
                              'v2': types.uint64,
                              'v3': types.uint64, })
    def _SINGLE_ROUND(v0, v1, v2, v3):
        v0, v1, v2, v3 = _HALF_ROUND(v0, v1, v2, v3, 13, 16)
        v2, v1, v0, v3 = _HALF_ROUND(v2, v1, v0, v3, 17, 21)
        return v0, v1, v2, v3

    @register_jitable(locals={'v0': types.uint64,
                              'v1': types.uint64,
                              'v2': types.uint64,
                              'v3': types.uint64, })
    def _DOUBLE_ROUND(v0, v1, v2, v3):
        v0, v1, v2, v3 = _SINGLE_ROUND(v0, v1, v2, v3)
        v0, v1, v2, v3 = _SINGLE_ROUND(v0, v1, v2, v3)
        return v0, v1, v2, v3

    def _gen_siphash(alg):
        if alg == 'siphash13':
            _ROUNDER = _SINGLE_ROUND
            _EXTRA_ROUND = True
        elif alg == 'siphash24':
            _ROUNDER = _DOUBLE_ROUND
            _EXTRA_ROUND = False
        else:
            assert 0, 'unreachable'

        @register_jitable(locals={'v0': types.uint64,
                                  'v1': types.uint64,
                                  'v2': types.uint64,
                                  'v3': types.uint64,
                                  'b': types.uint64,
                                  'mi': types.uint64,
                                  't': types.uint64,
                                  'mask': types.uint64,
                                  'jmp': types.uint64,
                                  'ohexefef': types.uint64})
        def _siphash(k0, k1, src, src_sz):
            b = types.uint64(src_sz) << 56
            v0 = k0 ^ types.uint64(0x736f6d6570736575)
            v1 = k1 ^ types.uint64(0x646f72616e646f6d)
            v2 = k0 ^ types.uint64(0x6c7967656e657261)
            v3 = k1 ^ types.uint64(0x7465646279746573)

            idx = 0
            while (src_sz >= 8):
                mi = grab_uint64_t(src, idx)
                idx += 1
                src_sz -= 8
                v3 ^= mi
                v0, v1, v2, v3 = _ROUNDER(v0, v1, v2, v3)
                v0 ^= mi

            # this is the switch fallthrough:
            # https://github.com/python/cpython/blob/d1dd6be613381b996b9071443ef081de8e5f3aff/Python/pyhash.c#L390-L400    # noqa: E501
            t = types.uint64(0x0)
            boffset = idx * 8
            ohexefef = types.uint64(0xff)
            if src_sz >= 7:
                jmp = (6 * 8)
                mask = ~types.uint64(ohexefef << jmp)
                t = (t & mask) | (types.uint64(grab_byte(src, boffset + 6))
                                  << jmp)
            if src_sz >= 6:
                jmp = (5 * 8)
                mask = ~types.uint64(ohexefef << jmp)
                t = (t & mask) | (types.uint64(grab_byte(src, boffset + 5))
                                  << jmp)
            if src_sz >= 5:
                jmp = (4 * 8)
                mask = ~types.uint64(ohexefef << jmp)
                t = (t & mask) | (types.uint64(grab_byte(src, boffset + 4))
                                  << jmp)
            if src_sz >= 4:
                t &= types.uint64(0xffffffff00000000)
                for i in range(4):
                    jmp = i * 8
                    mask = ~types.uint64(ohexefef << jmp)
                    t = (t & mask) | (types.uint64(grab_byte(src, boffset + i))
                                      << jmp)
            if src_sz >= 3:
                jmp = (2 * 8)
                mask = ~types.uint64(ohexefef << jmp)
                t = (t & mask) | (types.uint64(grab_byte(src, boffset + 2))
                                  << jmp)
            if src_sz >= 2:
                jmp = (1 * 8)
                mask = ~types.uint64(ohexefef << jmp)
                t = (t & mask) | (types.uint64(grab_byte(src, boffset + 1))
                                  << jmp)
            if src_sz >= 1:
                mask = ~(ohexefef)
                t = (t & mask) | (types.uint64(grab_byte(src, boffset + 0)))

            b |= t
            v3 ^= b
            v0, v1, v2, v3 = _ROUNDER(v0, v1, v2, v3)
            v0 ^= b
            v2 ^= ohexefef
            v0, v1, v2, v3 = _ROUNDER(v0, v1, v2, v3)
            v0, v1, v2, v3 = _ROUNDER(v0, v1, v2, v3)
            if _EXTRA_ROUND:
                v0, v1, v2, v3 = _ROUNDER(v0, v1, v2, v3)
            t = (v0 ^ v1) ^ (v2 ^ v3)
            return t

        return _siphash

    _siphash13 = _gen_siphash('siphash13')
    _siphash24 = _gen_siphash('siphash24')

    _siphasher = _siphash13 if _Py_hashfunc_name == 'siphash13' else _siphash24

else:
    msg = "Unsupported hashing algorithm in use %s" % _Py_hashfunc_name
    raise ValueError(msg)


@intrinsic
def _inject_hashsecret_read(tyctx, name):
    """Emit code to load the hashsecret.
    """
    if not isinstance(name, types.StringLiteral):
        raise errors.TypingError("requires literal string")

    sym = _hashsecret[name.literal_value].symbol
    resty = types.uint64
    sig = resty(name)

    def impl(cgctx, builder, sig, args):
        mod = builder.module
        try:
            # Search for existing global
            gv = mod.get_global(sym)
        except KeyError:
            # Inject the symbol if not already exist.
            gv = ir.GlobalVariable(mod, ir.IntType(64), name=sym)
        v = builder.load(gv)
        return v

    return sig, impl


def _load_hashsecret(name):
    return _hashsecret[name].value


@overload(_load_hashsecret)
def _impl_load_hashsecret(name):
    def imp(name):
        return _inject_hashsecret_read(name)
    return imp


# This is a translation of CPythons's _Py_HashBytes:
# https://github.com/python/cpython/blob/d1dd6be613381b996b9071443ef081de8e5f3aff/Python/pyhash.c#L145-L191    # noqa: E501


@register_jitable(locals={'_hash': _Py_uhash_t})
def _Py_HashBytes(val, _len):
    if (_len == 0):
        return process_return(0)

    if (_len < _Py_HASH_CUTOFF):
        # TODO: this branch needs testing, needs a CPython setup for it!
        # /* Optimize hashing of very small strings with inline DJBX33A. */
        _hash = _Py_uhash_t(5381)  # /* DJBX33A starts with 5381 */
        for idx in range(_len):
            _hash = ((_hash << 5) + _hash) + np.uint8(grab_byte(val, idx))

        _hash ^= _len
        _hash ^= _load_hashsecret('djbx33a_suffix')
    else:
        tmp = _siphasher(types.uint64(_load_hashsecret('siphash_k0')),
                         types.uint64(_load_hashsecret('siphash_k1')),
                         val, _len)
        _hash = process_return(tmp)
    return process_return(_hash)

# This is an approximate translation of CPython's unicode_hash:
# https://github.com/python/cpython/blob/d1dd6be613381b996b9071443ef081de8e5f3aff/Objects/unicodeobject.c#L11635-L11663    # noqa: E501


@overload_method(types.UnicodeType, '__hash__')
def unicode_hash(val):
    from numba.cpython.unicode import _kind_to_byte_width

    def impl(val):
        kindwidth = _kind_to_byte_width(val._kind)
        _len = len(val)
        # use the cache if possible
        current_hash = val._hash
        if current_hash != -1:
            return current_hash
        else:
            # cannot write hash value to cache in the unicode struct due to
            # pass by value on the struct making the struct member immutable
            return _Py_HashBytes(val._data, kindwidth * _len)

    return impl
