import inspect
from collections.abc import Callable
from typing import TypeVar, overload

T = TypeVar('T')
R = TypeVar('R')

ToApply = Callable[[T], R] | Callable[[], R]


@overload
def apply(data: T, function: ToApply[T, R], /) -> R: ...


@overload
def apply(function: ToApply[T, R], /) -> Callable[[T], R]: ...


@overload
def apply(data: T, /) -> Callable[[ToApply[T, R]], R]: ...


def apply(
    data: T | ToApply[T, R],
    function: ToApply[T, R] | None = None,
    /,
) -> R | ToApply[T, R]:
    """
    Applies function accepting 1 or 0 arguments to the data.

    Saves the user the manual check for the number of arguments (function's arity).

    Can be used data-first, data-last or data-first without function (assuming the data is not a callable).

    Parameters
    ----------
    data : T
        Input data (positional-only).
    function : Callable[[T], R] | Callable[[], R]
        Function to apply (positional-only).

    Returns
    -------
    R
        Result of applying function to data.

    Examples
    --------
    Data first:
    >>> R.apply(10, R.add(5))
    15
    >>> R.apply(10, R.constant('asd'))
    'asd'

    Data first without function:
    >>> R.apply(10)(R.add(5))
    15
    >>> R.apply(10)(R.constant('asd'))
    'asd'

    Data last:
    >>> R.apply(R.add(5))(10)
    15
    >>> R.apply(R.constant('asd'))(10)
    'asd'

    """
    if function is None:
        if isinstance(data, Callable):
            return lambda x: apply(x, data)  # pyright: ignore[reportUnknownArgumentType]
        else:
            return lambda x: apply(data, x)  # pyright: ignore[reportUnknownLambdaType, reportArgumentType]
    sig = inspect.signature(function)
    param_count = len(sig.parameters.values())
    return function(data) if param_count > 0 else function()  # pyright: ignore[reportUnknownVariableType, reportCallIssue, reportArgumentType]
