import math
import tkinter as tk
from tkinter import messagebox

BG = "#080e15"
PANEL = "#111b26"
DEEP = "#0b131c"
EDGE = "#24374b"
TXT = "#dce8f5"
DIM = "#7e96ad"
YEL = "#ffd54f"
CYAN = "#2fd3ff"
ORANGE = "#ff7a45"
RED = "#ff4d4d"
GREEN = "#57ff9f"
METAL = "#42556a"
BORE = "#04070a"
PIST = "#e3ecf6"

CX = 360.0
DECK = 148.0
GAP = 24.0
PIST_H = 66.0
ROD_L = 132.0
CRANK_R = 58.0
CRANK_Y = DECK + GAP + PIST_H / 2.0 + ROD_L + CRANK_R
MAX_DEPTH = 30.0
VMIN = 1.0 / 9.0
VMAX = 1.0 + VMIN
PHASE_FAULT = -70.0

STROKES = ("ВПУСК", "СЖАТИЕ", "РАБОЧИЙ ХОД", "ВЫПУСК")
STROKE_COL = (CYAN, "#a8c0d8", ORANGE, "#c9903a")

GW, GH = 712, 330
GX0, GX1, GY0, GY1 = 66.0, 700.0, 36.0, 258.0
PMAX = 70.0


def piston_offset(th):
    a = math.radians(th % 720.0)
    return ROD_L + CRANK_R - (CRANK_R * math.cos(a) + math.sqrt(ROD_L * ROD_L - (CRANK_R * math.sin(a)) ** 2))


def gx(th):
    return GX0 + (th / 720.0) * (GX1 - GX0)


def gy(p):
    return GY1 - (min(max(p, 0.0), PMAX) / PMAX) * (GY1 - GY0)


class EngineApp(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Четырёхтактный ДВС — интерактивная учебная модель")
        self.configure(bg=BG)
        self.geometry("1500x950")
        self.resizable(False, False)

        self.rpm = tk.DoubleVar(value=900.0)
        self.adv = tk.DoubleVar(value=25.0)
        self.ivo = tk.DoubleVar(value=10.0)
        self.ivc = tk.DoubleVar(value=45.0)
        self.evo = tk.DoubleVar(value=50.0)
        self.evc = tk.DoubleVar(value=10.0)
        self.fault = tk.BooleanVar(value=False)
        self.paused = tk.BooleanVar(value=False)

        self.theta = 0.0
        self.damaged = False
        self.collided = False
        self.col_frames = 0
        self.dirty = True
        self.err_shown = False
        self.border_state = None

        self.build()
        self.after(16, self.tick)

    def report_callback_exception(self, exc, val, tb):
        messagebox.showerror("Ошибка", f"{exc.__name__}: {val}")

    def build(self):
        tk.Label(self, text="ЧЕТЫРЁХТАКТНЫЙ ДВИГАТЕЛЬ ВНУТРЕННЕГО СГОРАНИЯ", bg=BG, fg=YEL,
                 font=("Segoe UI", 19, "bold")).pack(anchor="w", padx=16, pady=(10, 0))
        tk.Label(self,
                 text="учебный плакат-модель  •  такты, фазы газораспределения, опережение зажигания  •  визуальное замедление ×20",
                 bg=BG, fg=DIM, font=("Segoe UI", 10)).pack(anchor="w", padx=18, pady=(0, 6))

        body = tk.Frame(self, bg=BG)
        body.pack(padx=16, pady=(0, 12))

        self.canv = tk.Canvas(body, width=720, height=830, bg=DEEP, highlightthickness=2,
                              highlightbackground=EDGE)
        self.canv.pack(side="left")

        side = tk.Frame(body, bg=BG, width=724, height=830)
        side.pack(side="left", padx=(16, 0))
        side.pack_propagate(False)

        self.build_tools(side)
        self.build_sliders(side)
        self.build_graph(side)

    def build_tools(self, parent):
        bar = tk.Frame(parent, bg=BG)
        bar.pack(fill="x")
        self.btn_pause = tk.Button(bar, text="ПАУЗА ‖", command=self.toggle_pause, bg=YEL, fg="#04121a",
                                   activebackground="#ffffff", activeforeground="#04121a", bd=0,
                                   font=("Segoe UI", 11, "bold"), padx=16, pady=9, cursor="hand2")
        self.btn_pause.pack(side="left")
        tk.Button(bar, text="ШАГ ВПЕРЁД ▶", command=self.step_forward, bg=CYAN, fg="#04121a",
                  activebackground="#ffffff", activeforeground="#04121a", bd=0,
                  font=("Segoe UI", 11, "bold"), padx=16, pady=9, cursor="hand2").pack(side="left", padx=8)
        tk.Button(bar, text="СБРОС", command=self.reset, bg="#6d8199", fg="#04121a",
                  activebackground="#ffffff", activeforeground="#04121a", bd=0,
                  font=("Segoe UI", 11, "bold"), padx=16, pady=9, cursor="hand2").pack(side="left")
        tk.Checkbutton(bar, text="РЕЖИМ «НЕИСПРАВНОСТЬ»", variable=self.fault, command=self.on_fault,
                       bg=PANEL, fg=RED, selectcolor=DEEP, activebackground=PANEL, activeforeground=RED,
                       bd=0, highlightthickness=1, highlightbackground=EDGE,
                       font=("Segoe UI", 10, "bold"), padx=10, pady=6).pack(side="left", padx=10)

    def build_sliders(self, parent):
        grid = tk.Frame(parent, bg=BG)
        grid.pack(fill="x", pady=8)
        grid.columnconfigure(0, weight=1)
        grid.columnconfigure(1, weight=1)
        self.make_slider(grid, "Обороты, об/мин", self.rpm, 300, 4000, 50, "{:.0f}", 0, 0)
        self.make_slider(grid, "Опережение зажигания, ° до ВМТ", self.adv, 0, 45, 1, "{:.0f}", 0, 1)
        self.make_slider(grid, "Впуск: открытие, ° до ВМТ", self.ivo, 0, 60, 1, "{:.0f}", 1, 0)
        self.make_slider(grid, "Впуск: закрытие, ° после НМТ", self.ivc, 10, 90, 1, "{:.0f}", 1, 1)
        self.make_slider(grid, "Выпуск: открытие, ° до НМТ", self.evo, 10, 90, 1, "{:.0f}", 2, 0)
        self.make_slider(grid, "Выпуск: закрытие, ° после ВМТ", self.evc, 0, 40, 1, "{:.0f}", 2, 1)

    def make_slider(self, parent, label, var, lo, hi, res, fmt, row, col):
        cell = tk.Frame(parent, bg=PANEL, highlightthickness=1, highlightbackground=EDGE)
        cell.grid(row=row, column=col, sticky="nsew", padx=5, pady=5)
        head = tk.Frame(cell, bg=PANEL)
        head.pack(fill="x", padx=10, pady=(6, 0))
        tk.Label(head, text=label, bg=PANEL, fg=TXT, font=("Segoe UI", 10)).pack(side="left")
        vlab = tk.Label(head, text=fmt(float(var.get())), bg=PANEL, fg=YEL,
                        font=("Consolas", 12, "bold"))
        vlab.pack(side="right")
        sc = tk.Scale(cell, from_=lo, to=hi, resolution=res, orient="horizontal", variable=var,
                      showvalue=False, bg="#20303f", fg=TXT, troughcolor=DEEP, activebackground=CYAN,
                      highlightthickness=0, bd=0, sliderrelief="raised", length=300, width=14,
                      sliderlength=26, command=lambda v: self.on_slider(v, vlab, fmt))
        sc.pack(fill="x", padx=10, pady=(2, 8))
        return sc

    def on_slider(self, value, lab, fmt):
        try:
            lab.config(text=fmt(float(value)))
        except ValueError:
            lab.config(text=str(value))
        self.dirty = True

    def build_graph(self, parent):
        f = tk.Frame(parent, bg=BG)
        f.pack(fill="x", pady=(6, 0))
        tk.Label(f, text="ИНДИКАТОРНАЯ ДИАГРАММА — ДАВЛЕНИЕ В ЦИЛИНДРЕ", bg=BG, fg=YEL,
                 font=("Segoe UI", 11, "bold")).pack(anchor="w")
        self.graph = tk.Canvas(f, width=GW, height=GH, bg=PANEL, highlightthickness=2,
                               highlightbackground=EDGE)
        self.graph.pack(pady=(2, 0))
        self.draw_graph_static()
        tk.Label(f,
                 text="Оранжевая кривая — давление газов в цилиндре по углу поворота коленвала. "
                      "Жёлтый пунктир — момент искры. При сбое фаз ГРМ наполнение цилиндра падает, "
                      "кривая проседает, мощность снижается.",
                 bg=BG, fg=DIM, font=("Segoe UI", 9), justify="left", wraplength=712).pack(anchor="w", pady=(4, 0))

    def draw_graph_static(self):
        g = self.graph
        g.delete("all")
        bands = ((0, 180, "#0d2633"), (180, 360, "#111d28"), (360, 540, "#2a1509"), (540, 720, "#1d1a10"))
        for a, b, col in bands:
            g.create_rectangle(gx(a), GY0, gx(b), GY1, fill=col, outline="")
        for p in range(0, 71, 10):
            y = gy(p)
            g.create_line(GX0, y, GX1, y, fill="#3d5570" if p == 0 else "#1d2c3b")
            g.create_text(GX0 - 8, y, text=str(p), anchor="e", fill=DIM, font=("Segoe UI", 9))
        for a in (0, 180, 360, 540, 720):
            g.create_line(gx(a), GY0, gx(a), GY1, fill="#2c3f52", dash=(3, 3))
        names = (("ВПУСК", 0, 180), ("СЖАТИЕ", 180, 360), ("РАБОЧИЙ ХОД", 360, 540), ("ВЫПУСК", 540, 720))
        for nm, a, b in names:
            g.create_text((gx(a) + gx(b)) / 2.0, GY1 + 20, text=nm, fill=DIM, font=("Segoe UI", 9, "bold"))
        g.create_text(GX0 - 46, (GY0 + GY1) / 2.0, text="бар", angle=90, fill=TXT, font=("Segoe UI", 10, "bold"))
        g.create_text((GX0 + GX1) / 2.0, GY1 + 46, text="угол поворота коленвала, град.",
                      fill=TXT, font=("Segoe UI", 10))

    def valve_lift(self, th, op, span):
        d = (th - op) % 720.0
        if 0.0 <= d <= span and span > 0.0:
            return math.sin(math.pi * d / span)
        return 0.0

    def s_rel(self, th):
        return piston_offset(th) / (2.0 * CRANK_R)

    def pressure(self, th, power):
        t = th % 720.0
        v = VMIN + self.s_rel(t)
        pc = 0.9 * (VMAX / v) ** 1.32
        ign = 360.0 - self.adv.get()
        if t < 180.0:
            return 0.92 - 0.06 * self.s_rel(t)
        if t < 540.0:
            if t >= ign:
                x = min(1.0, (t - ign) / 40.0)
                x = x * x * (3.0 - 2.0 * x)
                return pc * (1.0 + 3.0 * x * power)
            return pc
        p540 = 0.9 * (VMAX / (VMIN + self.s_rel(540.0))) ** 1.32 * (1.0 + 3.0 * power)
        return 1.15 + (p540 - 1.15) * math.exp(-(t - 540.0) / 16.0)

    def timings(self):
        shift = PHASE_FAULT if self.fault.get() else 0.0
        iv_open = -self.ivo.get() + shift
        iv_span = 180.0 + self.ivc.get() + self.ivo.get()
        ev_open = 540.0 - self.evo.get() + shift
        ev_span = 180.0 + self.evc.get() + self.evo.get()
        return iv_open, iv_span, ev_open, ev_span

    def toggle_pause(self):
        self.paused.set(not self.paused.get())
        self.btn_pause.config(text="ПУСК ▶" if self.paused.get() else "ПАУЗА ‖")

    def step_forward(self):
        self.paused.set(True)
        self.btn_pause.config(text="ПУСК ▶")
        self.theta = (self.theta + 15.0) % 720.0

    def reset(self):
        self.theta = 0.0
        self.damaged = False
        self.collided = False
        self.col_frames = 0
        self.dirty = True

    def on_fault(self):
        self.damaged = False
        self.col_frames = 0
        self.dirty = True

    def update_state(self):
        th = self.theta
        ptop = DECK + GAP + piston_offset(th)
        iv_open, iv_span, ev_open, ev_span = self.timings()
        li = self.valve_lift(th, iv_open, iv_span)
        le = self.valve_lift(th, ev_open, ev_span)
        hit = (DECK + 6.0 + li * MAX_DEPTH > ptop) or (DECK + 6.0 + le * MAX_DEPTH > ptop)
        if hit:
            self.collided = True
            self.col_frames = 240
            if not self.damaged:
                self.damaged = True
                self.dirty = True
        else:
            self.collided = False
            if self.col_frames > 0:
                self.col_frames -= 1
            elif self.damaged:
                self.damaged = False
                self.dirty = True
        want = RED if self.fault.get() else EDGE
        if want != self.border_state:
            self.border_state = want
            self.canv.config(highlightbackground=want)
            self.graph.config(highlightbackground=want)

    def recalc_trace(self):
        self.graph.delete("curve")
        power = 0.32 if self.damaged else 1.0
        pts = []
        for i in range(0, 721, 2):
            pts.append(gx(i))
            pts.append(gy(self.pressure(i, power)))
        self.graph.create_line(*pts, fill=ORANGE, width=3, tags="curve", joinstyle="round")
        ign = 360.0 - self.adv.get()
        self.graph.create_line(gx(ign), GY0, gx(ign), GY1, fill=YEL, dash=(5, 4), width=2, tags="curve")
        self.graph.create_text(gx(ign), GY0 - 14, text=f"зажигание {self.adv.get():.0f}°",
                               fill=YEL, font=("Segoe UI", 9, "bold"), tags="curve")

    def draw_marker(self):
        self.graph.delete("mk")
        th = self.theta
        power = 0.32 if self.damaged else 1.0
        p = self.pressure(th, power)
        x = gx(th)
        y = gy(p)
        self.graph.create_line(x, GY0, x, GY1, fill="#ffffff", width=1, dash=(2, 3), tags="mk")
        self.graph.create_oval(x - 6, y - 6, x + 6, y + 6, fill="#ffffff", outline=ORANGE, width=2,
                               tags="mk")
        if x > GX1 - 90:
            self.graph.create_text(x - 9, y - 14, text=f"{p:.1f} бар", anchor="e", fill="#ffffff",
                                   font=("Segoe UI", 9, "bold"), tags="mk")
        else:
            self.graph.create_text(x + 9, y - 14, text=f"{p:.1f} бар", anchor="w", fill="#ffffff",
                                   font=("Segoe UI", 9, "bold"), tags="mk")

    def draw_valve(self, c, x, lv, col):
        dy = DECK + 6.0 + lv * MAX_DEPTH
        for i in range(4):
            yy = 74 + i * 9
            c.create_line(x - 15, yy, x + 15, yy, fill="#6d8199", width=2)
        c.create_line(x, 62, x, dy - 10, fill="#8ea3b8", width=9, capstyle="round")
        c.create_polygon(x - 25, dy - 12, x + 25, dy - 12, x + 17, dy, x - 17, dy,
                         fill=col, outline="#0a1018", width=2)

    def draw_engine(self):
        c = self.canv
        c.delete("all")
        th = self.theta
        s = piston_offset(th)
        ptop = DECK + GAP + s
        pin_y = ptop + PIST_H / 2.0
        a = math.radians(th)
        cpx = CX + CRANK_R * math.sin(a)
        cpy = CRANK_Y - CRANK_R * math.cos(a)

        iv_open, iv_span, ev_open, ev_span = self.timings()
        li = self.valve_lift(th, iv_open, iv_span)
        le = self.valve_lift(th, ev_open, ev_span)
        ign = 360.0 - self.adv.get()

        c.create_rectangle(CX - 192, 326, CX + 192, 594, fill="#151e29", outline=EDGE, width=2)
        c.create_rectangle(CX - 80, DECK, CX + 80, 384, fill=BORE, outline="")
        c.create_rectangle(CX - 108, DECK, CX - 80, 384, fill=METAL, outline=EDGE)
        c.create_rectangle(CX + 80, DECK, CX + 108, 384, fill=METAL, outline=EDGE)
        c.create_rectangle(CX - 176, 56, CX + 176, DECK, fill=METAL, outline=EDGE, width=2)
        c.create_rectangle(CX - 176, 104, CX - 52, 134, fill="#0a121a", outline=EDGE)
        c.create_rectangle(CX + 52, 104, CX + 176, 134, fill="#0a121a", outline=EDGE)

        if ptop - 6 > DECK + 6:
            if th < 180.0:
                col, st = "#1e6f8c", "gray25"
            elif th < 360.0:
                col, st = "#2b5f80", "gray50"
            elif th < 540.0:
                col, st = "#ff6a1f", "gray50"
            else:
                col, st = "#6a6a6a", "gray25"
            c.create_rectangle(CX - 78, DECK + 6, CX + 78, ptop - 6, fill=col, stipple=st, outline="")

        if ign - 3 <= th <= ign + 8:
            c.create_oval(CX - 26, DECK - 20, CX + 26, DECK + 22, fill="#ffe066", outline="")
            for dx, dy in ((-40, -20), (40, -20), (-48, 4), (48, 4)):
                c.create_line(CX + dx * 0.6, DECK - 4 + dy * 0.6, CX + dx, DECK - 4 + dy,
                              fill="#fff2a8", width=3, arrow="last")
        elif ign < th < ign + 90 and th < 540.0:
            c.create_oval(CX - 40, DECK - 8, CX + 40, DECK + 30, fill="#ff9d3a", stipple="gray50",
                          outline="")

        if li > 0.06:
            c.create_line(CX - 170, 119, CX - 96, 119, arrow="last", fill=CYAN, width=3)
        if le > 0.06:
            c.create_line(CX + 96, 119, CX + 170, 119, arrow="last", fill=ORANGE, width=3)

        c.create_line(CX, pin_y, cpx, cpy, fill="#e0a93c", width=17, capstyle="round")
        cw_x = CX - 34 * math.sin(a)
        cw_y = CRANK_Y + 34 * math.cos(a)
        c.create_oval(cw_x - 30, cw_y - 30, cw_x + 30, cw_y + 30, fill="#38495c", outline=EDGE, width=2)
        c.create_oval(CX - 46, CRANK_Y - 46, CX + 46, CRANK_Y + 46, fill="#2b3949", outline=EDGE, width=2)
        c.create_line(CX, CRANK_Y, cpx, cpy, fill="#54697f", width=14)
        c.create_oval(CX - 12, CRANK_Y - 12, CX + 12, CRANK_Y + 12, fill="#6f859c", outline=EDGE)
        c.create_oval(cpx - 15, cpy - 15, cpx + 15, cpy + 15, fill="#c8b06a", outline="#0a1018", width=2)

        c.create_rectangle(CX - 78, ptop, CX + 78, ptop + PIST_H, fill=PIST, outline="#7f95aa", width=2)
        for i in range(3):
            yy = ptop + 9 + i * 7
            c.create_line(CX - 78, yy, CX + 78, yy, fill="#7f95aa", width=2)
        c.create_oval(CX - 11, pin_y - 11, CX + 11, pin_y + 11, fill="#9fb3c8", outline="#5b7086", width=2)

        c.create_line(CX, 30, CX, 138, fill="#8ea3b8", width=11)
        c.create_rectangle(CX - 11, 40, CX + 11, 74, fill="#b9c8d8", outline=EDGE, width=2)
        c.create_line(CX - 8, 138, CX + 8, 138, fill="#d8e4f0", width=3)

        self.draw_valve(c, CX - 46, li, CYAN if li > 0.03 else "#3d6d80")
        self.draw_valve(c, CX + 46, le, ORANGE if le > 0.03 else "#7d4a37")

        c.create_text(CX - 72, 86, text="ВПУСК. КЛАПАН", anchor="e", fill=CYAN,
                      font=("Segoe UI", 9, "bold"))
        c.create_text(CX + 72, 86, text="ВЫПУСК. КЛАПАН", anchor="w", fill=ORANGE,
                      font=("Segoe UI", 9, "bold"))
        c.create_line(CX - 74, 94, CX - 56, 108, fill=CYAN, width=2)
        c.create_line(CX + 74, 94, CX + 56, 108, fill=ORANGE, width=2)
        c.create_text(CX, 16, text="СВЕЧА ЗАЖИГАНИЯ", fill=YEL, font=("Segoe UI", 10, "bold"))

        for yy, lab in ((DECK + GAP, "ВМТ"), (DECK + GAP + 2 * CRANK_R, "НМТ")):
            c.create_line(CX - 210, yy, CX - 114, yy, fill="#3d5570", dash=(4, 4))
            c.create_text(CX - 214, yy, text=lab, anchor="e", fill=DIM, font=("Segoe UI", 9, "bold"))

        c.create_text(CX - 120, 300, text="ЦИЛИНДР (разрез)", anchor="e", fill=DIM,
                      font=("Segoe UI", 9, "bold"))
        c.create_text(CX + 120, 216, text="ПОРШЕНЬ", anchor="w", fill=DIM, font=("Segoe UI", 9, "bold"))
        c.create_line(CX + 114, 216, CX + 82, 216, fill="#3d5570", dash=(3, 3))
        c.create_text(CX + 152, 440, text="ШАТУН", anchor="w", fill=DIM, font=("Segoe UI", 9, "bold"))
        c.create_text(CX - 152, 500, text="КОЛЕНВАЛ", anchor="e", fill=DIM, font=("Segoe UI", 9, "bold"))
        c.create_text(CX - 152, 520, text="∅ об/мин", anchor="e", fill="#2c3f52", font=("Segoe UI", 8))

        if self.fault.get():
            c.create_text(CX, 316, text="СБОЙ ФАЗ ГРМ", fill=RED, font=("Segoe UI", 12, "bold"))
        if self.collided:
            c.create_rectangle(232, 216, 488, 248, fill="#2a0b0b", outline=RED, width=2)
            c.create_text(360, 232, text="КЛАПАН БЬЁТ ПО ПОРШНЮ", fill=RED,
                          font=("Segoe UI", 11, "bold"))

        c.create_rectangle(16, 606, 704, 826, fill=PANEL, outline=EDGE, width=2)
        idx = int(th // 180) % 4
        c.create_text(360, 644, text=STROKES[idx], fill=STROKE_COL[idx], font=("Segoe UI", 34, "bold"))
        c.create_text(360, 688,
                      text=f"Угол коленвала {th:6.1f}°   •   {self.rpm.get():.0f} об/мин   •   "
                           f"опережение зажигания {self.adv.get():.0f}°",
                      fill=TXT, font=("Segoe UI", 12))
        c.create_text(360, 712,
                      text=f"Впускной клапан: {'ОТКРЫТ' if li > 0.02 else 'ЗАКРЫТ'}   •   "
                           f"Выпускной клапан: {'ОТКРЫТ' if le > 0.02 else 'ЗАКРЫТ'}",
                      fill=DIM, font=("Segoe UI", 10))
        pw = 0.32 if self.damaged else 1.0
        c.create_text(360, 736, text=f"МОЩНОСТЬ  {int(round(pw * 100))} %",
                      fill=GREEN if pw > 0.9 else RED, font=("Segoe UI", 12, "bold"))
        c.create_rectangle(60, 752, 660, 774, fill="#0a1119", outline=EDGE)
        if pw > 0.02:
            c.create_rectangle(60, 752, 60 + 600 * pw, 774, fill=GREEN if pw > 0.9 else RED, outline="")

        if self.damaged:
            c.create_text(360, 800,
                          text="СБОЙ ФАЗ ГАЗОРАСПРЕДЕЛЕНИЯ: ремень ГРМ сместил фазы на 70° вперёд. "
                               "Впускной клапан открывается слишком рано и встречается с поршнем в ВМТ — "
                               "мощность падает, двигатель повреждён.",
                          fill=RED, font=("Segoe UI", 9, "bold"), width=660, justify="center")
        else:
            c.create_text(360, 800,
                          text="Фазы газораспределения в норме: клапаны открываются, когда поршень далеко "
                               "от ВМТ, зажигание опережает ВМТ на заданный угол.",
                          fill=DIM, font=("Segoe UI", 9), width=660, justify="center")

    def tick(self):
        try:
            if not self.paused.get():
                self.theta = (self.theta + self.rpm.get() / 100.0) % 720.0
            self.update_state()
            if self.dirty:
                self.recalc_trace()
                self.dirty = False
            self.draw_engine()
            self.draw_marker()
        except Exception as exc:
            if not self.err_shown:
                self.err_shown = True
                messagebox.showerror("Ошибка модели", f"{type(exc).__name__}: {exc}")
        self.after(16, self.tick)


if __name__ == "__main__":
    EngineApp().mainloop()