"""
Author: Cooper
Date: 14/01/2020
Modified: 19/02/2025

This is the new VIMI module which only now deals with VIMI data and status
All previous

"""
import time
import logging
from copy import deepcopy

from hanip.itxpt import mqtt_payload_parser
from hanip.itxpt import status_handler


class VIMIMQTT(object):
    def __init__(self, config_dict, hw_dict):
        """
        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

        #Status variables
        self.status_poll_frequency = 30 # Interval in secs between auto-publishing the unit's status info. (ORIG: 300)
        self.status_publish_frequency = 30 # Interval in secs between short status requests & replies expected from sign. (ORIG: 30)
        self.sign_max_dest_text = 20

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

        self.mqpp =  mqtt_payload_parser.MQTTPayloadParser(self.config_dict)
        self.status_handler = status_handler.StatusHandler(self.hw_dict, self.config_dict)

        self.startTime = self.status_handler.getSystemUptime()

    """
    ###################################################################################################################
    VIMI Status Handlers
    """
    def get_vimi_status_topics(self):
        """
        This provides the topics that are meant to be used for status publishing
        """
        mac_as_num = self.status_handler.get_mac_as_string()
        vimi_status_topic = "/unit/hanover/sign/#/state".replace("#", mac_as_num)
        vimi_version_topic = "/unit/hanover/sign/#/version".replace("#", mac_as_num)
        vimi_config_topic = "/unit/hanover/sign/#/configuration".replace("#", mac_as_num)

        return vimi_status_topic, vimi_version_topic, vimi_config_topic

    def update_sign_status(self, short_status):
        """
        This allows the importing module to update the status within this module.
        """
        self.sign_short_status = short_status

    def get_vimi_version_message(self):
        """
        This generates the version message that seems to be only published at connection to a broker
        """
        version_message_dict = {
            "presentationName": "Hanover Sign",
            "versionName": self.hw_dict["onion_ver"]
        }

        return self.mqpp.convDictToJSON(version_message_dict, pretty=False)

    def get_configuration_status(self):
        """
        Config status ala VT.
        """
        vt_config = self.status_handler.generate_vt_config_message()
        vt_dict = self.mqpp.parseJSON(vt_config)

        config_dict = {
            "swVersion": self.hw_dict["onion_ver"],
            "swVersionHost": self.hw_dict["software_version"]
        }

        config_dict.update(vt_dict)

        return self.mqpp.convDictToJSON(config_dict, pretty=False)

    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) > self.status_poll_frequency:
            self.gatherStatusInfo()
            self.statusPollTime = time.time()

    def get_status(self):
        """ Publishes the status topic every 5 mins or when status has changed. """
        if self.statusChanged:
            logging.info("VIMI: STATUS CHANGE")
            self.statusChanged = False
            self.status_wait_time = self.status_last_update = time.time()
            return self.mqpp.convDictToJSON(self.currStatus, pretty=False)
        elif self.status_wait_time == 0 or (time.time() - self.status_wait_time) > self.status_publish_frequency:
            logging.info("VIMI: POLL INTERVAL %s" % (time.time() - self.status_wait_time))
            self.statusChanged = False
            self.status_wait_time = self.status_last_update = time.time()
            return self.mqpp.convDictToJSON(self.currStatus, pretty=False)
        else:
            return None

    def fillVIMIerrorList(self, statusByte):
        """ Fills in the list of errors for the VIMI state topic.

        Currently only returns a list of one error, since the signs can only usefully
        report one error at a time. Extends things slightly by reporting on complete comms
        failure, which is inferred rather than directly reported.

        The sign status is cumulative and is cleared on transmission of this message.
        Bits 0-2 operational status:
        0 No errors
        1 Message content error (eg, text message too large for the display).
        2 Transmission error (eg, incorrect checksum).
        3 Halogen lamp failure
        4 Comms Failure

        Bit 3 Sign busy (scrolling)
        Bit 4 Buffer full

        Parameter
        ---------
        The character returned as the status byte by the sign query's reply.
        """
        errorByte = statusByte & 0x07

        errorList = []
        errorDict = {
                "code": statusByte
            }

        if errorByte == 0x00:
            errorDict["severity"] = 0
            errorDict["message"] = "No errors"
        elif errorByte == 0x01:
            errorDict["severity"] = 1
            errorDict["message"] = "Message Content Error"
        elif errorByte == 0x02:
            errorDict["severity"] = 2
            errorDict["message"] = "Transmit Error"
        elif errorByte == 0x03:
            errorDict["severity"] = 3
            errorDict["message"] = "Halogen Lamp Failure"
        elif errorByte == 0x04:
            errorDict["severity"] = 4
            errorDict["message"] = "Comms Failure - Sign unreachable"
        else:
            errorDict["severity"] = 5
            errorDict["message"] = "Unrecognised Error"

        errorList.append(errorDict)

        if statusByte & 0xF0:
            warnDict = {
                    "code": statusByte
            }
            if statusByte & 0x08:
                warnDict["severity"] = 0
                warnDict["message"] = "Sign Busy"
            elif statusByte & 0x10:
                warnDict["severity"] = 1
                warnDict["message"] = "Sign Buffer Full"
            errorList.append(warnDict)

        return errorList

    def readSignStatusByte(self):
        """
        Obtains the short sign status from sign_task, this status can be 1 of three values:
        Empty: sign_task hasn't had a chance to grab this yet
        None: sign did not respond to a status query
        Status: The current sign status

        Returns
        -------
        An int containing the value supplied by the sign. A value of 0xFF means comms fail.
        """
        short_status = self.sign_short_status

        if short_status is None:
            statusByte = 0x04

        elif len(short_status) == 0:
            #If the short status is empty, it means that sign_task hasn't had a chance to obtain a value yet.
            #Lets pretend everything is ok until then :)
            statusByte = 0x00
        else:
            statusByte = int(short_status, 16)

        return statusByte

    def gatherStatusInfo(self):
        """
        Make any queries necessary to fill in the status topic.

        Compares the previous status with current, setting the statusChanged flag if any
        relevant item has changed. Relevant items are run state, errors since last update,
        & possibly application start time. If any changes are seen, it will do a deep copy
        of current status to previous.

        Side effects
        ------------
        May result in currStatus and/or prevStatus changing, as well as statusChanged flag.
        """

        self.currStatus = {}
        self.statusChanged = False
        # Obtain run state - compare to previous status
        runState = "running"
        try:
            if runState != self.prevStatus["status"]:
                self.statusChanged = True
        except KeyError:
            self.statusChanged = True

        # Obtain any errors - compare to previous error code byte
        errorByte = self.readSignStatusByte()
        if self.prevErrorByte != errorByte:
            # print("prevByte ", self.prevErrorByte, "!= currByte ", errorByte)
            self.prevErrorByte = errorByte
            self.statusChanged = True
        errors = self.fillVIMIerrorList(errorByte)

        # Obtain 'now' timestamp - no comparison required
        timestampNow = self.status_handler.get_timestamp()

        # Obtain application start time as uptime of class creation - compare to previous
        try:
            if self.startTime != self.prevStatus["uptimeStart"]:
                # print("PrevUp != currUp")
                self.statusChanged = True
        except KeyError:
            # print("Empty status!")
            self.statusChanged = True

        self.currStatus = {
            "status": runState,
            "error": errors,
            "epochNow": timestampNow,
            "uptimeStart": self.startTime,
            "uptimeNow": self.status_handler.getSystemUptime(),
            "publishInterval": self.status_publish_frequency
        }

        if self.statusChanged:
            self.prevStatus = deepcopy(self.currStatus)

    def get_last_will_payload(self):
        """
        Not sure how this is supposed to look as the numbers won't mean anything?

        """
        last_will = {
            "status": "dead",
        }

        return self.mqpp.convDictToJSON(last_will, pretty=False)

    def check_uptime_expired(self):
        """
        This belongs with ecomode functionality

        Checks how long the sign has been alive for and if it has expired return True, else return false in all other
        scenarios
        """
        auto_reboot_en = self.config_dict.get("ECOMODE_auto_reboot_en", False)
        max_uptime = self.config_dict.get("ECOMODE_max_uptime", 86400)          #Time in seconds

        if auto_reboot_en:
            current_uptime = self.status_handler.getSystemUptime()

            try:
                _current_uptime = float(current_uptime)
                _max_uptime = float(max_uptime)
            except ValueError:
                return False
            else:
                #Limit the max uptime to 5 mins because the sign will end up in a bootloop...
                if _max_uptime < 300:
                    _max_uptime = 300
                if _current_uptime > _max_uptime:
                    return True
                else:
                    return False
        else:
            return False

    """
    ###################################################################################################################
    VIMI Sign data handlers
    """
    def generate_sign_task_dict(self, route_number, topline, bottomline):
        """
        This bypasses all the original templating system and just makes a dict for sign_task to consume and process
        """
        if len(bottomline) > 0:
            dest_text = "%s/%s" % (topline, bottomline)
        else:
            dest_text = topline

        display_data = {
            "$bcol": "0,0,0",
            "$fcol": self.config_dict.get("TEMPLATES_default_foreground", "255,170,0"),
             "$rn": route_number,
            "$dest": [dest_text]
        }

        return display_data

    def doBraceSplit(self, splitText, braceAt, destText):
        logging.debug("VIMI: < doBraceSplit")
        if not splitText:
            topLine = destText
            bottomLine = ""
        else:
            topLine = destText[0:braceAt]
            bottomLine = destText[braceAt:]

        return topLine, bottomLine

    def doViaSplit(self, splitText, viaAt, destText):
        logging.debug("VIMI: > doViaSplit")
        if not splitText:
            topLine = destText
            bottomLine = ""
        else:
            bottomLine = destText[viaAt:]
            logging.debug("VIMI: bottomLine: %s" % bottomLine)
            if len(bottomLine) > self.sign_max_dest_text:
                # Bottom line too long - put the via back in top line
                logging.debug("VIMI: BottomLine length: %d" % len(bottomLine))
                newpos = viaAt + int(self.config_dict.get("VIMI_new_pos", "4"))
                topLine = destText[0:newpos]
                bottomLine = destText[newpos:]
                logging.debug("VIMI: newpos: %d" % newpos)
            else:
                # Bottom line will fit, so break after token.
                topLine = destText[0:viaAt]
                logging.debug("VIMI: topLine: %s" % topLine)

        return topLine, bottomLine

    def doPlainSplit(self, splitText, viaAt, destText):
        destText = destText.lstrip()
        if not splitText:
            topLine = destText
            bottomLine = ""
        else:
            topLine = destText
            countFwd = 0
            while countFwd != self.sign_max_dest_text:
                if topLine[countFwd] == " ":
                    break
                countFwd += 1
            topLine = destText[0:countFwd]
            bottomLine = destText[countFwd:]

        return topLine, bottomLine

    def breakLinesForTokens(self, destText):
        """Breaks lines where they need to be broken if they contain a "via" or
        opening brace token.
        Also takes care of text that may be too long to show on a single line.

        Args:
            destText (str): String containing all text to be shown.
        Returns:
            Top and bottom lines if token found or the text is too long for a single
            line, otherwise top line and an empty string
        """
        viaFound = destText.find(self.config_dict.get("VIMI_via_split", " via").lower())
        VIAfound = destText.find(self.config_dict.get("VIMI_via_split", " via").upper())
        viaAt = max(viaFound, VIAfound)
        braceAt = destText.find("(")

        splitText = len(destText) > self.sign_max_dest_text or viaAt > -1 or braceAt > -1

        if braceAt > -1:
            topLine, bottomLine = self.doBraceSplit(splitText, braceAt, destText)
        elif viaAt > -1:
            topLine, bottomLine = self.doViaSplit(splitText, viaAt, destText)
        else:
            topLine, bottomLine = self.doPlainSplit(splitText, -1, destText)

        return topLine.strip(), bottomLine.strip()

    def process_message(self, payload):
        """ Attempts to process the JSON payload received by extracting line number and destination
        name from it, formatting them into an appropriate SuperX string and sending that to the sign.
        Parameter
        ---------
        payload: str
            The payload to be processed.
        """
        payload_dict = self.mqpp.parseJSON(payload)

        if payload_dict == 1:
            return None
        else:
            # We subscribe to vimi/pis/route/journey, and can only use the Line Name. This element will either
            # contain a set of leading digits (the Line No) or not. If it does, we extract it as a separate
            # data item. If it does not, we ensure that the Line No is blank and only use the remaining text.
            lineName = self.mqpp.loop_through_dict(payload_dict, "lineName")
            words = lineName.split()
            if words[0].isnumeric():
                lineNo = words[0]
                destText = lineName.replace(lineNo, "", 1).lstrip()
            else:
                lineNo = ""
                destText = lineName

            top_line, bottom_line = self.breakLinesForTokens(destText)
            sign_dict = self.generate_sign_task_dict(lineNo, top_line, bottom_line)

            return sign_dict

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

