"""
Name: Hanover MQTT
Title: Hanover MQTT
Author: Gianandrea Manfredi
Date: 19/01/2021
Last Modified: 27/01/2022
Desc: Class containing properties and functions allowing for the set-up of an MQTT broker and client
connection to an MQTT broker. Additionally manages queues of messages being sent and received by the

The class is derived from the class offered in itxpt_mqtt, with modifications and integrating some
properties and functionalities held in other classes. These could be reviewed for refactoring.

User class needs only to initiate a thread of the class, and then can read and post to the queues to
process communications. Additional priority queues are provided if required.
"""

import _thread
import copy
import json
import time
from queue import Queue

from hanip.itxpt import DNS_SD, mqtt_client


class MQTTConnectionClass(object):
    """Class managing connection to mqtt services for the
    hanover_mqtt application Connection Process. This
    class is useable for signs and consoles.
    """
    def __init__(self, instance, config_dict, hw_dict, topic_list, secondary_broker=False):
        """
        Parameters
        ----------
        instance : int
            value of instance of connection, can be used to create
            multiple instances
        config_dict : dict
            dictionary containing configuration values for the device
            using the class
        hw_dict : dict
            dictionary of hardware information for the device using the class
        topic_list : str[]
            array of topics that should be subscribed to
        secondary_broker: bool
            bool informing class if this is a second broker connection
        """
        self.instance = instance
        self.mqtt_conn_dict = self.get_mqtt_conn_dict(config_dict, hw_dict, secondary_broker)
        self.hw_dict = hw_dict
        self.topic_list = topic_list
        self.instance_interval = 0

        self.service_ip = ""
        self.service_found = False
        self.service_connected = False
        self.status = "Disconnected"

        self.new_payload = False
        self.current_payload = ""
        self.current_rcvd_topic = ""

        self.mqtt_sub = None

        self.q = Queue(0)
        self.q_priority_in = Queue(0)
        self.q_out = Queue(0)
        self.q_prioirity = Queue(0)

    """
    ##############################################################################################
    Data
    """
    def get_mqtt_conn_dict(self, config_dict, hw_dict, secondary=False):
        """Generates a dictionary of MQTT settings obtained from
        the config file

        Parameters
        ----------
        secondary : bool, default
            bool defining if taking a first or second

        Returns
        -------
        mqtt_dictionary : dict
            mqtt connection dictionary defining values required to
            host/connect for/to mqtt
        """
        mqtt_conn_dict = {
            "broker": False,
            "discover": None,
            "timeout": 0,
            "service_type": "",
            "primary_hostname": "",
            "fallback_address": "",
            "broker_topic": "",
            "reply_topic": "",
            "status_topic": "",
        }

        for parameter in mqtt_conn_dict:
            if secondary:
                if "MQTT_Secondary" in config_dict.keys():
                    mqtt_conn_dict[parameter] = config_dict.get("MQTT_Secondary_%s" % parameter)
                else:
                    print("No secondary mqtt broker in configuration and will not be initiated")
            else:
                mqtt_conn_dict[parameter] = config_dict.get("MQTT_%s" % parameter)

        mqtt_conn_dict["address"] = hw_dict["address"]

        return mqtt_conn_dict

    def broadcast_service(self):
        """MQTT broadcasting service, called when device is set as 
        the mqtt broker"""
        txtrecord = {
            "txtversion": "1",
            "version": "1",
            "brand": "mosquitto",
            "manufacturer": "Hanover Displays",
            "proto": "3.1",
            "topic": self.mqtt_conn_dict["broker_topic"],
        }

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

    def setup_mqtt_register(self):
        """MQTT broker initiating function"""
        print("MQTT Service: Advertising MQTT broker at :" + self.hw_dict["unit_IP"])
        _thread.start_new_thread(self.broadcast_service, ())

    def start_service_discovery(self):
        """MQTT service discovery initating function"""
        servicetype = self.mqtt_conn_dict["service_type"]
        hostname = self.mqtt_conn_dict["primary_hostname"]

        self.service_discover = DNS_SD.DNSSD_Discover(servicetype, hostname)
        self.service_discover.run()
        print("MQTT Service: Instance %s: Service discovery running!" % self.instance)

    def discover_services(self):
        """MQTT servicer discovery"""
        service_timeout = time.time()
        timeout_val = int(self.mqtt_conn_dict.setdefault("timeout", 10))

        while 1:
            if self.service_discover.serviceIP == "":
                self.status = "Discovering"

                if timeout_val == 0:
                    time.sleep(0.5)
                    continue
                else:
                    if (time.time() - service_timeout) > timeout_val:
                        print("MQTT Service: Instance %s: Discover timout, attempting fallback" % self.instance)
                        # print("\ttimeout reached, using configured parameters")
                        self.service_ip = self.mqtt_conn_dict["fallback_address"]
                        self.service_found = True
                        self.status = "Fallback"
                        break

                time.sleep(1)

            else:
                print("MQTT Service: Instance %s: Service found" % self.instance)
                self.status = "Service Found"
                self.service_found = True
                self.service_ip = self.service_discover.serviceIP
                print(self.service_ip)
                break

    def connect_to_service(self):
        """Connects to MQTT service and subscribes to the appropriate topics"""
        topics = []
        for topic in self.topic_list:
            topics.append((topic, 0))
            print("MQTT Service: attempt subscription to: %s"%topic)

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

        print("MQTT Service: Instance %s: Connecting to broker" % self.instance)
        for retries in range(5):
            status = self.mqtt_sub.connect_client()
            print("MQTT Service: Instance %s: MQTT Status: %s" % (self.instance, status))

            if status:
                self.service_connected = True
                print("MQTT Service: Instance %s: Running MQTT Thread" % self.instance)
                self.mqtt_sub.subscribe_to_topics(topics)
                _thread.start_new_thread(self.mqtt_sub.run_client, ())

                return 1
            else:
                time.sleep(3)
                continue

        print("MQTT Service: Instance %s: MQTT couldn't connect, restarting process..." % self.instance)
        return 0

    def poll_new_data(self):
        """
        This grabs the payload from the MQTT client class if there is new data 
        to be grabbed that is of the correct topic.
        """
        if self.mqtt_sub.newMsg:
            self.mqtt_sub.newMsg = False

            current_rcvd_topic = copy.deepcopy(self.mqtt_sub.rcvdTopic)
            current_payload = copy.deepcopy(self.mqtt_sub.payload)

            for string in self.topic_list:
                string = string.replace("#", "")

                if string in current_rcvd_topic: 
                    """
                    if current_payload == prev_payload:
                        # self.new_payload = False
                        # I think this should be cleared by the data retriever
                        pass
                    """
                    if current_payload is not None:
                        try:
                            # print("MQTT Service: Instance %s: New MQTT payload Available" % self.instance)
                            jsontest = json.loads(current_payload)
                            del(jsontest)
                            self.q.put([current_rcvd_topic, current_payload])
                            self.current_payload = current_payload
                            self.new_payload = True
                        except Exception as e:
                            print("MQTT Service: invalid json in payload, topic will not be processed", e)
                    break

    def mqtt_msg_to_queue(self, topic, payload, priority=False):
        """send message to MQTT borker
        
        Parameters
        ----------
        topic : str
            topic to be sent to MQTT broker
        payload : str
            payload to be sent to MQTT broker
        priority : bool
            signifies if the message is part of the priority list
        """
        # print("MQTT Service: Adding out message to queue for processing")
        if priority:
            self.q_prioirity.put([topic, payload])
        else:
            self.q_out.put([topic, payload])

        # print("MQTT Service: out queue size %s" % str(Queue.qsize(self.q_out)))
        # print("MQTT Service: priority out queue size %s" % str(Queue.qsize(self.q_prioirity)))

    def mqtt_msg_send(self, priority=False):
        """send message to MQTT borker
        
        Parameters
        ----------
        priority : bool
            signifies if the message is part of the priority list
        """
        if self.service_connected:
            # print("MQTT Service: Sending message from the queue")
            if priority:
                item = self.q_prioirity.get()
            else: 
                item = self.q_out.get()
            self.mqtt_sub.publish_data(item[0], item[1])
        else:
            print("MQTT Service: Instance %s: No broker connection" % self.instance)

    def run(self):
        msg_printed = False

        """MQTT connection service Main"""
        if "broker" in self.mqtt_conn_dict and self.mqtt_conn_dict["broker"]:
            self.service_ip = "127.0.0.1"
            self.setup_mqtt_register()
            time.sleep(5)
        
        self.mqtt_conn_dict.setdefault("discover", "False")
        if self.mqtt_conn_dict["discover"]:
            self.start_service_discovery()
        
        while 1:
            if self.mqtt_conn_dict["discover"]:
                self.discover_services()
            else:
                self.service_found = True
                self.service_ip = self.mqtt_conn_dict["fallback_address"]

            if self.connect_to_service():
                self.status = "Connected"
                break

        while 1:
            if not self.mqtt_sub.brokerConnected:
                if msg_printed is False:
                    print("MQTT Service: Instance %s: Lost connection to broker" % self.instance)
                    msg_printed = True
                self.service_connected = False
                self.new_payload = False
                self.current_payload = None
                self.status = "Disconnected"

                while 1:
                    if msg_printed is False:
                        print("MQTT Service: Instance %s: Waiting for reconnection" % self.instance)
                        msg_printed = True
                    if self.mqtt_sub.brokerConnected:
                        print("MQTT Service: Instance %s: Connection Restablished!!" % self.instance)
                        self.service_connected = True
                        self.status = "Connected"
                        break
            else:
                msg_printed = False
                self.poll_new_data()
                time.sleep(self.instance_interval)
