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

import json
import os.path
import socket
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.type = None
        self.path = None
        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 SocketNmeaSource(NmeaSource):
    """Abstract class for socket sources."""

    def __init__(self,
                 update_callback,
                 **kwargs):
        super().__init__(update_callback, **kwargs)
        self.s = None

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

    def initialize(self):
        self._connect_socket()

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

        self.initialized = True

    def _really_get(self):
        # TODO: Support quectel-talker quirk
        if not self.initialized:
            self.initialize()

        buf = ''
        do_read = True

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

        return buf

    def _connect_socket(self):
        pass


class UnixSocketNmeaSource(SocketNmeaSource):
    def __init__(self,
                 update_callback,
                 socket_file_path=None,
                 **kwargs):
        super().__init__(update_callback, **kwargs)
        self.type = "Unix socket"
        self.path = socket_file_path

    def _connect_socket(self):
        if (self.path is None
                or not os.path.exists(self.path)):
            raise FileNotFoundError(f"Could not open socket {self.path}")

        self.s = socket.socket(socket.AF_UNIX,
                               socket.SOCK_NONBLOCK | socket.SOCK_STREAM)
        self.s.connect(self.path)


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


class InetSocketNmeaSource(SocketNmeaSource):
    def __init__(self,
                 update_callback,
                 host,
                 port,
                 **kwargs):
        super().__init__(update_callback, **kwargs)
        self.host = host
        self.port = port
        self.type = "Inet socket"
        self.path = f"{host}:{port}"

    def _connect_socket(self):
        # Set to nonblocking only after connecting to prevent timeout exception
        self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.s.connect((self.host, self.port))
        self.s.setblocking(False)


class GpsdNmeaSource(InetSocketNmeaSource):
    """Connect to gpsd and configure it to send NMEAs."""

    def __init__(self,
                 update_callback,
                 **kwargs):
        # TODO: Make host and port configurable
        host = "127.0.0.1"
        port = 2947
        super().__init__(update_callback, host, port, **kwargs)
        self.type = "gpsd"

    def _connect_socket(self):
        super()._connect_socket()
        self.s.setblocking(True)
        reply = self.s.recv(4096)
        version = json.loads(reply)
        if version.get('class', '') != 'VERSION':
            raise ValueError("No VERSION reply from gpsd")
        self.manufacturer = "gpsd"
        self.revision = version.get('release', 'unknown')

        self.s.sendall(b'?WATCH={"class":"WATCH", "nmea":true}\n')
        reply = self.s.recv(4096)
        replies = [str(r, 'utf-8') for r in reply.splitlines()]

        devices = json.loads(replies[0])
        if devices.get('class', '') != 'DEVICES':
            raise ValueError("No DEVICES reply from gpsd")
        devs = devices.get('devices', [])
        if not devs:
            raise ValueError("gpsd has no devices")
        self.model = '+'.join(d.get('path', 'unknown') for d in devs)

        watch = json.loads(replies[1])
        if watch.get('class', '') != 'WATCH':
            raise ValueError("No WATCH reply from gpsd")
        if (not watch.get('enable', False)
                or not watch.get('nmea', False)):
            raise ValueError('Could not enable NMEA output')

        self.s.setblocking(False)


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.type = "ReplayNmeaSource"
        self.path = replay_filename

    def initialize(self):
        try:
            with open(self.path, 'r') as ff:
                self.nmeas = ff.read().split("\n\n")
        except Exception:
            raise NmeaSourceNotFoundError(
                f"Could not read NMEA file '{self.path}'")
        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
