#!/usr/bin/env python
# encoding: utf-8
# SPDX-License-Identifier: BSD-2-clause
# This file is Copyright by the GPSD project
"""Track the currently sky view over time.

Usage:
    ./tracker.py

tracker continues until killed, while updating the skyview in POSIX
shared memory. on exit, it generates a /var/tmp JSON state file. If
this file is present when tracker starts, it's restored allowing
tracker to continue without losing state.

NOTE: Does not behave well if more than one source attached.
"""
from __future__ import absolute_import, print_function, division
import ctypes
from ctypes.util import find_library
import json
import os
import gps

C = ctypes.CDLL(find_library("c"))
C.shm_open.res_type = ctypes.c_int
C.shm_open.arg_types = [ctypes.c_char_p, ctypes.c_int, ctypes.c_int]
C.shm_unlink.res_type = ctypes.c_int
C.shm_unlink.arg_types = [ctypes.c_char_p]
TRACKMAX = 540
STALECOUNT = 10
JFILE = "/var/tmp/gpsd-tracker.json"
the_sets = {}
if str is bytes:
    pb = bytes
else:
    def pb(s):
        """Encode a string(like) to bytes."""
        return bytes(s, encoding="latin-1")


def novel(number, there):
    """Is the coordinate not already in the sets."""
    where = tuple([int(10 * f) for f in there])
    if number not in the_sets:
        sat.adds.add(number)
        the_sets[number] = set([where])
        return True
    if where in the_sets[number]:
        return False
    the_sets[number].add(where)
    return True


class SatTracks(gps.gps):
    """gpsd client writing JSON output."""

    def __init__(self):
        """Prepare the class instance."""
        super(SatTracks, self).__init__()
        self.sattrack = {}  # maps PRNs to Tracks
        self.state = None
        self.needsupdate = 0
        self.heads = [b"", b"", b""]
        self.fd1 = C.shm_open(
            "gpsd-tracker", os.O_CREAT | os.O_RDWR, 0o644
        )
        self.adds = None
        os.ftruncate(self.fd1, 1 << 16)

    def generate_json(self, fd):
        """Stash the current list to POSIX shared memory.."""
        if fd is None:
            exit(1)
        os.lseek(fd, 0, os.SEEK_SET)
        dictionary = {}
        for prn in self.sattrack.keys():
            dictionary[str(prn)] = self.sattrack[prn]["track"]
        for t in self.sattrack.values():
            if t["track"]:
                dictionary[t["prn"]] = t["track"]
        jsot = json.dumps(dictionary)
        os.write(fd, pb(jsot + "\0"))

    def delete_stale(self):
        """Prune dead tracks."""
        stales = []
        for prn in self.sattrack.keys():
            if self.sattrack[prn]["stale"] == 0:
                stales.append(prn)
                self.needsupdate = 1
        if stales:
            print("-%r" % stales)
        for prn in stales:
            del self.sattrack[prn]
            if prn in the_sets:
                the_sets[prn] = set([])

    def insert_sat(self, prn, x, y):
        """Add new entries to tracks."""
        try:
            t = self.sattrack[prn]
        except KeyError:
            t = self.sattrack[prn] = {
                "prn": prn,
                "stale": STALECOUNT,
                "track": [],
            }
        pos = (x, y)
        lenny = len(t["track"])
        t["stale"] = STALECOUNT
        if novel(prn, pos):
            t["track"].insert(0, pos)
            lenny += 1
            if lenny > TRACKMAX:
                print(
                    "Dropping record: Track %d too long %d."
                    % (prn, lenny)
                )
                t["track"] = t["track"][:TRACKMAX]
            self.needsupdate = True

    def update_tracks(self):
        """Age, add and then prune individual tracks."""
        for t in self.sattrack.values():
            if t["stale"]:
                t["stale"] -= 1
        self.adds = set()
        for s in self.satellites:
            self.insert_sat(s.PRN, s.elevation, s.azimuth)
        if self.adds:
            print("+%r" % self.adds)
        self.delete_stale()

    def run(self):
        """Loop through the to class."""
        self.needsupdate = 1
        self.stream(gps.WATCH_ENABLE | gps.WATCH_NEWSTYLE)
        for report in self:
            if report["class"] not in ("SKY"):
                continue
            if "satellites" not in report:
                continue
            if not report["satellites"]:
                continue
            self.update_tracks()
            if self.needsupdate:
                self.generate_json(self.fd1)
                self.needsupdate = 0

    def dump(self):
        """Save part of the state so we can restore it later."""
        print(repr(self.sattrack))
        print(repr(the_sets))
        with open(JFILE, "w") as j:
            json.dump(self.sattrack, j)

    def restore(self):
        """Restore a saved state from earlier."""
        if not os.path.isfile(JFILE):
            return
        print("INIT: Found tracks file.")
        with open(JFILE, "rb") as j:
            self.sattrack = json.load(j)
        self.adds = set()
        for prn in self.sattrack:
            for point in self.sattrack[prn]['track']:
                novel(int(prn), point)


if __name__ == "__main__":
    sat = SatTracks()
    sat.restore()  # attempt track restoration
    try:
        sat.run()
    except KeyboardInterrupt:
        pass
    finally:
        sat.dump()  # save the tracks
