# -*- coding: utf-8 -*-
#
# Copyright (c) The PyAMF Project.
# See LICENSE.txt for details.

"""
Provides the pure Python versions of L{BufferedByteStream}.

Do not reference directly, use L{pyamf.util.BufferedByteStream} instead.

@since: 0.6
"""

import struct

try:
    from cStringIO import StringIO as BytesIO
except ImportError:
    from io import BytesIO

from pyamf import python

# worked out a little further down
SYSTEM_ENDIAN = None


class BytesIOProxy(object):
    """
    I am a C{BytesIO} type object containing byte data from the AMF stream.
    @see: U{ByteArray on OSFlash
        <http://osflash.org/documentation/amf3#x0c_-_bytearray>}
    @see: U{Parsing ByteArrays on OSFlash
        <http://osflash.org/documentation/amf3/parsing_byte_arrays>}
    """

    def __init__(self, buf=None):
        """
        @raise TypeError: Unable to coerce C{buf} to C{BytesIO}.
        """
        self._buffer = BytesIO()

        if isinstance(buf, bytes):
            self._buffer.write(buf)
        elif isinstance(buf, str):
            self._buffer.write(buf.encode('utf-8'))
        elif hasattr(buf, 'getvalue'):
            self._buffer.write(buf.getvalue())
        elif (
                        hasattr(buf, 'read') and
                        hasattr(buf, 'seek') and
                        hasattr(buf, 'tell')):
            old_pos = buf.tell()
            buf.seek(0)
            self._buffer.write(buf.read())
            buf.seek(old_pos)
        elif buf is not None:
            raise TypeError("Unable to coerce buf->BytesIO got %r" % (buf,))

        self._get_len()
        self._len_changed = False
        self._buffer.seek(0, 0)

    def getvalue(self):
        """
        Get raw data from buffer.
        """
        return self._buffer.getvalue()

    def read(self, n=-1):
        """
        Reads C{n} bytes from the stream.
        """
        if n < -1:
            raise IOError('Cannot read backwards')

        bytes = self._buffer.read(n)

        return bytes

    def seek(self, pos, mode=0):
        """
        Sets the file-pointer offset, measured from the beginning of this
        stream, at which the next write operation will occur.
        @param pos:
        @type pos: C{int}
        @param mode:
        @type mode: C{int}
        """
        return self._buffer.seek(pos, mode)

    def tell(self):
        """
        Returns the position of the stream pointer.
        """
        return self._buffer.tell()

    def truncate(self, size=0):
        """
        Truncates the stream to the specified length.
        @param size: The length of the stream, in bytes.
        @type size: C{int}
        """
        if size == 0:
            self._buffer = BytesIO()
            self._len_changed = True

            return

        cur_pos = self.tell()
        self.seek(0)
        buf = self.read(size)
        self._buffer = BytesIO()

        self._buffer.write(buf)
        self.seek(cur_pos)
        self._len_changed = True

    def write(self, s, size=None):
        """
        Writes the content of the specified C{s} into this buffer.
        @param s: Raw bytes
        """
        if isinstance(s, str):
            s = s.encode()
        self._buffer.write(s)
        self._len_changed = True

    def _get_len(self):
        """
        Return total number of bytes in buffer.
        """
        if hasattr(self._buffer, 'len'):
            self._len = self._buffer.len

            return

        old_pos = self._buffer.tell()
        self._buffer.seek(0, 2)

        self._len = self._buffer.tell()
        self._buffer.seek(old_pos)

    def __len__(self):
        if not self._len_changed:
            return self._len

        self._get_len()
        self._len_changed = False

        return self._len

    def consume(self):
        """
        Chops the tail off the stream starting at 0 and ending at C{tell()}.
        The stream pointer is set to 0 at the end of this function.
        @since: 0.4
        """
        try:
            bytes = self.read()
        except IOError:
            bytes = ''

        self.truncate()

        if len(bytes) > 0:
            self.write(bytes)
            self.seek(0)


class DataTypeMixIn(object):
    """
    Provides methods for reading and writing basic data types for file-like
    objects.
    @ivar endian: Byte ordering used to represent the data. Default byte order
        is L{ENDIAN_NETWORK}.
    @type endian: C{str}
    """

    #: Network byte order
    ENDIAN_NETWORK = "!"

    #: Native byte order
    ENDIAN_NATIVE = "@"

    #: Little endian
    ENDIAN_LITTLE = "<"

    #: Big endian
    ENDIAN_BIG = ">"

    endian = ENDIAN_NETWORK

    def _read(self, length):
        """
        Reads C{length} bytes from the stream. If an attempt to read past the
        end of the buffer is made, L{IOError} is raised.
        """
        bytes = self.read(length)

        if len(bytes) != length:
            self.seek(0 - len(bytes), 1)

            raise IOError("Tried to read %d byte(s) from the stream" % length)

        return bytes

    def _is_big_endian(self):
        """
        Whether the current endian is big endian.
        """
        if self.endian == DataTypeMixIn.ENDIAN_NATIVE:
            return SYSTEM_ENDIAN == DataTypeMixIn.ENDIAN_BIG

        return self.endian in (
            DataTypeMixIn.ENDIAN_BIG,
            DataTypeMixIn.ENDIAN_NETWORK
        )

    def read_uchar(self):
        """
        Reads an C{unsigned char} from the stream.
        """
        return ord(self._read(1))

    def write_uchar(self, c):
        """
        Writes an C{unsigned char} to the stream.
        @param c: Unsigned char
        @type c: C{int}
        @raise TypeError: Unexpected type for int C{c}.
        @raise OverflowError: Not in range.
        """
        if type(c) not in python.int_types:
            raise TypeError('expected an int (got:%r)' % type(c))

        if not 0 <= c <= 255:
            raise OverflowError("Not in range, %d" % c)

        self.write(struct.pack("B", c))

    def read_char(self):
        """
        Reads a C{char} from the stream.
        """
        return struct.unpack("b", self._read(1))[0]

    def write_char(self, c):
        """
        Write a C{char} to the stream.
        @param c: char
        @type c: C{int}
        @raise TypeError: Unexpected type for int C{c}.
        @raise OverflowError: Not in range.
        """
        if type(c) not in python.int_types:
            raise TypeError('expected an int (got:%r)' % type(c))

        if not -128 <= c <= 127:
            raise OverflowError("Not in range, %d" % c)

        self.write(struct.pack("b", c))

    def read_ushort(self):
        """
        Reads a 2 byte unsigned integer from the stream.
        """
        return struct.unpack("%sH" % self.endian, self._read(2))[0]

    def write_ushort(self, s):
        """
        Writes a 2 byte unsigned integer to the stream.
        @param s: 2 byte unsigned integer
        @type s: C{int}
        @raise TypeError: Unexpected type for int C{s}.
        @raise OverflowError: Not in range.
        """
        if type(s) not in python.int_types:
            raise TypeError('expected an int (got:%r)' % (type(s),))

        if not 0 <= s <= 65535:
            raise OverflowError("Not in range, %d" % s)

        self.write(struct.pack("%sH" % self.endian, s))

    def read_short(self):
        """
        Reads a 2 byte integer from the stream.
        """
        return struct.unpack("%sh" % self.endian, self._read(2))[0]

    def write_short(self, s):
        """
        Writes a 2 byte integer to the stream.
        @param s: 2 byte integer
        @type s: C{int}
        @raise TypeError: Unexpected type for int C{s}.
        @raise OverflowError: Not in range.
        """
        if type(s) not in python.int_types:
            raise TypeError('expected an int (got:%r)' % (type(s),))

        if not -32768 <= s <= 32767:
            raise OverflowError("Not in range, %d" % s)

        self.write(struct.pack("%sh" % self.endian, s))

    def read_ulong(self):
        """
        Reads a 4 byte unsigned integer from the stream.
        """
        return struct.unpack("%sL" % self.endian, self._read(4))[0]

    def write_ulong(self, l):
        """
        Writes a 4 byte unsigned integer to the stream.
        @param l: 4 byte unsigned integer
        @type l: C{int}
        @raise TypeError: Unexpected type for int C{l}.
        @raise OverflowError: Not in range.
        """
        if type(l) not in python.int_types:
            raise TypeError('expected an int (got:%r)' % (type(l),))

        if not 0 <= l <= 4294967295:
            raise OverflowError("Not in range, %d" % l)

        self.write(struct.pack("%sL" % self.endian, l))

    def read_long(self):
        """
        Reads a 4 byte integer from the stream.
        """
        return struct.unpack("%sl" % self.endian, self._read(4))[0]

    def write_long(self, l):
        """
        Writes a 4 byte integer to the stream.
        @param l: 4 byte integer
        @type l: C{int}
        @raise TypeError: Unexpected type for int C{l}.
        @raise OverflowError: Not in range.
        """
        if type(l) not in python.int_types:
            raise TypeError('expected an int (got:%r)' % (type(l),))

        if not -2147483648 <= l <= 2147483647:
            raise OverflowError("Not in range, %d" % l)

        self.write(struct.pack("%sl" % self.endian, l))

    def read_24bit_uint(self):
        """
        Reads a 24 bit unsigned integer from the stream.
        @since: 0.4
        """
        order = None

        if not self._is_big_endian():
            order = [0, 8, 16]
        else:
            order = [16, 8, 0]

        n = 0

        for x in order:
            n += (self.read_uchar() << x)

        return n

    def write_24bit_uint(self, n):
        """
        Writes a 24 bit unsigned integer to the stream.
        @since: 0.4
        @param n: 24 bit unsigned integer
        @type n: C{int}
        @raise TypeError: Unexpected type for int C{n}.
        @raise OverflowError: Not in range.
        """
        if type(n) not in python.int_types:
            raise TypeError('expected an int (got:%r)' % (type(n),))

        if not 0 <= n <= 0xffffff:
            raise OverflowError("n is out of range")

        order = None

        if not self._is_big_endian():
            order = [0, 8, 16]
        else:
            order = [16, 8, 0]

        for x in order:
            self.write_uchar((n >> x) & 0xff)

    def read_24bit_int(self):
        """
        Reads a 24 bit integer from the stream.
        @since: 0.4
        """
        n = self.read_24bit_uint()

        if n & 0x800000 != 0:
            # the int is signed
            n -= 0x1000000

        return n

    def write_24bit_int(self, n):
        """
        Writes a 24 bit integer to the stream.
        @since: 0.4
        @param n: 24 bit integer
        @type n: C{int}
        @raise TypeError: Unexpected type for int C{n}.
        @raise OverflowError: Not in range.
        """
        if type(n) not in python.int_types:
            raise TypeError('expected an int (got:%r)' % (type(n),))

        if not -8388608 <= n <= 8388607:
            raise OverflowError("n is out of range")

        order = None

        if not self._is_big_endian():
            order = [0, 8, 16]
        else:
            order = [16, 8, 0]

        if n < 0:
            n += 0x1000000

        for x in order:
            self.write_uchar((n >> x) & 0xff)

    def read_double(self):
        """
        Reads an 8 byte float from the stream.
        """
        return struct.unpack("%sd" % self.endian, self._read(8))[0]

    def write_double(self, d):
        """
        Writes an 8 byte float to the stream.
        @param d: 8 byte float
        @type d: C{float}
        @raise TypeError: Unexpected type for float C{d}.
        """
        if not type(d) is float:
            raise TypeError('expected a float (got:%r)' % (type(d),))

        self.write(struct.pack("%sd" % self.endian, d))

    def read_float(self):
        """
        Reads a 4 byte float from the stream.
        """
        return struct.unpack("%sf" % self.endian, self._read(4))[0]

    def write_float(self, f):
        """
        Writes a 4 byte float to the stream.
        @param f: 4 byte float
        @type f: C{float}
        @raise TypeError: Unexpected type for float C{f}.
        """
        if type(f) is not float:
            raise TypeError('expected a float (got:%r)' % (type(f),))

        self.write(struct.pack("%sf" % self.endian, f))

    def read_utf8_string(self, length):
        """
        Reads a UTF-8 string from the stream.
        @rtype: C{unicode}
        """
        s = struct.unpack("%s%ds" % (
            self.endian, length),
                          self.read(length)
                          )

        return s[0].decode('utf-8')

    def write_utf8_string(self, u):
        """
        Writes a unicode object to the stream in UTF-8.
        @param u: unicode object
        @raise TypeError: Unexpected type for str C{u}.
        """
        if not isinstance(u, python.str_types):
            raise TypeError('Expected %r, got %r' % (python.str_types, u))

        bytes = u

        if isinstance(bytes, str):
            bytes = u.encode("utf8")

        self.write(struct.pack("%s%ds" % (self.endian, len(bytes)), bytes))


class BufferedByteStream(BytesIOProxy, DataTypeMixIn):
    """
    An extension of C{StringIO}.

    Features:
     - Raises L{IOError} if reading past end.
     - Allows you to C{peek()} into the stream.
    """

    def __init__(self, buf=None, min_buf_size=None):
        """
        @param buf: Initial byte stream.
        @type buf: C{str} or C{StringIO} instance
        @param min_buf_size: Ignored in the pure Python version.
        """
        BytesIOProxy.__init__(self, buf=buf)

    def read(self, length=-1):
        """
        Reads up to the specified number of bytes from the stream into
        the specified byte array of specified length.

        @raise IOError: Attempted to read past the end of the buffer.
        """
        if length == -1 and self.at_eof():
            raise IOError(
                'Attempted to read from the buffer but already at the end')
        elif length > 0 and self.tell() + length > len(self):
            raise IOError(
                'Attempted to read %d bytes from the buffer but only %d '
                'remain' % (length, len(self) - self.tell())
            )

        return BytesIOProxy.read(self, length)

    def peek(self, size=1):
        """
        Looks C{size} bytes ahead in the stream, returning what it finds,
        returning the stream pointer to its initial position.

        @param size: Default is 1.
        @type size: C{int}
        @raise ValueError: Trying to peek backwards.

        @return: Bytes.
        """
        if size == -1:
            return self.peek(len(self) - self.tell())

        if size < -1:
            raise ValueError("Cannot peek backwards")

        bytes = b''
        pos = self.tell()

        while not self.at_eof() and len(bytes) != size:
            bytes += self.read(1)

        self.seek(pos)

        return bytes

    def remaining(self):
        """
        Returns number of remaining bytes.

        @rtype: C{number}
        @return: Number of remaining bytes.
        """
        return len(self) - self.tell()

    def at_eof(self):
        """
        Returns C{True} if the internal pointer is at the end of the stream.

        @rtype: C{bool}
        """
        return self.tell() == len(self)

    def append(self, data):
        """
        Append data to the end of the stream. The pointer will not move if
        this operation is successful.

        @param data: The data to append to the stream.
        @type data: C{str} or C{unicode}
        @raise TypeError: data is not C{str} or C{unicode}
        """
        t = self.tell()

        # seek to the end of the stream
        self.seek(0, 2)

        if hasattr(data, 'getvalue'):
            self.write_utf8_string(data.getvalue())
        else:
            self.write_utf8_string(data)

        self.seek(t)

    def __add__(self, other):
        old_pos = self.tell()
        old_other_pos = other.tell()

        new = BufferedByteStream(self)

        other.seek(0)
        new.seek(0, 2)
        new.write(other.read())

        self.seek(old_pos)
        other.seek(old_other_pos)
        new.seek(0)

        return new


def is_float_broken():
    """
    Older versions of Python (<=2.5) and the Windows platform are renowned for
    mixing up 'special' floats. This function determines whether this is the
    case.

    @since: 0.4
    @rtype: C{bool}
    @return: Boolean indicating whether floats are broken on this platform.
    """
    return str(python.NaN) != str(
        struct.unpack("!d", b'\xff\xf8\x00\x00\x00\x00\x00\x00')[0])


# init the module from here ..

if is_float_broken():
    def read_double_workaround(self):
        """
        Override the L{DataTypeMixIn.read_double} method to fix problems
        with doubles by using the third-party C{fpconst} library.
        """
        bytes = self.read(8)

        if self._is_big_endian():
            if bytes == '\xff\xf8\x00\x00\x00\x00\x00\x00':
                return python.NaN

            if bytes == '\xff\xf0\x00\x00\x00\x00\x00\x00':
                return python.NegInf

            if bytes == '\x7f\xf0\x00\x00\x00\x00\x00\x00':
                return python.PosInf
        else:
            if bytes == '\x00\x00\x00\x00\x00\x00\xf8\xff':
                return python.NaN

            if bytes == '\x00\x00\x00\x00\x00\x00\xf0\xff':
                return python.NegInf

            if bytes == '\x00\x00\x00\x00\x00\x00\xf0\x7f':
                return python.PosInf

        return struct.unpack("%sd" % self.endian, bytes)[0]

    DataTypeMixIn.read_double = read_double_workaround

    def write_double_workaround(self, d):
        """
        Override the L{DataTypeMixIn.write_double} method to fix problems
        with doubles by using the third-party C{fpconst} library.

        @raise TypeError: Unexpected type for float C{d}.
        """
        if type(d) is not float:
            raise TypeError('expected a float (got:%r)' % (type(d),))

        if python.isNaN(d):
            if self._is_big_endian():
                self.write('\xff\xf8\x00\x00\x00\x00\x00\x00')
            else:
                self.write('\x00\x00\x00\x00\x00\x00\xf8\xff')
        elif python.isNegInf(d):
            if self._is_big_endian():
                self.write('\xff\xf0\x00\x00\x00\x00\x00\x00')
            else:
                self.write('\x00\x00\x00\x00\x00\x00\xf0\xff')
        elif python.isPosInf(d):
            if self._is_big_endian():
                self.write('\x7f\xf0\x00\x00\x00\x00\x00\x00')
            else:
                self.write('\x00\x00\x00\x00\x00\x00\xf0\x7f')
        else:
            write_double_workaround.old_func(self, d)

    x = DataTypeMixIn.write_double
    DataTypeMixIn.write_double = write_double_workaround
    write_double_workaround.old_func = x


if struct.pack('@H', 1)[0] == '\x01':
    SYSTEM_ENDIAN = DataTypeMixIn.ENDIAN_LITTLE
else:
    SYSTEM_ENDIAN = DataTypeMixIn.ENDIAN_BIG
