import six
from construct.lib.py3compat import int2byte


if six.PY3:
    def int_to_bin(number, width = 32):
        r"""
        Convert an integer into its binary representation in a bytes object.
        Width is the amount of bits to generate. If width is larger than the actual
        amount of bits required to represent number in binary, sign-extension is
        used. If it's smaller, the representation is trimmed to width bits.
        Each "bit" is either '\x00' or '\x01'. The MSBit is first.
    
        Examples:
    
            >>> int_to_bin(19, 5)
            b'\x01\x00\x00\x01\x01'
            >>> int_to_bin(19, 8)
            b'\x00\x00\x00\x01\x00\x00\x01\x01'
        """
        number = int(number)
        if number < 0:
            number += 1 << width
        i = width - 1
        bits = bytearray(width)
        while number and i >= 0:
            bits[i] = number & 1
            number >>= 1
            i -= 1
        return bytes(bits)

    # heavily optimized for performance
    def bin_to_int(bits, signed = False):
        r"""
        Logical opposite of int_to_bin. Both '0' and '\x00' are considered zero,
        and both '1' and '\x01' are considered one. Set sign to True to interpret
        the number as a 2-s complement signed integer.
        """
        bits = "".join("01"[b & 1] for b in bits)
        if signed and bits[0] == "1":
            bits = bits[1:]
            bias = 1 << len(bits)
        else:
            bias = 0
        return int(bits, 2) - bias

    _char_to_bin = [0] * 256
    _bin_to_char = {}
    for i in range(256):
        ch = int2byte(i)
        bin = int_to_bin(i, 8)
        # Populate with for both keys i and ch, to support Python 2 & 3
        _char_to_bin[i] = bin
        _bin_to_char[bin] = ord(ch)

    def encode_bin(data):
        """ 
        Create a binary representation of the given b'' object. Assume 8-bit
        ASCII. Example:
    
            >>> encode_bin('ab')
            b"\x00\x01\x01\x00\x00\x00\x00\x01\x00\x01\x01\x00\x00\x00\x01\x00"
        """
        return six.b("").join(_char_to_bin[int(ch)] for ch in data)

    def decode_bin(data):
        if len(data) & 7:
            raise ValueError("Data length must be a multiple of 8")
        i = 0
        j = 0
        l = len(data) // 8
        arr = bytearray(l)
        while j < l:
            arr[j] = _bin_to_char[data[i:i+8]]
            i += 8
            j += 1
        return arr
    
    def swap_bytes(bits, bytesize=8):
        r"""
        Bits is a b'' object containing a binary representation. Assuming each
        bytesize bits constitute a bytes, perform a endianness byte swap. Example:
    
            >>> swap_bytes(b'00011011', 2)
            b'11100100'
        """
        i = 0
        l = len(bits)
        output = [six.b("")] * ((l // bytesize) + 1)
        j = len(output) - 1
        while i < l:
            output[j] = bits[i : i + bytesize]
            i += bytesize
            j -= 1
        return six.b("").join(output)

else:

    def int_to_bin(number, width = 32):
        r"""
        Convert an integer into its binary representation in a bytes object.
        Width is the amount of bits to generate. If width is larger than the actual
        amount of bits required to represent number in binary, sign-extension is
        used. If it's smaller, the representation is trimmed to width bits.
        Each "bit" is either '\x00' or '\x01'. The MSBit is first.
    
        Examples:
    
            >>> int_to_bin(19, 5)
            '\x01\x00\x00\x01\x01'
            >>> int_to_bin(19, 8)
            '\x00\x00\x00\x01\x00\x00\x01\x01'
        """
        if number < 0:
            number += 1 << width
        i = width - 1
        bits = ["\x00"] * width
        while number and i >= 0:
            bits[i] = "\x00\x01"[number & 1]
            number >>= 1
            i -= 1
        return "".join(bits)

    # heavily optimized for performance
    def bin_to_int(bits, signed = False):
        r"""
        Logical opposite of int_to_bin. Both '0' and '\x00' are considered zero,
        and both '1' and '\x01' are considered one. Set sign to True to interpret
        the number as a 2-s complement signed integer.
        """
        bits = "".join("01"[ord(b) & 1] for b in bits)
        if signed and bits[0] == "1":
            bits = bits[1:]
            bias = 1 << len(bits)
        else:
            bias = 0
        return int(bits, 2) - bias

    _char_to_bin = [0] * 256
    _bin_to_char = {}
    for i in range(256):
        ch = int2byte(i)
        bin = int_to_bin(i, 8)
        # Populate with for both keys i and ch, to support Python 2 & 3
        _char_to_bin[i] = bin
        _bin_to_char[bin] = ch

    def encode_bin(data):
        """ 
        Create a binary representation of the given b'' object. Assume 8-bit
        ASCII. Example:
    
            >>> encode_bin('ab')
            b"\x00\x01\x01\x00\x00\x00\x00\x01\x00\x01\x01\x00\x00\x00\x01\x00"
        """
        return "".join(_char_to_bin[ord(ch)] for ch in data)

    def decode_bin(data):
        if len(data) & 7:
            raise ValueError("Data length must be a multiple of 8")
        i = 0
        j = 0
        l = len(data) // 8
        chars = [""] * l
        while j < l:
            chars[j] = _bin_to_char[data[i:i+8]]
            i += 8
            j += 1
        return "".join(chars)

    def swap_bytes(bits, bytesize=8):
        r"""
        Bits is a b'' object containing a binary representation. Assuming each
        bytesize bits constitute a bytes, perform a endianness byte swap. Example:
    
            >>> swap_bytes(b'00011011', 2)
            b'11100100'
        """
        i = 0
        l = len(bits)
        output = [""] * ((l // bytesize) + 1)
        j = len(output) - 1
        while i < l:
            output[j] = bits[i : i + bytesize]
            i += bytesize
            j -= 1
        return "".join(output)
