"""Discover bulbs in a network."""
import asyncio
import json
import logging
import socket

from pywizlight import wizlight

_LOGGER = logging.getLogger(__name__)


class DiscoveredBulb:
    """Representation of discovered bulb."""

    def __init__(self, ip_address, mac_address):
        """Initialize the discovered bulb."""
        self.ip_address = ip_address
        self.mac_address = mac_address

    @staticmethod
    def create_bulb_from_message(raw_addr, announce_message):
        """Create announce message."""
        ip_address = raw_addr[0]
        _LOGGER.debug("Found bulb with IP: {}".format(ip_address))
        bulb = DiscoveredBulb(
            ip_address=ip_address,
            mac_address=json.loads(announce_message)["result"]["mac"],
        )
        return bulb


class BulbRegistry(object):
    """Representation of the bulb registry."""

    def __init__(self):
        """Initialize the bulb registry."""
        self.bulbs_by_mac = {}

    def register(self, bulb):
        """Register a new bulb."""
        self.bulbs_by_mac[bulb.mac_address] = bulb

    def bulbs(self) -> list:
        """Get all present bulbs."""
        return list(self.bulbs_by_mac.values())


class BroadcastProtocol(object):
    """Protocol that sends an UDP broadcast message for bulb discovery."""

    def __init__(self, loop, registry, broadcast_address):
        """Init discovery function."""
        self.loop = loop
        self.registry = registry
        self.broadcast_address = broadcast_address

    def connection_made(self, transport):
        """Init connection to socket and register broadcasts."""
        self.transport = transport
        sock = transport.get_extra_info("socket")
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
        self.broadcast_registration()

    def broadcast_registration(self):
        """Send a registration method as UDP broadcast."""
        # Note: The IP and address we give the bulb here don't seem to matter for our
        # intents and purposes, so they're hardcoded to technically valid dummy data.
        # Fix for async problems if broaddcast_registration is called twice! See #13.
        # dirty dirty hack
        try:
            register_method = r'{"method":"registration","params":{"phoneMac":"AAAAAAAAAAAA","register":false,"phoneIp":"1.2.3.4","id":"1"}}'  # noqa: E501
            self.transport.sendto(
                register_method.encode(), (self.broadcast_address, 38899)
            )
            self.loop.call_later(1, self.broadcast_registration)
        except AttributeError:
            pass

    def datagram_received(self, data, addr):
        """Receive data from broadcast."""
        _LOGGER.debug("Received data {} from IP address {}".format(data, addr))
        decoded_message = data.decode()
        if """"success":true""" in decoded_message:
            bulb = DiscoveredBulb.create_bulb_from_message(addr, decoded_message)
            self.registry.register(bulb)

    def connection_lost(self, exc):
        """Return connection error."""
        _LOGGER.debug("Closing UDP discovery")


async def find_wizlights(wait_time=5, broadcast_address="255.255.255.255") -> list:
    """Start discovery and return list of IPs of the bulbs."""
    registry = BulbRegistry()
    loop = asyncio.get_event_loop()
    transport, protocol = await loop.create_datagram_endpoint(
        lambda: BroadcastProtocol(loop, registry, broadcast_address),
        local_addr=("0.0.0.0", 38899),
    )
    try:
        await asyncio.sleep(wait_time)
    finally:
        transport.close()
        bulbs = registry.bulbs()
        for bulb in bulbs:
            _LOGGER.info(
                "Discovered bulb {} with MAC {}".format(
                    bulb.ip_address, bulb.mac_address
                )
            )
        return bulbs


async def discover_lights(broadcast_space="255.255.255.255") -> list:
    """Find lights and return list with wizlight objects."""
    discovered_IPs = await find_wizlights(broadcast_address=broadcast_space)
    # empty list for adding bulbs
    bulbs = []
    # create light entities from register
    for entries in discovered_IPs:
        bulbs.append(wizlight(ip=entries.ip_address, mac=entries.mac_address))
    return bulbs
