"""
Variable-byte (varint) integer encoding and decoding.

Encodes unsigned integers using fewer bytes for smaller values, following the
standard MSB continuation-bit scheme used by Lucene, Tantivy, and Protocol
Buffers:

    - Each byte uses 7 bits for payload data (bits 0-6).
    - Bit 7 (MSB) is the continuation flag: 1 means more bytes follow,
      0 means this is the final byte.

Byte budget:
    0 ..              127  ->  1 byte
    128 ..          16_383  ->  2 bytes
    16_384 ..    2_097_151  ->  3 bytes
    2_097_152 .. 268_435_455  ->  4 bytes
    ...and so on (unlimited precision).

Also provides delta (gap) encoding helpers for sorted integer sequences,
which is the standard postings-list compression technique in information
retrieval.
"""

from __future__ import annotations


def encode_varint(n: int) -> bytes:
    """Encode a single unsigned integer as a variable-byte sequence.

    Parameters
    ----------
    n : int
        Non-negative integer to encode.

    Returns
    -------
    bytes
        The varint-encoded byte string (1-10 bytes for typical 64-bit values).

    Raises
    ------
    ValueError
        If *n* is negative.
    """
    if n < 0:
        raise ValueError(f"varint encoding requires a non-negative integer, got {n}")

    buf: list[int] = []
    while True:
        byte = n & 0x7F
        n >>= 7
        if n:
            buf.append(byte | 0x80)  # continuation bit set
        else:
            buf.append(byte)  # final byte, MSB clear
            break
    return bytes(buf)


def decode_varint(data: bytes, offset: int = 0) -> tuple[int, int]:
    """Decode a single varint starting at *offset* in *data*.

    Parameters
    ----------
    data : bytes
        The byte buffer to read from.
    offset : int, optional
        Starting position in *data* (default ``0``).

    Returns
    -------
    tuple[int, int]
        ``(value, bytes_consumed)`` where *bytes_consumed* is the number of
        bytes read so that the caller can advance the cursor.

    Raises
    ------
    ValueError
        If the data is truncated (continuation bit set on the last available
        byte).
    """
    result = 0
    shift = 0
    pos = offset
    length = len(data)

    while pos < length:
        byte = data[pos]
        result |= (byte & 0x7F) << shift
        pos += 1
        if not (byte & 0x80):
            return result, pos - offset
        shift += 7

    raise ValueError("truncated varint: unexpected end of data")


def encode_varints(values: list[int]) -> bytes:
    """Batch-encode a list of unsigned integers as concatenated varints.

    Parameters
    ----------
    values : list[int]
        Non-negative integers to encode.

    Returns
    -------
    bytes
        Concatenated varint-encoded byte string.
    """
    parts: list[bytes] = []
    for v in values:
        parts.append(encode_varint(v))
    return b"".join(parts)


def decode_varints(data: bytes) -> list[int]:
    """Decode all varints in *data* and return them as a list.

    Parameters
    ----------
    data : bytes
        Byte buffer of concatenated varints.

    Returns
    -------
    list[int]
        The decoded integer values.
    """
    result: list[int] = []
    offset = 0
    length = len(data)
    while offset < length:
        value, consumed = decode_varint(data, offset)
        result.append(value)
        offset += consumed
    return result


def encode_delta(sorted_ints: list[int]) -> bytes:
    """Delta-encode then varint-encode a sorted list of unsigned integers.

    Gap encoding stores successive differences instead of absolute values.
    For a sorted postings list like ``[10, 23, 29, 105]`` the gaps are
    ``[10, 13, 6, 76]``, which are much smaller and therefore compress
    better with varint encoding.

    Parameters
    ----------
    sorted_ints : list[int]
        Monotonically non-decreasing sequence of non-negative integers.

    Returns
    -------
    bytes
        Varint-encoded gap sequence.

    Raises
    ------
    ValueError
        If the input is not sorted or contains negative values.
    """
    if not sorted_ints:
        return b""

    gaps: list[int] = []
    prev = 0
    for v in sorted_ints:
        gap = v - prev
        if gap < 0:
            raise ValueError(
                f"encode_delta requires a sorted sequence, but found {v} after {prev}"
            )
        gaps.append(gap)
        prev = v

    return encode_varints(gaps)


def decode_delta(data: bytes) -> list[int]:
    """Decode a delta + varint encoded byte string back to a sorted list.

    Parameters
    ----------
    data : bytes
        Byte buffer produced by :func:`encode_delta`.

    Returns
    -------
    list[int]
        The original sorted integer sequence.
    """
    if not data:
        return []

    gaps = decode_varints(data)
    result: list[int] = []
    cumulative = 0
    for gap in gaps:
        cumulative += gap
        result.append(cumulative)
    return result
