"""
Живой график proximity для APDS-9960: наглядная визуализация приближения для демонстрации детям.
Читает обычный текстовый вывод прошивки из Serial Monitor (main.cpp) и рисует график
сырого значения PDATA (0-255) во времени вместе с текущими порогами калибровки.

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

Хорошо показывает, зачем нужна калибровка (main.cpp, THRESHOLD_NEAR/CLOSE/VERY_CLOSE) —
видно, что сырые значения "прыгают" по всему диапазону экземпляра датчика, и линии порогов
на графике сразу можно сверить с тем, где реально проходит рука (см. THEORY.md, раздел 5
"Калибровка: почему пороги — не часть драйвера").
"""
import re
import sys
from collections import deque

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

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

WINDOW_POINTS = 150  # сколько последних отсчётов показывать на графике

# Строка, которую печатает main.cpp в loop():
#   Prox: 87 | ##########          [ NEAR ]
PROX_RE = re.compile(r"Prox:\s*(\d+)\s*\|.*\[\s*(\w[\w\s]*\w|\w)\s*\]")

# Пороги калибровки — держим в синхроне со значениями в main.cpp вручную,
# драйвер их не публикует по I2C (это не показания датчика, а настройка стенда).
THRESHOLD_NEAR = 30
THRESHOLD_CLOSE = 60
THRESHOLD_VERY_CLOSE = 90

STATUS_COLORS = {
    "far": "#5fff8f",
    "NEAR": "#ffd23b",
    "CLOSE": "#ff9d3b",
    "VERY CLOSE": "#ff3b3b",
}


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, 4))
    fig.suptitle("APDS-9960 — сырое значение proximity (PDATA)")
    fig.patch.set_facecolor("#06210f")
    ax.set_facecolor("#06210f")
    ax.set_xlim(0, WINDOW_POINTS)
    ax.set_ylim(0, 255)
    ax.set_xlabel("последние отсчёты", color="#5fff8f")
    ax.set_ylabel("PDATA (0-255)", color="#5fff8f")
    ax.tick_params(colors="#5fff8f")
    for spine in ax.spines.values():
        spine.set_color("#5fff8f")
    ax.grid(color="#5fff8f", alpha=0.2)

    for value, label in ((THRESHOLD_NEAR, "NEAR"), (THRESHOLD_CLOSE, "CLOSE"),
                          (THRESHOLD_VERY_CLOSE, "VERY CLOSE")):
        ax.axhline(value, color="#5fff8f", alpha=0.4, linestyle="--", linewidth=1)
        ax.text(WINDOW_POINTS, value, f" {label}", color="#5fff8f", va="center", fontsize=8)

    line, = ax.plot([], [], color="#5fff8f", linewidth=2)
    status_text = ax.text(0.02, 0.95, "Ожидание данных...", transform=ax.transAxes,
                           ha="left", va="top", fontsize=14, color="#5fff8f")

    values: deque[int] = deque(maxlen=WINDOW_POINTS)

    def update(_frame):
        for raw_line in read_lines(ser):
            match = PROX_RE.search(raw_line)
            if not match:
                if raw_line.startswith("[!]") or raw_line.startswith("[OK]"):
                    status_text.set_text(raw_line)
                continue

            prox = int(match.group(1))
            status = match.group(2).strip()
            values.append(prox)

            status_text.set_text(f"Prox: {prox}  [{status}]")
            status_text.set_color(STATUS_COLORS.get(status, "#5fff8f"))

        if values:
            line.set_data(range(len(values)), list(values))

        return [line, status_text]

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


if __name__ == "__main__":
    main()
