"""
Name: mDNS_Broadcast
Title: 
Author: Cooper
Date: 28/08/2018
Last Modified: 12/05/2020

Desc:  mDNS/DNS-SD service broadcaster. Can form the basis of mDNS behaviour for any service just by
changing the serviceName etc.
Text records are initialised as a dict, with key= field name, val= text. Topics are string formatted
as string [find separator]

This was originally written for ITxPT Module Inventory Service which it served its purpose, this will need to be
modified to support IBIS-IP strings as they are very different.

ITxPT DNS-SD string:    "HanoverW0123456._inventory._itxpt_http._tcp.local. 3600 IN SRV 0 0 80 <HOSTNAME>"
IBIS-IP DNS-SD string:  "DeviceManagementService._ibisip_http.local. 3600 IN SRV 10 0 389 OnboardUnit_1.local."

There is now a mainclass for starting and stopping DNS-SD, now each family of protocols can inherit this class and
form their own SRVs

It would seem that ibisip breaks the standard structure of the DNS-SD service string, at least it raises an
exception with the libs used.  Changing line 262 in the zeroconf init file will fix this:

if not (type_.endswith('._tcp.local.') or type_.endswith('._udp.local.')):
    if "ibis" in type_:
        pass
    else:
        raise BadTypeInNameException("Type '%s' must end with '._tcp.local.' or '._udp.local.'" % type_)


mDNS_Broadcast and mDNS_Listen now merged and classes renamed

"""
import logging

import re
import socket
import time
import zeroconf
from zeroconf import ServiceInfo, Zeroconf, ServiceBrowser, ZeroconfServiceTypes

class DNSSD_Register():
    def __init__(self, ip, hostname):
        self.ip = ip
        self.hostname = hostname

        self.service_name = None
        self.service_registered = False

        self.serviceWeight = 0
        self.servicePriority = 0
        # Instead of messing about with the lib itself, just replace the string checking with a new regex pattern
        self.zeroconf_version = zeroconf.__version__
        if self.zeroconf_version == "0.21.3":
            self.old_version = True
            zeroconf._HAS_ONLY_A_TO_Z_NUM_HYPHEN = re.compile(r'^[A-Za-z0-9\-\_]+$')
        else:
            self.old_version = False
            zeroconf.service_type_name.__kwdefaults__['strict'] = False

        self.zeroconf = Zeroconf()

    def register_service(self, service_type, service_name, port, txtrecord):
        if self.old_version:
            self.info = ServiceInfo(service_type,
                                    service_name,
                                    address=socket.inet_aton(self.ip),
                                    port=port,
                                    weight=self.serviceWeight,
                                    priority=self.servicePriority,
                                    properties=txtrecord,
                                    server=self.hostname + ".local."
                                    )
        else:
            self.info = ServiceInfo(service_type,
                                    service_name,
                                    addresses=socket.inet_aton(self.ip),
                                    port=port,
                                    weight=self.serviceWeight,
                                    priority=self.servicePriority,
                                    properties=txtrecord,
                                    server=self.hostname + ".local."
                                    )

        """
        type_: fully qualified service type name
        name: fully qualified service name
        address: IP address as unsigned short, network byte order
        port: port that the service runs on
        weight: weight of the service
        priority: priority of the service
        properties: dictionary of properties (or a string holding the
                    bytes for the text field)
        server: fully qualified name for service host (defaults to name)
        """

        #I am pretty sure this no longer needs to be run as a thread as the lib takes care of that
        self.zeroconf.register_service(self.info)

        print("Service registered: %s @ %s" % (service_name, self.ip))
        self.service_registered = True
        self.service_name = service_name


    def unregister_service(self):
        if self.service_registered:
            self.zeroconf.unregister_service(self.info)

            print("Service unregistered: ", self.service_name)

class HANOVER_DNSSD(DNSSD_Register):
    def __init__(self, ip, hostname):
        super().__init__(ip, hostname)

    def hanover_service(self, txtrecord=None):
        """
        Just a dummy hanover service that doesn't serve much purpose
        :return:
        """
        service_type = "_http._tcp.local."
        service_name = self.hostname + "." + service_type
        service_port = 80

        if txtrecord == None:
            txtrecord = {
                "Hello": "World"
            }

        self.register_service(service_type, service_name, service_port, txtrecord)

    def hanover_wdm_credentials_service(self, txtrecord=None):
        """
        This enables signs to find the Hanover WDM credential broker.
        """
        service_type = "_mqtt._tcp.local."
        service_name = self.hostname + "._mqtt-broker." + service_type
        service_port = 8883

        if txtrecord == None:
            txtrecord = {
                "txtversion": "1",
                "version": "1",
                "topic": "hanover/"
            }

        self.register_service(service_type, service_name, service_port, txtrecord)

    def hanover_production_service(self, txtrecord=None):
        """
        This is the sign build production service, allowing the signs to run their test mode upon discovery
        """
        service_type = "_itxpt_http._tcp.local."
        service_name = "Han_prod" + "_inventory." + service_type
        service_port = 80

        if txtrecord == None:
            txtrecord = {
                "txtversion": "1",
                "version": "1",
                "topic": "hanover/"
            }

        self.register_service(service_type, service_name, service_port, txtrecord)

class ITxPT_DNSSD(DNSSD_Register):
    def __init__(self, ip, hostname):
        super().__init__(ip, hostname)

    def module_inventory_service(self, txtrecord):
        """
        Previous:   [UniqueIdentifier]._inventory._itxpt_http._tcp.local 3600 IN SRV 0 0 80 [hostname]
        2.1.2:      [UniqueIdentifier]_inventory._itxpt_http._tcp.local 120 IN SRV 0 0 80 [hostname]
        :param txtrecord:
        :return:
        """
        service_type = "_itxpt_http._tcp.local."
        service_name = self.hostname + "_inventory." + service_type
        service_port = 80

        self.register_service(service_type, service_name, service_port, txtrecord)

    def mqtt_broker_service(self, txtrecord):
        # [UniqueIdentifier]._mqtt-broker._mqtt._tcp.local 3600 IN SRV 0 0 8000 [hostname]
        service_type = "_mqtt._tcp.local."
        service_name = self.hostname + "._mqtt-broker." + service_type
        service_port = 1883

        self.register_service(service_type, service_name, service_port, txtrecord)

    def avms_service(self, txtrecord):
        service_type = "_itxpt_http._tcp.local."
        service_name = self.hostname + "_avms." + service_type
        service_port = 8000

        self.register_service(service_type, service_name, service_port, txtrecord)

    def vehicle_to_ip_service(self, txtrecord, port: int = 15030):
        """
        [UniqueIdentifier]_vehicletoip._itxpt_multicast._udp.local 120 IN SRV 0 0 15030 [hostname]

        This broadcasts the vehicle_to_ip service, but care must be taken to use the multicast address and not the
        devices IP address!!
        """
        service_type = "_itxpt_multicast._udp.local."
        service_name = self.hostname + "_vehicletoip." + service_type
        service_port = port

        self.register_service(service_type, service_name, service_port, txtrecord)


class IBISIP_DNSSD(DNSSD_Register):
    def __init__(self, ip, hostname):
        super().__init__(ip, hostname)

    def device_management_service(self, port, txtrecord):
        # "DeviceManagementService._ibisip_http.local. 3600 IN SRV 10 0 389 OnboardUnit_1.local."
        service_type = "_ibisip_http._tcp.local."
        service_name = ("DeviceManagementService_Hanover-%s." % self.hostname) + service_type    #For ibis-ip the service type is the same as the service name?

        #There is a lack of defintion in how DNS-SD names should be done, so if there is a conflict then the entire
        #DMS module crashes, so just let the DNS-SD fail in this case.
        try:
            self.register_service(service_type, service_name, port, txtrecord)
        except Exception as e:
            print(e)

    def customer_information_service(self, port, txtrecord):
        service_type = "_ibisip_http._tcp.local."
        service_name = "CustomerInformationService." + service_type  # For ibis-ip the service type is the same as the service name?

        self.register_service(service_type, service_name, port, txtrecord)

class DNSSD_Discover(object):
    def __init__(self, type, service):
        """
        Listener constructor.

        Params
        ------
        type : str
            Service type listened for.
        service: str
            The hostname of the service provider.

        """
        #If directly using the values below, note that they would be the most recently discovered service keeping this
        #for backwards compatibility for now, but would be preferable to move to the list of services in future.
        self.serviceIP = ""
        self.servicePort = ""
        self.server = ""
        self.properties = ""
        self.service = service
        self.type = type

        # Use self.discovered_services for all discovered services to allow importing class to pick
        self.discovered_services = []

        self.zeroconf = Zeroconf()

    def run(self):
        # zeroconf = Zeroconf()
        browser = ServiceBrowser(self.zeroconf, self.type, handlers=[self.zeroconf_handler])

    def look_for_service(self, timeout: int = 0):
        """
        As many services are using DNS-SD to look for services, to save having to implement the same code over and over
        this can be used instead.

        It will sit in a loop waiting a service forever, unless a timeout is specified and only break when something is
        found or the timeout expires
        """
        self.run()

        discovery_timeout = time.time()

        while 1:
            if self.serviceIP == "":
                if timeout == 0:
                    time.sleep(0.5)
                    continue
                else:
                    if (time.time() - discovery_timeout) > timeout:
                        self.close()
                        return 1

                time.sleep(1)

            else:
                self.close()
                return 0

    def close(self):
        """

        :return:
        """
        self.zeroconf.close()

    def service_dictionary_template(self):
        """
        This just returns a dictionary with the DNS-SD parameters to be updated by whoever asks for this.

        """
        service_dict = {
            "name": None,
            "type": None,
            "address": None,
            "port": 0,
            "weight": 0,
            "priority": 0,
            "server": None,
            "txt_records": None
        }

        return service_dict

    def zeroconf_handler(self, zeroconf, service_type, name, state_change):
        service_info = zeroconf.get_service_info(service_type, name)

        if service_info:
            discovered_service_name = service_info.name
            logging.debug(discovered_service_name)
            if self.service in discovered_service_name or self.service == "":
                service_dict = self.service_dictionary_template()

                service_dict["name"] = self.name = discovered_service_name
                service_dict["type"] = service_info.type
                service_dict["address"] = self.serviceIP = socket.inet_ntoa(service_info.address)
                service_dict["port"] = self.servicePort = service_info.port
                service_dict["weight"] = service_info.weight
                service_dict["priority"] = service_info.priority
                service_dict["server"] = self.server = service_info.server

                txt_records = self.properties = service_info.properties

                if txt_records:
                    service_dict["txt_records"] = txt_records

                self.discovered_services.append(service_dict)
        else:
            pass

    def list_all_services(self):
        print("\n".join(ZeroconfServiceTypes.find()))

if __name__ == '__main__':
    serialNumber = "Dell01234789"
    softwareVersion = "1"
    hardwareVersion = "RevA"
    model = "COLxxx-ITxPT"

    status = "0"

    mistxt = {
        "txtversion": "1",
        "version": "1",
        "type": "Cooper's Laptop",
        "model": model,
        "manufacturer": "Dell",
        "serialnumber": serialNumber,
        "softwareversion": softwareVersion,
        "hardwareVersion": hardwareVersion,
        "macaddress": "DE:AD:BE:EF:00:00",
        "status": status,
        "submodules": "False",
        "xstatus": "C00000000000000",
        "path": ""
    }

    avmstxt = {
        "txtversion": "1",
        "version": "1",
        "path": "/",
    }

    mqtttxt = {
        "txtversion": "1",
         "version": "1",
         "brand": "mosquitto",
         "proto": "3.1",
         "topic": "hanovermqtt"
    }

    ibisiptxt = {
        "version": "1.1",
        "path": "/"
    }

    # test = ITxPT_DNSSD("10.14.0.101", "Han_con3")
    # test.module_inventory_service(mistxt)

    # test2 = IBISIP_DNSSD("10.14.0.100", "Han_con1")
    # test2.device_management_service(80, ibisiptxt)

    # while True:
    #     time.sleep(0.1)

    # dnssd = DNSSD_Discover("_ibisip_http._tcp.local.", "CustomerInformationService")
    # dnssd.run()
    #
    # time.sleep(2)
    # print(dnssd.discovered_services)
    # print(dnssd.name, dnssd.serviceIP, dnssd.servicePort)

    dnssd = HANOVER_DNSSD("127.0.0.1", "blah")
    dnssd.hanover_production_service()

    while 1:
        time.sleep(1)
        pass






