import asyncio
import binascii
import logging
from enum import Enum

from airtouch5py.data_packet_factory import DataPacketFactory

from airtouch5py.packet_encoder import PacketEncoder
from airtouch5py.packet_reader import PacketReader

from airtouch5py.packets.datapacket import DataPacket

_LOGGER = logging.getLogger(__name__)


class Airtouch5ConnectionStateChange(Enum):
    CONNECTED = 1
    DISCONNECTED = 2


class Airtouch5Client:
    """
    The "Raw" (Hard mode) Airtouch 5 Client.
    You should probably use Airtouch5SimpleClient instead.

    Usage:
    Construct, call connect.
    Wait on packets_received to receive a CONNECTED message.
    Wait on updates from packets_received, use send_packet to send packets.

    Call disconnect to disconnect.

    When a DISCONNECTED message is received, call connect to reconnect.
    """

    _encoder = PacketEncoder()
    _packet_reader = PacketReader()

    ip: str
    packets_received: asyncio.Queue[Airtouch5ConnectionStateChange | DataPacket]

    _reader: asyncio.StreamReader | None
    _writer: asyncio.StreamWriter | None

    _disconnect_lock: asyncio.Lock

    def __init__(self, ip: str):
        self.ip = ip
        self.packets_received = asyncio.Queue()
        self.data_packet_factory = DataPacketFactory()

        self._connected = False
        self._should_be_connected = False
        self._writer, self._reader = None, None
        self._disconnect_lock = asyncio.Lock()

    async def connect(self):
        """
        Connect to the airtouch 5.
        Times out after 5 seconds.
        Throws if we fail to connect.
        Otherwise puts a connected message in the queue and starts up the reader task.
        """

        # Clear the queue before we connect (Might be dangling stuff in it from a previous connection)
        while not self.packets_received.empty():
            self.packets_received.get_nowait()

        _LOGGER.info(f"Connecting to {self.ip}:9005")
        self._reader, self._writer = await asyncio.wait_for(
            asyncio.open_connection(self.ip, 9005), 5
        )
        _LOGGER.info(f"Connected to {self.ip}:9005")

        self.packets_received.put_nowait(Airtouch5ConnectionStateChange.CONNECTED)
        self._reader_task = asyncio.create_task(self._read_packets())

    async def disconnect(self):
        """
        Disconnect the socket if it is connected.
        If stop_reader_task is true, also stop the reader task (Normally you should do this, unless called from the reader task)
        Pushes a DISCONNECTED message in to the queue if we actually disconnected (were connected before)
        """
        async with self._disconnect_lock:
            did_disconnect = False
            if self._writer is not None:
                did_disconnect = True
                self._writer.close()
                try:
                    await self._writer.wait_closed()
                except Exception:
                    # Ignore exceptions when closing
                    _LOGGER.debug("Exception when closing writer", exc_info=True)

            if self._reader_task is not None:
                self._reader_task.cancel()
                self._reader_task = None

            self._writer, self._reader = None, None
            if did_disconnect:
                self.packets_received.put_nowait(
                    Airtouch5ConnectionStateChange.DISCONNECTED
                )

    async def _read_packets(self):
        """
        Continuously read packets from the socket and put them in the queue.
        If we detect the socket has died, disconnect it
        """
        reader = self._reader
        if reader is None:
            raise Exception("Reader is None")

        try:
            while reader.at_eof() == False:
                read = await reader.read(1024)
                _LOGGER.debug(f"Received data: {binascii.hexlify(read)}")
                packets = self._packet_reader.read(read)
                for packet in packets:
                    self.packets_received.put_nowait(packet)
        except Exception as e:
            _LOGGER.error(f"Exception in reader task: {e}")

        # If we've finished reading for some reason, we should disconnect
        # Clear our task first so we don't get cancelled
        self._reader_task = None
        await self.disconnect()

    async def send_packet(self, packet: DataPacket):
        """
        Send the given packet to the airtouch 5.
        Throws if we aren't connected or if there is a connection issue
        """
        writer = self._writer
        if writer is None:
            raise Exception("Writer is None")

        try:
            data = self._encoder.encode(packet)
            _LOGGER.debug(f"Sending data: {binascii.hexlify(data)}")
            writer.write(data)
            await writer.drain()
        except Exception as e:
            _LOGGER.error(f"Exception when sending packet: {e}")
            await self.disconnect()
            raise e
