"""
Author: Cooper
Date: 27/11/2018

Desc: This class identifies what hardware the software is running on.  This is between an internal sign, external sign
and sign controller.  This class would initially control the serial port and then close it once the details have been
obtained.

Now with added regex!

Example of a hw_dict:
        self.hw_dict = {
            "serial_number": "",
            "software_version": "NA",
            "hardware_version": "NA",
            "superx_version": "",
            "model": "NA",
            "address": "",
            "sign_size": "",
            "hw_status": "1",
            "hw_type": "NA",
        }

Warning
-------
This will not work if running on a console unless the link between the Onion and the
host is using HANO-1F. More recent console versions (DG3 >= 1.35.X) the unit will auto-configure.
For cases where the onion is expecting to link as a Sign, the Sign's link jumper will have to be set to LKB
to set 38400 baud rate.

"""

import os
import re
import time
import json
import serial
import logging
from hanip.onionip import hcp
from hanip.onionip import hano1

logger = logging.getLogger("hwDetermine")

class HardwareDeterminer(object):
    """
    Class to allow the Onion to determine what hardware it's running on.

    Member variables
    ----------------
    host_port (str): Serial port name.
    host_baud_console (int): Baud rate used if running on console.
    host_baud_sign (int): Baud rate used if running on sign.
    config_dir (str): Directory where configuration files are stored.
    signAddress (str): HCP Address of a sign.
    hw_dict (dict): Contains some of the hardware-specific details.
    serport (Serial): Serial port object.
    hw_dict (dict): Dictionary containing hardware items.
    _hcp (HCP): Hanover Comms Protocol object.
    _hano (HANO-1F): HANO-1F protocol handler.
    
    """

    def __init__(self, host_port, host_baud_console, host_baud_sign, config_dir):
        """ Constructor - initialises all the instance variables, creating the HCP and HANO-1F handlers
        and defining the hardware items dictionary.

        Args:
            host_port (str): Serial port name.
            host_baud_console (int): Baud rate if running on console.
            host_baud_sign (int): Baud rate if running on sign.
            config_dir (str): Directory where configuration files are stored.
        """
        self.host_port = host_port
        self.host_baud_console = host_baud_console
        self.host_baud_sign = host_baud_sign
        self.config_dir = config_dir        #Location of the serial json
        self.signAddress = 0

        self.hw_dict = {
            "serial_number": "",
            "software_version": "NA",
            "software_name": "NA",
            "hardware_version": "NA",
            "superx_version": "",
            "model": "NA",
            "address": "",
            "physical_address": "",     # This is the physically set switch address for preservation purposes if the address is overriden
            "sign_size": "",
            "colour_resolution": None,
            "colour_alignment": None,
            "hw_status": "1",
            "hw_type": "NA",
        }

        self._hcp = hcp.HCP()
        self._hano = hano1.HANO1(None, None, False, None)

    """
    ###################################################################################################################
    Serial Port
    """
    def initSerial(self, baud):
        """ Sets up the serial port.
        Currently assumes N, 8, 1. Sets a timeout of 500ms.

        Args:
            baud (int): Baud rate to configure.
        """
        self.serport = serial.Serial(self.host_port, baud, 8, "N", 1, timeout=0.5)

    def closeSerial(self):
        """ Closes the serial port.

        """
        self.serport.close()

    """
    ###################################################################################################################
    Console Stuff
    """

    def pingController(self):
        """
        Issues an initial query signal to cause the console to recognise that the Onion is
        present. Then issues a POLL message, to which it expects a reply. If no reply is
        received, returns a 0 to indicate that the controller has not been contacted. If
        the reply is present (message is > 5 bytes) then returns a 1 to report success.

        Uses the HANO-1F protocol.

        Returns:
            int: 1 if reply received, 0 if not.
        """

        self.initSerial(self.host_baud_console)

        #This is the message that configures a DG3 to go into Onion mode if it isn't already.  We can probably just fire
        #this message out and then send a poll
        msg = self._hano.encodeHANO1("o")
        self.serport.write(msg.encode("latin-1"))

        msg = self._hano.encodeHANO1("a")
        self.serport.write(msg.encode("latin-1"))

        reply = self.serport.read(80)

        self.closeSerial()
        logging.debug("Reply: %s" % reply)

        if len(reply) > 5:
            return 1
        else:
            return 0

    def getConExtStatus(self):
        #HANO-1 will support firmware version request in the next coming versions.  Sending a 'S' will return the
        #version and hardware its running on.
        self.initSerial(self.host_baud_console)
        msg = self._hano.encodeHANO1("S")
        self.serport.write(msg.encode("latin-1"))

        reply = self.serport.read(80).decode("latin-1")
        self.closeSerial()

        if len(reply) > 10:
            logging.info(reply)
            vers = reply[4:-3]
            self.hw_dict["model"] = vers.split("*")[0]
            self.hw_dict["software_version"] = self.hw_dict["model"] + " " + vers.replace("*", ".")[len(self.hw_dict["model"])+1:]

    """
    ###################################################################################################################
    Sign Stuff
    """
    def pingSign(self):
        """
        Sends a Short Status Query to every possible HCP address until it gets a reply.
        When a reply is received, the address is stored.
        If it fails to get any reply, the address will be left set to zero.
            Uses the HCP protocol.
        Returns:
            True if an address is found, False if not.
        """

        self.initSerial(self.host_baud_sign)
        for addr in range(1, 16):
            msgStr = "2%X" % addr
            msg = self._hcp.encodeMaster(msgStr)
            logging.debug("Sending %s" % msg)
            self.serport.write(msg.encode("latin-1"))
            reply = self.serport.read(8).decode("latin-1")
            if len(reply) > 7:
                self.signAddress = addr
                break
        self.closeSerial()
        logging.info("Sign Reply: %s" % reply)

        if len(reply) > 7:
            return True
        else:
            return False

    def getSignExtStatus(self):
        """
        Sends an extended status query and waits for a reply. Populates hw_dict with status info.

        Examples:
            COLEMS V1.03.00 X2.2 #0 IA=00 160x24 [00] C=3D02 P=10/100 AE6322
            OLEMS V1.15.0 X2.2 #0 40x24 [00] C=5112 P=5/100 518B
            TLED V1.13.2 Addr:0 144x19 [00] C=0004 P=100/100 DC58
            TLED V1.13 Addr:0 144x19 P=100/100 [00]
            OLED V1.36.00 X2.2 #0 IA=00 72x10 [00] C=3D02 P=10/100 6142
            HiRes ....
        """
        self.initSerial(self.host_baud_sign)
        msgStr = "9%X" % self.signAddress
        msg = self._hcp.encodeMaster(msgStr)
        self.serport.write(msg.encode("latin-1"))

        reply = self.serport.read(80).decode("latin-1")[3:-3]

        if len(reply) < 1:
            self.closeSerial()
            return
        else:
            logging.info("HWDetermine: ext status: " + reply)

        extended_status_dict = self.parse_extended_status(reply)

        self.hw_dict["software_version"] = extended_status_dict["sign_family"] + " " + extended_status_dict["sw_version"]
        self.hw_dict["software_name"] = extended_status_dict["sign_family"]
        self.hw_dict["hw_type"] = extended_status_dict["sign_type"]
        self.hw_dict["sign_size"] = extended_status_dict["sign_size"]
        self.hw_dict["address"] = extended_status_dict["address"]
        self.hw_dict["physical_address"] = extended_status_dict["address"]
        self.hw_dict["superx_version"] = extended_status_dict["superx_version"]

        if extended_status_dict["sign_family"] in ["OLED", "COLEMS"]:
            colour_panel_dict = self.get_panel_config(self.signAddress)

            self.hw_dict["colour_resolution"] = colour_panel_dict["panel_size"]
            self.hw_dict["colour_alignment"] = colour_panel_dict["handedness"]

        self.closeSerial()

        #Put something here useful instead of NA
        if self.hw_dict["model"] == "NA":
            self.hw_dict["model"] = "SIGN"

    def parse_extended_status(self, extended_status_string):
        """
        This takes in a HMF9 string and splits it into the appropriate parts and forms it into a key/value list so that
        other modules can use this without having to implement this in other places.
        """

        #Init dictionary with some "defaults"
        extended_status_dict = {
            "sign_family": "",
            "sign_type": "",
            "sw_version": "NA",
            "sign_size": "160x19",
            "address": "0",
            "superx_version": None
        }

        #Regex: One or more occurances of a string (without whitespace) followed by zero or more instances of "-"
        #       and any amount of charaters
        try:
            sign_family = re.findall(r"\S+(?:-\S+)*", extended_status_string)[0]
            extended_status_dict["sign_family"] = sign_family

            _sign_family = sign_family.split("-")[0]

            if _sign_family in ["TLED", "ILED"]:
                extended_status_dict["sign_type"] = "int"
            elif _sign_family in ["OLEMS", "OLED", "COLEMS", "ELD"]:
                extended_status_dict["sign_type"] = "ext"
            elif _sign_family in ["BLED", "HiRes"]:
                extended_status_dict["sign_type"] = "HiRes"
            else:
                extended_status_dict["sign_type"] = "ext"
        except IndexError:
            pass

        #Regex: "V" then 1 or more numbers followed by one or more instances of "." and any amount of numbers
        #Turns out not all sign firmware versions preceed with "V"
        # sw_version = re.findall(r"V\d+(?:\.\d+)+", reply)[0]
        try:
            extended_status_dict["sw_version"] = re.findall(r"\d+(?:\.\d+)+", extended_status_string)[0]
        except IndexError:
            pass

        #Regex: The first match containing any two numbers separated by an "x"
        try:
            extended_status_dict["sign_size"] = re.findall(r"\d+x\d+", extended_status_string)[0]
        except IndexError:
            pass

        try:
            #Regex: The first match containing a preceding "#" followed by 0-9 or A-F
            extended_status_dict["address"] = re.findall(r"#\d+|#[A-F]", extended_status_string)[0].lstrip("#")
        except IndexError:
            try:
                # Regex: The first match containing a preceding "Addr:" followed by 0-9 or A-F
                # This is nested because there are two address formats, isnt that nice.
                extended_status_dict["address"] = re.findall(r"Addr:\d+|Addr:[A-E]", extended_status_string)[0][5:]
            except IndexError:
                pass

        try:
            # Regex: "X" then 1 or more numbers followed by one or more instances of "." and any amount of numbers
            extended_status_dict["superx_version"] = re.findall(r"X\d+(?:\.\d+)+", extended_status_string)[0]
        except IndexError:
            pass

        return extended_status_dict

    def get_panel_config(self, address):
        """
        Obtains the colour panel configuration of a sign, currently tested on COLED signs
        Response from 160x24 left handed COLEMS:
        40x24C(3.1.0), 40x24, 40x24, -1x-1, -1x-1, -1x-1

        :param address: Address of the sign to poll
        :return: A tuple containing the panel size and handedness, both values can be None of the data cannot be
        obtained
        """
        msgStr = "9%sCO?" % address
        msg = self._hcp.encodeMaster(msgStr)
        logging.debug("Sending %s" % msg)
        self.serport.write(msg.encode("latin-1"))
        reply = self.serport.read(100).decode("latin-1")
        reply = reply[3:-3]

        return self.parse_colour_panel_config(reply)

    def parse_colour_panel_config(self, input):
        """
        This takes in a panel config string and splits it into the appropriate parts and forms it into a key/value list so that
        other modules can use this without having to implement this in other places.
        """
        colour_panel_dict = {
            "panel_size": None,
            "handedness": None
        }

        panel_conf = input.replace(" ", "").split(",")

        for position, panel in enumerate(panel_conf):
            if "C" in panel:
                #Basically if it sees this C character in the split elements then this is where the colour panel is,
                #It can be in position 0, indicating left side, otherwise itll be in the last position i.e. right
                colour_panel_dict["panel_size"] = panel.split("C")[0]

                if position == 0:
                    colour_panel_dict["handedness"] = "left"
                else:
                    colour_panel_dict["handedness"] = "right"

                break

        return colour_panel_dict

    """
    ###################################################################################################################
    HW Configuration file
    """

    def save_hw_details(self, persistent=False):
        """
        This saves the contents of the hw_dict to a json file, used to see what hardware the onion is connected to.
        Generally in normal operation this file is not used,
        """
        if persistent:
            save_path = r"/etc/hanip/hw_details.json"
        else:
            save_path = r"/tmp/hw_details.json"

        with open(save_path, "w", encoding="utf-8") as hw_file:
            json.dump(self.hw_dict, hw_file, ensure_ascii=False, indent=4)

    def read_hw_details(self):
        pass

    def get_serial(self):
        """ Obtains the unit's serial number and other manufacturing details if available
        and puts them the hw_dict.

        Returns:
            If no data available or a parsing error, then None, otherwise the hw_dict.
        """
        if "hwdetails.cfg" in os.listdir(self.config_dir):
            logging.info("Serial JSON found")
            hwjson = open(os.path.join(self.config_dir, "hwdetails.cfg"), "r").read()

            try:
                jsondata = json.loads(hwjson)
            except json.decoder.JSONDecodeError:
                logging.exception("Parsing Error")
                return None
            else:
                self.hw_dict["serial_number"] = jsondata.get("manufactureData").get("serial", "NA")
                self.hw_dict["model"] = jsondata.get("manufactureData").get("product_no", "NA")
                self.hw_dict["hardware_version"] = jsondata.get("manufactureData").get("hw_revision", "NA")
                self.hw_dict["manufacture_date"] = jsondata.get("manufactureData").get("manufacture_date", "NA")
        else:
            logging.info("No serial data available")
            return None

        return self.hw_dict

    """
    ###################################################################################################################
    Main calls
    """
    def determine_hardware(self):
        """ Tries to find out what type of hardware we're running on.
        
        Attempts to contact a Sign first and then a Console. If both attempts fail, this will retry
        up to 5 times. If no contact is made, we have been unable to determine the hardware we're
        running on, and the main application will revert to Factory Mode.
        If one of the attempts succeeds, we interrogate the hardware further by issuing an Extended
        Status command and parsing the results into our Hardware Dictionary.
        The Hardware Dictionary entry "hw_status" is marked with a token to indicate success or
        failure - 0 for OK, 1 for KO.
        """
        logging.info("Determining Hardware...")
        for x in range(5):
            contstatus = self.pingController()
            if contstatus:
                self.getConExtStatus()
                self.hw_dict["hw_type"] = "con"
                self.hw_dict["hw_status"] = "0"
                logging.info("Found Console.")
                self.save_hw_details()
                return

            signstatus = self.pingSign()
            if signstatus:
                self.getSignExtStatus()
                self.hw_dict["hw_status"] = "0"
                logging.info("Found Sign.")
                self.save_hw_details()
                return

            time.sleep(1)
        logging.warning("Hardware not identified!")
        self.hw_dict["hw_status"] = "1"

    def get_hardware_dict(self):
        self.get_serial()
        self.determine_hardware()

        return self.hw_dict


if __name__ == "__main__":
    port = "/dev/ttyS1"
    baudCons = 115200
    baudSign = 38400
    hwd = HardwareDeterminer(port, baudCons, baudSign, "..")
    hwd.determine_hardware()

    print(hwd.hw_dict)

