# -*- coding: utf-8 -*-
from collections import deque

from ..backend.utils import WeakReferer
from ..backend.futures import Task, CancelledError, sleep

from .opus import OpusDecoder, opus
from .player import AudioSource
from .core import KOKORO

if opus is None:
    DECODER = None
else:
    DECODER = OpusDecoder()

EMPTY_VOICE_FRAME_ENCODED = b'\xf8\xff\xfe'
EMPTY_VOICE_FRAME_DECODED = b'\x00'*3840

class Array_uint_32b: #TODO : ask python to implement arrays already
    """
    Implements an uint32 array casted on bytes.
    
    Attributes
    ----------
    _data : `bytes`
        The source `bytes` object.
    _offset : `int`
        The first byte what is inside of the array.
    _limit : `int`
        The first byte, what is not inside of the array after `._offset`.
    """
    __slots__ = ('_data', '_offset', '_limit')
    def __init__(self, data, offset, limit):
        """
        Creates a new uint32 array from the given parameters.
        
        Parameters
        ----------
        data : `bytes`
            The source `bytes` object.
        offset : `int`
            The first byte what is inside of the array.
        limit : `int`
            The first byte, what is not inside of the array after `._offset`.
        """
        self._data = data
        self._offset = offset
        self._limit = limit
    
    def __len__(self):
        """Returns the array's length"""
        limit = self._limit
        offset = self._offset
        value = (limit-offset)>>2
        
        return value
    
    def __getitem__(self, index):
        """Returns the element of the array at the given index."""
        index = index<<2
        offset = self._offset+index
        value = int.from_bytes(self._data[offset:offset+4], 'big')
        
        return value
    
    def __iter__(self):
        """Iterated over the array's elements."""
        data = self._data
        offset = self._offset
        limit = self._limit
        
        while True:
            if offset == limit:
                break
            value = int.from_bytes(data[offset:offset+4], 'big')
            yield value
            
            offset = offset+4

class PacketBase:
    """
    Base class for packet subclasses.
    """
    __slots__ = ()

class VoicePacket(PacketBase):
    """
    Represents a voice packet.
    
    Attributes
    ----------
    decoded : `str`
    """
    __slots__ = ('decoded', 'encoded')
    
    def __init__(self, data):
        """
        Creates a new ``VoicePacket`` from the given data.
        
        Parameters
        ----------
        data : `bytes`
            Not yet decoded voice data.
        """
        self.encoded = data
        self.decoded = None
    
    def __repr__(self):
        """ Returns the voice packet's representation."""
        return (f'<{self.__class__.__name__} decoded={self.decoded is not None}>')

# http://www.rfcreader.com/#rfc3550_line548
class RTPPacket(PacketBase):
    """
    Represents an RTP packet: http://www.rfcreader.com/#rfc3550_line548.
    
    Attributes
    ----------
    _data : `bytes`
        The raw data of the packet on what it is casted on.
    _offset1 : `int`
        Offset of the received data marking the end of the CSRC identifiers and the start of the encrypted data.
    _offset2 : `int`
        Offset of the decrypted data marking the end of the extensions (?) and the start of the decrypted data.
    _decrypted : `bytes`
        Decrypted data of the RTP packet.
    """
    __slots__ = ('_data', '_offset1', '_offset2', '_decrypted',)
    
    def __init__(self, data, voice_client):
        self._data =data
        offset = 12
        cc = self.cc
        if cc:
            offset = offset+(cc<<2)
        self._offset1 = offset
        
        nonce = data[:12]+b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
        decrypted = voice_client._secret_box.decrypt(data[offset:], nonce)
        self._decrypted = decrypted
        
        if self.extended:
            extension=int.from_bytes(decrypted[2:4], 'big')
            offset = (extension<<2)+4
        else:
            offset = 0
        
        self._offset2 = offset
    
    @property
    def data(self):
        return memoryview(self._data)[self._offset1:]
    
    @property
    def decrypted(self):
        return memoryview(self._decrypted)[self._offset2:]
    
    @property
    def csrcs(self):
        return Array_uint_32b(self._data, 12, self._offset1)
    
    @property
    def extension_profile(self):
        if not self.extended:
            return 0
        
        profile = int.from_bytes(self._decrypted[0:2], 'big')
        return profile
    
    @property
    def extension_length(self):
        if not self.extended:
            return 0
        
        length = int.from_bytes(self._decrypted[2:4], 'big')
        return length
    
    @property
    def extension_values(self):
        limit = self._offset2
        if limit <= 4:
            return None
        return Array_uint_32b(self._decrypted, 4, limit)
    
    @property
    def version(self):
        return self._data[0]>>6
    
    @property
    def padding(self):
        return (self._data[0]&0b00100000)>>5
    
    @property
    def extended(self):
        return (self._data[0]&0b00010000)>>4
    
    @property
    def cc(self):
        return self._data[0]&0b00001111
    
    @property
    def marker(self):
        return (self._data[1]&0b10000000)>>7
    
    @property
    def payload(self):
        return self._data[1]&0b01111111
    
    @property
    def timestamp(self):
        return int.from_bytes(self._data[4:8], 'big')
    
    @property
    def source(self):
        return int.from_bytes(self._data[8:12], 'big')
    
    @property
    def sequence(self):
        return int.from_bytes(self._data[2:4], 'big')
    
    @property
    def header(self):
        return memoryview(self._data)[:12]

    def __repr__(self):
        return (f'<{self.__class__.__name__} timestamp={self.timestamp}, source={self.source}, sequence='
            f'{self.sequence}, size={len(self.data)}>')

#NOT USED

###http://www.rfcreader.com/#rfc3550_line855
##class RTCPPacket(PacketBase):
##    __slots__ = ('_data')
##    type = 0
##
##    def __init__(self,data):
##        self._data = data
##
##    @property
##    def version(self):
##        return self._data[0]>>6
##
##    @property
##    def padding(self):
##        return (self._data[0]&0b00100000)>>5
##
##    @property
##    def length(self):
##        return int.from_bytes(self._data[2:4], 'big')
##
##    @property
##    def report_count(self):
##        return self._data[0]&0b00011111
##
##    @property
##    def source(self):
##        return int.from_bytes(self._data[4:8], 'big')
##
##class _RPReport(self):
##    __slots__ = ('_data','_offset')
##    def __init__(self, data, offset):
##        self._data = data
##        self._offset = offset
##
##    @property
##    def source(self):
##        offset = self._offset
##        return int.from_bytes(self._data[offset:offset+4], 'big')
##
##    @property
##    def loss_percent(self):
##        offset = self._offset
##        return self._data[offset+4]
##
##    @property
##    def loss_total(self):
##        offset = self._offset+5
##        return int.from_bytes(self._data[offset:offset+3], 'big')
##
##    @property
##    def last_sequence(self):
##        offset = self._offset+8
##        return int.from_bytes(self._data[offset:offset+4], 'big')
##
##    @property
##    def jitter(self):
##        offset = self._offset+12
##        return int.from_bytes(self._data[offset:offset+4], 'big')
##
##    @property
##    def lsr(self):
##        offset = self._offset+16
##        return int.from_bytes(self._data[offset:offset+4], 'big')
##
##    @property
##    def dlsr(self):
##        offset = self._offset+20
##        return int.from_bytes(self._data[offset:offset+4], 'big')
##        
###http://www.rfcreader.com/#rfc3550_line1614
##class SenderReportPacket(RTCPPacket):
##    __slots__ = ('_decrypted', '_offset1')
##    type = 200
##    
##    def __init__(self, data, voice_client):
##        data_shard = data[:8]
##        self._data = data_shard
##        
##        nonce = data_shard+b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
##        decrypted = voice_client._secret_box.decrypt(data[8:], nonce)
##        self._decrypted = decrypted
##        
##        report_count = data_shard[0]&0b00011111
##        offset = (report_count*24)+20
##        self._offset1 = offset
##
##    def __repr__(self):
##        return (f'<{self.__class__.__name__} timestamp={self.timestamp}, length={self.length}, reports='
##            f'{self.report_count}>')
##    
##    @property
##    def decrypted(self):
##        return memoryview(self._decrypted)[24:]
##    
##    @property
##    def info_ntp_ts_msw(self):
##        return int.from_bytes(self._decrypted[0:4], 'big')
##
##    @property
##    def info_ntp_ts_lsw(self):
##        return int.from_bytes(self._decrypted[4:8], 'big')
##
##    @property
##    def info_ntp_ts(self):
##        low = int.from_bytes(self._decrypted[4:8], 'big')
##        low = low/(1<<low.bit_length())
##        
##        high = int.from_bytes(self._decrypted[0:4], 'big')
##        
##        return high+low
##
##    @property
##    def info_rtp_ts(self):
##        return int.from_bytes(self._decrypted[8:12], 'big')
##
##    @property
##    def info_sequence(self):
##        return int.from_bytes(self._decrypted[12:16], 'big')
##
##    @property
##    def info_octet(self):
##        return int.from_bytes(self._decrypted[16:20], 'big')
##
##    @property
##    def extension(self):
##        data = self._decrypted
##        offset = self._offset1
##        
##        if len(data)>offset:
##            return memoryview(decrypted)[offset:]
##
##    def report_get(self,index):
##        offset = (index*24)+20
##        obj = _RPReport(self._decrypted, offset)
##        
##        return obj
##
##    def report_iter(self, index):
##        data = self._decrypted
##        limit = self._offset1
##        offset = 20
##        
##        while True:
##            if offset >= limit:
##                break
##            obj = _RPReport(data, offset)
##            yield obj
##            
##            offset += 24
##
###http://www.rfcreader.com/#rfc3550_line1879
##class ReceiverReportPacket(RTCPPacket):
##    __slots__ = ('_decrypted', '_offset1')
##    type = 201
##    
##    def __init__(self,data,voice_client):
##        data_shard = data[:8]
##        self._data = data_shard
##        
##        nonce = data_shard+b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
##        decrypted = voice_client._secret_box.decrypt(data[8:], nonce)
##        self._decrypted = decrypted
##        
##        report_count = data_shard[0]&0b00011111
##        offset = report_count*24
##        self._offset1 = offset
##
##    def __repr__(self):
##        return (f'<{self.__class__.__name__} timestamp={self.timestamp}, length={self.length}, reports='
##            f'{self.report_count}>')
##
##    @property
##    def decrypted(self):
##        return memoryview(self._decrypted)
##    
##    def report_get(self,index):
##        offset = index*24
##        obj = _RPReport(self._decrypted,offset)
##        
##        return obj
##
##    def report_iter(self,index):
##        data = self._decrypted
##        limit = self._offset1
##        offset = 0
##        
##        while True:
##            if offset >= limit:
##                break
##            obj = _RPReport(data, offset)
##            yield obj
##            
##            offset += 24

class AudioStream(AudioSource):
    """
    Represents a received audio stream from Discord.
    
    Attributes
    ----------
    auto_decode : `bool`
        Whether the stream should decode the frames as received.
    buffer : `deque` of `VoicePacket`
        A queue of received voice packets.
    done : `bool`
        Whether the audio stream is stopped.
    client : `WeakReferer`
        Weakreference to the parent ``AudioReader`` to avoid reference loops.
    source : `None` or `int`
        Identifier value of the respective user.
    user : ``User`` or ``Client``
        The user, who's audio is received.
    yield_decoded : `bool`
        Whether the audio stream should yield encoded data.
    
    Class Attributes
    ----------------
    REPEATABLE : `bool` = `False`
        Whether the source can be repeated after it is exhausted once.
    """
    __slots__ = ('auto_decode', 'buffer', 'client', 'done', 'source', 'user', 'yield_decoded')
    
    def __init__(self, voice_client, user, *, auto_decode=False, yield_decoded=False):
        """
        Creates a new audio stream instance.
        
        Parameters
        ----------
        voice_client : ``VoiceClient``
            Parent ``AudioReader`` instance.
        user : ``User`` or ``Client``
            The user, who's audio is received.
        auto_decode : `bool`
            Whether the received packets should be auto decoded.
        yield_decoded : `bool`
            Whether the audio stream should yield decoded data.
        """
        try:
            audio_source = voice_client._audio_sources[user.id]
        except KeyError:
            audio_source = None
        
        self.client = voice_client
        self.buffer = deque()
        self.auto_decode = auto_decode
        self.yield_decoded = yield_decoded
        self.done = False
        self.user = user
        self.source = audio_source
    
    def stop(self):
        """
        Stops the audio stream by marking it as done and un-links it as well.
        """
        if self.done:
            return
        
        self.done = True
        self.client._unlink_audio_stream(self)
    
    async def cleanup(self):
        """
        Cleans the audio stream up.
        
        This method is a coroutine.
        """
        self.stop()
    
    __del__ = stop
    
    @property
    def NEEDS_ENCODE(self):
        """
        Returns whether the audio stream needs encoding when streaming to an audio player.
        
        Returns
        -------
        needs_encode : `bool`
        """
        return self.yield_decoded
    
    def feed(self, packet):
        """
        Adds the given packet to the buffer of the audio stream.
        
        Parameters
        ----------
        packet : ``VoicePacket``
        """
        self.buffer.append(packet)
        
        if self.auto_decode:
            if packet.decoded is None:
                packet.decoded = DECODER.decode(packet.encoded)
    
    async def read(self):
        """
        Reads a frame from the audio stream's buffer.
        
        With yielding `None` indicates end of stream.
        
        This method is a coroutine.
        
        Returns
        -------
        frame : `None` or `bytes`
        """
        buffer = self.buffer
        if buffer:
            packet = buffer.popleft()
            if self.yield_decoded:
                data = packet.decoded
                if data is None:
                    data = DECODER.decode(packet.encoded)
            else:
                data = packet.encoded
        
        else:
            if self.done:
                data = None
            else:
                if self.yield_decoded:
                    data = EMPTY_VOICE_FRAME_DECODED
                else:
                    data = EMPTY_VOICE_FRAME_ENCODED
        
        return data
    
    @property
    def title(self):
        """
        Returns the title of the audio stream.
        
        Returns
        -------
        title : `str`
        """
        return f'{self.__class__.__name__} from {self.user.full_name!r}'

class AudioReader:
    """
    Audio reader of a ``VoiceClient``.
    
    Attributes
    ----------
    audio_streams : `dict` of (`int`, (``AudioStream`` or (`list` of ``AudioStream``))) items
        `source` - ``AudioStream`` relation to store the receiving audio streams.
    client : ``VoiceClient``
        The parent voice client.
    done : `bool`
        Whether the audio reader is done receiving and should stop.
    task : `None` or ``Task``
        Audio reader task. Set as `None` if the reader is stopped.
    """
    __slots__ = ('audio_streams', 'client', 'done', 'task')
    
    def __init__(self, voice_client):
        """
        Creates an ``AudioReader`` instance bound to the given voice client.
        
        Parameters
        ----------
        voice_client : ``VoiceClient``
            The parent voice client.
        """
        self.client = voice_client
        self.done = False
        self.audio_streams = {}
        self.task = Task(self.run(), KOKORO)
    
    async def run(self):
        """
        The main runner of the ``AudioReader`` what keeps reading from the voice client's datagram stream and feeding
        them to the receiver ``AudioStream``-s.
        
        This method is a coroutine.
        """
        voice_client = self.client
        audio_streams = self.audio_streams
        
        try:
            if not voice_client.connected.is_set():
                await voice_client.connected
            
            protocol = voice_client._protocol
            while True:
                
                if self.done:
                    break
                
                if not voice_client.connected.is_set():
                    await voice_client.connected
                    protocol = voice_client._protocol
                
                try:
                    data = await protocol.read_once()
                except CancelledError:
                    if self.done:
                        protocol.cancel_current_reader()
                        return
                    
                    # If cancelled, we are probably establishing connection, so wait till that is done.
                    payload_waiter = protocol.payload_waiter
                    if payload_waiter is None:
                        # Wait some?
                        await sleep(0.2, KOKORO)
                        payload_waiter = protocol.payload_waiter
                        if payload_waiter is None:
                            continue
                    
                    await payload_waiter
                    continue
                
                if not audio_streams:
                    continue
            
                try:
                    if data[1] != 120:
                        # not voice data, we don't care
                        continue
                    
                    packet = RTPPacket(data, voice_client)
                    source = packet.source
                    try:
                        audio_stream = audio_streams[source]
                    except KeyError:
                        pass
                    else:
                        voice_packet = VoicePacket(bytes(packet.decrypted))
                        if type(audio_stream) is list:
                            for audio_stream in audio_stream:
                                audio_stream.feed(voice_packet)
                        else:
                            audio_stream.feed(voice_packet)
                
                except BaseException as err:
                    if isinstance(err, CancelledError) and self.done:
                        return
                    await KOKORO.render_exc_async(err, before=[
                        'Exception occurred at decoding voice packet at\n',
                        repr(self),
                        '\n',
                            ])
        
        except BaseException as err:
            if isinstance(err, CancelledError) and self.done:
                return
            
            await KOKORO.render_exc_async(err, before=[
                'Exception occurred at \n',
                repr(self),
                '\n',
                    ])
        
        self.stop()
    
    def stop(self):
        """
        Stops the streams of the audio player.
        """
        task = self.task
        if task is None:
            return
        
        self.task = None
        self.done = True
        task.cancel()
        
        voice_client = self.client
        if voice_client.reader is self:
            voice_client.reader = None
        
        audio_streams = self.audio_streams
        if audio_streams:
            collected_audio_streams = []
            for audio_stream in audio_streams.values():
                if type(audio_stream) is list:
                    collected_audio_streams.extend(audio_stream)
                else:
                    collected_audio_streams.append(audio_stream)
            
            audio_streams.clear()
            
            while collected_audio_streams:
                audio_stream = collected_audio_streams.pop()
                audio_stream.stop()
    
    __del__ = stop
