import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
import socket
import threading
import time
from pythonosc.osc_message import OscMessage

# ---------- Конфигурация ----------
VRC_LISTEN_PORT = 9001   # откуда VRChat отправляет
VRC_SEND_PORT = 9000     # куда VRChat принимает (стандартный)
LOCAL_IP = "127.0.0.1"
BUFFER_SIZE = 65535

# Каналы: для каждой программы указываем её receive-порт (куда роутер будет слать данные от VRChat)
# и send-порт (откуда роутер будет слушать данные от программы и пересылать в VRChat на 9000)
DEFAULT_CHANNELS = [
    {"name": "FaceTracking", "receive": 9005, "send": 9006},
    {"name": "Monitiring", "receive": 9008, "send": 9009},
    {"name": "OSCLeash", "receive": 9011, "send": 9012},
    {"name": "Motion", "receive": 9015, "send": 9016},
]

class OscRouterApp:
    def __init__(self, root):
        self.root = root
        self.root.title("OSC Router + Debug per Channel")
        self.root.geometry("900x700")

        self.channels = []
        self.running = True
        self.vrc_socket = None
        self.lock = threading.Lock()
        self.verbose_log = tk.BooleanVar(value=False)

        self.create_widgets()
        self.load_default_channels()
        self.start_vrc_listener()

    def create_widgets(self):
        frame = ttk.Frame(self.root)
        frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)

        columns = ("#", "Имя", "Receive", "Send", "Статус", "Пакеты (VRC->App)", "Пакеты (App->VRC)")
        self.tree = ttk.Treeview(frame, columns=columns, show="headings")
        for col in columns:
            self.tree.heading(col, text=col)
            self.tree.column(col, width=100, anchor="center")
        self.tree.pack(fill=tk.BOTH, expand=True)

        btn_frame = ttk.Frame(self.root)
        btn_frame.pack(fill=tk.X, padx=5, pady=5)

        ttk.Button(btn_frame, text="Добавить канал", command=self.add_channel_dialog).pack(side=tk.LEFT, padx=2)
        ttk.Button(btn_frame, text="Удалить канал", command=self.delete_channel).pack(side=tk.LEFT, padx=2)
        ttk.Button(btn_frame, text="Вкл/Выкл", command=self.toggle_channel).pack(side=tk.LEFT, padx=2)
        ttk.Button(btn_frame, text="Сброс счётчиков", command=self.reset_counts).pack(side=tk.LEFT, padx=2)
        ttk.Checkbutton(btn_frame, text="Подробный лог", variable=self.verbose_log).pack(side=tk.LEFT, padx=10)

        self.log_text = scrolledtext.ScrolledText(self.root, height=12, state='disabled')
        self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)

        self.status_var = tk.StringVar(value="Сервер запущен, слушаем порт 9001 и дублируем на 9000")
        status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN)
        status_bar.pack(fill=tk.X, padx=5, pady=2)

    def log(self, msg):
        self.log_text.config(state='normal')
        self.log_text.insert(tk.END, f"[{time.strftime('%H:%M:%S')}] {msg}\n")
        self.log_text.see(tk.END)
        self.log_text.config(state='disabled')

    def load_default_channels(self):
        for ch in DEFAULT_CHANNELS:
            self.add_channel(ch["name"], ch["receive"], ch["send"])

    def add_channel(self, name, recv_port, send_port):
        with self.lock:
            for ch in self.channels:
                if ch["recv_port"] == recv_port or ch["send_port"] == send_port:
                    self.log(f"Ошибка: порт {recv_port} или {send_port} занят")
                    return False

            try:
                sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
                sock.bind((LOCAL_IP, send_port))
                sock.setblocking(False)
            except Exception as e:
                self.log(f"Ошибка: не могу занять порт {send_port}: {e}")
                return False

            channel = {
                "name": name,
                "recv_port": recv_port,
                "send_port": send_port,
                "enabled": True,
                "recv_count": 0,
                "send_count": 0,
                "socket": sock,
            }
            self.channels.append(channel)

            threading.Thread(target=self.read_app_socket, args=(channel,), daemon=True).start()
            self.update_table()
            self.log(f"Канал '{name}' добавлен (Receive:{recv_port}, Send:{send_port})")
            return True

    def read_app_socket(self, channel):
        sock = channel["socket"]
        while self.running:
            try:
                data, addr = sock.recvfrom(BUFFER_SIZE)
                if data:
                    try:
                        send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
                        send_sock.sendto(data, (LOCAL_IP, VRC_SEND_PORT))
                        send_sock.close()
                        with self.lock:
                            channel["send_count"] += 1
                        self.log(f"[{channel['name']}->VRC] {len(data)} байт")
                        self.update_table()
                    except Exception as e:
                        self.log(f"Ошибка отправки в VRChat: {e}")
            except BlockingIOError:
                time.sleep(0.01)
                continue
            except Exception as e:
                if self.running:
                    self.log(f"Ошибка чтения с {channel['name']}: {e}")
                break

    def start_vrc_listener(self):
        try:
            self.vrc_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            self.vrc_socket.bind((LOCAL_IP, VRC_LISTEN_PORT))
            self.vrc_socket.setblocking(False)
        except Exception as e:
            messagebox.showerror("Ошибка", f"Не удалось занять порт {VRC_LISTEN_PORT}: {e}")
            self.root.destroy()
            return

        thread = threading.Thread(target=self.vrc_listener_loop, daemon=True)
        thread.start()
        self.log(f"Сервер запущен: слушаем {LOCAL_IP}:{VRC_LISTEN_PORT}")

    def vrc_listener_loop(self):
        while self.running:
            try:
                data, addr = self.vrc_socket.recvfrom(BUFFER_SIZE)
                if data:
                    # Парсим OSC, если включён подробный лог
                    osc_addr = None
                    osc_args = None
                    if self.verbose_log.get():
                        try:
                            msg = OscMessage(data)
                            osc_addr = msg.address
                            osc_args = msg.params
                        except Exception:
                            osc_addr = f"<не OSC, размер {len(data)} байт>"
                            osc_args = ()

                    # Отправляем на все включённые каналы
                    for ch in self.channels:
                        if ch["enabled"]:
                            try:
                                send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
                                send_sock.sendto(data, (LOCAL_IP, ch["recv_port"]))
                                send_sock.close()
                                with self.lock:
                                    ch["recv_count"] += 1
                                if self.verbose_log.get() and osc_addr is not None:
                                    args_str = ', '.join(str(a) for a in osc_args) if osc_args else ''
                                    self.log(f"[{ch['name']}] {osc_addr} ({args_str})")
                            except Exception as e:
                                self.log(f"Ошибка отправки на {ch['name']}: {e}")

                    # Дублируем на порт 9000 (для программ, слушающих по умолчанию)
                    try:
                        send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
                        send_sock.sendto(data, (LOCAL_IP, 9000))
                        send_sock.close()
                    except Exception:
                        pass

                    self.update_table()
            except BlockingIOError:
                time.sleep(0.01)
                continue
            except Exception as e:
                if self.running:
                    self.log(f"Ошибка в основном цикле: {e}")
                    break

    def update_table(self):
        self.root.after(0, self._update_table)

    def _update_table(self):
        with self.lock:
            for item in self.tree.get_children():
                self.tree.delete(item)
            for i, ch in enumerate(self.channels, start=1):
                status = "Вкл" if ch["enabled"] else "Выкл"
                packets = f"{ch['recv_count']}/{ch['send_count']}"
                self.tree.insert("", "end", values=(i, ch["name"], ch["recv_port"], ch["send_port"], status, packets))

    def add_channel_dialog(self):
        dialog = tk.Toplevel(self.root)
        dialog.title("Добавить канал")
        dialog.geometry("300x200")
        ttk.Label(dialog, text="Имя:").grid(row=0, column=0, padx=5, pady=5)
        name_entry = ttk.Entry(dialog)
        name_entry.grid(row=0, column=1, padx=5, pady=5)
        ttk.Label(dialog, text="Receive порт:").grid(row=1, column=0, padx=5, pady=5)
        recv_entry = ttk.Entry(dialog)
        recv_entry.grid(row=1, column=1, padx=5, pady=5)
        ttk.Label(dialog, text="Send порт:").grid(row=2, column=0, padx=5, pady=5)
        send_entry = ttk.Entry(dialog)
        send_entry.grid(row=2, column=1, padx=5, pady=5)

        def on_ok():
            name = name_entry.get().strip()
            if not name:
                messagebox.showerror("Ошибка", "Введите имя")
                return
            try:
                recv = int(recv_entry.get())
                send = int(send_entry.get())
            except ValueError:
                messagebox.showerror("Ошибка", "Порты должны быть числами")
                return
            if recv == send or recv <= 0 or send <= 0:
                messagebox.showerror("Ошибка", "Некорректные порты")
                return
            if self.add_channel(name, recv, send):
                dialog.destroy()
            else:
                messagebox.showerror("Ошибка", "Не удалось добавить канал (порт занят)")

        ttk.Button(dialog, text="OK", command=on_ok).grid(row=3, column=0, padx=5, pady=10)
        ttk.Button(dialog, text="Отмена", command=dialog.destroy).grid(row=3, column=1, padx=5, pady=10)

    def delete_channel(self):
        selected = self.tree.selection()
        if not selected:
            return
        item = selected[0]
        idx = self.tree.index(item)
        if idx < len(self.channels):
            ch = self.channels[idx]
            ch["socket"].close()
            with self.lock:
                self.channels.pop(idx)
            self.update_table()
            self.log(f"Канал '{ch['name']}' удалён")

    def toggle_channel(self):
        selected = self.tree.selection()
        if not selected:
            return
        item = selected[0]
        idx = self.tree.index(item)
        if idx < len(self.channels):
            ch = self.channels[idx]
            ch["enabled"] = not ch["enabled"]
            self.update_table()
            self.log(f"Канал '{ch['name']}' {'включён' if ch['enabled'] else 'выключен'}")

    def reset_counts(self):
        with self.lock:
            for ch in self.channels:
                ch["recv_count"] = 0
                ch["send_count"] = 0
        self.update_table()
        self.log("Счётчики сброшены")

    def on_closing(self):
        self.running = False
        if self.vrc_socket:
            self.vrc_socket.close()
        for ch in self.channels:
            ch["socket"].close()
        self.root.destroy()

if __name__ == "__main__":
    root = tk.Tk()
    app = OscRouterApp(root)
    root.protocol("WM_DELETE_WINDOW", app.on_closing)
    root.mainloop()
