"""
Author: D Glendining
Date: 10/02/2020

"""
import time
import _thread
import threading
import serial
from datetime import datetime
from collections import deque

from hanip.itxpt import mqtt_client
from hanip.itxpt import module_inventory_service
from hanip.itxpt import status_handler
from hanip.itxpt import DNS_SD
from hanip.itxpt import ecoMonitor

from hanip.onionip import hcp
from hanip.onionip.hcp import RawHMFmsg, HMFmsgFromJSON, ShortStatusResponse, HMFError
from hanip.onionip import hano1
from hanip.onionip import hwDetermine
from hanip.onionip import config_updater

from hanip.debug.print_text import PrintText

class Lausanne_App(object):
    """
    Class dedicated to the job of handling Hanover and consoles which comply Lausanne specification.
    """
    SUBSCRIBE_FAILED = 0
    """ Value returned when subscription attempt fails. """
    SUBSCRIBE_OK = 1
    """ Value returned when subscription attempt succeeds."""
    STATUS_PUB_FREQ = 20
    """ Interval in secs between auto-publishing the unit's status info.  (ORIG: 300) """
    STATUS_POLL_FREQ = 30
    """ Interval in secs between short status requests & replies expected from sign. (ORIG: 30)"""

    SIGN_STAT_SIGN_OK = 0x00
    SIGN_STAT_MSG_CONTENT_ERROR = 0x01
    SIGN_STAT_TX_ERROR = 0x02
    SIGN_STAT_LAMP_FAILURE = 0x03
    SIGN_STAT_BUSY = 0x08
    SIGN_STAT_BUFF_FULL = 0x10
    SIGN_STAT_COMMS_FAIL = 0xFF

    RUNNING = True
    itxpt_mqtt_app_ver = ""

    def __init__(self, config_dict, hw_dict, conf_dir, data_dir):
        """
        VIMI_App constructor.
        Parameters
        ----------
        config_dict: dict
            Contains onion configuration details.
        hw_dict: dict
            Contains hardware configuration details.
        conf_dir: str
            Path for location of configuration files.
        data_dir:
            Path for location of data files.
        """
        self.config_dict = config_dict
        self.hw_dict = hw_dict
        self.conf_dir = conf_dir
        self.data_dir = data_dir
        self.configUpdater = None
        self.hardwareDeterminer = None
        self.mis = None

        self.ftp_update = False
        self.current_payload = None
        self.acf_wait_time = 0      #Anti-Cooper filter timer

        self.status_wait_time = 0
        self.status_last_update = 0
        self.prevStatus = {}
        self.currStatus = ""
        self.prevErrorByte = 0
        self.statusPollTime = 0

        self.mqtt_sub = None
        self.subscribeList = []
        self.publishList = []

    def setup_MIS(self):
        """ Sets up the Module Inventory Service and advertises the unit's presence over HTTP. """
        self.mis = module_inventory_service.ModuleInventoryService(self.config_dict, self.hw_dict)
        self.mis.run()

    def update_mis(self):
        """ Updates the MIS entry when a new configuration comes in.
        """
        self.mis.unregister_service()
        self.mis.update_information(self.hw_dict)
        self.mis.register_service()

    def getTimestamp(self):
        """
        Obtains the current Linux time and converts it into an epoch timestamp.
        Returns
        -------
        The timestamp
        """
        now = datetime.now()
        return(datetime.timestamp(now))

    def getMACasNumber(self):
        """ Converts the MAC Address string to a simple hex number. """
        mac = self.hw_dict["unit_MAC"]
        return(mac.replace(":", ""))

    def setup(self):
        """ Sets up the various elements required to run both sign and console Lausanne features.

            Obtains the timestamp at which this function is invoked for status reporting.
            Sets up the Module Inventory Service.
            Instantiates the JSON handler.
        """
        #Initiate Status Handler
        self.status_handler = status_handler.StatusHandler(self.hw_dict, self.config_dict)
        # Snapshot uptime for status reporting
        self.startTime = self.status_handler.getSystemUptime()
        #Setup MIS
        self.setup_MIS()
        #The hardware determiner to take care of serial numbers etc.
        self.hardwareDeterminer = hwDetermine.HardwareDeterminer(None, None, None, self.conf_dir)

        self.setup_config_updater()

    def attemptSubscription(self, brokerAddress):
        """ Attempt to subscribe to the list of topics we need to access.

        Parameter
        ---------
        ipAddress: str
            Address of the service subscribed to.
        Returns
        -------
        0 for failure, 1 for successful subscription.
        """
        topics = []
        for topic in self.subscribeList:
            topics.append((topic, 0))

        self.mqtt_sub = mqtt_client.MQTT_Client()
        self.mqtt_sub.set_broker_address(brokerAddress)

        if self.mqtt_sub.connect_client():
            self.mqtt_sub.subscribe_to_topics(topics)
            return 1
        else:
            return 0

    def poll_mqtt(self):
        """ Periodically checks in with the MQTT thread to see if a new message has arrived
        and forwards it for processing if it passes the filtering criteria.

        Filters out irrelevant topics.
        """
        result = False
        if self.mqtt_sub.newMsg:
            self.mqtt_sub.newMsg = False
            if self.mqtt_sub.rcvdTopic in self.subscribeList:
                self.current_payload = self.mqtt_sub.payload
                result = True
        return result

    def gatherStatusInfo(self):
        """ Placeholder for subclasses """
        pass

    def pollStatusChange(self):
        """ Puts a timer around the activity of gathering status information from the sign or console.
        """
        if self.statusPollTime == 0 or (time.time() - self.statusPollTime) > Lausanne_App.STATUS_POLL_FREQ:
            self.gatherStatusInfo()
            self.statusPollTime = time.time()

    def setup_config_updater(self):
        """ Insantiates a ConfigUpdater and tells it to set up its webserver.
        """
        self.configUpdater = config_updater.ConfigUpdater(self.config_dict, self.hw_dict, self.conf_dir)
        self.configUpdater.setup_webserver()

    def check_config_updater(self):
        """ Checks whether the config updater has any changes to report.

        Might be either a Manufacturing Information change or a new configuration
        of some other kind or both. Updates the MIS and restarts the service if it's manufacturing info.

        Returns:
            bool: True if a new configuration is reported, otherwise False. New configurations
            require the application to restart from scratch.
        """
        if self.configUpdater.new_manu:
            self.configUpdater.new_manu = False

            new_hw_dict = self.hardwareDeterminer.get_serial()
            print("New hw details")

            self.hw_dict["serial_number"] = new_hw_dict["serial_number"]
            self.hw_dict["model"] = new_hw_dict["model"]
            self.hw_dict["hardware_version"] = new_hw_dict["hardware_version"]

            print(self.hw_dict)
            self.update_mis()
            # Parse now manufacturer details here and pass onto relevant modules, namely MIS
            self.configUpdater.hw_dict = self.hw_dict

        if self.configUpdater.new_conf:
            return 1

class Lausanne_ConsoleApp(Lausanne_App):
    """
    Class dedicated to the job of handling Hanover consoles which comply with the Lausanne MQTT solution.
    This solution is designed to be used in a scenario where consoles are required to be backwards-compatible
    with exitsing (non-ITxPT) installations. The console will be loaded with a standard ERIC.BIN database,
    and the onion's function is to intercept all the messages destined for signs on an RS485 port and to
    encapsulate these as JSON in order to publish them via MQTT.

    The messages will include status queries, to which a reply is expected. The console therefore has to
    be able to subscribe to status reply messages from signs, and to act correctly when either no replies
    are received or when an error condition is reported which would normally cause the console to signal
    that condition. The HANO-1 protocol contains no means of writing the status information to the console,
    but does provide a way to directly display to the console's screen.

    A correlation must be made by the console between the messages destined for individual signs, and since
    that is generally done via each sign's HCP Address, topics to be published make use of that, and this
    has to be used also for status replies.

    """
    TURNAROUND_TIME = 0.001     # Added to allow for RS485 turnaraound at 115K2
    NO_CONNECT_TIMEOUT = 60.0   # Number of seconds before a missing sign status forces us to say it's dead
    POLL_FREQUENCY = 5          # Number of seconds between talking to the onion over Hano1.

    def __init__(self, config_dict, hw_dict, conf_dir, data_dir):
        super().__init__(config_dict, hw_dict, conf_dir, data_dir)
        self.subStatus = Lausanne_App.SUBSCRIBE_FAILED
        self.connectedSigns = []
        self.loggedStatuses = {}
        self.publishList = deque()
        self.backgroundList = deque()
        self.syncedList = deque()
        self.syncLock = threading.RLock()
        #self.bkgLock = threading.RLock()
        self.lastStatusAddr = 0
        self.pokeTime = 0.0
        self.setup()

    def setup(self):
        """ Sets up the various elements required to run the console 

        Consoles must 
            Offer DHCP Server
            Offer services over MIS
            Checks for signs fitted
            Input: Monitors serial port
            Intercepts HMF messages
            Converts them to JSON
            Publishes topics for signs - COMMAND only
            Subscribes to signs' status query replies

            Obtains the timestamp at which this function is invoked for status reporting.
        """
        super().setup()
        #Initiate HANO-1
        self.hano1 = hano1.HANO1(
            self.config_dict["SERIAL_unix_comport"],
            self.config_dict["SERIAL_baudrate_host_console"],
            True,
            self.data_dir)
        self.displayVersion()
        #Initiate HW determiner for serial number handling
        self.hw_determine = hwDetermine.HardwareDeterminer(None, None, None, self.conf_dir)

        # Setup Serial port to snoop on sign activity. This uses a bitrate of 115200
        self.inputPort = serial.Serial(
            self.config_dict["SERIAL_rs485_comport"],
            self.config_dict["SERIAL_baudrate_host_console"],
            8, "N", stopbits=1, timeout=0.01
        )

        #Advertise MQTT broker
        print("Advertising MQTT broker")
        _thread.start_new_thread(self.broadcast_service,())

        self. show_terminal_status()

    def broadcast_service(self):
        """
        Runs in a thread to advertise the MQTT service to anyone interested.
        """
        txtrecord = {
            "txtversion": "1",
            "version": "1",
            "brand": "mosquitto",
            "manufacturer": "Hanover Displays",
            "proto": "3.1",
            "topic": self.config_dict["MQTT_broker_topic"],
        }

        mqtt_broadcast = DNS_SD.ITxPT_DNSSD(self.hw_dict["unit_IP"], "Han_%s_%s" % ("con", self.hw_dict["serial_number"]))
        print("Advertising MQTT Broker via DNS-SD...")
        mqtt_broadcast.mqtt_broker_service(txtrecord)

    def show_terminal_status(self):
        """
        Shows the Onion version and operating mode, the console version and the unit's IP Address.
        If any signs have reported their status, also shows a string depicting the status of each
        sign.
        """
        self.hano1.clear_terminal()
        self.hano1.show_on_terminal("0", "L", "Onion ver: %s %s" %
                                    (self.hw_dict["onion_ver"], self.config_dict["MODE_service_mode"]))
        self.hano1.show_on_terminal("1", "L", "Cons ver: %s" % self.hw_dict["software_version"])
        if len(self.loggedStatuses) > 0:
            self.hano1.show_on_terminal("2", "L", "IP: %s Signs: %s" % (self.hw_dict["unit_IP"], self.gatherSignStatus()))
        else:
            self.hano1.show_on_terminal("2", "L", "IP: %s" % self.hw_dict["unit_IP"])

    def displayVersion(self):
        """
        Shows the Onion version and operating mode on the console's display.
        """
        if Lausanne_App.itxpt_mqtt_app_ver == "":
            module_ver = self.hw_dict["onion_ver"]
        else:
            module_ver = "%s_%s" % (self.hw_dict["onion_ver"], Lausanne_App.itxpt_mqtt_app_ver)

        self.hano1.show_on_terminal("1", "C", "ITxPT Lausanne App Ver: %s" % (module_ver))

    def gatherSignStatus(self):
        """
        Scans through the list of logged status entries and gets the status info from each, formatting
        the results into a string in which each position depicts the status of a sign at that HCP address.
        Returns
        -------
        The string containing a depiction of the results.
        """
        result = bytearray('----------------'.encode("latin-1"))
        signNo = 0
        for item in sorted(self.loggedStatuses.items()):
            signNo = item[0] - 1
            response = ShortStatusResponse(item[1][0])
            if time.time() - item[1][1] > Lausanne_ConsoleApp.NO_CONNECT_TIMEOUT:
                result[signNo] = ord('4')
            else:
                # We lie here about 0x08 and 0x02 because otherwise we get swamped by clear sign messages from the console.
                stat = response.getStatusAsInt() & 0x04
                result[signNo] = stat + 0x30
        resultStr = result.decode("latin-1")
        finalStr = resultStr.rstrip("-")
        print("Result: [%s]" % finalStr)
        return finalStr

    def discover_services(self, servicetype, hostname):
        """
        Listens using the mDNSListener class for services which might be of use to us.

        Pulses the sign with an up-triangle/blank sequence to indicate that the waiting is
        in progress. Either finds the service's IP Address and returns it, or if after a number
        of seconds timeout (secs set in config file as MQTT_timeout) will abandon the attempt
        and return a fallback address.
        """
        mdns_listen = DNS_SD.DNSSD_Discover(servicetype, hostname)
        print("Discovering service...")

        mdns_listen.run()

        toggle = False
        timer = time.time()
        try:
            timeout = int(self.config_dict["MQTT_timeout"])
        except TypeError:
            timeout = 0

        while 1:
            serviceIP = mdns_listen.serviceIP

            if serviceIP == "":
                print("Waiting for service")
                if toggle:
                    self.hano1.show_on_terminal("1", "C", "IP: %s" % self.hw_dict["unit_IP"])
                else:
                    self.hano1.clear_terminal()
                    self.hano1.show_on_terminal("0", "C", "mDNS wait for service...")

                toggle = not toggle
                time.sleep(1)

                if timeout == 0:
                    continue
                else:
                    if (time.time() - timer) > timeout:
                        print("Cannot find service, reverting to fallback")
                        self.hano1.show_on_terminal("0", "C", "Service not advertised")
                        self.hano1.show_on_terminal("1", "C", "Using fallback at:")
                        self.hano1.show_on_terminal("2", "C", "IP: %s" % self.config_dict["MQTT_fallback_address"])
                        time.sleep(2)
                        self.hano1.clear_terminal()
                        return self.config_dict["MQTT_fallback_address"]
            else:
                print("Service Found:" + serviceIP)
                self.hano1.show_on_terminal("0", "L", "Service found at")
                self.hano1.show_on_terminal("1", "C", "IP: %s" % serviceIP)
                return serviceIP

    def connect_to_service(self, brokerAddress):
        """ Defines the topics we're interested in subscribing to and publishing.

        We're interested in subscribing to all the sign's status topics.
            raw_sign_status_topic: screwball/dpi/sign/status/#/json
        We're interested in publishing all the commands for each sign, and in any
        commands which are being broadcast to address 0.
            raw_command_topic: screwball/dpi/sign/command/#/json
            raw_broadcast_topic: screwball/dpi/sign/all/0/json

        Returns
        -------
        Lausanne_App.SUBSCRIBE_OK if all is well, otherwise Lausanne_App.SUBSCRIBE_FAILED.
        """
        # Set up the list of topics to subscribe to - signs status for HCP addresses 1 to 15
        generalTopic = self.config_dict["MQTT_raw_sign_status_topic"]
        for addr in range(1, 17):
            topic = generalTopic.replace("#", str(addr))
            self.subscribeList.append(topic)

        # Signal that we have reached the point where we're attempting to subscribe to the required service.
        self.hano1.show_on_terminal("1", "C", "Subscribing...")
        # Attempt to subscribe to the sign status topics.
        status = self.attemptSubscription(brokerAddress)
        self.subStatus = status

        print("MQTT Subscription Status: %s" % status)
        # Check the subscription status
        if status == Lausanne_App.SUBSCRIBE_OK:
            print("Running MQTT Subscriber Thread")
            _thread.start_new_thread(self.mqtt_sub.run_client, ())
            _thread.start_new_thread(self.pollInputPort, ())
        else:
            print("MQTT Subscribe Error")
        return status

    def process_message(self, payload):
        """ Attempts to process the JSON payload received. This will consist of
        status reports from any connected signs. Other MQTT messages will be ignored.
        Status replies must be stored so they can be used later, and timestamped so
        we can discover if a sign which has responded takes too long between reports,
        meaning it has become disconnected.
        Parameter
        ---------
        payload: str
            The payload to be processed.
        """
        hcpMessage = HMFmsgFromJSON(payload)
        msg = hcpMessage.extractHMF()
        self.loggedStatuses[hcpMessage.address] = (msg, time.time())
        print("\tStatus Rxd: %s %s" % (PrintText.to_ascii(msg), time.time()))

    def sendStatusReply(self, hmfMsg):
        """
        Originally intended to send a status reply pre-assembled from the list of logged
        Status information.
        Parameter
        ---------
        hmfMsg: RawHMFmsg
            The status query received.
        Note
        ----
            Currently this doesn't actually send the message, because timings are so loose
            that it is liable to interfere with incoming messages.
        """
        key = hmfMsg.address
        if key in self.loggedStatuses and key != self.lastStatusAddr:
            msg = self.loggedStatuses[hmfMsg.address][0]
            #self.inputPort.write(msg.encode("latin-1"))
            self.lastStatusAddr = key
            print("%s ->>> - %s" % (PrintText.to_ascii(msg), time.time()))

    def pollInputPort(self):
        """
        This is the serial port monitor loop. Its job is to assemble incoming HMF messages from
        the Console and to place them in appropriate lists to allow them to be sent via MQTT in a
        properly synchronised manner.

            See inline comments for quirks.
        """
        stuff = ""
        if self.inputPort.isOpen():
            body = []
            tail = 0
            accumulate = False
            self.inputPort.reset_input_buffer()
            while True:
                waiting = self.inputPort.in_waiting
                if waiting > 0:
                    for c in self.inputPort.read(size=waiting):
                        # Parse input character by character
                        if c == RawHMFmsg.STX and not accumulate:
                            accumulate = True
                        if accumulate:
                            body.append(chr(c))
                        if c == RawHMFmsg.ETX and accumulate:
                            # The ETX marker is 2 characters before the final char of the LRC.
                            # so we use tail to count them in...
                            tail = 1
                        elif tail == 1 and accumulate:
                            tail = 2
                        elif tail == 2 and accumulate:
                            # We now have the last char of the checksum, so try to create a RawHMFmsg.
                            accumulate = False
                            tail = 0
                            stuff = "".join(x for x in body)
                            try:
                                hmfMsg = RawHMFmsg(stuff)
                            except HMFError as e:
                                body = []
                                print("Exception: %s" % e)
                                break
                            if hmfMsg.isShortStatusQuery():
                                #print(" ->>> %s - %s" % (PrintText.to_ascii(stuff), time.time()))
                                body = []
                                #self.sendStatusReply(hmfMsg)
                                break
                            elif hmfMsg.isValid:
                                addr = hmfMsg.address
                                if hmfMsg.isClearSignMsg():
                                    addrs = self.loggedStatuses.keys()
                                    if len(addrs) > 0:
                                        if addr  in addrs:
                                            self.clearSyncedList(addr, hmfMsg)
                                            print("!!!!!!!!!!! Clear sign for addr %d - %s !!!!!!!!!" % (addr, time.time()))
                                if addr == 0:
                                    topic = self.config_dict["MQTT_raw_broadcast_topic"]
                                    self.addToPublishList(topic, hmfMsg)
                                else:
                                    topic = self.config_dict["MQTT_raw_command_topic"].replace("#", str(addr))
                                    self.addToPublishList(topic, hmfMsg)
                                body = []
                                break
                            else:
                                print("\nXXXXXXXXXX\tXXXXXXXXX BAD MESSAGE RX'd - %s\n" % time.time())
                                PrintText.print_ascii(body)
                                body = []
                time.sleep(Lausanne_ConsoleApp.TURNAROUND_TIME)
        else:
            print("Could not open serial port.")

    def clearSyncedList(self, addr, hmfMsg):
        """
        Clears any non-clear-sign message which might be queued to be sent to the sign at the specified address.
            Called when a Clear Sign message has been received.
        Parameter
        ---------
        addr: int
            The address of the sign whose messages must be removed from the list.
        """
        self.syncLock.acquire()
        for item in self.syncedList.copy():
            if item[1].address == addr:
                if not hmfMsg.isClearSignMsg:
                    self.syncedList.remove(item)
        self.syncLock.release()

    def addToPublishList(self, topic, hmfMsg):
        """
        Adds a topic and message to one of the lists of cached messages. Will only add messages
        for those signs which have reported on their status.

        Parameters
        ----------
        topic: str
            The topic against which the message will be sent.
        hmfMsg: RawHMFmsg
            The message to be sent.
        Note
        ----
            Graphics commands and Left or Right-handed Text messages must be
            synchronised. Others don't need to be.
        """
        cmd = hmfMsg.command
        addr = hmfMsg.address
        if addr in self.loggedStatuses or addr == 0:
            if cmd == RawHMFmsg.CMD_GRAPHIC or cmd == RawHMFmsg.CMD_LH_TEXT or cmd == RawHMFmsg.CMD_RH_TEXT or cmd == RawHMFmsg.CMD_CLEAR:
                self.addToSyncedList(topic, hmfMsg)
            else:
                self.addToBackgroundList(topic, hmfMsg)
        else:
            print("Address %d not yet acknowledged." % addr)

    def addToBackgroundList(self, topic, hmfMsg):
        """
        Adds a message to the list of items which don't need to be closely synchronised, if the message
        hasn't already been cached in the list.
        Parameters
        ----------
        topic: str
            The topic under which the message will be published.
        hmfMsg: RawHMFmsg
            The message to be published.
        """
        cmd = hmfMsg.command
        addr = hmfMsg.address
        skip = False
        try:
            for item in self.backgroundList.copy():
                if item[1].address == addr and item[1].command == cmd:
                    print("BG Q Skip: %x %x %s" % (addr, cmd, time.time()))
                    skip = True
            if not skip:
                print("BG Q Add: %x %x %s" % (addr, cmd, time.time()))
                self.backgroundList.append((topic, hmfMsg, addr))
        except Exception as e:
            print(e)

    def addToSyncedList(self, topic, hmfMsg):
        """
        Adds a message to the list of items which must be closely synchronised, if there isn't
        already a message waiting there for a sign with the same address.
        Parameters
        ----------
        topic: str
            The topic under which the message will be published.
        hmfMsg: RawHMFmsg
            The message to be published.
        """
        addr = hmfMsg.address
        chk = hmfMsg.checksum
        self.syncLock.acquire()
        skip = False
        for item in self.syncedList:
            if item[1].address == addr and item[1].checksum == chk:
                #print("SY Q Skip: %x %s %s" % (addr, chk, time.time()))
                skip = True
        if not skip:
            print("SY Q Add: %x %s %s" % (addr, chk, time.time()))
            self.syncedList.append((topic, hmfMsg, addr))
        self.syncLock.release()

    def pokeHano1(self):
        """
        Polls the Console to find out whether any status information needs to be displayed.
        If it's not, then send the Set Status command via HANO-1F to the console.
        Also ensures that the console knows there's an onion in the stew.
        """
        if self.pokeTime == 0 or (time.time() - self.pokeTime) > Lausanne_ConsoleApp.POLL_FREQUENCY:
            status = self.hano1.poll_console()
            if status[0] == 2 or status[0] == 4:
                self.show_terminal_status()
            elif status[0] == 3:
                #Keypresses not needed in this mode.
                pass
            else:
                # Send Sign status info to the console.
                statusStr = self.gatherSignStatus().replace("-", ".")
                self.hano1.sendSignStatus(statusStr)
            self.pokeTime = time.time()

    def runPublishLoop(self):
        """
        Foreground task (in the run() method) calls this to pick out and publish all the MQTT topics.
            Deals first with the background messages, which don't require synchronisation, and then
            with the list of messages which should arrive simultaneously at the signs.
        """
        listLen = len(self.backgroundList)
        if listLen > 0:
            print("\tBG Q count: %d" % listLen)
        while len(self.backgroundList) > 0:
            item = self.backgroundList.popleft()
            topic = item[0]
            mqttMsg = item[1].encodeAsJSON()
            print("\tBG Q rem %x %x %s" % (item[1].address, item[1].command, time.time()))
            self.mqtt_sub.publish_data(topic, mqttMsg)
        if listLen > 0:
            print("\tBG Q cleared: %s" % time.time())

        listLen = len(self.syncedList)
        if listLen > 0:
            print("\tSY Q count: %d" % listLen)
        # Send the clear sign messages first, if any.
        self.syncLock.acquire()
        for item in self.syncedList.copy():
            if item[1].command == RawHMFmsg.CMD_CLEAR:
                if item[1].validate():
                    mqttMsg = item[1].encodeAsJSON()
                    self.mqtt_sub.publish_data(item[0], mqttMsg)
                    self.syncedList.remove(item)
                    print("\tSY Clear Sign sent to address: %x, SY Q count: %d" % (item[1].address, len(self.syncedList)))
        self.syncLock.release()

        time.sleep((Lausanne_ConsoleApp.TURNAROUND_TIME * 100))

        listLen = len(self.syncedList)
        if listLen >= len(self.loggedStatuses) and listLen > 0:
            self.syncLock.acquire()
            while len(self.syncedList) > 0:
                item = self.syncedList.popleft()
                if not item[1].command == RawHMFmsg.CMD_CLEAR:
                    topic = item[0]
                    print("\tSY Q pub %x %s %s" % (item[1].address, item[1].checksum, time.time()))
                    if item[1].validate():
                        mqttMsg = item[1].encodeAsJSON()
                        self.mqtt_sub.publish_data(topic, mqttMsg)
                    else:
                        print("\tInvalid HMF message! Topic: %s" % topic)
            self.syncLock.release()
        if listLen > 0 and len(self.syncedList) == 0:
            print("\tSY Q cleared: %s" % time.time())

    def run(self):
        """ The main execution loop of the Lausanne flavoured console application """
        print("Lausanne-MQTT service running on console only")
        if self.config_dict["MQTT_discover"]:
            while 1:
                self.brokerAddress = self.discover_services(self.config_dict["MQTT_service_type"], self.config_dict["MQTT_primary_hostname"])
                if self.brokerAddress != 0:
                    break

        else:
            self.brokerAddress = self.config_dict["MQTT_fallback_address"]

        status = self.connect_to_service(self.brokerAddress)

        if status == Lausanne_App.SUBSCRIBE_OK:
            self.displayVersion()
        else:
            self.hano1.show_on_terminal("1", "L", "Subscription Failed!")

        while Lausanne_App.RUNNING:
            if not Lausanne_App.RUNNING:
                time.sleep(20)
            else:
                if self.check_config_updater():
                    break
                time.sleep(0.5)
                self.runPublishLoop()
                if self.poll_mqtt():
                    self.process_message(self.mqtt_sub.payload)
                self.pokeHano1()

class Lausanne_SignApp(Lausanne_App):
    """
    Class dedicated to the job of handling Hanover signs which comply with the Lausanne MQTT specification.
    """

    SIGN_BRIGHTNESS_NORMAL = 0
    SIGN_BRIGHTNESS_DIMMED = 1
    SIGN_BRIGHTNESS_BLANKED = 2

    def __init__(self, config_dict, hw_dict, conf_dir, data_dir):
        """ Constructor. Just calls the superclass constructor and then this subclass' setup() method.

        Args:
            config_dict ([type]): [description]
            hw_dict ([type]): [description]
            conf_dir ([type]): [description]
            data_dir ([type]): [description]
        """
        super().__init__(config_dict, hw_dict, conf_dir, data_dir)
        self.setup()

    def setup(self):
        """ Sets up the various elements required to run the sign Lausanne MQTT feature.

            Instantiates the ECO MODE monitor and sets its handler control variables
            Sets up the serial port for signs.
            Creates an HCP parser.
            Reads in the graphics symbols needed to show status on the sign & shows the startup state.
            Sets up Renderbox.

        """
        super().setup()
        # ECO MODE monitor class:
        self.ecoMon = ecoMonitor.ECOMonitor()
        # ECO MODE control variables:
        self.signs_blanked = False                      # Start by assuming signs not blanked
        self.blank_level = Lausanne_SignApp.SIGN_BRIGHTNESS_NORMAL       # Start by leaving signs alone.

        #Setup Serial ports
        self.ser = serial.Serial(
            self.config_dict["SERIAL_unix_comport"],
            self.config_dict["SERIAL_baudrate_host_sign"],
            timeout = 0.2
        )

        # Needed for sending whiffles to the sign
        self.hcp = hcp.HCP()

        # Setup Graphic library for signs to indicate status
        self.setup_graphic_library()

        # Show MIS services ready by sending upwards pointing triangle to sign
        self.sendToSign(self.graphic_dict["TRI-UP_%s" % self.hw_dict["hw_type"]])

        #Initiate Renderbox
        if self.config_dict["RENDERBOX_enable"]:
            from hanip.onionip import renderbox
            self.rb = renderbox.RenderBox(self.config_dict)
        addr = int((self.hw_dict["address"])) + 1
        self.statusQuery = self.hcp.encodeMaster("2%X" % addr)
        print("Status query: ")
        PrintText.print_ascii(self.statusQuery)

        reply = self.hcp.encodeSlave("2%X00" % addr)
        self.currStatus = RawHMFmsg(reply).encodeAsJSON()
        print(self.currStatus)

    def setup_graphic_library(self):
        """
        This used to rely on the sign graphics file but as that has not changed in many versions and to simply things
        it is now based in sign task for all modules that import this module.

        Any additions/changes here should be reflected in signApp if appropriate.
        """
        # CHECK changes signApp!!!
        self.graphic_dict = {
            "SQUARE_ext": r"00{\mode0\mss\at\al\pic\picw3\pich3 070507}",
            "TRI-UP_ext": r"00{\mode0\mss\at\al\pic\picw3\pich3 070301}",
            "TRI-DOWN_ext": r"00{\mode0\mss\at\al\pic\picw3\pich3 040607}",
            "ERROR_X_ext": r"00{\mode0\mss\at\al\pic\picw3\pich3 050205}",
        }

    def discover_services(self, servicetype, hostname):
        """
        Listens using the mDNSListener class for services which might be of use to us.

        Pulses the sign with an up-triangle/blank sequence to indicate that the waiting is
        in progress. Either finds the service's IP Address and returns it, or if after a number
        of seconds timeout (secs set in config file as MQTT_timeout) will abandon the attempt
        and return a fallback address.
        """
        mdns_listen = DNS_SD.DNSSD_Discover(servicetype, hostname)
        print("Discovering service...")

        mdns_listen.run()

        toggle = False
        timer = time.time()
        try:
            timeout = int(self.config_dict["MQTT_timeout"])
        except TypeError:
            timeout = 0

        while 1:
            serviceIP = mdns_listen.serviceIP

            if serviceIP == "":
                print("Waiting for service")
                if toggle:
                    self.sendToSign(self.graphic_dict["TRI-UP_%s" % self.hw_dict["hw_type"]])
                else:
                    self.sendToSign("C0")

                toggle = not toggle
                time.sleep(1)

                if timeout == 0:
                    continue
                else:
                    if (time.time() - timer) > timeout:
                        print("Cannot find service, reverting to fallback")
                        return self.config_dict["MQTT_fallback_address"]
            else:
                print("Service Found:" + serviceIP)
                return serviceIP

    def connect_to_service(self, brokerAddress):
        """ Defines the topics we're interested in subscribing to and publishing.

        We're interested in publishing the sign's status topics.
            raw_sign_status_topic: screwball/dpi/sign/status/#/json
        We're interested in subscribing to all the commands for this sign, and in any
        commands which are being broadcast to address 0.
            raw_command_topic: screwball/dpi/sign/command/#/json
            raw_broadcast_topic: screwball/dpi/sign/all/0/json

        Sends a visual clue to how the connection is going (show a down triangle).
        Attempts to make the connection, and if successful, publishes the unit's
        version topic before starting the MQTT client thread.

        Returns
        -------
        Lausanne_App.SUBSCRIBE_OK if all is well, otherwise Lausanne_App.SUBSCRIBE_FAILED.
        """
        # First, define the topics; we subscribe to the journey topic and must publish
        # the version and state topics
        hcpAddress = str(int(self.hw_dict["address"]) + 1)
        topicURI = self.config_dict["MQTT_raw_command_topic"].replace("#", hcpAddress)
        self.subscribeList.append(topicURI)
        topicURI = self.config_dict["MQTT_raw_broadcast_topic"].replace("#", hcpAddress)
        self.subscribeList.append(topicURI)

        self.statusTopic = self.config_dict["MQTT_raw_sign_status_topic"].replace("#", hcpAddress)

        # Signal that we have reached the point where we're attempting to subscribe to the required service.
        self.sendToSign(self.graphic_dict["TRI-DOWN_%s" % self.hw_dict["hw_type"]])
        # Attempt to subscribe to the journey topic
        status = self.attemptSubscription(brokerAddress)

        print("MQTT Subscription Status: %s" % status)
        # Check the subscription status
        if status == Lausanne_App.SUBSCRIBE_OK:
            self.publishUnitStatus()
            print("Running MQTT Subscriber Thread")
            _thread.start_new_thread(self.mqtt_sub.run_client, ())
        else:
            print("MQTT Subscribe Error")
        return status

    def process_message(self, payload):
        """ Processes the incoming JSON entity, which may be one of a number of HCP Commands that
        may either be sent directly to the sign controller without waiting for a reply, or which
        may be status queries expecting a reply.

        TODO: These commands are not yet fully dealt with!
            CMD_ADVERT = 0x05   (Find out if this is used any more)
            CMD_TTS = 0x07      (probably can be ignored)
            CMD_ESC_BIN = 0x0E  (*** This will need to be dealt with properly ***)
        Parameter
        ---------
        payload: str
            The payload to be processed.
        """
        print("JSON received")
        process = True
        incoming = HMFmsgFromJSON(payload)
        cmd = incoming.command
        if cmd == RawHMFmsg.CMD_STATUS:
            print("Status Query Rxd:")
            msg = incoming.extractHMF()
            if self.readSignStatus(msg):
                self.publishUnitStatus()
            return
        elif cmd == RawHMFmsg.CMD_EXT_STAT:
            print("Extended Status Query Rxd.")
            # It seems that we only care about the short extended status message.  So if an extended one is received,
            # I guess no point in sending it onto the sign...
            return
        elif cmd == RawHMFmsg.CMD_GRAPHIC:
            print("Graphics command Rxd:")
        elif cmd == RawHMFmsg.CMD_LH_TEXT or cmd == RawHMFmsg.CMD_RH_TEXT:
            print("LH/RH Text command Rxd:")
        elif cmd == RawHMFmsg.CMD_RN_BROAD:
            print("Route No Broadcast Rxd.")
        elif cmd == RawHMFmsg.CMD_PARAM_B:
            print("Param Broadcast Rxd.")
        elif cmd == RawHMFmsg.CMD_CLEAR:
            print("Clear Sign Rxd.")
        elif cmd == RawHMFmsg.CMD_TEST_MSG:
            print("Sign Self-test Rxd.")
        else:
            process = False
            print(payload)
        if process:
            msg = incoming.extractHMF()
            print("Rxd:\n%s" % PrintText.to_ascii(msg))
            if not self.signs_blanked:
                self.sendToSign(msg)
            time.sleep(0.2)

    def readSignStatus(self, query):
        """
        Sends a Status Query to the sign and attempts to read the reply.

        If a reply is obtained, updates the current status with a JSON encoded RawHMFmsg,
        ready to publish when scheduled.
        Returns
        -------
        True if a reply is obtained, otherwise False.
        """
        self.sendToSign(query)
        reply = self.ser.read(8).decode("latin-1")
        print("Reply: %s" % PrintText.to_ascii(reply))
        if len(reply) >= 6:
            try:
                self.currStatus = RawHMFmsg(reply).encodeAsJSON()
                return True
            except Exception as e:
                print(e)
                return False

    def publishUnitStatus(self):
        """ Publishes the Lausanne sign status message, which is a JSON encoded
        HMF status response message.

        """
        self.mqtt_sub.publish_data(self.statusTopic, self.currStatus)

    def sendStatus(self):
        """
        Status Information is sent according to an interval timer set by Lausanne_App.STATUS_PUB_FREQ.
        As soon as possible after startup and whenever the interval elapses, the sign's status is read
        and published.
        """
        if self.status_wait_time == 0 or (time.time() - self.status_wait_time) > Lausanne_App.STATUS_PUB_FREQ:
            if self.readSignStatus(self.statusQuery):
                self.publishUnitStatus()
            self.status_wait_time = time.time()

    def sendToSign(self, message):
        """ Formats the message passed in into a Hanover Message Frame and sends it over the
        serial link.
        Parameters
        ----------
        message: str
            The contents of the message to be sent.
        """
        #The store function is purely for allowing the sign to resume its display if a blank event for ECO
        if message[0] != "\x02":
            message = self.hcp.encodeMaster(message)
        time.sleep(0.1)
        self.ser.write(message.encode("latin-1"))

    def setMaxMinBrightness(self, max, min):
        """[summary]

        Args:
            max ([type]): [description]
            min ([type]): [description]
        """
        msg = "90SC=MB%s;MINB%s" % (max, min)
        self.sendToSign(msg)
        print("MAX/MIN %s" % msg)

    def handleEcoMode(self):
        """ Takes care of controlling the sign's brightness when ITxPT's ECO MODE is
        required.

        The Eco Monitor looks at the digital input, which is connected to a notional
        signal indicating whether the sign should dim for certain period, after which
        it blanks, or whether it should operate normally.
        We keep track of the following states:
            0: Normal operation
            1: Signs dimmed to reduce power draw
            2: Signs blanked
        If we are currently in States 1 or 2 and the monitor says the ignition signal
        is ACTIVE, we must revert to normal operation.
        If we are in normal operation and the ignition is in state INACTIVE, we transition
        to State 1.
        If we are in State 1 and the Blanking Timer has expired, we enter State 2.

        """
        if self.config_dict["ECOMODE_enable"]:
            stat, offTimer = self.ecoMon.getIgnitionStatus()

            if stat == "0": # Means ignition is ACTIVE
                if self.blank_level != Lausanne_SignApp.SIGN_BRIGHTNESS_NORMAL:
                    self.blank_level = Lausanne_SignApp.SIGN_BRIGHTNESS_NORMAL
                    default_min = self.config_dict["BRIGHTNESS_min_brightness"]
                    default_max = self.config_dict["BRIGHTNESS_max_brightness"]
                    print("ECOMODE: Exiting mode.")
                    self.setMaxMinBrightness(default_max, default_min)
                    self.signs_blanked = False
                else:
                    pass
            else:   # stat == "1", meaning INACTIVE
                if self.blank_level == Lausanne_SignApp.SIGN_BRIGHTNESS_NORMAL:
                    self.blank_level = Lausanne_SignApp.SIGN_BRIGHTNESS_DIMMED
                    blanking_level = self.config_dict["ECOMODE_blanking_level"]
                    print("ECOMODE: Dimming sign.")
                    self.setMaxMinBrightness(blanking_level, blanking_level)
                elif self.blank_level == Lausanne_SignApp.SIGN_BRIGHTNESS_DIMMED:
                    ign_off_time = (int(time.time() - offTimer))
                    if (ign_off_time / 60) > int(self.config_dict["ECOMODE_blank_after"]):
                        self.blank_level = Lausanne_SignApp.SIGN_BRIGHTNESS_BLANKED
                        print("ECOMODE: Blanking sign.")
                        self.sendToSign("C0")
                        self.signs_blanked = True

    def run(self):
        """ The main execution loop of the Lausanne flavoured sign application """
        print("Lausanne-MQTT service running on Sign only")
        if self.config_dict["MQTT_discover"]:
            while 1:
                self.brokerAddress = self.discover_services(self.config_dict["MQTT_service_type"], self.config_dict["MQTT_primary_hostname"])
                if self.brokerAddress != 0:
                    break

        else:
            self.brokerAddress = self.config_dict["MQTT_fallback_address"]

        status = self.connect_to_service(self.brokerAddress)

        if self.hw_dict["hw_type"] == "hires":
            pass
        else:
            if status == Lausanne_App.SUBSCRIBE_OK:
                self.sendToSign(self.graphic_dict["SQUARE_%s" % self.hw_dict["hw_type"]])
            else:
                self.sendToSign(self.graphic_dict["ERROR_X_%s" % self.hw_dict["hw_type"]])

        while Lausanne_App.RUNNING:
            if not Lausanne_App.RUNNING:
                time.sleep(20)
            else:
                self.handleEcoMode()
                if self.poll_mqtt():
                    self.process_message(self.mqtt_sub.payload)
                self.sendStatus()
                if self.check_config_updater():
                    break
                time.sleep(0.01)

if __name__ == "__main__":
    print("(Main function for testing only)")

