"""
Name: ibisip_data_parser
Title: IBIS-IP Data Parser
Author: Cooper
Date: 031 31 07 2020

Desc: In the course of developing this application, we have already seen that INIT (@RET) already deviates from the spec.
This class is purely for handling XML and returning a dictionary that the template_generator expects

"""
import re
import logging
import xmltodict

class IBISIP_Data_Parser(object):
    def __init__(self, config_dict):
        self.debug = False
        self.config_dict = config_dict

    """
    ###################################################################################################################
    XML Functions
    """

    def convert_xml_to_dict(self, raw_xml_data):
        """
        Takes in raw XML string and converts it into a python dictionary
        :param raw_xml_data: XML data as it comes from CIS
        :return: A dictionary of the raw_xml_data
        """
        try:
            raw_xml_dict = xmltodict.parse(raw_xml_data, "utf-8", dict_constructor=dict)
        except Exception as e:      #Exception is xml.parsers.expat.ExpatError but cannot use it directly
            print("XML issue: ", e)
            return None

        if self.debug:
            print(raw_xml_dict)

        return raw_xml_dict


    def get_xml_item(self, dictionary, itemlist, use_root=True):
        """
        XML is a pain with how many levels of stuff there is to deal with so it just loops through the dictionary until it
        gets to the bit it wants
        :param dictionary: This is the dictionary that will be looped through
        :param itemlist:  This is a list of levels of XML that will be looped through
        :param use_root:  This automatically inserts the xmlroot into the item list, generally required
        :return:  Data stored in a given XML element once the itemlist has been traversed
        """
        temp_dict = dictionary
        itemlist = itemlist.split(";")

        # The intention of the preamble is to strip out some repetition in the config file, this automatically obtains
        # the XML root and inserts it.
        if use_root:
            preamble = list(dictionary)[0]
            if len(preamble) > 1:
                itemlist.insert(0, preamble)

        # This part is for XML files where there are more than one instance of a "key", so in this instance, we want the
        # numerical index instead of the key, another variation to be aware of.
        for item in itemlist:
            try:
                item = int(item)
            except TypeError as e:
                pass
            except ValueError as e:
                pass

            try:
                temp_dict = temp_dict[item]
            except KeyError:
                if type(item) is int:
                    print("CISS: XML list index missing: %s in %s" % (item, itemlist))
                    continue
                else:
                    print("CISS: XML element missing: %s in %s" % (item, itemlist))
                # print(temp_dict)
                    return None
            except TypeError as e:
                pass

        else:
            return temp_dict

    """
    ###################################################################################################################
    Hanover Data Functions
    """

    def generate_display_dict(self, route_number, destination_data):
        """
        Generates expected Data for template generator
        display_data = {
            "$bcol": "0,0,0",
            "$fcol": "0,0,0
            "$rn": "12A",
            "$dest": ["Language1_TopLine/Language1_BottomLine", "Language2"],
            "destcode": "00000000000"
            }

        In the fullness of time, this will include other parameters like page time and colour.
        destcode is required even if it isnt used.

        :return: Dictionary that the template generator uses
        """
        display_data = {
            "$bcol": "0,0,0",
            "$fcol": "255,255,255",
            "$rn": route_number,
            "$dest": destination_data,
            "destcode": "0000000000"
        }

        return display_data

    def generate_internal_display_dict(self, route_number, stop_name, destination):
        """
        Generates expected data for the template generator
        display_data = {
            "$rn": "12A",
            "$stop": "StopName",
            "$dest": "Destination"
        }
        :return:
        """

        display_data = {
            "$rn": route_number,
            "$stop": stop_name,
            "$dest": destination
        }

        return display_data


    def convert_destination(self, data_list):
        """
        For lack of a better routine name, this takes all the values related to destination and returns something
        that doesnt give the template_generator indigestion:  ["Language1_TopLine/Language1_BottomLine", "Language2"]

        Data is given in the following format: [TimeStamp, RN, DestTop/DestSingle, DestBottom, SecondPage, PageTime]
        Note, DestBottom and SecondPage can be None
        :return: RN and destination in a format that the template generator expects
        """
        route_number = data_list[1]
        destination = []

        if data_list[3] == None:
            destination.append(data_list[2])
        else:
            destination.append(data_list[2] + ">" + data_list[3])
            #Make sure the delimiter matches in the config!!
        if data_list[4] == None:
            pass
        else:
            destination.append(data_list[4])

        return route_number, destination

class Generic_Data_Parser(IBISIP_Data_Parser):
    def __init__(self, config_dict):
        super().__init__(config_dict)

        self.xml_element_path_dict = None
        self.setup_xml_paths()

    def setup_xml_paths(self):
        """
        This sets up the paths of all the relevant data bits that something would want.  This is to allow both default
        locations and configurable ones as well.

        The default paths will cover most applications as the data is rather generic but it can be overridden.  If
        """
        config_dict_preamble = "IBISIP_"
        element_paths = {}

        default_element_paths = {
            "time_stamp_path": "CurrentDisplayContentData;TimeStamp;Value",
            "line_number_path": "CurrentDisplayContentData;CurrentDisplayContent;LineInformation;LineName;Value",
            "destination_single_line_path": "CurrentDisplayContentData;CurrentDisplayContent;Destination;DestinationName;Value",
            "destination_top_line_path": "CurrentDisplayContentData;CurrentDisplayContent;1;Destination;DestinationName;0;Value",
            "destination_bottom_line_path": "CurrentDisplayContentData;CurrentDisplayContent;1;Destination;DestinationName;1;Value",
            "destination_second_page_path": "CurrentDisplayContentData;CurrentDisplayContent;1;AdditionalInformation;Value",
            "stop_name_path": "CurrentStopPointData;CurrentStopPoint;StopName;Value",
            "page_time_path": "CurrentDisplayContentData;CurrentDisplayContent;1;Duration;Value"
        }

        for element, element_path in default_element_paths.items():
            config_parameter = config_dict_preamble + element[:-5]
            config_value = self.config_dict.get(config_parameter, None)

            if config_value is None:
                element_paths[element] = element_path
            else:
                element_paths[element] = config_value

        # print(element_paths)
        self.xml_element_path_dict = element_paths

    def extract_internal_data(self, raw_xml_dict):
        """

        """
        pass

    def extract_destination_data(self, raw_xml_dict):
        """
        This extracts the various bits of data needed whilst taking into account that expected data may not exist.

        Goodness knows what the format of the XML is...
        """
        time_stamp_path = self.xml_element_path_dict["time_stamp_path"]
        line_number_path = self.xml_element_path_dict["line_number_path"]
        destination_single_line_path = self.xml_element_path_dict["destination_single_line_path"]
        destination_top_line_path = self.xml_element_path_dict["destination_top_line_path"]
        destination_bottom_line_path = self.xml_element_path_dict["destination_bottom_line_path"]
        destination_second_page_path = self.xml_element_path_dict["destination_second_page_path"]

        data_list = []

        data_list.append(self.get_xml_item(raw_xml_dict, time_stamp_path))      #Time stamp

        line_number = self.get_xml_item(raw_xml_dict, line_number_path)     #Line Number
        if line_number == None:
            data_list.append("")
        else:
            data_list.append(line_number)

        #Decide whether the data obtained is a single line, or two line message
        destination_single_line = self.get_xml_item(raw_xml_dict, destination_single_line_path)

        if type(destination_single_line) is list:
            #If single_line returns a list, then we have not yet reached the desired data thus it is two line data
            topline = self.get_xml_item(raw_xml_dict, destination_top_line_path)
            data_list.append(topline)
            bottomline = self.get_xml_item(raw_xml_dict, destination_bottom_line_path)
            data_list.append(bottomline)
            # print(topline, bottomline)
        else:
            data_list.append(destination_single_line)
            data_list.append(None)

        # Grab the second page
        data_list.append(self.get_xml_item(raw_xml_dict, destination_second_page_path))

        #Append dummy pagetime
        data_list.append(None)

        # print(data_list)

        return data_list

    def get_display_dict(self, raw_xml_data):
        """
        The main call into this class, will handle whether the data is intended for an external or internal sign and
        act accordingly.
        """
        raw_xml_dict = self.convert_xml_to_dict(raw_xml_data)

        if raw_xml_dict == None:
            return None
        else:
            xml_root = list(raw_xml_dict)[0]  # Gets the first key in the dict, which happens to be the XML root
            if self.debug:
                print(xml_root)
            if "CurrentDisplayContent" in xml_root:
                parsed_data_list = self.extract_destination_data(raw_xml_dict)
                rn, dest = self.convert_destination(parsed_data_list)
                return self.generate_display_dict(rn, dest)
            elif "CurrentStopPoint" in xml_root:
                parsed_data_list = self.extract_internal_data(raw_xml_dict)
                return self.generate_internal_display_dict(parsed_data_list[1],
                                                           parsed_data_list[2],
                                                           parsed_data_list[3])
            else:
                print("XML type unexpected: %s" % xml_root)
                return None
    
class EBS_Data_Parser(Generic_Data_Parser):
    def __init__(self, config_dict):
        super().__init__(config_dict)

        self.xml_element_path_dict = None
        self.setup_xml_paths()
        
    def get_display_dict(self, raw_xml_data):
        """
        The main call into this class, will handle whether the data is intended for an external or internal sign and
        act accordingly.

        There are additional requirements to support some sort of "Not in service" message in whatever language that
        is using this.
        """
        raw_xml_dict = self.convert_xml_to_dict(raw_xml_data)

        if raw_xml_dict is None:
            return None
        else:
            xml_root = list(raw_xml_dict)[0]  # Gets the first key in the dict, which happens to be the XML root
            if self.debug:
                print(xml_root)
            if "CurrentDisplayContent" in xml_root:
                parsed_data_list = self.extract_destination_data(raw_xml_dict)

                try:
                    if parsed_data_list[1] == "" and parsed_data_list[2] is None:
                        return self.get_nis_dict()
                except IndexError:
                    return self.get_nis_dict()
                
                if self.detect_nis_message(parsed_data_list):
                    logging.info("EBS Parser: Geen Dienst detected")
                    return self.get_nis_dict()
                else:
                    rn, dest = self.convert_destination(parsed_data_list)

                return self.generate_display_dict(rn, dest)
            elif "CurrentStopPoint" in xml_root:
                parsed_data_list = self.extract_internal_data(raw_xml_dict)
                return self.generate_internal_display_dict(parsed_data_list[1],
                                                           parsed_data_list[2],
                                                           parsed_data_list[3])
            else:
                print("XML type unexpected: %s" % xml_root)
                return None
            
    def detect_nis_message(self, parsed_data_list):
        """
        This detects the presence of an nis message that is configured in the config file, but can be overriden.
        Defaults to Geen Dienst because they are Dutch and heel silly.
        
        """
        delimiter = self.config_dict.get("TEMPLATES_delimiter", ">")
        nis_message = self.config_dict.get("IBISIP_nis_message", "Geen>Dienst")
        nis_message_normialised = nis_message.replace(delimiter, " ").lower()
        
        for data in parsed_data_list:
            if data is not None:
                if data.lower() == nis_message_normialised:
                    return True
                elif data == "-1":
                    return True
            
        return False
        

    def get_nis_dict(self):
        """
        Generates the Not in Service dictionary
        """
        nis_message = self.config_dict.get("IBISIP_nis_message", "Geen>Dienst")
        nis_display_dict = self.generate_display_dict("", [nis_message])
        
        #The nis message has a delimiter in the case for RN signs, but for larger signs we want to remove it.
        nis_display_dict["override_delimiter"] = True
        
        return nis_display_dict


class RET_Data_Parser(IBISIP_Data_Parser):
    """
    RET data parser was originally written for RET in mind
    """
    def __init__(self, config_dict):
        super().__init__(config_dict)

        self.xml_element_path_dict = None
        self.setup_xml_paths()

    def setup_xml_paths(self):
        """
        This sets up the paths of all the relevant data bits that something would want.  This is to allow both default
        locations and configurable ones as well.

        The default paths will cover most applications as the data is rather generic but it can be overridden.  If
        """
        config_dict_preamble = "IBISIP_"
        element_paths = {}

        default_element_paths = {
            "time_stamp_path": "CurrentDisplayContentData;TimeStamp;Value",
            "line_number_path": "CurrentDisplayContentData;CurrentDisplayContent;1;LineInformation;LineName;Value",
            "destination_single_line_path": "CurrentDisplayContentData;CurrentDisplayContent;1;Destination;DestinationName;Value",
            "destination_top_line_path": "CurrentDisplayContentData;CurrentDisplayContent;1;Destination;DestinationName;0;Value",
            "destination_bottom_line_path": "CurrentDisplayContentData;CurrentDisplayContent;1;Destination;DestinationName;1;Value",
            "destination_second_page_path": "CurrentDisplayContentData;CurrentDisplayContent;1;AdditionalInformation;Value",
            "stop_name_path": "CurrentDisplayContentData;CurrentDisplayContent;1;Duration;Value",
            "page_time_path": "CurrentDisplayContentData;CurrentDisplayContent;1;Duration;Value"
        }

        for element, element_path in default_element_paths.items():
            config_parameter = config_dict_preamble + element[:-5]
            config_value = self.config_dict.get(config_parameter, None)

            if config_value is None:
                element_paths[element] = element_path
            else:
                element_paths[element] = config_value

        print(element_paths)
        self.xml_element_path_dict = element_paths

    def get_display_dict(self, raw_xml_data):
        """
        The main call into this class, will handle whether the data is intended for an external or internal sign and
        act accordingly
        :param raw_xml_data: Raw XML data obtained via HTTP
        :return: display dict compatible with template_generator if valid XML was given if the XML has the appropriate
        root, otherwise None
        """
        raw_xml_dict = self.convert_xml_to_dict(raw_xml_data)

        if raw_xml_dict == None:
            return None
        else:
            xml_root = list(raw_xml_dict)[0]      # Gets the first key in the dict, which happens to be the XML root
            if self.debug:
                print(xml_root)
            if "CurrentDisplayContent" in xml_root:
                parsed_data_list = self.extract_destination_data(raw_xml_dict)
                rn, dest = self.convert_destination(parsed_data_list)
                return self.generate_display_dict(rn, dest)
            elif "CurrentStopPoint" in xml_root:
                parsed_data_list = self.extract_internal_data(raw_xml_dict)
                return self.generate_internal_display_dict(parsed_data_list[1],
                                                           parsed_data_list[2],
                                                           parsed_data_list[3])
            else:
                print("XML type unexpected: %s" % xml_root)
                return None

    def extract_internal_data(self, raw_xml_dict):
        """
        The following are the key bits of data to extract:
        - Timestamp
        - StopName
        - LineName
        - DestinationName
        :return: List of IBIS-IP data items
        """

        time_stamp_path = "CurrentStopPointData;TimeStamp;Value"
        stop_name_path = "CurrentStopPointData;CurrentStopPoint;StopName;Value"
        line_number_path = "CurrentStopPointData;CurrentStopPoint;DisplayContent;LineInformation;LineName;Value"
        destination_path = "CurrentStopPointData;CurrentStopPoint;DisplayContent;Destination;DestinationName;Value"

        data_list = []

        data_list.append(self.get_xml_item(raw_xml_dict, time_stamp_path))
        data_list.append(self.get_xml_item(raw_xml_dict, line_number_path))
        data_list.append(self.get_xml_item(raw_xml_dict, stop_name_path))
        data_list.append(self.get_xml_item(raw_xml_dict, destination_path))

        return data_list


    def extract_destination_data(self, raw_xml_dict):
        """
        There are 5 key points of data to extract:
        - Timestamp, always present
        - Line number, always present
        - Dest, Single Line/Top line, this is always present
        - Dest, Bottom line, this is not always present
        - Additional Information, this is data thats displayed on the 2nd page, not always present
        - Duration, it appears that there is only one single value for all pages, always present
        :return: List of IBIS-IP data items e.g. ['2020-07-28T16:18:29', '33', 'Centraal Station', 'via Airport', None, 100]
        """

        time_stamp_path = self.xml_element_path_dict["time_stamp_path"]
        line_number_path = self.xml_element_path_dict["line_number_path"]
        destination_single_line_path = self.xml_element_path_dict["destination_single_line_path"]
        destination_top_line_path = self.xml_element_path_dict["destination_bottom_line_path"]
        destination_bottom_line_path = self.xml_element_path_dict["destination_second_page_path"]
        destination_second_page_path = self.xml_element_path_dict["destination_second_page_path"]
        page_time_path = self.xml_element_path_dict["page_time_path"]

        data_list = []

        data_list.append(self.get_xml_item(raw_xml_dict, time_stamp_path))      #Time stamp

        line_number = self.get_xml_item(raw_xml_dict, line_number_path)     #Line Number
        if line_number == None:
            data_list.append("")
        else:
            data_list.append(line_number)

        #Decide whether the data obtained is a single line, or two line message
        destination_single_line = self.get_xml_item(raw_xml_dict, destination_single_line_path)
        if type(destination_single_line) is list:
            #If single_line returns a list, then we have not yet reached the desired data thus it is two line data
            data_list.append(self.get_xml_item(raw_xml_dict, destination_top_line_path))
            data_list.append(self.get_xml_item(raw_xml_dict, destination_bottom_line_path))
        else:
            data_list.append(destination_single_line)
            data_list.append(None)

        # Grab the second page
        data_list.append(self.get_xml_item(raw_xml_dict, destination_second_page_path))

        # Grab the page time, although this is currently not used elsewhere...
        page_time = self.get_xml_item(raw_xml_dict, page_time_path)
        if page_time == None:
            data_list.append(None)
        else:
            data_list.append(self.parse_page_time(page_time))

        # print(data_list)
        return data_list

    def parse_page_time(self, xml_value):
        """
        The typical format of a duration is:  P0Y0M0DT0H0M10S
        :param xml_value:
        :return: page time in deciseconds
        """
        re_search_string = "[0-9]+S"
        page_time = re.search(re_search_string, xml_value)

        if page_time != None:
            page_time = page_time.group()[:-1]  #Strip the "S" after the value
        else:
            return None

        try:
            page_time = int(page_time)
        except ValueError:
            return None

        #Hanover page times are in deciseconds...  so multiply by 10.
        return page_time*10

"""
###################################################################################################################
APC Parser Functions
"""
class IRMA_APC_Parser(IBISIP_Data_Parser):
    def __init__(self, config_dict):
        super().__init__(config_dict)

        self.xml_element_path_dict = None
        self.setup_xml_paths()

    def setup_xml_paths(self):
        """
        This sets up the paths of all the relevant data bits that something would want.  This is to allow both default
        locations and configurable ones as well.

        The default paths will cover most applications as the data is rather generic but it can be overridden.  If
        """
        config_dict_preamble = "IBISIP_"
        element_paths = {}

        default_element_paths = {
            "time_stamp_path": "AllData;TimeStamp;Value",
            "door_id_path": "AllData;CountingData;DoorID;Value",
            "count_path": "AllData;CountingData;Count",
            "object_class_path": "ObjectClass",
            "in_value_path": "In;Value",
            "out_value_path": "Out;Value"
        }

        for element, element_path in default_element_paths.items():
            config_parameter = config_dict_preamble + element[:-5]
            config_value = self.config_dict.get(config_parameter, None)

            if config_value is None:
                element_paths[element] = element_path
            else:
                element_paths[element] = config_value

        # print(element_paths)
        self.xml_element_path_dict = element_paths

    def extract_door_id(self, raw_alldata_xml):
        """
        This parses the xml and extracts the door ID only
        """
        raw_alldata_dict = self.convert_xml_to_dict(raw_alldata_xml)

        time_stamp_path = self.xml_element_path_dict["time_stamp_path"]
        door_id_path = self.xml_element_path_dict["door_id_path"]

        time_stamp = self.get_xml_item(raw_alldata_dict, time_stamp_path)
        door_id = self.get_xml_item(raw_alldata_dict, door_id_path)

        return door_id


    def extract_pc_data(self, raw_alldata_xml):
        """
        Parses the values out and returns everything into a nice dictionary
        """
        count_dict = {}

        raw_alldata_dict = self.convert_xml_to_dict(raw_alldata_xml)

        # print(raw_alldata_dict)

        time_stamp_path = self.xml_element_path_dict["time_stamp_path"]
        count_path = self.xml_element_path_dict["count_path"]
        object_path = self.xml_element_path_dict["object_class_path"]
        in_path = self.xml_element_path_dict["in_value_path"]
        out_path = self.xml_element_path_dict["out_value_path"]

        time_stamp = self.get_xml_item(raw_alldata_dict, time_stamp_path)
        all_count = self.get_xml_item(raw_alldata_dict, count_path)

        #Count is provided as a list, so there will need to be a way to see what each element references
        for count in all_count:
            object_class = self.get_xml_item(count, object_path, False)
            in_value = self.get_xml_item(count, in_path, False)
            out_value = self.get_xml_item(count, out_path, False)

            try:
                _in_value = int(in_value)
                _out_value = int(out_value)
            except ValueError:
                _in_value = 0
                _out_value = 0
                total = 0
            else:
                total = _in_value - _out_value

                #If the sensor gets confused then just force the value to 0 if it works out to be negative passengers
                if total < 0:
                    total = 0

            count_dict[object_class] = {
                "in": _in_value,
                "out": _out_value,
                "total": total
            }

        return count_dict


"""
###################################################################################################################
Test Functions
"""

def test_generic_parser():
    """
    Test routine for the generic parser
    """
    config_dict = {

    }

    retp = Generic_Data_Parser(config_dict)

    root_path = r"C:\git\hanip\test_scripts\ibis-ip\cis_data\ebs"
    xml_paths = ["currentDisplayContent_ebs.txt", "CXX_vdv301_Current_singel_line_raw.txt", "CXX_vdv301_Current_duble_line_raw.txt"]

    for xml_path in xml_paths:
        print(xml_path)
        xml_data_path = os.path.join(root_path, xml_path)
        xml_data = open(xml_data_path, "r").read()

        display_dict = retp.get_display_dict(xml_data)
        print(display_dict)

        print("\n\n")

def test_ret_parser():
    """
    RET was the first project to use this so things were slightly bespoke for them so things are done a little differently
    """

    config_dict = {
        "IBISIP_time_stamp": "CurrentDisplayContentData;TimeStamp;Value",
        "IBISIP_line_number": "CurrentDisplayContentData;CurrentDisplayContent;1;LineInformation;LineName;Value",
        "IBISIP_destination": "CurrentDisplayContentData;CurrentDisplayContent;1;Destination;DestinationName;0;Value"
    }

    retp = RET_Data_Parser(config_dict)
    retp.debug = True

    root_path = r"C:\git\hanip\test_scripts\ibis-ip\cis_data\ret"
    xml_paths = ["currentDisplayContent1.txt", "currentDisplayContent2.txt", "currentDisplayContent3.txt"]

    for xml_path in xml_paths:
        print(xml_path)
        xml_data_path = os.path.join(root_path, xml_path)
        xml_data = open(xml_data_path, "r").read()

        display_dict = retp.get_display_dict(xml_data)
        print(display_dict)
        
def test_ebs_parser():
    """
    Test routine for the EBS parser
    """
    config_dict = {
    
    }
    
    retp = EBS_Data_Parser(config_dict)
    
    root_path = r"C:\git\hanip\test_scripts\ibis-ip\cis_data\ebs"
    xml_paths = ["currentDisplayContent_ebs.txt", "CXX_vdv301_Current_singel_line_raw.txt",
                 "CXX_vdv301_Current_duble_line_raw.txt", "geendienst.txt"]
    
    for xml_path in xml_paths:
        print(xml_path)
        xml_data_path = os.path.join(root_path, xml_path)
        xml_data = open(xml_data_path, "r").read()
        
        display_dict = retp.get_display_dict(xml_data)
        print(display_dict)
        
        print("\n\n")

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

    print("Testing Generic Parser\n")
    test_generic_parser()

    print("\n\n")

    print("Testing RET Parser\n")
    test_ret_parser()
    
    print("\n\n")

    print("Testing EBS Parser\n")
    test_ebs_parser()
