import time
import serial
from serial import Serial
import glob
import sys
import numpy as np
from pandas import DataFrame
import csv

import matplotlib
from matplotlib import pyplot as plt
matplotlib.use("TkAgg")


def readline(port):
    """
    Used to out from buffer of VersaPic electronics.
    Timeout is set to 2 sec, and answer will end at b'\ r'.
    """
    read_values = bytes()
    totalstring = bytes()
    start_time = time.time()
    loop = 0
    while True:
        chars = port.read()
        read_values += chars
        if chars == b'\r':
            totalstring += read_values + b'\n'
        if time.time() - start_time > 0.03:
            return totalstring[0:-1]





def extract_value(string):
    """
    Extract value from a string and returns float
    """
    try:
        start = string.index('\t') + len('\t')
        end = string.index('\r', start)
        return np.float(string[start:end])
    except ValueError:
        return None


def serial_ports():
    """ Lists and returns serial port names
        checks for either Win, Linux or Cygwin platform.
        :raises EnvironmentError:
            On unsupported or unknown platforms
        :returns:
            A list of the serial ports available on the system
    """
    if sys.platform.startswith('win'):
        ports = ['COM%s' % (i + 1) for i in range(256)]
    elif sys.platform.startswith('linux') or sys.platform.startswith('cygwin'):
        # this excludes your current terminal "/dev/tty"
        ports = glob.glob('/dev/tty[A-Za-z]*')
    elif sys.platform.startswith('darwin'):
        ports = glob.glob('/dev/tty.*')
    else:
        raise EnvironmentError('Unsupported platform')

    result = []
    for port in ports:
        try:
            s = serial.Serial(port)
            s.close()
            result.append(port)
        except (OSError, serial.SerialException):
            pass
    return result


#if __name__ == '__main__':
    #print(serial_ports())


# ----------------------------------------------------------------------------------------------------------------------
# Start of Spectrometer Class
# ----------------------------------------------------------------------------------------------------------------------


class Spectrometer():  # pylint: disable-msg=R0904
    """ Spectrometer object
    A connection to a spectrometer or TEC board.

    """

    def open(self, comport, baudrate):

        try:
            print(comport)
            print(baudrate)
            port = serial.Serial(comport,
                                 baudrate=baudrate,
                                 timeout=0.1,
                                 parity=serial.PARITY_NONE,
                                 stopbits=serial.STOPBITS_ONE,
                                 bytesize=serial.EIGHTBITS)
            port.write(b'\r\n')
            time.sleep(1)
            ans0 = port.read(port.inWaiting())  # emptying any left over scramble
            # Read the version of electronics used
            port.write(b'*VERS?\r\n')
            rcv = readline(port)
            self.port = port
            if 'PIC_Ve' in rcv.decode("utf-8"):
                self.type_name = 'PIC_'
                self.type = 0
                self.port.write(b'*PARA:PIX?\r\n')
                rcp = readline(self.port)
                pix = int(extract_value(rcp.decode("utf-8")))
                if pix == 2048:
                    self.rbl = 4098
                elif pix == 1024:
                    self.rbl = 2050
                elif pix == 512:
                    self.rbl = 1026
                elif pix == 256:
                    self.rbl = 514
                print(' PIC', pix, 'pixels')
                self.pix = pix
                self.get_wavelengthaxis()
                self.serial = self.get_deviceid()
                self.set_scaletime()
        except OSError:
            print('There was some sort of error')

    def close(self):
        """Closes the connection to the COM port

        Close the serial port connection of the spectrometer object
        It is just good practice to use this, as it frees the port for other
        applications.
        """
        self.port.close()
        print('Connection to spectrometer closed')

# ---------------------------------------------------------------------------------------------------
    # SPECTROMETER FUNCTIONS
# ---------------------------------------------------------------------------------------------------
    # GET FUNCTIONS

    def get_wavelengthaxis(self):
        """Returns the wavelength axis

        Returns
        ---------
        out : 1d-array
            numpy array with the center wavelength per pixel
        """
        lst = self.get_calibrationcoefficients()
        pixel = np.arange(0, self.pix).astype(float)
        wavelength_axis = lst[0] + lst[1] * pixel + lst[2] * pixel ** 2 + \
                          lst[3] * pixel ** 3 + lst[4] * pixel ** 4
        self.wavelength_axis = wavelength_axis
        return wavelength_axis

    def get_calibrationcoefficients(self):
        """Get the 5 calibration coefficients

        The calibration coefficients are polynomial coefficients that relate the
        wavelength in nm to the pixel number p via:
        lambda(nm)=f0+f1*p+f2*p**2+f3*p**3+f4*p**4

        Returns
        --------
        (float, float, float, float, float)
            a tuple with the 5 polynomial parameters (floats) in order f0 to f4
        """
        port = self.port
        port.write(b'*PARA:FIT0?\r\n')
        fit0 = readline(port)
        port.write(b'*PARA:FIT1?\r\n')
        fit1 = readline(port)
        port.write(b'*PARA:FIT2?\r\n')
        fit2 = readline(port)
        port.write(b'*PARA:FIT3?\r\n')
        fit3 = readline(port)
        port.write(b'*PARA:FIT4?\r\n')
        fit4 = readline(port)
        fitv0 = extract_value(fit0.decode("utf-8"))
        fitv1 = extract_value(fit1.decode("utf-8"))
        fitv2 = extract_value(fit2.decode("utf-8"))
        fitv3 = extract_value(fit3.decode("utf-8"))
        fitv4 = extract_value(fit4.decode("utf-8"))
        return (fitv0, fitv1, fitv2, fitv3, fitv4)

    def get_deviceid(self):
        """ get the version of the electronics
        """
        port = self.port
        port.write(b'*PARA:SERN?\r\n')
        time.sleep(0.1)
        serialnum = readline(port)
        serialnumber = extract_value(serialnum.decode("utf-8"))
        return serialnumber

    def get_basic(self):
        """ get the configured basic parameters of the device
        Example.
        COMMAND: A(ask)
        PIC_Versa256 VERSION 2.605 12.02.12 COMP S/N 1964
        CHANNELS              :      1
        PIXEL PER LINE        :   2048
        LAMP PREHEAT TIME[ms] :      1
        INTEGRATION TIME[ms]  :      2
        CHANNEL 0 FITx^0      : 2.064757E+02
        CHANNEL 0 FITx^1      : 4.998266E-01
        CHANNEL 0 FITx^2      : -4.278255E-05
        CHANNEL 0 FITx^3      : -2.753093E-08
        CHANNEL 0 FITx^4      : 6.510066E-12
        """
        port = self.port
        port.write(b'*PARA:BASIC?\r\n')
        time.sleep(0.1)
        # -----------------------------
        read_values = bytes()
        start_time = time.time()
        while True:
            chars = port.read()
            read_values += chars
            if time.time() - start_time > 0.1:
                return read_values.decode("utf-8")
                break

    def get_statusErr(self):
        """ get the error status of the device
        """
        port = self.port
        port.write(b'*STAT:ERR?\r\n')
        time.sleep(0.1)
        StatErr = readline(port).decode("utf-8")
        return StatErr

    def get_statusErrDescription(self):
        """ get the error status of the device along with a description of the error code.
        """
        port = self.port
        port.write(b'*STAT:TXTERR?\r\n')
        time.sleep(0.1)
        StatErr = readline(port).decode("utf-8")
        return StatErr

    def get_firmwareversion(self):
        """ get the firmware version of the electronics
        """
        port = self.port
        port.write(b'*VERS?\r\n')
        time.sleep(0.1)
        firmware = readline(port)
        firmwareversion = firmware.decode("utf-8")
        return firmwareversion

    def get_ControlCommandList(self):
        """ Get a help list of the control commands
        """
        port = self.port
        port.write(b'*CONTR?\r\n')
        time.sleep(0.1)
        ListCommands = readline(port).decode("utf-8")
        #print(ListCommands)
        return ListCommands

    def get_ScanDelay(self):
        """ Get a help list of the control commands
        """
        port = self.port
        port.write(b'*PARA:SDEL?\r\n')
        time.sleep(0.1)
        scandelay = readline(port).decode("utf-8")
        return scandelay

    # GET FUNCTIONS

    def set_ScanDelay(self, delay=0):

        command = '*PARA:SDEL ' + str(delay) + '\r\n'
        self.port.write(command.encode(encoding='UTF-8'))
        time.sleep(0.3)
        return None

    def get_ADCresolution(self):
        """ Get a help list of the control commands
        """
        port = self.port
        port.write(b'*PARA:ADCR?\r\n')
        time.sleep(0.1)
        scandelay = readline(port).decode("utf-8")
        return scandelay

    def get_AllParameters(self):
        """ Get a help list of the control commands
        """
        port = self.port
        port.write(b'*PARA:ALLPARA?\r\n')
        time.sleep(0.2)
        allpara = readline(port).decode("Latin-1")
        return allpara

    def set_scaletime(self, val=0):
        """
        Sets the time scale for the spectrometer's integration time.
        Time is in milliseconds scale by the values as 10^(-val)
        0 = 1 ms
        1 = 100 μs
        2 = 10 μs
        3 = 1 μs
        """
        if val == 0 or val == 1 or val == 2 or val == 3:
            command = '*PARA:SCALTI ' + str(val) + '\r\n'
            self.port.write(command.encode(encoding='UTF-8'))
            time.sleep(0.3)
            out1 = self.port.read(self.port.inWaiting())
        else:
            ValueError('Input should be 0,1,2 or 3')
        return None

    def get_scaletime(self):
        """ Get the PARA:SCALTI
        """
        command = '*PARA:SCALTI?' + '\r\n'
        self.port.write(command.encode(encoding='UTF-8'))
        time.sleep(0.3)
        string = self.port.read(self.port.inWaiting()).decode("latin-1")
        val = extract_value(string)
        return int(val)

    def set_faultypixel(self, pixnum):
        """
        Used to set a faulty pixel in the sensor array.
        """
        command = '*PARA:FAULTPI ' + str(pixnum) + '\r\n'
        self.port.write(command.encode(encoding='UTF-8'))
        time.sleep(0.3)
        out1 = self.port.read(self.port.inWaiting())

        command = '*PARA:SAVE' + '\r\n'
        self.port.write(command.encode(encoding='UTF-8'))
        time.sleep(0.3)

        command = '*PARA:FAULTPI?' + '\r\n'
        self.port.write(command.encode(encoding='UTF-8'))
        time.sleep(0.3)
        string = self.port.read(self.port.inWaiting()).decode("latin-1")
        print(string)
        val = extract_value(string)
        print(val)

        return None

    def raw_measure(self, integration_time, average, nr_of_attempts=1):
        """ Perform a raw measurement.
        INPUT:
        - time, the integration time in ms
        - average, the number of averages
        OPTIONAL INPUT:
        - attempts, the number of times the program tries to get the measurement
        done. This is to catch communication errors)
        OUTPUT
        """
        if integration_time > 65535:
            ValueError('Integration time to large')
        if integration_time < 1:
            ValueError('Integration time to small')

        for attempt_nr in range(nr_of_attempts):
            if attempt_nr > 1:
                print('Second measure attempt, communication not fluent')
            fail = False
            self.port.read(self.port.inWaiting())
            command = '*MEAS:DARK ' + str(int(integration_time)) + ' ' + \
                      str(int(average)) + ' 1\r\n'
            self.port.write(command.encode(encoding='UTF-8'))
            dat = self.port.read(2)
            start_time = time.time()
            while len(dat) < self.rbl:
                dat = dat + self.port.read(self.port.inWaiting())
                if time.time() - start_time > 10 + integration_time / 100:
                    # it might be worthwile to put a repeat here
                    print('Error reporting')
                    print(len(dat))
                    print(dat)
                    print(np.fromstring(dat, dtype="uint16"))
                    print('timeout, not enough bits were transferred')
                    fail = True
            if fail:
                raise IOError('timeout, not enough bits were transferred')
            if (self.type_name == 'PIC_') and (self.pix == 2048) and (fail is False):
                data = np.frombuffer(dat, dtype="uint16")
                # dat[0:2]==b'\x06\x07'
                # print('L',len(dat))
                return data[1:]
            if (self.type_name == 'PIC_') and (self.pix == 1024) and (fail is False):
                data = np.frombuffer(dat, dtype="uint16")
                return data[1:]
            if (self.type_name == 'PIC_') and (self.pix == 512) and (fail is False):
                data = np.frombuffer(dat, dtype="uint16")
                return data[1:]
            if (self.type_name == 'PIC_') and (self.pix == 256) and (fail is False):
                data = np.frombuffer(dat, dtype="uint16")
                return data[1:]
        raise IOError('timeout, not enough bits were transferred 4 times')

# ---------------------------------------------------------------------------------------------------
    # TEC CONTROL FUNCTIONS
# ---------------------------------------------------------------------------------------------------
    # GET FUNCTIONS

    def TEC_getBaud(self):
        """ Get the Baudrate of the TEC board
        """
        command = '*PARA:BAUD?' + '\r\n'
        self.port.write(command.encode(encoding='UTF-8'))
        time.sleep(0.3)
        string = self.port.read(self.port.inWaiting()).decode("latin-1")
        val = extract_value(string)
        return int(val)

    def TEC_getTEMP(self):
        """ Get the current Temperature of the TEC board
        """
        command = '*PARA:tectemp?' + '\r\n'
        self.port.write(command.encode(encoding='UTF-8'))
        time.sleep(0.3)
        string = self.port.read(self.port.inWaiting()).decode("latin-1")
        val = extract_value(string)
        return int(val)

    def TEC_getKP(self):
        """ Get the value of KP
        """
        command = '*PARA:teckp?' + '\r\n'
        self.port.write(command.encode(encoding='UTF-8'))
        time.sleep(0.3)
        string = self.port.read(self.port.inWaiting()).decode("latin-1")
        val = extract_value(string)
        return int(val)

    def TEC_getKI(self):
        """ Get the value of KI
        """
        command = '*PARA:tecki?' + '\r\n'
        self.port.write(command.encode(encoding='UTF-8'))
        time.sleep(0.3)
        string = self.port.read(self.port.inWaiting()).decode("latin-1")
        val = extract_value(string)
        return int(val)

    def TEC_getsetTemp(self):
        """ Get the Target temperature of TEC board
        """
        command = '*PARA:tecref?' + '\r\n'
        self.port.write(command.encode(encoding='UTF-8'))
        time.sleep(0.3)
        string = self.port.read(self.port.inWaiting()).decode("latin-1")
        val = extract_value(string)
        return int(val)

    def TEC_getAdapthreshold(self):
        """ Get the Threshold value. This determines the window which the temperature is kept within.
        """
        command = '*PARA:tecthres?' + '\r\n'
        self.port.write(command.encode(encoding='UTF-8'))
        time.sleep(0.3)
        string = self.port.read(self.port.inWaiting()).decode("latin-1")
        val = extract_value(string)
        return int(val)

    # SET FUNCTIONS

    def TEC_setKP(self, NewKP):
        """ Set a new value of KP
        """
        command = '*PARA:teckp ' + NewKP + '\r\n'
        self.port.write(command.encode(encoding='UTF-8'))
        time.sleep(0.3)

    def TEC_setKI(self, NewKI):
        """ Set a new value of KI
        """
        command = '*PARA:tecki ' + NewKI + '\r\n'
        self.port.write(command.encode(encoding='UTF-8'))
        time.sleep(0.3)

    def TEC_setRefTemp(self, NewSetTemp):
        """ Set a new target temperature
        """
        command = '*para:tecref ' + NewSetTemp + '\r\n'
        self.port.write(command.encode(encoding='UTF-8'))
        time.sleep(0.3)

    def TEC_setAdapThreshold(self, NewAdapThreshold):
        """ Set a new value for the threshold
        """
        command = '*para:tecthres ' + NewAdapThreshold + '\r\n'
        self.port.write(command.encode(encoding='UTF-8'))
        time.sleep(0.3)

# -----------------------------------------------------------------------------------------------------------------------
# Example of how to use the script
# -----------------------------------------------------------------------------------------------------------------------

# Use the spectrometer class to create a new object named VersaPic. Adjust as needed.
VersaPic = Spectrometer()
VersaPic.open('COM4', 115200)
TEC = Spectrometer()
TEC.open('COM22', 115200)

# Print different values stored in spectrometer electronics.
print('Device ID: ', VersaPic.get_deviceid())
print('The firmware version: ', VersaPic.get_firmwareversion())
print('Number of pixels for the spectrometer detector: ', VersaPic.pix)
print('Wavelength calibration coefficient: ', VersaPic.get_calibrationcoefficients())
print('Status of the device: ', VersaPic.get_ControlCommandList())
print('Delay of the device: ', VersaPic.get_ADCresolution())
print(VersaPic.get_AllParameters())

# Print different values stored in TEC CONTROL electronics.
print('TEC Baud rate', TEC.TEC_getBaud())
print('TEC Temperature', TEC.TEC_getTEMP())
print('TEC KP value', TEC.TEC_getKP())
print('TEC KI value', TEC.TEC_getKI())
print('TEC Set temperature', TEC.TEC_getsetTemp())
print('TEC Threshold around set temperature', TEC.TEC_getAdapthreshold())


# -----------------------------------------------------------------------------------------------------------------------


# Create a wavelength axis for spectrum plot
wavelengthAxis = VersaPic.get_wavelengthaxis()

plt.figure(1)
# Plot a graph of 10 spectra averaged at 100 ms integration time and display the result.
x = VersaPic.raw_measure(100, 10)
plt.plot(x, color='b')

plt.ylabel('Counts')
plt.xlabel('Wavelength [nm]')
plt.show()

# Write the spectrum values to a .csv file named python.csv
with open('python.csv', 'w', newline='') as file:
    writer = csv.writer(file)
    writer.writerow(x)

# Close the COM port of the spectrometer
VersaPic.close()
TEC.close()


