#!/usr/bin/python3

import argparse
import datetime
import os
import random
import sys
import time
import dbus
import gi
gi.require_version('Geoclue', '2.0')
from gi.repository import Geoclue
import requests
import pynmea2
import socket

parser = argparse.ArgumentParser(description='Configure the LIV3F GNSS module',
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('-c', '--cold', action='store_true',
        help='Reset the module clearing time, location, almanac and ephemeris')
parser.add_argument('-e', '--ephemeris', action='store_true',
        help='Download the ephemeris from the internet and upload it to the module')
parser.add_argument('-a', '--almanac', action='store_true',
        help='Download the almanac from the internet and upload it to the module')
parser.add_argument('--dev', default='/dev/gnss0',
        help='The gnss device')
parser.add_argument('-u', '--url', default='https://storage.puri.sm/agps',
        help='The URL to download the almanac and ephemris from')
parser.add_argument('-s', '--seed', action='store_true',
        help='Download the RxNetworks seed')
parser.add_argument('-d', '--date', action='store_true',
        help='Set the date')
parser.add_argument('-l', '--location', action='store_true',
        help='Set the aproximate location derived from geoclue')
parser.add_argument('-r', '--rand', action='store_true',
        help='Set a random date for testing')
parser.add_argument('--const', action='store_true',
        help='Enable more constellations')
parser.add_argument('-w', '--wait', action='store_true',
        help='Wait for a fix')
args = parser.parse_args()

class gnss():

    def __init__(self, args):

        self.sock = None
        self.sock_buf = None
        self.args = args

        if (args.ephemeris or args.almanac) and os.path.exists('/var/run/gnss-share.sock'):
            try:
                self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
                self.sock.connect('/var/run/gnss-share.sock')
                self.sock_buf = b''
            except Exception as e:
                print("Failed to open socket", e)
                self.sock = None

        if not self.sock:
            try:
                sys.stderr.write('Trying port %s\n' % (args.dev))
                self.ser = open(args.dev, "w+b", buffering=0)
            except:
                print('Make sure you have write access to %s' %(args.dev))
                sys.exit(1)

            # 'warm up' with reading some input
            while True:
                line = self.readline()
                sys.stderr.write('read: %s\n' % line)
                try:
                    # try to parse (will throw an exception if input is not valid NMEA)
                    pynmea2.parse(self.readline().decode('ascii', errors='replace'))
                except Exception as e:
                    print("not valid NMEA", e)
                    continue

                print("Found valid NMEA", line)
                return

    def readline(self):
        line = None

        while not line:
            if self.sock:
                if not len(self.sock_buf) or b'\n' not in self.sock_buf:
                    buf = self.sock.recv(256)
                    print("buf: ", buf)
                    self.sock_buf = self.sock_buf + buf

                if b'\n' in self.sock_buf:
                    line = self.sock_buf[:self.sock_buf.index(b'\n')+1]
                    self.sock_buf = self.sock_buf[len(line):]
                print("sock_buf", self.sock_buf)
            else:
                line = self.ser.readline()[:-1]

        print("line: ", line)
        return line.strip()

    def write_cmd(self, cmd, expect=None):
        if self.sock:
            print("Not safe to write serial port")
            return True, ''

        print("cmd: %s" % cmd)
        self.ser.write(str(cmd).encode("ascii"))
        self.ser.write(b'\r\n')
        if expect:
            for i in range(40):
                line = self.readline()
                sys.stderr.write('read: %s\n' % line)
                if expect.encode("ascii") in line:
                    sys.stderr.write('found: %s:%s\n' % (expect, line))
                    return True, line
                if b'ERROR' in line:
                    return False, line

            sys.stderr.write('not found: %s:%s\n' % (expect, line))
            return False, line

        # wait for cmd completion
        for i in range(40):
            line = self.readline()
            sys.stderr.write('wait: %s\n' % line)
            if str(cmd).encode("ascii") in line:
                sys.stderr.write('done: %s:%s\n' % (expect, line))
                return True, line
            if b'ERROR' in line:
                return False, line

        return False, line


    def stm_pause(self):
        msg = pynmea2.GGA('P', 'STMGPSSUSPEND', ())
        self.write_cmd(msg)


    def stm_restart(self):
        msg = pynmea2.GGA('P', 'STMGPSRESTART', ())
        self.write_cmd(msg)


    def stm_savepar(self):
        msg = pynmea2.GGA('P', 'STMSAVEPAR', ())
        self.write_cmd(msg)


    def stm_reset(self):
        msg = pynmea2.GGA('P', 'STMSRR', ())
        self.write_cmd(msg)


    def get_version(self):
        msg = pynmea2.GGA('P', 'STMGETSWVER', ("0"))
        self.write_cmd(msg, "PSTMVER")
        msg = pynmea2.GGA('P', 'STMGETSWVER', ("1"))
        self.write_cmd(msg, "PSTMVER")
        msg = pynmea2.GGA('P', 'STMGETSWVER', ("2"))
        self.write_cmd(msg, "PSTMVER")
        msg = pynmea2.GGA('P', 'STMGETSWVER', ("6"))
        self.write_cmd(msg, "PSTMVER")
        msg = pynmea2.GGA('P', 'STMGETSWVER', ("7"))
        self.write_cmd(msg, "PSTMVER")
        msg = pynmea2.GGA('P', 'STMGETSWVER', ("11",))
        self.write_cmd(msg, "PSTMVER")
        msg = pynmea2.GGA('P', 'STMGETSWVER', ("12",))
        self.write_cmd(msg, "PSTMVER")


    def rf_test(self, sat_id, enable):
        if enable:
            msg = pynmea2.GGA('P', 'STMRFTESTON', (str(sat_id)))
        else:
            msg = pynmea2.GGA('P', 'STMRFTESTOFF', ())
        self.write_cmd(msg)

    def set_constmask(self, cons_mask):
        '''
        bit 0: GPS constellation enabling/disabling
        bit 1: GLONASS constellation enabling/disabling
        bit 2: QZSS constellation enabling/disabling
        bit 3: GALILEO constellation enabling/disabling
        bit 7: BEIDOU constellation enabling/disabling
        '''

        msg = pynmea2.GGA('P', 'STMSETCONSTMASK', (cons_mask, ))
        self.write_cmd(msg)


    def set_const(self):
        msg = pynmea2.GGA('P', 'STMCFGCONST', ('2', '2', '2', '0', '0', ))
        self.write_cmd(msg)


    def set_notch(self, t, mode):
        '''
        type    Sat type ANF path [0 -> GPS; 1->GLONASS]
        mode    ANF operation mode [0, disable, 1 always on, 2 Auto (suggested)]
        '''
        msg = pynmea2.GGA('P', 'STMNOTCH', (str(t), str(mode)))
        self.write_cmd(msg)


    def factory_reset(self):
        msg = pynmea2.GGA('P', 'STMRESTOREPAR', ())
        self.write_cmd(msg)


    def cold_restart(self):
        msg = pynmea2.GGA('P', 'STMCOLD', ('F'))
        self.write_cmd(msg)


    def warm_restart(self):
        msg = pynmea2.GGA('P', 'STMWARM', ())
        self.write_cmd(msg)

    def hot_restart(self):
        msg = pynmea2.GGA('P', 'STMHOT', ())
        self.write_cmd(msg)


    def set_agps(self, enable):
        msg = pynmea2.GGA('P', 'STMCFGAGPS,%d' % (enable), ())
        self.write_cmd(msg)


    def clear_databases(self):
        msg = pynmea2.GGA('P', 'STMSTAGPSINVALIDATE', ('7'))
        self.write_cmd(msg)


    def enable_nmea(self, enable):
        msg = pynmea2.GGA('P', 'STMNMEAONOFF', ('%d' % enable))
        self.write_cmd(msg)


    def gen_pass(self, vendor_id, model_id):
        # $PSTMSTAGPS8PASSGEN,<time>,<VendorID>,<ModelID>
        unix_date = datetime.datetime(1970, 1, 1)
        gps_date = datetime.datetime(1980, 1, 5)
        difference = (gps_date - unix_date)
        total_seconds = difference.total_seconds()
        now = int(time.time())
        gps_time = int(now - total_seconds)

        msg = pynmea2.GGA('P', 'STMSTAGPS8PASSGEN', (str(gps_time), vendor_id, model_id))
        res, line = self.write_cmd(msg, "STMSTAGPS8PASSRTN")
        print("password ", line)
        return line


    def get_seed(self):
        vendor_id = 'ZYDLLXxEH94dEeX2'
        model_id = 'MYST'
        pass_line = self.gen_pass(vendor_id, model_id)
        dId = pass_line.split(b',')[1]
        pw = pass_line[:-4].split(b',')[2]
        print("dID:", dId, " pw:", pw)
        header = {
            'Content-Type': 'application/json',
            'Authorization': 'RXN-SP cId=%s,mId=%s,dId=%s,pw=%s' %
            (vendor_id, model_id, dId.decode('utf-8'), pw.decode('utf-8'))}
        # data = '[ { "ee": {"version": 8, "constellations": [ "gps" ], "seedAge": 0 } } ]'
        data = '[ { "rtAssistance": {"format": "byte", "msgs": [  "GPS:1NAC", "GPS:1ALM" , ] } } ]'
        print("header:", header)
        print("data:", data)
        req = requests.post('http://stm.api.location.io:80/rxn-api/locationApi', data=data, headers=header)
        print(req)
        print(req.text)


    def set_date(self):
        if args.rand:
            random.seed()
            year = random.randint(0, 99)
            mon = random.randint(1, 12)
            day = random.randint(1, 28)
            hour = random.randint(0, 23)
            min = random.randint(0, 59)
            msg = pynmea2.GGA('P', 'STMINITTIME',
                    ('%02d' % (day), '%02d' % (mon), '20%02d' % (year),
                     '%02d' % (hour), '%02d' % (min), '30', ))
        else:
            now = datetime.datetime.utcnow()
            # INITTIME expects values to be 2 or 4 digits long
            msg = pynmea2.GGA('P', 'STMINITTIME', (
                        now.strftime('%d'),
                        now.strftime('%m'),
                        now.strftime('%Y'),
                        now.strftime('%H'),
                        now.strftime('%M'),
                        now.strftime('%S'),
                        ))
        self.write_cmd(msg, "PSTMINITTIMEOK")


    def set_dateloc(self, loc):
        # $PSTMINITGPS,<Lat>,<LatRef>,<Lon>,<LonRef>,<Alt>,<Day>,<Month>,<Year>,<Hour>,<Minute>,<Second>*<checksum><cr><lf>a
        lat = loc.get_property('latitude')
        lon = loc.get_property('longitude')
        if lat < 0:
            lat_ref = 'S'
            lat = lat * -1.0
        else:
            lat_ref = 'N'
        if lon < 0:
            lon_ref = 'W'
            lon = lon * -1.0
        else:
            lon_ref = 'E'
        now = datetime.datetime.utcnow()
        msg = pynmea2.GGA('P', "STMINITGPS,%d%06.3f,%s,%d%06.3f,%s,0010" %
                (int(lat), (lat - int(lat)) * 60.0, lat_ref, int(lon), (lon - int(lon)) * 60.0, lon_ref), (
                    now.strftime('%d'),
                    now.strftime('%m'),
                    now.strftime('%Y'),
                    now.strftime('%H'),
                    now.strftime('%M'),
                    now.strftime('%S'),
                    ))
        self.write_cmd(msg, "PSTMINITGPSOK")
        # GPS engine needs to be restarted
        if self.sock:
            self.hot_restart()


    def write_file(self, path):
        with open(path, "r") as f:
            for line in f:
                if line.startswith("$PSTMEPHEM"):
                    expect = "STMEPHEMOK"
                elif line.startswith("$PSTMALMANAC"):
                    expect = "STMALMANACOK"
                else:
                    expect = None

                self.write_cmd(line, expect)

        time.sleep(0.02)


    def time_synced(self):
        bus = dbus.SystemBus()
        proxy = bus.get_object('org.freedesktop.timedate1',
            '/org/freedesktop/timedate1')
        interface = dbus.Interface(proxy, 'org.freedesktop.DBus.Properties')
        properties = interface.GetAll('org.freedesktop.timedate1')
        print(properties)

        ntp_synced = properties.get('NTPSynchronized', False)

        print("time synced: ", ntp_synced)
        return ntp_synced

    def get_agps_file(self, filename):
        try:
            req = requests.get('%s/%s.txt' % (args.url, filename))
        except Exception as e:
            print("get AGPS file failed", e)
            return False

        print(req.text)
        if self.sock:
            ext = 'txt'
        else:
            ext = 'new'

        try:
            os.makedirs('/var/cache/gnss-share', exist_ok=True)
            with open('/var/cache/gnss-share/%s.%s' % (filename, ext), 'w') as f:
                f.write(req.text)
        except Exception as e:
            print('access to /var/cache/gnss-share/ failed', e)
            sys.exit(1)

        if not self.sock:
            self.write_file('/var/cache/gnss-share/%s.new' % (filename))

        return True


def main(argv):
    g = gnss(args)

    if not g.sock:
        g.enable_nmea(0)

    if args.seed:
        g.set_date()
        g.get_seed()

    if args.const:
        g.set_const()
        g.stm_savepar()
        g.stm_reset()
        g.set_constmask('7')

    if args.cold:
        g.cold_restart()

    if args.rand:
        print("setting random date")
        g.set_date()
    elif args.date:
        if g.time_synced():
            print("time network synced")
        else:
            print("setting date from system clock - YMMV")
        try:
            clue = Geoclue.Simple.new_sync('gnss-config', Geoclue.AccuracyLevel.NEIGHBORHOOD, None)
            location = clue.get_location()
        except Exception as e:
            print("Getting approximate location failed", e)
            location = None

        if args.location and location:
            print("location valid - setting date and location")
            g.set_dateloc(location)
        else:
            print("location invalid - setting date")
            g.set_date()

    if args.ephemeris or args.almanac:
        g.enable_nmea(0)
        if args.almanac:
            while not g.get_agps_file("almanac"):
                time.sleep(5)
                # wait for the server to respond
        if args.ephemeris:
            while not g.get_agps_file("ephemeris"):
                time.sleep(5)
                # wait for the server to respond
    if g.sock:
        os.system("killall -s SIGUSR1 gnss-share")
    else:
        g.enable_nmea(1)

    if args.wait:
        start = time.time()

        fixes = 0

        while fixes < 6:
            line = g.readline()
            try:
                msg = pynmea2.parse(line.decode('ascii', errors='replace'))
            except Exception as e:
                print("NMEA parse exception", e)
                continue
            if isinstance(msg, pynmea2.types.talker.GGA):
                print(repr(msg))
                if msg.gps_qual != 0:
                    fixes = fixes + 1
            if isinstance(msg, pynmea2.types.talker.RMC):
                print(repr(msg))
            sys.stderr.write('read: %s\n' % line)

        print("TTFF: %d" % (time.time() - start))


if __name__ == "__main__":
    sys.exit(main(sys.argv))
