import sys
import struct
from typing import Union, Any
from neo.Network.core.uint256 import UInt256
from neo.Network.core.uint160 import UInt160
from neo.IO.MemoryStream import StreamManager


class BinaryReader(object):
    """A convenience class for reading data from byte streams"""

    def __init__(self, stream: Union[bytes, bytearray]) -> None:
        """
        Create an instance.

        Args:
            stream (BytesIO, bytearray): a stream to operate on.
        """
        super(BinaryReader, self).__init__()
        self._stream = StreamManager.GetStream(stream)

    def _unpack(self, fmt, length=1) -> Any:
        """
        Unpack the stream contents according to the specified format in `fmt`.
        For more information about the `fmt` format see: https://docs.python.org/3/library/struct.html

        Args:
            fmt (str): format string.
            length (int): amount of bytes to read.

        Returns:
            variable: the result according to the specified format.
        """
        try:
            values = struct.unpack(fmt, self._stream.read(length))
            return values[0]
        except struct.error as e:
            raise ValueError(e)

    def read_byte(self) -> bytes:
        """
        Read a single byte.

        Raises:
            ValueError: if 1 byte of data cannot be read from the stream

        Returns:
            bytes: a single byte.
        """
        value = self._stream.read(1)
        if len(value) != 1:
            raise ValueError("Could not read byte from empty stream")
        return value

    def read_bytes(self, length: int) -> bytes:
        """
        Read the specified number of bytes from the stream.

        Args:
            length (int): number of bytes to read.

        Returns:
            bytes: `length` number of bytes.
        """
        value = self._stream.read(length)
        if len(value) != length:
            raise ValueError("Could not read {} bytes from stream. Only found {} bytes of data".format(length, len(value)))

        return value

    def read_bool(self) -> bool:
        """
        Read 1 byte as a boolean value from the stream.

        Returns:
            bool:
        """
        return self._unpack('?')

    def read_uint8(self, endian="<"):
        """
        Read 1 byte as an unsigned integer value from the stream.

        Args:
            endian (str): specify the endianness. (Default) Little endian ('<'). Use '>' for big endian.

        Returns:
            int:
        """
        return self._unpack('%sB' % endian)

    def read_uint16(self, endian="<"):
        """
        Read 2 byte as an unsigned integer value from the stream.

        Args:
            endian (str): specify the endianness. (Default) Little endian ('<'). Use '>' for big endian.

        Returns:
            int:
        """
        return self._unpack('%sH' % endian, 2)

    def read_uint32(self, endian="<"):
        """
        Read 4 bytes as an unsigned integer value from the stream.

        Args:
            endian (str): specify the endianness. (Default) Little endian ('<'). Use '>' for big endian.

        Returns:
            int:
        """
        return self._unpack('%sI' % endian, 4)

    def read_uint64(self, endian="<"):
        """
        Read 8 bytes as an unsigned integer value from the stream.

        Args:
            endian (str): specify the endianness. (Default) Little endian ('<'). Use '>' for big endian.

        Returns:
            int:
        """
        return self._unpack('%sQ' % endian, 8)

    def read_var_int(self, max=sys.maxsize) -> int:
        """
        Read a variable length integer from the stream.
        The NEO network protocol supports encoded storage for space saving. See: http://docs.neo.org/en-us/node/network-protocol.html#convention

        Args:
            max: (Optional) maximum number of bytes to read.

        Returns:
            int:
        """
        fb = int.from_bytes(self.read_byte(), 'little')
        if fb is 0:
            return fb

        if fb == 0xfd:
            value = self.read_uint16()
        elif fb == 0xfe:
            value = self.read_uint32()
        elif fb == 0xff:
            value = self.read_uint64()
        else:
            value = fb

        if value > max:
            raise ValueError("Invalid format")

        return value

    def read_var_bytes(self, max=sys.maxsize):
        """
        Read a variable length of bytes from the stream.
        The NEO network protocol supports encoded storage for space saving. See: http://docs.neo.org/en-us/node/network-protocol.html#convention

        Args:
            max (int): (Optional) maximum number of bytes to read.

        Returns:
            bytes:
        """
        length = self.read_var_int(max)
        return self.read_bytes(length)

    def read_var_string(self, max=sys.maxsize) -> str:
        """
        Similar to `ReadString` but expects a variable length indicator instead of the fixed 1 byte indicator.

        Args:
            max (int): (Optional) maximum number of bytes to read.

        Returns:
            bytes:
        """
        length = self.read_var_int(max)
        try:
            data = self._unpack(str(length) + 's', length)
            return data.decode('utf-8')

        except UnicodeDecodeError as e:
            raise e
        except Exception as e:
            raise e

    def read_fixed_string(self, length: int) -> str:
        """
        Read a fixed length string from the stream.

        Args:
            length (int): length of string to read.

        Raises:
            ValueError: if not enough data could be read from the stream

        Returns:
            str:
        """
        return self.read_bytes(length).rstrip(b'\x00')

    def read_uint256(self):
        """
        Read a UInt256 value from the stream.

        Returns:
            UInt256:
        """
        return UInt256(data=bytearray(self.read_bytes(32)))

    def read_uint160(self):
        """
        Read a UInt160 value from the stream.

        Returns:
            UInt160:
        """
        return UInt160(data=bytearray(self.read_bytes(20)))

    def cleanup(self):
        if self._stream:
            StreamManager.ReleaseStream(self._stream)