"""Binary encodings for bup."""

# Copyright (C) 2010 Rob Browning
#
# This code is covered under the terms of the GNU Library General
# Public License as described in the bup LICENSE file.

# Variable length integers are encoded as vints -- see lucene.

from io import BytesIO

from bup import _helpers


def write_vuint(port, x):
    port.write(encode_vuint(x))


def encode_vuint(x):
    try:
        return _helpers.vuint_encode(x)
    except OverflowError:
        if x < 0:
            raise ValueError(f'cannot encode negative value {x}') from None
    assert x != 0, 'the C code did not handle zero'
    ret = b''
    while True:
        seven_bits = x & 0x7f
        x >>= 7
        if x:
            ret += (0x80 | seven_bits).to_bytes(1, 'big')
        else:
            ret += seven_bits.to_bytes(1, 'big')
            break
    return ret


def read_vuint(port):
    c = port.read(1)
    if not c:
        return None
    assert isinstance(c, bytes)
    if ord(c) == 0:
        return 0
    result = 0
    offset = 0
    while True:
        b = ord(c)
        if b & 0x80:
            result |= ((b & 0x7f) << offset)
            offset += 7
            c = port.read(1)
            if not c:
                raise EOFError('encountered EOF while reading vuint')
        else:
            result |= (b << offset)
            break
    return result


def write_vint(port, x):
    # Sign is handled with the second bit of the first byte.  All else
    # matches vuint.
    port.write(encode_vint(x))


def encode_vint(x):
    try:
        return _helpers.vint_encode(x)
    except OverflowError:
        pass
    assert x != 0, 'the C code did not handle zero'
    if x < 0:
        x = -x
        sign_and_six_bits = (x & 0x3f) | 0x40
    else:
        sign_and_six_bits = x & 0x3f
    x >>= 6
    assert x != 0, f'the C code did not handle the small value {x}'
    return (0x80 | sign_and_six_bits).to_bytes(1, 'big') + encode_vuint(x)


def read_vint(port):
    c = port.read(1)
    if not c:
        return None
    assert isinstance(c, bytes)
    negative = False
    result = 0
    offset = 0
    # Handle first byte with sign bit specially.
    b = ord(c)
    if b & 0x40:
        negative = True
    result |= (b & 0x3f)
    if b & 0x80:
        offset += 6
        c = port.read(1)
        if not c:
            raise EOFError('encountered EOF while reading vint')
    elif negative:
        return -result
    else:
        return result
    while True:
        b = ord(c)
        if b & 0x80:
            result |= ((b & 0x7f) << offset)
            offset += 7
            c = port.read(1)
            if not c:
                raise EOFError('encountered EOF while reading vint')
        else:
            result |= (b << offset)
            break
    if negative:
        return -result
    return result


def write_bvec(port, x):
    write_vuint(port, len(x))
    port.write(x)


def read_bvec(port):
    n = read_vuint(port)
    if n is None:
        return None
    val = port.read(n)
    if len(val) != n: # e.g. EOF when n != 0
        raise EOFError('EOF while reading bvec bytes')
    return val

def encode_bvec(x):
    return _helpers.vuint_encode(len(x)) + x


def skip_bvec(port):
    n = read_vuint(port)
    if n is None:
        raise EOFError('encountered EOF while skipping bvec')
    val = port.read(n)
    if not val:
        raise EOFError('encountered EOF while skipping bvec')


def send(port, types, *args):
    if len(types) != len(args):
        raise ValueError('number of arguments does not match format string')
    for (type, value) in zip(types, args):
        if type == 'V':
            write_vuint(port, value)
        elif type == 'v':
            write_vint(port, value)
        elif type == 's':
            write_bvec(port, value)
        else:
            raise ValueError(f'unknown format string item {type!r}')


def recv(port, types):
    result = []
    for type in types:
        if type == 'V': decode = read_vuint
        elif type == 'v': decode = read_vint
        elif type == 's': decode = read_bvec
        else: raise ValueError(f'unknown format string item {type!r}')
        x = decode(port)
        if x is None:
            raise EOFError(f'EOF while reading type {type!r}')
        result.append(x)
    return result


def pack(types, *args):
    try:
        return _helpers.limited_vint_pack(types, args)
    except OverflowError:
        pass
    assert len(types) == len(args)
    ret = []
    for typ, value in zip(types, args):
        if typ == 'V':
            ret.append(encode_vuint(value))
        elif typ == 'v':
            ret.append(encode_vint(value))
        elif typ == 's':
            ret.append(encode_bvec(value))
        else:
            raise ValueError(f'unknown format string item {typ!r}')
    return b''.join(ret)


def unpack(types, data):
    port = BytesIO(data)
    return recv(port, types)
