import tkinter as tk
from tkinter import ttk
import configparser
from typing import Self
from threading import Thread
import threading
from datetime import datetime
import time
import socket
import os
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
import platform
import subprocess
import re

class App(ttk.Frame):
    def __init__(self, parent):
        super().__init__(parent)
        self.parent = parent
        self.parent.title("Tally Triggers V1.2 HTTP")


        # Create tabs
        self.tabControl = ttk.Notebook(self.parent)
        self.tab1 = ttk.Frame(self.tabControl)
        self.tab2 = ttk.Frame(self.tabControl)


        # Add tabs to the tab control
        self.tabControl.add(self.tab1, text="Main")
        self.tabControl.add(self.tab2, text="Settings")
        self.tabControl.pack(expand=1, fill="both")

        #Load Config
        self.load_config()

        # Start TCP1 listener thread
        self.tcp_thread1 = threading.Thread(target=self.tcp_listener, args=(self.TCP_Port1,), daemon=True)
        self.tcp_thread1.start()

        # Start TCP2 listener thread
        self.tcp_thread2 = threading.Thread(target=self.tcp_listener, args=(self.TCP_Port2,), daemon=True)
        self.tcp_thread2.start()


        # Create widgets
        self.setup_widgets()


        # Start Tally Box connectivity check thread
        self.connection_thread = threading.Thread(target=self.check_tally_connection, daemon=True)
        self.connection_thread.start()





    def load_config(self):
        # Load configuration
        config = configparser.ConfigParser()
        config.read('Config.ini')

        # Courts
        self.Relay_1_Name = config['Courts']['Relay1']
        self.Relay_2_Name = config['Courts']['Relay2']


        #TMCC IPs
        self.Relay1_TMCCIP = config['TMMC']['Relay1']
        self.Relay2_TMCCIP = config['TMMC']['Relay2']


        #TCP Commands
        self.Relay1_Command = config['TCP Commands']['Relay1']
        self.Relay2_Command = config['TCP Commands']['Relay2']


        #General
        self.TallyBoxIP = config['General']['TallyBoxIP']
        self.TCP_Port1 = int(config['General']['TCP_Port1'])
        self.TCP_Port2 = int(config['General']['TCP_Port2'])
        self.On_Time = int(config['General']['On_Time'])
        self.PosDistanceThreshold = int(config['General']['Pos_Dist'])
        self.NegDistanceThreshold = int(config['General']['Neg_Dist'])


    

     
    def setup_widgets(self):
        
        
        #####################################################################################
        # Relay 1 Frame
        self.Relay1_frame = ttk.LabelFrame(self.tab1, text="Relay 1", padding=(20, 10))
        self.Relay1_frame.grid(
            row=0, column=0, padx=(5, 5), pady=(20, 10), sticky="nsew"
        )

        # Relay 1
        self.Relay1 = ttk.Label(self.Relay1_frame,text=self.Relay_1_Name,justify="center",font=("-size", 10, "-weight", "bold"),
        )
        self.Relay1.grid(row=0, column=0, padx=0, pady=(0, 20), sticky="ns")


        # Canvas for the circle
        self.Relay1Status = tk.Canvas(self.Relay1_frame, width=45, height=50)
        self.Relay1Status.grid(row=1, column=0, pady=(0, 10))

        # Draw circle
        self.oval_id1 = self.Relay1Status.create_oval(10, 10, 40, 40, fill="red", outline="grey")

        # Relay 1 TMCC Connection
        self.Relay1TMCCConnection = ttk.Label(self.Relay1_frame,text="TMCC",justify="center",font=("-size", 10, "-weight", "bold"),
        )
        self.Relay1TMCCConnection.grid(row=2, column=0, padx=0, pady=(0, 20), sticky="ns")

        # Relay 1 last trigger time
        self.Relay1time = ttk.Label(self.Relay1_frame,text="Never",justify="center",font=("-size", 10, "-weight", "bold"),
        )
        self.Relay1time.grid(row=3, column=0, padx=0, pady=(0, 20), sticky="ns")

        # Test Button
        self.Relay1button = ttk.Button(self.Relay1_frame, text="Test", style="Accent.TButton",width = 12, command=self.publish_relay1)
        self.Relay1button.grid(row=4, column=0, padx=0, pady=10, sticky="nsew")

        #####################################################################################
        # Relay 2 Frame
        self.Relay2_frame = ttk.LabelFrame(self.tab1, text="Relay 2", padding=(20, 10))
        self.Relay2_frame.grid(
            row=0, column=1, padx=(5, 5), pady=(20, 10), sticky="nsew"
        )

        # Entry Relay 2
        self.Relay2 = ttk.Label(self.Relay2_frame,text=self.Relay_2_Name,justify="center",font=("-size", 10, "-weight", "bold"),
        )
        self.Relay2.grid(row=0, column=0, padx=0, pady=(0, 20), sticky="ns")

        # Canvas for the circle
        self.Relay2Status = tk.Canvas(self.Relay2_frame, width=45, height=50)
        self.Relay2Status.grid(row=1, column=0, pady=(0, 10))

        # Draw circle
        self.oval_id2 = self.Relay2Status.create_oval(10, 10, 40, 40, fill="red", outline="grey")

        # Relay 2 TMCC Connection
        self.Relay2TMCCConnection = ttk.Label(self.Relay2_frame,text="TMCC",justify="center",font=("-size", 10, "-weight", "bold"),
        )
        self.Relay2TMCCConnection.grid(row=2, column=0, padx=0, pady=(0, 20), sticky="ns")

        # Entry Relay 2 last trigger time
        self.Relay2time = ttk.Label(self.Relay2_frame,text="Never",justify="center",font=("-size", 10, "-weight", "bold"),
        )
        self.Relay2time.grid(row=3, column=0, padx=0, pady=(0, 20), sticky="ns")

        # Test Button
        self.Relay2button = ttk.Button(self.Relay2_frame, text="Test", style="Accent.TButton",width = 12, command=self.publish_relay2)
        self.Relay2button.grid(row=4, column=0, padx=0, pady=10, sticky="nsew")


        #####################################################################################
        # Extras Frame
        self.Extras_frame = ttk.LabelFrame(self.tab1, text="Extras", padding=(20, 10))
        self.Extras_frame.grid(
            row=1, column=0, columnspan = 8, padx=(5, 5), pady=(5, 5), sticky="nsew"
        )

        # Connection Status
        self.ConnectionStatus = ttk.Label(self.Extras_frame,text="Connected to Tally Box:",justify="center",font=("-size", 10, "-weight", "bold"),
        )
        self.ConnectionStatus.grid(row=0, column=0, padx=0, pady=(0, 0), sticky="ew")

        # Canvas for the circle
        self.ConnectionStatusLight = tk.Canvas(self.Extras_frame, width=50, height=50)
        self.ConnectionStatusLight.grid(row=0, column=1, pady=(0, 0))

        # Draw circle
        self.oval_id9 = self.ConnectionStatusLight.create_oval(10, 10, 40, 40, fill="red", outline="grey")

        # Test All Button
        self.TestAllButton = ttk.Button(self.Extras_frame, text="Test All", style="Accent.TButton",width = 12, command=self.publish_relay_all)
        self.TestAllButton.grid(row=0, column=4, padx=(50,0), pady=0, sticky="nsew")


        #######################################################################################
        #####################################################################################



        # Relay 1 Settings Frame
        self.Relay1_frame = ttk.LabelFrame(self.tab2, text="Relay 1", padding=(20, 10))
        self.Relay1_frame.grid(
            row=0, column=0, padx=(5, 5), pady=(20, 10), sticky="nsew"
        )

        # Label Relay 1 Courtname
        self.Relay1Name = ttk.Label(self.Relay1_frame,text="Court Name",justify="center",font=("-size", 10, "-weight", "bold"),
        )
        self.Relay1Name.grid(row=0, column=0, padx=0, pady=(0, 20), sticky="ns")

        # Entry Relay 1 Courtname
        self.Relay1Entry = ttk.Entry(self.Relay1_frame, textvariable=self.Relay_1_Name, justify="center", font=("-size", 10, "-weight", "bold"))
        self.Relay1Entry.insert(0, self.Relay_1_Name) 
        self.Relay1Entry.grid(row=1, column=0, padx=0, pady=(0, 20), sticky="ew")

        # Save Button
        self.Save1button = ttk.Button(self.Relay1_frame, text="Save", style="Accent.TButton",width = 12, command=self.save_relay_names)
        self.Save1button.grid(row=2, column=0, padx=0, pady=10, sticky="nsew")
        ###########################################################################################################################################
        # Relay 2 Settings Frame
        self.Relay2_frame = ttk.LabelFrame(self.tab2, text="Relay 2", padding=(20, 10))
        self.Relay2_frame.grid(
            row=0, column=1, padx=(5, 5), pady=(20, 10), sticky="nsew"
        )

        # Label Relay 2 Courtname
        self.Relay2Name = ttk.Label(self.Relay2_frame,text="Court Name",justify="center",font=("-size", 10, "-weight", "bold"),
        )
        self.Relay2Name.grid(row=0, column=0, padx=0, pady=(0, 20), sticky="ns")

        # Entry Relay 2 Courtname
        self.Relay2Entry = ttk.Entry(self.Relay2_frame, textvariable=self.Relay_2_Name, justify="center", font=("-size", 10, "-weight", "bold"))
        self.Relay2Entry.insert(0, self.Relay_2_Name) 
        self.Relay2Entry.grid(row=1, column=0, padx=0, pady=(0, 20), sticky="ew")

        # Save Button
        self.Save2button = ttk.Button(self.Relay2_frame, text="Save", style="Accent.TButton",width = 12, command=self.save_relay_names)
        self.Save2button.grid(row=2, column=0, padx=0, pady=10, sticky="nsew")
        ###########################################################################################################################################

        # Function to send a TCP message to the TALLY BOX
    def send_message(self, command, timeout=5):
        try:
            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
                s.settimeout(timeout)
                s.connect((self.TallyBoxIP, 17494))

                # command may already be bytes, so handle both cases
                if isinstance(command, str):
                    s.sendall(bytes.fromhex(command))
                else:
                    s.sendall(command)

                data = s.recv(1024)
                return data  # Return response bytes
        except (socket.timeout, ConnectionRefusedError, socket.error) as e:
            print(f"Error sending message: {e}")
            return None


    def publish_relay1(self):
        def toggle_relay1():
            relay_id = 1

            # ---- Turn ON ----
            message = bytes([0x20, relay_id, 0x00])
            response = self.send_message(message)
            current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
            print(f"[{current_time}] Sent command: 20 {relay_id:02X} 00 - Relay {relay_id} ON")

            if response:
                resp_val = response[0]
                print(f"[{current_time}] Device response: {resp_val}")
                if resp_val == 0:
                    self.Relay1Status.itemconfig(self.oval_id1, fill="green")
                    self.Relay1time.config(text=current_time)
                else:
                    print(f"[{current_time}] Relay {relay_id} ON failed (response={resp_val})")
                    return
            else:
                print(f"[{current_time}] No response from device for Relay {relay_id} ON")
                return

            time.sleep(self.On_Time)

            # ---- Turn OFF ----
            message = bytes([0x21, relay_id, 0x00])
            response = self.send_message(message)
            current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
            print(f"[{current_time}] Sent command: 21 {relay_id:02X} 00 - Relay {relay_id} OFF")

            if response:
                resp_val = response[0]
                print(f"[{current_time}] Device response: {resp_val}")
                if resp_val == 0:
                    self.Relay1Status.itemconfig(self.oval_id2, fill="red")
                else:
                    print(f"[{current_time}] Relay {relay_id} OFF failed (response={resp_val})")
            else:
                print(f"[{current_time}] No response from device for Relay {relay_id} OFF")

        threading.Thread(target=toggle_relay1, daemon=True).start()


    def publish_relay2(self):
        def toggle_relay2():
            relay_id = 2

            # ---- Turn ON ----
            message = bytes([0x20, relay_id, 0x00])
            response = self.send_message(message)
            current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
            print(f"[{current_time}] Sent command: 20 {relay_id:02X} 00 - Relay {relay_id} ON")

            if response:
                resp_val = response[0]
                print(f"[{current_time}] Device response: {resp_val}")
                if resp_val == 0:
                    self.Relay2Status.itemconfig(self.oval_id2, fill="green")
                    self.Relay2time.config(text=current_time)
                else:
                    print(f"[{current_time}] Relay {relay_id} ON failed (response={resp_val})")
                    return
            else:
                print(f"[{current_time}] No response from device for Relay {relay_id} ON")
                return

            time.sleep(self.On_Time)

            # ---- Turn OFF ----
            message = bytes([0x21, relay_id, 0x00])
            response = self.send_message(message)
            current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
            print(f"[{current_time}] Sent command: 21 {relay_id:02X} 00 - Relay {relay_id} OFF")

            if response:
                resp_val = response[0]
                print(f"[{current_time}] Device response: {resp_val}")
                if resp_val == 0:
                    self.Relay2Status.itemconfig(self.oval_id2, fill="red")
                else:
                    print(f"[{current_time}] Relay {relay_id} OFF failed (response={resp_val})")
            else:
                print(f"[{current_time}] No response from device for Relay {relay_id} OFF")

        threading.Thread(target=toggle_relay2, daemon=True).start()



    def publish_relay_all(self):
        self.publish_relay1()
        self.publish_relay2()

    def check_tally_connection(self):
        """Continuously checks if the Tally Box is reachable and updates the status light."""
        while True:
            if self.ping_tally_box():
                self.ConnectionStatusLight.itemconfig(self.oval_id9, fill="green")  # Change to green if reachable
            else:
                self.ConnectionStatusLight.itemconfig(self.oval_id9, fill="red")  # Change to red if unreachable
            time.sleep(5)  # Check every 5 seconds

    def ping_tally_box(self):
        """Pings the Tally Box IP to check connectivity."""
        param = "-n" if platform.system().lower() == "windows" else "-c"
        command = ["ping", param, "1", self.TallyBoxIP]
        try:
            subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
            return True  # Ping was successful
        except subprocess.CalledProcessError:
            return False  # Ping failed

    def save_relay_names(self):
        """Saves the Relay 1 and Relay 2 names to the Config.ini file and updates the UI labels."""
        new_relay1_name = self.Relay1Entry.get()
        new_relay2_name = self.Relay2Entry.get()

        # Update labels in the UI
        self.Relay1.config(text=new_relay1_name)
        self.Relay2.config(text=new_relay2_name)

        # Update the config file
        config = configparser.ConfigParser()
        config.read('Config.ini')

        if 'Courts' not in config:
            config['Courts'] = {}

        config['Courts']['Relay1'] = new_relay1_name
        config['Courts']['Relay2'] = new_relay2_name

        with open('Config.ini', 'w') as configfile:
            config.write(configfile)

        print("Config updated successfully!")
 




    def tcp_listener(self, port):
        # TCP/IP socket setup
        server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server_socket.bind(('0.0.0.0', port))  # Bind to all interfaces
        server_socket.listen(1)  # Listen for incoming connections

        print(f"TCP server listening on port {port} \n")

        while True:
            # Accept a connection
            conn, addr = server_socket.accept()
            print(f"Connection from {addr} on port {port}")

            try:
                # Receive the message
                message = conn.recv(1024).decode('utf-8').strip()
                print(f"Received raw message: {message}")

                # Extract values using regular expressions
                close_call_match = re.search(r'"Close Call":\s*"(\w+)"', message)
                distance_match = re.search(r'"Distance":\s*(-?[\d\.]+)', message)

                if not (close_call_match and distance_match):
                    print("Error: Invalid message format")
                    continue  # Skip processing this message

                # Convert extracted values
                close_call = close_call_match.group(1) == "True"
                distance = float(distance_match.group(1))

                # Check if Close Call is True and within tolerance
                if close_call:
                    if port == self.TCP_Port1:
                        self.publish_relay1()
                    elif port == self.TCP_Port2:
                        self.publish_relay2()
                    else:
                        print(f"Message received but conditions not met for port {port}")

            except Exception as e:
                print(f"Error processing message: {e}")

            finally:
                conn.close()











if __name__ == "__main__":
    root = tk.Tk()
    root.title("Tally Triggers V1.2 HTTP")
    root.iconbitmap(r'Resources/icon1.ico')


    # Simply set the theme
    root.tk.call("source", "Resources/azure.tcl")
    root.tk.call("set_theme", "dark")

    app = App(root)
    app.pack(fill="both", expand=True)


    # Set a minsize for the window, and place it in the middle
    root.update()
    root.minsize(root.winfo_width(), root.winfo_height())


    root.mainloop()
    
