# -*- coding:utf-8 -*-
"""
Description:
    Binary Writer

Usage:
    from neocore.IO.BinaryWriter import BinaryWriter
"""
import sys
import os
import inspect
import struct
import binascii

from logzero import logger

from neocore.UInt160 import UInt160
from neocore.UInt256 import UInt256


def swap32(i):
    """
    Change the endianness from little endian to big endian.
    Args:
        i (int):

    Returns:
        int:
    """
    return struct.unpack("<I", struct.pack(">I", i))[0]


def convert_to_uint160(value):
    """
    Convert an int value to a 10 bytes binary string value.
    Note: the return value is not really 160 bits, nor is it of the neocore.UInt160 type

    Args:
        value (int): number to convert.

    Returns:
        str:
    """
    return bin(value + 2 ** 20)[-20:]


def convert_to_uint256(value):
    """
    Convert an int value to a 16 bytes binary string value.
    Note: the return value is not really 256 bits, nor is it of the neocore.UInt256 type

    Args:
        value (int): number to convert.

    Returns:
        str:
    """
    return bin(value + 2 ** 32)[-32:]


class BinaryWriter(object):
    """docstring for BinaryWriter"""

    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(BinaryWriter, self).__init__()
        self.stream = stream

    def WriteByte(self, value):
        """
        Write a single byte to the stream.

        Args:
            value (bytes, str or int): value to write to the stream.
        """
        if type(value) is bytes:
            self.stream.write(value)
        elif type(value) is str:
            self.stream.write(value.encode('utf-8'))
        elif type(value) is int:
            self.stream.write(bytes([value]))

    def WriteBytes(self, value, unhex=True):
        """
        Write a `bytes` type to the stream.

        Args:
            value (bytes): array of bytes to write to the stream.
            unhex (bool): (Default) True. Set to unhexlify the stream. Use when the bytes are not raw bytes; i.e. b'aabb'

        Returns:
            int: the number of bytes written.
        """
        if unhex:
            try:
                value = binascii.unhexlify(value)
            except binascii.Error:
                pass
        return self.stream.write(value)

    def pack(self, fmt, data):
        """
        Write bytes by packing them according to the provided format `fmt`.
        For more information about the `fmt` format see: https://docs.python.org/3/library/struct.html

        Args:
            fmt (str): format string.
            data (object): the data to write to the raw stream.

        Returns:
            int: the number of bytes written.
        """
        return self.WriteBytes(struct.pack(fmt, data), unhex=False)

    def WriteChar(self, value):
        """
        Write a 1 byte character value to the stream.

        Args:
            value: value to write.

        Returns:
            int: the number of bytes written.
        """
        return self.pack('c', value)

    def WriteFloat(self, value, endian="<"):
        """
        Pack the value as a float and write 4 bytes to the stream.

        Args:
            value (number): the value to write to the stream.
            endian (str): specify the endianness. (Default) Little endian ('<'). Use '>' for big endian.

        Returns:
            int: the number of bytes written.
        """
        return self.pack('%sf' % endian, value)

    def WriteDouble(self, value, endian="<"):
        """
        Pack the value as a double and write 8 bytes to the stream.

        Args:
            value (number): the value to write to the stream.
            endian (str): specify the endianness. (Default) Little endian ('<'). Use '>' for big endian.

        Returns:
            int: the number of bytes written.
        """
        return self.pack('%sd' % endian, value)

    def WriteInt8(self, value, endian="<"):
        """
        Pack the value as a signed byte and write 1 byte to the stream.

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

        Returns:
            int: the number of bytes written.
        """
        return self.pack('%sb' % endian, value)

    def WriteUInt8(self, value, endian="<"):
        """
        Pack the value as an unsigned byte and write 1 byte to the stream.

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

        Returns:
            int: the number of bytes written.
        """
        return self.pack('%sB' % endian, value)

    def WriteBool(self, value):
        """
        Pack the value as a bool and write 1 byte to the stream.

        Args:
            value: the boolean value to write.

        Returns:
            int: the number of bytes written.
        """
        return self.pack('?', value)

    def WriteInt16(self, value, endian="<"):
        """
        Pack the value as a signed integer and write 2 bytes to the stream.

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

        Returns:
            int: the number of bytes written.
        """
        return self.pack('%sh' % endian, value)

    def WriteUInt16(self, value, endian="<"):
        """
        Pack the value as an unsigned integer and write 2 bytes to the stream.

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

        Returns:
            int: the number of bytes written.
        """
        return self.pack('%sH' % endian, value)

    def WriteInt32(self, value, endian="<"):
        """
        Pack the value as a signed integer and write 4 bytes to the stream.

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

        Returns:
            int: the number of bytes written.
        """
        return self.pack('%si' % endian, value)

    def WriteUInt32(self, value, endian="<"):
        """
        Pack the value as an unsigned integer and write 4 bytes to the stream.

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

        Returns:
            int: the number of bytes written.
        """
        return self.pack('%sI' % endian, value)

    def WriteInt64(self, value, endian="<"):
        """
        Pack the value as a signed integer and write 8 bytes to the stream.

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

        Returns:
            int: the number of bytes written.
        """
        return self.pack('%sq' % endian, value)

    def WriteUInt64(self, value, endian="<"):
        """
        Pack the value as an unsigned integer and write 8 bytes to the stream.

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

        Returns:
            int: the number of bytes written.
        """
        return self.pack('%sQ' % endian, value)

    def WriteUInt160(self, value):
        """
        Write a UInt160 type to the stream.

        Args:
            value (UInt160):

        Raises:
            Exception: when `value` is not of neocore.UInt160 type.
        """
        if type(value) is UInt160:
            value.Serialize(self)
        else:
            raise Exception("value must be UInt160 instance ")

    def WriteUInt256(self, value):
        """
        Write a UInt256 type to the stream.

        Args:
            value (UInt256):

        Raises:
            Exception: when `value` is not of neocore.UInt256 type.
        """
        if type(value) is UInt256:
            value.Serialize(self)
        else:
            raise Exception("Cannot write value that is not UInt256")

    def WriteVarInt(self, value, endian="<"):
        """
        Write an integer value in a space saving way to the stream.
        Read more about variable size encoding here: http://docs.neo.org/en-us/node/network-protocol.html#convention

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

        Raises:
            TypeError: if `value` is not of type int.
            Exception: if `value` is < 0.

        Returns:
            int: the number of bytes written.
        """
        if not isinstance(value, int):
            raise TypeError('%s not int type.' % value)

        if value < 0:
            raise Exception('%d too small.' % value)

        elif value < 0xfd:
            return self.WriteByte(value)

        elif value <= 0xffff:
            self.WriteByte(0xfd)
            return self.WriteUInt16(value, endian)

        elif value <= 0xFFFFFFFF:
            self.WriteByte(0xfe)
            return self.WriteUInt32(value, endian)

        else:
            self.WriteByte(0xff)
            return self.WriteUInt64(value, endian)

    def WriteVarBytes(self, value, endian="<"):
        """
        Write an integer value in a space saving way to the stream.
        Read more about variable size encoding here: http://docs.neo.org/en-us/node/network-protocol.html#convention

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

        Returns:
            int: the number of bytes written.
        """
        length = len(value)
        self.WriteVarInt(length, endian)

        return self.WriteBytes(value, unhex=False)

    def WriteVarString(self, value, encoding="utf-8"):
        """
        Write a string value to the stream.
        Read more about variable size encoding here: http://docs.neo.org/en-us/node/network-protocol.html#convention

        Args:
            value (string): value to write to the stream.
            encoding (str): string encoding format.
        """
        if type(value) is str:
            value = value.encode(encoding)

        length = len(value)
        ba = bytearray(value)
        byts = binascii.hexlify(ba)
        string = byts.decode(encoding)
        self.WriteVarInt(length)
        self.WriteBytes(string)

    def WriteFixedString(self, value, length):
        """
        Write a string value to the stream.

        Args:
            value (str): value to write to the stream.
            length (int): length of the string to write.
        """
        towrite = value.encode('utf-8')
        slen = len(towrite)
        if slen > length:
            raise Exception("string longer than fixed length: %s " % length)
        self.WriteBytes(towrite)
        diff = length - slen

        while diff > 0:
            self.WriteByte(0)
            diff -= 1

    def WriteSerializableArray(self, array):
        """
        Write an array of serializable objects to the stream.

        Args:
            array(list): a list of serializable objects. i.e. extending neo.IO.Mixins.SerializableMixin
        """
        if array is None:
            self.WriteByte(0)
        else:
            self.WriteVarInt(len(array))
            for item in array:
                item.Serialize(self)

    def Write2000256List(self, arr):
        """
        Write an array of 64 byte items to the stream.

        Args:
            arr (list): a list of 2000 items of 64 bytes in size.
        """
        for item in arr:
            ba = bytearray(binascii.unhexlify(item))
            ba.reverse()
            self.WriteBytes(ba)

    def WriteHashes(self, arr):
        """
        Write an array of hashes to the stream.

        Args:
            arr (list): a list of 32 byte hashes.
        """
        length = len(arr)
        self.WriteVarInt(length)
        for item in arr:
            ba = bytearray(binascii.unhexlify(item))
            ba.reverse()
            #            logger.info("WRITING HASH %s " % ba)
            self.WriteBytes(ba)

    def WriteFixed8(self, value, unsigned=False):
        """
        Write a Fixed8 value to the stream.

        Args:
            value (neo.Fixed8):
            unsigned: (Not used)

        Returns:
            int: the number of bytes written
        """
        #        if unsigned:
        #            return self.WriteUInt64(int(value.value))
        return self.WriteInt64(value.value)