#!/usr/bin/env python3
"""Generate a self-contained tkinter GUI flasher (firmware embedded) and build
it into a single Windows .exe with PyInstaller."""
import base64, os, subprocess, sys

# project root = parent of this script's directory (portable)
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
FW   = f"{ROOT}/stock-fw/rt4d_ru_full.bin"
DIST = f"{ROOT}/dist"

fw = open(FW, "rb").read()
b64 = base64.b64encode(fw).decode()
info = f"Ru-4D V3.25 (русифицированная) - {len(fw)} байт"

GUI = r'''# -*- coding: utf-8 -*-
import base64, time, threading, queue
import tkinter as tk
from tkinter import ttk
import serial
from serial.tools import list_ports

FW = base64.b64decode("__B64__")
INFO = "__INFO__"
SEED = 0x48

def cksum(b):
    return bytes([(SEED + sum(b)) & 0xFF])

def find_ports():
    return list(list_ports.comports())

def auto_port(ports):
    for p in ports:                       # CH343 = VID 0x1A86
        if (p.vid or 0) == 0x1A86:
            return p.device
    return ports[0].device if ports else None

def do_flash(port_name, log, prog):
    p = serial.Serial(port_name, 115200, timeout=0.3, write_timeout=3)
    try:
        log("Порт " + port_name + " открыт. Синхронизация...")
        p.reset_input_buffer()
        ready = False; t0 = time.time()
        while time.time() - t0 < 7:
            b = p.read(1)
            if not b: p.write(b"\xff")
            elif b == b"\xff": ready = True; break
        if not ready:
            raise RuntimeError("Рация не отвечает. Выключите и включите её\nс зажатой тангентой PTT, затем повторите.")
        log("Бутлоадер найден. Стирание...")
        for trig in (0x10, 0x55):
            pl = bytes([0x39, 0x33, 0x05, trig]); pl += cksum(pl)
            p.reset_input_buffer(); p.write(pl)
            ok = False; t = time.time()
            while time.time() - t < 4:
                if p.read(1) == b"\x06": ok = True; break
            if not ok: raise RuntimeError("Ошибка стирания.")
        log("Запись прошивки...")
        pad = (-len(FW)) % 1024
        data = FW + b"\xff" * pad; total = len(data)
        for off in range(0, total, 1024):
            chunk = data[off:off + 1024]
            pl = bytes([0x57, (off >> 8) & 0xFF, off & 0xFF]) + chunk; pl += cksum(pl)
            p.reset_input_buffer(); p.write(pl)
            ok = False; t = time.time()
            while time.time() - t < 3:
                if p.read(1) == b"\x06": ok = True; break
            if not ok: raise RuntimeError("Ошибка записи на 0x%X." % off)
            prog((off + 1024) / total)
        prog(1.0)
        log("[OK] Готово! Выключите и включите рацию обычно.")
    finally:
        p.close()

class App:
    def __init__(self, root):
        self.root = root; self.q = queue.Queue(); self.busy = False
        root.title("Ru-4D - Прошивка рации"); root.geometry("460x520"); root.resizable(False, False)
        root.configure(bg="#131a24")
        F = dict(bg="#131a24", fg="#dfe7ef")
        tk.Label(root, text="Ru-4D - Прошивка рации", font=("Segoe UI", 15, "bold"), **F).pack(pady=(16, 0))
        tk.Label(root, text=INFO, font=("Segoe UI", 9), bg="#131a24", fg="#7f8ea3").pack()
        steps = ("1. Выключите рацию.\n"
                 "2. Включите её, удерживая тангенту PTT (режим прошивки).\n"
                 "3. Подключите рацию к ПК кабелем USB-C.\n"
                 "4. Выберите COM-порт и нажмите  Прошить .\n"
                 "5. Не отключайте кабель до конца записи.")
        tk.Label(root, text=steps, justify="left", font=("Segoe UI", 10), bg="#131a24", fg="#c3cdd9").pack(padx=18, pady=12, anchor="w")
        row = tk.Frame(root, bg="#131a24"); row.pack(fill="x", padx=18)
        tk.Label(row, text="Порт:", **F).pack(side="left")
        self.port_var = tk.StringVar()
        self.port_cb = ttk.Combobox(row, textvariable=self.port_var, state="readonly", width=28)
        self.port_cb.pack(side="left", padx=6)
        tk.Button(row, text="↻", command=self.refresh, width=3).pack(side="left")
        self.btn = tk.Button(root, text="Прошить", font=("Segoe UI", 13, "bold"),
                             bg="#22bb77", fg="#04120b", activebackground="#2ad", relief="flat",
                             command=self.start)
        self.btn.pack(fill="x", padx=18, pady=(14, 6))
        self.pb = ttk.Progressbar(root, maximum=1000); self.pb.pack(fill="x", padx=18, pady=4)
        self.log = tk.Text(root, height=9, font=("Consolas", 9), bg="#0c1219", fg="#9fb0c2",
                           relief="flat", wrap="word"); self.log.pack(fill="both", expand=True, padx=18, pady=(6, 14))
        self.refresh(); self.poll()

    def refresh(self):
        ports = find_ports()
        self.port_cb["values"] = [f"{p.device} - {p.description}" for p in ports]
        ap = auto_port(ports)
        if ap:
            for i, p in enumerate(ports):
                if p.device == ap: self.port_cb.current(i); break

    def logline(self, m, tag=None):
        self.log.insert("end", m + "\n"); self.log.see("end")

    def prog(self, f): self.pb["value"] = int(f * 1000)

    def start(self):
        if self.busy: return
        sel = self.port_var.get()
        if not sel:
            self.logline("Выберите COM-порт (подключите рацию в режиме прошивки)."); return
        port = sel.split(" - ")[0]
        self.busy = True; self.btn.config(state="disabled"); self.log.delete("1.0", "end"); self.pb["value"] = 0
        threading.Thread(target=self.worker, args=(port,), daemon=True).start()

    def worker(self, port):
        try:
            do_flash(port, lambda m: self.q.put(("log", m)), lambda f: self.q.put(("prog", f)))
        except Exception as e:
            self.q.put(("log", "[ОШИБКА] " + str(e)))
        self.q.put(("done", None))

    def poll(self):
        try:
            while True:
                k, v = self.q.get_nowait()
                if k == "log": self.logline(v)
                elif k == "prog": self.prog(v)
                elif k == "done": self.busy = False; self.btn.config(state="normal")
        except queue.Empty:
            pass
        self.root.after(60, self.poll)

if __name__ == "__main__":
    root = tk.Tk()
    try:
        App(root); root.mainloop()
    except Exception as e:
        import traceback; traceback.print_exc()
'''

gui = GUI.replace("__B64__", b64).replace("__INFO__", info)
os.makedirs(DIST, exist_ok=True)
gui_path = f"{DIST}/flasher_gui.py"
open(gui_path, "w", encoding="utf-8").write(gui)
print("wrote", gui_path, len(gui), "bytes")

cmd = [sys.executable, "-m", "PyInstaller", "--onefile", "--windowed",
       "--name", "Ru-4D_Flasher", "--distpath", DIST,
       "--workpath", f"{DIST}/build", "--specpath", f"{DIST}/build",
       "--clean", "--noconfirm", gui_path]
print("running:", " ".join(cmd[2:]))
r = subprocess.run(cmd, capture_output=True, text=True)
print(r.stdout[-1500:]); print(r.stderr[-1500:])
print("EXIT", r.returncode)
