"""
Name: ibisipApp
Title: IBIS-IP application
Author: Cooper
Date: 20/04/2020

Desc:  This is the main entry point for the IBIS-IP portion of things.  A lot of the mechanisms involved with IBIS-IP
are shared with ITxPT, except that the syntax, names for things and behaviours are completely different.

The bare minimum for IBIS-IP support is:
- Local Link IP address 169.254.x.x
- DNS-SD, both for device advertising but also locating appropriate services
- DMS DeviceManagementService, similar to MIS
- CIS CustomerInformationService, similar to AVMS

The transport mechanisms are HTTP and UDP.  For our purposes, only HTTP is required.

"""
import time
import _thread
import logging

from hanip.onionip.console import sign_manager
from hanip.onionip.console import console_task

from hanip.onionip.sign import sign_task

from hanip.onionip import config_updater
from hanip.onionip import hwDetermine
from hanip.onionip import networkConfigurator

from hanip.ibis_ip import device_management_service
from hanip.ibis_ip import cust_info_service_subscriber
from hanip.ibis_ip import ibisip_data_parser

ibisipapp_version = "0.0.1"
desc = "IBIS-IP Module.  Version %s" % ibisipapp_version
print(desc)

class IBISIPApplication(object):
    def __init__(self, config_dict, hw_dict, config_dir, data_dir):
        self.config_dict = config_dict
        self.hw_dict = hw_dict
        self.config_dir = config_dir
        self.data_dir = data_dir

        self.services_started = False

        self.hwd = hwDetermine.HardwareDeterminer(None, None, None, self.config_dir)
        self.network_config = networkConfigurator.NetworkConfigurator(self.config_dict, self.config_dir)

        # Hack to support EG4
        try:
            self.init_g4()
        except ImportError:
            self.eg4_mode = False
        else:
            self.eg4_mode = True

    def init_g4(self):
        # Attempt to connect to the g4 console application.
        from __main__ import G4_CONSOLE
        logging.info("G4: Initialising")
        self.g4hook = G4_CONSOLE()

    def start_dms(self):
        """
        Start the DeviceManagementService
        """
        if self.config_dict.get("IBISIP_enable_dms", True):
            logging.info("IBISIP: DMS enabled")
            self.dms = device_management_service.DeviceManagementService(self.config_dict, self.hw_dict)
            _thread.start_new_thread(self.dms.start_service, ())
        else:
            logging.info("IBISIP: DMS disabled")

    def start_cu(self):
        """
        Start the config updater
        """
        self.cu = config_updater.ConfigUpdater(self.config_dict, self.hw_dict, self.config_dir)
        self.cu.setup_webserver()

    def wait_for_network_availability(self):
        """
        For lack of a better name, the idea of this is to sit in a loop in it's own thread and wait for the network to
        be ready before starting the services without having to bother the main loop.  This is probably only appropriate
        for the console, as there is no point in running anything else on a sign when there isnt' a network.

        It will constantly obtain the IP address, When a network is established, it updates the hw_dict with the IP address
        and breaks the loop.  It then starts the services that require a network interface and sets services_started flag
        to true to indicate to the rest of the module that services are usable.
        """
        while 1:
            self.network_config.getIPAddress()

            ip_address = self.network_config.ip_address

            if ip_address == None:
                print("Waiting for network")
                time.sleep(1)
            else:
                self.hw_dict["unit_IP"] = ip_address
                break

        print("Network established, starting services")

        self.start_dms()
        self.start_cis()
        self.start_cu()

        self.services_started = True

    def check_config_updater(self):
        """
        As the config updater runs in its own thread, this allows the application to check its status and act
        accordingly.  This module only notices the manufacturer details and configuration.
        :return: Returns a 1 if an update requires a restart to the application otherwise return None
        """
        if self.cu.new_manu:
            self.cu.new_manu = False

            self.hw_dict = self.hwd.get_serial()
            print("New hw_dict")
            print(self.hw_dict)

            # Parse now manufacturer details here and pass onto relevant modules, namely DMS
            self.dms.hw_dict = self.hw_dict
            self.cu.hw_dict = self.hw_dict

        if self.cu.new_conf:
            return 1

    """
    ###################################################################################################################
    CIS Functions
    """
    def setup_ibis_xml_parser(self):
        """
        Now that we have different flavours of IBISIP payloads we need to take into account the different parsers for it.

        Incase they put the parameter in the wrong place, this looks both in the MODE and IBISIP subcategories.
        """
        configured_parser = self.config_dict.get("MODE_payload_format", None)

        if configured_parser is None:
            configured_parser = self.config_dict.get("IBISIP_payload_format", "generic")

        if configured_parser.lower() == "generic":
            ibis_xml_parser = ibisip_data_parser.Generic_Data_Parser(self.config_dict)
        elif configured_parser.lower() == "ret":
            ibis_xml_parser = ibisip_data_parser.RET_Data_Parser(self.config_dict)
        elif configured_parser.lower() == "ebs":
            ibis_xml_parser = ibisip_data_parser.EBS_Data_Parser(self.config_dict)
        else:
            #Default to generic if all else fails
            ibis_xml_parser = ibisip_data_parser.Generic_Data_Parser(self.config_dict)

        return ibis_xml_parser

    def start_cis(self):
        """
        Start the CustomerInformationService.  If configured it will look for service via DNS-SD in another thread.  If
        not, it will use the details set in the configuration file and manually set the cis_server_found flag in the
        cis module.

        This will only do the setup required to use cis, it will not subscribe to the service!!!
        """
        self.cis = cust_info_service_subscriber.CustomerInformationServiceSubscriber(self.config_dict, self.hw_dict)
        if self.config_dict["IBISIP_discover"]:
            _thread.start_new_thread(self.cis.look_for_service, ())
        else:
            print("IA: Using config parameters for CIS server details")
            self.cis.service_ip = self.config_dict["IBISIP_service_ip"]
            self.cis.service_port = self.config_dict["IBISIP_service_port"]
            self.cis.service_path_request = self.config_dict["IBISIP_service_path_request"]
            self.cis.service_path_subscribe = self.config_dict["IBISIP_service_path_subscribe"]
            self.cis.cis_server_found = True

    def subscribe_to_cis(self):
        """
        Subscribe to CIS.  Only happens if it meets the conditions below which are:
        - If it is configured to subscribe
        - It it hasnt already subscribed
        - If a service has been found
        """
        if self.config_dict["IBISIP_subscribe"]:
            if not self.cis.subscribed:
                if self.cis.cis_server_found:
                    self.cis.send_subscription_request()

    def get_cis_data(self):
        """
        Grabs the data from the CIS module, and converts it into a format that the template_generator expects
        :return: If successful it will return a dictionary of data items that the template_generator expects,
        else returns None
        """
        if self.services_started:
            if self.cis.cis_server_found:
                cis_data = self.cis.get_cis_data()
                return cis_data
            else:
                print("CIS Service unavailable")
                return None
        else:
            print("CIS Service unavailable")
            return None

    def check_cis_timeout(self):
        """
        This checks the timeout in CIS.  Each time CIS gets some stuff posted the timer is reset.
        """
        if (time.time() - self.cis.subscription_timer) > self.cis.subscription_timeout:
            logging.info("IBISIPApp: Subscription timeout expired")
            self.cis.subscribed = False
            return True
        else:
            return False


    def run(self):
        """
        This is a standalone mode where its only job is to grab data and respond to status requests.
        This will not check for network status and assumes whoever is importing this has some sort of network setup
        else it will cry and it will not be pretty.

        This was originally intended for EG4 operation
        """
        self.start_dms()
        self.start_cis()

        while 1:
            if self.config_dict["IBISIP_subscribe"]:
                while 1:
                    self.subscribe_to_cis()
                    if self.cis.subscribed:
                        logging.info("Subscribed to CIS!")
                        break
                    else:
                        time.sleep(1)

            while 1:
                if self.check_cis_timeout():
                    break

                if self.config_dict["IBISIP_subscribe"]:
                    #cis.new_data only applies when data is posted, otherwise this will always be false...
                    raw_xml = self.cis.retrieve_raw_xml()
                else:
                    raw_xml = self.cis.retrieve_raw_xml(True)

                if raw_xml is None:
                    time.sleep(3)
                    continue

                if self.eg4_mode:
                    self.g4hook.process_ibis_ip_xml(raw_xml)
                    time.sleep(4)

                time.sleep(1)

    def run_apc(self):
        """
        This is a temp hook for me to start the right things on EG4.  Leaving it in for now.
        """
        from hanip.ibis_ip import apc_service_subscriber

        apc = apc_service_subscriber.PassengerCountingSubscriber(self.config_dict, self.hw_dict)
        apc.main()



class IBISIPSignApplication(IBISIPApplication):
    def __init__(self, config_dict, hw_dict, config_dir, data_dir):
        super().__init__(config_dict, hw_dict, config_dir, data_dir)

        # self.ibisip_parser = ibisip_data_parser.RET_Data_Parser(self.config_dict)
        self.ibisip_parser = self.setup_ibis_xml_parser()

    def init_sign_task(self):
        """
        Initiates the sign_task and initiates the serial port so that the application can communicate with the sign, it
        does not start the main loop of sign_task
        """
        self.st = sign_task.SignTask(self.config_dict, self.hw_dict, self.config_dir, self.data_dir)
        self.st.init_serial()


    def start_sign_task(self):
        """
        Starts the sign task thread, which then permits updating the display data
        """
        _thread.start_new_thread(self.st.run, ())

    def update_sign_task(self) -> None:
        """
        Updates sign task with data from CIS that is parsed.
        """
        raw_xml = self.get_cis_data()
        sign_data = self.ibisip_parser.get_display_dict(raw_xml)

        if sign_data is not None:
            self.st.update_data_dict(sign_data)

    def run(self):
        """
        The sign behaves quite differently from the console as it cannot do much else without a service unlike the console
        which can still be operated manually.  So in this instance it is permitted for parts of it to be blocking.
        Steps required:
            Wait for network
            Setup IBIS-IP modules: CIS, DMS
            Wait for subscription to CIS if configured
            Forever:
                Check config updater (break if there is a config)
                Obtain CIS data
                Update sign task
        """
        print("Running IBIS App")
        self.init_sign_task()
        self.wait_for_network_availability()

        #Service discovery loop, theres no point in doing much else if there is no service
        if self.config_dict["IBISIP_subscribe"]:
            toggle = False
            while 1:
                self.subscribe_to_cis()
                if self.cis.subscribed:
                    print("Subscribed to CIS!")
                    break

                #Flash the down triangle to indicate attempt to subscribe
                if toggle:
                    self.st.display_sign_graphic("TRI-DOWN")
                else:
                    self.st.transmit_message("C0", 0)
                toggle = not toggle
                time.sleep(1)

        #Start sign_task process, and grab some data to display
        self.st.display_sign_graphic("SQUARE")
        self.start_sign_task()

        #Send out a not in service message if it is supported
        try:
            self.st.update_data_dict(self.ibisip_parser.get_nis_dict())
        except AttributeError:
            pass
        self.update_sign_task()

        #Main display loop
        while 1:
            if self.check_cis_timeout():
                while 1:
                    self.subscribe_to_cis()
                    if self.cis.subscribed:
                        print("Subscribed to CIS!")
                        break

            if self.check_config_updater() == 1:
                break
            else:
                self.st.hw_dict = self.hw_dict      #Not sure what this does...

            if self.config_dict["IBISIP_subscribe"]:
                #cis.new_data only applies when data is posted, otherwise this will always be false...
                raw_xml = self.cis.retrieve_raw_xml()
                if raw_xml is not None:
                    self.update_sign_task()

                time.sleep(1)
            else:
                self.update_sign_task()
                time.sleep(5)


class IBISIPConsoleApplication(IBISIPApplication):
    def __init__(self, config_dict, hw_dict, config_dir, data_dir):
        super().__init__(config_dict, hw_dict, config_dir, data_dir)

        self.ibisip_parser = ibisip_data_parser.RET_Data_Parser(self.config_dict)

    def start_console_task(self):
        """
        Start the console task for handling console related matters
        """
        self.ct = console_task.ConsoleTask(self.config_dict, self.hw_dict, self.config_dir, self.data_dir)
        _thread.start_new_thread(self.ct.run_sign_data_mode(), ())

    def start_sign_manager(self):
        """
        Start the sign manager for handling sign related matters
        """
        self.sm = sign_manager.SignManagerConsole(self.config_dict, self.data_dir)
        _thread.start_new_thread(self.sm.run, ())

    def process_updates(self):
        """
        As the console task runs in its own thread, this allows the application to see if theres any updates it needs to
        process and instructs other modules to process the updates.
        :return:  Returns a 1 if an update requires a restart to the application otherwise 0
        """
        update_flags = self.ct.update_flags
        print("Processing updates...")

        time.sleep(1)

        print(update_flags)
        if update_flags["eric"]:
            print("New eric.bin")

            if self.config_dict["DATABASE_send_to_console"]:
                print("\t...sending to console")
                self.ct.transfer_database = True

            self.ct.reset_update_flag("eric")

        if update_flags["config"]:  #Restart the application if there is a new config
            return 1
        else:
            return 0

    def update_sign_manager(self):
        """

        """
        if self.ct.manual_code != "0"*10:
            raw_xml = self.get_cis_data()
            #sign_data is a display dict for template generator to ingest.
            sign_data = self.ibisip_parser.get_display_dict(raw_xml)

            if sign_data is not None:
                self.sm.update_data_via_display_dict(sign_data)
        else:
            self.sm.update_data_from_console_task(self.ct.sign_messages)

    def run(self):
        """
        Main routine that starts the relevant threads before sitting in a loop processing the necessary data.  The loop
        is broken when there is an update allowing the application to terminate.
        """
        print("IA: Running IBIS App")
        print("IP", self.hw_dict["unit_IP"])

        _thread.start_new_thread(self.wait_for_network_availability, ())
        self.start_console_task()
        self.start_sign_manager()

        #Sit in a loop until all (if any) the signs connected are discovered, allows the system to determine the signs
        #first before it starts to send data as the modules run indipendently of each other.
        while 1:
            print("IA: Waiting for SM")
            if self.sm.sign_dictionary == {}:
                time.sleep(1)
            else:
                print("IA: SM ready")
                break

        #Main loop
        while 1:
            if self.services_started:
                self.subscribe_to_cis()

                if self.check_config_updater() == 1:
                    break
                else:
                    self.ct.hw_dict = self.hw_dict

            if self.process_updates():
                break

            #Transfer data to sign manager
            if self.ct.test_mode:
                self.sm.test_mode = True
            else:
                self.sm.test_mode = False
                self.update_sign_manager()

            self.ct.display_message = self.sm.tg.console_display_text

            time.sleep(1)

if __name__ == "__main__":
    configs = {
        "IBISIP_discover": False,
        "IBISIP_timeout": 0,
        "IBISIP_service_ip": "10.104.0.149",
        "IBISIP_service_port": 8080,        #This is the port of the IBISIP CIS Server
        "IBISIP_service_path_request": "/CustomerInformationService/GetCurrentDisplayContent",
        "IBISIP_service_path_subscribe": "/CustomerInformationService/SubscribeCurrentDisplayContent",
        "IBISIP_service_path_request_int": "/CustomerInformationService/GetCurrentDisplayContent",
        "IBISIP_service_path_subscribe_int": "/CustomerInformationService/SubscribeCurrentDisplayContent",
        "IBISIP_subscribe": True,
        "IBISIP_server_port": 8080,     #This is the port of the local server
        "IBISIP_server_path": "/CustomerInformationSubscriber/CurrentDisplayContentSub",
        "IBISIP_cis_preamble": "CustomerInformationService.GetCurrentDisplayContentResponse",
        "IBISIP_time_stamp": "CurrentDisplayContentData;TimeStamp;Value",
        "IBISIP_line_number": "CurrentDisplayContentData;CurrentDisplayContent;LineInformation;LineRef;Value",
        "IBISIP_destination": "CurrentDisplayContentData;CurrentDisplayContent;Destination;DestinationRef;Value",
        "IBISIP_subscription_timeout": 20
    }

    hw = {
        "unit_IP": "10.104.0.37",
        "hw_type": "con",
        "serial_number": "0121do1"
    }
    logging.basicConfig(level=logging.DEBUG)

    iba = IBISIPApplication(configs, hw, "/tmp", "/tmp")
    iba.run()