"""
Name: itxpt vehicle to IP
Title: Vehicle to IP
Author: Cooper
Date: 18/03/2025

Desc:  This module is the implementation of ITxPT Vehicle to IP.

Information is provided over UDP multicast, to access this the module should join the multicast group.

Typical SRV: [UniqueIdentifier]_vehicletoip._itxpt_multicast._udp.local 120 IN SRV 0 0 15030 [hostname]

From this, the IP address of the multicast service and the port number is available

The VEHICLEtoIP service provides the following data included in the VEHICLEtoIPMessage.

    Tachymetry: distance travelled
    Door status: unlocked / closed and locked
    Cab in service
    Presence 750 V: equivalent of Engine On for bus; for power management
    Vehicle Identification Number: equivalent of bus VIN

"""
import json
import time
import socket
import struct
import logging

import xmltodict

from hanip.itxpt import DNS_SD

class VehicleToIP(object):
    """
    This forms the ITxPT VehicleToIP module.
    """
    def __init__(self, config_dict):
        self.config_dict = config_dict

        self.service_status = None
        self.multicast_group = "224.1.1.1"
        self.multicast_port = 5007
        self.packet_read_size = 10240

        # Data stores, theres only a few that we care about but should something require additional then they can just
        # use the dict and find the item they want.
        self.vehicle_to_ip_dict = {}

        self.time_stamp = None
        self.door_status = None         # This is the general door status
        self.door_status_list = []      # This stores a list of the door states
        self.stop_request = False
        self.vehicle_number = None

        self.service_discover = DNS_SD.DNSSD_Discover("_itxpt_multicast._udp.local.", "vtip")

    """
    ##################################################################################################################
    Socket Stuff
    """

    def setup_socket(self):
        """
        Sets up the socket to listen to UDP multicast messages from a given IP and port and some other multicast
        wizardry.
        """
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
        # allows multiple sockets to be bound to exactly the same combination of source multicast address and port:
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # Sets the TTL of the packet, although 32 seems a bit high if it isnt traversing the internet
        self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 8)
        self.sock.bind(("", self.multicast_port))

        mreq = struct.pack("4sl", socket.inet_aton(self.multicast_group), socket.INADDR_ANY)
        self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)

        logging.info("VtIP: Listening socket setup")

    def poll_socket(self):
        """
        Polls the socket to see if there's any data, if there is then it will go on to parse it.
        """
        data = self.sock.recv(self.packet_read_size)

        if len(data) > 1:
            self.parse_elements(data.decode("utf-8"))

    """
    ##################################################################################################################
    Service Discovery
    """
    def look_for_service(self):
        """
        Finds the VehicleToIP Service using DNS-SD.  By default it will wait forever as it we cannot assume any
        multicast address, but if a timeout is set, it will use the fallback details after the timeeout expires
        """
        logging.info("VtIP: Looking for service")
        self.service_status = "Discovering"
        status = self.service_discover.look_for_service(self.config_dict.get("VEHICLETOIP_timeout", 0))       #This is a blocking call

        if status:
            logging.info("VtIP: Service not found, using fallback")
            self.service_status = "Fallback"
            self.multicast_group = self.config_dict.get()
            self.multicast_port = self.config_dict.get()
        else:
            logging.info("VtIP: Service found!")
            self.service_status = "Service Found"
            self.multicast_group = self.service_discover.serviceIP
            self.multicast_port = self.service_discover.servicePort


    """
    ##################################################################################################################
    Data Handling
    """
    def convert_to_dict(self, raw_xml: str):
        """
        Converts raw XML into a python dictionary
        """
        try:
            xml_dict = xmltodict.parse(raw_xml, encoding="utf-8")
        except Exception:
            logging.warning("VtIP: XML conversion error!")
            return None
        else:
            return xml_dict

    def parse_elements(self, raw_xml: str):
        """
        Obtains the key elements from the XML but also saves it in its entirety so that anyone else who needs another
        bit of info can then grab it themselves.
        """
        vtip_dict = self.convert_to_dict(raw_xml)

        if vtip_dict is None:
            pass
        else:
            logging.debug(json.dumps(vtip_dict))
            self.vehicle_to_ip_dict = vtip_dict

            subdict = vtip_dict["VEHICLEtoIPDelivery"]["VEHICLEtoIP"]

            self.time_stamp = subdict["RecordedAtTime"]
            self.door_status = subdict["Doors"]["DoorsStatus"]
            self.door_status_list = subdict["Doors"]["Door"]
            self.stop_request = subdict["StopRequest"]

            # print(self.time_stamp)
            # print(self.door_status)
            # print(self.stop_request)


    """
    ##################################################################################################################
    Main
    """

    def run(self):
        """
        Main loop
        """
        if self.config_dict.get("VEHICLETOIP_discover", True):
            self.look_for_service()
        else:
            self.multicast_group = self.config_dict.get("VEHICLETOIP_fallback", "224.1.1.1")
            self.multicast_port = self.config_dict.get("VEHICLETOIP_port", 15030)

        self.setup_socket()

        logging.info("VtIP: Listening loop running")

        while 1:
            self.poll_socket()
            time.sleep(self.config_dict.get("VEHICLETOIP_poll_frequency", 1))


config_dict = {
    "VEHICLETOIP_discover": True,
    "VEHICLETOIP_timeout": "",
    "VEHICLETOIP_fallback": "",
    "VEHICLETOIP_port": "",
}

logging.basicConfig(level=logging.DEBUG)

vtip = VehicleToIP(config_dict)
vtip.run()