"""
Tracker-Daten

Mögliche Typen:
  HERO - Regatta Hero
  SDCARD - nur für ESP32 relevant
  LOCAL - Logdatei. GPX ist ungeeignet, weil das Gerät zu jedem Zeitpunkt
          ausgeschaltet werden kann und somit die Datei in einem 
          ungültigen Zustand sein kann.
          Es wird eine Datei track.log.geschrieben.
          Um fehlende Daten festzustellen wird ein fortlaufender Zähler
          geschrieben.
  SERVER - spezielle Software benötigt, kann auch ein Raspi an Bord sein
           Der Tracker sendet einen HTTP-POST. Der Server speichert
           die Daten für mehrere Geräte. Vorläufiger Servername:
           trackserver
  NONE - kein Tracking

Wenn die Verbindung zum Server im Internet nicht funktioniert, werden
die Positionen in eine Warteschlange gesichert und nach 
Wiederherstellung der Verbindung übertragen.

TODO
- Nach einem Disconnect manuelle Neuverbindung ermöglichen
- Audioausgabe für Regatta Hero

"""

import os
import time
import paho.mqtt.client as mqtt
import http.client
import ssl
import json
import socket
import math
import subprocess # für Audioausgabe / mpg123

class Tracker():

    def __init__(self, logger, cfg):
        self.log = logger
        self.appdata = None

        validtypes = ('HERO', 'SDCARD', 'SERVER', 'LOCAL', 'NONE')
        if cfg['tracker']['type'] not in validtypes:
            raise TypeError("Invalid tracker type: '{}'. Only supported: {}".format(cfg['tracker']['type'], ','.join(validtypes)))
        self.ttype = cfg['tracker']['type']

        self.local_lfdno = 0
        self.local_dt = cfg['tracker']['interval'] # Eintrag alle <n> Sekunden schreiben

        self.trace =  cfg['tracker']['trace']   # Debugging
        self.trace_fh = None                    # File Handle der Tracedatei

        self.audiopath = cfg['audiopath']

        self.buoys = {}      # Tonnen (Hero=20)
        self.courses = []    # Bahnen
        self.races = []      # Regatten

        self.mqtt_connected = False
        self.activated = False
        self.lat = None    # last latitude
        self.lon = None    # last longitude
        self.tspos = None  # timestamp (hh:ss:mm) as datetime.time
        self.sog = None

        # Statische Daten
        # TODO Wirklich alles hier im Tracker oder ist einiges generisch?
        # appdata?
        self.boatid = cfg['boat']['uuid']
        self.sailno =  cfg['boat']['sailno']
        self.boatname = cfg['boat']['name']
        self.boatclass = cfg['boat']['class']
        self.handicap = cfg['boat']['handicap']
        self.club = cfg['boat']['club']
        self.team = cfg['boat']['team']

        if self.ttype == 'HERO':
            # Regatta Hero
            self.hero_orgid = cfg['tracker']['username'] # Eingestellt in Gerätekonfiguration
            self.hero_passcode = cfg['tracker']['password'] 
            self.hero_host = cfg['tracker']['host']
            self.hero_port = cfg['tracker']['port']
            self.hero_viewerpass = None # Wird vom Server in "org" gesendet

            # Vorlage für Anfragen
            self.http_payload_template = {
                "orgid": self.hero_orgid,
                "passcode": self.hero_passcode,
                "raceid": "",
                "replay": "live",
                "replaytime": 0,
                "updateType": "timerUpdate"
            }

            self.hero_raceid = None  # Aktuell ausgewählte Regatta
            self.hero_racephase = 0  # Bei Änderung Event auslösen
            self.hero_shortened = False # Bahnverkürzung aktiv

            # MQTT
            self.client = mqtt.Client()
            self.client.on_connect = self.mqtt_on_connect
            self.client.on_message = self.mqtt_on_message
            self.client_race_age = 0 # Age of last mqtt race data in seconds

            self.hero_orgstatus = None 
            self.hero_racestatus = None
            self.hero_timedelta = 0 # Zeitdifferenz zum Server in sec
            self.hero_givenup = False

            # Hole erste Daten vom Server
            self.hero_query_org()

    def is_server_active(self, hostname, port):
        """
        ohne Netzwerkverbindung wirft socket.gethostbyname eine Exception,
        ggf. "Erro -3 temporary failure in name resolution"
        """
        try:
            host = socket.gethostbyname(hostname) # DNS Lookup
            s = socket.create_connection((host, port), 2)
            s.close()
            return True
        except Exception:
            pass
        return False

    def is_active(self):
       return self.activated

    def set_active(self, newval):
       self.activated = newval

    def local_tracker(self, cfg, appdata, boatdata):
        self.log.info("Local tracker enabled")
        if not os.path.exists(cfg['histpath']):
            os.makedirs(cfg['histpath'])
            self.log.info(f"History path created: '{cfg['histpath']}'")

        # Zugriff auf Boatdata: Referenzen für leichten schnellen Zugriff
        bv_lat = boatdata.getRef("LAT")
        bv_lon = boatdata.getRef("LON")
        bv_hdop = boatdata.getRef("HDOP")
        bv_tspos = boatdata.getRef("TSPOS")

        trackerfile = os.path.join(cfg['histpath'], "localtrack.log")
        fh = open(trackerfile, 'a+')
        n = 0
        while not appdata.shutdown:
            time.sleep(1)
            if not self.activated:
                continue
            n += 1
            if n % self.local_dt != 0:
                continue
            self.local_lfdno += 1
            data = "{},{}Z,{},{},{}\n".format(
                self.local_lfdno,
                bv_tspos.getValue(),
                bv_lat.getValue(),
                bv_lon.getValue(),
                bv_hdop.getValue())
            fh.write(data)
            fh.flush()
        fh.close()

    def set_hero_raceid(self, newraceid):
        self.hero_raceid = newraceid

    #def set_type(self, newtype):
    #    validtypes = ('HERO', 'SDCARD', 'SERVER', 'LOCAL', 'NONE')
    #    newtype = newtype.upper() 
    #    if newtype not in validtypes:
    #        raise TypeError(f"Invalid tracker type: '{newtype}'. Only supported: {validtypes}")
    #    self.ttype = newtype

    """
    Audioevents:

    Einfach nur die Zahl
    'eins', 'zwei', 'drei', 'vier', 'fuenf', 'sechs', 'sieben',
    'acht', 'neun', 'zehn', 

    <n> Minuten bis zum Start
    'eineMin', 'zweiMin', 'dreiMin', 'vierMin', 'fuenfMin'

    <n> Sekunden bis zum Start
    'fuenfzehn', 'zwanzig', 'dreissig', 'vierzig', 'fuenfzig'

    '30sek_alerter'         30 sec kein GPS. Smartphonetext
    'abbruch'               Abbruch der Wettfahrt
    'alive'                 sehr leises Geräusch
    'allgmRueck'
    'bahnmarke'
    'bahnVerk'              Bahnverkürzung
    'batteryLevel'          Ladezustand unter 20%. Smartphonetext
    'cellback'              Mobilfunkverbindung wiederhergestellt, Daten übertragen
    'einzelRueck'
    'endeEineMinute'        Zeitrennen in einer Minute zuende
    'endeWettfahrt'         Ende der Wettfahrt
    'endstartVerschiebung'
    'jumping'               Springende Koordinaten. Smartphonetext.
    'neueAnsage'            Achtung neue Bekanntmachung
    'noconnection'          Kein Mobilfunk. GPS-Daten werden zwischengespeichert.
    'runde'
    'startErfolgt'          BEEEP
    'startlinie'            Startlinie überquert
    'startnotready'         Startlinie nicht bereit
    'startready'
    'startVerschiebung'
    'wartenAnkuend'         Warten auf Ankündigungssignal
    'ziellinie'             Ziellinie überquert

    """

    def hero_mqtt_subscribe(self, raceid, lastid=None):
        topicbase = f"regattahero/racestatus/{self.hero_orgid}"
        if lastid is not None:
            topic = '/'.join((topicbase, lastid))
            self.client.unsubscribe(topic)
            self.log.info(f"Sent unsubscribe for '{topic}'")
        topic = '/'.join((topicbase, raceid))
        self.client.subscribe(topic)
        self.log.info(f"Sent subscribe for '{topic}'")

    def hero_giveup(self):
        # TODO nach Aufgabe noch ein paar Pakete senden bis dier
        # Tracker dann abgestellt wird.
        self.hero_givenup = True

    def hero_play_audio(self, event, lang='de'):
        """
        Ein Event ist mit einer Audiodatei gekoppelt. Das Event trägt
        den Basisnamen der Audiodatei. Es werden nur MP3-Dateien unterstützt
        """
        lang = lang.lower()
        if lang == 'de':
            filename = f"{event}.mp3"
        else:
            filename = f"{event}_{lang}.mp3"
        mp3file = os.path.join(self.audiopath, filename)
        subprocess.Popen(["mpg123", "-q", mp3file])

    def hero_get_degrees(azimut):
        """
        Winkel von einer Bahnmarke zur nächsten basierend auf dem Datenfeld azimutNext
        """
        if (azimut <= math.pi * 3/2):
            return math.degrees(2 * math.pi - azimut - math.pi / 2) 
        else:
            return math.degrees(2 * math.pi - azimut + math.pi * 3 / 2)

    def hero_query(self, raceid):
        """
        Query Regatta Hero HTTP-Server
        """
        ssl_context = ssl.create_default_context()
        conn = http.client.HTTPSConnection(self.hero_host, self.hero_port, context=ssl_context)
        endpoint = '/mapupdate'
        payload = self.http_payload_template
        payload['raceid'] = raceid
        json_data = json.dumps(payload)
        headers =  {
            'Content-Type': 'application/json',
            'Content-Length': str(len(json_data))
        }
        data = {} # empty
        try:
            conn.request("POST", endpoint, body=json_data, headers=headers)
            response = conn.getresponse()
            if response.status == 200:
                self.log.info("HTTP: Response received successfully!")
                data = json.loads(response.read().decode())
            else:
                self.log.warning(f"HTTP: Failed to retrieve data. Status code: {response.status}")
                return data
        except http.client.HTTPException as e:
            self.log.warning(f"HTTP error occurred: {e}")
        except ssl.SSLError as ssl_error:
            self.log.warning(f"SSL error occurred: {ssl_error}")
        finally:
            conn.close()
        return data

    def hero_query_org(self):
        """
        Abfrage des Datenservers / Basisdaten
          - Namen der Regatten und Kurse
        """
        data = self.hero_query('[No race]')
        self.viewerpass = data['org']['viewerPasscode']
        self.courses = []
        for c in data['org']['courses']:
            self.courses.append(c)
        self.races = []
        for r in data['org']['races'].values():
            if not r['hiderace']:
                self.races.append(r['raceid'])
        # Regatta automatisch auswählen wenn nur genau eine aktivierbar ist
        if len(self.races) == 1:
            self.hero_raceid = self.races[0]

    def hero_query_course(self, raceid):
        """
        Bojen und Kurs für ein gegebenes Rennen
        """
        data = self.hero_query(raceid)

        print(self.hero_orgdata)

        # Bojen 
        self.buoys.clear()
        for key, val in self.hero_orgdata['org']['buoysets']['default']['buoyset']['marks'].items():
            self.buoys[key] = {
                'label': val['label'],
                'lat': val['label'],
                'lon': val['label']
            }
        print(self.buoys)

        # Kurse
        self.courses = []
        for c in data['org']['courses']:
            self.courses.append(c)
        self.races = []
        for r in data['org']['races'].values():
            if not r['hiderace']:
                self.races.append(r['raceid'])

    def hero_query_race(self, raceid):
        """
        Aktuelle Regattadaten vom HTTP-Server holen, ggf. weil MQTT nichts mehr sendet
        """
        data = self.hero_query(raceid)
        self.hero_racestatus = data['race']['racestatus']
        print(self.hero_racestatus)
        # 'raceactive': False
        # 'racefinished': True

    def get_position(self):
        # Positionsabfrage für die Payload
        # LAT, LON, TSPOS, SOG
        return (self.lat, self.lon, self.tspos, self.sog)
    """
    def hero_add_race(self, raceid):
        self.races.add(raceid)

    def hero_set_races(self, newraces):
        self.races = set(newraces)
    """
    def hero_get_races(self):
        if not self.hero_orgstatus:
            return []
        races = []
        for r in self.hero_orgstatus['races'].values():
            if not r['hiderace']:
                races.append(r['raceid'])
        return races

    def mqtt_on_connect(self, client, userdata, flags, rc):
        self.mqtt_connected = False
        if rc == 0:
            self.log.info("MQTT connected, subscribing to organisation topic")
            client.subscribe(f"regattahero/orgstatus/{self.hero_orgid}")
            self.mqtt_connected = True
        elif rc == 1:
            self.log.error("MQTT connection refused, unacceptable protocol version")
        elif rc == 2:
            self.log.error("MQTT connection refused, identifier rejected")
        elif rc == 3:
            self.log.error("MQTT connection refused, server unavailable")
        elif rc == 4:
            self.log.error("MQTT connection refused, bad user name or password")
        elif rc == 5:
            self.log.error("MQTT connection refused, not authorized")
        else:
            self.log.info(f"MQTT  connection refused, error #{rc}")

    def mqtt_on_message(self, client, userdata, msg):
        """
        Meldungen vom Server empfangen
        TODO raceid über userdata? dann topic prüfen?
        """
        if self.trace:
            self.trace_fh.write(msg.topic)
            self.trace_fh.write("\n")
            self.trace_fh.write(msg.payload.decode())
            self.trace_fh.write("\n\n")
            self.trace_fh.flush()
        if msg.topic == f"regattahero/orgstatus/{self.hero_orgid}":
            # kommt alle 10s
            self.hero_orgstatus = json.loads(msg.payload)

            # Zeitdifferenz Client/Server ermitteln für ggf. spätere Warnung
            server_ts = self.appdata.track.hero_orgstatus['timestamp'].split(':')
            sec1 = int(server_ts[0]) * 3600 + int(server_ts[1]) * 60 + int(server_ts[2])
            ts = time.localtime()
            sec2 = ts.tm_hour * 3600 + ts.tm_min * 60 + ts.tm_sec
            self.hero_timedelta = abs(sec1 - sec2)

            if self.hero_orgstatus['allLogout']:
                self.log.info("All logout received!")
                client.disconnect()
                self.activated = False
                return
            if self.hero_orgstatus['message']:
                # TODO Alarm-Funktion nutzen?
                pass
        elif msg.topic.startswith(f"regattahero/racestatus/{self.hero_orgid}"):
            # kommt alle 1s
            # dem Topic angehängt ist noch die raceid
            self.client_race_age = 0
            payload = json.loads(msg.payload)
            self.hero_racestatus = payload['racestatus']
            racephase = payload['racestatus']['racephase']
            if self.hero_racephase != racephase:
                # Phasenänderung! Event!
                print("Event: Phasenübergang {} -> {}".format(self.hero_racephase, racephase))
                if self.hero_racephase == 0:
                    if racephase == 1:
                        # Startlinie bereit
                        self.hero_play_audio('startready')
                        print(payload['racestatus'])
                        #subprocess.popen("mpg123 -q " + os.path.join(self.audiopath, "startready.mp3"))
                elif self.hero_racephase == 1:
                    if racephase == 1:
                        # Startlinie bereit
                        self.hero_play_audio('startVerschiebung')
                    elif racephase == 3:
                        # Startverschiebung aufgehoben
                        pass
                    elif racephase == 4:
                        self.hero_play_audio('dreiMin')
                elif self.hero_racephase == 2:
                    if racephase == 3:
                        self.hero_play_audio('vierMin')
                    elif racephase == 4:
                        self.hero_play_audio('dreiMin')
                elif self.hero_racephase == 3:
                    if racephase == 4:
                         self.hero_play_audio('dreiMin')
                    if racephase == 1:
                        self.hero_play_audio('startVerschiebung')

                elif self.hero_racephase == 4:
                    if racephase == 1:
                        self.hero_play_audio('startVerschiebung')
                    if racephase == 5:
                        # Blauer Peter oben
                        self.hero_play_audio('zweiMin')
                elif self.hero_racephase == 5:
                    if racephase == 1:
                        self.hero_play_audio('startVerschiebung')
                    elif racephase == 6:
                        # Blauer Peter runter
                        pass
                elif self.hero_racephase == 6:
                    if racephase == 1:
                        self.hero_play_audio('startVerschiebung')
                elif self.hero_racephase == 7:
                    if racephase == 8:
                        # Gestartet
                        pass
                elif self.hero_racephase == 8:
                    if racephase == 2:
                        if payload['racestatus']['recallgeneral']:
                            self.hero_play_audio('allgmRueck')
                        elif payload['racestatus']['racecancelled']:
                            self.hero_play_audio('abbruch')
                        elif payload['racestatus']['racefinished']:
                            self.hero_play_audio('endeWettfahrt')
            self.hero_racephase = racephase

            # TODO Einzelrückruf, keine Änderung in der Phase!
            #if payload['racestatus']['recallindiv']:
            #    self.hero_play_audio('einzelRueck')

            # Timing
            timer = payload['racestatus']['time']
            countdown = {
               -60: 'eineMin',
               -50: 'fuenfzig', -40: 'vierzig', -30: 'dreissig',
               -20: 'zwanzig', -15: 'fuenfzehn', -10: 'zehn',
                -9: 'neun', -8: 'acht', -7: 'sieben', -6: 'sechs',
                -5: 'fuenf', -4: 'vier', -3: 'drei', -2: 'zwei',
                -1: 'eins', 0 : 'startErfolgt'
            }
            # Komplexere Bedingung die nach Start fast keinen Aufwand
            # mehr bedeutet
            if (timer <= 0) and (timer >= -60) and timer in countdown:
                self.hero_play_audio(countdown[timer])

            # payload['racestatus']['racestarted']
            # payload['racesettings']

            # Bahnverkürzung
            if payload['racestatus']['shortend'] and not self.hero_shortened:
                self.hero_shortened = True
                self.hero_play_audio('bahnVerk')
                self.log.info("Bahnverkürzung: Zu Bahnmarke {}".format(payload['racestatus']['shortendsel']))

            """
            time: negativ: Zeit vor dem Start, positiv: Zeit nach dem Start
                  in Sekunden
            flags: [0, 0, 14, 10] 
            raceactive: true bedeutet orange Flagge ist oben
            racestarted: true

            Signale der Wettfahrtleitung hier anzeigen
               Regattaabbruch
               Bahnverkürzung
               Rückrufe

            """
        else:
            self.log.warning(f"UNKNOWN TOPIC: {msg.topic}")
            self.log.debug(msg.payload)

    def mqtt_publish(self, topic, payload, bv_lat, bv_lon, bv_sog):
        """
        Payload vorbelegt als Template, so daß nur noch die veränderlichen
        GPS-Daten eingefügt werden müssen: LAT LON SOG TIMESTAMP
        
        isTracking kann ausgeschaltet werden,
        """
        lat = bv_lat.getValueRaw()
        lon = bv_lon.getValueRaw()
        sog = bv_sog.getValueRaw()
        if lat and lon and (sog is not None):
            payload['raceid'] = self.hero_raceid
            payload['gps']['lat'] = round(lat, 5)
            payload['gps']['lon'] = round(lon, 5)
            payload['gps']['speed'] = sog
            payload['gps']['timestamp'] = time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime())
            self.client.publish(topic, json.dumps(payload))
        else:
            self.log.debug("No GPS data available. Nothing published!")

    def mqtt_tracker(self, cfg, boat, appdata, boatdata):
        self.log.info("MQTT tracker enabled")
        self.appdata = appdata
        self.client.username_pw_set(username=cfg['mqtt_user'], password=cfg['mqtt_pass'])
        try:
            self.client.connect(cfg['mqtt_host'], cfg['mqtt_port'], 60)
        except ConnectionRefusedError:
            self.log.error("MQTT connection refused. Check username and password.")
            return

        # Initial die Organisationsdaten abfragen um Tonnen und Kurse zu erhalten
        # self.hero_query_org(cfg['host'], cfg['port'], cfg['username'], cfg['password'])

        if self.trace:
            tracefile = os.path.join(os.path.expanduser(cfg['logdir']), 'tracker.log')
            self.trace_fh = open(tracefile, 'w+')
            self.log.info(f"MQTT trace file '{tracefile}' enabled")

        topic = "regattahero/tracker/" + self.hero_orgid
        payload = {
            "passcode": cfg['password'],
            "orgid": self.hero_orgid,
            "raceid": None, # Nach Auswahl einstellen
            "gps": {
                "lat": 0.0,
                "lon": 0.0,
                "speed": 0.0,
                "age": 500,         # letzter Kontakt zum GPS empfänger in ms
                # "odo": 1000,      # deprecated
                "bat": 1.0,
                "timestamp": ""     # ISO8601 Format mit Millisekunden in UTC
                },
            "boat": {
                "boatid": self.boatid,
                "sailno": self.sailno,
                "team": self.team,
                "boatclass": self.boatclass,
                "handicap": self.handicap,
                "club": self.club,
                "boatname": self.boatname,
                "isTracking": True,
                "hasGivenUp": False
                },
            "device": {             # optional
                "os" : "linux",
                "osVer" : "OBP60v 0.1",
                "isIOS" : False,
                "isAndroid" : False,
                "isTracker" : True
                }
            }

        # Zugriff auf Boatdata: Referenzen für leichten schnellen Zugriff
        bv_lat = boatdata.getRef("LAT")
        bv_lon = boatdata.getRef("LON")
        bv_sog = boatdata.getRef("SOG")

        self.client.loop_start()
        while not appdata.shutdown:
            time.sleep(1)
            self.client_race_age += 1
            if self.activated and self.hero_raceid is not None:
                self.mqtt_publish(topic, payload, bv_lat, bv_lon, bv_sog)
                if self.client_race_age > 15:
                    # Server sendet keine Daten mehr oder Verbindung verloren
                    # TODO Frage den http-Server nach den letzten Racestatus
                    self.log.warning(f"Keine Daten vom MQTT-Sever seit ca. {self.client_race_age} Sekunden")
                    self.log.warning("Hole Daten vom HTTP-Server")
                    self.hero_query_race(self.hero_raceid)
                    if self.hero_racestatus['racefinished'] == True:
                        self.hero_racephase = 0
                        self.hero_play_audio('endeWettfahrt')
                    self.activated = False
        self.client.loop_stop()
        self.client.disconnect()
        if cfg['trace']:
            self.trace_fh.close()
