import os
import sys
import tkinter as tk
from tkinter import filedialog as fd, messagebox, simpledialog
import pandas as pd
import matplotlib.pyplot as plt
from xml.etree import ElementTree as ET
import ttkbootstrap as ttk
from ttkbootstrap.constants import *
import threading
import time
import chardet
import serial.tools.list_ports
import subprocess
from datetime import datetime, timezone
import re
import csv
import gpxpy
import gpxpy.gpx

# --- ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ---
pvt_running = False
pvt_thread = None
pvt_data_list = []

# --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ---
def comma_to_dot(value):
    if value is None:
        return None
    try:
        return float(value.replace(',', '.'))
    except (ValueError, AttributeError):
        return None

def is_valid_lat(val):
    try:
        f = float(val)
        return -90 <= f <= 90
    except:
        return False

def is_valid_lon(val):
    try:
        f = float(val)
        return -180 <= f <= 180
    except:
        return False

def log_print(message):
    timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    log_text.insert('end', f"[{timestamp}] {message}\n")
    log_text.see('end')

def get_startupinfo():
    if sys.platform == "win32":
        startupinfo = subprocess.STARTUPINFO()
        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
        startupinfo.wShowWindow = subprocess.SW_HIDE
        return startupinfo
    return None

def fix_decimal_separator(file_path):
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()
        content = content.replace(',', '.')
        with open(file_path, 'w', encoding='utf-8') as f:
            f.write(content)
    except IOError as e:
        log_print(f"Ошибка при обработке файла: {e}")

def format_point_display(point):
    display = f"{point['name']} ({point['lat']:.6f}, {point['lon']:.6f})"
    if point.get('depth') is not None:
        display += f", глубина: {point['depth']}"
    if point.get('temperature') is not None:
        display += f", темп.: {point['temperature']}"
    return display

# --- УНИВЕРСАЛЬНЫЙ ПАРСЕР GPX ---
class UniversalGPXParser:
    @staticmethod
    def _comma_to_dot(value):
        if value is None:
            return None
        try:
            return float(value.replace(',', '.'))
        except (ValueError, AttributeError):
            return None

    @staticmethod
    def _extract_point_data(point_el, namespaces, point_type):
        try:
            lat = point_el.get('lat')
            lon = point_el.get('lon')
            if lat is None or lon is None:
                return None

            lat = UniversalGPXParser._comma_to_dot(lat)
            lon = UniversalGPXParser._comma_to_dot(lon)
            if lat is None or lon is None:
                return None

            name = point_type
            name_el = None
            for tag in ['name', 'gpx:name', '{http://www.topografix.com/GPX/1/0}name']:
                el = point_el.find(tag, namespaces)
                if el is not None:
                    name_el = el
                    break
            if name_el is not None and name_el.text:
                name = name_el.text.strip()

            depth = None
            temperature = None

            # Поиск через URI
            for uri in [
                'http://www.garmin.com/xmlschemas/GpxExtensions/v3',
                'http://www.garmin.com/xmlschemas/TrackPointExtension/v1'
            ]:
                if depth is None:
                    d_el = point_el.find(f'{{{uri}}}depth')
                    if d_el is not None and d_el.text:
                        depth = UniversalGPXParser._comma_to_dot(d_el.text)
                if temperature is None:
                    t_el = point_el.find(f'{{{uri}}}temperature')
                    if t_el is not None and t_el.text:
                        temperature = UniversalGPXParser._comma_to_dot(t_el.text)

            # Поиск через префиксы
            if depth is None:
                for tag in ['gpxx:depth', 'gpxtpx:depth']:
                    el = point_el.find(tag, namespaces)
                    if el is not None and el.text:
                        depth = UniversalGPXParser._comma_to_dot(el.text)
                        break
            if temperature is None:
                for tag in ['gpxx:temperature', 'gpxtpx:temperature']:
                    el = point_el.find(tag, namespaces)
                    if el is not None and el.text:
                        temperature = UniversalGPXParser._comma_to_dot(el.text)
                        break

            return {
                'name': name,
                'lat': lat,
                'lon': lon,
                'depth': depth,
                'temperature': temperature,
                'type': point_type
            }
        except Exception:
            return None

    @staticmethod
    def parse_gpx_file(filepath):
        try:
            with open(filepath, 'rb') as f:
                raw = f.read()
            enc = chardet.detect(raw)['encoding'] or 'utf-8'
            content = raw.decode(enc, errors='replace')

            # Замена запятых в атрибутах
            content = re.sub(r'(lat|lon)="([^"]*),([^"]*)"', r'\1="\2.\3"', content)

            # Добавление xmlns:gpxx / gpxtpx
            if '<gpxx:' in content and 'xmlns:gpxx=' not in content:
                content = content.replace('<gpx ', '<gpx xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3" ', 1)
            if '<gpxtpx:' in content and 'xmlns:gpxtpx=' not in content:
                content = content.replace('<gpx ', '<gpx xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" ', 1)

            try:
                root = ET.fromstring(content)
            except ET.ParseError:
                content = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f]', '', content)
                if '</gpx>' not in content:
                    content += '</gpx>'
                root = ET.fromstring(content)

            NAMESPACES = {}
            for key, uri in root.attrib.items():
                if key.startswith('xmlns:'):
                    NAMESPACES[key[6:]] = uri
            NAMESPACES.setdefault('gpx', 'http://www.topografix.com/GPX/1/0')
            NAMESPACES.setdefault('gpxx', 'http://www.garmin.com/xmlschemas/GpxExtensions/v3')
            NAMESPACES.setdefault('gpxtpx', 'http://www.garmin.com/xmlschemas/TrackPointExtension/v1')

            points = []
            for tag in ['trkpt', 'wpt', 'rtept']:
                for pt in root.findall(f'.//{tag}', NAMESPACES):
                    p = UniversalGPXParser._extract_point_data(pt, NAMESPACES, tag)
                    if p:
                        points.append(p)
            return points if points else []

        except Exception:
            return UniversalGPXParser._parse_gpx_fallback(content)

    @staticmethod
    def _parse_gpx_fallback(content):
        points = []
        pattern = r'<(wpt|trkpt|rtept)\s+lat="([^"]+)"\s+lon="([^"]+)"[^>]*>(.*?)</\1>'
        matches = re.findall(pattern, content, re.DOTALL)
        for point_type, lat_str, lon_str, inner in matches:
            try:
                lat = UniversalGPXParser._comma_to_dot(lat_str)
                lon = UniversalGPXParser._comma_to_dot(lon_str)
                if lat is None or lon is None:
                    continue
                name_match = re.search(r'<name>([^<]+)</name>', inner)
                name = name_match.group(1).strip() if name_match else point_type

                depth = None
                temp = None
                d_match = re.search(r'<(?:gpxx:)?depth>([^<]+)</(?:gpxx:)?depth>', inner)
                if d_match:
                    depth = UniversalGPXParser._comma_to_dot(d_match.group(1))
                t_match = re.search(r'<(?:gpxx:)?temperature>([^<]+)</(?:gpxx:)?temperature>', inner)
                if t_match:
                    temp = UniversalGPXParser._comma_to_dot(t_match.group(1))

                points.append({
                    'name': name,
                    'lat': lat,
                    'lon': lon,
                    'depth': depth,
                    'temperature': temp,
                    'type': point_type
                })
            except:
                continue
        return points

# --- ЭХОЛОТ: ОБРАБОТКА GPX ---
def parse_sonar_gpx(filepath):
    points = UniversalGPXParser.parse_gpx_file(filepath)
    if not points:
        raise ValueError("Не удалось извлечь данные из GPX-файла")
    df = pd.DataFrame(points)
    df.rename(columns={'lat': 'latitude', 'lon': 'longitude'}, inplace=True)
    df['source_file'] = os.path.basename(filepath)
    return df, len(points)

# --- РАБОТА С ЭХОЛОТОМ ЧЕРЕЗ pygarmin ---
def get_waypoints():
    selected_port = combo.get()
    if not selected_port:
        log_print("Выберите COM-порт!")
        return

    # Используем временное имя в системной папке, чтобы избежать проблем с пробелами/кириллицей
    import tempfile
    with tempfile.NamedTemporaryFile(delete=False, suffix='.gpx') as tmp:
        temp_gpx_path = tmp.name

    try:
        startupinfo = get_startupinfo()
        # Передаём путь как строку, оборачивая в кавычки
        command = ['pygarmin', '-p', selected_port, 'get-waypoints', temp_gpx_path, '-t', 'gpx']
        process = subprocess.Popen(
            command,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            startupinfo=startupinfo,
            text=False  # ← ВАЖНО: не декодируем внутри subprocess
        )
        stdout_data, stderr_data = process.communicate()

        # Кодировка вывода ошибки — OEM (cp866 на русской Windows)
        stderr_text = stderr_data.decode('cp866', errors='replace')
        if stderr_text.strip():
            log_print(f"Ошибка pygarmin: {stderr_text}")
            return

        # Считываем GPX как есть (он в UTF-8 или ASCII)
        try:
            with open(temp_gpx_path, 'r', encoding='utf-8') as f:
                gpx_content = f.read()
        except UnicodeDecodeError:
            with open(temp_gpx_path, 'r', encoding='cp1251') as f:
                gpx_content = f.read()

        # Сохраняем пользователю
        save_path = fd.asksaveasfilename(defaultextension='.gpx', filetypes=[('GPX файл', '*.gpx')])
        if save_path:
            with open(save_path, 'w', encoding='utf-8') as f:
                f.write(gpx_content)
            log_print(f"Точки выгружены: {save_path}")
        else:
            log_print("Сохранение отменено пользователем")

    except Exception as e:
        log_print(f"Критическая ошибка: {e}")
    finally:
        if os.path.exists(temp_gpx_path):
            os.unlink(temp_gpx_path)

def get_tracks():
    selected_port = combo.get()
    if not selected_port:
        log_print("Выберите COM-порт!")
        return
    output_file_path = fd.asksaveasfilename(
        defaultextension='.gpx',
        filetypes=[('GPX файл', '*.gpx'), ('CSV файл', '*.csv'), ('Текст', '*.txt')]
    )
    if not output_file_path:
        return
    startupinfo = get_startupinfo()
    command = ['pygarmin', '-p', selected_port, 'get-tracks']
    process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=startupinfo)
    stdout_data, stderr_data = process.communicate()
    stdout_data = stdout_data.decode('utf-8', errors='replace')
    stderr_data = stderr_data.decode('utf-8', errors='replace')
    if stderr_data:
        log_print(f"Ошибка выгрузки трека: {stderr_data}")
        return
    if output_file_path.endswith('.gpx'):
        with open(output_file_path, 'w', encoding='utf-8') as f:
            f.write(stdout_data)
        log_print(f"Трек сохранён в GPX: {output_file_path}")
    elif output_file_path.endswith('.csv'):
        lines = stdout_data.splitlines()
        with open(output_file_path, 'w', newline='', encoding='utf-8') as f:
            writer = csv.writer(f)
            writer.writerow(['raw_line'])
            for line in lines:
                writer.writerow([line])
        log_print(f"Трек сохранён в CSV: {output_file_path}")
    else:
        with open(output_file_path, 'w', encoding='utf-8') as f:
            f.write(stdout_data)
        log_print(f"Трек сохранён как текст: {output_file_path}")

def put_routes():
    selected_port = combo.get()
    if not selected_port:
        log_print("Выберите COM-порт!")
        return
    gpx_file_path = fd.askopenfilename(filetypes=[('GPX файл', '*.gpx'), ('XML файл', '*.xml')])
    if not gpx_file_path:
        return
    if not gpx_file_path.endswith(('.gpx', '.xml')):
        log_print("Неподдерживаемый формат файла.")
        return
    startupinfo = get_startupinfo()
    fmt = 'gpx' if gpx_file_path.endswith('.gpx') else 'xml'
    command = ['pygarmin', '-p', selected_port, 'put-routes', gpx_file_path, '-t', fmt]
    process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=startupinfo)
    stdout_data, stderr_data = process.communicate()
    stdout_data = stdout_data.decode('cp1251', errors='replace')
    stderr_data = stderr_data.decode('cp1251', errors='replace')
    if stderr_data:
        log_print(f"Ошибка загрузки маршрута: {stderr_data}")
    else:
        log_print(f"Маршрут загружен: {stdout_data}")

def put_waypoints():
    selected_port = combo.get()
    if not selected_port:
        log_print("Выберите COM-порт!")
        return
    gpx_file_path = fd.askopenfilename(filetypes=[('GPX файл', '*.gpx')])
    if not gpx_file_path:
        return
    fix_decimal_separator(gpx_file_path)
    startupinfo = get_startupinfo()
    command = ['pygarmin', '-p', selected_port, 'put-waypoints', gpx_file_path, '-t', 'gpx']
    process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=startupinfo)
    stdout_data, stderr_data = process.communicate()
    stdout_data = stdout_data.decode('cp1251', errors='replace')
    stderr_data = stderr_data.decode('cp1251', errors='replace')
    if stderr_data:
        log_print(f"Ошибка загрузки точек: {stderr_data}")
    else:
        log_print(f"Точки загружены: {stdout_data}")

def info_file():
    selected_port = combo.get()
    if not selected_port:
        log_print("Выберите COM-порт!")
        return
    startupinfo = get_startupinfo()
    command = ['pygarmin', '-p', selected_port, 'info']
    process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=startupinfo)
    stdout_data, stderr_data = process.communicate()
    stdout_data = stdout_data.decode('cp866', errors='replace')
    stderr_data = stderr_data.decode('cp866', errors='replace')
    if stderr_data:
        log_print(f"Ошибка получения информации: {stderr_data}")
    else:
        log_print(f"Информация: {stdout_data}")

def id_info():
    selected_port = combo.get()
    if not selected_port:
        log_print("Выберите COM-порт!")
        return
    startupinfo = get_startupinfo()
    command = ['pygarmin', '-p', selected_port, 'unit-id']
    process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=startupinfo)
    stdout_data, stderr_data = process.communicate()
    stdout_data = stdout_data.decode('cp866', errors='replace')
    stderr_data = stderr_data.decode('cp866', errors='replace')
    if stderr_data:
        log_print(f"Ошибка получения ID: {stderr_data}")
    else:
        log_print(f"ID устройства: {stdout_data}")

def get_position():
    selected_port = combo.get()
    if not selected_port:
        log_print("Выберите COM-порт!")
        return
    startupinfo = get_startupinfo()
    command = ['pygarmin', '-p', selected_port, 'get-position']
    process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=startupinfo)
    stdout_data, stderr_data = process.communicate()
    stdout_data = stdout_data.decode('cp866', errors='replace')
    stderr_data = stderr_data.decode('cp866', errors='replace')
    if stderr_data:
        log_print(f"Ошибка получения позиции: {stderr_data}")
        return
    try:
        match = re.search(r'lat=([-\d.]+),\s*lon=([-\d.]+)', stdout_data)
        if match:
            lat = float(match.group(1))
            lon = float(match.group(2))
            log_print(f"Позиция: Ш {lat:.6f}, Д {lon:.6f}")
        else:
            log_print("Координаты не найдены.")
    except Exception as e:
        log_print(f"Ошибка разбора координат: {e}")

# --- PVT В РЕАЛЬНОМ ВРЕМЕНИ ---
def pvt_worker():
    global pvt_running, pvt_data_list
    selected_port = combo.get()
    if not selected_port:
        root.after(0, log_print, "COM-порт не выбран!")
        return
    log_print("PVT-поток запущен...")
    pvt_data_list = []
    startupinfo = get_startupinfo()
    try:
        process = subprocess.Popen(
            ['pygarmin', '-p', selected_port, 'pvt'],
            stdout=subprocess.PIPE, stderr=subprocess.PIPE,
            text=True, encoding='utf-8', errors='replace',
            startupinfo=startupinfo
        )
        while pvt_running:
            line = process.stdout.readline()
            if not line:
                if process.poll() is not None:
                    break
                continue
            if 'D800(' in line:
                utc_time = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
                posn_match = re.search(r'posn=\[([-\d.]+),\s*([-\d.]+)\]', line)
                if not posn_match:
                    continue
                lat_rad = float(posn_match.group(1))
                lon_rad = float(posn_match.group(2))
                lat_deg = lat_rad * 57.29577951308232
                lon_deg = lon_rad * 57.29577951308232
                record = {'Lat': round(lat_deg, 6), 'Lon': round(lon_deg, 6), 'time': utc_time, 'speed': '', 'course': ''}
                pvt_data_list.append(record)
                disp = f"Lat: {record['Lat']}, Lon: {record['Lon']}, time: {record['time']}"
                root.after(0, lambda txt=disp: pvt_textbox.insert('end', txt + '\n') or pvt_textbox.see('end'))
        process.terminate()
    except Exception as e:
        root.after(0, log_print, f"PVT ошибка: {e}")
    log_print("PVT-поток остановлен.")

def start_pvt():
    global pvt_running
    if not combo.get():
        log_print("Выберите COM-порт!")
        return
    if not pvt_running:
        pvt_running = True
        pvt_data_list.clear()
        pvt_textbox.delete('1.0', 'end')
        threading.Thread(target=pvt_worker, daemon=True).start()
        log_print("PVT запущен.")

def stop_pvt():
    global pvt_running
    if pvt_running:
        pvt_running = False
        log_print("Остановка PVT...")

def save_pvt_to_file():
    if not pvt_data_list:
        log_print("Нет данных для сохранения.")
        return
    filename = fd.asksaveasfilename(defaultextension='.csv', filetypes=[('CSV', '*.csv')])
    if not filename:
        return
    try:
        with open(filename, 'w', newline='', encoding='utf-8') as f:
            writer = csv.DictWriter(f, fieldnames=['Lat', 'Lon', 'time', 'speed', 'course'])
            writer.writeheader()
            writer.writerows(pvt_data_list)
        log_print(f"PVT сохранён: {filename}")
    except Exception as e:
        log_print(f"Ошибка сохранения: {e}")

# --- ВКЛАДКА: АНАЛИЗ ЭХОЛОТА ---
class SonarAnalyzerFrame(ttk.Frame):
    def __init__(self, parent):
        super().__init__(parent)
        self.pack(fill=ttk.BOTH, expand=True)
        ttk.Label(self, text="Анализ эхолота: глубина, температура, карта", font=("Arial", 12, "bold")).pack(pady=10)

        btn_frame = ttk.Frame(self)
        btn_frame.pack(pady=10)
        self.btn_select = ttk.Button(btn_frame, text="Выбрать GPX", command=self.select, bootstyle=PRIMARY)
        self.btn_select.pack(side=ttk.LEFT, padx=5)
        self.btn_process = ttk.Button(btn_frame, text="→ CSV", command=self.start, bootstyle=SUCCESS)
        self.btn_process.pack(side=ttk.LEFT, padx=5)
        self.btn_cancel = ttk.Button(btn_frame, text="Отмена", command=self.cancel, bootstyle=DANGER, state=DISABLED)
        self.btn_cancel.pack(side=ttk.LEFT, padx=5)
        self.btn_plot = ttk.Button(btn_frame, text="Карта", command=self.plot, bootstyle=INFO)
        self.btn_plot.pack(side=ttk.LEFT, padx=5)

        self.use_temp = tk.BooleanVar(value=True)
        ttk.Checkbutton(self, text="Цвет: температура (иначе — глубина)", variable=self.use_temp, bootstyle="round-toggle").pack(pady=5)

        self.listbox = tk.Listbox(self, height=6)
        self.listbox.pack(fill=ttk.BOTH, expand=True, padx=20, pady=5)

        self.status = ttk.Label(self, text="Готово", anchor='w')
        self.status.pack(fill=ttk.X, padx=20, pady=(0, 5))

        self.progress = ttk.Progressbar(self, mode='determinate', bootstyle="success-striped")
        self.progress.pack(fill=ttk.X, padx=20, pady=(0, 10))

        self.files = []
        self.df = None
        self.cancel_flag = False

    def select(self):
        files = fd.askopenfilenames(filetypes=[("GPX files", "*.gpx")])
        if files:
            self.files = list(files)
            self.listbox.delete(0, tk.END)
            for f in self.files:
                self.listbox.insert(tk.END, os.path.basename(f))
            self.progress['value'] = 0
            self.status.config(text="Готово")

    def cancel(self):
        self.cancel_flag = True
        self.status.config(text="Отмена...")

    def start(self):
        if not self.files:
            messagebox.showwarning("Ошибка", "Выберите GPX-файлы!")
            return
        self.cancel_flag = False
        self.btn_process.config(state=DISABLED)
        self.btn_select.config(state=DISABLED)
        self.btn_cancel.config(state=NORMAL)
        self.status.config(text="Обработка...")
        self.progress['value'] = 0
        self.progress['maximum'] = len(self.files)
        threading.Thread(target=self._process, daemon=True).start()

    def _process(self):
        all_data = []
        total = len(self.files)
        for i, f in enumerate(self.files, 1):
            if self.cancel_flag:
                self.master.after(0, self._on_cancel)
                return
            try:
                self.master.after(0, self.status.config, {'text': f"Файл {i}/{total}: {os.path.basename(f)}"})
                df, _ = parse_sonar_gpx(f)
                if not df.empty:
                    all_data.append(df)
            except Exception as e:
                self.master.after(0, self._on_error, f, str(e))
                return
            self.master.after(0, self.progress.config, {'value': i})
            time.sleep(0.01)
        if self.cancel_flag:
            self.master.after(0, self._on_cancel)
            return
        if not all_data:
            self.master.after(0, self._on_empty)
        else:
            self.df = pd.concat(all_data, ignore_index=True)
            self.master.after(0, self._save_csv)

    def _on_error(self, file, msg):
        self._reset_ui()
        messagebox.showerror("Ошибка", f"{os.path.basename(file)}:\n{msg}")

    def _on_cancel(self):
        self._reset_ui()
        messagebox.showinfo("Отмена", "Обработка прервана.")

    def _on_empty(self):
        self._reset_ui()
        messagebox.showinfo("Внимание", "Нет данных для сохранения.")

    def _save_csv(self):
        path = fd.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV", "*.csv")])
        if path:
            try:
                self.df.to_csv(path, index=False, float_format="%.6f", encoding='utf-8-sig')
                final_msg = f"Успешно: {len(self.df)} точек из {len(self.files)} файлов"
                self.status.config(text=final_msg)
                messagebox.showinfo("Успех", final_msg)
            except Exception as e:
                messagebox.showerror("Ошибка записи", str(e))
        self._reset_ui()

    def _reset_ui(self):
        self.btn_process.config(state=NORMAL)
        self.btn_select.config(state=NORMAL)
        self.btn_cancel.config(state=DISABLED)
        self.status.config(text="Готово")

    def plot(self):
        if self.df is None or self.df.empty:
            messagebox.showwarning("Ошибка", "Сначала обработайте файлы!")
            return
        use_temp = self.use_temp.get()
        col = 'temperature' if use_temp else 'depth'
        df_plot = self.df.dropna(subset=[col])
        if df_plot.empty:
            messagebox.showinfo("Информация", f"Нет данных {'температуры' if use_temp else 'глубины'}")
            return
        cmap = 'coolwarm' if use_temp else 'viridis_r'
        label = 'Температура (°C)' if use_temp else 'Глубина (м)'
        plt.figure(figsize=(10, 6))
        sc = plt.scatter(df_plot['longitude'], df_plot['latitude'], c=df_plot[col], cmap=cmap, s=20, alpha=0.8)
        plt.colorbar(sc, label=label)
        plt.xlabel('Долгота (°)')
        plt.ylabel('Широта (°)')
        plt.grid(True, linestyle='--', alpha=0.5)
        plt.title(f"Карта {'температуры' if use_temp else 'глубины'}")
        plt.tight_layout()
        plt.show()

# --- ЭКСПОРТ МАРШРУТОВ ---
class GPXRouteExporter:
    def __init__(self, points, route_name="Route"):
        self.points = points
        self.route_name = route_name

    def export(self, filename):
        gpx = ET.Element("gpx",
                         version="1.1",
                         creator="Garmin Striker Toolkit",
                         xmlns="http://www.topografix.com/GPX/1/1",
                         xmlns_gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3",
                         xmlns_xsi="http://www.w3.org/2001/XMLSchema-instance",
                         xsi_schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
                         )
        gpx.set("xmlns:gpxx", "http://www.garmin.com/xmlschemas/GpxExtensions/v3")

        metadata = ET.SubElement(gpx, "metadata")
        ET.SubElement(metadata, "name").text = self.route_name

        rte = ET.SubElement(gpx, "rte")
        ET.SubElement(rte, "name").text = self.route_name

        for p in self.points:
            rtept = ET.SubElement(rte, "rtept", lat=str(p['lat']), lon=str(p['lon']))
            ET.SubElement(rtept, "name").text = p['name']
            if p.get('time'):
                ET.SubElement(rtept, "time").text = p['time']
            if p.get('depth') is not None:
                ET.SubElement(rtept, "depth").text = str(p['depth'])
            # Garmin-совместимые расширения
            extensions = ET.SubElement(rtept, "extensions")
            gpxx_ext = ET.SubElement(extensions, "gpxx:RoutePointExtension")
            subclass = ET.SubElement(gpxx_ext, "gpxx:Subclass")
            subclass.text = "000000000000ffffffffffffffffffffffff"

        tree = ET.ElementTree(gpx)
        tree.write(filename, encoding="utf-8", xml_declaration=True)

# --- РЕДАКТОР МАРШРУТОВ ---
class AddPointDialog(simpledialog.Dialog):
    def body(self, master):
        self.title("Добавить точку вручную")
        tk.Label(master, text="Имя точки:").grid(row=0, column=0, sticky="w")
        tk.Label(master, text="Широта (lat):").grid(row=1, column=0, sticky="w")
        tk.Label(master, text="Долгота (lon):").grid(row=2, column=0, sticky="w")
        tk.Label(master, text="Время (ISO, опц.):").grid(row=3, column=0, sticky="w")
        tk.Label(master, text="Глубина (опц.):").grid(row=4, column=0, sticky="w")

        now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
        self.e_name = tk.Entry(master)
        self.e_lat = tk.Entry(master)
        self.e_lon = tk.Entry(master)
        self.e_time = tk.Entry(master)
        self.e_depth = tk.Entry(master)

        self.e_name.grid(row=0, column=1)
        self.e_lat.grid(row=1, column=1)
        self.e_lon.grid(row=2, column=1)
        self.e_time.grid(row=3, column=1)
        self.e_depth.grid(row=4, column=1)
        self.e_time.insert(0, now)
        return self.e_name

    def validate(self):
        name = self.e_name.get().strip()
        lat = self.e_lat.get().strip()
        lon = self.e_lon.get().strip()
        if not name:
            messagebox.showwarning("Ошибка", "Имя точки не задано!")
            return False
        if not is_valid_lat(lat):
            messagebox.showwarning("Ошибка", "Некорректная широта!")
            return False
        if not is_valid_lon(lon):
            messagebox.showwarning("Ошибка", "Некорректная долгота!")
            return False
        return True

    def apply(self):
        depth_str = self.e_depth.get().strip()
        depth = comma_to_dot(depth_str) if depth_str else None
        self.result = {
            "name": self.e_name.get().strip(),
            "lat": comma_to_dot(self.e_lat.get().strip()),
            "lon": comma_to_dot(self.e_lon.get().strip()),
            "time": self.e_time.get().strip(),
            "depth": depth
        }

class RouteEditorFrame(ttk.Frame):
    def __init__(self, parent):
        super().__init__(parent)
        self.all_points = []
        ttk.Label(self, text="Точки маршрута:", font=('Arial', 10, 'bold')).pack(pady=(10, 5))
        self.points_listbox = tk.Listbox(self, selectmode=tk.SINGLE, height=12)
        self.points_listbox.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)

        btn_frame = ttk.Frame(self)
        btn_frame.pack(fill=tk.X, padx=10, pady=5)
        ttk.Button(btn_frame, text="Загрузить GPX", command=self.load_gpx).pack(side=tk.LEFT, padx=5)
        ttk.Button(btn_frame, text="Добавить вручную", command=self.add_point_manually).pack(side=tk.LEFT, padx=5)
        ttk.Button(btn_frame, text="Удалить", command=self.delete_point).pack(side=tk.LEFT, padx=5)
        ttk.Button(btn_frame, text="Сохранить маршрут (GPX)", command=self.save_route_gpx).pack(side=tk.RIGHT, padx=5)
        ttk.Button(btn_frame, text="Сохранить CSV", command=self.save_csv).pack(side=tk.RIGHT, padx=5)

        move_frame = ttk.Frame(self)
        move_frame.pack(fill=tk.X, padx=10, pady=5)
        ttk.Button(move_frame, text="↑ Вверх", command=self.move_up).pack(side=tk.LEFT, padx=5)
        ttk.Button(move_frame, text="↓ Вниз", command=self.move_down).pack(side=tk.LEFT, padx=5)

    def load_gpx(self):
        paths = fd.askopenfilenames(filetypes=[("GPX files", "*.gpx")])
        for path in paths:
            try:
                points = UniversalGPXParser.parse_gpx_file(path)
                for p in points:
                    self.all_points.append(p)
                    self.points_listbox.insert(tk.END, format_point_display(p))
                messagebox.showinfo("Успех", f"Загружено {len(points)} точек")
            except Exception as e:
                messagebox.showerror("Ошибка", str(e))

    def add_point_manually(self):
        dialog = AddPointDialog(self)
        if dialog.result:
            self.all_points.append(dialog.result)
            self.points_listbox.insert(tk.END, format_point_display(dialog.result))

    def delete_point(self):
        sel = self.points_listbox.curselection()
        if sel:
            idx = sel[0]
            del self.all_points[idx]
            self.points_listbox.delete(idx)

    def move_up(self):
        sel = self.points_listbox.curselection()
        if not sel or sel[0] == 0:
            return
        idx = sel[0]
        self.all_points[idx-1], self.all_points[idx] = self.all_points[idx], self.all_points[idx-1]
        self.points_listbox.delete(idx-1, idx+1)
        self.points_listbox.insert(idx-1, format_point_display(self.all_points[idx-1]))
        self.points_listbox.insert(idx, format_point_display(self.all_points[idx]))
        self.points_listbox.selection_set(idx-1)

    def move_down(self):
        sel = self.points_listbox.curselection()
        if not sel or sel[0] == len(self.all_points) - 1:
            return
        idx = sel[0]
        self.all_points[idx], self.all_points[idx+1] = self.all_points[idx+1], self.all_points[idx]
        self.points_listbox.delete(idx, idx+2)
        self.points_listbox.insert(idx, format_point_display(self.all_points[idx]))
        self.points_listbox.insert(idx+1, format_point_display(self.all_points[idx+1]))
        self.points_listbox.selection_set(idx+1)

    def save_csv(self):
        if not self.all_points:
            messagebox.showwarning("Ошибка", "Нет точек!")
            return
        path = fd.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV", "*.csv")])
        if not path:
            return
        try:
            df = pd.DataFrame(self.all_points)
            df.to_csv(path, index=False, encoding='utf-8-sig')
            messagebox.showinfo("Успех", "Сохранено!")
        except Exception as e:
            messagebox.showerror("Ошибка", str(e))

    def save_route_gpx(self):
        if not self.all_points:
            messagebox.showwarning("Ошибка", "Нет точек для маршрута!")
            return
        path = fd.asksaveasfilename(defaultextension=".gpx", filetypes=[("GPX", "*.gpx")])
        if not path:
            return
        try:
            exporter = GPXRouteExporter(self.all_points, route_name="MyRoute")
            exporter.export(path)
            messagebox.showinfo("Успех", f"Маршрут сохранён: {path}")
        except Exception as e:
            messagebox.showerror("Ошибка", f"Не удалось сохранить маршрут:\n{e}")

# --- ОСНОВНОЕ ОКНО ---
root = ttk.Window(themename='solar')
root.iconbitmap('')# Указать путь к G-Striker 4.ico('C:/Users/...')
root.title('Garmin Striker Toolkit')
try:
    root.iconbitmap('')# Указать путь к G-striker 4.ico('C:/Users/...')
except:
    pass
root.geometry("950x850")

notebook = ttk.Notebook(root)
notebook.pack(pady=10, padx=10, fill='both', expand=True)

# Вкладка PVT
frame1 = ttk.Frame(notebook, padding=10)
notebook.add(frame1, text="PVT")
ttk.Button(frame1, text="Старт PVT", command=start_pvt).grid(row=0, column=0, padx=5, pady=5)
ttk.Button(frame1, text="Стоп PVT", command=stop_pvt).grid(row=0, column=1, padx=5, pady=5)
ttk.Button(frame1, text="Сохранить", command=save_pvt_to_file).grid(row=0, column=2, padx=5, pady=5)
pvt_textbox = ttk.Text(frame1, width=90, height=20, wrap='word')
pvt_textbox.grid(row=1, column=0, columnspan=3, pady=10)

# Остальные вкладки
frame2 = ttk.Frame(notebook, padding=10)
notebook.add(frame2, text="Выгрузка точек")
ttk.Button(frame2, text="Выгрузить точки (GPX)", command=get_waypoints).pack(pady=5)

frame3 = ttk.Frame(notebook, padding=10)
notebook.add(frame3, text="Выгрузка трека")
ttk.Button(frame3, text="Выгрузить трек", command=get_tracks).pack(pady=5)

frame4 = ttk.Frame(notebook, padding=10)
notebook.add(frame4, text="Загрузка пути")
ttk.Button(frame4, text="Загрузить путь", command=put_routes).pack(pady=5)

frame5 = ttk.Frame(notebook, padding=10)
notebook.add(frame5, text="Загрузка точек")
ttk.Button(frame5, text="Загрузить точки", command=put_waypoints).pack(pady=5)

frame6 = ttk.Frame(notebook, padding=10)
notebook.add(frame6, text="Информация")
ttk.Button(frame6, text="Получить информацию", command=info_file).pack(pady=5)

# Анализ эхолота
frame_sonar = SonarAnalyzerFrame(notebook)
notebook.add(frame_sonar, text="Анализ эхолота")

# Редактор маршрутов — ПОЛНОСТЬЮ РЕАЛИЗОВАН!
frame_route = RouteEditorFrame(notebook)
notebook.add(frame_route, text="Редактор маршрутов")

frame7 = ttk.Frame(notebook, padding=10)
notebook.add(frame7, text="ID устройства")
ttk.Button(frame7, text="Получить ID", command=id_info).pack(pady=5)

frame9 = ttk.Frame(notebook, padding=10)
notebook.add(frame9, text="Координаты")
ttk.Button(frame9, text="Получить координаты", command=get_position).pack(pady=5)

# Нижняя панель
bottom_frame = ttk.Frame(root)
bottom_frame.pack(side='bottom', fill='x', padx=10, pady=5)
ports = serial.tools.list_ports.comports()
combo = ttk.Combobox(bottom_frame, values=[p.device for p in ports], width=15)
combo.pack(side='left', padx=5)
log_text = ttk.Text(bottom_frame, height=8, width=80)
log_text.pack(side='left', padx=10, fill='x', expand=True)

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