"""Switcher water heater unofficial API and bridge, Network protocols.

.. codeauthor:: <NAME> <<EMAIL>>

"""

# fmt: off
from asyncio import (
    AbstractEventLoop, BaseTransport, DatagramProtocol, Event, Future, Queue,
    QueueEmpty, QueueFull, Transport, ensure_future)
from datetime import datetime
from functools import partial
from typing import Optional, Tuple, Union, cast

from .bridge.messages import SwitcherV2BroadcastMSG
from .devices import SwitcherV2Device

# fmt: on


class SwitcherV2UdpProtocolFactory(DatagramProtocol):
    """Represntation of the Asyncio UDP protocol factory.

    Args:
      loop: the event loop for the factory to run in.
      phone_id: the phone id of the desired device.
      device_id: the id of the desired device.
      device_password: the password of the desired device.
      queue: a ``asyncio.Queue`` for the factory to save messages in.
      run_factory_evt ``asyncio.Event`` for signaling the factory to run.

    Todo:
      * replace ``factory_future`` attribute with ``get_factory_future``
        method.

    """

    def __init__(
        self,
        loop: AbstractEventLoop,
        phone_id: str,
        device_id: str,
        device_password: str,
        queue: Queue,
        run_factory_evt: Event,
    ) -> None:
        """Initialize the protocol."""
        self._loop = loop
        self._phone_id = phone_id
        self._device_id = device_id
        self._device_password = <PASSWORD>
        self._queue = queue
        self._run_factory = run_factory_evt
        self._factory_future = self._loop.create_future()
        self._factory_future.add_done_callback(self.close_transport)
        self._accept_datagrams = Event()
        self.transport = None  # type: Optional[Transport]
        self._device = None  # type: Optional[SwitcherV2Device]

    def connection_made(self, transport: BaseTransport) -> None:
        """Call on connection established."""
        self.transport = cast(Transport, transport)
        self._accept_datagrams.set()

    def datagram_received(self, data: Union[bytes, str], addr: Tuple) -> None:
        """Call on datagram recieved."""
        if self._run_factory.is_set() and self._accept_datagrams.is_set():
            ensure_future(
                self.handle_incoming_messages(data, addr), loop=self._loop
            )

    def error_received(self, exc: Optional[Exception]) -> None:
        """Call on exception recieved."""
        if exc:
            self.factory_future.set_exception(exc)
        else:
            self.factory_future.set_result(None)

    def connection_lost(self, exc: Optional[Exception]) -> None:
        """Call on connection lost."""
        self.factory_future.set_result(exc if exc else None)

    def close_transport(self, future: Future) -> None:
        """Call for closing the transport."""
        if self.transport:
            self.transport.close()

    async def handle_incoming_messages(
        self, data: Union[bytes, str], addr: Tuple
    ) -> None:
        """Use for Handling incoming messages."""
        self._accept_datagrams.clear()
        msg = SwitcherV2BroadcastMSG(self._loop, data)
        msg.init_future.add_done_callback(
            partial(self.get_device_from_message, addr[0])
        )

        return None

    def get_device_from_message(self, ip_addr: str, future: Future) -> None:
        """Use for extracting the device from the broadcast message."""
        msg = future.result()
        if msg.verified:
            if self._device_id == msg.device_id:
                if self._device:
                    # Update known device
                    self._device.update_device_data(
                        ip_addr,
                        msg.name,
                        msg.device_state,
                        msg.remaining_time_to_off,
                        msg.auto_off_set,
                        msg.power,
                        msg.current,
                        (
                            self._device.last_state_change
                            if msg.device_state == self._device.state
                            else datetime.now()
                        ),
                    )
                else:
                    # New device discoverd
                    self._device = SwitcherV2Device(
                        msg.device_id,
                        ip_addr,
                        msg.mac_address,
                        msg.name,
                        msg.device_state,
                        msg.remaining_time_to_off,
                        msg.auto_off_set,
                        msg.power,
                        msg.current,
                        self._phone_id.lower(),
                        self._device_password.lower(),
                        datetime.now(),
                    )
                try:
                    self._queue.put_nowait(self._device)
                except QueueFull:
                    try:
                        self._queue.get_nowait()
                    except QueueEmpty:
                        pass
                    ensure_future(
                        self._queue.put(self._device), loop=self._loop
                    )
        self._accept_datagrams.set()

    @property
    def factory_future(self) -> Future:
        """asyncio.Future: Representing the initialization status."""
        return self._factory_future

"""Switcher water heater unofficial API and bridge, Network protocols.

.. codeauthor:: <NAME> <<EMAIL>>

"""

# fmt: off
from asyncio import (
    AbstractEventLoop, BaseTransport, DatagramProtocol, Event, Future, Queue,
    QueueEmpty, QueueFull, Transport, ensure_future)
from datetime import datetime
from functools import partial
from typing import Optional, Tuple, Union, cast

from .bridge.messages import SwitcherV2BroadcastMSG
from .devices import SwitcherV2Device

# fmt: on


class SwitcherV2UdpProtocolFactory(DatagramProtocol):
    """Represntation of the Asyncio UDP protocol factory.

    Args:
      loop: the event loop for the factory to run in.
      phone_id: the phone id of the desired device.
      device_id: the id of the desired device.
      device_password: the password of the desired device.
      queue: a ``asyncio.Queue`` for the factory to save messages in.
      run_factory_evt ``asyncio.Event`` for signaling the factory to run.

    Todo:
      * replace ``factory_future`` attribute with ``get_factory_future``
        method.

    """

    def __init__(
        self,
        loop: AbstractEventLoop,
        phone_id: str,
        device_id: str,
        device_password: str,
        queue: Queue,
        run_factory_evt: Event,
    ) -> None:
        """Initialize the protocol."""
        self._loop = loop
        self._phone_id = phone_id
        self._device_id = device_id
        self._device_password = <PASSWORD>
        self._queue = queue
        self._run_factory = run_factory_evt
        self._factory_future = self._loop.create_future()
        self._factory_future.add_done_callback(self.close_transport)
        self._accept_datagrams = Event()
        self.transport = None  # type: Optional[Transport]
        self._device = None  # type: Optional[SwitcherV2Device]

    def connection_made(self, transport: BaseTransport) -> None:
        """Call on connection established."""
        self.transport = cast(Transport, transport)
        self._accept_datagrams.set()

    def datagram_received(self, data: Union[bytes, str], addr: Tuple) -> None:
        """Call on datagram recieved."""
        if self._run_factory.is_set() and self._accept_datagrams.is_set():
            ensure_future(
                self.handle_incoming_messages(data, addr), loop=self._loop
            )

    def error_received(self, exc: Optional[Exception]) -> None:
        """Call on exception recieved."""
        if exc:
            self.factory_future.set_exception(exc)
        else:
            self.factory_future.set_result(None)

    def connection_lost(self, exc: Optional[Exception]) -> None:
        """Call on connection lost."""
        self.factory_future.set_result(exc if exc else None)

    def close_transport(self, future: Future) -> None:
        """Call for closing the transport."""
        if self.transport:
            self.transport.close()

    async def handle_incoming_messages(
        self, data: Union[bytes, str], addr: Tuple
    ) -> None:
        """Use for Handling incoming messages."""
        self._accept_datagrams.clear()
        msg = SwitcherV2BroadcastMSG(self._loop, data)
        msg.init_future.add_done_callback(
            partial(self.get_device_from_message, addr[0])
        )

        return None

    def get_device_from_message(self, ip_addr: str, future: Future) -> None:
        """Use for extracting the device from the broadcast message."""
        msg = future.result()
        if msg.verified:
            if self._device_id == msg.device_id:
                if self._device:
                    # Update known device
                    self._device.update_device_data(
                        ip_addr,
                        msg.name,
                        msg.device_state,
                        msg.remaining_time_to_off,
                        msg.auto_off_set,
                        msg.power,
                        msg.current,
                        (
                            self._device.last_state_change
                            if msg.device_state == self._device.state
                            else datetime.now()
                        ),
                    )
                else:
                    # New device discoverd
                    self._device = SwitcherV2Device(
                        msg.device_id,
                        ip_addr,
                        msg.mac_address,
                        msg.name,
                        msg.device_state,
                        msg.remaining_time_to_off,
                        msg.auto_off_set,
                        msg.power,
                        msg.current,
                        self._phone_id.lower(),
                        self._device_password.lower(),
                        datetime.now(),
                    )
                try:
                    self._queue.put_nowait(self._device)
                except QueueFull:
                    try:
                        self._queue.get_nowait()
                    except QueueEmpty:
                        pass
                    ensure_future(
                        self._queue.put(self._device), loop=self._loop
                    )
        self._accept_datagrams.set()

    @property
    def factory_future(self) -> Future:
        """asyncio.Future: Representing the initialization status."""
        return self._factory_future

Switcher water heater unofficial API and bridge, Network protocols.
 
 .. codeauthor:: <NAME> <<EMAIL>> # fmt: off # fmt: on Represntation of the Asyncio UDP protocol factory.
 
     Args:
       loop: the event loop for the factory to run in.
       phone_id: the phone id of the desired device.
       device_id: the id of the desired device.
       device_password: the password of the desired device.
       queue: a ``asyncio.Queue`` for the factory to save messages in.
       run_factory_evt ``asyncio.Event`` for signaling the factory to run.
 
     Todo:
       * replace ``factory_future`` attribute with ``get_factory_future``
         method. Initialize the protocol. # type: Optional[Transport] # type: Optional[SwitcherV2Device] Call on connection established. Call on datagram recieved. Call on exception recieved. Call on connection lost. Call for closing the transport. Use for Handling incoming messages. Use for extracting the device from the broadcast message. # Update known device # New device discoverd asyncio.Future: Representing the initialization status.