#!/usr/bin/env python3

import datetime, os, re, signal, sys, time
import random
import base64
import requests
import socket
import serial
import http.client
from xml.dom import minidom
from http.client import HTTPConnection, HTTPSConnection
from base64 import b64encode

# ---------------------------------------- wait()
def wait( nSeconds, prompt ):
  utcStart = datetime.datetime.utcnow()
  utcNow   = datetime.datetime.utcnow()
  tDelta   = utcNow - utcStart
  while tDelta.total_seconds() < nSeconds:
    secondsRemaining = nSeconds - tDelta.total_seconds()
    sys.stderr.write( "\rWaiting for %.1f seconds " % ( secondsRemaining ) )
    if not None == prompt:
      sys.stderr.write( "- %s       " % ( prompt ) )

    if secondsRemaining < 1.0:
      time.sleep( secondsRemaining )
    else:
      time.sleep( 1.0 )
    utcNow = datetime.datetime.utcnow()
    tDelta = utcNow - utcStart
  print

# ---------------------------------------- getOrPost()
gGetTimeout = 150
def getOrPost( url, username, password, postData ):
  global gGetTimeout

  request = urllib2.Request( url )
  if username != None and password != None:
    credentials = base64.b64encode( '%s:%s' % ( username, password ) )
    request.add_header( "Authorization", "Basic %s" % ( credentials ) )

  result = None
  action = "GET"
  if not None == postData:
    action = "POST"

  try:
    if None == postData:
      result = urllib2.urlopen( request, timeout=gGetTimeout )
    else:
      result = urllib2.urlopen( request, data=postData )
    response = ""
    for line in result.readlines():
      line = line.rstrip()
      response += line + "\n"
  except:
    sys.stderr.write( "%s exception: %s\n" % ( action, sys.exc_info()[0] ) )
    response = None

  return response

# ---------------------------------------- get()
#def get( url, username, password ):
#  return getOrPost( url, username, password, None )
# -------------------------------------------------- get()
def get(host, suburl, username, password, secure=False):
  ''' Perform HTTP GET.
        host     - Remote host IP or FQDN. Does not include "http(s)://".
        username - Remote host IP or FQDN. Does not include "http(s)://".
        password - Remote host IP or FQDN. Does not include "http(s)://".
        path     - Subpath to fetch.
        secure   - Set True for HTTPS.
  '''
  url = 'http'
  if secure:
    url += 's'
  url += '://' + host + suburl

  try:
    r = requests.get(url, auth=(username,password), verify=False, headers={'Acept-Ending':'gzip'}, timeout = 10)
    if r.status_code == 404:
      return None
    else:
      return r.content

  except requests.exceptions.ConnectTimeout:
    # Sleep for a bit in case we are sending too many requests.
    print("WARNING: requests.get() -> requests.exceptions.ConnectTimeout")
    print("Sleep for 2.0 seconds")
    time.sleep(2.0)

    return None

  except requests.exceptions.ConnectionError:
    # Make sure we are not sending too many failed requests.
    print("WARNING: requests.get() -> requests.exceptions.ConnectError")
    print("Sleep for 2.0 seconds")
    time.sleep(2.0)

    return None

  except requests.exceptions.ReadTimeout:
    print("WARNING: requests.get() -> requests.exceptions.ReadTimeout")
    print("Sleep for 2.0 seconds")
    time.sleep(2.0)

    return None

# Old get() body.
#  conn = None
#  if secure:
#    conn = HTTPSConnection( host,
#                            context = ssl._create_unverified_context()
#                          )
#  else:
#    conn = HTTPConnection( host )
#
#  headers = None
#  if not None == username and not None == password:
#    userPassStr = "%s:%s" % (username, password)
#    userPassBytes = bytearray( userPassStr, 'utf-8' )
#    userPass = b64encode( userPassBytes ).decode( "utf-8" )
#    headers = { 'Authorization' : 'Basic %s' % userPass }
#
#  data = None
#  errorStr = ""
#  try:
#    errorStr = "conn.request() failed: %s" % ( host )
#    if None == headers:
#      conn.request( "GET", suburl )
#    else:
#      conn.request( "GET", suburl, headers=headers )
#
#    errorStr = "conn.getresponse() failed"
#    response = conn.getresponse()
#
#    errorStr = "response.read() failed"
#    data = response.read()
#
#  except socket.timeout as st:
#    print("get() error: %s [socket.timeout]" % ( errorStr ))
#    data = None
#
#  except http.client.HTTPException as e:
#    print("get() error: %s [http.client.HTTPException]" % ( errorStr ))
#    data = None
#
#  #except http.client.HTTPException:
#  #  print("get() error: %s HTTPException]" % ( errorStr ))
#  #  data = None
#
#  #except http.client.NotConnected:
#  #  print("get() error: %s [NotConnected]" % ( errorStr ))
#  #  data = None
#
#  #except http.client.InvalidURL:
#  #  print("get() error: %s [InvalidURL]" % ( errorStr ))
#  #  data = None
#
#  #except http.client.UnknownProtocol:
#  #  print("get() error: %s [UnknownProtocol]" % ( errorStr ))
#  #  data = None
#
#  #except http.client.UnknownTransferEncoding:
#  #  print("get() error: %s [UnknownTransferEncoding]" % ( errorStr ))
#  #  data = None
#
#  #except http.client.UnimplementedFileMode:
#  #  print("get() error: %s [UnimplementedFileMode]" % ( errorStr ))
#  #  data = None
#
#  #except http.client.IncompleteRead:
#  #  print("get() error: %s [IncompleteRead]" % ( errorStr ))
#  #  data = None
#
#  #except http.client.ImproperConnectionState:
#  #  print("get() error: %s [ImproperConnectionState]" % ( errorStr ))
#  #  data = None
#
#  #except http.client.CannotSendRequest:
#  #  print("get() error: %s [CannotSendRequest]" % ( errorStr ))
#  #  data = None
#
#  #except http.client.CannotSendHeader:
#  #  print("get() error: %s [CannotSendHeader]" % ( errorStr ))
#  #  data = None
#
#  #except http.client.ResponseNotReady:
#  #  print("get() error: %s [ResponseNotReady]" % ( errorStr ))
#  #  data = None
#
#  #except http.client.BadStatusLine:
#  #  print("get() error: %s [BadStatusLine]" % ( errorStr ))
#  #  data = None
#
#  #except http.client.LineTooLong:
#  #  print("get() error: %s [LineTooLong]" % ( errorStr ))
#  #  data = None
#
#  #except http.client.RemoteDisconnected:
#  #  print("get() error: %s [RemoteDisconnected]" % ( errorStr ))
#  #  data = None
#
#  except:
#    print("get() error: %s" % ( errorStr ))
#    data = None
#
#  conn.close()
#
#  return data

# ---------------------------------------- post()

def post( url, username, password, postData ):
  return getOrPost( url, username, password, postData )

# ---------------------------------------- class: Receiver
class Receiver():

  def __init__(self, host, username, password):
    self.host = host
    self.username = username
    self.password = password
    self.isOkResponse = re.compile( "^OK.*" )
    self.isOkXml = re.compile( "^<OK>1</OK>.*" )
    self.verbose = True

  def setGetTimeout(self, timeout):
    global gGetTimeout
    gGetTimeout = timeout

  def getCgiBin(self, subUrl):
    suburl = "/cgi-bin/%s" % ( subUrl )
    response = get( self.host, suburl, self.username, self.password ).decode('utf-8')
    try:
      xmlDoc = minidom.parseString( response )
      xmlPretty = xmlDoc.toprettyxml()
      #print("getCgiBin(%s) returns XML: %s" % ( subUrl, response ))
      return xmlPretty
    except:
      #print("getCgiBin(%s) returns TEXT: %s" % ( subUrl, response ))
      return response

  def getXml(self, xmlFile):
    suburl = "/xml/dynamic/%s" % ( xmlFile )
    response = get( self.host, suburl, self.username, self.password )
    if None == response:
      print("get() failed: %s %s %s %s" % ( self.host, suburl, self.username, self.password ))
      return None

    xmlString = response.decode('utf-8')
    try:
      xmlDoc = minidom.parseString( xmlString )
      xmlPretty = xmlDoc.toprettyxml()
      return xmlPretty
    except:
      if self.verbose:
        print("Error: %s -> %s" % ( xmlFile, xmlString ))
      return None

  def getStoreXml(self, xmlFile, prefix):
    xmlPretty = self.getXml( xmlFile )
    if not None == xmlPretty:
      with open( "%s%s" % (prefix, xmlFile), "w" ) as f:
        sys.stderr.write('write: %s%s\n' % ( prefix, xmlFile ))
        f.write(xmlPretty)
    return xmlPretty

  def getStoreTimestampedXml(self, xmlFile):
    utc = datetime.datetime.utcnow()
    dt =   "%04d%02d%02d-%02d%02d%02d-" \
         % ( utc.year, utc.month, utc.day, utc.hour, utc.minute, utc.second )
    return self.getStoreXml( xmlFile, dt )

  def piGet(self, progIntCommand):
    suburl = "/prog/%s" % ( progIntCommand )
    response = get( self.host, suburl, self.username, self.password )
    if not None == response:
      response = response.decode('utf-8')
    return response

  def piPost(self, progIntCommand, fileToPost):
    url = "http://%s/prog/%s" % ( self.host, progIntCommand )
    #response = post( url, self.username, self.password, postData )
    cmd = "curl -F f=@%s --user \"%s:%s\" \"%s\"" \
          % ( fileToPost, self.username,  self.password, url )
    response = os.system( cmd )
    return response

  def createFile(self, path, size):
    ret = self.piGet( "set?file&path=%s&size=%d&create=yes&checkspace=yes" \
                          % ( path, size ) )
    time.sleep(0.1)
    if None == ret:
      return False
    elif self.isOkResponse.match( ret ):
      return True
    else:
      return False

  def deleteFile(self, path):
    ret = self.piGet( "delete?file&path=%s" % ( path ) )
    time.sleep(0.1)
    if None == ret:
      return False
    elif self.isOkResponse.match( ret ):
      return True
    else:
      return False

  def reset(self):
    ret = self.piGet( "reset?system" )
    if None == ret:
      return False
    else:
      return self.isOkResponse.match( ret )

  def installFirmware(self, path, failsafe, postInstallWait, prefix=None):
    fsArg = "no"
    if failsafe:
      fsArg = "yes"

    #postData = None
    #with open( path, "r" ) as f:
    #  postData = f.read()

    ret = self.piPost( "upload?firmwareFile&failsafe=%s" % ( fsArg ), path )

    wait1Prompt = "failsafe install (4 minutes)"
    wait2Prompt = "post-install startup(%.1f sec)" % postInstallWait
    if None == prefix:
      print( "Post return: %s" % ( ret ) )
    else:
      wait1Prompt = "%s: %s" % ( prefix, wait1Prompt )
      wait2Prompt = "%s: %s" % ( prefix, wait2Prompt )
      print( "%s: Post return: %s" % ( prefix, ret ) )

    if failsafe:
      wait(240, wait1Prompt)

    if not None == postInstallWait:
      wait(postInstallWait, wait2Prompt)

    return 0 == int(ret)

  def uploadClone(self, path, name):
    ret = self.piPost( "upload?datafile&file=/Internal/Clone/%s" % ( name ), path )
    print("upload?datafile(clone) return: %s" % ( ret ))

    time.sleep(3.0)

    return 0 == int(ret)

  def uploadToEmmc(self, chunkFile, sectorBase):
    ret = self.piPost(  "upload?datafile&file=/emmc_image&sectorbase=%d" \
                       % sectorBase,                                     \
                       chunkFile )
                       
    print("upload?datafile(emmc) return: %s" % ( ret ))

    return 0 == int(ret)

  def uploadDataFile(self, localFile, receiverPath):
    ret = self.piPost(  "upload?datafile&file=%s" % receiverPath, localFile )
    print("upload?datafile(%s -> %s) return: %s" \
          % ( localFile, receiverPath, ret ))
    return 0 == int(ret)

  def downloadTextFile( self, receiverPath ):
    raw  = self.piGet( "show?file&path=%s" % ( receiverPath ) )
    return raw

  def setTestMode(self, enable):
    yesno = "yes"
    if not enable:
      yesno = "no"
    ret = self.piGet( "set?testmode&enable=%s&password=TURING" % ( yesno ) )
    print("set?testmode return: %s" % ( ret ))
    if None == ret:
      return False
    else:
      return self.isOkResponse.match( ret )

  def installClone(self, name):
    ret = self.piGet( "install?clone&name=%s" % ( name ) )
    print("install?clone return: %s" % ( ret ))
    time.sleep(3.0)
    if None == ret:
      return False
    else:
      return self.isOkResponse.match( ret )

  def resetSystem(self, postResetWait):
    ret = self.piGet( "reset?system" )

    if not None == postResetWait:
      wait(postResetWait, "post-reset startup (%.1f sec)" % postResetWait)

    print("reset?system return: %s" % ( ret ))

    if None == ret:
      return False
    else:
      return self.isOkResponse.match( ret )

  def setPowerSaving(self, saveEnable, wakeEnable, wakeIntSec, wakeDurSec):
    saving = "off"
    if saveEnable:
      saving = "on"

    waking = "off"
    if wakeEnable:
      waking = "on"

    cmd  = "xml/dynamic/dataLogger.xml?powerSave=1"
    payload = {
      'powerSaveEnable' : saving,
      'failsafe'        : waking,
      'duration'        : "%d" % ( wakeDurSec ),
      'interval'        : "%d" % ( wakeIntSec )
    }

    suburl  = "/xml/dynamic/dataLogger.xml"
    suburl += "?powerSaveEnable=%s" % ( saving )
    suburl += "&failsafeEnable=%s" % ( waking )
    suburl += "&duration=%d" % ( wakeDurSec )
    suburl += "&interval=%d" % ( wakeIntSec )
    suburl += "&powerSave=%1"

    response = get( self.host, suburl, self.username, self.password ).decode('utf-8')

    if None == response:
      print("response: None")
      return False
    else:
      retValue = self.isOkXml.match( response )
      if not retValue:
        print("response: %s" % ( response ))
      return retValue

  def getSerialNumber( self ):
    returnValue = None
    response = self.piGet( "show?serialnumber" )
    if not None == response:
      m = re.match( r"^SerialNumber sn=([^ ]+) .+$", response )
      if m:
        returnValue = m.group(1)
    return returnValue
    

  def resetSpaStatistics( self ):
    resp = self.getCgiBin("resetProcessorLoading.xml?ResetProcessorLoading=1")
    try:
      xmlDoc = minidom.parseString( resp )
      items = xmlDoc.getElementsByTagName( 'OK' )
      if items[0].firstChild.nodeValue == '1':
        return True
      else:
        return False
    except:
      return False

  def addUser( self, username, password, allowConfig, fileDownload,
               fileDelete, editUsers, ntrip ):
    cmd = "security_conf.xml?addUser_userName=%s&addUser_password=%s" \
          % ( username, password )
    if editUsers:
      cmd += "&allowEditUser=1"
    if fileDownload:
      cmd += "&allowDownload=1"
    if fileDelete:
      cmd += "&allowDelete=1"
    if allowConfig:
      cmd += "&allowConfig=1"
    if ntrip:
      cmd += "&allowNTripCaster=1"
    cmd += "&ntripConnectLimit=1"

    resp = self.getCgiBin( cmd )
    print("Response: %s" % ( resp ))

  def getDir( self, rootPath, recursive ):
    global reStart, reStop, reDir, reFile
    reStart = re.compile( r"^.show directory path=.*$" )
    reStop  = re.compile( r"^.end of show directory.$" )
    reSize  = re.compile( r"^size +([0-9]+)$" )
    reAvail = re.compile( r"^available +([0-9]+)$" )
    reDir   = re.compile( r"^directory name=(.+)" )
    reFile  = re.compile( r"^file +name=([^ ]+) +size=([^ ]+) +ctime=([^ ]+) +attr=([^ ]+) *$" )
  
    ret = []
  
    if not rootPath[:1] == "/":
      rootPath = "/" + rootPath

    response = self.piGet( "show?directory&path=%s" % ( rootPath ) )
  
    if response == None:
      sys.stderr.write( "Error: show dir failed: %s\n" % ( rootPath ) )
      return None
  
    lines = response.split( '\n' )
  
    size      = None
    available = None
    inListing = False
    for line in lines:
      line = line.rstrip()
  
      if 0 == len(line):
        continue
  
      if reStart.match( line ):
        inListing = True
      elif reStop.match( line ):
        inListing = False
  
      elif size == None:
        m = reSize.match( line )
        if m:
          size = m.group(1)
          ret.append( ["size", size, rootPath] )
        else:
          print("err: expected size")
          print("response: %s" % ( response ))
    
      elif available == None:
        m = reAvail.match( line )
        if m:
          available = m.group(1)
          ret.append( ["available", available, rootPath] )
        else:
          print("err: expected available")
  
      else:
        md = reDir.match( line )
        mf = reFile.match( line )
  
        if md:
          # Always store the full path to the directory.
          ret.append( [ "dir", rootPath + "/" + md.group(1) ] )
        elif mf:
          # Append: name, size, ctime, attr, dir
          ret.append( [ "file", mf.group(1), mf.group(2), mf.group(3), mf.group(4), rootPath ] )
        else:
          sys.stderr.write( "unrecognized: %s\n" % ( line ) )
  
    if recursive:
      subdirs = [ ]
      for el in ret:
        if el[0] == "dir":
          # sys.stderr.write( "adding subdir from %s: %s\n" % ( rootPath, el[1] ) )
          subdirs.append( el[1] )
  
      for subdir in subdirs:
        # sys.stderr.write( "getting subdir: %s\n" % ( subdir ) )
        _ret = self.getDir( subdir, recursive )
        if _ret == None:
          sys.stderr.write( "Error: getDir() returns None: %s\n" % ( subdir ) )
        else:
          for el in _ret:
            ret.append( el )
  
    return ret

# ---------------------------------------- class RxSerial()
# Warning: This was implemented for testing at one point but is not really
#          actively used/supported. It should probably be discarded or merged
#          with the Receiver() class.
class RxSerial():
  def __init__(self,  comPort, baudRateValue):
    self.serialPort = serial.Serial( port=comPort, baudrate=baudRateValue, \
                        parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, \
                        timeout=5 )

  def __del__(self):
    # Call to close serial port and invoke Dispose()
    self.serialPort.Close()

  def reset(self):
    cmd = bytearray()
    cmd.append( 0x02 )
    cmd.append( 0x00 )
    cmd.append( 0x58 )
    cmd.append( 0x07 )
    cmd.append( 0x03 )
    cmd.append( 0x00 )
    cmd.append( ord('R') )
    cmd.append( ord('E') )
    cmd.append( ord('S') )
    cmd.append( ord('E') )
    cmd.append( ord('T') )
    sum = 0
    for i in range(1,11):
      sum += cmd[i]
    cmd.append( sum % 256 )
    cmd.append( 0x03 )
    self.serialPort.write( cmd )

  def getSerial(self):
    cmd = b"\x02\x00\x06\x00\x06\x03"
    self.serialPort.write( cmd )
    response = self.serialPort.read( 200 )
    return response

  def breakRet(self):
    cmd = b"\x02\x00\x6f\x00\x6f\x03"
    self.serialPort.write( cmd )
    response = self.serialPort.read( 200 )
    return response

  def breakRet(self):
    cmd = b"\x02\x00\x6f\x00\x6f\x03"
    self.serialPort.write( cmd )
    response = self.serialPort.read( 200 )
    return response
    
  def printRoutingTable( self ):
    cmd = b"\x02\x00\xAB\x03\xFF\x03\x01\xB1\x03"
    self.serialPort.write( cmd )
    response = self.serialPort.read( 200 )
    print( "response: %s" % ( response.decode( 'utf-8' ) ))

    cmd = b"\x02\x00\xAB\x03\xFF\x03\x01\xB1\x03"
    self.serialPort.write( cmd )
    while not None == response:
      response = self.serialPort.read( 200 )
      print( "response: %s" % ( response.decode( 'utf-8' ) ))

  def getInternal( self ):
    suburl  = "download/Internal"
    raw = get( self.host, suburl, self.username, self.password )
    return raw


# ---------------------------------------- class Log()

class Log():
  def __init__(self,  fileIdentifier, suffix):
    self.fileIdentifier = fileIdentifier
    self.fileSuffix     = suffix
    self.logFileName    = None
    self.utcStart       = None
    self.enableSeconds  = False
    self.enableMinutes  = False

  def addElapsedSeconds(self):
    self.enableSeconds = True

  def addElapsedMinutes(self):
    self.enableMinutes = True

  def log(self, str):
    utc = datetime.datetime.utcnow()
    dt = "%04d/%02d/%02d %02d:%02d:%02d" % ( utc.year, utc.month, utc.day, utc.hour, utc.minute, utc.second )
    dt += ".%03d" % ( utc.microsecond / 1000 )

    if self.logFileName is None:
      self.utcStart = utc
      self.logFileName = "%04d%02d%02d-%02d%02d%02d-%s.%s" \
                         % ( utc.year, utc.month, utc.day, \
                             utc.hour, utc.minute, utc.second, \
                             self.fileIdentifier, self.fileSuffix )

    with open( self.logFileName, "a" ) as flog:
      timePrefix = dt
      if self.enableSeconds:
        timePrefix += ",%.3f" % ( (utc - self.utcStart).total_seconds() )
      if self.enableMinutes:
        timePrefix += ",%.5f" % ( (utc - self.utcStart).total_seconds()/60.0 )

      flog.write( "%s,%s\n" % ( timePrefix, str ) )

    print("%s: %s" % ( timePrefix, str ))

    return "%s %s" % ( timePrefix, str )

# -------------------------------------------------- getNodeText()
def getNodeText(nodelist):
  rc = []
  for node in nodelist:
    if node.nodeType == node.TEXT_NODE:
      rc.append(node.data)
  return ''.join(rc)

# -------------------------------------------------- getNodeText()
def nlTextByName(nodelist, name):
  rc = []
  for node in nodelist.childNodes:
    if node.nodeName == name and node.nodeType == node.TEXT_NODE:
      rc.append(node.data)
  return ''.join(rc)

# -------------------------------------------------- getElemText()
def elGetText( xmlDoc, name ):
  items = xmlDoc.getElementsByTagName( name )
  if None == items:
    return None
  if items.length > 0:
    return getNodeText(items[0].childNodes)
  else:
    return ""

