"""UI widgets for DJ HUD — region selectors, editors, output windows, and diagnostics dialog."""

import sys
import time
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from dataclasses import asdict
from typing import Callable, List, Optional, Tuple

from djhud import __version__
from djhud.models import CaptureRegion, SnapHelper, DEFAULT_REGION_H
from djhud.platform import PLATFORM, IS_MAC

try:
    from PIL import ImageTk
except ImportError:
    ImageTk = None


# ═══════════════════════════════════════════════════════════════════════════════
# SCROLLABLE FRAME
# ═══════════════════════════════════════════════════════════════════════════════

class ScrollableFrame(ttk.Frame):
    def __init__(self, master, target_width=480, *args, **kwargs):
        super().__init__(master, *args, **kwargs)
        self._target_width = target_width
        self.canvas = tk.Canvas(self, highlightthickness=0, width=target_width, height=600)
        self.v_scroll = ttk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
        self.canvas.configure(yscrollcommand=self.v_scroll.set)
        self.inner = ttk.Frame(self.canvas)
        self.inner_id = self.canvas.create_window((0, 0), window=self.inner, anchor="nw")
        self.canvas.grid(row=0, column=0, sticky="nsew")
        self.v_scroll.grid(row=0, column=1, sticky="ns")
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)
        self.inner.bind("<Configure>", self._on_inner_configure)
        self.canvas.bind("<Configure>", self._on_canvas_configure)
        # Mousewheel / trackpad scrolling
        self.canvas.bind("<Enter>", self._bind_mousewheel)
        self.canvas.bind("<Leave>", self._unbind_mousewheel)
        # macOS tkinter needs multiple delayed kicks to lay out the inner frame.
        # A single 100ms delay isn't enough — the window may not be fully mapped yet.
        self._update_count = 0
        for delay in (50, 150, 300, 600, 1200):
            self.after(delay, self._force_update)

    def _on_inner_configure(self, event=None):
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))

    def _on_canvas_configure(self, event):
        self.canvas.itemconfigure(self.inner_id, width=event.width)

    def _force_update(self):
        """Force tkinter to recalculate layout — needed on macOS at startup."""
        self._update_count += 1
        self.inner.update_idletasks()
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))
        w = self.inner.winfo_reqwidth()
        canvas_w = self.canvas.winfo_width()
        target = max(self._target_width, w, canvas_w)
        if target > 1:
            self.canvas.itemconfigure(self.inner_id, width=target)

    def _bind_mousewheel(self, event=None):
        """Bind mousewheel when mouse enters the scrollable area."""
        if IS_MAC:
            self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)
        else:
            # Windows and Linux
            self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)
            self.canvas.bind_all("<Button-4>", self._on_mousewheel_linux)
            self.canvas.bind_all("<Button-5>", self._on_mousewheel_linux)

    def _unbind_mousewheel(self, event=None):
        """Unbind mousewheel when mouse leaves."""
        self.canvas.unbind_all("<MouseWheel>")
        if not IS_MAC:
            self.canvas.unbind_all("<Button-4>")
            self.canvas.unbind_all("<Button-5>")

    def _on_mousewheel(self, event):
        """Handle mousewheel on Mac and Windows."""
        if IS_MAC:
            self.canvas.yview_scroll(int(-event.delta), "units")
        else:
            self.canvas.yview_scroll(int(-event.delta / 120), "units")

    def _on_mousewheel_linux(self, event):
        """Handle mousewheel on Linux (Button-4/5)."""
        if event.num == 4:
            self.canvas.yview_scroll(-3, "units")
        elif event.num == 5:
            self.canvas.yview_scroll(3, "units")


# ═══════════════════════════════════════════════════════════════════════════════
# REGION SELECTOR — overlay for drawing new capture regions
# ═══════════════════════════════════════════════════════════════════════════════

class RegionSelector(tk.Toplevel):
    HANDLE_SIZE = 2
    HANDLE_HIT = 5

    def __init__(self, master, source_rect: dict, on_done: Callable, continuous: bool = False,
                 existing_regions: Optional[List[CaptureRegion]] = None, nudge_step: int = 1,
                 snap_to_grid: bool = False, grid_size: int = 10, background_image=None):
        super().__init__(master)
        self.source_rect = source_rect
        self.on_done = on_done
        self.continuous = continuous
        self.rect: Optional[list] = None
        self.rect_id = None
        self.handle_ids = []
        self.drag_mode: Optional[str] = None
        self.drag_anchor = (0, 0)
        self.drag_start_rect = None
        self.existing_regions = [CaptureRegion(**asdict(r)) for r in (existing_regions or [])]
        self.nudge_step = max(1, int(nudge_step))
        self.snap = SnapHelper(snap_to_grid, grid_size)
        self.background_image = background_image
        self.bg_photo = None
        self.committed_rects = [
            (r.src_x, r.src_y, r.src_x + r.src_w, r.src_y + r.src_h, f"R{i+1}")
            for i, r in enumerate(self.existing_regions)
        ]

        if background_image is None:
            self.overrideredirect(True)
            self.attributes("-topmost", True)
            try:
                self.attributes("-alpha", 0.33)
            except tk.TclError:
                pass
            self.configure(bg="black")
            self.geometry(f'{source_rect["width"]}x{source_rect["height"]}+{source_rect["left"]}+{source_rect["top"]}')
        else:
            self.title("Region selector — draw regions, press Enter to add, Esc when done")
            # Don't use topmost on Mac — it blocks Cmd-Tab
            if not IS_MAC:
                self.attributes("-topmost", True)
            self.configure(bg="black")
            self.geometry(f'{source_rect["width"]}x{source_rect["height"]}+80+80')

        self.canvas = tk.Canvas(self, bg="gray15", highlightthickness=0, cursor="crosshair",
                                width=source_rect["width"], height=source_rect["height"])
        self.canvas.pack(fill="both", expand=True)
        if background_image is not None and ImageTk is not None:
            self.bg_photo = ImageTk.PhotoImage(background_image)
            self.canvas.create_image(0, 0, anchor="nw", image=self.bg_photo, tags="bg")

        help_text = (
            "Drag to draw. Then: drag inside to move, drag edge/corner to resize. "
            "Del = clear box, Enter = add region, Esc = done"
            if self.continuous else
            "Drag to draw. Then: drag inside to move, drag edge/corner to resize. "
            "Del = clear box, Enter = save, Esc = cancel"
        )
        self.canvas.create_text(16, 14, anchor="nw", text=help_text, fill="white",
                                font=("Helvetica", 12, "bold"),
                                width=max(300, source_rect["width"] - 32))

        self.canvas.bind("<ButtonPress-1>", self.on_press)
        self.canvas.bind("<B1-Motion>", self.on_drag)
        self.canvas.bind("<ButtonRelease-1>", self.on_release)
        self.bind("<Delete>", self.on_delete)
        self.bind("<BackSpace>", self.on_delete)
        self.bind("<Return>", self.finish)
        self.bind("<Escape>", lambda e: self.destroy())
        for seq in ("<Left>", "<Right>", "<Up>", "<Down>",
                     "<Shift-Left>", "<Shift-Right>", "<Shift-Up>", "<Shift-Down>"):
            self.bind(seq, self.on_arrow_key)
        self.focus_force()
        # Don't grab_set on Mac — it prevents Cmd-Tab switching
        if not IS_MAC:
            self.grab_set()

    @property
    def _max_w(self):
        return self.source_rect["width"]

    @property
    def _max_h(self):
        return self.source_rect["height"]

    def on_delete(self, event=None):
        self.rect = None
        self.redraw()

    def redraw(self):
        self.canvas.delete("selector_overlay")
        if self.rect_id is not None:
            self.canvas.delete(self.rect_id)
            self.rect_id = None
        for hid in self.handle_ids:
            self.canvas.delete(hid)
        self.handle_ids.clear()

        for x1, y1, x2, y2, label in self.committed_rects:
            self.canvas.create_rectangle(x1, y1, x2, y2, outline="#6ec6ff", width=1, tags="selector_overlay")
            self.canvas.create_text(x1 + 4, y1 + 2, anchor="nw", text=label, fill="white", tags="selector_overlay")

        if not self.rect:
            return

        x1, y1, x2, y2 = self.rect
        self.rect_id = self.canvas.create_rectangle(x1, y1, x2, y2, outline="red", width=1)
        hs = self.HANDLE_SIZE
        points = [
            (x1, y1), ((x1 + x2) // 2, y1), (x2, y1),
            (x1, (y1 + y2) // 2), (x2, (y1 + y2) // 2),
            (x1, y2), ((x1 + x2) // 2, y2), (x2, y2),
        ]
        for px, py in points:
            hid = self.canvas.create_rectangle(px - hs, py - hs, px + hs - 1, py + hs - 1,
                                                outline="red", fill="white", width=1)
            self.handle_ids.append(hid)

    def _hit_test(self, x, y):
        if not self.rect:
            return None
        x1, y1, x2, y2 = self.rect
        hs = self.HANDLE_HIT
        handles = {
            "nw": (x1 - hs, y1 - hs, x1 + hs, y1 + hs),
            "n": (((x1 + x2) // 2) - hs, y1 - hs, ((x1 + x2) // 2) + hs, y1 + hs),
            "ne": (x2 - hs, y1 - hs, x2 + hs, y1 + hs),
            "w": (x1 - hs, ((y1 + y2) // 2) - hs, x1 + hs, ((y1 + y2) // 2) + hs),
            "e": (x2 - hs, ((y1 + y2) // 2) - hs, x2 + hs, ((y1 + y2) // 2) + hs),
            "sw": (x1 - hs, y2 - hs, x1 + hs, y2 + hs),
            "s": (((x1 + x2) // 2) - hs, y2 - hs, ((x1 + x2) // 2) + hs, y2 + hs),
            "se": (x2 - hs, y2 - hs, x2 + hs, y2 + hs),
        }
        for name, (hx1, hy1, hx2, hy2) in handles.items():
            if hx1 <= x <= hx2 and hy1 <= y <= hy2:
                return name
        if x1 <= x <= x2 and y1 <= y <= y2:
            return "move"
        return None

    def on_press(self, event):
        hit = self._hit_test(event.x, event.y)
        self.drag_anchor = (event.x, event.y)
        if hit and self.rect:
            self.drag_mode = hit
            self.drag_start_rect = tuple(self.rect)
            return
        self.drag_mode = "new"
        self.drag_start_rect = (event.x, event.y, event.x, event.y)
        self.rect = [event.x, event.y, event.x, event.y]
        self.redraw()

    def on_drag(self, event):
        if not self.drag_mode or not self.drag_start_rect:
            return
        sx, sy = self.drag_anchor
        x, y = event.x, event.y
        x1, y1, x2, y2 = self.drag_start_rect

        if self.drag_mode == "new":
            self.rect = self.snap.clamp_rect(self.snap.snap_rect_endpoints([sx, sy, x, y]), self._max_w, self._max_h)
        elif self.drag_mode == "move":
            dx, dy = x - sx, y - sy
            w, h = x2 - x1, y2 - y1
            nx1 = min(max(0, x1 + dx), self._max_w - w - 1)
            ny1 = min(max(0, y1 + dy), self._max_h - h - 1)
            self.rect = self.snap.snap_move_rect([nx1, ny1, nx1 + w, ny1 + h])
        else:
            nx1, ny1, nx2, ny2 = x1, y1, x2, y2
            if "w" in self.drag_mode: nx1 = x
            if "e" in self.drag_mode: nx2 = x
            if "n" in self.drag_mode: ny1 = y
            if "s" in self.drag_mode: ny2 = y
            self.rect = self.snap.clamp_rect(
                self.snap.normalize_rect(self.snap.snap_rect_endpoints([nx1, ny1, nx2, ny2])),
                self._max_w, self._max_h)
        self.redraw()

    def on_release(self, event):
        if self.rect:
            if self.drag_mode == "move":
                self.rect = self.snap.clamp_rect(self.snap.snap_move_rect(self.rect), self._max_w, self._max_h)
            else:
                self.rect = self.snap.clamp_rect(
                    self.snap.normalize_rect(self.snap.snap_rect_endpoints(self.rect)),
                    self._max_w, self._max_h)
            x1, y1, x2, y2 = self.rect
            if (x2 - x1) < 5 or (y2 - y1) < 5:
                self.rect = None
        self.redraw()
        self.drag_mode = None
        self.drag_start_rect = None

    def on_arrow_key(self, event=None):
        if not self.rect:
            return "break"
        step = self.nudge_step
        if event is not None and (event.state & 0x0001):
            step *= 10
        dx = dy = 0
        if event.keysym == "Left": dx = -step
        elif event.keysym == "Right": dx = step
        elif event.keysym == "Up": dy = -step
        elif event.keysym == "Down": dy = step
        else: return
        x1, y1, x2, y2 = self.rect
        self.rect = self.snap.clamp_rect(
            self.snap.snap_move_rect([x1 + dx, y1 + dy, x2 + dx, y2 + dy]),
            self._max_w, self._max_h)
        self.redraw()
        return "break"

    def finish(self, event=None):
        if not self.rect:
            if not self.continuous:
                messagebox.showwarning("No selection", "Draw a valid selection first.")
            return
        x1, y1, x2, y2 = self.rect
        self.on_done((x1, y1, x2 - x1, y2 - y1))
        if self.continuous:
            label = f"R{len(self.committed_rects) + 1}"
            self.committed_rects.append((x1, y1, x2, y2, label))
            self.drag_mode = None
            self.drag_start_rect = None
            self.redraw()
            return
        self.destroy()


# ═══════════════════════════════════════════════════════════════════════════════
# SOURCE REGION EDITOR — edit all regions at once on the source
# ═══════════════════════════════════════════════════════════════════════════════

class SourceRegionEditor(tk.Toplevel):
    HANDLE_SIZE = 2

    def __init__(self, master, source_rect: dict, regions: List[CaptureRegion],
                 on_done: Callable, nudge_step: int = 1, snap_to_grid: bool = False,
                 grid_size: int = 10, background_image=None):
        super().__init__(master)
        self.source_rect = source_rect
        self.on_done = on_done
        self.regions = [CaptureRegion(**asdict(r)) for r in regions]
        self.selected_index: Optional[int] = len(self.regions) - 1 if self.regions else None
        self.drag_mode: Optional[str] = None
        self.drag_anchor = (0, 0)
        self.drag_start_rect = None
        self.temp_new_rect: Optional[list] = None
        self.nudge_step = max(1, int(nudge_step))
        self.snap = SnapHelper(snap_to_grid, grid_size)
        self.background_image = background_image
        self.bg_photo = None

        if background_image is None:
            self.overrideredirect(True)
            self.attributes("-topmost", True)
            try:
                self.attributes("-alpha", 0.33)
            except tk.TclError:
                pass
            self.configure(bg="black")
            self.geometry(f'{source_rect["width"]}x{source_rect["height"]}+{source_rect["left"]}+{source_rect["top"]}')
        else:
            self.title("Source region editor — click regions to edit, drag empty area to create, Esc = cancel")
            if not IS_MAC:
                self.attributes("-topmost", True)
            self.configure(bg="black")
            self.geometry(f'{source_rect["width"]}x{source_rect["height"]}+100+100')

        self.canvas = tk.Canvas(self, bg="gray15", highlightthickness=0, cursor="crosshair",
                                width=source_rect["width"], height=source_rect["height"])
        self.canvas.pack(fill="both", expand=True)
        if background_image is not None and ImageTk is not None:
            self.bg_photo = ImageTk.PhotoImage(background_image)
            self.canvas.create_image(0, 0, anchor="nw", image=self.bg_photo, tags="bg")

        self.canvas.create_text(
            16, 14, anchor="nw",
            text=("Source region editor: click a region to select/edit. "
                  "Click and drag outside to create a new region. "
                  "Del = delete selected, Enter = save, Esc = cancel"),
            fill="white", font=("Helvetica", 12, "bold"),
            width=max(300, source_rect["width"] - 32),
        )
        self.canvas.bind("<ButtonPress-1>", self.on_press)
        self.canvas.bind("<B1-Motion>", self.on_drag)
        self.canvas.bind("<ButtonRelease-1>", self.on_release)
        for tag in (self, self.canvas):
            tag.bind("<Delete>", self.on_delete)
            tag.bind("<BackSpace>", self.on_delete)
        self.bind("<Return>", self.finish)
        self.bind("<Escape>", lambda e: self.destroy())
        self.canvas.configure(takefocus=1)
        for seq in ("<Left>", "<Right>", "<Up>", "<Down>",
                     "<Shift-Left>", "<Shift-Right>", "<Shift-Up>", "<Shift-Down>"):
            self.bind(seq, self.on_arrow_key)
            self.canvas.bind(seq, self.on_arrow_key)
        self.focus_force()
        if not IS_MAC:
            self.grab_set()
        self.canvas.focus_set()
        self.redraw()

    @property
    def _max_w(self):
        return self.source_rect["width"]

    @property
    def _max_h(self):
        return self.source_rect["height"]

    def _region_rect(self, region: CaptureRegion):
        return [region.src_x, region.src_y, region.src_x + region.src_w, region.src_y + region.src_h]

    def _hit_test_region(self, idx, x, y):
        region = self.regions[idx]
        x1, y1, x2, y2 = self._region_rect(region)
        hs = self.HANDLE_SIZE + 2
        handles = {
            "nw": (x1 - hs, y1 - hs, x1 + hs, y1 + hs),
            "n": (((x1 + x2) // 2) - hs, y1 - hs, ((x1 + x2) // 2) + hs, y1 + hs),
            "ne": (x2 - hs, y1 - hs, x2 + hs, y1 + hs),
            "w": (x1 - hs, ((y1 + y2) // 2) - hs, x1 + hs, ((y1 + y2) // 2) + hs),
            "e": (x2 - hs, ((y1 + y2) // 2) - hs, x2 + hs, ((y1 + y2) // 2) + hs),
            "sw": (x1 - hs, y2 - hs, x1 + hs, y2 + hs),
            "s": (((x1 + x2) // 2) - hs, y2 - hs, ((x1 + x2) // 2) + hs, y2 + hs),
            "se": (x2 - hs, y2 - hs, x2 + hs, y2 + hs),
        }
        for name, (hx1, hy1, hx2, hy2) in handles.items():
            if hx1 <= x <= hx2 and hy1 <= y <= hy2:
                return name
        if x1 <= x <= x2 and y1 <= y <= y2:
            return "move"
        return None

    def _make_new_region(self, rect):
        x1, y1, x2, y2 = rect
        w, h = max(1, x2 - x1), max(1, y2 - y1)
        default_x, default_y = 10, 10
        if self.regions:
            last = self.regions[-1]
            default_x = last.out_x + last.estimated_width() + 10
            default_y = last.out_y
        return CaptureRegion(
            name=f"Region {len(self.regions) + 1}",
            src_x=x1, src_y=y1, src_w=w, src_h=h,
            out_x=default_x, out_y=default_y,
            target_h=min(DEFAULT_REGION_H, max(20, h)), enabled=True,
        )

    def redraw(self):
        self.canvas.delete("region")
        hs = self.HANDLE_SIZE
        for i, region in enumerate(self.regions):
            x1, y1, x2, y2 = self._region_rect(region)
            color = "orange" if i == self.selected_index else "red"
            width = 3 if i == self.selected_index else 2
            self.canvas.create_rectangle(x1, y1, x2, y2, outline=color, width=width, tags="region")
            self.canvas.create_text(x1 + 4, y1 + 2, anchor="nw", text=region.name, fill="white", tags="region")
            if i == self.selected_index:
                pts = [
                    (x1, y1), ((x1 + x2) // 2, y1), (x2, y1),
                    (x1, (y1 + y2) // 2), (x2, (y1 + y2) // 2),
                    (x1, y2), ((x1 + x2) // 2, y2), (x2, y2),
                ]
                for px, py in pts:
                    self.canvas.create_rectangle(px - hs, py - hs, px + hs - 1, py + hs - 1,
                                                  outline="#6ec6ff", fill="#6ec6ff", width=1, tags="region")
        if self.temp_new_rect:
            x1, y1, x2, y2 = self.temp_new_rect
            self.canvas.create_rectangle(x1, y1, x2, y2, outline="white", width=1, dash=(3, 2), tags="region")

    def on_press(self, event):
        self.canvas.focus_set()
        self.drag_mode = None
        self.drag_start_rect = None
        self.temp_new_rect = None
        hit_index = hit_mode = None
        for i in reversed(range(len(self.regions))):
            hit = self._hit_test_region(i, event.x, event.y)
            if hit:
                hit_index, hit_mode = i, hit
                break
        if hit_index is not None:
            self.selected_index = hit_index
            self.drag_mode = hit_mode
            self.drag_anchor = (event.x, event.y)
            self.drag_start_rect = tuple(self._region_rect(self.regions[hit_index]))
        else:
            self.selected_index = None
            self.drag_mode = "new"
            self.drag_anchor = (event.x, event.y)
            self.drag_start_rect = (event.x, event.y, event.x, event.y)
            self.temp_new_rect = [event.x, event.y, event.x, event.y]
        self.redraw()

    def on_drag(self, event):
        if self.drag_mode is None or self.drag_start_rect is None:
            return
        sx, sy = self.drag_anchor
        x, y = event.x, event.y

        if self.drag_mode == "new":
            self.temp_new_rect = self.snap.clamp_rect(
                self.snap.snap_rect_endpoints([sx, sy, x, y]), self._max_w, self._max_h)
            self.redraw()
            return
        if self.selected_index is None:
            return

        region = self.regions[self.selected_index]
        x1, y1, x2, y2 = self.drag_start_rect

        if self.drag_mode == "move":
            dx, dy = x - sx, y - sy
            w, h = x2 - x1, y2 - y1
            nx1 = min(max(0, x1 + dx), self._max_w - w - 1)
            ny1 = min(max(0, y1 + dy), self._max_h - h - 1)
            rect = self.snap.snap_move_rect([nx1, ny1, nx1 + w, ny1 + h])
        else:
            nx1, ny1, nx2, ny2 = x1, y1, x2, y2
            if "w" in self.drag_mode: nx1 = x
            if "e" in self.drag_mode: nx2 = x
            if "n" in self.drag_mode: ny1 = y
            if "s" in self.drag_mode: ny2 = y
            rect = self.snap.clamp_rect(self.snap.snap_rect_endpoints([nx1, ny1, nx2, ny2]),
                                         self._max_w, self._max_h)

        rect = self.snap.clamp_rect(rect, self._max_w, self._max_h)
        if (rect[2] - rect[0]) >= 5 and (rect[3] - rect[1]) >= 5:
            region.src_x, region.src_y = rect[0], rect[1]
            region.src_w, region.src_h = rect[2] - rect[0], rect[3] - rect[1]
        self.redraw()

    def on_release(self, event):
        if self.drag_mode == "new" and self.temp_new_rect is not None:
            rect = self.snap.normalize_rect(self.snap.snap_rect_endpoints(self.temp_new_rect))
            if (rect[2] - rect[0]) >= 5 and (rect[3] - rect[1]) >= 5:
                region = self._make_new_region(rect)
                self.regions.append(region)
                self.selected_index = len(self.regions) - 1
            self.temp_new_rect = None
            self.redraw()
        elif self.selected_index is not None and self.drag_mode in {"move", "nw", "n", "ne", "w", "e", "sw", "s", "se"}:
            region = self.regions[self.selected_index]
            rect = [region.src_x, region.src_y, region.src_x + region.src_w, region.src_y + region.src_h]
            if self.drag_mode == "move":
                rect = self.snap.clamp_rect(self.snap.snap_move_rect(rect), self._max_w, self._max_h)
            else:
                rect = self.snap.clamp_rect(
                    self.snap.normalize_rect(self.snap.snap_rect_endpoints(rect)),
                    self._max_w, self._max_h)
            region.src_x, region.src_y = rect[0], rect[1]
            region.src_w, region.src_h = rect[2] - rect[0], rect[3] - rect[1]
            self.redraw()
        self.drag_mode = None
        self.drag_start_rect = None

    def on_delete(self, event=None):
        if self.selected_index is None:
            return
        del self.regions[self.selected_index]
        if not self.regions:
            self.selected_index = None
        else:
            self.selected_index = min(self.selected_index, len(self.regions) - 1)
        self.redraw()

    def on_arrow_key(self, event=None):
        self.canvas.focus_set()
        if self.selected_index is None:
            return "break"
        step = self.nudge_step
        if event is not None and (event.state & 0x0001):
            step *= 10
        dx = dy = 0
        if event.keysym == "Left": dx = -step
        elif event.keysym == "Right": dx = step
        elif event.keysym == "Up": dy = -step
        elif event.keysym == "Down": dy = step
        else: return
        region = self.regions[self.selected_index]
        x1, y1, x2, y2 = self._region_rect(region)
        w, h = x2 - x1, y2 - y1
        nx1, ny1 = x1 + dx, y1 + dy
        rect = self.snap.clamp_rect(self.snap.snap_move_rect([nx1, ny1, nx1 + w, ny1 + h]),
                                     self._max_w, self._max_h)
        cw, ch = rect[2] - rect[0], rect[3] - rect[1]
        if cw != w:
            if dx < 0: rect[0], rect[2] = 0, w
            else: rect[2] = self._max_w - 1; rect[0] = rect[2] - w
        if ch != h:
            if dy < 0: rect[1], rect[3] = 0, h
            else: rect[3] = self._max_h - 1; rect[1] = rect[3] - h
        rect = self.snap.clamp_rect(rect, self._max_w, self._max_h)
        region.src_x, region.src_y = rect[0], rect[1]
        region.src_w, region.src_h = rect[2] - rect[0], rect[3] - rect[1]
        self.redraw()
        return "break"

    def finish(self, event=None):
        self.on_done(self.regions)
        self.destroy()


# ═══════════════════════════════════════════════════════════════════════════════
# HUD OUTPUT WINDOW
# ═══════════════════════════════════════════════════════════════════════════════

class HUDOutputWindow(tk.Toplevel):
    def __init__(self, master):
        super().__init__(master)
        self.title("DJ HUD Output")
        self.configure(bg="black")
        self.geometry("1080x80+100+100")
        # Don't make output window always-on-top — it was stealing focus
        self.preview_label = tk.Label(self, bg="black", bd=0)
        self.preview_label.pack(fill="both", expand=True)
        self.photo = None

    def set_image(self, img):
        if ImageTk is None:
            return
        self.photo = ImageTk.PhotoImage(img)
        self.preview_label.configure(image=self.photo)
        # Don't call lift(), focus_force(), or any focus-stealing methods


# ═══════════════════════════════════════════════════════════════════════════════
# DIAGNOSTICS DIALOG
# ═══════════════════════════════════════════════════════════════════════════════

class DiagnosticsDialog(tk.Toplevel):
    """Modal dialog showing system diagnostics results."""

    def __init__(self, master, results, diag):
        super().__init__(master)
        self.title(f"DJ HUD v{__version__} — System Diagnostics")
        self.geometry("700x520")
        self.configure(bg="#1a1a2e")
        self.diag = diag
        self.transient(master)

        # Header
        header = tk.Frame(self, bg="#16213e", padx=16, pady=12)
        header.pack(fill="x")
        tk.Label(header, text=f"DJ HUD v{__version__} System Diagnostics",
                 font=("Helvetica", 14, "bold"), fg="white", bg="#16213e").pack(anchor="w")
        tk.Label(header, text=f"{PLATFORM} | Python {sys.version.split()[0]} | {time.strftime('%Y-%m-%d %H:%M')}",
                 font=("Helvetica", 10), fg="#a0a0a0", bg="#16213e").pack(anchor="w")

        # Summary
        ok_count = sum(1 for r in results if r.status == "ok")
        warn_count = sum(1 for r in results if r.status == "warning")
        err_count = sum(1 for r in results if r.status == "error")

        summary = tk.Frame(self, bg="#1a1a2e", padx=16, pady=8)
        summary.pack(fill="x")
        for count, label, color in [(ok_count, "OK", "#2ecc71"), (warn_count, "Warnings", "#f39c12"),
                                     (err_count, "Errors", "#e74c3c")]:
            tk.Label(summary, text=f"  {count} {label}  ", font=("Helvetica", 11, "bold"),
                     fg=color, bg="#1a1a2e").pack(side="left", padx=(0, 12))

        # Results
        list_frame = tk.Frame(self, bg="#1a1a2e", padx=16, pady=4)
        list_frame.pack(fill="both", expand=True)
        list_frame.columnconfigure(0, weight=1)
        list_frame.rowconfigure(0, weight=1)

        self.text = tk.Text(list_frame, bg="#0f3460", fg="white", font=("Courier", 10),
                            wrap="word", relief="flat", padx=12, pady=8, state="disabled")
        scrollbar = ttk.Scrollbar(list_frame, command=self.text.yview)
        self.text.configure(yscrollcommand=scrollbar.set)
        self.text.grid(row=0, column=0, sticky="nsew")
        scrollbar.grid(row=0, column=1, sticky="ns")

        self.text.tag_configure("ok", foreground="#2ecc71")
        self.text.tag_configure("warning", foreground="#f39c12")
        self.text.tag_configure("error", foreground="#e74c3c")
        self.text.tag_configure("heading", foreground="#e94560", font=("Courier", 10, "bold"))
        self.text.tag_configure("detail", foreground="#a0a0c0")

        self.text.configure(state="normal")
        for r in results:
            icon = {"ok": "[OK]", "warning": "[!!]", "error": "[XX]"}.get(r.status, "[??]")
            self.text.insert("end", f" {icon} ", r.status)
            self.text.insert("end", f" {r.name}\n", "heading")
            self.text.insert("end", f"       {r.message}\n")
            if r.detail:
                self.text.insert("end", f"       {r.detail}\n", "detail")
            self.text.insert("end", "\n")
        self.text.configure(state="disabled")

        # Buttons
        btn_frame = tk.Frame(self, bg="#1a1a2e", padx=16, pady=12)
        btn_frame.pack(fill="x")
        ttk.Button(btn_frame, text="Export to JSON", command=self._export_json).pack(side="left", padx=(0, 8))
        ttk.Button(btn_frame, text="Copy to clipboard", command=self._copy_clipboard).pack(side="left", padx=(0, 8))
        ttk.Button(btn_frame, text="Close", command=self.destroy).pack(side="right")

    def _export_json(self):
        path = filedialog.asksaveasfilename(
            title="Export diagnostics", defaultextension=".json",
            filetypes=[("JSON files", "*.json")],
            initialfile=f"djhud_diagnostics_{time.strftime('%Y%m%d_%H%M%S')}.json")
        if not path:
            return
        with open(path, "w", encoding="utf-8") as f:
            f.write(self.diag.to_json())
        messagebox.showinfo("Exported", f"Diagnostics saved to:\n{path}")

    def _copy_clipboard(self):
        self.clipboard_clear()
        self.clipboard_append(self.diag.to_json())
        messagebox.showinfo("Copied", "Diagnostics JSON copied to clipboard.")
