import serial
import time
import serial.tools.list_ports

class CncMachine:
    """
    Represents the CNC machine. Communication is via USB serial port.
    """

    def __init__(self):
        """
        Initializes serial communication and sets the global
        variable cnc used by other methods.
        """
        # Get a list of available serial ports
        ports = serial.tools.list_ports.comports() 
#         print('port stuff ', len(ports))
        if len(ports) == 0:
            # machine likely on or USB cable not connected
            raise Exception('ERROR: CNC machine NOT turned on or NOT connected')
            
        else:
            # assume there is only one; extract name
            port, desc, hwid = ports[0]

            # connect to CNC
            self.cnc = serial.Serial(port, 115200)

            # wake up CNC controller
            self.send_cmd("\r\n")
            time.sleep(2)   # Wait for grbl to initialize
            self.cnc.reset_input_buffer()  # Flush startup text in serial input
    
    def configure(self, gcode_generator):
        """
        Configures the CNC machine for rendering.
        :raises Exception: when error occurs
        """       
        # go home
        self.send_cmd_check_status(gcode_generator.go_home, True)
        self.send_cmd_check_status(gcode_generator.execution_state_set, True)
        # set "touch" speed
        self.send_cmd_check_status(gcode_generator.z_feed_speed_set, True)
        # set the work origin at home position temporarily
        self.send_cmd_check_status(gcode_generator.work_origin_set, True)
        # move to the desired work origin
        self.send_cmd_check_status(gcode_generator.fix_work_origin_1, True)
        self.send_cmd_check_status(gcode_generator.fix_work_origin_2, True)   
        self.send_cmd_check_status(gcode_generator.fix_work_origin_3, True)   
            
        # send commands to establish the REAL workspace origin.
        self.send_cmd_check_status(gcode_generator.machine_pos_set, True) # set machine position
        self.send_cmd_check_status(gcode_generator.work_origin_set, True) # set work position
           
    def send_cmd(self, cmd):
        """
        Sends a command to the CNC machine
        :param cmd: the command to send; G-code or other
        :return: nothing
        """
        cmd = cmd + '\n'
        str_b = cmd.encode('utf-8')
        self.cnc.write(str_b)
        
    def send_cmd_get_status(self, cmd):
        """
        Sends a command to the CNC machine, and retrieves
        a single line response. The response is 'ok' or
        'error'.
        :param cmd: the command to send; G-code or other
        :return: response to command
        """
        self.send_cmd(cmd)
        resp = self.get_resp_line()
        return resp
        
    def send_cmd_check_status(self, cmd, gen_exception=False):
        """
        Sends a command to the CNC machine, and retrieves
        a single line response. The response is 'ok' or
        'error'. Returns True for 'ok' and False for 'error'
        when gen_except parameter is false; else returns True
        for 'ok' and throws an exception otherwise.
        :param cmd: the command to send; G-code or other
        :param gen_except: indicates whether to raise an exception on error
        :raises Exception: on error if requested
        :return: status of command execution: True or False
        """
        self.send_cmd(cmd)
        resp = self.get_resp_line()
        if resp == 'ok':
            return True
        else:
            print('For command: ', cmd)
            print("CNC error: ", resp)
            if not gen_exception:
                return False
            else:
                raise Exception('ERROR: CNC machine returned ', resp)
        
    def get_resp_line(self):
        """
        Gets one line of a potentially multi-line response.

        In general, most commands should produce a single line, more
        particularly a simple 'ok'. Informational commands produce
        multiple lines.
        :return: Response string
        """
        # wait for response on one line
        ret = self.cnc.read_until()
        
        # produce string from response
        ret_string = ret.decode('utf-8')
        ret_string = ret_string.strip()
                
        return ret_string
        
    def more_lines(self):
        """
        Checks if there are more lines in a response.
        :return: True if more lines; False otherwise.
        """
        # check buffer
        if self.cnc.in_waiting == 0:
            return False
        else:
            return True
    
    def soft_reset(self):
        """
        Sends a soft reset command to the machine.

        Intended for debug and development only.
        """
        self.cnc.write(bytes([24]))
        time.sleep(2)
        while True:
            ret = self.get_resp_line()
                    
            print(ret)
            
            if not self.more_lines():
                break

    def wait_for_idle(self):
        """
        Waits until the machine state is Idle, meaning
        the machine has reached a commanded location.

        Depends on the '?' command to report status. Note that
        get a status line then an 'ok' line
        """
        state = ''

        while not state == '<Idle':
            self.send_cmd('?')
            resp1 = self.get_resp_line() # status
            resp2 = self.get_resp_line() # ok
            
            resp1 = resp1.split("|", 1)
            state = resp1[0]

    def close(self):
        """
        Closes the CNC machine
        :return: none
        """
        self.cnc.close()
