import asyncio
import socket
import struct
import zlib

from libwavesync import Packetizer, AudioConfig
from libwavesync import time_machine

class Receiver(asyncio.DatagramProtocol):
    """
    Packet receiver

    - Receive packets
    - decode headers
    - store in chunk list.
    """

    def __init__(self, chunk_queue, channel, sink_latency_ms, stats):
        self.stats = stats

        # Store config
        self.channel = channel

        self.chunk_queue = chunk_queue

        # Audio configuration sent by transmitter
        self.audio_config = None
        self.sink_latency_ms = sink_latency_ms

        super().__init__()

    def connection_made(self, transport):
        "Configure multicast"
        sock = transport.get_extra_info('socket')
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

        # Check if address is multicast and join group.
        group, port = self.channel

        multicast = True
        octets = group.split('.')

        # Received audio chunk counter
        self.chunk_queue.init_queue()

        if len(octets) != 4:
            multicast = False
        else:
            try:
                octet_0 = int(octets[0])
                if not 224 <= octet_0 <= 239:
                    multicast = False
            except ValueError:
                multicast = False

        # If not multicast - end
        if multicast is False:
            print("Assuming unicast reception on %s:%d" % (group, port))
            return

        # Multicast - join group
        print("Joining multicast group", group)

        group = socket.inet_aton(group)
        mreq = struct.pack('4sL', group, socket.INADDR_ANY)
        sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)

    def _handle_status(self, data):

        if len(data) < (2 + 20):
            print("WARNING: Status header too short")

        (sender_timestamp,
         sender_chunk_no,
         rate, sample,
         channels,
         chunk_size,
         latency_ms) = struct.unpack('dIHBBHH',
                                     data[2:2 + 8+4+2+1+1+2+2])

        q = self.chunk_queue

        # Handle timestamp
        now = time_machine.now()
        self.stats.network_latency = (now - sender_timestamp)

        # Handle audio configuration
        audio_config = AudioConfig(rate, sample, channels,
                                   latency_ms,
                                   sink_latency_ms=self.sink_latency_ms)
        audio_config.chunk_size = chunk_size

        if audio_config != self.audio_config:
            # If changed - sent further
            q.chunk_list.append((q.CMD_CFG, audio_config))
            self.audio_config = audio_config

        # Handle dropped packets

        # If this is first status packet
        # or low sender_chunk_no indicates that sender was restarted
        if q.last_sender_chunk_no is None or sender_chunk_no < 1500:
            q.last_sender_chunk_no = sender_chunk_no
            q.chunk_no = 0
            return

        # How many chunks were transmitted since previous status packet?
        chunks_sent = sender_chunk_no - q.last_sender_chunk_no
        dropped = chunks_sent - q.chunk_no

        q.last_sender_chunk_no = sender_chunk_no
        q.chunk_no = 0

        self.stats.network_drops += dropped
        if dropped < 0:
            print("WARNING: More pkts received than sent! "
                  "You are receiving multiple streams or duplicates.")
        elif dropped > 0:
            q.chunk_list.append((q.CMD_DROPS, dropped))
            q.chunk_available.set()

    def datagram_received(self, data, addr):
        "Handle incoming datagram - audio chunk, or status packet"
        header = data[:2]
        mark = data[2:4]
        chunk = data[4:]
        if header == Packetizer.HEADER_RAW_AUDIO:
            pass
        elif header == Packetizer.HEADER_COMPRESSED_AUDIO:
            try:
                chunk = zlib.decompress(chunk)
            except zlib.error:
                print("WARNING: Invalid compressed data - dropping")
                return
        elif header == Packetizer.HEADER_STATUS:
            # Status header!
            self._handle_status(data)
            return
        else:
            print("Invalid header!")
            return

        if self.chunk_queue.ignore_audio_packets != 0:
            self.chunk_queue.ignore_audio_packets -= 1
            return

        mark = time_machine.to_absolute_timestamp(time_machine.now(),
                                                  mark)
        item = (mark, chunk)

        # Count received audio-chunks
        self.chunk_queue.chunk_no += 1

        self.chunk_queue.chunk_list.append((self.chunk_queue.CMD_AUDIO, item))
        self.chunk_queue.chunk_available.set()

    def error_received(self, exc):
        print('Error received:', exc)

    def connection_lost(self, exc):
        print("Socket closed, stop the event loop")
        loop = asyncio.get_event_loop()
        loop.stop()