# -*- coding: utf-8 -*-
import contextlib
import operator
import socket
import struct
import threading

from resources.lib.kodi import kodilogging
from resources.lib.kodi.utils import get_setting_as_bool
from resources.lib.tubecast.kodicast import Kodicast
from resources.lib.tubecast.utils import build_template, str_to_bytes, PY3

if PY3:
    from socketserver import DatagramRequestHandler, ThreadingUDPServer
else:
    from SocketServer import DatagramRequestHandler, ThreadingUDPServer


logger = kodilogging.get_logger("ssdp")


def get_interface_address(if_name):
    import fcntl  # late import as this is only supported on Unix platforms.
    sciocgifaddr = 0x8915
    with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_DGRAM)) as s:
        return fcntl.ioctl(s.fileno(), sciocgifaddr, struct.pack(b'256s', if_name[:15]))[20:24]


class ControlMixin(object):

    def __init__(self, handler, poll_interval):
        self._thread = None
        self.poll_interval = poll_interval
        self._handler = handler

    def start(self):
        self._thread = t = threading.Thread(name=type(self).__name__,
                                            target=self.serve_forever,
                                            args=(self.poll_interval,))
        t.setDaemon(True)
        t.start()

    def stop(self):
        self.shutdown()
        self._thread.join()
        self._thread = None


class MulticastServer(ControlMixin, ThreadingUDPServer):

    allow_reuse_address = True

    def __init__(self, addr, handler, chromecast_addr, poll_interval=0.5, bind_and_activate=True, interfaces=None):
        ThreadingUDPServer.__init__(self, ('', addr[1]),
                                    handler,
                                    bind_and_activate)
        ControlMixin.__init__(self, handler, poll_interval)
        self.chromecast_addr = chromecast_addr
        self._multicast_address = addr
        self._listen_interfaces = interfaces
        self.set_loopback_mode(1)  # localhost
        self.set_ttl(2)  # localhost and local network
        self.handle_membership(socket.IP_ADD_MEMBERSHIP)

    def set_loopback_mode(self, mode):
        mode = struct.pack("b", operator.truth(mode))
        self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP,
                               mode)

    def server_bind(self):
        try:
            if hasattr(socket, "SO_REUSEADDR"):
                self.socket.setsockopt(
                    socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        except Exception as e:
            logger.error(e)
        try:
            if hasattr(socket, "SO_REUSEPORT"):
                self.socket.setsockopt(
                    socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
        except Exception as e:
            logger.error(e)
        ThreadingUDPServer.server_bind(self)

    def handle_membership(self, cmd):
        if self._listen_interfaces is None:
            mreq = struct.pack(
                str("4sI"), socket.inet_aton(self._multicast_address[0]),
                socket.INADDR_ANY)
            self.socket.setsockopt(socket.IPPROTO_IP,
                                   cmd, mreq)
        else:
            for interface in self._listen_interfaces:
                try:
                    if_addr = socket.inet_aton(interface)
                except socket.error:
                    if_addr = get_interface_address(interface)
                mreq = socket.inet_aton(self._multicast_address[0]) + if_addr
                self.socket.setsockopt(socket.IPPROTO_IP,
                                       cmd, mreq)

    def set_ttl(self, ttl):
        ttl = struct.pack("B", ttl)
        self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)

    def server_close(self):
        self.handle_membership(socket.IP_DROP_MEMBERSHIP)


class SSDPHandler(DatagramRequestHandler):

    header = '''\
HTTP/1.1 200 OK\r
LOCATION: http://{{ ip }}:{{ port }}/ssdp/device-desc.xml\r
CACHE-CONTROL: max-age=1800\r
EXT: \r
SERVER: UPnP/1.0\r
BOOTID.UPNP.ORG: 1\r
USN: uuid:{{ uuid }}\r
ST: urn:dial-multiscreen-org:service:dial:1\r
\r
'''

    def handle(self):
        data = self.request[0].strip()
        self.datagram_received(data, self.client_address)

    def reply(self, data, address):
        socket = self.request[1]
        socket.sendto(str_to_bytes(data), address)

    @staticmethod
    def get_remote_ip(address):
        # Create a socket to determine what address the client should use
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(address)
        iface = s.getsockname()[0]
        return iface if PY3 else unicode(iface)

    def datagram_received(self, datagram, address):
        if get_setting_as_bool('debug-ssdp'):
            logger.debug('Datagram received. Address:{}; Content:{}'.format(address, datagram))
        if b"urn:dial-multiscreen-org:service:dial:1" in datagram and b"M-SEARCH" in datagram:
            if get_setting_as_bool('debug-ssdp'):
                logger.debug("Answering datagram")

            _, port = self.server.chromecast_addr
            data = build_template(self.header).render(
                ip=self.get_remote_ip(address),
                port=port,
                uuid=Kodicast.uuid
            )
            self.reply(data, address)


class SSDPserver(object):
    SSDP_ADDR = '239.255.255.250'
    SSDP_PORT = 1900

    def start(self, chromecast_addr, interfaces=None):
        logger.info('Starting SSDP server')
        self.server = MulticastServer((self.SSDP_ADDR, self.SSDP_PORT), SSDPHandler,
                                      chromecast_addr=chromecast_addr,
                                      interfaces=interfaces)
        self.server.start()

    def shutdown(self):
        logger.info('Stopping SSDP server')
        self.server.server_close()
        self.server.stop()