#!/usr/bin/env python3
import time
import mqtt_service as mqtt
import datetime
import traceback
import threading
import select
import logging
import signal
logger = logging.getLogger(__name__)

class GPIO(object):
    def __init__(self, sysfs_path, inverted, watch_for_changes, service, callback):
        self.sysfs_path = sysfs_path
        self.service = service
        self.callback = callback
        self.inverted = inverted
        self.watch_for_changes = watch_for_changes
        self.gpio_file = open(self.sysfs_path, "r")

        # mutex access to value
        self.lock = threading.Lock()

        # get initial value
        self.readValue()

    def __del__(self):
        self.stop()
        if hasattr(self, 'gpio_file') and self.gpio_file:
            self.gpio_file.close()

    def start(self):
        if self.watch_for_changes:
            logger.debug("Starting watchThread for %s" % self.sysfs_path)
            self.thread = threading.Thread(target=self.watchThread, args=(1,))
        else:
            logger.debug("Starting pollThread for %s" % self.sysfs_path)
            self.thread = threading.Thread(target=self.pollThread, args=(1,))
        self.running = True
        self.thread.start()

    def stop(self):
        if hasattr(self, 'thread') and self.thread.is_alive():
            logger.debug("Stopping GPIO %s monitoring" % self.sysfs_path)
            self.running = False
            self.thread.join()

    def getValue(self):
        with self.lock:
            return self.value
        
    def readValue(self):
        value = self.gpio_file.read(1)
        self.gpio_file.seek(0)

        if self.inverted:
            if value == "0":
                self.value = "1"
            else:
                self.value = "0"
        else:
            self.value = value

    def pollInput(self):
        with self.lock:
            oldValue = self.value
            self.readValue()
            if self.value != oldValue:
                logger.debug("value: %s" % self.value)
                self.callback(self.service, self.sysfs_path, self.value)

    def pollThread(self, name):
        while self.running:
            time.sleep(1)
            self.pollInput()

    def watchThread(self, name):
        logger.debug("Starting epoll for %s" % self.sysfs_path)
        self.poll = select.epoll()
        self.poll.register(self.gpio_file.fileno(), select.EPOLLPRI | select.EPOLLET | select.EPOLLIN)

        while self.running:
            events = self.poll.poll(1)
            if events:
                logger.debug(f"gpio {self.sysfs_path} events: %s" % (str(events),))
                self.pollInput()

        logger.debug("Closing epoll for %s" % self.sysfs_path)
        self.poll.unregister(self.gpio_file.fileno())
        self.poll.close()

class gpioMonitoringService(object):
    def __init__(self, broker            = "127.0.0.1",
                       port              = 1883,
                       user_name         = "",
                       password          = "",
                       customerID        = "",
                       VIN               = "",
                ):

        def inc_level(sig, frame):
            level = logger.getEffectiveLevel()
            if level == logging.CRITICAL:
                logger.setLevel(logging.ERROR)
            elif level == logging.ERROR:
                logger.setLevel(logging.WARNING)
            elif level == logging.WARNING:
                logger.setLevel(logging.INFO)
            elif level == logging.INFO:
                logger.setLevel(logging.DEBUG)
            print(f"Set logging to {logging.getLevelName(logger.getEffectiveLevel())}")

        def dec_level(sig, frame):
            level = logger.getEffectiveLevel()
            if level == logging.NOTSET:
                logger.setLevel(logging.DEBUG)
            elif level == logging.DEBUG:
                logger.setLevel(logging.INFO)
            elif level == logging.INFO:
                logger.setLevel(logging.WARNING)
            elif level == logging.WARNING:
                logger.setLevel(logging.ERROR)
            elif level == logging.ERROR:
                logger.setLevel(logging.CRITICAL)
            print(f"Set logging to {logging.getLevelName(logger.getEffectiveLevel())}")

        signal.signal(signal.SIGUSR2, inc_level)
        signal.signal(signal.SIGUSR1, dec_level)

        if customerID == "":
            self.digouts_topic = "digital_outputs/states"
            self.digins_topic = "digital_inputs/states"
        else:
            self.digouts_topic = customerID + "/" + VIN + "/hanover/digital_outputs/states"
            self.digins_topic = customerID + "/" + VIN + "/hanover/digital_inputs/states"

        self.mqttService = mqtt.mqttService("mqtt-monitoring-gpio-service",
                                            broker    = broker,
                                            port      = port,
                                            user_name = user_name,
                                            password  = password)
        
        self.digouts_topic_dictionary = {
            "atDateTime":        "",
            "states":            ""
        }

        self.digins_topic_dictionary = {
            "atDateTime":        "",
            "states":            ""
        }

        # mutex access to gpio update
        self.lock = threading.Lock()

        # todo: read these from a config file?

        self.digouts_dictionary = {
            "/sys/class/gpio/gpio61/value":     0,
            "/sys/class/gpio/gpio87/value":     0
        }

        self.digins_dictionary = {
            "/sys/class/gpio/gpio12/value":     0,
            "/sys/class/gpio/gpio2/value":      0,
            "/sys/class/gpio/gpio14/value":     0
        }

        self.digout_gpios = []

        for path in self.digouts_dictionary.keys():

            try:
                digout = GPIO(sysfs_path        = path,
                              inverted          = False, # digouts are not inverted
                              watch_for_changes = False, # digouts can't be watched
                              service           = self,
                              callback          = self.gpioCallback)

                self.digouts_dictionary[path] = digout.getValue()
                self.digout_gpios.append(digout)
                digout.start()

            except IOError:
                logger.error("Error opening %s" % path)

        self.digin_gpios = []

        for path in self.digins_dictionary.keys():
            try:
                digin = GPIO(sysfs_path         = path,
                             inverted           = True, # digins are inverted
                             watch_for_changes  = True,
                             service            = self,
                             callback           = self.gpioCallback)

                self.digins_dictionary[path] = digin.getValue()
                self.digin_gpios.append(digin)
                digin.start()

            except IOError:
                logger.error("Error opening %s" % path)

        self.publish()

    def __del__(self):
        self.stop()

    def stop(self):
        logger.debug("Stopping GPIO Monitoring Service")
        for gpio in getattr(self, 'digout_gpios', []):
            gpio.stop()
        for gpio in getattr(self, 'digin_gpios', []):
            gpio.stop()

    def gpioCallback(gpio, self, path, value):
        with self.lock:
            logger.debug(f"path: {path}, value: {value}")
            if path in self.digouts_dictionary:
                self.digouts_dictionary[path] = value
            elif path in self.digins_dictionary:
                self.digins_dictionary[path] = value
            else:
                logger.error(f"Error - gpio is not recognised: {path}")
            self.publish()

    def updateDictionaries(self):
        digouts = "b"
        for value in self.digouts_dictionary.values():
            digouts += str(value)
        self.digouts_topic_dictionary["atDateTime"] = datetime.datetime.now().astimezone().replace(microsecond=0).isoformat()
        self.digouts_topic_dictionary["states"] = digouts

        digins = "b"
        for value in self.digins_dictionary.values():
            digins += str(value)
        self.digins_topic_dictionary["atDateTime"] = datetime.datetime.now().astimezone().replace(microsecond=0).isoformat()
        self.digins_topic_dictionary["states"] = digins

    def publish(self):
        self.updateDictionaries()

        logger.debug(f"publish - {self.digouts_topic}\n{self.digouts_topic_dictionary}\n")
        self.mqttService.publish(self.digouts_topic, self.digouts_topic_dictionary)

        logger.debug(f"publish - {self.digins_topic}\n{self.digins_topic_dictionary}\n")
        self.mqttService.publish(self.digins_topic, self.digins_topic_dictionary)

if __name__ == "__main__":
    logger.info("GPIO Monitoring Service running in foreground")

    #use local broker to forward topics
    gpioMonService = gpioMonitoringService()

    #send topics direct to cloud
    #gpioMonService = gpioMonitoringService(broker            = "mqtt.ver.hanover.cloud",
    #                                       port              = 2023,
    #                                       user_name         = "1031_300",
    #                                       password          = "z3Qs38jn",
    #                                       customerID        = "1031",
    #                                       VIN               = "0")

    while 1:
        time.sleep(5)
