from collections.abc import Callable
from typing import ParamSpec, TypeVar, cast

from remedapy.local_types import NotSet

P = ParamSpec('P')
T = TypeVar('T')


def once(function: Callable[P, T], /) -> Callable[P, T]:
    """
    Wraps a function into a function that runs the original at the first call and caches the result.

    Subsequent calls with return the cached value without re-running the function.

    Parameters
    ----------
    function: Callable[P, T]
        The function to wrap.

    Returns
    -------
    Callable[P, T]
        Wrapped function.

    Examples
    --------
    Data first:
    >>> x = []
    >>> fn = R.once(lambda: x.append(1))
    >>> fn()
    >>> fn()
    >>> x
    [1]

    """
    value: T | object = NotSet

    def returned(*args: P.args, **kwargs: P.kwargs) -> T:
        nonlocal value
        if value is NotSet:
            value = function(*args, **kwargs)
        return cast(T, value)

    return returned
