"""UI helpers and reusable Tk callbacks."""

from __future__ import annotations

import colorsys
import tkinter as tk
import tkinter.font as tkfont
from tkinter import ttk


class UIBuilderMixin:
    """Constructs the Tkinter UI and common widgets."""

    def setup_ui(self) -> None:
        self._create_titlebar()

        toolbar = ttk.Frame(self.root)
        toolbar.pack(fill=tk.X, padx=12, pady=(4, 2))
        buttons = [
            ("📂", self._t("toolbar.open_image"), self.load_image),
            ("📁", self._t("toolbar.open_folder"), self.load_folder),
            ("🎨", self._t("toolbar.choose_color"), self.choose_color),
            ("🖱", self._t("toolbar.pick_from_image"), self.enable_pick_mode),
            ("💾", self._t("toolbar.save_overlay"), self.save_overlay),
            ("△", self._t("toolbar.toggle_free_draw"), self.toggle_exclusion_mode),
            ("🧹", self._t("toolbar.clear_excludes"), self.clear_excludes),
            ("↩", self._t("toolbar.undo_exclude"), self.undo_exclude),
            ("🔄", self._t("toolbar.reset_sliders"), self.reset_sliders),
            ("🌓", self._t("toolbar.toggle_theme"), self.toggle_theme),
        ]
        self._toolbar_buttons: list[dict[str, object]] = []
        self._nav_buttons: list[tk.Button] = []

        buttons_frame = ttk.Frame(toolbar)
        buttons_frame.pack(side=tk.LEFT)
        for icon, label, command in buttons:
            self._add_toolbar_button(buttons_frame, icon, label, command)

        status_container = ttk.Frame(toolbar)
        status_container.pack(side=tk.RIGHT, expand=True, fill=tk.X)
        self.status = ttk.Label(
            status_container,
            text=self._t("status.no_file"),
            anchor="e",
            foreground="#efefef",
        )
        self.status.pack(fill=tk.X)
        self._attach_copy_menu(self.status)
        self.status_default_text = self.status.cget("text")
        self._status_palette = {"fg": self.status.cget("foreground")}

        palette_frame = ttk.Frame(self.root)
        palette_frame.pack(fill=tk.X, padx=12, pady=(6, 8))
        default_colour = self._default_colour_hex()

        current_frame = ttk.Frame(palette_frame)
        current_frame.pack(side=tk.LEFT, padx=(0, 16))
        ttk.Label(current_frame, text=self._t("palette.current")).pack(side=tk.LEFT, padx=(0, 6))
        self.current_colour_sw = tk.Canvas(
            current_frame,
            width=24,
            height=24,
            highlightthickness=0,
            background=default_colour,
            bd=0,
        )
        self.current_colour_sw.pack(side=tk.LEFT, pady=2)
        self.current_colour_label = ttk.Label(current_frame, text=f"({default_colour})")
        self.current_colour_label.pack(side=tk.LEFT, padx=(6, 0))

        ttk.Label(palette_frame, text=self._t("palette.more")).pack(side=tk.LEFT, padx=(0, 8))
        swatch_container = ttk.Frame(palette_frame)
        swatch_container.pack(side=tk.LEFT)
        for name, hex_code in self._preset_colours():
            self._add_palette_swatch(swatch_container, name, hex_code)

        sliders_frame = ttk.Frame(self.root)
        sliders_frame.pack(fill=tk.X, padx=12, pady=4)
        sliders = [
            (self._t("sliders.hue_min"), self.hue_min, 0, 360),
            (self._t("sliders.hue_max"), self.hue_max, 0, 360),
            (self._t("sliders.sat_min"), self.sat_min, 0, 100),
            (self._t("sliders.val_min"), self.val_min, 0, 100),
            (self._t("sliders.val_max"), self.val_max, 0, 100),
            (self._t("sliders.alpha"), self.alpha, 0, 255),
        ]
        for index, (label, variable, minimum, maximum) in enumerate(sliders):
            self.add_slider_with_value(sliders_frame, label, variable, minimum, maximum, column=index)
            sliders_frame.grid_columnconfigure(index, weight=1)

        main = ttk.Frame(self.root)
        main.pack(fill=tk.BOTH, expand=True, padx=12, pady=12)

        left_column = ttk.Frame(main)
        left_column.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 6))
        left_column.grid_columnconfigure(1, weight=1)
        left_column.grid_rowconfigure(0, weight=1)

        self._create_navigation_button(left_column, "◀", self.show_previous_image, column=0)

        self.canvas_orig = tk.Canvas(
            left_column,
            bg=self._canvas_background_colour(),
            highlightthickness=0,
            relief="flat",
        )
        self.canvas_orig.grid(row=0, column=1, sticky="nsew")
        self.canvas_orig.bind("<Button-1>", self.on_canvas_click)
        self.canvas_orig.bind("<ButtonPress-3>", self._exclude_start)
        self.canvas_orig.bind("<B3-Motion>", self._exclude_drag)
        self.canvas_orig.bind("<ButtonRelease-3>", self._exclude_end)

        right_column = ttk.Frame(main)
        right_column.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(6, 0))
        right_column.grid_columnconfigure(0, weight=1)
        right_column.grid_rowconfigure(0, weight=1)

        self.canvas_overlay = tk.Canvas(
            right_column,
            bg=self._canvas_background_colour(),
            highlightthickness=0,
            relief="flat",
        )
        self.canvas_overlay.grid(row=0, column=0, sticky="nsew")
        self._create_navigation_button(right_column, "▶", self.show_next_image, column=1)


        info_frame = ttk.Frame(self.root)
        info_frame.pack(fill=tk.X, padx=12, pady=(0, 12))
        self.filename_label = ttk.Label(
            info_frame,
            text="—",
            font=("Segoe UI", 10, "bold"),
            anchor="center",
            justify="center",
        )
        self.filename_label.pack(anchor="center")
        self._attach_copy_menu(self.filename_label)

        self.ratio_label = ttk.Label(
            info_frame,
            text=self._t("stats.placeholder"),
            font=("Segoe UI", 10, "bold"),
            anchor="center",
            justify="center",
        )
        self.ratio_label.pack(anchor="center", pady=(4, 0))
        self._attach_copy_menu(self.ratio_label)

        self.root.bind("<Escape>", self.disable_pick_mode)
        self.root.bind("<ButtonPress-1>", self._maybe_focus_window)

    def add_slider_with_value(self, parent, text, var, minimum, maximum, column=0):
        cell = ttk.Frame(parent)
        cell.grid(row=0, column=column, sticky="we", padx=6)
        header = ttk.Frame(cell)
        header.pack(fill="x")
        name_lbl = ttk.Label(header, text=text)
        name_lbl.pack(side="left")
        self._attach_copy_menu(name_lbl)
        val_lbl = ttk.Label(header, text=f"{float(var.get()):.0f}")
        val_lbl.pack(side="right")
        self._attach_copy_menu(val_lbl)
        style_name = getattr(self, "scale_style", "Horizontal.TScale")
        ttk.Scale(
            cell,
            from_=minimum,
            to=maximum,
            orient="horizontal",
            variable=var,
            style=style_name,
            command=self.on_slider_change,
        ).pack(fill="x", pady=(2, 8))

        def on_var_change(*_):
            val_lbl.config(text=f"{float(var.get()):.0f}")

        try:
            var.trace_add("write", on_var_change)
        except Exception:
            var.trace("w", lambda *_: on_var_change())  # type: ignore[attr-defined]

    def on_slider_change(self, *_):
        if self._update_job is not None:
            try:
                self.root.after_cancel(self._update_job)
            except Exception:
                pass
        self._update_job = self.root.after(self.update_delay_ms, self.update_preview)

    def _preset_colours(self):
        return [
            (self._t("palette.swatch.red"), "#ff3b30"),
            (self._t("palette.swatch.orange"), "#ff9500"),
            (self._t("palette.swatch.yellow"), "#ffd60a"),
            (self._t("palette.swatch.green"), "#34c759"),
            (self._t("palette.swatch.teal"), "#5ac8fa"),
            (self._t("palette.swatch.blue"), "#0a84ff"),
            (self._t("palette.swatch.violet"), "#af52de"),
            (self._t("palette.swatch.magenta"), "#ff2d55"),
            (self._t("palette.swatch.white"), "#ffffff"),
            (self._t("palette.swatch.grey"), "#8e8e93"),
            (self._t("palette.swatch.black"), "#000000"),
        ]

    def _add_palette_swatch(self, parent, name: str, hex_code: str) -> None:
        swatch = tk.Canvas(
            parent,
            width=24,
            height=24,
            highlightthickness=0,
            background=hex_code,
            bd=0,
            relief="flat",
            takefocus=1,
            cursor="hand2",
        )
        swatch.pack(side=tk.LEFT, padx=4, pady=2)

        def trigger(_event=None, colour=hex_code, label=name):
            self.apply_sample_colour(colour, label)

        swatch.bind("<Button-1>", trigger)
        swatch.bind("<space>", trigger)
        swatch.bind("<Return>", trigger)
        swatch.bind("<Enter>", lambda _e: swatch.configure(cursor="hand2"))
        swatch.bind("<Leave>", lambda _e: swatch.configure(cursor="arrow"))

    def _add_toolbar_button(self, parent, icon: str, label: str, command) -> None:
        font = tkfont.Font(root=self.root, family="Segoe UI", size=9)
        padding_x = 12
        gap = font.measure("  ")
        icon_width = font.measure(icon) or font.measure("  ")
        label_width = font.measure(label)
        width = padding_x * 2 + icon_width + gap + label_width
        height = 28
        radius = 9
        bg = self.root.cget("bg") if hasattr(self.root, "cget") else "#f2f2f7"
        canvas = tk.Canvas(
            parent,
            width=width,
            height=height,
            bd=0,
            highlightthickness=0,
            bg=bg,
            relief="flat",
            cursor="hand2",
            takefocus=1,
        )
        canvas.pack(side=tk.LEFT, padx=4, pady=1)

        palette = self._toolbar_palette()
        rect_id = self._create_round_rect(
            canvas,
            1,
            1,
            width - 1,
            height - 1,
            radius,
            fill=palette["normal"],
            outline=palette["outline"],
            width=1,
        )
        icon_id = canvas.create_text(
            padding_x,
            height / 2,
            text=icon,
            font=font,
            fill=palette["text"],
            anchor="w",
        )
        label_id = canvas.create_text(
            padding_x + icon_width + gap,
            height / 2,
            text=label,
            font=font,
            fill=palette["text"],
            anchor="w",
        )

        button_data = {
            "canvas": canvas,
            "rect": rect_id,
            "text_ids": (icon_id, label_id),
            "command": command,
            "palette": palette.copy(),
            "dimensions": (width, height, radius),
        }
        self._toolbar_buttons.append(button_data)

        def set_fill(state: str) -> None:
            pal: dict[str, str] = button_data["palette"]  # type: ignore[index]
            canvas.itemconfigure(rect_id, fill=pal[state])  # type: ignore[index]

        def execute():
            command()

        def on_press(_event=None):
            set_fill("active")

        def on_release(event=None):
            if event is not None and (
                event.x < 0 or event.y < 0 or event.x > width or event.y > height
            ):
                set_fill("normal")
                return
            set_fill("hover")
            self.root.after_idle(execute)

        def on_enter(_event):
            set_fill("hover")

        def on_leave(_event):
            set_fill("normal")

        def on_focus_in(_event):
            pal: dict[str, str] = button_data["palette"]  # type: ignore[index]
            canvas.itemconfigure(rect_id, outline=pal["outline_focus"])  # type: ignore[index]

        def on_focus_out(_event):
            pal: dict[str, str] = button_data["palette"]  # type: ignore[index]
            canvas.itemconfigure(rect_id, outline=pal["outline"])  # type: ignore[index]

        def invoke_keyboard(_event=None):
            set_fill("active")
            canvas.after(120, lambda: set_fill("hover"))
            self.root.after_idle(execute)

        canvas.bind("<ButtonPress-1>", on_press)
        canvas.bind("<ButtonRelease-1>", on_release)
        canvas.bind("<Enter>", on_enter)
        canvas.bind("<Leave>", on_leave)
        canvas.bind("<FocusIn>", on_focus_in)
        canvas.bind("<FocusOut>", on_focus_out)
        canvas.bind("<space>", invoke_keyboard)
        canvas.bind("<Return>", invoke_keyboard)

    @staticmethod
    def _create_round_rect(canvas: tk.Canvas, x1, y1, x2, y2, radius, **kwargs):
        points = [
            x1 + radius,
            y1,
            x2 - radius,
            y1,
            x2,
            y1,
            x2,
            y1 + radius,
            x2,
            y2 - radius,
            x2,
            y2,
            x2 - radius,
            y2,
            x1 + radius,
            y2,
            x1,
            y2,
            x1,
            y2 - radius,
            x1,
            y1 + radius,
            x1,
            y1,
        ]
        return canvas.create_polygon(points, smooth=True, splinesteps=24, **kwargs)

    def _create_navigation_button(self, container, symbol: str, command, *, column: int) -> None:
        palette = self._navigation_palette()
        bg = palette["bg"]
        fg = palette["fg"]
        container.grid_rowconfigure(0, weight=1)
        btn = tk.Button(
            container,
            text=symbol,
            command=command,
            font=("Segoe UI", 26, "bold"),
            relief="flat",
            borderwidth=0,
            background=bg,
            activebackground=bg,
            highlightthickness=0,
            fg=fg,
            activeforeground=fg,
            cursor="hand2",
            width=2,
        )
        btn.grid(row=0, column=column, sticky="ns", padx=6)
        self._nav_buttons.append(btn)

    def _create_titlebar(self) -> None:
        bar_bg = "#1f1f1f"
        title_bar = tk.Frame(self.root, bg=bar_bg, relief="flat", height=34)
        title_bar.pack(fill=tk.X, side=tk.TOP)
        title_bar.pack_propagate(False)

        logo = None
        try:
            from PIL import Image, ImageTk  # type: ignore
            from importlib import resources

            logo_resource = resources.files("app.assets").joinpath("logo.png")
            with resources.as_file(logo_resource) as logo_path:
                image = Image.open(logo_path).convert("RGBA")
                image.thumbnail((26, 26))
                logo = ImageTk.PhotoImage(image)
        except Exception:
            logo = None

        if logo is not None:
            logo_label = tk.Label(title_bar, image=logo, bg=bar_bg)
            logo_label.image = logo  # keep reference
            logo_label.pack(side=tk.LEFT, padx=(10, 6), pady=4)

        title_label = tk.Label(
            title_bar,
            text=self._t("app.title"),
            bg=bar_bg,
            fg="#f5f5f5",
            font=("Segoe UI", 11, "bold"),
            anchor="w",
        )
        title_label.pack(side=tk.LEFT, padx=6)

        close_btn = tk.Button(
            title_bar,
            text="✕",
            command=self._close_app,
            bg=bar_bg,
            fg="#f5f5f5",
            activebackground="#ff3b30",
            activeforeground="#ffffff",
            borderwidth=0,
            highlightthickness=0,
            relief="flat",
            font=("Segoe UI", 10, "bold"),
            cursor="hand2",
            width=3,
        )
        close_btn.pack(side=tk.RIGHT, padx=8, pady=4)
        close_btn.bind("<Enter>", lambda _e: close_btn.configure(bg="#cf212f"))
        close_btn.bind("<Leave>", lambda _e: close_btn.configure(bg=bar_bg))

        for widget in (title_bar, title_label):
            widget.bind("<ButtonPress-1>", self._start_window_drag)
            widget.bind("<B1-Motion>", self._perform_window_drag)

    def _close_app(self) -> None:
        try:
            self.root.destroy()
        except Exception:
            pass

    def _start_window_drag(self, event) -> None:
        self._drag_offset = (event.x_root - self.root.winfo_rootx(), event.y_root - self.root.winfo_rooty())

    def _perform_window_drag(self, event) -> None:
        offset = getattr(self, "_drag_offset", None)
        if offset is None:
            return
        x = event.x_root - offset[0]
        y = event.y_root - offset[1]
        self.root.geometry(f"+{x}+{y}")

    def _maybe_focus_window(self, _event) -> None:
        try:
            self.root.focus_set()
        except Exception:
            pass

    def _toolbar_palette(self) -> dict[str, str]:
        is_dark = getattr(self, "theme", "light") == "dark"
        if is_dark:
            return {
                "normal": "#2f2f35",
                "hover": "#3a3a40",
                "active": "#1f1f25",
                "outline": "#4d4d50",
                "outline_focus": "#7c7c88",
                "text": "#f1f1f5",
            }
        return {
            "normal": "#ffffff",
            "hover": "#ededf4",
            "active": "#dcdce6",
            "outline": "#d0d0d8",
            "outline_focus": "#a9a9b2",
            "text": "#1f1f1f",
        }

    def _navigation_palette(self) -> dict[str, str]:
        is_dark = getattr(self, "theme", "light") == "dark"
        default_bg = "#0f0f10" if is_dark else "#ededf2"
        bg = self.root.cget("bg") if hasattr(self.root, "cget") else default_bg
        fg = "#f5f5f5" if is_dark else "#1f1f1f"
        return {"bg": bg, "fg": fg}

    def _refresh_toolbar_buttons_theme(self) -> None:
        if not getattr(self, "_toolbar_buttons", None):
            return
        bg = self.root.cget("bg") if hasattr(self.root, "cget") else "#f2f2f7"
        palette = self._toolbar_palette()
        for data in self._toolbar_buttons:
            canvas = data["canvas"]  # type: ignore[index]
            rect_id = data["rect"]  # type: ignore[index]
            text_ids = data["text_ids"]  # type: ignore[index]
            data["palette"] = palette.copy()
            canvas.configure(bg=bg)
            canvas.itemconfigure(rect_id, fill=palette["normal"], outline=palette["outline"])
            for text_id in text_ids:
                canvas.itemconfigure(text_id, fill=palette["text"])

    def _refresh_navigation_buttons_theme(self) -> None:
        if not getattr(self, "_nav_buttons", None):
            return
        palette = self._navigation_palette()
        for btn in self._nav_buttons:
            btn.configure(
                background=palette["bg"],
                activebackground=palette["bg"],
                fg=palette["fg"],
                activeforeground=palette["fg"],
            )

    def _canvas_background_colour(self) -> str:
        return "#0f0f10" if getattr(self, "theme", "light") == "dark" else "#ffffff"

    def _refresh_canvas_backgrounds(self) -> None:
        bg = self._canvas_background_colour()
        for attr in ("canvas_orig", "canvas_overlay"):
            canvas = getattr(self, attr, None)
            if canvas is not None:
                try:
                    canvas.configure(bg=bg)
                except Exception:
                    pass

    def _refresh_status_palette(self, fg: str) -> None:
        self.status.configure(foreground=fg)
        self._status_palette["fg"] = fg

    def _refresh_accent_labels(self, colour: str) -> None:
        try:
            self.filename_label.configure(foreground=colour)
            self.ratio_label.configure(foreground=colour)
        except Exception:
            pass

    def _default_colour_hex(self) -> str:
        defaults = getattr(self, "DEFAULTS", {})
        hue_min = float(defaults.get("hue_min", 0.0))
        hue_max = float(defaults.get("hue_max", hue_min))
        if hue_min <= hue_max:
            hue = (hue_min + hue_max) / 2.0
        else:
            span = ((hue_max + 360.0) - hue_min) / 2.0
            hue = (hue_min + span) % 360.0

        sat_min = float(defaults.get("sat_min", 0.0))
        saturation = (sat_min + 100.0) / 2.0

        val_min = float(defaults.get("val_min", 0.0))
        val_max = float(defaults.get("val_max", 100.0))
        value = (val_min + val_max) / 2.0

        r, g, b = colorsys.hsv_to_rgb(hue / 360.0, saturation / 100.0, value / 100.0)
        return f"#{int(r * 255):02x}{int(g * 255):02x}{int(b * 255):02x}"

    def _init_copy_menu(self):
        self._copy_target = None
        self.copy_menu = tk.Menu(self.root, tearoff=0)
        label = self._t("menu.copy") if hasattr(self, "_t") else "Copy"
        self.copy_menu.add_command(label=label, command=self._copy_current_label)

    def _attach_copy_menu(self, widget):
        widget.bind("<Button-3>", lambda event, w=widget: self._show_copy_menu(event, w))
        widget.bind("<Control-c>", lambda event, w=widget: self._copy_widget_text(w))

    def _show_copy_menu(self, event, widget):
        self._copy_target = widget
        try:
            self.copy_menu.tk_popup(event.x_root, event.y_root)
        finally:
            self.copy_menu.grab_release()

    def _copy_current_label(self):
        if self._copy_target is not None:
            self._copy_widget_text(self._copy_target)

    def _copy_widget_text(self, widget):
        try:
            text = widget.cget("text")
        except Exception:
            text = ""
        if not text:
            return
        try:
            self.root.clipboard_clear()
            self.root.clipboard_append(text)
        except Exception:
            pass


__all__ = ["UIBuilderMixin"]
