"""
Absfuyu: GUI
------------
Custom tkinter GUI

Version: 6.8.0
Date updated: 26/04/2026 (dd/mm/yyyy)
"""

# Module level
# ---------------------------------------------------------------------------
__all__ = ["CustomTkinterApp", "ScrollableFrame"]


# Library
# ---------------------------------------------------------------------------
import tkinter as tk
from tkinter import ttk

from absfuyu.pkg_data.logo import AbsfuyuLogo


# Class
# ---------------------------------------------------------------------------
class CustomTkinterApp(tk.Tk):
    """
    Custom Tkinter Application GUI
    """

    def __init__(self, title: str | None = None, size: tuple[int, int] | None = None) -> None:
        """
        Custom Tkinter GUI

        Parameters
        ----------
        title : str | None, optional
            Title of the app, by default None

        size : tuple[int, int] | None, optional
            Size of the app (width, height), by default None
        """
        super().__init__()

        # Set custom icon
        self.iconphoto(True, tk.PhotoImage(data=AbsfuyuLogo.SHORT))

        # Title
        self.title(title)

        # Set size
        self._absfuyu_set_width_height(size)

    # @versionadded("5.10.0")
    def _absfuyu_set_width_height(self, size: tuple[int, int] | None = None) -> None:
        """
        Set width and height for the app.

        Parameters
        ----------
        size : tuple[int, int] | None, optional
            Size of the app (width, height), by default ``None``
            (420 x 250)
        """
        # Set GUI appears in center
        if size is None:
            _width = 420
            _height = 250
        else:
            _width, _height = size

        _width_screen = self.winfo_screenwidth()
        _width_screen_offset = 0.0052  # x offset mutiplier
        _x_offset = int(_width_screen * _width_screen_offset)

        _height_screen = self.winfo_screenheight()
        _height_screen_offset = 0.0926  # y offset mutiplier
        _y_offset = int(_height_screen * _height_screen_offset)

        if _width > _width_screen:
            _width = int(_width_screen * (1 - 0.001))
            # _x_offset = 0

        if _height > (_height_screen - _y_offset):
            _height = int(_height_screen * (1 - 0.001))
            _y_offset = 0

        _x = (_width_screen / 2) - (_width / 2) - _x_offset
        _y = (_height_screen / 2) - (_height / 2) - _y_offset

        self.geometry(f"{_width}x{_height}+{int(_x)}+{int(_y)}")


# @versionadded("6.7.0")
class ScrollableFrame(ttk.Frame):
    """
    Flexible scrollable frame with configurable scroll behavior.


    Example:
    --------
    >>> root = tk.Tk()
    ... root.geometry("200x400")
    ... sf = ScrollableFrame(root, scroll_x=True)
    ... sf.pack(fill="both", expand=True)
    ... for i in range(200):  # Add widgets
    ...     lbl = sf.add(ttk.Label, text="Item " + str(i) + " " + "-" * 50)
    ...     lbl.pack()
    ... root.mainloop()
    """

    def __init__(
        self,
        container: tk.Widget,
        *,
        scroll_x: bool = False,
        scroll_y: bool = True,
        show_scrollbar: bool = True,
        **kwargs,
    ) -> None:
        """
        Flexible scrollable frame with configurable scroll behavior.

        Parameters
        ----------
        container : tk.Widget
            Parent widget

        scroll_x : bool
            Enable horizontal scrolling, by default False

        scroll_y : bool
            Enable vertical scrolling, by default True

        show_scrollbar : bool
            Whether to display scrollbars, by default True
        """
        super().__init__(container, **kwargs)

        self.scroll_x = scroll_x
        self.scroll_y = scroll_y

        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(0, weight=1)

        # Canvas
        self.canvas = tk.Canvas(self, highlightthickness=0)
        self.canvas.grid(row=0, column=0, sticky="nsew")

        # Inner frame
        self.inner = ttk.Frame(self.canvas)
        self.window_id = self.canvas.create_window((0, 0), window=self.inner, anchor="nw")

        # Scrollbars
        if show_scrollbar:
            if scroll_y:
                self.v_scrollbar = ttk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
                self.v_scrollbar.grid(row=0, column=1, sticky="ns")
                self.canvas.configure(yscrollcommand=self.v_scrollbar.set)

            if scroll_x:
                self.h_scrollbar = ttk.Scrollbar(self, orient="horizontal", command=self.canvas.xview)
                self.h_scrollbar.grid(row=1, column=0, sticky="ew")
                self.canvas.configure(xscrollcommand=self.h_scrollbar.set)

        # Update scroll region
        self.inner.bind("<Configure>", self._update_scrollregion)
        self.canvas.bind("<Configure>", self._resize_inner)

        # Bind recursively
        self._bind_recursive(self.inner)

        # Make canvas focusable
        self.canvas.configure(takefocus=True)

        # Focus when hover
        self.canvas.bind("<Enter>", lambda e: self.canvas.focus_set())

        # Bind scroll directly to canvas
        self.canvas.bind("<MouseWheel>", self._on_mousewheel)
        self.canvas.bind("<Shift-MouseWheel>", self._on_shift_mousewheel)
        self.canvas.bind("<Button-4>", self._on_mousewheel)
        self.canvas.bind("<Button-5>", self._on_mousewheel)

    # Core
    # --------------------
    def _update_scrollregion(self, event: tk.Event) -> None:
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))

    def _resize_inner(self, event: tk.Event) -> None:
        """
        Only lock dimension when that axis is NOT scrollable
        """
        # if both enabled -> DO NOT lock anything
        if self.scroll_y and not self.scroll_x:
            # vertical only -> lock width
            self.canvas.itemconfig(self.window_id, width=event.width)

        elif self.scroll_x and not self.scroll_y:
            # horizontal only -> lock height
            self.canvas.itemconfig(self.window_id, height=event.height)

    # Mouse scroll
    # --------------------
    def _bind_recursive(self, widget: tk.Widget) -> None:
        """Bind mouse events to all children (robust solution)."""

        # Bind to ALL widgets including Frame
        widget.bind("<MouseWheel>", self._on_mousewheel, add="+")
        widget.bind("<Shift-MouseWheel>", self._on_shift_mousewheel, add="+")
        widget.bind("<Button-4>", self._on_mousewheel, add="+")
        widget.bind("<Button-5>", self._on_mousewheel, add="+")

        for child in widget.winfo_children():
            self._bind_recursive(child)

        # Ensure frame also reacts
        widget.bind("<Enter>", lambda e: widget.focus_set(), add="+")

    def _rebind_children(self, widget: tk.Widget) -> None:
        for child in widget.winfo_children():
            self._bind_recursive(child)

    def _normalize_delta(self, event: tk.Event):
        """
        Normalize scroll delta across platforms.
        Always return ±1 step.
        """
        if event.num == 4:  # Linux up
            return -1
        elif event.num == 5:  # Linux down
            return 1

        if event.delta == 0:
            return 0

        # Normalize to ±1
        return -1 if event.delta > 0 else 1

    def _on_mousewheel(self, event: tk.Event) -> None:
        if not self.scroll_y:
            return

        step = self._normalize_delta(event)
        if step != 0:
            self.canvas.yview_scroll(step, "units")

    def _on_shift_mousewheel(self, event: tk.Event) -> None:
        if not self.scroll_x:
            return

        step = self._normalize_delta(event)
        if step != 0:
            self.canvas.xview_scroll(step, "units")

    def _on_linux_scroll(self, event: tk.Event) -> None:
        delta = -1 if event.num == 4 else 1

        if self.scroll_y:
            self.canvas.yview_scroll(delta, "units")

    # API
    # --------------------
    def add[T: tk.Widget](self, widget_class: type[T], *args, **kwargs) -> T:
        widget = widget_class(self.inner, *args, **kwargs)

        # Bind immediately for new widget
        self._bind_recursive(widget)

        return widget

    def get_frame(self) -> ttk.Frame:
        return self.inner

    def clear(self) -> None:
        for w in self.inner.winfo_children():
            w.destroy()


if __name__ == "__main__":
    app = CustomTkinterApp("absfuyu")
    app.mainloop()
