import serial

MOTOR_UNIPOLAR = 0
MOTOR_BIPOLAR = 1
MOTOR_DC = 2
MOTOR_STEPDIR = 3
MOTOR_TYPES = [MOTOR_UNIPOLAR, MOTOR_BIPOLAR, MOTOR_DC, MOTOR_STEPDIR]
WIRING_LUNATICO_NORMAL = 0
WIRING_LUNATICO_REVERSED = 1
WIRING_RFMOONLITE_NORMAL = 2
WIRING_RFMOONLITE_REVERSED = 3
WIRINGS = [WIRING_LUNATICO_NORMAL, WIRING_LUNATICO_REVERSED, WIRING_RFMOONLITE_NORMAL, WIRING_RFMOONLITE_REVERSED]
INT_TEMP_SENSOR = 0
EXT_TEMP_SENSOR = 1
TEMP_SENSORS = [INT_TEMP_SENSOR, EXT_TEMP_SENSOR]
PORT_MAIN = 0
PORT_EXP = 1
PORT_TOTAL = 2
PORTS = [PORT_MAIN, PORT_EXP, PORT_TOTAL]

def generic_parser(prefix='', type=int):
    def f(answer, axis={}):
        if not answer.startswith(prefix.format(**axis).encode()):
            raise ValueError(f'Answer not understood: {answer} should start with {prefix}')
        if not answer.endswith(b'\r\n'):
            raise ValueError(f'Answer incomplete: {answer}')
        return type(answer[len(prefix):-2])
    return f

def command_factory(self, command, axis={}):
    form, doc, variables, answer = _ascii_commands[command]
    def f(*args):
        if len(args) != len(variables):
            raise ValueError(f'Command expect {len(variables)} positionnal arguments: {variables}')
        keys=dict(zip(variables, args))
        for v in variables:
            s = variables[v] 
            if s is None:
                pass
            if isinstance(s, set):
                if not keys[v] in variables[v]:
                    raise ValueError(f'{v} sould be in {s}')
            else:
                if keys[v] > s[1] or keys[v] < s[0]:
                    raise ValueError(f'{v} should lie in range {s}')
        keys.update(axis)
        #print((form.format(**keys)+'\r').encode())
        self.serial.write((form.format(**keys)+'\r').encode())
        if answer is not None:
            try:
                size = answer.size()
                ans=self.serial.read(size)
                return answer(ans, axis=axis)
            except AttributeError:
                ans = self.serial.readline()
                return answer(ans, axis=axis)
    f.__doc__ = doc.format(**axis)
    return f

temp_parser = generic_parser

_ascii_commands = {'get_version': ['!seletek version#', 'Return version number', {}, generic_parser()],
                   'set_pos': ["!step setpos {port} {pos}#", 'Define the step number corresponding to current position', {'port':PORTS, 'pos':None}, generic_parser],
                   'get_pos': ["!step getpos {port}#", 'Return current position in steps', {'port': PORTS}, generic_parser],
                   'get_temp': ["!read temps {sensor}#", 'Read internal temperature sensor of the focuser controler', {'sensor': TEMP_SENSORS}, temp_parser],
                   'set_speed': ["!step speedrangeus {port} {speed} {speed}#", 'Set stepping speed', {'port': PORTS, 'speed': [50, 500000]}, generic_parser],
                   'set_wiring': ["!step wiremode {port} {wiring}#", 'Define motor wiring', {'port': PORTS, 'wiring': WIRINGS}, generic_parser], 
                   'set_stepmode': ["!step halfstep {port} {stepmode}#", 'Set step mode (see )', {'port': PORTS, 'stepmode': [0, 1]}, generic_parser], 
                   'set_motor_model': ["!step model {port} {model}#",  'Define Motor model', {'port': PORTS, 'model': MOTOR_TYPES}, generic_parser], 
                   'goto': ["!step goto {port} {target} {backlash}#", 'Move focuser with backlash correction', {'port': PORTS, 'target': None, 'backlash': None}, generic_parser],
                   'gotor': ["!step gopr {port} {target}#", 'Move focuser without backlash correction', {'port': PORTS, 'target': None}, generic_parser],
                   'stop': ["!step stop {port}#", 'stop movement', {'port':PORTS}, generic_parser],
}

class Seletek(object):
    def __init__(self, port='/dev/ttyUSB0'):
        #self.ser = serial.Serial('/dev/ttyUSB0', baudrate=115200)
        self._register_commands()

    def _register_commands(self):
        for command in _ascii_commands:
            setattr(self, command, command_factory(self, command))

    
if __name__ == '__main__':
    s = Seletek()
