#! /usr/bin/env python
#
#
# RF Monitor
#
#
# Copyright 2015 Al Brown
#
# RF signal monitor
#
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

import threading
import time

from serial.serialutil import SerialException
import serial.tools.list_ports

from rfmonitor.events import Event, Events, post_event


TIMEOUT = 15


class GpsDevice(object):
    BITS = [serial.FIVEBITS, serial.SIXBITS, serial.SEVENBITS,
            serial.EIGHTBITS]
    PARITIES = [serial.PARITY_NONE, serial.PARITY_EVEN, serial.PARITY_ODD,
                serial.PARITY_MARK, serial.PARITY_SPACE]
    STOPS = [serial.STOPBITS_ONE, serial.STOPBITS_ONE_POINT_FIVE,
             serial.STOPBITS_TWO]

    def __init__(self):
        self.enabled = False
        self.port = ''
        self.baud = 115200
        self.bits = serial.EIGHTBITS
        self.parity = serial.PARITY_NONE
        self.stops = serial.STOPBITS_ONE
        self.soft = False

    def get_ports(self):
        ports = [port[0] for port in serial.tools.list_ports.comports()]
        return ports

    def get_bauds(self):
        return list(serial.Serial.BAUDRATES)


class Gps(threading.Thread):
    def __init__(self, eventHandler, gpsDevice):
        threading.Thread.__init__(self)
        self.name = 'GPS'

        self._gpsDevice = gpsDevice
        self._eventHandler = eventHandler

        self._comm = None
        self._timeout = None
        self._cancel = False

        self._sats = {}

        self.start()

    def __timeout(self):
        self.stop()
        event = Event(Events.GPS_TIMEOUT, msg='GPS timeout')
        post_event(self._eventHandler, event)

    def __serial_read(self):
        isSentence = False
        sentence = ''
        while not self._cancel:
            data = self._comm.read(1)
            if data:
                self._timeout.reset()
                if data == '$':
                    isSentence = True
                    continue
                if data == '\r' or data == '\n':
                    isSentence = False
                    if sentence:
                        yield sentence
                        sentence = ''
                if isSentence:
                    sentence += data
            else:
                time.sleep(0.1)

    def __checksum(self, data):
        checksum = 0
        for char in data:
            checksum ^= ord(char)
        return "{0:02X}".format(checksum)

    def __global_fix(self, data):
        if data[6] in ['1', '2']:
            lat = self.__coord(data[2], data[3])
            lon = self.__coord(data[4], data[5])

            event = Event(Events.GPS_LOC, loc=(lat, lon))
            post_event(self._eventHandler, event)

    def __sats(self, data):
        message = int(data[1])
        messages = int(data[1])
        viewed = int(data[3])

        if message == 1:
            self._sats.clear()

        blocks = (len(data) - 4) / 4
        for i in range(0, blocks):
            sat = int(data[4 + i * 4])
            level = data[7 + i * 4]
            used = True
            if level == '':
                level = None
                used = False
            else:
                level = int(level)
            self._sats[sat] = {'Level': level,
                               'Used': used}

        if message == messages and len(self._sats) == viewed:
            event = Event(Events.GPS_SATS, sats=self._sats)
            post_event(self._eventHandler, event)

    def __coord(self, coord, orient):
        pos = None

        if '.' in coord:
            if coord.index('.') == 4:
                try:
                    degrees = int(coord[:2])
                    minutes = float(coord[2:])
                    pos = degrees + minutes / 60.
                    if orient == 'S':
                        pos = -pos
                except ValueError:
                    pass
            elif coord.index('.') == 5:
                try:
                    degrees = int(coord[:3])
                    minutes = float(coord[3:])
                    pos = degrees + minutes / 60.
                    if orient == 'W':
                        pos = -pos
                except ValueError:
                    pass

        return pos

    def __open(self):
        self._timeout = Timeout(self.__timeout)
        self._comm = serial.Serial(self._gpsDevice.port,
                                   baudrate=self._gpsDevice.baud,
                                   bytesize=self._gpsDevice.bits,
                                   parity=self._gpsDevice.parity,
                                   stopbits=self._gpsDevice.stops,
                                   xonxoff=self._gpsDevice.soft,
                                   timeout=0)

    def __read(self):
        for resp in self.__serial_read():
            nmea = resp.split('*')
            if len(nmea) == 2:
                data = nmea[0].split(',')
                if data[0] in ['GPGGA', 'GPGSV']:
                    checksum = self.__checksum(nmea[0])
                    if checksum == nmea[1]:
                        if data[0] == 'GPGGA':
                            self.__global_fix(data)
                        elif data[0] == 'GPGSV':
                            self.__sats(data)
                    else:
                        warn = 'Invalid checksum for {} sentence'.format(data[0])
                        event = Event(Events.GPS_WARN, msg=warn)
                        post_event(self._eventHandler, event)

    def __close(self):
        if self._timeout is not None:
            self._timeout.cancel()
        if self._comm is not None:
            self._comm.close()

    def run(self):
        try:
            self.__open()
            self.__read()
        except SerialException as error:
            event = Event(Events.GPS_ERROR, msg=error.message)
            post_event(self._eventHandler, event)
        except OSError as error:
            event = Event(Events.GPS_ERROR, msg=error)
            post_event(self._eventHandler, event)
        except ValueError as error:
            event = Event(Events.GPS_ERROR, msg=error)
            post_event(self._eventHandler, event)

        self.__close()

    def stop(self):
        self._cancel = True


class Timeout(threading.Thread):
    def __init__(self, callback):
        threading.Thread.__init__(self)
        self.name = 'GPS Timeout'

        self._callback = callback
        self._done = threading.Event()
        self._reset = True

        self.start()

    def run(self):
        while self._reset:
            self._reset = False
            self._done.wait(TIMEOUT)

        if not self._done.isSet():
            self._callback()

    def reset(self):
        self._reset = True
        self._done.clear()

    def cancel(self):
        self._done.set()


if __name__ == '__main__':
    print 'Please run rfmonitor.py'
    exit(1)