"""
Copyright (C) 2018-2019 The ontology Authors
This file is part of The ontology library.

The ontology is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

The ontology is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with The ontology.  If not, see <http://www.gnu.org/licenses/>.
"""

import sys
import struct
import binascii
import importlib

from ontology.io.memory_stream import StreamManager
from ontology.exception.error_code import ErrorCode
from ontology.exception.exception import SDKException


class BinaryReader(StreamManager):
    """
    Description:
    Binary Reader

    Usage:
        from ontology.io.binary_reader import BinaryReader
    """

    def __init__(self, stream):
        """
        Create an instance.

        Args:
            stream (BytesIO): a stream to operate on. i.e. a neo.IO.MemoryStream or raw BytesIO.
        """
        super().__init__()
        self.stream = stream

    def unpack(self, fmt, length=1):
        """
        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:
            info = struct.unpack(fmt, self.stream.read(length))[0]
        except struct.error as e:
            raise SDKException(ErrorCode.unpack_error(e.args[0]))
        return info

    def read_byte(self, do_ord=True) -> int:
        """
        Read a single byte.
        Args:
            do_ord (bool): (default True) convert the byte to an ordinal first.
        Returns:
            bytes: a single byte if successful. 0 (int) if an exception occurred.
        """
        try:
            if do_ord:
                return ord(self.stream.read(1))
            else:
                return self.stream.read(1)
        except Exception as e:
            raise SDKException(ErrorCode.read_byte_error(e.args[0]))

    def read_bytes(self, length) -> 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)
        return value

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

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

    def read_char(self):
        """
        Read 1 byte as a character from the stream.

        Returns:
            str: a single character.
        """
        return self.unpack('c')

    def read_float(self, little_endian=True):
        """
        Read 4 bytes as a float value from the stream.

        Args:
            little_endian (bool): specify the endianness. (Default) Little endian.

        Returns:
            float:
        """
        if little_endian:
            endian = "<"
        else:
            endian = ">"
        return self.unpack("%sf" % endian, 4)

    def read_double(self, little_endian=True):
        """
        Read 8 bytes as a double value from the stream.

        Args:
            little_endian (bool): specify the endianness. (Default) Little endian.

        Returns:
            float:
        """
        if little_endian:
            endian = "<"
        else:
            endian = ">"
        return self.unpack("%sd" % endian, 8)

    def read_int8(self, little_endian=True):
        """
        Read 1 byte as a signed integer value from the stream.

        Args:
            little_endian (bool): specify the endianness. (Default) Little endian.

        Returns:
            int:
        """
        if little_endian:
            endian = "<"
        else:
            endian = ">"
        return self.unpack('%sb' % endian)

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

        Args:
            little_endian (bool): specify the endianness. (Default) Little endian.

        Returns:
            int:
        """
        if little_endian:
            endian = "<"
        else:
            endian = ">"
        return self.unpack('%sB' % endian)

    def read_int16(self, little_endian=True):
        """
        Read 2 byte as a signed integer value from the stream.

        Args:
            little_endian (bool): specify the endianness. (Default) Little endian.

        Returns:
            int:
        """
        if little_endian:
            endian = "<"
        else:
            endian = ">"
        return self.unpack('%sh' % endian, 2)

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

        Args:
            little_endian (bool): specify the endianness. (Default) Little endian.

        Returns:
            int:
        """
        if little_endian:
            endian = "<"
        else:
            endian = ">"
        return self.unpack('%sH' % endian, 2)

    def read_int32(self, little_endian=True):
        """
        Read 4 bytes as a signed integer value from the stream.

        Args:
            little_endian (bool): specify the endianness. (Default) Little endian.

        Returns:
            int:
        """
        if little_endian:
            endian = "<"
        else:
            endian = ">"
        return self.unpack('%si' % endian, 4)

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

        Args:
            little_endian (bool): specify the endianness. (Default) Little endian.

        Returns:
            int:
        """
        if little_endian:
            endian = "<"
        else:
            endian = ">"
        return self.unpack('%sI' % endian, 4)

    def read_int64(self, little_endian=True):
        """
        Read 8 bytes as a signed integer value from the stream.

        Args:
            little_endian (bool): specify the endianness. (Default) Little endian.

        Returns:
            int:
        """
        if little_endian:
            endian = "<"
        else:
            endian = ">"
        return self.unpack('%sq' % endian, 8)

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

        Args:
            little_endian (bool): specify the endianness. (Default) Little endian.

        Returns:
            int:
        """
        if little_endian:
            endian = "<"
        else:
            endian = ">"
        return self.unpack('%sQ' % endian, 8)

    def read_var_int(self, max_size=sys.maxsize):
        """
        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_size (int): (Optional) maximum number of bytes to read.

        Returns:
            int:
        """
        fb = self.read_byte()
        if fb is 0:
            return fb
        if hex(fb) == '0xfd':
            value = self.read_uint16()
        elif hex(fb) == '0xfe':
            value = self.read_uint32()
        elif hex(fb) == '0xff':
            value = self.read_uint64()
        else:
            value = fb
        if value > max_size:
            raise SDKException(ErrorCode.param_err('Invalid format'))
        return int(value)

    def read_var_bytes(self, max_size=sys.maxsize) -> bytes:
        """
        Read a variable length of bytes from the stream.

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

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

    def read_str(self):
        """
        Read a string from the stream.

        Returns:
            str:
        """
        length = self.read_uint8()
        return self.unpack(str(length) + 's', length)

    def read_var_str(self, max_size=sys.maxsize):
        """
        Similar to `ReadString` but expects a variable length indicator instead of the fixed 1 byte indicator.

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

        Returns:
            bytes:
        """
        length = self.read_var_int(max_size)
        return self.unpack(str(length) + 's', length)

    def read_fixed_str(self, length):
        """
        Read a fixed length string from the stream.
        Args:
            length (int): length of string to read.

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

    def read_serializable_array(self, class_name, max_size=sys.maxsize):
        """
        Deserialize a stream into the object specific by `class_name`.

        Args:
            class_name (str): a full path to the class to be deserialized into. e.g. 'neo.Core.Block.Block'
            max_size (int): (Optional) maximum number of bytes to read.

        Returns:
            list: list of `class_name` objects deserialized from the stream.
        """
        module = '.'.join(class_name.split('.')[:-1])
        class_name = class_name.split('.')[-1]
        class_attr = getattr(importlib.import_module(module), class_name)
        length = self.read_var_int(max_size=max_size)
        items = []
        try:
            for _ in range(0, length):
                item = class_attr()
                item.Deserialize(self)
                items.append(item)
        except Exception as e:
            raise SDKException(ErrorCode.param_err("Couldn't deserialize %s" % e))
        return items

    def read_2000256_list(self):
        """
        Read 2000 times a 64 byte value from the stream.

        Returns:
            list: a list containing 2000 64 byte values in reversed form.
        """
        items = []
        for _ in range(0, 2000):
            data = self.read_bytes(64)
            ba = bytearray(binascii.unhexlify(data))
            ba.reverse()
            items.append(ba.hex().encode('utf-8'))
        return items

    def read_hashes(self):
        """
        Read Hash values from the stream.

        Returns:
            list: a list of hash values. Each value is of the bytearray type.
        """
        var_len = self.read_var_int()
        items = []
        for _ in range(0, var_len):
            ba = bytearray(self.read_bytes(32))
            ba.reverse()
            items.append(ba.hex())
        return items