from PyQt6.QtWidgets import *
from PyQt6.QtCore import *
from PyQt6.QtGui import *
import serial
import serial.tools.list_ports
import re


class Window(QMainWindow):
    def __init__(self):
        super().__init__()

        # Create a serial object for serial communication
        self.ser = serial.Serial()
        self.ser.baudrate = 115200

        ################################################################
        # GUI
        ################################################################

        # Set the window title and icon
        self.setWindowTitle('SCARA GUI')
        self.setWindowIcon(QIcon('images/python.png'))

        # Create GUI items
        self.QComboBox_port = QComboBox()
        self.QComboBox_port.addItems(['No Ports'])

        self.QLabel_left_top = QLabel('Settings')
        self.QLabel_left_bottom = QLabel('System Status')
        self.QLabel_right_top = QLabel('Jog Controller')
        self.QLabel_right_bottom = QLabel('Console Controller')
        self.QLabel_baud = QLabel('Baud')
        self.QLabel_command = QLabel('Command: ')
        self.QLabel_console = QLabel('Console')
        self.QLabel_port = QLabel('Port')
        self.QLabel_stepSize = QLabel('XYZ\nStep\nSize:')
        self.QLabel_x = QLabel('X:')
        self.QLabel_y = QLabel('Y:')
        self.QLabel_z = QLabel('Z:')

        self.QLineEdit_baud = QLineEdit('115200')
        self.QLineEdit_baud.setReadOnly(True)
        self.QLineEdit_command = QLineEdit()
        self.QLineEdit_stepSize = QLineEdit('5')
        self.QLineEdit_x = QLineEdit('0')
        self.QLineEdit_x.setReadOnly(True)
        self.QLineEdit_y = QLineEdit('0')
        self.QLineEdit_y.setReadOnly(True)
        self.QLineEdit_z = QLineEdit('0')
        self.QLineEdit_z.setReadOnly(True)

        self.QPushButton_jog_xpos = QPushButton('X')
        self.QPushButton_jog_xneg = QPushButton('X-')
        self.QPushButton_jog_ypos = QPushButton('Y')
        self.QPushButton_jog_yneg = QPushButton('Y-')
        self.QPushButton_jog_zpos = QPushButton('Z')
        self.QPushButton_jog_zneg = QPushButton('Z-')
        self.QPushButton_connect = QPushButton('Connect')
        self.QPushButton_execute = QPushButton('Execute')
        self.QPushButton_refreshPorts = QPushButton('Refresh Ports')
        self.QPushButton_units = QPushButton('mm')

        self.QTextEdit_status = QTextEdit('Disconnected')
        self.QTextEdit_status.setReadOnly(True)
        self.QTextEdit_console = QTextEdit()
        self.QTextEdit_console.setReadOnly(True)

        # Create widgets
        self.QWidget_main = QWidget()
        self.QWidget_left_top = QWidget()
        self.QWidget_left_bottom = QWidget()
        self.QWidget_right_top = QWidget()
        self.QWidget_right_bottom = QWidget()

        # Create layouts
        self.QLayout_main = QGridLayout()
        self.QLayout_left_top = QVBoxLayout()
        self.QLayout_left_bottom = QGridLayout()
        self.QLayout_right_top = QGridLayout()
        self.QLayout_right_bottom = QVBoxLayout()

        # Add items to layouts
        self.QLayout_main.addWidget(self.QLabel_left_top, 0, 0)
        self.QLayout_main.addWidget(self.QWidget_left_top, 1, 0)
        self.QLayout_main.addWidget(self.QLabel_left_bottom, 2, 0)
        self.QLayout_main.addWidget(self.QWidget_left_bottom, 3, 0)
        self.QLayout_main.addWidget(self.QLabel_right_top, 0, 1)
        self.QLayout_main.addWidget(self.QWidget_right_top, 1, 1)
        self.QLayout_main.addWidget(self.QLabel_right_bottom, 2, 1)
        self.QLayout_main.addWidget(self.QWidget_right_bottom, 3, 1)

        self.QLayout_left_top.addWidget(self.QLabel_port)
        self.QLayout_left_top.addWidget(self.QPushButton_refreshPorts)
        self.QLayout_left_top.addWidget(self.QComboBox_port)
        self.QLayout_left_top.addWidget(self.QPushButton_connect)
        self.QLayout_left_top.addWidget(self.QLabel_baud)
        self.QLayout_left_top.addWidget(self.QLineEdit_baud)

        self.QLayout_left_bottom.addWidget(self.QTextEdit_status, 0, 0, 1, 2)
        self.QLayout_left_bottom.addWidget(self.QLabel_x, 1, 0)
        self.QLayout_left_bottom.addWidget(self.QLineEdit_x, 1, 1)
        self.QLayout_left_bottom.addWidget(self.QLabel_y, 2, 0)
        self.QLayout_left_bottom.addWidget(self.QLineEdit_y, 2, 1)
        self.QLayout_left_bottom.addWidget(self.QLabel_z, 3, 0)
        self.QLayout_left_bottom.addWidget(self.QLineEdit_z, 3, 1)

        self.QLayout_right_top.addWidget(self.QPushButton_jog_xpos, 0, 2)
        self.QLayout_right_top.addWidget(self.QPushButton_jog_xneg, 0, 0)
        self.QLayout_right_top.addWidget(self.QPushButton_jog_ypos, 0, 1)
        self.QLayout_right_top.addWidget(self.QPushButton_jog_yneg, 1, 1)
        self.QLayout_right_top.addWidget(self.QPushButton_jog_zpos, 1, 0)
        self.QLayout_right_top.addWidget(self.QPushButton_jog_zneg, 1, 2)
        self.QLayout_right_top.addWidget(self.QPushButton_units, 2, 0)
        self.QLayout_right_top.addWidget(self.QLabel_stepSize, 2, 1)
        self.QLayout_right_top.addWidget(self.QLineEdit_stepSize, 2, 2)

        self.QLayout_right_bottom.addWidget(self.QLabel_console)
        self.QLayout_right_bottom.addWidget(self.QTextEdit_console)
        self.QLayout_right_bottom.addWidget(self.QLabel_command)
        self.QLayout_right_bottom.addWidget(self.QLineEdit_command)
        self.QLayout_right_bottom.addWidget(self.QPushButton_execute)

        # Set layouts to widgets
        self.QWidget_main.setLayout(self.QLayout_main)
        self.QWidget_left_top.setLayout(self.QLayout_left_top)
        self.QWidget_left_bottom.setLayout(self.QLayout_left_bottom)
        self.QWidget_right_top.setLayout(self.QLayout_right_top)
        self.QWidget_right_bottom.setLayout(self.QLayout_right_bottom)

        # Set central widget
        self.setCentralWidget(self.QWidget_main)

        ################################################################
        # STYLING
        ################################################################

        size_1 = 32
        size_2 = 64

        self.QComboBox_port.setFixedHeight(size_1)
        self.QComboBox_port.setCursor(Qt.CursorShape.PointingHandCursor)

        self.QLabel_stepSize.setFixedSize(size_2, size_2)

        self.QLineEdit_baud.setFixedHeight(size_1)
        self.QLineEdit_command.setFixedHeight(size_2)
        self.QLineEdit_stepSize.setFixedSize(size_2, size_2)
        self.QLineEdit_x.setFixedHeight(size_2)
        self.QLineEdit_y.setFixedHeight(size_2)
        self.QLineEdit_z.setFixedHeight(size_2)
        self.QLineEdit_command.setStyleSheet('QLineEdit:hover{ background-color: "#fff1c1";}')
        self.QLineEdit_stepSize.setStyleSheet('QLineEdit:hover{ background-color: "#fff1c1";}')
        self.QLineEdit_stepSize.setAlignment(Qt.AlignmentFlag.AlignHCenter)

        self.QPushButton_jog_xpos.setFixedSize(size_2, size_2)
        self.QPushButton_jog_xneg.setFixedSize(size_2, size_2)
        self.QPushButton_jog_ypos.setFixedSize(size_2, size_2)
        self.QPushButton_jog_yneg.setFixedSize(size_2, size_2)
        self.QPushButton_jog_zpos.setFixedSize(size_2, size_2)
        self.QPushButton_jog_zneg.setFixedSize(size_2, size_2)
        self.QPushButton_connect.setFixedHeight(size_1)
        self.QPushButton_execute.setFixedHeight(size_1)
        self.QPushButton_refreshPorts.setFixedHeight(size_1)
        self.QPushButton_units.setFixedSize(size_2, size_2)
        self.QPushButton_jog_xpos.setCursor(Qt.CursorShape.PointingHandCursor)
        self.QPushButton_jog_xneg.setCursor(Qt.CursorShape.PointingHandCursor)
        self.QPushButton_jog_ypos.setCursor(Qt.CursorShape.PointingHandCursor)
        self.QPushButton_jog_yneg.setCursor(Qt.CursorShape.PointingHandCursor)
        self.QPushButton_jog_zpos.setCursor(Qt.CursorShape.PointingHandCursor)
        self.QPushButton_jog_zneg.setCursor(Qt.CursorShape.PointingHandCursor)
        self.QPushButton_connect.setCursor(Qt.CursorShape.PointingHandCursor)
        self.QPushButton_execute.setCursor(Qt.CursorShape.PointingHandCursor)
        self.QPushButton_refreshPorts.setCursor(Qt.CursorShape.PointingHandCursor)
        self.QPushButton_units.setCursor(Qt.CursorShape.PointingHandCursor)

        self.QTextEdit_console.setMinimumHeight(size_2 * 4)
        self.QTextEdit_status.setFixedHeight(size_2)

        ################################################################
        # FUNCTIONS
        ################################################################

        # Connect buttons to functions
        self.QLineEdit_command.returnPressed.connect(lambda: self.execute())

        self.QPushButton_jog_xpos.clicked.connect(lambda: self.jog_xyz(self.QPushButton_jog_xpos.text()))
        self.QPushButton_jog_xneg.clicked.connect(lambda: self.jog_xyz(self.QPushButton_jog_xneg.text()))
        self.QPushButton_jog_ypos.clicked.connect(lambda: self.jog_xyz(self.QPushButton_jog_ypos.text()))
        self.QPushButton_jog_yneg.clicked.connect(lambda: self.jog_xyz(self.QPushButton_jog_yneg.text()))
        self.QPushButton_jog_zpos.clicked.connect(lambda: self.jog_xyz(self.QPushButton_jog_zpos.text()))
        self.QPushButton_jog_zneg.clicked.connect(lambda: self.jog_xyz(self.QPushButton_jog_zneg.text()))
        self.QPushButton_connect.clicked.connect(lambda: self.connect())
        self.QPushButton_execute.clicked.connect(lambda: self.execute())
        self.QPushButton_refreshPorts.clicked.connect(lambda: self.refresh_ports())
        self.QPushButton_units.clicked.connect(lambda: self.units())

    # Refreshes list of ports
    def refresh_ports(self):
        # Get ports
        ports = serial.tools.list_ports.comports()

        self.QTextEdit_status.clear()
        self.QTextEdit_status.insertPlainText('Refreshing ports...')

        # Update port drop down list in GUI
        self.QComboBox_port.clear()
        if not ports:
            self.QComboBox_port.addItem("No Ports")
        else:
            for port_name in ports:
                self.QComboBox_port.addItem(str(port_name))

        # Pause to let Arduino settle
        self.sleep2sec()

        if not ports:
            self.QTextEdit_status.clear()
            self.QTextEdit_status.insertPlainText('Found no ports')
        else:
            self.QTextEdit_status.clear()
            self.QTextEdit_status.insertPlainText(f'Found {len(ports)} ports')

    # Connect to port selected by user
    def connect(self):
        # Get port selected by user
        selected_port_name = self.QComboBox_port.currentText()

        if selected_port_name == 'No Ports':
            self.QTextEdit_status.clear()
            self.QTextEdit_status.insertPlainText('No ports connected')
            return

        # Refresh ports
        self.refresh_ports()
        selected_port_index = self.QComboBox_port.findText(selected_port_name)

        # Check if selected port is still available
        if selected_port_index == -1:
            self.QTextEdit_status.clear()
            self.QTextEdit_status.insertPlainText(f'{selected_port_name} not connected')
            return
        else:
            self.QComboBox_port.setCurrentIndex(selected_port_index)
            self.QTextEdit_status.clear()
            self.QTextEdit_status.insertPlainText(f'Connecting to port {selected_port_name}')

        # Connect to Arduino
        ports = serial.tools.list_ports.comports()
        for p in ports:
            if 'CH340' in p.description:
                self.ser.close()
                self.ser.port = str(p.device)
                self.ser.open()

        self.QTextEdit_status.clear()
        self.QTextEdit_status.insertPlainText(f'Connecting to port {selected_port_name}...')

        # Pause to let Arduino settle
        self.sleep2sec()

        self.QTextEdit_status.clear()
        self.QTextEdit_status.insertPlainText(f'Connected to port {selected_port_name}')

    # Controls the SCARA based on console command line
    def execute(self):
        command = self.QLineEdit_command.text()
        self.QLineEdit_command.clear()
        self.send_command(command)

    # Moves the arm along XYZ axis
    def jog_xyz(self, direction):
        step_size = self.QLineEdit_stepSize.text()
        self.send_command('G91')
        self.send_command(f'G0 {direction}{step_size}')

        # if direction == 'X':
        #     current_value = self.QLineEdit_x.toPlainText()
        #     new_value = int(current_value) + {step_size}
        #     self.QLineEdit_x.setText(f'{str(new_value)}')

    # Toggles the units of measurement between millimeters (mm) or inches (in)
    def units(self):
        # Change to mm
        if self.QPushButton_units.text() != 'mm':
            self.QPushButton_units.setText('mm')
            self.send_command('G21')

        # Change to in
        else:
            self.QPushButton_units.setText('in')
            self.send_command('G20')

    # Sends G-Code commands to an Arduino
    def send_command(self, command):
        # Check if Arduino is connected
        try:
            self.ser.inWaiting()
        except:
            self.QTextEdit_status.clear()

            self.QTextEdit_status.insertPlainText('No ports connected\n')
            return

        # Disable GUI
        self.QWidget_main.setEnabled(False)
        self.repaint()

        # Check if command is empty
        if command == '':
            self.QTextEdit_status.clear()
            self.QTextEdit_status.insertPlainText('Command is empty\n')
        else:
            # Check if command matches valid command patterns
            pattern_1 = r"^(G20|G21|G90|G91|M2|M6|M72)$"
            pattern_2 = r"^(G0|G1) (X|Y|Z)(-?)(\d+)((.\d+)?)$"
            match = re.match(pattern_1, command) or re.match(pattern_2, command)
            if not match:
                self.QTextEdit_status.clear()
                self.QTextEdit_status.insertPlainText('Invalid command\n')
            else:
                # If here, must be able to send command
                self.QTextEdit_console.insertPlainText(f'{command}\n')
                self.repaint()
                self.ser.write(command.encode('utf-8'))

                # Wait for Arduino response
                while True:
                    response = self.ser.readline()
                    self.QTextEdit_status.clear()
                    self.QTextEdit_status.insertPlainText('Waiting...')
                    self.repaint()
                    if len(response) > 0:
                        self.QTextEdit_status.clear()
                        self.QTextEdit_status.insertPlainText(f'{response}')
                        break

        # Re-enable GUI
        self.QWidget_main.setDisabled(False)

    def sleep2sec(self):
        self.QWidget_main.setEnabled(False)
        QTimer.singleShot(2000, lambda: self.QWidget_main.setDisabled(False))


# Create an application instance and run the event loop
app = QApplication([])
with open('styles.css', 'r') as file:
    app.setStyleSheet(file.read())
window = Window()
window.show()
app.exec()
