from .upnp import Device
from .util import _getLogger
import socket
import re
from datetime import datetime, timedelta
import select
import netifaces

DISCOVER_TIMEOUT = 2
SSDP_TARGET = ("239.255.255.250", 1900)
SSDP_MX = DISCOVER_TIMEOUT
ST_ALL = "ssdp:all"
ST_ROOTDEVICE = "upnp:rootdevice"


class Entry(object):
    def __init__(self, location):
        self.location = location


def ssdp_request(ssdp_st, ssdp_mx=SSDP_MX):
    """Return request bytes for given st and mx."""
    return "\r\n".join(
        [
            "M-SEARCH * HTTP/1.1",
            "ST: {}".format(ssdp_st),
            "MX: {:d}".format(ssdp_mx),
            'MAN: "ssdp:discover"',
            "HOST: {}:{}".format(*SSDP_TARGET),
            "USER-AGENT: hardlinep2p"
            "",
            "",
        ]
    ).encode("utf-8")


def scan(timeout=5):
    urls = []
    sockets = []
    ssdp_requests = [ssdp_request(ST_ALL), ssdp_request(ST_ROOTDEVICE), ssdp_request("urn:schemas-upnp-org:device:InternetGatewayDevice")]
    stop_wait = datetime.now() + timedelta(seconds=timeout)

    for addr in get_addresses_ipv4():
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM,socket.IPPROTO_UDP)
            sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 16)
            sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)

            sock.bind((addr, 0))

            sockets.append(sock)
        except socket.error:
            pass

    for sock in [s for s in sockets]:
        try:
            for req in ssdp_requests:
                #WHY can't I just send multicast and actually have it &*(%*()%)*ing work?
                sock.sendto(req, ("255.255.255.255",1900))
                sock.sendto(req, SSDP_TARGET)
            sock.setblocking(False)
        except socket.error:
            sockets.remove(sock)
            sock.close()
    try:
        seen ={}
        while sockets:
            time_diff = stop_wait - datetime.now()
            seconds_left = time_diff.total_seconds()
            if seconds_left <= 0:
                break

            ready = select.select(sockets, [], [], seconds_left)[0]

            for sock in ready:
                try:
                    data, address = sock.recvfrom(1024)
                    response = data.decode("utf-8")
                except UnicodeDecodeError:
                    _getLogger(__name__).debug(
                        "Ignoring invalid unicode response from %s", address
                    )
                    continue
                except socket.error:
                    _getLogger(__name__).exception(
                        "Socket error while discovering SSDP devices"
                    )
                    sockets.remove(sock)
                    sock.close()
                    continue
                locations = re.findall(
                    r"LOCATION: *(?P<url>\S+)\s+", response, re.IGNORECASE
                )
                if locations and len(locations) > 0:
                    if not locations[0] in seen:
                        urls.append(Entry(locations[0]))
                    seen[locations[0]]=True

    finally:
        for s in sockets:
            s.close()

    return set(urls)



def get_addresses_ipv4():
    """
    Find all IPs for this machine.

    :return: ``set`` of IP addresses (``IPAddress``).
    """
    ips = set()
    interfaces = netifaces.interfaces()
    for interface in interfaces:
        addresses = netifaces.ifaddresses(interface)
        for address_family in (netifaces.AF_INET,):
            family_addresses = addresses.get(address_family,[])
            if not family_addresses:
                continue
            for address in family_addresses:
                ips.add(address['addr'])
    return [i for i in ips if not i.startswith('127.')]





def discover(timeout=5):
    """
    Convenience method to discover UPnP devices on the network. Returns a
    list of `upnp.Device` instances. Any invalid servers are silently
    ignored.
    """
    devices = {}
    for entry in scan(timeout):
        if entry.location in devices:
            continue
        try:
            devices[entry.location] = Device(entry.location)
        except Exception as exc:
            log = _getLogger("ssdp")
            log.debug("Error '%s' for %s", exc, entry)
    return list(devices.values())
