"""
Таймлайн событий для PIR_sensor_AM312: наглядная визуализация движения для демонстрации детям.
Читает обычный текстовый вывод прошивки из Serial Monitor (main.cpp) и рисует ленту времени:
закрашенные участки — периоды, когда датчик считал, что перед ним есть движение.

Запуск:
    pip install -r requirements.txt
    python3 timeline_view.py [/dev/ttyUSB0]

Хорошо показывает hold time (main.cpp, kHoldTimeMs) — почему полоса не гаснет мгновенно при
первом же кратковременном спаде сигнала, а держится ещё некоторое время после последнего фронта
движения (см. THEORY.md, раздел 4 "Логика удержания статуса").
"""
import re
import sys
import time
from collections import deque

import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.patches import Rectangle

PORT = sys.argv[1] if len(sys.argv) > 1 else "/dev/ttyUSB0"
BAUD = 115200  # должно совпадать с Serial.begin() в main.cpp

WINDOW_SECONDS = 30  # сколько последних секунд показывать на ленте

# Строки, которые печатает main.cpp:
#   [MOTION] движение обнаружено
#   [MOTION] движение обнаружено прогрев
#   [IDLE]   покой  предыдущее=2150 мс
MOTION_RE = re.compile(r"\[MOTION\]")
IDLE_RE = re.compile(r"\[IDLE\]")
WARMUP_RE = re.compile(r"прогрев")


def open_port(port: str) -> "serial.Serial":
    import serial
    try:
        return serial.Serial(port, BAUD, timeout=0.1)
    except serial.SerialException as e:
        print(f"Не удалось открыть {port}: {e}")
        print("Проверьте порт (ls /dev/ttyUSB*) и что Serial Monitor прошивки закрыт.")
        sys.exit(1)


def read_lines(ser) -> list[str]:
    if ser.in_waiting == 0:
        return []
    raw = ser.read(ser.in_waiting).decode("utf-8", errors="ignore")
    return [line.strip() for line in raw.split("\n") if line.strip()]


def main():
    ser = open_port(PORT)
    print(f"Подключено к {PORT} @ {BAUD}.")
    print("Если лента не оживает — проверьте, что Serial Monitor прошивки закрыт.")

    fig, ax = plt.subplots(figsize=(10, 3))
    fig.suptitle("PIR AM312 — таймлайн движения")
    fig.patch.set_facecolor("#06210f")
    ax.set_facecolor("#06210f")
    ax.set_xlim(-WINDOW_SECONDS, 0)
    ax.set_ylim(0, 1)
    ax.set_yticks([])
    ax.set_xlabel("секунд назад", color="#5fff8f")
    ax.tick_params(colors="#5fff8f")
    for spine in ax.spines.values():
        spine.set_color("#5fff8f")
    ax.grid(axis="x", color="#5fff8f", alpha=0.2)

    status_text = ax.text(0.02, 0.5, "Ожидание данных...", transform=ax.transAxes,
                           ha="left", va="center", fontsize=14, color="#5fff8f")

    # События: (t_start_monotonic, t_end_monotonic_or_None)
    events: deque[list] = deque()
    state = {"active": False, "start": None}
    start_time = time.monotonic()

    def update(_frame):
        now = time.monotonic()
        for line in read_lines(ser):
            warm = " (прогрев)" if WARMUP_RE.search(line) else ""
            if MOTION_RE.search(line) and not state["active"]:
                state["active"] = True
                state["start"] = now
                events.append([now, None])
                status_text.set_text(f"ДВИЖЕНИЕ{warm}")
                status_text.set_color("#ff3b3b")
            elif IDLE_RE.search(line) and state["active"]:
                state["active"] = False
                if events:
                    events[-1][1] = now
                status_text.set_text(f"покой{warm}")
                status_text.set_color("#5fff8f")

        # Убираем события старше окна показа.
        while events and events[0][0] < now - WINDOW_SECONDS - 5:
            events.popleft()

        ax.patches.clear()
        for t_start, t_end in events:
            end = t_end if t_end is not None else now
            ax.add_patch(Rectangle((t_start - now, 0), max(end - t_start, 0.05), 1,
                                    color="#ff3b3b", alpha=0.8))

        return [status_text, *ax.patches]

    ani = FuncAnimation(fig, update, interval=150, blit=False, cache_frame_data=False)
    plt.tight_layout()
    plt.show()


if __name__ == "__main__":
    main()
