"""
Живая визуализация качества радиосвязи nRF24L01+: наглядно показывает, как
падает доля успешных Auto-ACK при удалении/экранировании антенны.
Читает обычный текстовый вывод прошивки из Serial Monitor (main.cpp).

Запуск (на стороне ПЕРЕДАТЧИКА):
    pip install -r requirements.txt
    python3 link_quality_view.py [/dev/ttyUSB0] --role tx

Запуск (на стороне ПРИЁМНИКА):
    python3 link_quality_view.py [/dev/ttyUSB0] --role rx

Формат строк совпадает с main.cpp:
  TX: Seq:12,AckRate:87.5,Sent:120,Acked:105,Ack:1
  RX: Seq:12,DelayMs:8,Value:73.21

См. THEORY.md, раздел 3 — почему AckRate не то же самое, что RSSI.
"""
import argparse
import re
import sys
import time
from collections import deque

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

DEFAULT_PORT = "/dev/ttyUSB0"
BAUD = 115200  # должно совпадать с Serial.begin() в main.cpp

WINDOW_SECONDS = 30

TX_RE = re.compile(r"Seq:(\d+),AckRate:([\d.]+),Sent:(\d+),Acked:(\d+),Ack:([01])")
RX_RE = re.compile(r"Seq:(\d+),DelayMs:(\d+),Value:([\d.]+)")


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 style_axes(ax, xlabel, ylabel):
    ax.set_facecolor("#06210f")
    ax.set_xlabel(xlabel, color="#5fff8f")
    ax.set_ylabel(ylabel, color="#5fff8f")
    ax.tick_params(colors="#5fff8f")
    for spine in ax.spines.values():
        spine.set_color("#5fff8f")
    ax.grid(color="#5fff8f", alpha=0.2)


def run_tx(ser):
    fig, ax = plt.subplots(figsize=(10, 4))
    fig.suptitle("nRF24L01+ — доля успешных Auto-ACK (не RSSI!)")
    fig.patch.set_facecolor("#06210f")
    style_axes(ax, "секунд назад", "AckRate, %")
    ax.set_xlim(-WINDOW_SECONDS, 0)
    ax.set_ylim(0, 105)
    ax.axhline(50, color="#5fff8f", alpha=0.3, linestyle="--", linewidth=1)

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

    points: deque[tuple[float, float]] = deque()

    def update(_frame):
        now = time.monotonic()
        for raw_line in read_lines(ser):
            match = TX_RE.search(raw_line)
            if not match:
                continue
            _seq, ack_rate, sent, acked, _ack = match.groups()
            ack_rate = float(ack_rate)
            points.append((now, ack_rate))

            if ack_rate >= 80:
                color = "#5fff8f"
            elif ack_rate >= 40:
                color = "#ffd23b"
            else:
                color = "#ff3b3b"
            status_text.set_text(f"AckRate: {ack_rate:.1f}%  (отправлено {sent}, подтверждено {acked})")
            status_text.set_color(color)

        while points and points[0][0] < now - WINDOW_SECONDS - 2:
            points.popleft()

        if points:
            xs = [t - now for t, _ in points]
            ys = [v for _, v in points]
            line.set_data(xs, ys)

        return [line, status_text]

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


def run_rx(ser):
    fig, (ax_value, ax_delay) = plt.subplots(2, 1, figsize=(10, 6))
    fig.suptitle("nRF24L01+ — принятая телеметрия")
    fig.patch.set_facecolor("#06210f")
    style_axes(ax_value, "секунд назад", "Value")
    style_axes(ax_delay, "секунд назад", "Задержка, мс (оценочно)")
    ax_value.set_xlim(-WINDOW_SECONDS, 0)
    ax_delay.set_xlim(-WINDOW_SECONDS, 0)

    value_line, = ax_value.plot([], [], color="#5fff8f", linewidth=2)
    delay_line, = ax_delay.plot([], [], color="#3bd6ff", linewidth=2)
    status_text = fig.text(0.5, 0.02, "Ожидание пакетов...", ha="center", color="#5fff8f", fontsize=11)

    points: deque[tuple[float, float, float]] = deque()

    def update(_frame):
        now = time.monotonic()
        for raw_line in read_lines(ser):
            match = RX_RE.search(raw_line)
            if not match:
                continue
            seq, delay_ms, value = match.groups()
            points.append((now, float(delay_ms), float(value)))
            status_text.set_text(f"Seq:{seq}  Задержка:{delay_ms} мс  Value:{value}")

        while points and points[0][0] < now - WINDOW_SECONDS - 2:
            points.popleft()

        if points:
            xs = [t - now for t, _, _ in points]
            delays = [d for _, d, _ in points]
            values = [v for _, _, v in points]
            value_line.set_data(xs, values)
            delay_line.set_data(xs, delays)
            ax_value.set_ylim(min(values) - 5, max(values) + 5)
            ax_delay.set_ylim(0, max(delays) + 5)

        return [value_line, delay_line, status_text]

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


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("port", nargs="?", default=DEFAULT_PORT)
    parser.add_argument("--role", choices=("tx", "rx"), required=True,
                         help="какая плата подключена к этому порту: tx (передатчик) или rx (приёмник)")
    args = parser.parse_args()

    ser = open_port(args.port)
    print(f"Подключено к {args.port} @ {BAUD}, роль: {args.role}")

    if args.role == "tx":
        run_tx(ser)
    else:
        run_rx(ser)


if __name__ == "__main__":
    main()
