# Copyright 2021-2022 Teemu Ikonen
# SPDX-License-Identifier: GPL-3.0-only

import re
import satellite.modem_manager_defs as mm
import os.path
import socket
from pydbus import SystemBus
from pynmea2.nmea import NMEASentence
from gi.repository import GLib


class NmeaSourceNotFoundError(Exception):
    pass


class ModemError(Exception):
    pass


class ModemLockedError(ModemError):
    pass


class ModemNoNMEAError(ModemError):
    pass


class NmeaSource:
    def __init__(self, update_callback, refresh_rate=1, save_filename=None,
                 **kwargs):
        self.update_callback = update_callback
        self.refresh_rate = refresh_rate
        self.save_file = None if save_filename is None \
            else open(save_filename, 'w')
        self.initialized = False
        self.manufacturer = None
        self.model = None
        self.revision = None

    def __del__(self):
        if self.save_file is not None:
            self.save_file.close()

    def initialize(self):
        pass

    def _really_get(self):
        pass

    def _maybe_save(self, nmeas):
        if self.save_file is not None:
            self.save_file.write(nmeas)
            self.save_file.write("\n\n")
            self.save_file.flush()

    def get(self):
        nmeas = self._really_get()
        self._maybe_save(nmeas)
        return nmeas

    def close(self):
        pass


class UnixSocketNmeaSource(NmeaSource):
    def __init__(self,
                 update_callback,
                 socket_file_path=None,
                 **kwargs):
        super().__init__(update_callback, **kwargs)
        self.socket_file_path = socket_file_path

    def on_read_data_available(self, io_channel, condition, **unused):
        self.update_callback()

    def initialize(self):
        if self.socket_file_path is None or \
           not os.path.exists(self.socket_file_path):
            return

        self.s = socket.socket(socket.AF_UNIX,
                               socket.SOCK_NONBLOCK | socket.SOCK_STREAM)
        try:
            self.s.connect(self.socket_file_path)
        except Exception as e:
            raise e

        self.channel = GLib.IOChannel.unix_new(self.s.fileno())
        self.channel.add_watch(GLib.IO_IN, self.on_read_data_available)

        self.old_nmea_buf = ''
        self.initialized = True

    def _really_get(self):
        if not self.initialized:
            self.initialize()

        buf = ''
        do_read = True

        while do_read:
            read_buf = self.channel.readline()
            buf += read_buf
            if read_buf == '':
                do_read = False

        return buf


class GnssShareNmeaSource(UnixSocketNmeaSource):
    def __init__(self, update_callback, **kwargs):
        super().__init__(update_callback,
                         socket_file_path='/var/run/gnss-share.sock',
                         **kwargs)


class ModemManagerNmeaSource(NmeaSource):
    def __init__(self, update_callback, **kwargs):
        super().__init__(update_callback, **kwargs)
        self.bus = SystemBus()
        self.manager = self.bus.get('.ModemManager1')
        self.modem = None
        self.old_refresh_rate = None
        self.old_sources_enabled = None
        self.old_signals = None
        self.location_updated = None

    def initialize(self):
        objs = self.manager.GetManagedObjects()
        mkeys = list(objs.keys())
        if mkeys:
            mstr = mkeys[0]
        else:
            raise NmeaSourceNotFoundError("No Modems Found")
        info = objs[mstr]['org.freedesktop.ModemManager1.Modem']
        self.manufacturer = info.get('Manufacturer')
        self.model = info.get('Model')
        self.revision = info.get('Revision')
        self.modem = self.bus.get('.ModemManager1', mstr)

        try:
            if self.modem.State > 0:
                if self.old_refresh_rate is None:
                    self.old_refresh_rate = self.modem.GpsRefreshRate
                if self.old_sources_enabled is None:
                    self.old_sources_enabled = self.modem.Enabled
                if self.old_signals is None:
                    self.old_signals = self.modem.SignalsLocation
                cap = self.modem.Capabilities
                if (cap & mm.MM_MODEM_LOCATION_SOURCE_GPS_NMEA) == 0:
                    raise NmeaSourceNotFoundError(
                        "Modem does not support NMEA")
                self.modem.Setup(
                    (mm.MM_MODEM_LOCATION_SOURCE_GPS_NMEA
                     | (cap & mm.MM_MODEM_LOCATION_SOURCE_AGPS_MSB)),
                    True)
            else:
                raise ModemError("Modem state is: %d" % self.modem.State)
        except AttributeError as e:
            if self.modem.State == mm.MM_MODEM_STATE_LOCKED:
                raise ModemLockedError from e
            else:
                raise ModemError from e
        except Exception as e:
            raise e

        self.modem.SetGpsRefreshRate(self.refresh_rate)
        self.location_updated = self.bus.subscribe(
            sender='org.freedesktop.ModemManager1',
            iface='org.freedesktop.DBus.Properties',
            signal='PropertiesChanged',
            arg0='org.freedesktop.ModemManager1.Modem.Location',
            signal_fired=self.update_callback)
        self.initialized = True

    def _really_get(self):
        if not self.initialized:
            self.initialize()
        try:
            loc = self.modem.GetLocation()
        except Exception as e:
            self.initialized = False
            raise e

        retval = loc.get(mm.MM_MODEM_LOCATION_SOURCE_GPS_NMEA)
        if retval is None:
            self.initialized = False
            raise ModemNoNMEAError
        return retval

    def close(self):
        if self.location_updated is not None:
            self.location_updated.disconnect()
        if self.old_sources_enabled is not None:
            self.modem.Setup(self.old_sources_enabled, self.old_signals)
        if self.old_refresh_rate is not None:
            self.modem.SetGpsRefreshRate(self.old_refresh_rate)


class QuectelNmeaSource(ModemManagerNmeaSource):

    def _really_get(self):
        return self.fix_talker(super()._really_get())

    def fix_talker(self, nmeas):
        pq_re = re.compile(r'''
            ^\s*\$?
            (?P<talker>PQ)
            (?P<sentence>\w{3})
            (?P<data>[^*]*)
            (?:[*](?P<checksum>[A-F0-9]{2}))$''', re.VERBOSE)
        out = []
        for nmea in (n for n in nmeas.split('\r\n') if n):
            mo = pq_re.match(nmea)
            if mo:
                # The last extra data field is Signal ID, these are
                # 1 = GPS, 2 = Glonass, 3 = Galileo, 4 = BeiDou, 5 = QZSS
                # Determine talker from Signal ID
                talker = 'QZ' if mo.group('data').endswith('5') else 'BD'
                # Fake talker and checksum
                fake = talker + "".join(mo.group(2, 3))
                out.append('$' + fake + "*%02X" % NMEASentence.checksum(fake))
            else:
                out.append(nmea)

        return "\r\n".join(out)


class ReplayNmeaSource(NmeaSource):
    def __init__(self, update_callback, replay_filename=None, refresh_rate=1,
                 **kwargs):
        super().__init__(update_callback, refresh_rate=refresh_rate, **kwargs)
        self.nmeas = []
        self.counter = 0
        if replay_filename is None:
            raise NmeaSourceNotFoundError("NMEA file name not given")
        self.replay_filename = replay_filename
        self.manufacturer = "Satellite"
        self.model = "ReplayNmeaSource"

    def initialize(self):
        try:
            with open(self.replay_filename, 'r') as ff:
                self.nmeas = ff.read().split("\n\n")
        except Exception:
            raise NmeaSourceNotFoundError(
                f"Could not read NMEA file '{self.replay_filename}'")
        self.counter = 0
        GLib.timeout_add(self.refresh_rate * 1000, self.callback, None)

    def callback(self, x):
        self.update_callback()
        return True

    def _really_get(self):
        if not self.nmeas:
            return None
        nmea = self.nmeas[self.counter]
        self.counter += 1
        if self.counter >= len(self.nmeas):
            self.counter = 0
        return nmea
