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

LAT_TOPIC = "LATITUDE"
LONG_TOPIC = "LONGITUDE"
LAT_TOPIC_PATH = "htc_ipc/+/export_symbols/" + LAT_TOPIC
LONG_TOPIC_PATH = "htc_ipc/+/export_symbols/" + LONG_TOPIC

class locationMonitoringService(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.topic = "vehicle_location/position"
        else:
            self.topic = customerID + "/" + VIN + "/hanover/vehicle_location/position"

        self.mqttService = mqtt.mqttService("mqtt-monitoring-location-service",
                                            broker    = broker,
                                            port      = port,
                                            user_name = user_name,
                                            password  = password,
                                            callback  = self.topicUpdated,
                                            service   = self)
        
        self.location_dictionary = {
            "atDateTime":        "",
            "latitude":          {"degree": "0.0", "direction": "W"},
            "longitude":         {"degree": "0.0", "direction": "W"}
        }

        self.lat = None
        self.long = None

        self.htc_source = False
        self.htc_source_last_updated = 0

        self.mqttService.subscribe(LONG_TOPIC_PATH)
        self.mqttService.subscribe(LAT_TOPIC_PATH)

        # mutex access to the above dictionary
        self.lock = threading.Lock()

        self.thread = threading.Thread(target=self.locationThread, args=(1,))
        self.thread.start()

    def updateDict(self, lat, long):
        lat_dir = "E"
        long_dir = "N"

        # check for negative values and convert to absolute with directions
        if lat < 0:
            lat = abs(lat)
            lat_dir = "W"
        if long < 0:
            long = abs(long)
            long_dir = "S"

        lat_dict = {
            "degree": lat,
            "direction": lat_dir
        }

        long_dict = {
            "degree": long,
            "direction": long_dir
        }

        with self.lock:
            self.location_dictionary["atDateTime"] = datetime.datetime.now().astimezone().replace(microsecond=0).isoformat()
            self.location_dictionary["latitude"] = lat_dict
            self.location_dictionary["longitude"] = long_dict

    def topicUpdated(mqtt, self, topic, payload):
        if not (topic.endswith(LAT_TOPIC) or topic.endswith(LONG_TOPIC)):
            return

        try:
            with self.lock:
                if not self.htc_source:
                    logger.info("using htc_ipc topic")
                    self.htc_source = True
                self.htc_source_last_updated = int(time.time())

                if topic.endswith(LAT_TOPIC):
                    self.lat = float(payload)
                elif topic.endswith(LONG_TOPIC):
                    self.long = float(payload)

                # Only publish when lat and long updated
                lat = self.lat
                long = self.long

            if lat is None or long is None:
                logger.debug("htc_ipc update received but location incomplete yet")
                return

            self.updateDict(lat, long)
        except Exception:
            logger.error(f"exception handling htc lat / long: {traceback.format_exc()}")

    def __del__(self):
        if hasattr(self, 'thread'):
            self.thread.join()
        if hasattr(self, 'session'):
            self.session.close()

    def locationThread(self, name):
        self.session = gps.gps(mode=gps.WATCH_ENABLE)
        while 1:
            try:
                should_wait_for_htc = False
                with self.lock:
                    if self.htc_source:
                        elapsed = int(time.time()) - self.htc_source_last_updated
                        # fall back to reading from gpsd directly if no htc apps updates in 60 seconds
                        if elapsed < 60:
                            should_wait_for_htc = True

                if should_wait_for_htc:
                    time.sleep(1)
                    continue

                if self.session.read() == 0:
                    if not (gps.MODE_SET & self.session.valid):
                        # not useful, probably not a TPV message
                        continue

                    if ((gps.isfinite(self.session.fix.latitude) and
                        gps.isfinite(self.session.fix.longitude))):
                        self.updateDict(self.session.fix.latitude, self.session.fix.longitude)
                    else:
                        logger.debug("awaiting fix")
                else:
                    logger.error("error reading GPS - restarting")
                    # restart the session, as normally it's some error in gpsd
                    try:
                        self.session.close()
                    except Exception:
                        logger.error(f"could not close session")
                    self.session = gps.gps(mode=gps.WATCH_ENABLE)

            except Exception as e:
                logger.error(f"exception reading GPS: {traceback.format_exc()}")
                # restart the session, as normally it's some error in gpsd
                try:
                    self.session.close()
                except Exception:
                    logger.error(f"could not close session")
                self.session = gps.gps(mode=gps.WATCH_ENABLE)

    def publish(self, lock = True):
        # Allow calling without locking, otherwise if the C code has the GIL
        # then the Python thread could have already locked but effectively be
        # frozen while this function is called, causing a deadlock
        if lock:
            with self.lock:
                self.publish_()
        else:
            self.publish_()

    def publish_(self):
        # only publish a valid location once retrieved, not the initial state
        if (self.location_dictionary["atDateTime"] != ""):
            logger.debug(f"publish - {self.topic}\n{self.location_dictionary}\n")
            self.mqttService.publish(self.topic, self.location_dictionary)

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

    #use local broker to forward topics
    lcMonService = locationMonitoringService()

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

    while 1:
        lcMonService.publish()
        time.sleep(5)
