import json
import socket
import threading
import time
from ipc_util import our_eth_mac_addr, our_real_ip_addr


class IPCDiscoveryService:
    def __init__(self, svcobj):
        self.__port = svcobj.data["settings"]["discovery_port"]
        self.__stop_event = threading.Event()

        self.monitoring_thread = threading.Thread(
            target=monitoring,
            name="UDP-M",
            kwargs={"svcobj": svcobj, "stop_event": self.__stop_event})
        self.monitoring_thread.start()

        self.listening_thread = threading.Thread(
            target=listening,
            name="UDP-L",
            kwargs={"svcobj": svcobj, "port": self.__port, "stop_event": self.__stop_event})
        self.listening_thread.start()

        self.broadcasting_thread = threading.Thread(
            target=broadcasting,
            name="UDP-B",
            kwargs={"svcobj": svcobj, "port": self.__port, "stop_event": self.__stop_event})
        self.broadcasting_thread.start()

    def stop_service(self):
        self.__stop_event.set()
        try:
            self.monitoring_thread.join()
            self.broadcasting_thread.join()
            self.listening_thread.join()
        except (RuntimeError, AttributeError):
            pass


def monitoring(svcobj, stop_event: threading.Event):
    print("UDP list monitoring started.")

    our_hw_addr = our_eth_mac_addr()
    while not stop_event.is_set():
        svcobj.check_neighbours(our_hw_addr)
        time.sleep(1)

    print("UDP list monitoring ends.")


def listening(svcobj, port: int, stop_event: threading.Event):
    # Initiate UDP listening socket
    in_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
    in_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    try:
        in_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
    except AttributeError:
        print("FYI: SO_REUSERPORT not supported on this system.")
    in_sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, 20)
    in_sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)
    in_sock.settimeout(1)

    in_sock.bind(('', port))  # Local listening port

    ipaddr = socket.gethostbyname(socket.gethostname())
    in_sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton(ipaddr))

    print("UDP listening started.")

    our_hw_addr = our_eth_mac_addr()
    while not stop_event.is_set():
        try:
            data, remote_addr = in_sock.recvfrom(1024)
            data_str = json.loads(data)
            print("UDP received ")
            if data_str["hw_addr"] != our_hw_addr:
                svcobj.update_neighbour(our_hw_addr = our_hw_addr,
                                        hw_addr = data_str["hw_addr"],
                                        ip_addr = remote_addr[0], 
                                        ip_subnet = data_str.get("ip_subnet",""),
                                        ip_gateway = data_str.get("ip_gateway",""),
                                        desc = data_str.get("desc",""),
                                        model = data_str.get("model",""),
                                        cloud_device_name = data_str.get("cloud_device_name",""),
                                        serial = data_str.get("serial",""),
                                        firmware_version = data_str.get("firmware_version",""),
                                        oem_label = data_str.get("oem_label",""),
                                        oem_model = data_str.get("oem_model",""),
                                        oem_desc = data_str.get("oem_desc",""),
                                        dante_capable = data_str.get("dante_capable",False),
                                        last_report_time = time.time())
        except (socket.timeout, ValueError, TypeError):
            # print("UDP error ")
            continue

    print("UDP listening ends.")
    in_sock.close()


def broadcasting(svcobj, port: int, stop_event: threading.Event):
    out_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
    out_sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
    out_sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 10)

    our_mac_addr = our_eth_mac_addr()

    message = dict()
    message["hw_addr"] = our_mac_addr

    print("UDP broadcasting started.")
    while not stop_event.is_set():
        # Some parameters may change at runtime

        ip_infos = our_real_ip_addr(True).split()
        message["ip_addr"] = ip_infos[0]
        message["ip_subnet"] = ip_infos[1]
        message["ip_gateway"] = ip_infos[2]
        message["desc"] = svcobj.data["settings"].get("discovery_id", "")
        message["model"] = svcobj.data["settings"].get("model", "")
        message["cloud_device_name"] = svcobj.data["settings"].get("cloud_device_name", "")
        message["serial"] = svcobj.data["settings"].get("serial", "")
        message["firmware_version"] = svcobj.data["settings"].get("hardware_version", "")
        message["oem_label"] = svcobj.oem.get("oem_label", "")
        message["oem_model"] = svcobj.oem.get("oem_model", "")
        message["oem_desc"] = svcobj.oem.get("oem_desc", "")
        message["dante_capable"] = svcobj.oem.get("dante_capable", "")

        message_bstr = json.dumps(message).encode()

        try:
            out_sock.sendto(message_bstr, ("255.255.255.255", port))
        except OSError:
            pass
        time.sleep(3)

    print("UDP broadcasting ends.")
    out_sock.close()
