#!/usr/bin/env python3
"""
Simple Linux GUI wrapper for pk2cmd-minus / PICkit3.

Save as: pk2cmd_gui.py
Recommended location:
  /home/ian/pk2cmd/pk2cmd/pk2cmd_gui.py

Run with:
  cd /home/ian/pk2cmd/pk2cmd
  sudo python3 pk2cmd_gui.py

Notes:
- Run the GUI with sudo, then leave "Use sudo in command" unticked.
- The GUI runs pk2cmd from its own folder so PK2DeviceFile.dat is found.
"""

import os
import re
import shlex
import subprocess
import threading
import tkinter as tk
from tkinter import filedialog, messagebox, ttk

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
DEFAULT_PK2CMD = os.path.join(SCRIPT_DIR, "pk2cmd")
DEFAULT_WORKDIR = SCRIPT_DIR
DEFAULT_DEVICE = "PIC16F88"
DEFAULT_VOLTAGE = "3.0"

class PK2CmdGui(tk.Tk):
    def __init__(self):
        super().__init__()

        self.title("PICkit / pk2cmd GUI")
        self.geometry("900x640")
        self.minsize(820, 560)

        self.pk2cmd_path = tk.StringVar(value=DEFAULT_PK2CMD)
        self.workdir = tk.StringVar(value=DEFAULT_WORKDIR)
        self.device = tk.StringVar(value=DEFAULT_DEVICE)
        self.hex_file = tk.StringVar(value="")
        self.voltage = tk.StringVar(value=DEFAULT_VOLTAGE)
        self.use_sudo = tk.BooleanVar(value=False)
        self.leave_powered = tk.BooleanVar(value=True)
        self.release_mclr = tk.BooleanVar(value=True)

        self._build_ui()

    def _build_ui(self):
        pad = {"padx": 8, "pady": 5}
        frm = ttk.Frame(self)
        frm.pack(fill="both", expand=True, padx=10, pady=10)

        ttk.Label(frm, text="pk2cmd executable").grid(row=0, column=0, sticky="w", **pad)
        ttk.Entry(frm, textvariable=self.pk2cmd_path).grid(row=0, column=1, sticky="ew", **pad)
        ttk.Button(frm, text="Browse", command=self.browse_pk2cmd).grid(row=0, column=2, **pad)

        ttk.Label(frm, text="Working folder").grid(row=1, column=0, sticky="w", **pad)
        ttk.Entry(frm, textvariable=self.workdir).grid(row=1, column=1, sticky="ew", **pad)
        ttk.Button(frm, text="Browse", command=self.browse_workdir).grid(row=1, column=2, **pad)

        ttk.Label(frm, text="Device").grid(row=2, column=0, sticky="w", **pad)
        ttk.Entry(frm, textvariable=self.device, width=18).grid(row=2, column=1, sticky="w", **pad)

        ttk.Label(frm, text="VDD voltage").grid(row=3, column=0, sticky="w", **pad)
        ttk.Entry(frm, textvariable=self.voltage, width=10).grid(row=3, column=1, sticky="w", **pad)

        ttk.Label(frm, text="HEX file").grid(row=4, column=0, sticky="w", **pad)
        ttk.Entry(frm, textvariable=self.hex_file).grid(row=4, column=1, sticky="ew", **pad)
        ttk.Button(frm, text="Browse", command=self.browse_hex).grid(row=4, column=2, **pad)

        opt = ttk.LabelFrame(frm, text="Options")
        opt.grid(row=5, column=0, columnspan=3, sticky="ew", padx=8, pady=8)
        ttk.Checkbutton(opt, text="Use sudo in command", variable=self.use_sudo).grid(row=0, column=0, sticky="w", padx=8, pady=4)
        ttk.Checkbutton(opt, text="Leave target powered (-T)", variable=self.leave_powered).grid(row=0, column=1, sticky="w", padx=8, pady=4)
        ttk.Checkbutton(opt, text="Release MCLR (-R)", variable=self.release_mclr).grid(row=0, column=2, sticky="w", padx=8, pady=4)

        buttons = ttk.Frame(frm)
        buttons.grid(row=6, column=0, columnspan=3, sticky="ew", padx=8, pady=6)
        ttk.Button(buttons, text="Detect Device", command=self.detect_device).pack(side="left", padx=4)
        ttk.Button(buttons, text="Program HEX", command=self.program_hex).pack(side="left", padx=4)
        ttk.Button(buttons, text="Verify HEX", command=self.verify_hex).pack(side="left", padx=4)
        ttk.Button(buttons, text="Power On", command=self.power_on).pack(side="left", padx=4)
        ttk.Button(buttons, text="Power Off / Hold Reset", command=self.power_off).pack(side="left", padx=4)
        ttk.Button(buttons, text="Clear Output", command=self.clear_output).pack(side="right", padx=4)

        ttk.Label(frm, text="Output").grid(row=7, column=0, sticky="w", **pad)
        self.output = tk.Text(frm, height=21, wrap="word")
        self.output.grid(row=8, column=0, columnspan=3, sticky="nsew", padx=8, pady=5)
        scroll = ttk.Scrollbar(frm, command=self.output.yview)
        scroll.grid(row=8, column=3, sticky="ns")
        self.output.configure(yscrollcommand=scroll.set)

        frm.columnconfigure(1, weight=1)
        frm.rowconfigure(8, weight=1)

        self.log("2026 IanThor.com\n")
        self.log("Ready. Recommended: start this GUI with sudo from the pk2cmd folder, leave 'Use sudo in command' unticked.\n")
        self.log(f"Default pk2cmd: {DEFAULT_PK2CMD}\n")
        self.log(f"Default workdir: {DEFAULT_WORKDIR}\n")

    def browse_pk2cmd(self):
        path = filedialog.askopenfilename(title="Select pk2cmd executable")
        if path:
            self.pk2cmd_path.set(path)
            self.workdir.set(os.path.dirname(path))

    def browse_workdir(self):
        path = filedialog.askdirectory(title="Select working folder containing PK2DeviceFile.dat")
        if path:
            self.workdir.set(path)

    def browse_hex(self):
        path = filedialog.askopenfilename(
            title="Select HEX file",
            filetypes=[("HEX files", "*.hex *.HEX"), ("All files", "*")],
        )
        if path:
            self.hex_file.set(path)

    def clear_output(self):
        self.output.delete("1.0", "end")

    def log(self, text):
        self.output.insert("end", text)
        self.output.see("end")
        self.output.update_idletasks()

    def base_cmd(self):
        pk2cmd = self.pk2cmd_path.get().strip()
        device = self.device.get().strip()
        voltage = self.voltage.get().strip()
        workdir = self.workdir.get().strip()

        if not pk2cmd:
            raise ValueError("pk2cmd executable path is blank")
        if not os.path.isfile(pk2cmd):
            raise ValueError(f"pk2cmd executable not found: {pk2cmd}")
        if not os.access(pk2cmd, os.X_OK):
            raise ValueError(f"pk2cmd is not executable. Run: chmod +x {pk2cmd}")
        if not workdir:
            raise ValueError("Working folder is blank")
        if not os.path.isdir(workdir):
            raise ValueError(f"Working folder not found: {workdir}")
        if not os.path.isfile(os.path.join(workdir, "PK2DeviceFile.dat")):
            raise ValueError("PK2DeviceFile.dat not found in working folder")
        if not device:
            raise ValueError("Device is blank")

        cmd = []
        if self.use_sudo.get():
            cmd.append("sudo")
        cmd.append(pk2cmd)
        cmd.append(f"-P{device}")
        if voltage:
            cmd.append(f"-A{voltage}")
        return cmd

    def append_power_options(self, cmd):
        if self.leave_powered.get():
            cmd.append("-T")
        if self.release_mclr.get():
            cmd.append("-R")
        return cmd

    def run_cmd(self, cmd, parse_detect=False):
        workdir = self.workdir.get().strip()
        self.log("\n$ " + " ".join(shlex.quote(c) for c in cmd) + "\n")
        self.log(f"cwd: {workdir}\n")
        self.log("Running...\n")

        def worker():
            try:
                proc = subprocess.Popen(
                    cmd,
                    cwd=workdir,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.STDOUT,
                    text=True,
                    bufsize=1,
                )
                full_output = []
                for line in proc.stdout:
                    full_output.append(line)
                    self.after(0, self.log, line)
                proc.wait()
                output_text = "".join(full_output)
                if parse_detect:
                    self.after(0, self.parse_detect_output, output_text)
                self.after(0, self.log, f"\nExit code: {proc.returncode}\n")
            except Exception as e:
                self.after(0, self.log, f"ERROR: {e}\\n")

        threading.Thread(target=worker, daemon=True).start()

    def parse_detect_output(self, text):
        # Example output:
        # Device ID = 0760
        # Revision  = 0008
        # Device Name = PIC16F88
        m = re.search(r"Device Name\s*=\s*([A-Za-z0-9]+)", text)
        if m:
            detected = m.group(1).strip().upper()
            self.device.set(detected)
            self.log(f"Detected device field updated to: {detected}\n")

    def detect_device(self):
        try:
            cmd = self.base_cmd()
            cmd.append("-I")
            self.run_cmd(cmd, parse_detect=True)
        except Exception as e:
            messagebox.showerror("Error", str(e))
            self.log(f"ERROR: {e}\\n")

    def program_hex(self):
        try:
            hexfile = self.hex_file.get().strip()
            if not hexfile:
                raise ValueError("No HEX file selected")
            if not os.path.exists(hexfile):
                raise ValueError(f"HEX file not found: {hexfile}")
            cmd = self.base_cmd()
            cmd.append(f"-F{hexfile}")
            cmd.append("-M")
            self.append_power_options(cmd)
            self.run_cmd(cmd)
        except Exception as e:
            messagebox.showerror("Error", str(e))
            self.log(f"ERROR: {e}\\n")

    def verify_hex(self):
        try:
            hexfile = self.hex_file.get().strip()
            if not hexfile:
                raise ValueError("No HEX file selected")
            if not os.path.exists(hexfile):
                raise ValueError(f"HEX file not found: {hexfile}")
            cmd = self.base_cmd()
            cmd.append(f"-F{hexfile}")
            cmd.append("-Y")
            self.append_power_options(cmd)
            self.run_cmd(cmd)
        except Exception as e:
            messagebox.showerror("Error", str(e))
            self.log(f"ERROR: {e}\\n")

    def power_on(self):
        try:
            cmd = self.base_cmd()
            # pk2cmd needs a real operation. -I reads the device ID, then -T/-R leaves it powered/running.
            cmd.append("-I")
            cmd.append("-T")
            if self.release_mclr.get():
                cmd.append("-R")
            self.run_cmd(cmd, parse_detect=True)
        except Exception as e:
            messagebox.showerror("Error", str(e))
            self.log(f"ERROR: {e}\\n")

    def power_off(self):
        try:
            cmd = self.base_cmd()
            # pk2cmd needs a real operation. -I reads the device ID; no -T means VDD off afterwards.
            # no -R means MCLR remains asserted by default.
            cmd.append("-I")
            self.run_cmd(cmd, parse_detect=True)
        except Exception as e:
            messagebox.showerror("Error", str(e))
            self.log(f"ERROR: {e}\\n")

if __name__ == "__main__":
    app = PK2CmdGui()
    app.mainloop()

