import math
import random
import tkinter as tk
from tkinter import ttk, messagebox

class Molecule:
    __slots__ = ('x', 'y', 'z', 'vx', 'vy', 'vz', 'radius', 'item', 'highlight_item')
    def __init__(self, x, y, z, vx, vy, vz, radius, item, highlight_item):
        self.x = x
        self.y = y
        self.z = z
        self.vx = vx
        self.vy = vy
        self.vz = vz
        self.radius = radius
        self.item = item
        self.highlight_item = highlight_item

class GasApp(tk.Tk):
    COLORS = [
        ("Красный", "#FF0000"),
        ("Оранжевый", "#FFA500"),
        ("Жёлтый", "#FFFF00"),
        ("Зелёный", "#008000"),
        ("Голубой", "#00BFFF"),
        ("Синий", "#0000FF"),
        ("Фиолетовый", "#8B00FF")
    ]
    DEPTH = 200
    FOCAL = 200
    HIGHLIGHT_OFFSET_RATIO = 0.3
    HIGHLIGHT_RADIUS_X_RATIO = 0.35
    HIGHLIGHT_RADIUS_Y_RATIO = 0.25

    def __init__(self):
        super().__init__()
        self.title("Молекулы идеального газа")
        self.geometry("900x700")
        self.molecules = []
        self.running = False
        self.paused = False
        self.after_id = None
        self._create_widgets()
        self.protocol("WM_DELETE_WINDOW", self._on_close)

    def _create_widgets(self):
        control_frame = ttk.Frame(self, padding=5)
        control_frame.pack(side=tk.TOP, fill=tk.X)

        ttk.Label(control_frame, text="Количество:").pack(side=tk.LEFT, padx=(0, 5))
        self.count_var = tk.IntVar(value=100)
        count_spin = ttk.Spinbox(control_frame, from_=1, to=500, textvariable=self.count_var, width=8)
        count_spin.pack(side=tk.LEFT, padx=(0, 15))

        ttk.Label(control_frame, text="Скорость:").pack(side=tk.LEFT, padx=(0, 5))
        self.speed_var = tk.DoubleVar(value=3.0)
        speed_spin = ttk.Spinbox(control_frame, from_=0.5, to=50.0, increment=0.5,
                                 textvariable=self.speed_var, width=8)
        speed_spin.pack(side=tk.LEFT, padx=(0, 15))

        ttk.Label(control_frame, text="Диаметр:").pack(side=tk.LEFT, padx=(0, 5))
        self.diameter_var = tk.IntVar(value=5)
        diameter_spin = ttk.Spinbox(control_frame, from_=2, to=50, textvariable=self.diameter_var, width=8)
        diameter_spin.pack(side=tk.LEFT, padx=(0, 15))

        self.start_button = ttk.Button(control_frame, text="Запустить", command=self.start_simulation)
        self.start_button.pack(side=tk.LEFT, padx=5)
        self.pause_button = ttk.Button(control_frame, text="Пауза", command=self.toggle_pause, state=tk.DISABLED)
        self.pause_button.pack(side=tk.LEFT, padx=5)
        self.clear_button = ttk.Button(control_frame, text="Очистить", command=self.clear_simulation)
        self.clear_button.pack(side=tk.LEFT, padx=5)

        self.canvas = tk.Canvas(self, bg='white', bd=2, relief=tk.SUNKEN)
        self.canvas.pack(fill=tk.BOTH, expand=True)

    def start_simulation(self):
        if self.running:
            self.stop_animation()
            self.clear_canvas()
        try:
            count = int(self.count_var.get())
            speed = float(self.speed_var.get())
            diameter = int(self.diameter_var.get())
        except (ValueError, tk.TclError):
            messagebox.showerror("Ошибка", "Введите корректные числовые значения.")
            return
        if count < 1 or count > 500:
            messagebox.showerror("Ошибка", "Количество должно быть от 1 до 500.")
            return
        if speed <= 0:
            messagebox.showerror("Ошибка", "Скорость должна быть положительной.")
            return
        if diameter < 2 or diameter > 50:
            messagebox.showerror("Ошибка", "Диаметр должен быть от 2 до 50 пикселей.")
            return
        self.canvas.update_idletasks()
        width = self.canvas.winfo_width()
        height = self.canvas.winfo_height()
        if width < 20 or height < 20:
            width, height = 800, 600
        self._create_molecules(count, speed, width, height, diameter)
        self.running = True
        self.paused = False
        self.pause_button.config(text="Пауза", state=tk.NORMAL)
        self.start_button.config(text="Перезапустить")
        if self.after_id is None:
            self.after_id = self.after(16, self._animate)

    def _create_molecules(self, count, speed, width, height, diameter):
        self.clear_canvas()
        self.molecules = []
        radius = diameter / 2
        cols = max(1, int(math.ceil(math.sqrt(count * width / height))))
        rows = max(1, int(math.ceil(count / cols)))
        cell_w = width / cols
        cell_h = height / rows
        spacing = diameter + 2
        placed = 0
        for r in range(rows):
            for c in range(cols):
                if placed >= count:
                    break
                cx = c * cell_w + cell_w / 2
                cy = r * cell_h + cell_h / 2
                offset_x = random.uniform(-min(cell_w, spacing) / 4, min(cell_w, spacing) / 4)
                offset_y = random.uniform(-min(cell_h, spacing) / 4, min(cell_h, spacing) / 4)
                x = max(radius, min(width - radius, cx + offset_x))
                y = max(radius, min(height - radius, cy + offset_y))
                z = random.uniform(0, self.DEPTH)
                dx, dy, dz = self._random_direction()
                vx = speed * dx
                vy = speed * dy
                vz = speed * dz
                color = random.choice(self.COLORS)[1]
                scale = self.FOCAL / (self.FOCAL + z)
                screen_x = width / 2 + (x - width / 2) * scale
                screen_y = height / 2 + (y - height / 2) * scale
                screen_radius = radius * scale
                item = self.canvas.create_oval(screen_x - screen_radius, screen_y - screen_radius,
                                               screen_x + screen_radius, screen_y + screen_radius,
                                               fill=color, outline='')
                hx = screen_x - screen_radius * self.HIGHLIGHT_OFFSET_RATIO
                hy = screen_y - screen_radius * self.HIGHLIGHT_OFFSET_RATIO
                hrx = screen_radius * self.HIGHLIGHT_RADIUS_X_RATIO
                hry = screen_radius * self.HIGHLIGHT_RADIUS_Y_RATIO
                highlight_item = self.canvas.create_oval(hx - hrx, hy - hry,
                                                         hx + hrx, hy + hry,
                                                         fill='white', outline='')
                self.molecules.append(Molecule(x, y, z, vx, vy, vz, radius, item, highlight_item))
                placed += 1
            if placed >= count:
                break

    def _random_direction(self):
        z = random.uniform(-1, 1)
        phi = random.uniform(0, 2 * math.pi)
        r = math.sqrt(1 - z * z)
        return r * math.cos(phi), r * math.sin(phi), z

    def toggle_pause(self):
        if not self.running:
            return
        self.paused = not self.paused
        self.pause_button.config(text="Продолжить" if self.paused else "Пауза")

    def clear_simulation(self):
        self.stop_animation()
        self.clear_canvas()
        self.molecules = []
        self.start_button.config(text="Запустить")
        self.pause_button.config(text="Пауза", state=tk.DISABLED)

    def stop_animation(self):
        self.running = False
        self.paused = False

    def clear_canvas(self):
        self.canvas.delete("all")

    def _animate(self):
        if self.running and not self.paused:
            self._update_simulation()
        self.after_id = self.after(16, self._animate)

    def _update_simulation(self):
        width = self.canvas.winfo_width()
        height = self.canvas.winfo_height()
        if width < 20 or height < 20:
            return
        depth = self.DEPTH
        for mol in self.molecules:
            mol.x += mol.vx
            mol.y += mol.vy
            mol.z += mol.vz
            if mol.x - mol.radius < 0:
                mol.x = mol.radius
                mol.vx = -mol.vx
            elif mol.x + mol.radius > width:
                mol.x = width - mol.radius
                mol.vx = -mol.vx
            if mol.y - mol.radius < 0:
                mol.y = mol.radius
                mol.vy = -mol.vy
            elif mol.y + mol.radius > height:
                mol.y = height - mol.radius
                mol.vy = -mol.vy
            if mol.z - mol.radius < 0:
                mol.z = mol.radius
                mol.vz = -mol.vz
            elif mol.z + mol.radius > depth:
                mol.z = depth - mol.radius
                mol.vz = -mol.vz
        n = len(self.molecules)
        for i in range(n):
            mi = self.molecules[i]
            for j in range(i + 1, n):
                mj = self.molecules[j]
                dx = mi.x - mj.x
                dy = mi.y - mj.y
                dz = mi.z - mj.z
                dist_sq = dx * dx + dy * dy + dz * dz
                min_dist = mi.radius + mj.radius
                if dist_sq < min_dist * min_dist:
                    if dist_sq < 1e-12:
                        dx, dy, dz = self._random_direction()
                        dist = min_dist
                        dist_sq = dist * dist
                    else:
                        dist = math.sqrt(dist_sq)
                    nx = dx / dist
                    ny = dy / dist
                    nz = dz / dist
                    dvx = mi.vx - mj.vx
                    dvy = mi.vy - mj.vy
                    dvz = mi.vz - mj.vz
                    dvn = dvx * nx + dvy * ny + dvz * nz
                    if dvn > 0:
                        mi.vx -= dvn * nx
                        mi.vy -= dvn * ny
                        mi.vz -= dvn * nz
                        mj.vx += dvn * nx
                        mj.vy += dvn * ny
                        mj.vz += dvn * nz
                    overlap = min_dist - dist
                    mi.x += nx * overlap / 2
                    mi.y += ny * overlap / 2
                    mi.z += nz * overlap / 2
                    mj.x -= nx * overlap / 2
                    mj.y -= ny * overlap / 2
                    mj.z -= nz * overlap / 2
        for mol in self.molecules:
            if mol.x - mol.radius < 0:
                mol.x = mol.radius
                mol.vx = abs(mol.vx)
            elif mol.x + mol.radius > width:
                mol.x = width - mol.radius
                mol.vx = -abs(mol.vx)
            if mol.y - mol.radius < 0:
                mol.y = mol.radius
                mol.vy = abs(mol.vy)
            elif mol.y + mol.radius > height:
                mol.y = height - mol.radius
                mol.vy = -abs(mol.vy)
            if mol.z - mol.radius < 0:
                mol.z = mol.radius
                mol.vz = abs(mol.vz)
            elif mol.z + mol.radius > depth:
                mol.z = depth - mol.radius
                mol.vz = -abs(mol.vz)
        for mol in self.molecules:
            scale = self.FOCAL / (self.FOCAL + mol.z)
            screen_x = width / 2 + (mol.x - width / 2) * scale
            screen_y = height / 2 + (mol.y - height / 2) * scale
            screen_radius = mol.radius * scale
            self.canvas.coords(mol.item, screen_x - screen_radius, screen_y - screen_radius,
                               screen_x + screen_radius, screen_y + screen_radius)
            hx = screen_x - screen_radius * self.HIGHLIGHT_OFFSET_RATIO
            hy = screen_y - screen_radius * self.HIGHLIGHT_OFFSET_RATIO
            hrx = screen_radius * self.HIGHLIGHT_RADIUS_X_RATIO
            hry = screen_radius * self.HIGHLIGHT_RADIUS_Y_RATIO
            self.canvas.coords(mol.highlight_item, hx - hrx, hy - hry,
                               hx + hrx, hy + hry)

    def _on_close(self):
        self.stop_animation()
        if self.after_id is not None:
            self.after_cancel(self.after_id)
        self.destroy()

if __name__ == "__main__":
    app = GasApp()
    app.mainloop()