# hashtable - Open-addressing hash table library
# Linear probing for collision resolution, supports auto-resizing and tombstone deletion
# Key type fixed as str (i8*), value type is void* (generic pointer)
import t, c
import string
import memhub
from stdint import *

# Empty slot marker (hash == 0)
_HT_EMPTY: t.CDefine = 0
# Tombstone slot marker (hash == 1)
_HT_TOMBSTONE: t.CDefine = 1
# Default initial capacity (must be power of two)
_HT_INIT_CAP: t.CDefine = 16
# Slot memory layout: hash(8) + key pointer(8) + value pointer(8) = 24 bytes
_HT_SLOT_SIZE: t.CDefine = 24
# FNV-1a 64-bit constants
_FNV_OFFSET: t.CUInt64T = 0xcbf29ce484222325
_FNV_PRIME: t.CUInt64T = 0x100000001b3


def _fnv1a_hash(key: str) -> t.CUInt64T:
    """FNV-1a 64-bit hash function."""
    h: t.CUInt64T = _FNV_OFFSET
    k: str = key
    while k[0] != 0:
        h = h ^ t.CUInt64T(k[0])
        h = h * _FNV_PRIME
        k += 1
    # Ensure h >= 2 (avoids empty slot marker 0 and tombstone marker 1)
    if h < 2:
        h = 2
    return h


class HashTable:
    """Open-addressing hash table using linear probing for collision resolution.

    Key type fixed as str (i8*), value type is void* (generic pointer).
    Memory is managed via the mbuddy allocator. Automatically resizes when load factor exceeds 0.75.

    Usage:
        mb: memhub.MemBuddy = memhub.MemBuddy(arena, arena_size)
        ht: HashTable = HashTable(mb)
        ht.set_int("x", 42)
        p: t.CPtr = ht["x"]
        v: int = c.Deref(p)
    """
    __slots__:      t.CVoid | t.CPtr              # Base address of slot array
    __count__:      t.CSizeT = 0                  # Number of occupied entries
    __capacity__:   t.CSizeT = 16                 # Total slot count (power of two)
    __tombstones__: t.CSizeT = 0                  # Tombstone entry count
    __mbuddy__:     memhub.MemBuddy | t.CPtr
    __iter_index__: t.CSizeT = 0

    def __new__(self, mb: memhub.MemBuddy | t.CPtr) -> t.CPtr:
        # Heap-allocate HashTable object (6 fields × 8 bytes = 48 bytes)
        return mb.alloc(48)

    def __init__(self, mb: memhub.MemBuddy | t.CPtr):
        self.__mbuddy__ = mb
        self.__capacity__ = _HT_INIT_CAP
        self.__count__ = 0
        self.__tombstones__ = 0
        self.__slots__ = mb.alloc(self.__capacity__ * _HT_SLOT_SIZE)
        # Initialize all slots to empty (hash = 0)
        string.memset(self.__slots__, 0, self.__capacity__ * _HT_SLOT_SIZE)

    def _slot_hash(self, idx: t.CSizeT) -> t.CUInt64T:
        """Read hash value from target slot."""
        ptr: t.CUInt64T | t.CPtr = (t.CVoid | t.CPtr)(t.CUInt64T(self.__slots__) + idx * _HT_SLOT_SIZE)
        return ptr[0]

    def _slot_key(self, idx: t.CSizeT) -> str:
        """Read key pointer from target slot."""
        ptr: str | t.CPtr = (t.CVoid | t.CPtr)(t.CUInt64T(self.__slots__) + idx * _HT_SLOT_SIZE + 8)
        return ptr[0]

    def _slot_value(self, idx: t.CSizeT) -> t.CPtr:
        """Read value pointer from target slot."""
        ptr: t.CUInt64T | t.CPtr = (t.CVoid | t.CPtr)(t.CUInt64T(self.__slots__) + idx * _HT_SLOT_SIZE + 16)
        return ptr[0]

    def _set_slot(self, idx: t.CSizeT, h: t.CUInt64T, key: str, val: t.CPtr):
        """Assign three fields of the target slot."""
        h_ptr: t.CUInt64T | t.CPtr = (t.CVoid | t.CPtr)(t.CUInt64T(self.__slots__) + idx * _HT_SLOT_SIZE)
        h_ptr[0] = h
        k_ptr: str | t.CPtr = (t.CVoid | t.CPtr)(t.CUInt64T(self.__slots__) + idx * _HT_SLOT_SIZE + 8)
        k_ptr[0] = key
        v_ptr: t.CUInt64T | t.CPtr = (t.CVoid | t.CPtr)(t.CUInt64T(self.__slots__) + idx * _HT_SLOT_SIZE + 16)
        v_ptr[0] = val

    def _find_slot(self, key: str, h: t.CUInt64T) -> t.CSizeT:
        """Locate slot index for key insertion or lookup.

        Returns: Index of matching key if found; otherwise returns index of first usable empty/tombstone slot.
        """
        mask: t.CSizeT = self.__capacity__ - 1
        idx: t.CSizeT = t.CSizeT(h) & mask
        first_tombstone: t.CSizeT = self.__capacity__
        i: t.CSizeT = 0
        while i < self.__capacity__:
            sh: t.CUInt64T = self._slot_hash(idx)
            if sh == _HT_EMPTY:
                if first_tombstone < self.__capacity__:
                    return first_tombstone
                return idx
            if sh == _HT_TOMBSTONE:
                if first_tombstone >= self.__capacity__:
                    first_tombstone = idx
            else:
                if sh == h:
                    sk: str = self._slot_key(idx)
                    if sk == key:
                        return idx
            idx = (idx + 1) & mask
            i += 1
        if first_tombstone < self.__capacity__:
            return first_tombstone
        return self.__capacity__

    def _resize(self, new_cap: t.CSizeT):
        """Resize table and rehash all entries."""
        old_slots: t.CVoid | t.CPtr = self.__slots__
        old_cap: t.CSizeT = self.__capacity__
        new_slots: t.CVoid | t.CPtr = self.__mbuddy__.alloc(new_cap * _HT_SLOT_SIZE)
        if new_slots == None:
            return
        string.memset(new_slots, 0, new_cap * _HT_SLOT_SIZE)
        self.__slots__ = new_slots
        self.__capacity__ = new_cap
        self.__count__ = 0
        self.__tombstones__ = 0
        i: t.CSizeT = 0
        while i < old_cap:
            old_h_ptr: t.CUInt64T | t.CPtr = (t.CVoid | t.CPtr)(t.CUInt64T(old_slots) + i * _HT_SLOT_SIZE)
            h: t.CUInt64T = old_h_ptr[0]
            if h != _HT_EMPTY and h != _HT_TOMBSTONE:
                old_k_ptr: str | t.CPtr = (t.CVoid | t.CPtr)(t.CUInt64T(old_slots) + i * _HT_SLOT_SIZE + 8)
                old_v_ptr: t.CUInt64T | t.CPtr = (t.CVoid | t.CPtr)(t.CUInt64T(old_slots) + i * _HT_SLOT_SIZE + 16)
                k: str = old_k_ptr[0]
                v: t.CPtr = old_v_ptr[0]
                idx: t.CSizeT = self._find_slot(k, h)
                self._set_slot(idx, h, k, v)
                self.__count__ += 1
            i += 1

    def __setitem__(self, key: str, val: t.CNeedPtr):
        """Insert or update key-value pair. val automatically decays to pointer (t.CNeedPtr: no conversion if already pointer)."""
        h: t.CUInt64T = _fnv1a_hash(key)
        idx: t.CSizeT = self._find_slot(key, h)
        if idx >= self.__capacity__:
            return
        sh: t.CUInt64T = self._slot_hash(idx)
        if sh == _HT_EMPTY or sh == _HT_TOMBSTONE:
            if sh == _HT_TOMBSTONE:
                self.__tombstones__ -= 1
            self._set_slot(idx, h, key, val)
            self.__count__ += 1
            # Resize when load factor > 0.75
            load: t.CSizeT = self.__count__ + self.__tombstones__
            if load * 4 >= self.__capacity__ * 3:
                self._resize(self.__capacity__ * 2)
        else:
            self._set_slot(idx, h, key, val)

    def __getitem__(self, key: str) -> t.CPtr:
        """Lookup key and return value pointer. Returns None if key not found."""
        h: t.CUInt64T = _fnv1a_hash(key)
        idx: t.CSizeT = self._find_slot(key, h)
        if idx >= self.__capacity__:
            return None
        sh: t.CUInt64T = self._slot_hash(idx)
        if sh == _HT_EMPTY or sh == _HT_TOMBSTONE:
            return None
        return self._slot_value(idx)

    def __contains__(self, key: str) -> t.CInt:
        """Check whether key exists in table."""
        h: t.CUInt64T = _fnv1a_hash(key)
        idx: t.CSizeT = self._find_slot(key, h)
        if idx >= self.__capacity__:
            return 0
        sh: t.CUInt64T = self._slot_hash(idx)
        if sh == _HT_EMPTY or sh == _HT_TOMBSTONE:
            return 0
        return 1

    def __delitem__(self, key: str) -> t.CInt:
        """Delete entry by key. Return 1 on success, 0 if key not present."""
        h: t.CUInt64T = _fnv1a_hash(key)
        idx: t.CSizeT = self._find_slot(key, h)
        if idx >= self.__capacity__:
            return 0
        sh: t.CUInt64T = self._slot_hash(idx)
        if sh == _HT_EMPTY or sh == _HT_TOMBSTONE:
            return 0
        h_ptr: t.CUInt64T | t.CPtr = (t.CVoid | t.CPtr)(t.CUInt64T(self.__slots__) + idx * _HT_SLOT_SIZE)
        h_ptr[0] = _HT_TOMBSTONE
        self.__count__ -= 1
        self.__tombstones__ += 1
        return 1

    def __len__(self) -> t.CSizeT:
        return self.__count__

    def __iter__(self) -> HashTable | t.CPtr:
        self.__iter_index__ = 0
        return self

    def __next__(self) -> str:
        while self.__iter_index__ < self.__capacity__:
            idx: t.CSizeT = self.__iter_index__
            self.__iter_index__ = idx + 1
            sh: t.CUInt64T = self._slot_hash(idx)
            if sh != _HT_EMPTY and sh != _HT_TOMBSTONE:
                return self._slot_key(idx)
        raise StopIteration

    def get(self, key: str, default: t.CPtr) -> t.CPtr:
        """Return value pointer; return default pointer if key not found."""
        h: t.CUInt64T = _fnv1a_hash(key)
        idx: t.CSizeT = self._find_slot(key, h)
        if idx >= self.__capacity__:
            return default
        sh: t.CUInt64T = self._slot_hash(idx)
        if sh == _HT_EMPTY or sh == _HT_TOMBSTONE:
            return default
        return self._slot_value(idx)

    def set_int(self, key: str, val: int):
        """Store integer value. Value is copied into mbuddy-managed storage."""
        storage: t.CVoid | t.CPtr = self.__mbuddy__.alloc(8)
        int_ptr: int | t.CPtr = t.CVoid(t.CUInt64T(storage), t.CPtr)
        int_ptr[0] = val
        self[key] = storage

    def set_str(self, key: str, val: str):
        """Store string value. Directly store the string pointer."""
        self[key] = val
