# Version 15. Sep. 2017

import re
import subprocess

from PyQt5.QtCore import QIODevice
from PyQt5.QtSerialPort import QSerialPortInfo, QSerialPort
from PyQt5.QtNetwork import QTcpSocket, QUdpSocket


###############################################################################
# cRingBuffer
# this is the class to handle a ring buffer
#-----------------------------------------------------------------------------#
class cRingBuffer():
    def __init__(self, size):
        self.writepos = 0
        self.readpos = 0
        self.SIZEMASK = size-1
        self.buf = bytearray(size)

    def putInt(self, c):
        nextpos = ( self.writepos + 1 ) & self.SIZEMASK
        if nextpos != self.readpos: #fifo not full
            self.buf[self.writepos] = c;
            self.writepos = nextpos

    # buf must be a bytearray
    def putBuf(self, buf):
        for c in buf: self.putInt( c )

    def getInt(self):
        if self.writepos != self.readpos: #fifo not empty
            c = self.buf[self.readpos]
            self.readpos = ( self.readpos + 1 ) & self.SIZEMASK
            return c

    def available(self):
        d = self.writepos - self.readpos
        if d < 0: return d + (self.SIZEMASK+1)
        return d

    def free(self):
        d = self.writepos - self.readpos;
        if d < 0: return d + (self.SIZEMASK+1)
        return self.SIZEMASK - d #the maximum is size-1

    def isEmpty(self):
        if self.writepos == self.readpos: return True
        return False

    def isNotFull(self):
        netxpos = ( self.writepos + 1 ) & (selfSIZEMASK)
        if nextpos != self.readpos: return True
        return False

    def flush(self):
        self.writepos = 0
        self.readpos = 0

    def size(self):
        return self.SIZEMASK + 1
        
"""
###############################################################################
# cSerialPortComboBox
# this is the class to select a serial COM port
#-----------------------------------------------------------------------------#
class cSerialPortList:

    def key(self,_s):
        return int(_s[3:7])

    def populateList(self):
        availableSerialPortInfoList = QSerialPortInfo().availablePorts()
        availableSerialPortList = []
        for portInfo in availableSerialPortInfoList:
            '''
            print('-')
            print(portInfo)
            print(portInfo.description())
            print(portInfo.portName())
            print(portInfo.serialNumber())
            print(portInfo.productIdentifier())
            print(portInfo.manufacturer())
            print(portInfo.systemLocation()) #unix type port name
            print(portInfo.vendorIdentifier())
            '''
            s = portInfo.portName()
            while len(s)<13: s += ' '
            p = portInfo.description()
            if re.search(r'Virtual COM Port',p):  d = 'Virtual COM Port'
            elif re.search(r'USB Serial Port',p): d = 'USB Serial Port'
            elif re.search(r'Bluetooth',p):       d = 'Bluetooth'
            #elif re.search(r'Standard',p):        d = 'Standard Port'
            else: d = 'Standard'
            availableSerialPortList.append(s+d)
        availableSerialPortList.sort(key=self.key)

        #the idea is from here: http://stackoverflow.com/questions/31868486/list-all-wireless-networks-python-for-pc
        startupinfo = subprocess.STARTUPINFO()
        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
        startupinfo.wShowWindow = subprocess.SW_HIDE
        #results = subprocess.check_output(["netsh", "wlan", "show", "network"], startupinfo=startupinfo)
        #if b'ENSYS NT Logger' in results:
        #    availableSerialPortList.append('ENSYS NT Logger')
        #bug found by rcgroups user fs008
        try:
            results = subprocess.check_output(["netsh", "wlan", "show", "network"], startupinfo=startupinfo)
        except:
            pass
        else:
            if b'ENSYS NT Logger' in results:
                availableSerialPortList.append('ENSYS NT Logger')

        return availableSerialPortList
"""

###############################################################################
# cSerialStream
# this is the class to select a serial COM port
#-----------------------------------------------------------------------------#
#cleaned up to only use TCP, as this seems what ENSYS has finally settled for
cTCP_URL = "172.16.0.1"
cTCP_PORT = 7000
#EspWifiUrl = '192.168.4.1'   
#EspWifiPort = '80' #'23'; #'7167'; #23';

class cSerialStream():

    def __init__(self,baud=115200):
        # they all use the fifo
        self.fifo = cRingBuffer( 16*1024 ) #1024*1024 )

        self.serialIsSocket = False #assume the default

        self.port = QSerialPort()
        self.port.setBaudRate(baud)       #THESE SETTINGS SEEM TO HAVE NO EFFECT !!!!    self.port.baudRate = baud  also doesn't do anything
        self.port.setDataBits(8)
        self.port.setParity(QSerialPort.NoParity)
        self.port.setStopBits(1)
        self.port.setFlowControl(QSerialPort.NoFlowControl)
        self.port.setReadBufferSize( 16*1024 ) #256*1024 )
        self.port.readyRead.connect(self.readPort)

        self.tcpUrl = cTCP_URL
        self.tcpPort = cTCP_PORT
        self.tcp = QTcpSocket()
        self.tcp.setReadBufferSize( 16*1024 ) #256*1024 )
        self.tcp.readyRead.connect(self.readTcp)

    ############################################
    # port low level functions
    
    # signal for QSerialPort
    # readAll is in bytes, fifo needs bytearray
    def readPort(self):
        self.fifo.putBuf( bytearray(self.port.readAll()) )
        
    def writePort(self,barray):
        self.port.write(barray)
        self.port.flush() #see docu
        
    def flushPort(self):
        self.port.clear(QSerialPort.AllDirections)
        self.fifo.flush()

    def openPort(self,portname):
        self.serialIsSocket = False
        self.port.setPortName(portname)
        return self.port.open(QIODevice.ReadWrite) #this unfortunatley b?locks the GUI!!!

    def closePort(self):
        self.port.close()

    ############################################
    # tcp low level functions
    
    # signal for QTcpSocket
    # readAll is in bytes, fifo needs bytearray
    def readTcp(self):
        self.fifo.putBuf( bytearray(self.tcp.readAll()) )
        
    def writeSocket(self,barray):
        self.tcp.write(barray)
        self.tcp.flush()
        
    def flushSocket(self):
        #self.tcp.clear(QSerialPort.AllDirections) #WHAT INSTEAD ???
        self.fifo.flush()
        
    def openSocket(self,tcpUrl=cTCP_URL,tcpPort=8080):
        self.serialIsSocket = True
        self.tcpUrl = tcpUrl
        self.tcpPort = tcpPort
        self.tcp.connectToHost(tcpUrl, tcpPort, QIODevice.ReadWrite ) #QIODevice.ReadWrite is the default
        self.tcp.waitForConnected(500) #waits max 0.5s

    def closeSocket(self):
        self.tcp.close()
        
    ############################################
    # high level user functions

    def open(self, portname,useTcp=False,tcpUrl=cTCP_URL,tcpPort=8080):
        self.fifo.flush()
        if useTcp:
            return self.openSocket(tcpUrl,tcpPort)
        else:
            return self.openPort(portname)

    def close(self):
        if self.serialIsSocket:
            self.closeSocket()
        else:
            self.closePort()

    def isValid(self):
        if self.serialIsSocket:
            if not self.tcp.isValid(): return False
        else:
            if self.port.error(): return False
        return True

    def bytesAvailable(self):
        return self.fifo.available()

    def readOneByte(self):
        return bytes([self.fifo.getInt()])

    def writeBytes(self,barray):
        if self.serialIsSocket:
            self.writeSocket(barray)
        else:    
            self.writePort(barray)
           
    def flush(self):
        if self.serialIsSocket:
            self.flushSocket()
        else:    
            self.flushPort()
            
    def isPort(self):
        if self.serialIsSocket: return False
        return True

    def isSocket(self):
        if self.serialIsSocket: return True
        return False
        
        
'''        
###############################################################################
# cSerialReaderThread
# this is the main class to read via a serial port
# is a worker thread to avoid GUI blocking
#-----------------------------------------------------------------------------#
class cSerialStreamThread(QThread):

    def __init__(self,_serial):
        super().__init__()
        self.canceled = True

        self.serial = _serial

    def __del__(self):
        self.wait()

    def run(self):
        self.canceled = False
        self.runCallback()

    def cancel(self):
        self.canceled = True
        self.cancelCallback()

    def cancelIfRunning(self):
        if self.isRunning(): self.cancel()

    def cancelCallback(self):
        pass

    #helper function, called before thread is started
    def openSerial(self,currentport):
        self.serial.open( currentport )

    #helper function, called after thread is stopped
    def closeSerial(self):
        self.serial.close()

    def runCallback(self):
        #self.port.open(QIODevice.ReadWrite) #this must be done in Main!!
        if not self.serial.isValid(): return
        while True:
            if self.canceled: break

            while self.serial.bytesAvailable(): #digest all data accumulated since the last call
                b = self.readByte()

            self.msleep(5)


    def readOneByte(self):
        return self.serial.readOneByte()

    def emitNewSerialDataAvailable(self):
        self.newSerialDataAvailable.emit()

'''