"""
.. module:: laser
    :platform: unix
    :synopsis: This module is for communicating with the laser.

.. codeauthor:: Nick Mondrik
Date: 01/2020

Control code for an Ekspla NT-252 tunable laser
Serial Number PGD 217
"""

import serial
import argparse
import string
import numpy as np
import logging
import time

NPULSES_MIN = 1
NPULSES_MAX = 50000
WAVELENGTH_MIN = 335
WAVELENGTH_MAX = 2600

class LaserSerialInterface:
    """
    This class is for communicating with the NT252 laser through the RS232 serial interface.
    TODO: Add code to check error states in all modules

    """

    def __init__(self, port='/dev/ttyACM.LASER'):
        self.state = 'Not Connected'
        self.commands = {'state_query': '/M_CPU800/18/Power',
            'mode_select': '/M_CPU800/18/Continuous%20%2F%20Burst%20mode%20%2F%20Trigger%20burst',
            'npulses': '/M_CPU800/18/Burst length',
            'wavelength': '/MidiOPG/31/WaveLength',
            'energy_level': '/M_CPU800/18/Output Energy level',
            'ext_interlock_state': '/M_CPU800/18/External interlock state'}
        self.serial = serial.Serial(port=port, baudrate=19200, timeout=5)
        self._read_termination = b'\x03'
        self._write_termination = '\r'
        self.check_state()


    def read(self):
        """
        Mid-level function to read from the laser registers.
        """
        response = self.serial.read_until(self._read_termination)
        response = response.decode('ascii')
        response = response.strip('\r\n\x03')
        return response
        

    def write(self, cmd):
        """
        Mid-level function to write a command to the laser.  Wraps the serial.write low level command.
        Before writing, the command string must first be encoded to ASCII bytes -- so no unicode allowed.
        """
        if cmd[-1] != self._write_termination:
            cmd += self._write_termination
        cmd = cmd.encode('ascii')
        self.serial.write(cmd)
        return


    def check_state(self):
        """
        Queries the "Power" register in the laser controler to determine the state of the system. Should be [ON, OFF, FAULT]
        """

        cmd = self.commands['state_query']
        self.write(cmd)
        self.state = self.read().lower()
        if self.state not in ['on', 'off', 'fault']:
            raise RuntimeError('check_state: Laser reports unrecognized state {}'.format(self.state))
        if self.state == 'fault':
            print('check_state: Warning!  Laser reports fault state.')
        logging.info('check_state: State: {}'.format(self.state))
        return

    def check_empty_response(self, calling_function):
        """
        After some set requests, the laser responds with an empty string.  If that doesn't happen, we should know.
        Needs the name of the calling function to specify where the error occurred.

        :param calling_function: String specifying the name of the calling function.  This is used to help localize the source of the error.
        """
        
        response = self.read()
        if response != '':
            raise RuntimeError('{}: Expected empty response, got {} instead.'.format(calling_function, response))
        return


    def set_wavelength(self, wavelength):
        """
        This method changes the wavelength of the laser.

        :param wavelength: This is the value of the wavelength to be set. Units are in nanometers.
        :return:
        """

        wavelength = np.float(wavelength)
        wavelength_change_msg = self.commands['wavelength'] + '/{:6.2f}'.format(wavelength)
        if not (WAVELENGTH_MIN <= wavelength <= WAVELENGTH_MAX):
            logging.info('set_wavelength: Wavelength {} out of bounds {} to {}'.format(wavelength, WAVELENGTH_MIN, WAVELENGTH_MAX))
            raise ValueError('set_wavelength: Wavelength {} out of bounds {} to {}'.format(wavelength, WAVELENGTH_MIN, WAVELENGTH_MAX))

        self.write(wavelength_change_msg)
        self.check_empty_response('set_wavelength')
        logging.info('set_wavelength: Sent change wavelength change request for {}'.format(wavelength))
        return


    def get_wavelength(self):
        """
        :return wavelength: wavelength of laser
        """
        self.write(self.commands['wavelength'])
        response = self.read()
        if response == '':
            raise RuntimeError('get_wavelength: No response to wavelength check.')
        wavelength = np.float(response.strip('nm'))
        return wavelength
    
    
    def set_mode(self, mode):
        """
        Change the laser to either continuous or burst mode.  If in burst mode, you will have to explicity call
        self.trigger_burst to trigger a burst.  You should also use self.set_npulses to select the number of pulses

        :param mode: Mode to change to. Must be either 'Continuous' or 'Burst'
        """
        
        if mode.lower() == 'continuous':
            cmd_suffix = 'Continuous'
        elif mode.lower() == 'burst':
            cmd_suffix = 'Burst'
        else:
            raise ValueError('set_mode: mode {} not recognized'.format(mode))
        
        cmd = self.commands['mode_select'] + '/{}'.format(cmd_suffix)
        self.write(cmd)

        self.check_empty_response('set_mode')
        return


    def get_mode(self):
        """
        Query the laser to determine the current operating mode.
        TODO: Add logic to ensure mode returned is a recognized value
        """
        cmd = self.commands['mode_select']
        self.write(cmd)
        response = self.read()
        return response


    def trigger_burst(self):
        """
        Fire a burst of npulses from the laser.
        """

        cmd = self.commands['mode_select'] + '/{}'.format('Trigger')
        self.write(cmd)

        self.check_empty_response('trigger_burst') # Just a guess that this should be the case
        return


    def set_npulses(self, npulses):
        """
        Sets number of pulses to be fired by the laser.  The rep rate of the laser is generally 1kHz, so total exposure time provided
        by npulses is (npulses / 1000) seconds.

        :param npulses: Int.  Number of pulses to be fired.
        """

        if type(npulses) != type(1):
            raise TypeError('set_npulses: npulses must be an int.  You provided type {}'.format(type(npulses)))
        if not (NPULSES_MIN <= npulses <= NPULSES_MAX):
            raise ValueError('set_npulses: npulses must be between {} and {}.  You requested {}.'.format(NPULSES_MIN, NPULSES_MAX, npulses))
        
        cmd = self.commands['npulses'] + '/{:5d}'.format(npulses)
        self.write(cmd)

        self.check_empty_response('set_npulses')
        return


    def get_npulses(self):
        """
        Read and return the current burst length stored in the laser.
        
        TODO: Add logic to make sure response from the laser makes sense
        """

        cmd = self.commands['npulses']
        self.write(cmd)
        response = self.read()
        return int(response)

    def set_energy_level(self, energy_level):
        """
        Sets the energy output level of the laser.  Allowed values are [OFF, Adjust, MAX].
        :param energy_level: Requested energy level.
        """
        if energy_level.lower() == 'off':
            elevel = 'OFF'
        elif energy_level.lower() == 'adjust':
            elevel = 'Adjust'
        elif energy_level.lower() == 'max':
            elevel = 'MAX'
        else:
            raise ValueError('set_energy_level: energy level {} not recognized.'.format(energy_level))

        cmd = self.commands['energy_level'] + '/{}'.format(elevel)
        self.write(cmd)
        self.check_empty_response('set_energy_level')
        return

    def get_energy_level(self):
        """
        Fetch current output energy level from the laser register.
        """
        cmd = self.commands['energy_level']
        self.write(cmd)
        response = self.read()
        if response not in ['OFF', 'Adjust', 'MAX']:
            raise RuntimeError('get_energy_level: Response {} not in expected energy level states.'.format(response))
        return response

    def get_ext_interlock_state(self):
        """
        Find out whether the external interlock is closed or open.
        """
        cmd = self.commands['ext_interlock_state']
        self.write(cmd)
        response = self.read()
        if response not in ['OK', 'NOT OK', 'Defeated']:
            raise RuntimeError('get_ext_interlock_state: Response {} not in expected ext interlock states'.format(response))
        return response

    def set_power(self, state):
        """
        Turns laser on and off. WARNING: if the laser is in "Continuous" mode, IT WILL BEGIN EMITTING.
        ENSURE PEOPLE AND EQUIPMENT IN THE ROOM ARE APPROPRIATELY PREPARED.
        ALSO ENSURE THAT THE WAVELENGTH SETTING IS APPROPRIATE FOR THE PROTECTIVE EYEWEAR.

        :param state: String specifying power state.  Should be "on" or "off".
        """
        cmd = None


def create_parser():
    parser = argparse.ArgumentParser(description='Changes the wavelength of the laser using rs232 interface.')
    parser.add_argument('wavelength', nargs=1, help='Sets the wavelength of the laser.', type=float)
    return parser


def main(wavelength, port):
    """
    This creates a command line and arguments for the script.

    :return: None
    """

    laser_interface = LaserSerialInterface(port=port)
    laser_interface.set_wavelength(wavelength)


if __name__ == '__main__':
    parser = create_parser()
    args = parser.parse_args()
    wavelength = np.float(args.wavelength[0])
    main(wavelength, port='/dev/ttyACM.LASER')
