from inspect import iscoroutinefunction
from typing import (
    AsyncIterator,
    Iterable,
    AsyncIterable,
    Union,
    Generic,
    Optional,
    Awaitable,
    Callable,
)

from ._typing import T, AnyIterable


class Sentinel:
    """Placeholder with configurable ``repr``"""

    __slots__ = ("name",)

    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return self.name


def aiter(subject: AnyIterable[T]) -> AsyncIterator[T]:
    """
    An async iterator object yielding elements from ``subject``

    :raises TypeError: if ``subject`` does not support any iteration protocol

    The ``subject`` must support
    the async iteration protocol (the :py:meth:`object.__aiter__` method),
    the regular iteration protocol (the :py:meth:`object.__iter__` method),
    or it must support the sequence protocol (the :py:meth:`object.__getitem__`
    method with integer arguments starting at 0).
    In either case, an async iterator is returned.
    """
    if isinstance(subject, AsyncIterable):
        return subject.__aiter__()
    else:
        return _aiter_sync(subject).__aiter__()


async def _aiter_sync(iterable: Iterable[T]) -> AsyncIterator[T]:
    """Helper to provide an async iterator for a regular iterable"""
    for item in iterable:
        yield item


class ScopedIter(Generic[T]):
    """Context manager that provides and cleans up an iterator for an iterable"""

    __slots__ = ("_iterable", "_iterator")

    def __init__(self, iterable: AnyIterable[T]):
        self._iterable: Optional[AnyIterable[T]] = iterable
        self._iterator: Optional[AsyncIterator[T]] = None

    async def __aenter__(self) -> AsyncIterator[T]:
        assert (
            self._iterable is not None
        ), f"{self.__class__.__name__} is not re-entrant"
        self._iterator = aiter(self._iterable)
        self._iterable = None
        return self._iterator

    async def __aexit__(self, exc_type, exc_val, exc_tb) -> bool:
        try:
            aclose = self._iterator.aclose()  # type: ignore
        except AttributeError:
            pass
        else:
            await aclose
        return False


async def borrow(iterator: AsyncIterator):
    """Borrow an async iterator for iteration, preventing it from being closed"""
    async for item in iterator:
        yield item


def awaitify(
    function: Union[Callable[..., T], Callable[..., Awaitable[T]]]
) -> Callable[..., Awaitable[T]]:
    """Ensure that ``function`` can be used in ``await`` expressions"""
    if iscoroutinefunction(function):
        return function  # type: ignore
    else:
        return Awaitify(function)


class Awaitify(Generic[T]):
    """Helper to peek at the return value of ``function`` and make it ``async``"""

    __slots__ = "__wrapped__", "_async_call"

    def __init__(self, function: Union[Callable[..., T], Callable[..., Awaitable[T]]]):
        self.__wrapped__ = function
        self._async_call: Optional[Callable[..., Awaitable[T]]] = None

    def __call__(self, *args, **kwargs) -> Awaitable[T]:
        async_call = self._async_call
        if async_call is None:
            value = self.__wrapped__(*args, **kwargs)
            if isinstance(value, Awaitable):
                self._async_call = self.__wrapped__  # type: ignore
                return value
            else:
                self._async_call = force_async(self.__wrapped__)  # type: ignore
                return await_value(value)
        else:
            return async_call(*args, **kwargs)


async def await_value(value: T) -> T:
    return value


def force_async(call: Callable[..., T]) -> Callable[..., Awaitable[T]]:
    async def async_wrapped(*args, **kwargs):
        return call(*args, **kwargs)

    return async_wrapped
