"""
Name: udp_client
Title: UDP Client
Author: Cooper
Date: 05/09/2024

Desc:  This is the UDP client somewhat ported from G5 after some rigorous diet regime

"""
import logging
import time
import socket
import json

from hanip.onionip import hcp

class UDPClient(object):
    """

    """
    def __init__(self, config_dict, hw_dict):
        self.config_dict = config_dict
        self.hw_dict = hw_dict

        self.hcp = hcp.HCP()
        self.address = self.hw_dict["address"]      #This is the switch address not HCP.
        self.hcp_address = int(self.address) + 1

        self.udp_properties = {
            "interface": self.config_dict.get("UDP_interface", "br-lan"),
            "host": self.config_dict.get("UDP_host", "0.0.0.0"),
            "port": self.strtoint(self.config_dict.get("UDP_port", 1492), 1492),
            "encoding": self.config_dict.get("UDP_encoding", "utf-8"),

            "mute_broadcast": self.config_dict.get("UDP_mute_broadcast", False),
            "broadcast_address": self.config_dict.get("UDP_broadcast_address", None),
            "broadcast_timeout": self.strtoint(self.config_dict.get("UDP_broadcast_timeout", 7), 7),
            "reply_address": "",

            "packet_size": self.strtoint(self.config_dict.get("UDP_packet_size", 50 * 1024), 50*1024),
            "buffer_limit": self.strtoint(self.config_dict.get("UDP_buffer_limit", 100 * 2014), 100*1024),
            "default_timeout": 0.005
        }

        self.client = None
        self.connected = False
        self.server_address = None
        self.broadcast_address = self.get_broadcast_address()

        # manage broadcast behaviours
        self.broadcast_timer = time.time()
        self.mute_broadcast: bool = self.udp_properties["mute_broadcast"]
        self.broadcast_timeout: float = self.udp_properties["broadcast_timeout"]

        self.udp_connect()

    def strtoint(self, string, default):
        """
        Converts a string to int, and returns default if it cannot
        """
        try:
            value = int(string)
        except ValueError:
            return default
        else:
            return value

    def get_broadcast_address(self):
        """
        Sets the broadcast address depending on whats in the config, and if nothing then generates one
        """
        if self.udp_properties["broadcast_address"] is None:
            ip_octets = self.hw_dict["unit_IP"].split(".")
            ip_octets[-1] = "255"
            broadcast_address = ".".join(ip_octets)
        else:
            broadcast_address = self.udp_properties["broadcast_address"]

        return broadcast_address

    def udp_connect(self):
        """

        """
        host = self.udp_properties["host"]
        port = self.udp_properties["port"]

        udp_connection_dict = {
            # "client": "",
            "host": host,
            "port": port,
            "broadcast": self.broadcast_address
        }

        logging.info("UDP: setting up connection")
        logging.info(f"connection config: {udp_connection_dict}")

        try:
            self.client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
            self.client.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
            self.client.bind((host, port))
            self.client.settimeout(self.udp_properties["default_timeout"])
            logging.debug("connection established")
            self.connected = True
        except Exception as e:
            logging.exception("%s", exc_info=e)
            logging.error('UDP: Failed to create socket')

    def udp_disconnect(self):
        """
        Disconnect the current connection
        """
        if self.client:
            try:
                logging.debug("UDP: shutting down udp connection")
                self.client.shutdown(socket.SHUT_RDWR)
            except OSError:
                logging.error("UDP: could not shutdown connection")

            try:
                logging.debug("UDP: closing udp connection")
                self.client.close()
            except OSError:
                logging.debug("UDP: could not close udp connection")

    def get_message(self, filter=False):
        """
        Looks to see if there is a message waiting
        """
        if self.client is not None:
            try:
                rcvd_bytes, address = self.client.recvfrom(self.udp_properties["packet_size"])

                if address[0] == self.hw_dict["unit_IP"]:
                    logging.debug("UDP: Packet from self")
                    return None
                else:
                    self.server_address = address
            except TimeoutError:
                return None

            except Exception as e:
                logging.exception("%s", exc_info=e)
                return None

            if filter:
                dst_address = chr(rcvd_bytes[2])

                if dst_address == str(self.hcp_address):
                    return rcvd_bytes.decode("latin-1")
                else:
                    logging.info("UDP: Message for sign %s rejected" % dst_address)
                    return None
            else:
                return rcvd_bytes.decode("latin-1")


    def send_udp_response(self, message, address):
        """
        Sends a UDP message to a given address
        """
        if self.client is not None:
            if isinstance(message, dict):
                message = json.dumps(message)
            elif isinstance(message, str):
                logging.info(f"UDP: sending {message} to {address}")
            else:
                message = str(message)
                logging.info(f"UDP: sending {message} to {address}")
            try:
                msg = self.hcp.encodeSlave(message)
                self.client.sendto(msg.encode(self.udp_properties["encoding"]), address)
            except OSError:
                logging.error(f"UDP: OSError could not send: {message}")


    def poll(self, filter=False):
        """
        There are two parts to this subroutine, the first one is the part that looks to see if any UDP packets have been
        received.  The second deals with sending periodic status packets
        """
        message = self.get_message(filter)

        if not self.mute_broadcast and not message and self.broadcast_address:
            if (time.time() - self.broadcast_timer) > self.broadcast_timeout:
                msg = f"2{self.hcp_address:X}00"
                self.send_udp_response(
                    msg, (self.broadcast_address, self.udp_properties["port"])
                )
                self.broadcast_timer = time.time()

        return message

if __name__ == "__main__":
    logging.basicConfig(level=logging.DEBUG)

    config_dict = {
        "UDP_broadcast_address": "255.255.255.255"
    }

    hw_dict = {
        "address": "1"
    }

    udpc = UDPClient(config_dict, hw_dict)

    while 1:
        message = udpc.poll(False)
        if message is not None:
            print("Message: ", message)

        time.sleep(0.1)
