#!/usr/bin/env python3
# coding=utf-8

"""
ahuman.py is to showcase agps3.py, a Python 2.7-3.5 GPSD interface
Defaults host='127.0.0.1', port=2947, gpsd_protocol='json'

Toggle Lat/Lon form with '0', '1', '2', '3' for RAW, DDD, DMM, DMS

Toggle units with  '0', 'm', 'i', 'n', for 'raw', Metric, Imperial, Nautical

Toggle gpsd protocol with 'j', 'a' for 'json', 'nmea' displays

Quit with 'q' or '^c'

python[X] human.py --help for list of commandline options.
"""

import argparse
import curses
import sys
from datetime import datetime
from math import modf
from time import sleep

from gps3 import agps3  # Moe, remember to CHANGE to straight 'import agps3' if not installed,
# or check which Python version it's installed in. You forget sometimes.

__author__ = 'Moe'
__copyright__ = 'Copyright 2015-2016  Moe'
__license__ = 'MIT'
__version__ = '0.33.2'

CONVERSION = {'raw': (1, 1, 'm/s', 'meters'),
              'metric': (3.6, 1, 'kph', 'meters'),
              'nautical': (1.9438445, 1, 'kts', 'meters'),
              'imperial': (2.2369363, 3.2808399, 'mph', 'feet')}


def add_args():
    """Adds commandline arguments and formatted Help"""
    parser = argparse.ArgumentParser()

    parser.add_argument('-host', action='store', dest='host', default='127.0.0.1', help='DEFAULT "127.0.0.1"')
    parser.add_argument('-port', action='store', dest='port', default='2947', help='DEFAULT 2947', type=int)
    parser.add_argument('-json', dest='gpsd_protocol', const='json', action='store_const', default='json', help='DEFAULT JSON objects */')
    parser.add_argument('-device', dest='devicepath', action='store', help='alternate devicepath e.g.,"-device /dev/ttyUSB4"')
    # Infrequently used options
    parser.add_argument('-nmea', dest='gpsd_protocol', const='nmea', action='store_const', help='*/ output in NMEA */')
    # parser.add_argument('-rare', dest='gpsd_protocol', const='rare', action='store_const', help='*/ output of packets in hex */')
    # parser.add_argument('-raw', dest='gpsd_protocol', const='raw', action='store_const', help='*/ output of raw packets */')
    # parser.add_argument('-scaled', dest='gpsd_protocol', const='scaled', action='store_const', help='*/ scale output to floats */')
    # parser.add_argument('-timing', dest='gpsd_protocol', const='timing', action='store_const', help='*/ timing information */')
    # parser.add_argument('-split24', dest='gpsd_protocol', const='split24', action='store_const', help='*/ split AIS Type 24s */')
    # parser.add_argument('-pps', dest='gpsd_protocol', const='pps', action='store_const', help='*/ enable PPS JSON */')
    parser.add_argument('-v', '--version', action='version', version='Version: {}'.format(__version__))
    cli_args = parser.parse_args()
    return cli_args


def satellites_used(feed):
    """Counts number of satellites used in calculation from total visible satellites
    Arguments:
        feed feed=data_stream.satellites
    Returns:
        total_satellites(int):
        used_satellites (int):
    """
    total_satellites = 0
    used_satellites = 0

    if not isinstance(feed, list):
        return 0, 0

    for satellites in feed:
        total_satellites += 1
        if satellites['used'] is True:
            used_satellites += 1
    return total_satellites, used_satellites


def make_time(gps_datetime_str):
    """Makes datetime object from string object"""
    if not 'n/a' == gps_datetime_str:
        datetime_string = gps_datetime_str
        datetime_object = datetime.strptime(datetime_string, "%Y-%m-%dT%H:%M:%S")
        return datetime_object


def elapsed_time_from(start_time):
    """calculate time delta from latched time and current time"""
    time_then = make_time(start_time)
    time_now = datetime.utcnow().replace(microsecond=0)
    if time_then is None:
        return
    delta_t = time_now - time_then
    return delta_t


def unit_conversion(thing, units, length=False):
    """converts base data between metric, imperial, or nautical units"""
    if 'n/a' == thing:
        return 'n/a'
    try:
        thing = round(thing * CONVERSION[units][0 + length], 2)
    except TypeError:
        thing = 'fubar'
    return thing, CONVERSION[units][2 + length]


def sexagesimal(sexathang, latlon, form='DDD'):
    """
    Arguments:
        sexathang: (float), -15.560615 (negative = South), -146.241122 (negative = West)  # Apataki Carenage
        latlon: (str) 'lat' | 'lon'
        form: (str), 'DDD'|'DMM'|'DMS', decimal Degrees, decimal Minutes, decimal Seconds
    Returns:
        latitude: e.g., '15°33'38.214"S'
        longitude: e.g., '146°14'28.039"W'
    """
    cardinal = 'O'
    if not isinstance(sexathang, float):
        sexathang = 'n/a'
        return sexathang

    if latlon == 'lon':
        if sexathang > 0.0:
            cardinal = 'E'
        if sexathang < 0.0:
            cardinal = 'W'

    if latlon == 'lat':
        if sexathang > 0.0:
            cardinal = 'N'
        if sexathang < 0.0:
            cardinal = 'S'

    if form == 'RAW':
        sexathang = '{0:4.9f}°'.format(sexathang)  # 4 to allow -100° through -179.999999° to -180°
        return sexathang

    if form == 'DDD':
        sexathang = '{0:3.6f}°'.format(abs(sexathang))

    if form == 'DMM':
        _latlon = abs(sexathang)
        minute_latlon, degree_latlon = modf(_latlon)
        minute_latlon *= 60
        sexathang = '{0}°{1:2.5f}\''.format(int(degree_latlon), minute_latlon)

    if form == 'DMS':
        _latlon = abs(sexathang)
        minute_latlon, degree_latlon = modf(_latlon)
        second_latlon, minute_latlon = modf(minute_latlon * 60)
        second_latlon *= 60.0
        sexathang = '{0}°{1}\'{2:2.3f}\"'.format(int(degree_latlon), int(minute_latlon), second_latlon)

    return sexathang + cardinal


def show_human():
    """Curses terminal with standard outputs """
    form = 'RAW'
    units = 'raw'

    data_window = curses.newwin(19, 39, 0, 0)
    sat_window = curses.newwin(14, 39, 0, 40)
    device_window = curses.newwin(6, 39, 13, 40)
    packet_window = curses.newwin(7, 79, 19, 0)

    for new_data in gpsd_socket:
        if new_data:
            data_stream.unpack(new_data)

            screen.nodelay(1)
            key_press = screen.getch()

            if key_press == ord('q'):  # quit
                shut_down()
            elif key_press == ord('a'):  # NMEA
                gpsd_socket.watch(enable=False, gpsd_protocol='json')
                gpsd_socket.watch(gpsd_protocol='nmea')
                show_nmea()
            elif key_press == ord('0'):  # raw
                form = 'RAW'
                units = 'raw'
                data_window.clear()
            elif key_press == ord('1'):  # DDD
                form = 'DDD'
                data_window.clear()
            elif key_press == ord('2'):  # DMM
                form = 'DMM'
                data_window.clear()
            elif key_press == ord('3'):  # DMS
                form = 'DMS'
                data_window.clear()
            elif key_press == ord('m'):  # Metric
                units = 'metric'
                data_window.clear()
            elif key_press == ord('i'):  # Imperial
                units = 'imperial'
                data_window.clear()
            elif key_press == ord('n'):  # Nautical
                units = 'nautical'
                data_window.clear()
            elif key_press == ord('d'):  # Refresh device listings
                gpsd_socket.send('?DEVICES;')
                device_window.clear()

            data_window.box()
            data_window.addstr(0, 2, 'AGPS3 Python {}.{}.{} GPSD Interface'.format(*sys.version_info), curses.A_BOLD)
            data_window.addstr(1, 2, 'Time:  {} '.format(data_stream.time))
            data_window.addstr(2, 2, 'Latitude:  {} '.format(sexagesimal(data_stream.lat, 'lat', form)))
            data_window.addstr(3, 2, 'Longitude: {} '.format(sexagesimal(data_stream.lon, 'lon', form)))
            data_window.addstr(4, 2, 'Altitude:  {} {}'.format(*unit_conversion(data_stream.alt, units, length=True)))
            data_window.addstr(5, 2, 'Speed:     {} {}'.format(*unit_conversion(data_stream.speed, units)))
            data_window.addstr(6, 2, 'Heading:   {}° True'.format(data_stream.track))
            data_window.addstr(7, 2, 'Climb:     {} {}'.format(*unit_conversion(data_stream.climb, units, length=True)))
            data_window.addstr(8, 2, 'Status:     {:<}D  '.format(data_stream.mode))
            data_window.addstr(9, 2, 'Latitude Err:  +/-{} {}'.format(*unit_conversion(data_stream.epx, units, length=True)))
            data_window.addstr(10, 2, 'Longitude Err: +/-{} {}'.format(*unit_conversion(data_stream.epy, units, length=True)))
            data_window.addstr(11, 2, 'Altitude Err:  +/-{} {}'.format(*unit_conversion(data_stream.epv, units, length=True)))
            data_window.addstr(12, 2, 'Course Err:    +/-{}   '.format(data_stream.epc), curses.A_DIM)
            data_window.addstr(13, 2, 'Speed Err:     +/-{} {}'.format(*unit_conversion(data_stream.eps, units)), curses.A_DIM)
            data_window.addstr(14, 2, 'Time Offset:   +/-{}  '.format(data_stream.ept), curses.A_DIM)
            data_window.addstr(15, 2, 'gdop:{}  pdop:{}  tdop:{}'.format(data_stream.gdop, data_stream.pdop, data_stream.tdop))
            data_window.addstr(16, 2, 'ydop:{}  xdop:{} '.format(data_stream.ydop, data_stream.xdop))
            data_window.addstr(17, 2, 'vdop:{}  hdop:{} '.format(data_stream.vdop, data_stream.hdop))

            sat_window.clear()
            sat_window.box()
            sat_window.addstr(0, 2, 'Using {0[1]}/{0[0]} satellites (truncated)'.format(satellites_used(data_stream.satellites)))
            sat_window.addstr(1, 2, 'PRN     Elev   Azimuth   SNR   Used')
            line = 2
            if isinstance(data_stream.satellites, list):  # Nested lists of dictionaries are strings before data is present
                for sats in data_stream.satellites[0:10]:
                    sat_window.addstr(line, 2, '{PRN:>2}   {el:>6}   {az:>5}   {ss:>5}   {used:}'.format(**sats))
                    line += 1

            device_window.clear()
            device_window.box()
            if not isinstance(data_stream.devices, list):  # Local machines need a 'device' kick start
                gpsd_socket.send('?DEVICES;')  # to have valid data.  I don't know why.

            if isinstance(data_stream.devices, list):  # Nested lists of dictionaries are strings before data is present.
                for gizmo in data_stream.devices:
                    start_time, _uicroseconds = gizmo['activated'].split('.')  # Remove '.000Z'
                    elapsed = elapsed_time_from(start_time)

                    device_window.addstr(1, 2, 'Activated: {}'.format(gizmo['activated']))
                    device_window.addstr(2, 2, 'Host:{0.host}:{0.port} {1}'.format(args, gizmo['path']))
                    device_window.addstr(3, 2, 'Driver:{driver}     BPS:{bps}'.format(**gizmo))
                    device_window.addstr(4, 2, 'Cycle:{0} Hz {1!s:>14} Elapsed'.format(1 / gizmo['cycle'], elapsed))

            packet_window.clear()
            # packet_window.border(0)
            packet_window.scrollok(True)
            packet_window.addstr(0, 0, '{}'.format(new_data))

#            sleep(.9)

            data_window.refresh()
            sat_window.refresh()
            device_window.refresh()
            packet_window.refresh()
        else:  # Reduced CPU cycles with the non-blocking socket read, by putting 'sleep' here, rather than hitting
            sleep(.3)  # the socket fast and furious with hundreds of empty checks between sleeps.


def show_nmea():
    """NMEA output in curses terminal"""
    data_window = curses.newwin(24, 79, 0, 0)

    for new_data in gpsd_socket:
        if new_data:
            screen.nodelay(1)
            key_press = screen.getch()
            if key_press == ord('q'):
                shut_down()
            elif key_press == ord('j'):  # raw
                gpsd_socket.watch(enable=False, gpsd_protocol='nmea')
                gpsd_socket.watch(gpsd_protocol='json')
                show_human()

            data_window.border(0)
            data_window.addstr(0, 2, 'AGPS3 Python {}.{}.{} GPSD Interface Showing NMEA protocol'.format(*sys.version_info), curses.A_BOLD)
            data_window.addstr(2, 2, '{}'.format(gpsd_socket.response))
            data_window.refresh()
        else:
            sleep(.1)


def shut_down():
    """Closes connection and restores terminal"""
    curses.nocbreak()
    curses.echo()
    curses.endwin()
    gpsd_socket.close()
    print('Keyboard interrupt received\nTerminated by user\nGood Bye.\n')
    sys.exit(1)


if __name__ == '__main__':
    args = add_args()
    gpsd_socket = agps3.GPSDSocket()
    gpsd_socket.connect(args.host, args.port)
    gpsd_socket.watch(gpsd_protocol=args.gpsd_protocol)
    data_stream = agps3.DataStream()

    screen = curses.initscr()
    screen.clear()
    screen.scrollok(True)
    curses.noecho()
    curses.curs_set(0)
    curses.cbreak()

    try:
        if 'json' in args.gpsd_protocol:
            show_human()
        if 'nmea' in args.gpsd_protocol:
            show_nmea()

    except KeyboardInterrupt:
        shut_down()
    except (OSError, IOError) as error:
        gpsd_socket.close()
        curses.nocbreak()
        curses.echo()
        curses.endwin()
        sys.stderr.write('\rAHUMAN error--> {}'.format(error))
        sys.stderr.write('\rahuman connection to gpsd at \'{0}\' on port \'{1}\' failed.\n'.format(args.host, args.port))
        sys.exit(1)  # TODO: gpsd existence check and start

#
# Someday a cleaner Python interface will live here
#
# End