"""The motion budget — restrained, instrument-like, optional.

Brand law (00-brand §8): motion is instrumentation, not theatre —
≤260 ms, standard easing, never looping (busy pulses excepted). The
whole application's motion inventory is: the busy-chip pulse
(:mod:`~mgf.brand_qt.widgets.chips`), Qt's indeterminate
progress animation, and the page fade below.

The kit-level reduce-motion policy (the desktop analog of
``prefers-reduced-motion``, which Qt does not expose) lives HERE:
consumers wire their persisted setting to :func:`set_reduce_motion`,
and every kit animation consults :func:`reduce_motion` — the state
change happens, the movement doesn't. (PAPER-02's sub-ask: this
docstring used to *name* the policy without the kit shipping it.)
"""

from __future__ import annotations

import contextlib

from PySide6.QtCore import QEasingCurve, QPropertyAnimation
from PySide6.QtWidgets import QGraphicsOpacityEffect, QWidget

from mgf.brand_qt.tokens import DURATION_MS

_REDUCE_MOTION = False


def set_reduce_motion(enabled: bool) -> None:
    """Set the process-wide reduce-motion policy (consumer-persisted).

    Already-running loops (a visible busy pulse) re-consult the policy
    on their next state or visibility change; one-shot animations
    consult it at start.
    """
    global _REDUCE_MOTION
    _REDUCE_MOTION = enabled


def reduce_motion() -> bool:
    """True when the consumer asked for movement-free state changes."""
    return _REDUCE_MOTION


def fade_in(
    widget: QWidget,
    *,
    duration_ms: int | None = None,
    enabled: bool | None = None,
) -> None:
    """Fade ``widget`` in over the brand's fast duration.

    A no-op (instant full opacity) when disabled. ``enabled=None`` —
    the default — defers to the kit policy (:func:`reduce_motion`);
    an explicit bool overrides it for this call. The effect removes
    itself when done so it never lingers on the widget.
    """
    if enabled is None:
        enabled = not _REDUCE_MOTION
    if not enabled:
        widget.setGraphicsEffect(None)  # type: ignore[arg-type]
        return
    effect = QGraphicsOpacityEffect(widget)
    widget.setGraphicsEffect(effect)
    animation = QPropertyAnimation(effect, b"opacity", widget)
    animation.setDuration(duration_ms if duration_ms is not None else DURATION_MS["fast"])
    animation.setStartValue(0.0)
    animation.setEndValue(1.0)
    animation.setEasingCurve(QEasingCurve.Type.OutCubic)

    def _clear_effect() -> None:
        # The widget may already be gone when the animation lands
        # (fast window teardown) — never let that cross the event loop.
        with contextlib.suppress(RuntimeError):
            widget.setGraphicsEffect(None)  # type: ignore[arg-type]

    animation.finished.connect(_clear_effect)
    animation.start(QPropertyAnimation.DeletionPolicy.DeleteWhenStopped)


__all__ = ["fade_in", "reduce_motion", "set_reduce_motion"]
