"""Utility helpers for aeg.

Currently provides Python-side aligned allocation helpers that avoid relying
on libc/posix_memalign. Memory is owned by Python; C code only borrows it.
"""

from typing import Protocol

from ._loader import ffi

__all__ = ["new_aligned_struct", "aligned_address", "Buffer", "nonce_increment", "wipe"]

try:
    from collections.abc import Buffer  # type: ignore
except ImportError:
    # Fallback for Python < 3.12
    class Buffer(Protocol):
        def __buffer__(self, flags: int) -> memoryview: ...


def aligned_address(obj) -> int:
    """Return the integer address of the start of a cffi array object."""
    return int(ffi.cast("uintptr_t", ffi.addressof(obj, 0)))


class StructHolder:
    """Proxy object for aligned struct allocation.

    Exposes the aligned pointer as a property and wipes the buffer on deletion.
    """

    def __init__(self, ptr: object, view: memoryview):
        self._ptr = ptr
        self._view = view  # Keep memoryview slice and its bytearray alive

    @property
    def ptr(self) -> object:
        """The aligned pointer to the struct."""
        return self._ptr

    def __del__(self):
        wipe(self._view)
        del self._ptr, self._view


def new_aligned_struct(ctype: str, alignment: int) -> StructHolder:
    """Allocate memory for one instance of ``ctype`` with requested alignment."""
    # Allocate backing storage with extra space for alignment
    size = ffi.sizeof(ctype)
    view = memoryview(bytearray(size + alignment - 1))
    # Compute alignment offset from the base address
    offset = (-aligned_address(ffi.from_buffer(view))) & (alignment - 1)
    # Slice the memoryview to the aligned region (keeps bytearray alive)
    view = view[offset : offset + size]
    return StructHolder(ffi.from_buffer(f"{ctype} *", view), view)


def nonce_increment(nonce: Buffer) -> None:
    """Increment the nonce in place using little-endian byte order.

    Useful for generating unique nonces for each consecutive message.

    Args:
        nonce: The nonce buffer to increment (modified in place).
    """
    n = memoryview(nonce)
    for i in range(len(n)):
        if n[i] < 255:
            n[i] += 1
            return
        n[i] = 0


def wipe(buffer: Buffer) -> None:
    """Securely clearing sensitive data from memory. Sets all bytes of the buffer to 0xFF.

    Args:
        buffer: The buffer to wipe (modified in place).
    """
    # This is the fastest method I have found in Python
    n = memoryview(buffer).cast("B")
    n[:] = b"\xff" * len(n)
