"""
Живой трек GPS для GpsNeo: наглядная визуализация маршрута для демонстрации детям.
Читает обычный текстовый вывод прошивки из Serial Monitor (main.cpp) и рисует
накопленный путь в метрах относительно точки старта — та же формула, что описана
в ROADMAP.md (проекция коротких расстояний), применённая к живым координатам.

Запуск (без карты, работает офлайн):
    pip install -r requirements.txt
    python3 gps_track_view.py [/dev/ttyUSB0]

Запуск с подложкой OpenStreetMap (нужен интернет и пакет contextily):
    pip install contextily
    python3 gps_track_view.py [/dev/ttyUSB0] --map

Первая валидная точка становится началом координат (0, 0) — походите с модулем
на улице с открытым видом на небо (см. GUIDE.md, раздел 5 про время до Fix) и
смотрите, как трек рисуется в реальном времени.
"""
import argparse
import math
import re
import sys
from collections import deque

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

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

# Метров на градус широты — постоянная величина (меридиан почти окружность).
METERS_PER_DEG_LAT = 111_320.0

# Строка, которую печатает main.cpp раз в 2 секунды при наличии fix:
#   Lat:48.117300,Lon:11.516700,SpeedKmh:12.3,Course:84.4,Alt:545.4,HDOP:0.9
LINE_RE = re.compile(
    r"Lat:(-?[\d.]+),Lon:(-?[\d.]+),SpeedKmh:([\d.]+),Course:([\d.]+),"
    r"Alt:(-?[\d.]+),HDOP:([\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 latlon_to_local_meters(lat: float, lon: float, lat0: float, lon0: float) -> tuple[float, float]:
    """Плоское приближение для коротких расстояний (десятки-сотни метров): достаточно
    точно, когда кривизна Земли на длине трека пренебрежимо мала — тот же порядок
    точности, что и Haversine на таких дистанциях (см. ROADMAP.md, раздел 2.1)."""
    meters_per_deg_lon = METERS_PER_DEG_LAT * math.cos(math.radians(lat0))
    dx = (lon - lon0) * meters_per_deg_lon
    dy = (lat - lat0) * METERS_PER_DEG_LAT
    return dx, dy


def try_load_basemap():
    try:
        import contextily
        return contextily
    except ImportError:
        print("[!] --map запрошен, но пакет 'contextily' не установлен.")
        print("    Установите: pip install contextily")
        print("    Продолжаю без подложки карты.")
        return None


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("port", nargs="?", default=DEFAULT_PORT)
    parser.add_argument("--map", action="store_true",
                         help="попробовать подгрузить подложку OpenStreetMap (нужен интернет)")
    args = parser.parse_args()

    ser = open_port(args.port)
    print(f"Подключено к {args.port} @ {BAUD}.")
    print("Ожидание первого GPS Fix... (см. GUIDE.md, раздел 5, если долго нет фикса)")

    contextily = try_load_basemap() if args.map else None

    fig, ax = plt.subplots(figsize=(8, 8))
    fig.suptitle("GPS NEO — трек движения")
    fig.patch.set_facecolor("#06210f")
    ax.set_facecolor("#06210f")
    ax.set_xlabel("метров на восток от старта", color="#5fff8f")
    ax.set_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)
    ax.set_aspect("equal")

    track_line, = ax.plot([], [], color="#5fff8f", linewidth=2, alpha=0.8)
    current_point, = ax.plot([], [], "o", color="#ff3b3b", markersize=10)
    status_text = fig.text(0.02, 0.02, "Ожидание fix...", color="#5fff8f", fontsize=11)

    state = {"origin": None, "map_drawn": False}
    xs: deque[float] = deque()
    ys: deque[float] = deque()

    def update(_frame):
        for raw_line in read_lines(ser):
            match = LINE_RE.search(raw_line)
            if not match:
                continue

            lat, lon, speed, course, alt, hdop = map(float, match.groups())

            if state["origin"] is None:
                state["origin"] = (lat, lon)
                print(f"Точка старта: {lat:.6f}, {lon:.6f}")

            lat0, lon0 = state["origin"]
            x, y = latlon_to_local_meters(lat, lon, lat0, lon0)
            xs.append(x)
            ys.append(y)

            status_text.set_text(
                f"Скорость: {speed:.1f} км/ч  Курс: {course:.0f}°  "
                f"Высота: {alt:.1f} м  HDOP: {hdop:.1f}"
            )

        if xs:
            track_line.set_data(xs, ys)
            current_point.set_data([xs[-1]], [ys[-1]])

            margin = 5.0
            ax.set_xlim(min(xs) - margin, max(xs) + margin)
            ax.set_ylim(min(ys) - margin, max(ys) + margin)

            if contextily is not None and not state["map_drawn"] and len(xs) > 1:
                try:
                    contextily.add_basemap(
                        ax, crs=None, source=contextily.providers.OpenStreetMap.Mapnik
                    )
                    state["map_drawn"] = True
                except Exception as e:
                    print(f"[!] Не удалось загрузить подложку карты: {e}")
                    print("    Проверьте интернет-соединение. Продолжаю без подложки.")
                    state["map_drawn"] = True  # не пытаться на каждом кадре при отсутствии сети

        return [track_line, current_point, status_text]

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


if __name__ == "__main__":
    main()
