from construct import *
from construct.lib import *
import binascii


def integer2bits(number, width, signed=False):
    r"""
    Converts an integer into its binary representation in a bit-string. Width is the amount of bits to generate. Each bit is represented as either \\x00 or \\x01. The most significant bit is first, big-endian. This is reverse to `bits2integer`.

    Examples:

        >>> integer2bits(19, 8)
        b'\x00\x00\x00\x01\x00\x00\x01\x01'
    """
    if width < 0:
        raise ValueError(f"width {width} must be non-negative")
    if width == 0:
        return b""

    if signed:
        min = -(2 ** width // 2)
        max = 2 ** width // 2 - 1
    else:
        min = 0
        max = 2 ** width - 1
    if not min <= number <= max:
        raise ValueError(f"number {number} is out of range (min={min}, max={max})")

    if number < 0:
        number += 1 << width
    bits = bytearray(width)
    i = width - 1
    while number and i >= 0:
        bits[i] = number & 1
        number >>= 1
        i -= 1
    return bytes(bits)


def integer2bytes(number, width, signed=False):
    r"""
    Converts an integer into a byte-string. This is reverse to `bytes2integer`.

    Examples:

        >>> integer2bytes(19, 4)
        '\x00\x00\x00\x13'
    """
    # pypy does not check this in int.to_bytes, lazy fuckers
    if width < 0:
        raise ValueError(f"width {width} must be non-negative")

    try:
        return int.to_bytes(number, width, 'big', signed=signed)
    except OverflowError:
        raise ValueError(f"number {number} does not fit width {width} signed {signed}")


def bits2integer(data, signed=False):
    r"""
    Converts a bit-string into an integer. Set signed to interpret the number as a 2-s complement signed integer. This is reverse to `integer2bits`.

    Examples:

        >>> bits2integer(b"\x01\x00\x00\x01\x01")
        19
    """
    if data == b"":
        return 0

    number = 0
    for b in data:
        number = (number << 1) | b

    if signed and data[0]:
        bias = 1 << len(data)
        return number - bias
    else:
        return number


def bytes2integer(data, signed=False):
    r"""
    Converts a byte-string into an integer. This is reverse to `integer2bytes`.

    Examples:

        >>> bytes2integer(b'\x00\x00\x00\x13')
        19
    """
    return int.from_bytes(data, 'big', signed=signed)


BYTES2BITS_CACHE = {i:integer2bits(i,8) for i in range(256)}
def bytes2bits(data):
    r""" 
    Converts between bit-string and byte-string representations, both as bytes type.

    Example:

        >>> bytes2bits(b'ab')
        b"\x00\x01\x01\x00\x00\x00\x00\x01\x00\x01\x01\x00\x00\x00\x01\x00"
    """
    return b"".join(BYTES2BITS_CACHE[b] for b in data)


BITS2BYTES_CACHE = {bytes2bits(int2byte(i)):i for i in range(256)}
def bits2bytes(data):
    r""" 
    Converts between bit-string and byte-string representations, both as bytes type. Its length must be multiple of 8.

    Example:

        >>> bits2bytes(b"\x00\x01\x01\x00\x00\x00\x00\x01\x00\x01\x01\x00\x00\x00\x01\x00")
        b'ab'
    """
    if len(data) % 8:
        raise ValueError(f"data length {len(data)} must be a multiple of 8")
    return bytes(BITS2BYTES_CACHE[data[i:i+8]] for i in range(0,len(data),8))


def swapbytes(data):
    r"""
    Performs an endianness swap on byte-string.

    Example:

        >>> swapbytes(b'abcd')
        b'dcba'
    """
    return data[::-1]


def swapbytesinbits(data):
    r"""
    Performs an byte-swap within a bit-string. Its length must be multiple of 8.

    Example:

        >>> swapbytesinbits(b'0000000011111111')
        b'1111111100000000'
    """
    if len(data) % 8:
        raise ValueError(f"data length {len(data)} must be a multiple of 8")
    return b"".join(data[i:i+8] for i in reversed(range(0,len(data),8)))


SWAPBITSINBYTES_CACHE = {i:byte2int(bits2bytes(swapbytes(bytes2bits(int2byte(i))))) for i in range(256)}
def swapbitsinbytes(data):
    r"""
    Performs a bit-reversal on each byte within a byte-string.

    Example:

        >>> swapbitsinbytes(b"\xf0\x00")
        b"\x0f\x00"
    """
    return bytes(SWAPBITSINBYTES_CACHE[b] for b in data)


def hexlify(data):
    """Returns binascii.hexlify(data)."""
    return binascii.hexlify(data)


def unhexlify(data):
    """Returns binascii.unhexlify(data)."""
    return binascii.unhexlify(data)
