class ModularClient(object):
    '''
    ModularClient contains an instance of serial_interface.SerialInterface
    and adds methods to it, like auto discovery of available modular
    devices in Linux, Windows, and Mac OS X. This class automatically
    creates methods from available functions reported by the modular
    device when it is running the appropriate firmware. This is the
    modular device client library for communicating with and calling
    remote methods on modular device servers.

    Example Usage:

    dev = ModularClient() # Might automatically find device if one available
    # if it is not found automatically, specify port directly
    dev = ModularClient(port='/dev/ttyACM0') # Linux specific port
    dev = ModularClient(port='/dev/tty.usbmodem262471') # Mac OS X specific port
    dev = ModularClient(port='COM3') # Windows specific port
    dev.get_device_id()
    dev.get_methods()

    '''
    _TIMEOUT = 0.05
    _WRITE_READ_DELAY = 0.001
    _WRITE_WRITE_DELAY = 0.005
    _RESET_DELAY = 2.0
    _METHOD_ID_GET_METHOD_IDS = 0

    def __init__(self,*args,**kwargs):
        name = None
        form_factor = None
        serial_number = None
        if 'debug' in kwargs:
            self.debug = kwargs['debug']
        else:
            kwargs.update({'debug': DEBUG})
            self.debug = DEBUG
        if 'try_ports' in kwargs:
            try_ports = kwargs.pop('try_ports')
        else:
            try_ports = None
        if 'baudrate' not in kwargs:
            kwargs.update({'baudrate': BAUDRATE})
        elif (kwargs['baudrate'] is None) or (str(kwargs['baudrate']).lower() == 'default'):
            kwargs.update({'baudrate': BAUDRATE})
        if 'timeout' not in kwargs:
            kwargs.update({'timeout': self._TIMEOUT})
        if 'write_read_delay' not in kwargs:
            kwargs.update({'write_read_delay': self._WRITE_READ_DELAY})
        if 'write_write_delay' not in kwargs:
            kwargs.update({'write_write_delay': self._WRITE_WRITE_DELAY})
        if 'name' in kwargs:
            name = kwargs.pop('name')
        if 'form_factor' in kwargs:
            form_factor = kwargs.pop('form_factor')
        if 'serial_number' in kwargs:
            serial_number = kwargs.pop('serial_number')
        if ('port' not in kwargs) or (kwargs['port'] is None):
            port =  find_modular_device_port(baudrate=kwargs['baudrate'],
                                             name=name,
                                             form_factor=form_factor,
                                             serial_number=serial_number,
                                             try_ports=try_ports,
                                             debug=kwargs['debug'])
            kwargs.update({'port': port})

        t_start = time.time()
        self._serial_interface = SerialInterface(*args,**kwargs)
        atexit.register(self._exit_modular_client)
        time.sleep(self._RESET_DELAY)
        self._method_dict = self._get_method_dict()
        try:
            self._method_dict_inv = dict([(v,k) for (k,v) in self._method_dict.items()])
        except AttributeError:
            self._method_dict_inv = dict([(v,k) for (k,v) in self._method_dict.iteritems()])
        self._create_methods()
        t_end = time.time()
        self._debug_print('Initialization time =', (t_end - t_start))

    def _debug_print(self, *args):
        if self.debug:
            print(*args)

    def _exit_modular_client(self):
        pass

    def _args_to_request(self,*args):
        request = json.dumps(args,separators=(',',':'))
        request += '\n';
        return request

    def _handle_response(self,response,request_id):
        if response is None:
            error_message = 'Did not receive server response.'
            raise IOError(error_message)
        try:
            response_dict = json_string_to_dict(response)
        except Exception as e:
            error_message = 'Unable to parse server response {0}.'.format(str(e))
            raise IOError(error_message)
        try:
            id  = response_dict.pop('id')
        except KeyError:
            error_message = 'Server response does not contain id member.'
            raise IOError(error_message)
        if not id == request_id:
            raise IOError('Response id does not match request id.')
        try:
            error = response_dict.pop('error')
            try:
                message = error.pop('message')
            except KeyError:
                message = ''
            try:
                data = error.pop('data')
            except KeyError:
                data = ''
            try:
                code = error.pop('code')
            except KeyError:
                code = ''
            error_message = '(from server) message: {0}, data: {1}, code: {2}'.format(message,data,code)
            raise IOError(error_message)
        except KeyError:
            pass
        try:
            result  = response_dict.pop('result')
        except KeyError:
            error_message = 'Server response does not contain result member.'
            raise IOError(error_message)
        return result

    def _send_request_get_result(self,*args):
        '''
        Sends request to server over serial port and
        returns response result
        '''
        request = self._args_to_request(*args)
        self._debug_print('request', request)
        response = self._serial_interface.write_read(request,use_readline=True,check_write_freq=True)
        self._debug_print('response', response)
        if (type(response) != str):
            response = response.decode('utf-8')
        self._debug_print('type(response)', type(response))
        result = self._handle_response(response,args[0])
        return result

    def _get_method_dict(self):
        method_dict = self._send_request_get_result(self._METHOD_ID_GET_METHOD_IDS)
        return method_dict

    def _send_request_by_method_name(self,name,*args):
        method_id = self._method_dict[name]
        method_args = [method_id]
        method_args.extend(args)
        result = self._send_request_get_result(*method_args)
        return result

    def _method_func_base(self,method_name,*args):
        if len(args) == 1 and type(args[0]) is dict:
            args_dict = args[0]
            args_list = self._args_dict_to_list(args_dict)
        else:
            args_list = args
        result = self._send_request_by_method_name(method_name,*args_list)
        return result

    def _create_methods(self):
        self._method_func_dict = {}
        for method_id, method_name in sorted(self._method_dict_inv.items()):
            method_func = functools.partial(self._method_func_base, method_name)
            setattr(self,inflection.underscore(method_name),method_func)
            self._method_func_dict[method_name] = method_func

    def _args_dict_to_list(self,args_dict):
        key_set = set(args_dict.keys())
        try:
            order_list = sorted([(num,name) for (name,num) in order_dict.items()])
        except AttributeError:
            order_list = sorted([(num,name) for (name,num) in order_dict.iteritems()])
        args_list = [args_dict[name] for (num, name) in order_list]
        return args_list

    def close(self):
        '''
        Close the device serial port.
        '''
        self._serial_interface.close()

    def get_port(self):
        return self._serial_interface.port

    def get_methods(self):
        '''
        Get a list of modular methods automatically attached as class methods.
        '''
        return [inflection.underscore(key) for key in list(self._method_dict.keys())]

    def call_get_result(self,method_name,*args):
        method_name = inflection.camelize(method_name,False)
        return self._send_request_get_result(method_name,*args)

    def call(self,method_name,*args):
        self.call_get_result(method_name,*args)

    def send_json_request(self,request):
        '''
        Sends json request to device over serial port and returns result
        '''
        request_python = json.loads(request)
        try:
            request_id = request_python["id"]
        except TypeError:
            pass
        except KeyError:
            error_message = 'Request does not contain an id.'
            raise IOError(error_message)
        try:
            request_python["method"] = inflection.camelize(request_python["method"],False)
        except TypeError:
            pass
        except KeyError:
            error_message = 'Request does not contain a method.'
            raise IOError(error_message)
        try:
            request_python[0] = inflection.camelize(request_python[0],False)
            request_id = request_python[0]
        except IndexError:
            error_message = 'Request does not contain a method.'
            raise IOError(error_message)
        request = json.dumps(request_python,separators=(',',':'))
        request += '\n'
        self._debug_print('request', request)
        response = self._serial_interface.write_read(request,use_readline=True,check_write_freq=True)
        self._debug_print('response', response)
        result = self._handle_response(response,request_id)
        return result

    def convert_to_json(self,python_to_convert,response_indent=None):
        '''
        Convert python object to json string.
        '''
        converted_json = json.dumps(python_to_convert,separators=(',',':'),indent=response_indent)
        return converted_json

    def save_device_id(self,output_directory):
        '''
        Save device_id as a json file.
        '''
        if output_directory is None:
            output_directory = os.path.join(os.path.curdir)
        elif len(os.path.splitext(output_directory)[1]) > 0:
            output_directory = os.path.dirname(output_directory)
        if not os.path.exists(output_directory):
            os.makedirs(output_directory)
        result = self.call_get_result('getDeviceId')
        output = {}
        output['id'] = 'getDeviceId'
        output['result'] = result
        output_path = os.path.join(output_directory,'device_id.json')
        with open(output_path,'w') as output_file:
            json.dump(output,output_file,separators=(',',':'),indent=2)
        try:
            os.removedirs(output_directory)
        except OSError:
            pass

    def save_device_info(self,output_directory):
        '''
        Save device_info as a json file.
        '''
        if output_directory is None:
            output_directory = os.path.join(os.path.curdir)
        elif len(os.path.splitext(output_directory)[1]) > 0:
            output_directory = os.path.dirname(output_directory)
        if not os.path.exists(output_directory):
            os.makedirs(output_directory)
        result = self.call_get_result('getDeviceInfo')
        output = {}
        output['id'] = 'getDeviceInfo'
        output['result'] = result
        output_path = os.path.join(output_directory,'device_info.json')
        with open(output_path,'w') as output_file:
            json.dump(output,output_file,separators=(',',':'),indent=2)
        try:
            os.removedirs(output_directory)
        except OSError:
            pass

    def save_api(self,output_directory,verbosity='DETAILED',firmware='ALL'):
        '''
        Save api as a set of json files.
        '''
        if output_directory is None:
            output_directory = os.path.join(os.path.curdir,'api')
        elif len(os.path.splitext(output_directory)[1]) > 0:
            output_directory = os.path.dirname(output_directory)
        if not os.path.exists(output_directory):
            os.makedirs(output_directory)
        device_info = self.call_get_result('getDeviceInfo')
        for firmware_info in device_info['firmware']:
            if (firmware == 'ALL') or (firmware == firmware_info['name']):
                result = self.call_get_result('getApi',verbosity,[firmware_info['name']])
                api = {}
                api['id'] = 'getApi'
                api['result'] = result
                output_path = os.path.join(output_directory,firmware_info['name'] + '.json')
                with open(output_path,'w') as api_file:
                    json.dump(api,api_file,separators=(',',':'),indent=2)
        try:
            os.removedirs(output_directory)
        except OSError:
            pass
class MettlerToledoDevice(object):
    '''
    This module (mettler_toledo_quantos) creates a class named MettlerToledoDevice, 
    to interface to Mettler Toledo Quantos using the Mettler Toledo
    Standard Interface Command Set for Quantos system (MT-SICS-Quantos).
    '''
    _TIMEOUT = 0.05
    _WRITE_WRITE_DELAY = 0.05
    _RESET_DELAY = 2.0

    def __init__(self,*args,**kwargs):
        if 'debug' in kwargs:
            self.debug = kwargs['debug']
        else:
            kwargs.update({'debug': DEBUG})
            self.debug = DEBUG
        if 'baudrate' not in kwargs:
            kwargs.update({'baudrate': BAUDRATE})
        elif (kwargs['baudrate'] is None) or (str(kwargs['baudrate']).lower() == 'default'):
            kwargs.update({'baudrate': BAUDRATE})
        if 'timeout' not in kwargs:
            kwargs.update({'timeout': self._TIMEOUT})
        if 'write_write_delay' not in kwargs:
            kwargs.update({'write_write_delay': self._WRITE_WRITE_DELAY})
        if ('port' not in kwargs) or (kwargs['port'] is None):
            err_string = 'Specify port.\n'
            raise RuntimeError(err_string)
        else:
            self.port = kwargs['port']

        t_start = time.time()
        self._serial_device = SerialInterface(*args,**kwargs)
        atexit.register(self._exit_mettler_toledo_device)
        time.sleep(self._RESET_DELAY)
        t_end = time.time()
        self._debug_print('Initialization time =', (t_end - t_start))


    def _debug_print(self, *args):
        if self.debug:
            print(*args)

    def _exit_mettler_toledo_device(self):
        pass

    def close(self):
        '''
        Close the device serial port.
        '''
        self._serial_device.close()

    def _args_to_request(self,*args):
        request = ''.join(map(str,args))
        request = request + '\r\n';MettlerToledoError
        return request

    def _send_request_get_response(self,*args):

        '''Sends request to device over serial port and
        returns response'''

        request = self._args_to_request(*args)
        self._debug_print('request', request)
        response = self._serial_device.write_read(request,use_readline=True,check_write_freq=True)
        response = response.decode().replace('"','')
        response_list = response.split()
        if 'L' in response_list:
            raise MettlerToledoError('Syntax Error!')
        return response_list

    def move_frontdoor_open(self):
        '''
        Opens the Quantos front door.
        '''
        response = self._send_request_get_response('QRA 60 7 3')
        if 'I' in response[3]:
            if '1' in response[4]:
                raise MettlerToledoError('Not mounted.')
            elif '2'in response[4]:
                self._debug_print('Another job is running -- retrying.')
                self.move_frontdoor_open()
                #raise MettlerToledoError('Another job is running.')
            elif '3'in response[4]:
                raise MettlerToledoError('Timeout.')
            elif '4'in response[4]:
                raise MettlerToledoError('Not selected.')
            elif '5'in response[4]:
                raise MettlerToledoError('Not allowed at the moment')
            else:
                raise MettlerToledoError('Stopped by external action.')
        return response

    def move_frontdoor_close(self):
        '''
        Close the Quantos front door.
        '''
        #response = self._send_request_get_response('QRA 60 2 4')
        response = self._send_request_get_response('QRA 60 7 2')
        if 'I' in response[3]:
            if '1' in response[4]:
                raise MettlerToledoError('Not mounted.')
            elif '2'in response[4]:
                self._debug_print('Another job is running -- retrying.')
                self.move_frontdoor_close()
                #raise MettlerToledoError('Another job is running.')
            elif '3'in response[4]:
                raise MettlerToledoError('Timeout.')
            elif '4'in response[4]:
                raise MettlerToledoError('Not selected.')
            elif '5'in response[4]:
                raise MettlerToledoError('Not allowed at the moment.')
            else:
                raise MettlerToledoError('Stopped by external action.')
        return response

    def move_to(self,position):
        '''
        Move Quantos sampler, taking the position as an argument.
        '''
        response = self._send_request_get_response('QRA 60 8 ' + str(position))
        return response

    def unlock_dosing_pin(self):
        '''
        Unlock dosing pin, allowing removal of dosing head.
        '''
        response = self._send_request_get_response('QRA 60 2 3')
        return response

    def lock_dosing_pin(self):
        '''
        Lock dosing pin, readying dosing head for dispensing.
        '''
        response = self._send_request_get_response('QRA 60 2 4')
        return response

    def set_target_value_mg(self, value):
        '''
        Sets the target dosing value in mg.
        Value must be between 0.1 and 250000. Simple error handling incorporated.
        '''
        if int(value) < 0.10:
            self._debug_print('The target value must be greater than 0.1 mg. -'
                              'Change the value and try again.')
        elif int(value) > 250000:
            self._debug_print('The target value must be less than 250,000 mg. -'
                              'Change the value and try again.')
        else:
            response = self._send_request_get_response('QRD 1 1 5 ' + str(value))
            return response

    def set_tolerance_value_pct(self, value):
        '''
        Sets the tolerance value as a percentage.
        Value must be between 0.1 and 40. Simple error handling incorporated.
        '''
        if int(value) < 0.10:
            self._debug_print('The tolerance must be greater than 0.1% -'
                              'Change the value and try again.')
        elif int(value) > 40:
            self._debug_print('The tolerance must be less than 40% -'
                              'Change the value and try again.')
        else:
            response = self._send_request_get_response('QRD 1 1 6 ' + str(value))
            return response

    def start_dosing(self):
        '''
        Starts dosing. Uses previously set parameters including:
        target, tolerance and powder dosing algorithm
        '''
        response = self._send_request_get_response('QRA 61 1')
        # Error handling
        if 'I' in response[3]:
            if '1' in response[4]:
                raise MettlerToledoError('Not mounted.')
            elif '2' in response[4]:
                self._debug_print('Another job is running -- retrying.')
                self.start_dosing()
            elif '3' in response[4]:
                raise MettlerToledoError('Timeout.')
            elif '4' in response[4]:
                raise MettlerToledoError('Not selected.')
            elif '5' in response[4]:
                raise MettlerToledoError('Not allowed at the moment.')
            elif '6' in response[4]:
                self._debug_print('Weight not stable - trying again in 5 seconds.')
                time.sleep(5)
                self.start_dosing()
            elif '7' in response[4]:
                raise MettlerToledoError('Powderflow error.')
            elif '8' in response[4]:
                raise MettlerToledoError('Stopped by external action.')
            elif '9' in response[4]:
                raise MettlerToledoError('Safe position error.')
            elif '10' in response[4]:
                raise MettlerToledoError('Head not allowed.')
            elif '11' in response[4]:
                raise MettlerToledoError('Head limit reached.')
            elif '12' in response[4]:
                raise MettlerToledoError('Head expiry date reached.')
            elif '13' in response[4]:
                raise MettlerToledoError('Sampler blocked.')
        return response

    def request_frontdoor_position(self):
        '''
        Requests the position of the front door.
        '''
        response = self._send_request_get_response('QRD 2 3 7')

        # return position as text
        if '2' in response[4]:
            return self._debug_print('Door is closed.')
        if '3' in response[4]:
            return self._debug_print('Door is opened.')
        if '8' in response[4]:
            return self._debug_print('Door is not detectable.')
        if '9' in response[4]:
            return self._debug_print('Running.')
        # Error handling
        if 'I' in response[4]:
            if '1' in response[5]:
                raise MettlerToledoError('Not mounted.')
            elif '2' in response[5]:
                self._debug_print('Another job is running - waiting 5 seconds and trying again.')
                time.sleep(5)
                self.request_frontdoor_position()
            elif '3' in response[5]:
                raise MettlerToledoError('Timeout.')
            elif '4' in response[5]:
                raise MettlerToledoError('Not selected.')
            elif '5' in response[5]:
                raise MettlerToledoError('Not allowed at the moment.')
            elif '8' in response[5]:
                raise MettlerToledoError('Stopped by external action.')
        return response

    def request_autosampler_position(self):
        '''
        Requests the position of the autosampler.

        !!! This is currently not working - throwing up 'Syntax error',
        command is understood but not executable. !!!
        '''
        response = self._send_request_get_response('QRD 2 3 8')

        # Return position as text
        if 'I' in response[4]:
            if '1' in response[5]:
                raise MettlerToledoError('Not mounted.')
            elif '2' in response[5]:
                self._debug_print('Another job is running - waiting 5 seconds and trying again.')
                time.sleep(5)
                self.request_frontdoor_position()
            elif '3' in response[5]:
                raise MettlerToledoError('Timeout.')
            elif '4' in response[5]:
                raise MettlerToledoError('Not selected.')
            elif '5' in response[5]:
                raise MettlerToledoError('Not allowed at the moment.')
            elif '8' in response[5]:
                raise MettlerToledoError('Stopped by external action.')
        else:
            return response


    def quantos_test(self):
        '''
        Close the Quantos front door.
        '''
        response = self._send_request_get_response('QRD 1 1 2 1')
        #response = self._send_request_get_response('QRA 60 2 3')
        '''
        if 'I' in response[3]:
            if '1' in response[4]:
                raise MettlerToledoError('Not mounted.')
            elif '2'in response[4]:
                self._debug_print('Another job is running -- retrying.')
                self.move_frontdoor_close()
                #raise MettlerToledoError('Another job is running.')
            elif '3'in response[4]:
                raise MettlerToledoError('Timeout.')
            elif '4'in response[4]:
                raise MettlerToledoError('Not selected.')
            elif '5'in response[4]:
                raise MettlerToledoError('Not allowed at the moment.')
            else:
                raise MettlerToledoError('Stopped by external action.')
        '''
        return response