"""Helpers to place child windows consistently relative to the launcher."""

from __future__ import annotations

import ctypes
import logging
import math
import tkinter as tk
from pathlib import Path

from version_info import module_version

__version__ = module_version(__name__)

MODULE_LOG_NAME = "window_placement"
MODULE_LOG_PATH = Path(__file__).with_name("window_placement.log")
_MODULE_LOGGER = logging.getLogger(MODULE_LOG_NAME)
if not _MODULE_LOGGER.handlers:
    _MODULE_LOGGER.setLevel(logging.DEBUG)
    _MODULE_LOGGER.propagate = False
    _handler = logging.FileHandler(MODULE_LOG_PATH, encoding="utf-8")
    _handler.setFormatter(logging.Formatter("%(asctime)s | %(levelname)s | %(message)s"))
    _MODULE_LOGGER.addHandler(_handler)


def _safe_xy(window: tk.Misc) -> tuple[int | None, int | None]:
    """Return current window coordinates without raising."""

    try:
        return int(window.winfo_x()), int(window.winfo_y())
    except Exception:
        return None, None


def _safe_wh(window: tk.Misc) -> tuple[int | None, int | None]:
    """Return current window size without raising."""

    try:
        return int(window.winfo_width()), int(window.winfo_height())
    except Exception:
        return None, None


def _window_label(window: tk.Misc) -> str:
    """Return a readable label for log messages."""

    try:
        title = str(window.title()).strip()
        if title:
            return title
    except Exception:
        pass
    try:
        return str(window)
    except Exception:
        return "<window>"


def _work_area_bounds(window: tk.Misc) -> tuple[int, int, int, int]:
    """Return the desktop work area excluding the taskbar when available."""

    try:
        if hasattr(ctypes, "windll"):
            class RECT(ctypes.Structure):
                _fields_ = [
                    ("left", ctypes.c_long),
                    ("top", ctypes.c_long),
                    ("right", ctypes.c_long),
                    ("bottom", ctypes.c_long),
                ]

            rect = RECT()
            SPI_GETWORKAREA = 0x0030
            if ctypes.windll.user32.SystemParametersInfoW(SPI_GETWORKAREA, 0, ctypes.byref(rect), 0):
                return int(rect.left), int(rect.top), int(rect.right), int(rect.bottom)
    except Exception:
        pass

    return 0, 0, int(window.winfo_screenwidth()), int(window.winfo_screenheight())


def _taskbar_thickness(window: tk.Misc) -> int:
    """Return the Windows taskbar thickness when it can be determined."""

    try:
        screen_h = int(window.winfo_screenheight())
        _left, _top, _right, work_bottom = _work_area_bounds(window)
        inferred = max(0, screen_h - int(work_bottom))
        if inferred > 0:
            return inferred
    except Exception:
        pass

    try:
        if hasattr(ctypes, "windll"):
            class RECT(ctypes.Structure):
                _fields_ = [
                    ("left", ctypes.c_long),
                    ("top", ctypes.c_long),
                    ("right", ctypes.c_long),
                    ("bottom", ctypes.c_long),
                ]

            class APPBARDATA(ctypes.Structure):
                _fields_ = [
                    ("cbSize", ctypes.c_uint),
                    ("hWnd", ctypes.c_void_p),
                    ("uCallbackMessage", ctypes.c_uint),
                    ("uEdge", ctypes.c_uint),
                    ("rc", RECT),
                    ("lParam", ctypes.c_long),
                ]

            ABM_GETTASKBARPOS = 0x00000005
            abd = APPBARDATA()
            abd.cbSize = ctypes.sizeof(APPBARDATA)
            if ctypes.windll.shell32.SHAppBarMessage(ABM_GETTASKBARPOS, ctypes.byref(abd)):
                rect = abd.rc
                width = max(0, int(rect.right) - int(rect.left))
                height = max(0, int(rect.bottom) - int(rect.top))
                return max(width, height)
    except Exception:
        pass

    return 0


def _window_nonclient_extra(window: tk.Misc) -> tuple[int, int]:
    """Return extra outer frame size added by Windows around the client area."""

    try:
        if not hasattr(ctypes, "windll"):
            return 0, 0

        class RECT(ctypes.Structure):
            _fields_ = [
                ("left", ctypes.c_long),
                ("top", ctypes.c_long),
                ("right", ctypes.c_long),
                ("bottom", ctypes.c_long),
            ]

        class POINT(ctypes.Structure):
            _fields_ = [
                ("x", ctypes.c_long),
                ("y", ctypes.c_long),
            ]

        user32 = ctypes.windll.user32
        hwnd = int(window.winfo_id())
        outer = RECT()
        client = RECT()
        origin = POINT()
        if not user32.GetWindowRect(hwnd, ctypes.byref(outer)):
            return 0, 0
        if not user32.GetClientRect(hwnd, ctypes.byref(client)):
            return 0, 0
        if not user32.ClientToScreen(hwnd, ctypes.byref(origin)):
            return 0, 0

        outer_w = max(0, int(outer.right) - int(outer.left))
        outer_h = max(0, int(outer.bottom) - int(outer.top))
        client_w = max(0, int(client.right) - int(client.left))
        client_h = max(0, int(client.bottom) - int(client.top))
        extra_w = max(0, outer_w - client_w)
        extra_h = max(0, outer_h - client_h)
        return extra_w, extra_h
    except Exception:
        return 0, 0


def _window_outer_bounds(window: tk.Misc) -> tuple[int, int, int, int] | None:
    """Return the actual outer window rect in screen coordinates."""

    try:
        if not hasattr(ctypes, "windll"):
            return None

        class RECT(ctypes.Structure):
            _fields_ = [
                ("left", ctypes.c_long),
                ("top", ctypes.c_long),
                ("right", ctypes.c_long),
                ("bottom", ctypes.c_long),
            ]

        rect = RECT()
        hwnd = int(window.winfo_id())
        if not ctypes.windll.user32.GetWindowRect(hwnd, ctypes.byref(rect)):
            return None
        return int(rect.left), int(rect.top), int(rect.right), int(rect.bottom)
    except Exception:
        return None


def _set_window_alpha(window: tk.Misc, alpha: float) -> None:
    """Best-effort helper to change a window opacity."""

    try:
        window.attributes("-alpha", float(alpha))
    except Exception:
        pass


def _set_window_bounds(child: tk.Misc, x: int, y: int, width: int | None = None, height: int | None = None) -> None:
    """Move a toplevel to the requested bounds, resizing it when dimensions are provided."""

    before_x, before_y = _safe_xy(child)
    before_w, before_h = _safe_wh(child)
    _MODULE_LOGGER.debug(
        "set_bounds.start window=%s from=(%s,%s,%s,%s) target=(%s,%s,%s,%s)",
        _window_label(child),
        before_x,
        before_y,
        before_w,
        before_h,
        x,
        y,
        width,
        height,
    )
    try:
        child.state("normal")
    except Exception:
        pass
    try:
        child.deiconify()
    except Exception:
        pass
    try:
        if hasattr(ctypes, "windll"):
            hwnd = int(child.winfo_id())
            SWP_NOZORDER = 0x0004
            SWP_NOACTIVATE = 0x0010
            flags = SWP_NOZORDER | SWP_NOACTIVATE
            move_w = 0 if width is None else int(width)
            move_h = 0 if height is None else int(height)
            if width is None or height is None:
                flags |= 0x0001  # SWP_NOSIZE
            ctypes.windll.user32.SetWindowPos(hwnd, 0, int(x), int(y), move_w, move_h, flags)
        if width is None or height is None:
            child.geometry(f"+{x}+{y}")
        else:
            child.geometry(f"{int(width)}x{int(height)}+{x}+{y}")
    except Exception:
        fallback_w = max(child.winfo_width(), child.winfo_reqwidth()) if width is None else int(width)
        fallback_h = max(child.winfo_height(), child.winfo_reqheight()) if height is None else int(height)
        child.geometry(f"{fallback_w}x{fallback_h}+{x}+{y}")
    try:
        child.update_idletasks()
    except Exception:
        pass
    after_x, after_y = _safe_xy(child)
    after_w, after_h = _safe_wh(child)
    _MODULE_LOGGER.debug(
        "set_bounds.end window=%s final=(%s,%s,%s,%s) target=(%s,%s,%s,%s)",
        _window_label(child),
        after_x,
        after_y,
        after_w,
        after_h,
        x,
        y,
        width,
        height,
    )


def _set_window_position(child: tk.Misc, x: int, y: int) -> None:
    """Move a toplevel to the requested screen coordinates without resizing it."""

    _set_window_bounds(child, x, y, None, None)


def _ensure_window_position(child: tk.Misc, x: int, y: int, *, tolerance: int = 2) -> None:
    """Only correct the window position when it really drifted from the target."""

    try:
        current_x, current_y = _safe_xy(child)
        if current_x is None or current_y is None:
            _set_window_position(child, x, y)
            return
        if abs(current_x - int(x)) <= tolerance and abs(current_y - int(y)) <= tolerance:
            return
    except Exception:
        pass
    _set_window_position(child, x, y)


def _batch_set_window_positions(windows: list[tk.Misc], positions: list[tuple[int, int]]) -> bool:
    """Try to reposition multiple windows in one Win32 batch to reduce flicker."""

    if not hasattr(ctypes, "windll"):
        return False
    if len(windows) != len(positions) or not windows:
        return False

    try:
        user32 = ctypes.windll.user32
        WM_SETREDRAW = 0x000B
        RDW_INVALIDATE = 0x0001
        RDW_ALLCHILDREN = 0x0080
        RDW_FRAME = 0x0400
        redraw_hwnds: list[int] = []
        hdwp = user32.BeginDeferWindowPos(len(windows))
        if not hdwp:
            return False

        SWP_NOSIZE = 0x0001
        SWP_NOZORDER = 0x0004
        SWP_NOACTIVATE = 0x0010
        SWP_NOREDRAW = 0x0008
        SWP_DEFERERASE = 0x2000
        flags = SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOREDRAW | SWP_DEFERERASE

        for child, (x, y) in zip(windows, positions):
            try:
                child.state("normal")
            except Exception:
                pass
            try:
                child.deiconify()
            except Exception:
                pass
            hwnd = int(child.winfo_id())
            redraw_hwnds.append(hwnd)
            try:
                user32.SendMessageW(hwnd, WM_SETREDRAW, 0, 0)
            except Exception:
                pass
            hdwp = user32.DeferWindowPos(hdwp, hwnd, 0, int(x), int(y), 0, 0, flags)
            if not hdwp:
                for redraw_hwnd in redraw_hwnds:
                    try:
                        user32.SendMessageW(redraw_hwnd, WM_SETREDRAW, 1, 0)
                        user32.RedrawWindow(redraw_hwnd, None, None, RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_FRAME)
                    except Exception:
                        pass
                return False

        ok = bool(user32.EndDeferWindowPos(hdwp))
        for redraw_hwnd in redraw_hwnds:
            try:
                user32.SendMessageW(redraw_hwnd, WM_SETREDRAW, 1, 0)
                user32.RedrawWindow(redraw_hwnd, None, None, RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_FRAME)
            except Exception:
                pass
        return ok
    except Exception:
        return False


def _restack_windows(windows: list[tk.Misc]) -> None:
    """Lift windows from back to front without mixing movement and z-order updates."""

    for child in windows:
        try:
            child.lift()
        except Exception:
            pass


def place_window_below_parent(parent: tk.Misc, child: tk.Misc, *, x_offset: int = 0, y_gap: int = 0):
    """Place ``child`` so its outer top edge sits just below ``parent``.

    The placement uses root-window coordinates and preserves the child's
    computed width/height. Call it after the child has a geometry.
    """

    try:
        parent.update_idletasks()
        child.update_idletasks()
        # On Windows/Tk, ``winfo_rootx`` starts at the inner client area,
        # while ``winfo_x`` tracks the outer window frame. Using ``winfo_x``
        # keeps child windows flush with the launcher's external left border.
        x = parent.winfo_x() + int(x_offset)
        y = parent.winfo_rooty() + parent.winfo_height() + int(y_gap)
        _set_window_position(child, x, y)
    except Exception:
        pass


def place_window_below_parent_later(parent: tk.Misc, child: tk.Misc, *, x_offset: int = 0, y_gap: int = 0):
    """Schedule child placement on the Tk queue after geometry settles."""

    try:
        child.after(0, lambda: place_window_below_parent(parent, child, x_offset=x_offset, y_gap=y_gap))
    except Exception:
        place_window_below_parent(parent, child, x_offset=x_offset, y_gap=y_gap)


def place_window_fullsize_below_parent(parent: tk.Misc, child: tk.Misc, *, x_offset: int = 0, y_gap: int = 0):
    """Place a child below the launcher and size it to the full remaining work area."""

    try:
        parent.update_idletasks()
        child.update_idletasks()
        work_left, _work_top, work_right, work_bottom = _work_area_bounds(parent)
        screen_h = int(parent.winfo_screenheight())
        x = parent.winfo_x() + int(x_offset)
        y = parent.winfo_rooty() + parent.winfo_height() + int(y_gap)
        taskbar_h = _taskbar_thickness(parent)
        usable_bottom = int(work_bottom)
        if taskbar_h > 0:
            usable_bottom = min(usable_bottom, screen_h - int(taskbar_h))
        extra_w, extra_h = _window_nonclient_extra(child)
        width = max(320, int(work_right) - int(x) - int(extra_w))
        height = max(240, int(usable_bottom) - int(y) - int(extra_h))
        _MODULE_LOGGER.debug(
            "fullsize.calc window=%s x=%s y=%s work_right=%s usable_bottom=%s taskbar=%s extra=(%s,%s) final=(%s,%s)",
            _window_label(child),
            x,
            y,
            work_right,
            usable_bottom,
            taskbar_h,
            extra_w,
            extra_h,
            width,
            height,
        )
        _set_window_bounds(child, x, y, width, height)
    except Exception:
        pass


def _fit_window_to_work_area(parent: tk.Misc, child: tk.Misc, *, x_offset: int = 0, y_gap: int = 0) -> None:
    """Trim a rendered child window so its outer frame stays above the taskbar."""

    try:
        if not getattr(child, "winfo_exists", lambda: False)():
            return
        parent.update_idletasks()
        child.update_idletasks()
        _work_left, _work_top, _work_right, work_bottom = _work_area_bounds(parent)
        screen_h = int(parent.winfo_screenheight())
        taskbar_h = _taskbar_thickness(parent)
        usable_bottom = int(work_bottom)
        if taskbar_h > 0:
            usable_bottom = min(usable_bottom, screen_h - int(taskbar_h))
        outer = _window_outer_bounds(child)
        if outer is None:
            return
        left, top, right, bottom = outer
        overflow = int(bottom) - int(usable_bottom)
        _MODULE_LOGGER.debug(
            "fit_window.check window=%s outer=(%s,%s,%s,%s) usable_bottom=%s overflow=%s",
            _window_label(child),
            left,
            top,
            right,
            bottom,
            usable_bottom,
            overflow,
        )
        if overflow <= 0:
            return
        current_w, current_h = _safe_wh(child)
        if current_w is None or current_h is None:
            return
        new_h = max(240, int(current_h) - int(overflow) - 2)
        x = parent.winfo_x() + int(x_offset)
        y = parent.winfo_rooty() + parent.winfo_height() + int(y_gap)
        _MODULE_LOGGER.debug(
            "fit_window.apply window=%s current=(%s,%s) new_h=%s",
            _window_label(child),
            current_w,
            current_h,
            new_h,
        )
        _set_window_bounds(child, x, y, int(current_w), int(new_h))
    except Exception:
        _MODULE_LOGGER.exception("fit_window.error")


def place_window_fullsize_below_parent_later(parent: tk.Misc, child: tk.Misc, *, x_offset: int = 0, y_gap: int = 0):
    """Schedule full-size placement below the launcher after geometry settles."""

    try:
        try:
            _set_window_alpha(child, 0.0)
        except Exception:
            pass
        child.after(0, lambda: place_window_fullsize_below_parent(parent, child, x_offset=x_offset, y_gap=y_gap))
        child.after(120, lambda: _fit_window_to_work_area(parent, child, x_offset=x_offset, y_gap=y_gap))
        child.after(260, lambda: _fit_window_to_work_area(parent, child, x_offset=x_offset, y_gap=y_gap))
        child.after(300, lambda: _set_window_alpha(child, 1.0) if getattr(child, "winfo_exists", lambda: False)() else None)
    except Exception:
        place_window_fullsize_below_parent(parent, child, x_offset=x_offset, y_gap=y_gap)


def tile_children_below_parent(parent: tk.Misc, children: list[tk.Misc], *, gap: int = 8, margin: int = 8):
    """Arrange open children in a compact grid below the launcher."""

    windows = [w for w in children if w is not None and getattr(w, "winfo_exists", lambda: False)()]
    if not windows:
        return

    try:
        parent.update_idletasks()
        start_x = parent.winfo_rootx() + int(margin)
        start_y = parent.winfo_rooty() + parent.winfo_height() + int(margin)
        screen_w = parent.winfo_screenwidth()
        screen_h = parent.winfo_screenheight()
        avail_w = max(320, screen_w - start_x - int(margin))
        avail_h = max(240, screen_h - start_y - int(margin))

        count = len(windows)
        cols = max(1, math.ceil(math.sqrt(count)))
        rows = max(1, math.ceil(count / cols))
        cell_w = max(320, (avail_w - (cols - 1) * int(gap)) // cols)
        cell_h = max(240, (avail_h - (rows - 1) * int(gap)) // rows)

        for idx, child in enumerate(windows):
            row = idx // cols
            col = idx % cols
            x = start_x + col * (cell_w + int(gap))
            y = start_y + row * (cell_h + int(gap))
            child.geometry(f"{cell_w}x{cell_h}+{x}+{y}")
            try:
                child.lift()
            except Exception:
                pass
    except Exception:
        pass


def cascade_children_below_parent(
    parent: tk.Misc,
    children: list[tk.Misc],
    *,
    x_offset_step: int = 20,
    y_offset_step: int = 20,
    margin_left: int = 0,
    margin_top: int = 0,
):
    """Arrange open children in cascade order below the launcher."""

    windows = [w for w in children if w is not None and getattr(w, "winfo_exists", lambda: False)()]
    if not windows:
        return

    try:
        parent.update_idletasks()
        base_x = parent.winfo_x() + int(margin_left)
        base_y = parent.winfo_rooty() + parent.winfo_height() + int(margin_top)
        positions: list[tuple[int, int]] = []
        _MODULE_LOGGER.info(
            "cascade.start parent=%s base=(%s,%s) count=%s x_step=%s y_step=%s",
            _window_label(parent),
            base_x,
            base_y,
            len(windows),
            x_offset_step,
            y_offset_step,
        )

        for idx, child in enumerate(windows):
            child.update_idletasks()
            x = base_x + idx * int(x_offset_step)
            y = base_y + idx * int(y_offset_step)
            positions.append((x, y))
            _MODULE_LOGGER.info(
                "cascade.window index=%s window=%s target=(%s,%s)",
                idx,
                _window_label(child),
                x,
                y,
            )

        batched = _batch_set_window_positions(windows, positions)
        _MODULE_LOGGER.info("cascade.batch_applied=%s", batched)

        for child, (x, y) in zip(windows, positions):
            try:
                if not batched:
                    _set_window_position(child, x, y)
            except Exception:
                pass
        try:
            parent.after(10, lambda wins=list(windows): _restack_windows([w for w in wins if getattr(w, "winfo_exists", lambda: False)()]))
        except Exception:
            _restack_windows(windows)
        for child, (x, y) in zip(windows, positions):
            try:
                child.after(
                    110,
                    lambda w=child, px=x, py=y: _ensure_window_position(w, px, py)
                    if getattr(w, "winfo_exists", lambda: False)()
                    else None,
                )
            except Exception:
                pass
    except Exception:
        _MODULE_LOGGER.exception("cascade.error")
