"""Experimental features for anywidget."""

from __future__ import annotations

import dataclasses
import typing

import psygnal

from ._descriptor import MimeBundleDescriptor

if typing.TYPE_CHECKING:  # pragma: no cover
    import pathlib

    from ._protocols import WidgetBase

__all__ = ["dataclass", "widget", "MimeBundleDescriptor"]

_T = typing.TypeVar("_T")
T = typing.TypeVar("T")


def widget(
    *,
    esm: str | pathlib.Path,
    css: None | str | pathlib.Path = None,
    **kwargs: typing.Any,
) -> typing.Callable[[T], T]:
    """Decorator to register a widget class as a mimebundle.

    Parameters
    ----------
    esm : str | pathlib.Path
        The path or contents of an ES Module for the widget.
    css : None | str | pathlib.Path, optional
        The path or contents of a CSS file for the widget.
    **kwargs
        Additional keyword arguments to pass to the

    Returns
    -------
    Callable
        A decorator that registers the widget class as a mimebundle.
    """
    kwargs["_esm"] = esm
    if css is not None:
        kwargs["_css"] = css

    def _decorator(cls: _T) -> _T:
        setattr(cls, "_repr_mimebundle_", MimeBundleDescriptor(**kwargs))  # noqa: B010
        return cls

    return _decorator


# To preserve the signature of the decorated class.
# see: https://github.com/pyapp-kit/magicgui/blob/5e068f31eaeeb130f43c38727b25423cc3ea4de3/src/magicgui/schema/_guiclass.py#L145-L162
def __dataclass_transform__(
    *,
    eq_default: bool = True,
    order_default: bool = False,
    kw_only_default: bool = False,
    field_specifiers: tuple[type | typing.Callable[..., typing.Any], ...] = (()),
) -> typing.Callable[[_T], _T]:
    return lambda a: a


@__dataclass_transform__(field_specifiers=(dataclasses.Field, dataclasses.field))
def dataclass(
    cls: T | None = None,
    *,
    esm: str | pathlib.Path,
    css: None | str | pathlib.Path = None,
    **dataclass_kwargs: typing.Any,
) -> typing.Callable[[T], T]:
    """Turns class into a dataclass, makes it evented, and registers it as a widget.

    Parameters
    ----------
    cls : T | None
        The class to decorate.
    esm : str | pathlib.Path
        The path or contents of an ES Module for the widget.
    css : None | str | pathlib.Path, optional
        The path or contents of a CSS file for the widget.
    dataclass_kwargs : typing.Any
        Additional keyword arguments to pass to the dataclass decorator.

    Returns
    -------
    type
        The evented dataclass.

    Examples
    --------
    >>> @dataclass(esm="index.js")
    ... class Counter:
    ...     value: int = 0
    ...
    >>> counter = Counter()
    >>> counter.value = 1
    >>> counter
    """

    def _decorator(cls: T) -> T:
        cls = dataclasses.dataclass(cls, **dataclass_kwargs)  # type: ignore[call-overload]
        cls = psygnal.evented(cls)  # type: ignore[call-overload]
        cls = widget(esm=esm, css=css)(cls)
        return cls

    return _decorator(cls) if cls is not None else _decorator  # type: ignore[return-value]


_ANYWIDGET_COMMAND = "_anywidget_command"
_ANYWIDGET_COMMANDS = "_anywidget_commands"

_AnyWidgetCommand = typing.Callable[
    [typing.Any, typing.Any, typing.List[bytes]],
    typing.Tuple[typing.Any, typing.List[bytes]],
]


def command(cmd: _AnyWidgetCommand) -> _AnyWidgetCommand:
    """Mark a function as a command for anywidget."""
    setattr(cmd, _ANYWIDGET_COMMAND, True)
    return cmd


def _collect_anywidget_commands(widget_cls: type) -> None:
    cmds: dict[str, _AnyWidgetCommand] = {}
    for base in widget_cls.__mro__:
        if not hasattr(base, "__dict__"):
            continue
        for name, attr in base.__dict__.items():
            if callable(attr) and getattr(attr, _ANYWIDGET_COMMAND, False):
                cmds[name] = attr

    setattr(widget_cls, _ANYWIDGET_COMMANDS, cmds)


def _register_anywidget_commands(widget: WidgetBase) -> None:
    """Register a custom message reducer for a widget if it implements the protocol."""
    # Only add the callback if the widget has any commands.
    cmds = typing.cast(
        "dict[str, _AnyWidgetCommand]",
        getattr(type(widget), _ANYWIDGET_COMMANDS, {}),
    )
    if not cmds:
        return None

    def handle_anywidget_command(
        self: WidgetBase, msg: str | list | dict, buffers: list[bytes]
    ) -> None:
        if not isinstance(msg, dict) or msg.get("kind") != "anywidget-command":
            return
        cmd = cmds[msg["name"]]
        response, buffers = cmd(widget, msg["msg"], buffers)
        self.send(
            {
                "id": msg["id"],
                "kind": "anywidget-command-response",
                "response": response,
            },
            buffers,
        )

    widget.on_msg(handle_anywidget_command)
