import queue
import socket
import threading
import time
import tkinter as tk
from tkinter import messagebox, ttk


class Messenger:

    def __init__(self, root):
        self.root = root
        self.events = queue.Queue()
        self.outgoing = queue.Queue()
        self.connection_lock = threading.Lock()
        self.stop_event = None
        self.server_socket = None
        self.connection = None
        self.running = False

        self.my_port_var = tk.StringVar(value="5050")
        self.peer_host_var = tk.StringVar(value="127.0.0.1")
        self.peer_port_var = tk.StringVar(value="5051")
        self.message_var = tk.StringVar()
        self.status_var = tk.StringVar(value="Не запущено")

        self.root.title("Мессенджер")
        self.root.geometry("720x580")
        self.root.minsize(600, 440)
        self.build_ui()
        self.root.protocol("WM_DELETE_WINDOW", self.on_close)
        self.root.after(50, self.poll_events)

    def build_ui(self):
        settings = ttk.LabelFrame(self.root, text="Настройки соединения")
        settings.pack(fill="x", padx=10, pady=(10, 6))

        ttk.Label(settings, text="Мой порт:").grid(row=0, column=0, padx=(10, 6), pady=(10, 4), sticky="w")
        self.my_port_entry = ttk.Entry(settings, textvariable=self.my_port_var, width=8)
        self.my_port_entry.grid(row=0, column=1, padx=(0, 14), pady=(10, 4), sticky="w")

        ttk.Label(settings, text="Адрес собеседника:").grid(row=0, column=2, padx=(0, 6), pady=(10, 4), sticky="w")
        self.peer_host_entry = ttk.Entry(settings, textvariable=self.peer_host_var, width=16)
        self.peer_host_entry.grid(row=0, column=3, padx=(0, 14), pady=(10, 4), sticky="w")

        ttk.Label(settings, text="Порт собеседника:").grid(row=0, column=4, padx=(0, 6), pady=(10, 4), sticky="w")
        self.peer_port_entry = ttk.Entry(settings, textvariable=self.peer_port_var, width=8)
        self.peer_port_entry.grid(row=0, column=5, padx=(0, 14), pady=(10, 4), sticky="w")

        self.start_button = ttk.Button(settings, text="Запустить", command=self.toggle)
        self.start_button.grid(row=0, column=6, padx=(0, 12), pady=(10, 4), sticky="w")

        hint = ("Для двух программ на одном компьютере: у первой оставьте порты 5050 и 5051, "
                "у второй укажите свой порт 5051 и порт собеседника 5050. Программа, у которой "
                "номер собственного порта меньше, сама устанавливает соединение.")
        ttk.Label(settings, text=hint, wraplength=680, foreground="#555555", justify="left").grid(
            row=1, column=0, columnspan=7, padx=10, pady=(0, 10), sticky="w")

        status = ttk.Label(self.root, textvariable=self.status_var, relief="sunken", anchor="w")
        status.pack(side="bottom", fill="x", ipady=4)

        input_frame = ttk.Frame(self.root)
        input_frame.pack(side="bottom", fill="x", padx=10, pady=(4, 8))

        self.message_entry = ttk.Entry(input_frame, textvariable=self.message_var)
        self.message_entry.pack(side="left", fill="x", expand=True)
        self.message_entry.bind("<Return>", self.on_return)

        self.send_button = ttk.Button(input_frame, text="Отправить", command=self.send_message)
        self.send_button.pack(side="left", padx=(8, 0))

        chat_frame = ttk.LabelFrame(self.root, text="Переписка")
        chat_frame.pack(fill="both", expand=True, padx=10, pady=6)

        self.chat = tk.Text(chat_frame, wrap="word", state="disabled", height=14, padx=6, pady=6)
        self.chat.pack(side="left", fill="both", expand=True, padx=(8, 0), pady=8)

        scrollbar = ttk.Scrollbar(chat_frame, orient="vertical", command=self.chat.yview)
        scrollbar.pack(side="right", fill="y", padx=(0, 8), pady=8)
        self.chat.configure(yscrollcommand=scrollbar.set)

        self.chat.tag_configure("me", foreground="#0b6b3a")
        self.chat.tag_configure("peer", foreground="#12448a")
        self.chat.tag_configure("system", foreground="#808080")

    def on_return(self, event):
        self.send_message()
        return "break"

    def toggle(self):
        if self.running:
            self.stop_networking()
        else:
            self.start_networking()

    def start_networking(self):
        my_port_text = self.my_port_var.get().strip()
        peer_port_text = self.peer_port_var.get().strip()
        peer_host = self.peer_host_var.get().strip()

        if not my_port_text.isdigit() or not peer_port_text.isdigit():
            messagebox.showerror("Ошибка", "Номера портов должны быть целыми числами.")
            return

        my_port = int(my_port_text)
        peer_port = int(peer_port_text)

        if not (1 <= my_port <= 65535) or not (1 <= peer_port <= 65535):
            messagebox.showerror("Ошибка", "Номера портов должны быть в диапазоне от 1 до 65535.")
            return

        if not peer_host:
            messagebox.showerror("Ошибка", "Укажите адрес собеседника.")
            return

        if self.port_is_busy(my_port):
            messagebox.showerror("Ошибка", f"Порт {my_port} уже используется другой программой.")
            return

        server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        try:
            server.bind(("0.0.0.0", my_port))
            server.listen(5)
        except OSError as exc:
            server.close()
            messagebox.showerror("Ошибка", f"Не удалось занять порт {my_port}.\n{exc}")
            return

        self.server_socket = server
        self.stop_event = threading.Event()
        self.outgoing = queue.Queue()
        self.running = True

        self.start_button.configure(text="Остановить")
        for entry in (self.my_port_entry, self.peer_host_entry, self.peer_port_entry):
            entry.state(["disabled"])

        threading.Thread(target=self.accept_loop, args=(self.stop_event,), daemon=True).start()
        threading.Thread(target=self.sender_loop,
                         args=(self.stop_event, self.outgoing), daemon=True).start()

        if my_port < peer_port:
            self.set_status(f"Подключение к {peer_host}:{peer_port}")
            self.append(f"Попытка подключения к {peer_host}:{peer_port}...", "system")
            threading.Thread(target=self.connect_loop,
                             args=(self.stop_event, peer_host, peer_port), daemon=True).start()
        else:
            self.set_status(f"Ожидание подключения на порту {my_port}")
            self.append(f"Ожидание подключения на порту {my_port}.", "system")

        self.message_entry.focus_set()

    def stop_networking(self):
        stop_event = self.stop_event
        if stop_event is not None:
            stop_event.set()
        self.stop_event = None

        with self.connection_lock:
            connection = self.connection
            self.connection = None

        if connection is not None:
            try:
                connection.shutdown(socket.SHUT_RDWR)
            except OSError:
                pass
            try:
                connection.close()
            except OSError:
                pass

        server = self.server_socket
        self.server_socket = None
        if server is not None:
            try:
                server.close()
            except OSError:
                pass

        self.running = False
        self.start_button.configure(text="Запустить")
        for entry in (self.my_port_entry, self.peer_host_entry, self.peer_port_entry):
            entry.state(["!disabled"])

        while True:
            try:
                self.events.get_nowait()
            except queue.Empty:
                break

        self.set_status("Остановлено")
        self.append("Соединение остановлено.", "system")

    def port_is_busy(self, port):
        probe = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        probe.settimeout(0.3)
        try:
            probe.connect(("127.0.0.1", port))
            return True
        except OSError:
            return False
        finally:
            try:
                probe.close()
            except OSError:
                pass

    def accept_loop(self, stop_event):
        while not stop_event.is_set():
            server = self.server_socket
            if server is None:
                break
            try:
                sock, addr = server.accept()
            except OSError:
                break

            with self.connection_lock:
                busy = self.connection is not None or stop_event.is_set()
                if not busy:
                    self.connection = sock

            if busy:
                try:
                    sock.close()
                except OSError:
                    pass
                continue

            self.events.put(("system", f"Подключился собеседник {addr[0]}:{addr[1]}."))
            self.events.put(("status", "Соединено"))
            threading.Thread(target=self.reader_loop, args=(stop_event, sock), daemon=True).start()

    def connect_loop(self, stop_event, host, port):
        while not stop_event.is_set():
            with self.connection_lock:
                already = self.connection is not None
            if already:
                time.sleep(0.3)
                continue

            try:
                sock = socket.create_connection((host, port), timeout=2.0)
            except OSError:
                time.sleep(1.0)
                continue

            sock.settimeout(None)

            with self.connection_lock:
                if self.connection is not None or stop_event.is_set():
                    try:
                        sock.close()
                    except OSError:
                        pass
                    continue
                self.connection = sock

            self.events.put(("system", f"Соединение установлено с {host}:{port}."))
            self.events.put(("status", "Соединено"))
            threading.Thread(target=self.reader_loop, args=(stop_event, sock), daemon=True).start()

    def reader_loop(self, stop_event, sock):
        buffer = b""
        try:
            while not stop_event.is_set():
                data = sock.recv(4096)
                if not data:
                    break
                buffer += data
                while b"\n" in buffer:
                    line, buffer = buffer.split(b"\n", 1)
                    text = line.decode("utf-8", errors="replace").strip()
                    if text:
                        self.events.put(("message", text))
        except OSError:
            pass
        finally:
            with self.connection_lock:
                if self.connection is sock:
                    self.connection = None
            try:
                sock.close()
            except OSError:
                pass
            if not stop_event.is_set():
                self.events.put(("system", "Соединение потеряно."))
                self.events.put(("status", "Нет соединения"))

    def sender_loop(self, stop_event, outgoing):
        while True:
            try:
                text = outgoing.get(timeout=0.2)
            except queue.Empty:
                if stop_event.is_set():
                    return
                continue

            if stop_event.is_set():
                return

            with self.connection_lock:
                sock = self.connection

            if sock is None:
                self.events.put(("system", "Сообщение не отправлено: нет соединения."))
                continue

            try:
                sock.sendall((text + "\n").encode("utf-8"))
            except OSError as exc:
                self.events.put(("system", f"Не удалось отправить сообщение: {exc}"))

    def send_message(self):
        if not self.running:
            self.append("Сначала запустите соединение.", "system")
            return

        text = self.message_var.get().strip()
        if not text:
            return

        with self.connection_lock:
            connected = self.connection is not None

        if not connected:
            self.append("Нет соединения: сообщение не отправлено.", "system")
            return

        self.message_var.set("")
        self.append("Вы: " + text, "me")
        self.outgoing.put(text)

    def poll_events(self):
        while True:
            try:
                kind, payload = self.events.get_nowait()
            except queue.Empty:
                break

            if kind == "message":
                self.append("Собеседник: " + payload, "peer")
            elif kind == "system":
                self.append(payload, "system")
            elif kind == "status":
                if self.running:
                    self.set_status(payload)

        self.root.after(50, self.poll_events)

    def append(self, text, tag):
        self.chat.configure(state="normal")
        self.chat.insert("end", text + "\n", tag)
        self.chat.see("end")
        self.chat.configure(state="disabled")

    def set_status(self, text):
        self.status_var.set(text)

    def on_close(self):
        self.stop_networking()
        self.root.destroy()


def main():
    root = tk.Tk()
    Messenger(root)
    root.mainloop()


if __name__ == "__main__":
    main()