import tkinter as tk
from tkinter import ttk
import serial.tools.list_ports
import time
import threading


ignition_delay = [0.0]*30
ser_msg = ''


def refresh_ports():
    ports = [str(i).split(' ')[0] for i in serial.tools.list_ports.comports()]
    choose_port['values'] = ports


def connect():
    if ser.is_open:
        ser.close()
        connect_button.configure(text='Connect')
        main_window.output_label.configure(foreground='red', text='Not connected')
        disable_buttons()
    else:
        ser.port = main_window.selected_port.get()

        try:
            ser.open()
        except serial.serialutil.SerialException:
            main_window.output_label['text'] = 'Failed!'
        else:
            connect_button.configure(text='Disconnect')
            main_window.output_label.configure(foreground='green', text='Connected')
            enable_buttons()
            SerialThread().start()


def on_closing():
    ser.close()
    main_window.destroy()


class SerialThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)

    def run(self):
        global ser_msg
        while True:
            try:
                if ser.inWaiting():
                    ser_msg = ser.readline().decode("utf-8")
                    # print(ser_msg)  # just a debug feature
            except OSError:
                connect_button.configure(text='Connect')
                main_window.output_label.configure(foreground='red', text='Not connected')
                disable_buttons()
                break
            except TypeError:
                break
            except UnicodeDecodeError:
                pass
            time.sleep(0.1)


def disable_buttons():
    sequence_button['state'] = 'disable'
    alias_button['state'] = 'disable'
    status_button['state'] = 'disable'


def enable_buttons():
    sequence_button['state'] = 'normal'
    alias_button['state'] = 'normal'
    status_button['state'] = 'normal'


class Window(tk.Toplevel):
    def __init__(self, style):
        super().__init__()
        self.resizable(True, True)
        self.configure(bg='#d9d9d9')

        self.style = style
        self.index_label = []
        iteration = 0

        # select window for setting sequences or setting aliases
        if style == 'sequences':
            self.geometry("1020x340+100+100")
            self.minsize(1020, 340)
            self.entry_min = []
            self.entry_sec = []
            self.entry_millis = []

            for col in range(0, 5):
                for row in range(0, 6):
                    self.index_label.append(ttk.Label(self, font='bold', text=('  ' + str(iteration + 1) + ':')))
                    self.index_label[iteration].grid(column=(col*7), row=row, sticky=tk.E, **paddings)

                    self.entry_min.append(ttk.Entry(self, width=2, ))
                    self.entry_min[iteration].grid(column=(col*7 + 1), row=row)

                    ttk.Label(self, text=' m  ').grid(column=(col*7 + 2), row=row)

                    self.entry_sec.append(ttk.Entry(self, width=2))
                    self.entry_sec[iteration].grid(column=(col*7 + 3), row=row)

                    ttk.Label(self, text=' s  ').grid(column=(col*7 + 4), row=row)

                    self.entry_millis.append(ttk.Entry(self, width=3))
                    self.entry_millis[iteration].grid(column=(col*7 + 5), row=row)

                    ttk.Label(self, text=' ms').grid(column=(col*7 + 6), row=row)

                    iteration += 1

        elif style == 'aliases':
            self.geometry("445x340+100+100")
            self.minsize(445, 340)
            window_entry = []
            self.alias = []

            for col in range(0, 5):
                for row in range(0, 6):
                    self.index_label.append(ttk.Label(self, font='bold', text=('  ' + str(iteration + 1) + ':')))
                    self.index_label[iteration].grid(column=(col*2), row=row, sticky=tk.E, **paddings)

                    self.alias.append(tk.StringVar(self))
                    window_entry.append(ttk.Entry(self, width=3, textvariable=self.alias[iteration]))
                    window_entry[iteration].grid(column=(col*2 + 1), row=row)

                    iteration += 1

        self.send_label = ttk.Label(self, text='Click button to send')
        send_button = ttk.Button(self, text='Send', command=self.send_function)

        # place send button in middle of the window
        if style == 'sequences':
            self.send_label.grid(column=0, row=7, columnspan=45, **paddings)
            send_button.grid(column=0, row=8, columnspan=45, **paddings)
        elif style == 'aliases':
            self.send_label.grid(column=0, row=7, columnspan=10, **paddings)
            send_button.grid(column=0, row=8, columnspan=10, **paddings)

    # function for sending data to device
    def send_function(self):
        global ser_msg

        if self.style == 'sequences':
            output_id = []
            delay_time = []
            global ignition_delay

            for i in range(0, 30):
                value_min = self.entry_min[i].get()
                minutes = 0
                value_sec = self.entry_sec[i].get()
                seconds = 0
                value_millis = self.entry_millis[i].get()
                millis = 0

                self.index_label[i].configure(foreground='black')  # reset label color to black

                if value_min != '' or value_sec != '' or value_millis != '':
                    try:
                        if value_min != '':
                            minutes = int(value_min)
                        elif value_min != str(range(0, 9)):
                            self.entry_min[i].insert(0, '0')
                        if value_sec != '':
                            seconds = int(value_sec)
                        elif value_sec != str(range(0, 9)):
                            self.entry_sec[i].insert(0, '0')
                        if value_millis != '':
                            millis = int(value_millis)
                        elif value_millis != str(range(0, 9)):
                            self.entry_millis[i].insert(0, '0')
                    except ValueError:
                        self.index_label[i].configure(foreground='red')
                        self.send_label.configure(foreground='red', text='Err pos. {0}: Only numbers!'.format(i + 1))
                        return
                    else:
                        output_id.append(i + 1)
                        delay_time.append(minutes*60000 + seconds*1000 + millis)
                        ignition_delay[i] = minutes*60 + seconds + millis/1000

            self.update()  # update window

            for i in range(0, len(output_id)):
                command = 'Delay({0})[{1}]'.format((output_id[i]), delay_time[i])  # format command syntax
                # print(command)  # print command in terminal
                ser.write(command.encode())   # send command to unit
                ser_msg = ''
                wait_time = time.time() + 2

                # msg = ser.readline().decode("utf-8")  # read reply from unit
                # print(msg)  # just a debug feature

                while wait_time > time.time():
                    if ser_msg != '':
                        break
                    time.sleep(0.1)

                if ser_msg.startswith('ReDelay({0})[{1}]'.format((output_id[i]), delay_time[i])):
                    # print('OK\n')
                    self.index_label[output_id[i] - 1].configure(foreground='green')
                    self.send_label.configure(foreground='green', text='Success')
                    self.update()  # update window
                else:
                    # print('Error\n')
                    self.index_label[output_id[i] - 1].configure(foreground='red')
                    self.send_label.configure(foreground='red', text='Communication error')
                    break

        elif self.style == 'aliases':
            output_id = []
            alias = []

            for i in range(0, 30):
                value = self.alias[i].get()

                self.index_label[i].configure(foreground='black')   # reset label color to black

                # if is some value in entry box, then try if it is number
                if value != '':
                    try:
                        alias.append(int(value))
                    except ValueError:
                        self.index_label[i].configure(foreground='red')
                        self.send_label.configure(foreground='red', text='Err pos. {0}: Only numbers!'.format(i + 1))
                        return
                    else:
                        output_id.append(i + 1)

            self.update()  # update window

            for i in range(0, len(output_id)):
                command = 'Alias({0})[{1}]'.format((output_id[i]), alias[i])  # format command syntax
                # print(command)  # print command in terminal
                ser.write(command.encode())   # send command to unit
                ser_msg = ''
                wait_time = time.time() + 2

                # msg = ser.readline().decode("utf-8")    # read reply from unit
                # print(msg)  # just a debug feature
                while wait_time > time.time():
                    if ser_msg != '':
                        break
                    time.sleep(0.1)

                if ser_msg.startswith('ReAlias({0})[{1}]'.format((output_id[i]), alias[i])):
                    # print('OK\n')
                    self.index_label[output_id[i] - 1].configure(foreground='green')
                    self.send_label.configure(foreground='green', text='Success')
                    self.update()  # update window
                else:
                    # print('Error\n')
                    self.index_label[output_id[i] - 1].configure(foreground='red')
                    self.send_label.configure(foreground='red', text='Communication error')
                    break


class StatusWindow(tk.Toplevel):
    def __init__(self):
        super().__init__()
        self.resizable(True, True)
        self.geometry("950x710+200+0")
        self.minsize(950, 710)
        self.configure(bg='#d9d9d9')

        self.unit_image = tk.PhotoImage(file='./unit_image.png')
        self.timer_label = []
        self.timer_running = False
        self.start_time = time.time()
        self.unit_info_label = []
        self.test_btn = []

        global ser_msg
        ser_msg = ''

        iteration = 0

        for i_y in range(0, 2):
            for i_x in range(0, 3):
                if i_y == 1 and i_x == 2:
                    break

                x_move_1 = 0
                x_move_2 = 0
                if i_x > 0 and i_y == 0:
                    x_move_2 = 5
                if i_y > 0 or i_x > 1:
                    x_move_1 = 7
                    x_move_2 = 5

                ttk.Label(self, image=self.unit_image).place(x=(50 + 300*i_x), y=(50 + 350*i_y))

                # add labels to outputs
                for i in range(0, 3):
                    ttk.Label(self, font=('bold', 14), foreground='white', background='#262626',
                              text='{0}'.format(1 + i + 6*i_x + 18*i_y)).place(x=(61 + 300*i_x - x_move_1),
                                                                               y=(83 + 350*i_y + 104*i))
                for i in range(0, 3):
                    ttk.Label(self, font=('bold', 14), foreground='white', background='#262626',
                              text='{0}'.format(6 - i + 6*i_x + 18*i_y)).place(x=(276 + 300*i_x - x_move_2),
                                                                               y=(83 + 350*i_y + 104*i))

                # add timers to outputs
                for i in range(0, 3):
                    str_delay_time = time.strftime("%M:%S", time.gmtime(ignition_delay[i + 6*i_x + 18*i_y]))
                    self.timer_label.append(ttk.Label(self, font=('bold', 12), foreground='white', background='#262626',
                                                      text=str_delay_time))
                    self.timer_label[iteration].place(x=(71 + 300*i_x), y=(60 + 350*i_y + 104*i))
                    iteration += 1
                for i in range(0, 3):
                    str_delay_time = time.strftime("%M:%S", time.gmtime(ignition_delay[5 - i + 6*i_x + 18*i_y]))
                    self.timer_label.append(ttk.Label(self, font=('bold', 12), foreground='white', background='#262626',
                                                      text=str_delay_time))
                    self.timer_label[iteration].place(x=(233 + 300*i_x), y=(60 + 350*i_y + 104*i))
                    iteration += 1

                # add unit information label
                self.unit_info_label.append(ttk.Label(self, font=('bold', 12), text='Temp.: --.-- °C  Bat.: --- %'))
                self.unit_info_label[i_x + i_y*3].place(x=(65 + 300*i_x), y=(30 + 350*i_y))

                # add test button
                self.test_btn.append(tk.Button(self, background='#262626', foreground='white', font=('bold', 12),
                                               text='LED &\nBuzzer\nTest',
                                               command=lambda unit=i_x + i_y*3 + 1: self.test_function(unit)))
                self.test_btn[i_x + i_y*3].place(x=(125 + 300*i_x), y=(208 + 350*i_y), width=102, height=69)

        self.refresh_button = ttk.Button(self, text='Refresh', command=self.send_refresh)
        self.refresh_button.place(x=727, y=440, width=100, height=60)

        self.fire_button = tk.Button(self, background='black', foreground='#ffff00', font='bold 20', text='Fire',
                                     state='disabled', command=self.send_fire)
        self.fire_button.place(x=727, y=580, width=100, height=60)

        self.refresh_window()

    @staticmethod
    def test_function(unit):
        command = '/m({0})'.format(unit)
        ser.write(command.encode())

    def send_fire(self):
        ser.write('Fire_ALL'.encode())
        time.sleep(0.9)
        self.start_time = time.time()
        self.timer_running = True

    def send_refresh(self):
        ser.write('/l(0)'.encode())
        for i in self.test_btn:
            i.configure(background='#262626', foreground='white')
        for i in self.unit_info_label:
            i.configure(text='Temp.: --.-- °C  Bat.: --- %')

    def refresh_window(self):
        iteration = 0
        global ser_msg

        if ser_msg.startswith('Arm!'):
            self.fire_button.configure(state='normal', background='#ff0000')
            ser_msg = ''
        elif ser_msg.startswith('Disarm!'):
            self.fire_button.configure(state='disabled', background='black')
            self.timer_running = False
            ser_msg = ''
        elif ser_msg.startswith('UI'):
            unit = int(ser_msg.split('(')[1].split(')')[0])
            data = ser_msg.split('[')[1].split(']')[0].split(', ')
            ser_msg = ''

            # refresh unit information label
            self.unit_info_label[unit - 1].configure(text='Temp.: {0} °C  Bat.: {1} %'.format(data[0], data[1]))

            # refresh unit test button
            if data[2] == 'True':
                self.test_btn[unit - 1].configure(background='#ff0000', foreground='#ffff00')
            elif data[2] == 'False':
                self.test_btn[unit - 1].configure(background='#00ff00', foreground='black')

        for i_y in range(0, 2):
            for i_x in range(0, 3):
                if i_y == 1 and i_x == 2:
                    break

                # refresh outputs timers
                if self.timer_running:
                    for i in range(0, 3):
                        output = i + 6*i_x + 18*i_y
                        elapsed_time = time.time() - self.start_time - 1
                        if (elapsed_time + 1) <= ignition_delay[output]:
                            str_delay_time = time.strftime("%M:%S", time.gmtime(ignition_delay[output] - elapsed_time))
                            self.timer_label[iteration].configure(foreground='#00ff00', text=str_delay_time)
                        else:
                            self.timer_label[iteration].configure(foreground='red', text='00:00')
                        iteration += 1
                    for i in range(0, 3):
                        output = 5 - i + 6*i_x + 18*i_y
                        elapsed_time = time.time() - self.start_time - 1
                        if (elapsed_time + 1) <= ignition_delay[output]:
                            str_delay_time = time.strftime("%M:%S", time.gmtime(ignition_delay[output] - elapsed_time))
                            self.timer_label[iteration].configure(foreground='#00ff00', text=str_delay_time)
                        else:
                            self.timer_label[iteration].configure(foreground='red', text='00:00')
                        iteration += 1

        self.after(100, self.refresh_window)


if __name__ == "__main__":
    main_window = tk.Tk()
    main_window.geometry("350x140+100+100")
    main_window.resizable(True, True)
    main_window.minsize(350, 140)
    main_window.title('Firing system configuration tool')
    main_window.configure(bg='#d9d9d9')

    ser = serial.Serial(timeout=2)

    # paddings for widgets using the grid layout
    paddings = {'padx': 10, 'pady': 10}

    # main label
    label = ttk.Label(main_window, font='bold', text='Connect to a serial port:')
    label.grid(column=0, row=0, columnspan=3, sticky=tk.W, **paddings)

    # add port choose box
    main_window.selected_port = tk.StringVar(main_window)
    choose_port = ttk.Combobox(main_window, width=12, textvariable=main_window.selected_port, postcommand=refresh_ports)
    choose_port.grid(column=0, row=1, **paddings)

    # add connect button
    connect_button = ttk.Button(main_window, text='Connect', command=connect)
    connect_button.grid(column=1, row=1, **paddings)

    # connection status label
    main_window.output_label = ttk.Label(main_window, foreground='red', text='Disconnected')
    main_window.output_label.grid(column=2, row=1, **paddings)

    # add button for open new window for setting sequences
    sequence_button = ttk.Button(main_window, text='Set sequences', state='disabled',
                                 command=lambda: Window('sequences'))
    sequence_button.grid(column=0, row=2, **paddings)

    # add button for open new window for setting aliases
    alias_button = ttk.Button(main_window, text='Set aliases', state='disabled', command=lambda: Window('aliases'))
    alias_button.grid(column=1, row=2, **paddings)

    # add button for open new status window
    status_button = ttk.Button(main_window, text='Status', state='disabled', command=lambda: StatusWindow())
    status_button.grid(column=2, row=2, **paddings)

    main_window.protocol("WM_DELETE_WINDOW", on_closing)
    main_window.mainloop()
