text: "gpsd client functions"
  # This file is Copyright (c) 2010 by the GPSD project
  # BSD terms apply: see the file COPYING in the distribution root for details.
  #
  # This code run compatibly under Python 2 and 3.x for x >= 2.
  # Preserve this property!
  from __future__ import absolute_import, print_function, division
  
  import json
  import select
  import socket
  import sys
  import time
  
  from .misc import polystr, polybytes
  from .watch_options import *
  
  GPSD_PORT = "2947"
  
  
  class gpscommon(object):
      "Isolate socket handling and buffering from the protocol interpretation."
  
      host = "127.0.0.1"
      port = GPSD_PORT
  
      def __init__(self, host="127.0.0.1", port=GPSD_PORT, verbose=0,
                   should_reconnect=False):
          self.stream_command = b''
          self.linebuffer = b''
          self.received = time.time()
          self.reconnect = should_reconnect
          self.verbose = verbose
          self.sock = None        # in case we blow up in connect
          # Provide the response in both 'str' and 'bytes' form
          self.bresponse = b''
          self.response = polystr(self.bresponse)
  
          if host is not None:
              self.host = host
          if port is not None:
              self.port = port
          self.connect(self.host, self.port)
  
      def connect(self, host, port):
          """Connect to a host on a given port.
  
          If the hostname ends with a colon (`:') followed by a number, and
          there is no port specified, that suffix will be stripped off and the
          number interpreted as the port number to use.
          """
          if not port and (host.find(':') == host.rfind(':')):
              i = host.rfind(':')
              if i >= 0:
                  host, port = host[:i], host[i + 1:]
              try:
                  port = int(port)
              except ValueError:
                  raise socket.error("nonnumeric port")
          # if self.verbose > 0:
          #    print 'connect:', (host, port)
          msg = "getaddrinfo returns an empty list"
          self.sock = None
          for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
              af, socktype, proto, _canonname, sa = res
              try:
                  self.sock = socket.socket(af, socktype, proto)
                  # if self.debuglevel > 0: print 'connect:', (host, port)
                  self.sock.connect(sa)
                  if self.verbose > 0:
                      print('connected to tcp://{}:{}'.format(host, port))
                  break
              # do not use except ConnectionRefusedError
              # # Python 2.7 doc does have this exception
              except socket.error as e:
                  if self.verbose > 1:
                      msg = str(e) + ' (to {}:{})'.format(host, port)
                      sys.stderr.write("error: {}\n".format(msg.strip()))
                  self.close()
                  raise  # propogate error to caller
  
      def close(self):
          "Close the gpsd socket"
          if self.sock:
              self.sock.close()
          self.sock = None
  
      def __del__(self):
          "Close the gpsd socket"
          self.close()
  
      def waiting(self, timeout=0):
          "Return True if data is ready for the client."
          if self.linebuffer:
              return True
          if self.sock is None:
              return False
  
          (winput, _woutput, _wexceptions) = select.select(
              (self.sock,), (), (), timeout)
          return winput != []
  
      def read(self):
          "Wait for and read data being streamed from the daemon."
  
          if None is self.sock:
              self.connect(self.host, self.port)
              if None is self.sock:
                  return -1
              self.stream()
  
          eol = self.linebuffer.find(b'\n')
          if eol == -1:
              # RTCM3 JSON can be over 4.4k long, so go big
              frag = self.sock.recv(8192)
  
              self.linebuffer += frag
              if not self.linebuffer:
                  if self.verbose > 1:
                      sys.stderr.write(
                          "poll: no available data: returning -1.\n")
                  # Read failed
                  return -1
  
              eol = self.linebuffer.find(b'\n')
              if eol == -1:
                  if self.verbose > 1:
                      sys.stderr.write("poll: partial message: returning 0.\n")
                  # Read succeeded, but only got a fragment
                  self.response = ''  # Don't duplicate last response
                  return 0
          else:
              if self.verbose > 1:
                  sys.stderr.write("poll: fetching from buffer.\n")
  
          # We got a line
          eol += 1
          # Provide the response in both 'str' and 'bytes' form
          self.bresponse = self.linebuffer[:eol]
          self.response = polystr(self.bresponse)
          self.linebuffer = self.linebuffer[eol:]
  
          # Can happen if daemon terminates while we're reading.
          if not self.response:
              return -1
          if 1 < self.verbose:
              sys.stderr.write("poll: data is %s\n" % repr(self.response))
          self.received = time.time()
          # We got a \n-terminated line
          return len(self.response)
  
      # Note that the 'data' method is sometimes shadowed by a name
      # collision, rendering it unusable.  The documentation recommends
      # accessing 'response' directly.  Consequently, no accessor method
      # for 'bresponse' is currently provided.
  
      def data(self):
          "Return the client data buffer."
          return self.response
  
      def send(self, commands):
          "Ship commands to the daemon."
          lineend = "\n"
          if isinstance(commands, bytes):
              lineend = polybytes("\n")
          if not commands.endswith(lineend):
              commands += lineend
  
          if self.sock is None:
              self.stream_command = commands
          else:
              self.sock.send(polybytes(commands))
  
  
  class json_error(BaseException):
      "Class for JSON errors"
  
      def __init__(self, data, explanation):
          BaseException.__init__(self)
          self.data = data
          self.explanation = explanation
  
  
  class gpsjson(object):
      "Basic JSON decoding."
  
      def __init__(self):
          self.data = None
          self.stream_command = None
          self.verbose = -1
  
      def __iter__(self):
          "Broken __iter__"
          return self
  
      def unpack(self, buf):
          "Unpack a JSON string"
          try:
              self.data = dictwrapper(json.loads(buf.strip(), encoding="ascii"))
          except ValueError as e:
              raise json_error(buf, e.args[0])
          # Should be done for any other array-valued subobjects, too.
          # This particular logic can fire on SKY or RTCM2 objects.
          if hasattr(self.data, "satellites"):
              self.data.satellites = [dictwrapper(x)
                                      for x in self.data.satellites]
  
      def stream(self, flags=0, devpath=None):
          "Control streaming reports from the daemon,"
  
          if 0 < flags:
              self.stream_command = self.generate_stream_command(flags, devpath)
          else:
              self.stream_command = self.enqueued
  
          if self.stream_command:
              if self.verbose > 1:
                  sys.stderr.write("send: stream as:"
                                   " {}\n".format(self.stream_command))
              self.send(self.stream_command)
          else:
              raise TypeError("Invalid streaming command!! : " + str(flags))
  
      def generate_stream_command(self, flags=0, devpath=None):
          "Generate stream command"
          if flags & WATCH_OLDSTYLE:
              return self.generate_stream_command_old_style(flags)
  
          return self.generate_stream_command_new_style(flags, devpath)
  
      @staticmethod
      def generate_stream_command_old_style(flags=0):
          "Generate stream command, old style"
          if flags & WATCH_DISABLE:
              arg = "w-"
              if flags & WATCH_NMEA:
                  arg += 'r-'
  
          elif flags & WATCH_ENABLE:
              arg = 'w+'
              if flags & WATCH_NMEA:
                  arg += 'r+'
  
          return arg
  
      @staticmethod
      def generate_stream_command_new_style(flags=0, devpath=None):
          "Generate stream command, new style"
  
          if (flags & (WATCH_JSON | WATCH_OLDSTYLE | WATCH_NMEA |
                       WATCH_RAW)) == 0:
              flags |= WATCH_JSON
  
          if flags & WATCH_DISABLE:
              arg = '?WATCH={"enable":false'
              if flags & WATCH_JSON:
                  arg += ',"json":false'
              if flags & WATCH_NMEA:
                  arg += ',"nmea":false'
              if flags & WATCH_RARE:
                  arg += ',"raw":1'
              if flags & WATCH_RAW:
                  arg += ',"raw":2'
              if flags & WATCH_SCALED:
                  arg += ',"scaled":false'
              if flags & WATCH_TIMING:
                  arg += ',"timing":false'
              if flags & WATCH_SPLIT24:
                  arg += ',"split24":false'
              if flags & WATCH_PPS:
                  arg += ',"pps":false'
          else:  # flags & WATCH_ENABLE:
              arg = '?WATCH={"enable":true'
              if flags & WATCH_JSON:
                  arg += ',"json":true'
              if flags & WATCH_NMEA:
                  arg += ',"nmea":true'
              if flags & WATCH_RARE:
                  arg += ',"raw":1'
              if flags & WATCH_RAW:
                  arg += ',"raw":2'
              if flags & WATCH_SCALED:
                  arg += ',"scaled":true'
              if flags & WATCH_TIMING:
                  arg += ',"timing":true'
              if flags & WATCH_SPLIT24:
                  arg += ',"split24":true'
              if flags & WATCH_PPS:
                  arg += ',"pps":true'
              if flags & WATCH_DEVICE:
                  arg += ',"device":"%s"' % devpath
          arg += "}"
          return arg
  
  
  class dictwrapper(object):
      "Wrapper that yields both class and dictionary behavior,"
  
      def __init__(self, ddict):
          "Init class dictwrapper"
          self.__dict__ = ddict
  
      def get(self, k, d=None):
          "Get dictwrapper"
          return self.__dict__.get(k, d)
  
      def keys(self):
          "Keys dictwrapper"
          return self.__dict__.keys()
  
      def __getitem__(self, key):
          "Emulate dictionary, for new-style interface."
          return self.__dict__[key]
  
      def __iter__(self):
          "Iterate dictwrapper"
          return self.__dict__.__iter__()
  
      def __setitem__(self, key, val):
          "Emulate dictionary, for new-style interface."
          self.__dict__[key] = val
  
      def __contains__(self, key):
          "Find key in dictwrapper"
          return key in self.__dict__
  
      def __str__(self):
          "dictwrapper to string"
          return "<dictwrapper: " + str(self.__dict__) + ">"
      __repr__ = __str__
  
      def __len__(self):
          "length of dictwrapper"
          return len(self.__dict__)
  
  #
  # Someday a cleaner Python interface using this machinery will live here
  #
  
  # End
  