import logging
import asyncio
import pathlib
from typing import Callable, Union, List

from AiDragonfly.bleak.backends.device import BLEDevice
from AiDragonfly.bleak.backends.dotnet.utils import BleakDataReader
from AiDragonfly.bleak.backends.scanner import BaseBleakScanner

# Import of Bleak CLR->UWP Bridge. It is not needed here, but it enables loading of Windows.Devices
from BleakBridge import Bridge  # noqa: F401

from Windows.Devices.Bluetooth.Advertisement import (
    BluetoothLEAdvertisementWatcher,
    BluetoothLEScanningMode,
    BluetoothLEAdvertisementType,
    BluetoothLEAdvertisementReceivedEventArgs,
    BluetoothLEAdvertisementWatcherStoppedEventArgs,
)
from Windows.Foundation import TypedEventHandler

logger = logging.getLogger(__name__)
_here = pathlib.Path(__file__).parent


def _format_bdaddr(a):
    return ":".join("{:02X}".format(x) for x in a.to_bytes(6, byteorder="big"))


def _format_event_args(e):
    try:
        return "{0}: {1}".format(
            _format_bdaddr(e.BluetoothAddress), e.Advertisement.LocalName or "Unknown"
        )
    except Exception:
        return e.BluetoothAddress


class BleakScannerDotNet(BaseBleakScanner):
    """The native Windows Bleak BLE Scanner.

    Implemented using `pythonnet <https://pythonnet.github.io/>`_, a package that provides an integration to
    the .NET Common Language Runtime (CLR). Therefore, much of the code below has a distinct C# feel.

    Keyword Args:

        scanning mode (str): Set to ``Passive`` to avoid the ``Active`` scanning mode.

        SignalStrengthFilter (``Windows.Devices.Bluetooth.BluetoothSignalStrengthFilter``): A
          BluetoothSignalStrengthFilter object used for configuration of Bluetooth LE advertisement
          filtering that uses signal strength-based filtering.

        AdvertisementFilter (``Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementFilter``): A
          BluetoothLEAdvertisementFilter object used for configuration of Bluetooth LE advertisement
          filtering that uses payload section-based filtering.

    """

    def __init__(self, **kwargs):
        super(BleakScannerDotNet, self).__init__()

        self.watcher = None
        self._devices = {}
        self._scan_responses = {}

        self._callback = None

        self._received_token = None
        self._stopped_token = None

        if "scanning_mode" in kwargs and kwargs["scanning_mode"].lower() == "passive":
            self._scanning_mode = BluetoothLEScanningMode.Passive
        else:
            self._scanning_mode = BluetoothLEScanningMode.Active

        self._signal_strength_filter = kwargs.get("SignalStrengthFilter", None)
        self._advertisement_filter = kwargs.get("AdvertisementFilter", None)

    def _received_handler(self, sender, event_args):
        if sender == self.watcher:
            logger.debug("Received {0}.".format(_format_event_args(event_args)))
            if (
                event_args.AdvertisementType
                == BluetoothLEAdvertisementType.ScanResponse
            ):
                if event_args.BluetoothAddress not in self._scan_responses:
                    self._scan_responses[event_args.BluetoothAddress] = event_args
            else:
                if event_args.BluetoothAddress not in self._devices:
                    self._devices[event_args.BluetoothAddress] = event_args
        if self._callback is not None:
            self._callback(sender, event_args)

    def _stopped_handler(self, sender, event_args):
        if sender == self.watcher:
            logger.debug(
                "{0} devices found. Watcher status: {1}.".format(
                    len(self._devices), self.watcher.Status
                )
            )

    async def start(self):
        self.watcher = BluetoothLEAdvertisementWatcher()
        self.watcher.ScanningMode = self._scanning_mode

        self._received_token = self.watcher.add_Received(
            TypedEventHandler[
                BluetoothLEAdvertisementWatcher,
                BluetoothLEAdvertisementReceivedEventArgs,
            ](self._received_handler)
        )
        self._stopped_token = self.watcher.add_Stopped(
            TypedEventHandler[
                BluetoothLEAdvertisementWatcher,
                BluetoothLEAdvertisementWatcherStoppedEventArgs,
            ](self._stopped_handler)
        )

        if self._signal_strength_filter is not None:
            self.watcher.SignalStrengthFilter = self._signal_strength_filter
        if self._advertisement_filter is not None:
            self.watcher.AdvertisementFilter = self._advertisement_filter

        self.watcher.Start()

    async def stop(self):
        self.watcher.Stop()

        if self._received_token:
            self.watcher.remove_Received(self._received_token)
            self._received_token = None
        if self._stopped_token:
            self.watcher.remove_Stopped(self._stopped_token)
            self._stopped_token = None

        self.watcher = None

    async def set_scanning_filter(self, **kwargs):
        """Set a scanning filter for the BleakScanner.

        Keyword Args:
          SignalStrengthFilter (``Windows.Devices.Bluetooth.BluetoothSignalStrengthFilter``): A
            BluetoothSignalStrengthFilter object used for configuration of Bluetooth
            LE advertisement filtering that uses signal strength-based filtering.
          AdvertisementFilter (Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementFilter): A
            BluetoothLEAdvertisementFilter object used for configuration of Bluetooth LE
            advertisement filtering that uses payload section-based filtering.

        """
        if "SignalStrengthFilter" in kwargs:
            # TODO: Handle SignalStrengthFilter parameters
            self._signal_strength_filter = kwargs["SignalStrengthFilter"]
        if "AdvertisementFilter" in kwargs:
            # TODO: Handle AdvertisementFilter parameters
            self._advertisement_filter = kwargs["AdvertisementFilter"]

    async def get_discovered_devices(self) -> List[BLEDevice]:
        found = []
        for event_args in list(self._devices.values()):
            new_device = self.parse_eventargs(event_args)
            if (
                not new_device.name
                and event_args.BluetoothAddress in self._scan_responses
            ):
                new_device.name = self._scan_responses[
                    event_args.BluetoothAddress
                ].Advertisement.LocalName
            found.append(new_device)

        return found

    @staticmethod
    def parse_eventargs(event_args):
        bdaddr = _format_bdaddr(event_args.BluetoothAddress)
        uuids = []
        for u in event_args.Advertisement.ServiceUuids:
            uuids.append(u.ToString())
        data = {}
        for m in event_args.Advertisement.ManufacturerData:
            with BleakDataReader(m.Data) as reader:
                data[m.CompanyId] = reader.read()
        local_name = event_args.Advertisement.LocalName
        return BLEDevice(
            bdaddr, local_name, event_args, uuids=uuids, manufacturer_data=data
        )

    def register_detection_callback(self, callback: Callable):
        """Set a function to act as Received Event Handler.

        Documentation for the Event Handler:
        https://docs.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothleadvertisementwatcher.received

        Args:
            callback: Function accepting two arguments:
             sender (``Windows.Devices.Bluetooth.AdvertisementBluetoothLEAdvertisementWatcher``) and
             eventargs (``Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementReceivedEventArgs``)

        """
        self._callback = callback

    # Windows specific

    @property
    def status(self) -> int:
        """Get status of the Watcher.

        Returns:

            Aborted 4
            An error occurred during transition or scanning that stopped the watcher due to an error.

            Created 0
            The initial status of the watcher.

            Started 1
            The watcher is started.

            Stopped 3
            The watcher is stopped.

            Stopping 2
            The watcher stop command was issued.

        """
        return self.watcher.Status if self.watcher else None

    @classmethod
    async def find_device_by_address(
        cls, device_identifier: str, timeout: float = 10.0, **kwargs
    ) -> Union[BLEDevice, None]:
        """A convenience method for obtaining a ``BLEDevice`` object specified by Bluetooth address.

        Args:

            device_identifier (str): The Bluetooth address of the Bluetooth peripheral.

            timeout (float): Optional timeout to wait for detection of specified peripheral
              before giving up. Defaults to 10.0 seconds.

        Keyword Args:

          scanning mode (str): Set to ``Passive`` to avoid the ``Active`` scanning mode.

          SignalStrengthFilter (``Windows.Devices.Bluetooth.BluetoothSignalStrengthFilter``): A
            BluetoothSignalStrengthFilter object used for configuration of Bluetooth LE advertisement
            filtering that uses signal strength-based filtering.

          AdvertisementFilter (``Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementFilter``): A
            BluetoothLEAdvertisementFilter object used for configuration of Bluetooth LE
            advertisement filtering that uses payload section-based filtering.

        Returns:

            The ``BLEDevice`` sought or ``None`` if not detected.

        """

        ulong_id = int(device_identifier.replace(":", ""), 16)
        loop = asyncio.get_event_loop()
        stop_scanning_event = asyncio.Event()
        scanner = cls(timeout=timeout)

        def stop_if_detected(sender, event_args):
            if event_args.BluetoothAddress == ulong_id:
                loop.call_soon_threadsafe(stop_scanning_event.set)

        return await scanner._find_device_by_address(
            device_identifier, stop_scanning_event, stop_if_detected, timeout
        )
