from enum import Enum
from functools import wraps
from collections.abc import Callable
import inspect
from typing import Iterable, overload


class _Undef(Enum):
    UNDEFINED = "undefined"


UNDEFINED = _Undef.UNDEFINED


def renamed_fn(name: str):
    "Rename a function's __name__."

    def _curried(fn):
        @wraps(fn)
        def _wrapped(*args, **kwargs):
            return fn(*args, **kwargs)

        _wrapped.__name__ = name
        return _wrapped

    return _curried


# TODO: should this even be exposed?
# TODO: should this work for functions of any arg length, and specify
# it's only for the first one?
def type_checked[A, R](f: Callable[[A], R]) -> Callable[[A], R]:
    # TODO: handle this error for unannotated fn
    anno = next(iter(inspect.signature(f).parameters.values())).annotation

    @wraps(f)
    def _checked(arg: A) -> R:
        if not isinstance(arg, anno):
            raise TypeError(f"argument was not of type {anno}")
        return f(arg)

    return _checked


def _identity_fn[T](x: T) -> T:
    return x


@overload
def _last[T](ible: Iterable[T], /) -> T: ...
@overload
def _last[T, O = None](ible: Iterable[T], default: O = None, /) -> T | O: ...
def _last(ible: Iterable, default=_Undef.UNDEFINED, /):
    iter_ = iter(ible)

    try:
        item = next(iter_)
    except StopIteration:
        if default is not _Undef.UNDEFINED:
            return default
        raise

    for item in iter_:
        continue

    return item
