from __future__ import annotations

import asyncio
import copy
from contextvars import ContextVar  # noqa # contextvars not understood as stdlib
from typing import Any  # noqa # contextvars not understood as stdlib
from typing import Callable, Dict, Optional


class TaskLocal:
    """An object local to the current task."""

    __slots__ = ("_storage",)

    def __init__(self) -> None:
        # Note as __setattr__ is overidden below, use the object __setattr__
        object.__setattr__(self, "_storage", ContextVar("storage"))

    def __getattr__(self, name: str) -> Any:
        values = self._storage.get({})
        try:
            return values[name]
        except KeyError:
            raise AttributeError(name)

    def __setattr__(self, name: str, value: Any) -> None:
        values = self._storage.get({})
        values[name] = value
        self._storage.set(values)

    def __delattr__(self, name: str) -> None:
        values = self._storage.get({})
        try:
            del values[name]
            self._storage.set(values)
        except KeyError:
            raise AttributeError(name)

    @staticmethod
    def _task_identity() -> int:
        loop = asyncio.get_event_loop()
        if loop.is_running():
            task = asyncio.current_task()
            task_id = id(task)
            return task_id
        else:
            return 0


class LocalStack:
    def __init__(self) -> None:
        self._task_local = TaskLocal()

    def push(self, value: Any) -> None:
        stack = getattr(self._task_local, "stack", None)
        if stack is None:
            self._task_local.stack = stack = []
        stack.append(value)

    def pop(self) -> Any:
        stack = getattr(self._task_local, "stack", None)
        if stack is None or stack == []:
            return None
        else:
            return stack.pop()

    @property
    def top(self) -> Any:
        try:
            return self._task_local.stack[-1]
        except (AttributeError, IndexError):
            return None


class LocalProxy:
    """Proxy to a task local object."""

    __slots__ = ("__dict__", "__local", "__wrapped__")

    def __init__(self, local: Callable, name: Optional[str] = None) -> None:
        # Note as __setattr__ is overidden below, use the object __setattr__
        object.__setattr__(self, "__LocalProxy_local", local)
        object.__setattr__(self, "__wrapped__", local)
        object.__setattr__(self, "__name__", name)

    def _get_current_object(self) -> Any:
        return object.__getattribute__(self, "__LocalProxy_local")()

    @property
    def __dict__(self) -> Dict[str, Any]:  # type: ignore
        try:
            return self._get_current_object().__dict__
        except RuntimeError:
            raise AttributeError("__dict__")

    def __repr__(self) -> str:
        try:
            obj = self._get_current_object()
        except RuntimeError:
            return "<%s unbound>" % self.__class__.__name__
        return repr(obj)

    def __bool__(self) -> bool:
        try:
            return bool(self._get_current_object())
        except RuntimeError:
            return False

    def __dir__(self) -> Any:
        try:
            return dir(self._get_current_object())
        except RuntimeError:
            return []

    def __getattr__(self, name: Any) -> Any:
        if name == "__members__":
            return dir(self._get_current_object())
        return getattr(self._get_current_object(), name)

    def __setitem__(self, key: Any, value: Any) -> Any:
        self._get_current_object()[key] = value

    def __delitem__(self, key: Any) -> Any:
        del self._get_current_object()[key]

    async def __aiter__(self) -> Any:
        async for x in self._get_current_object():
            yield x

    __setattr__ = lambda x, n, v: setattr(  # noqa: E731, E501
        x._get_current_object(), n, v  # type: ignore
    )
    __delattr__ = lambda x, n: delattr(x._get_current_object(), n)  # type: ignore # noqa: E731
    __str__ = lambda x: str(x._get_current_object())  # type: ignore # noqa: E731
    __lt__ = lambda x, o: x._get_current_object() < o  # noqa: E731
    __le__ = lambda x, o: x._get_current_object() <= o  # noqa: E731
    __eq__ = lambda x, o: x._get_current_object() == o  # type: ignore # noqa: E731
    __ne__ = lambda x, o: x._get_current_object() != o  # type: ignore # noqa: E731
    __gt__ = lambda x, o: x._get_current_object() > o  # noqa: E731
    __ge__ = lambda x, o: x._get_current_object() >= o  # noqa: E731
    __hash__ = lambda x: hash(x._get_current_object())  # type: ignore # noqa: E731
    __call__ = lambda x, *a, **kw: x._get_current_object()(*a, **kw)  # noqa: E731
    __len__ = lambda x: len(x._get_current_object())  # noqa: E731
    __getitem__ = lambda x, i: x._get_current_object()[i]  # noqa: E731
    __iter__ = lambda x: iter(x._get_current_object())  # noqa: E731
    __contains__ = lambda x, i: i in x._get_current_object()  # noqa: E731
    __add__ = lambda x, o: x._get_current_object() + o  # noqa: E731
    __sub__ = lambda x, o: x._get_current_object() - o  # noqa: E731
    __mul__ = lambda x, o: x._get_current_object() * o  # noqa: E731
    __floordiv__ = lambda x, o: x._get_current_object() // o  # noqa: E731
    __mod__ = lambda x, o: x._get_current_object() % o  # noqa: E731
    __divmod__ = lambda x, o: x._get_current_object().__divmod__(o)  # noqa: E731
    __pow__ = lambda x, o: x._get_current_object() ** o  # noqa: E731
    __lshift__ = lambda x, o: x._get_current_object() << o  # noqa: E731
    __rshift__ = lambda x, o: x._get_current_object() >> o  # noqa: E731
    __and__ = lambda x, o: x._get_current_object() & o  # noqa: E731
    __xor__ = lambda x, o: x._get_current_object() ^ o  # noqa: E731
    __or__ = lambda x, o: x._get_current_object() | o  # noqa: E731
    __div__ = lambda x, o: x._get_current_object().__div__(o)  # noqa: E731
    __truediv__ = lambda x, o: x._get_current_object().__truediv__(o)  # noqa: E731
    __neg__ = lambda x: -(x._get_current_object())  # noqa: E731
    __pos__ = lambda x: +(x._get_current_object())  # noqa: E731
    __abs__ = lambda x: abs(x._get_current_object())  # noqa: E731
    __invert__ = lambda x: ~(x._get_current_object())  # noqa: E731
    __complex__ = lambda x: complex(x._get_current_object())  # noqa: E731
    __int__ = lambda x: int(x._get_current_object())  # noqa: E731
    __float__ = lambda x: float(x._get_current_object())  # noqa: E731
    __oct__ = lambda x: oct(x._get_current_object())  # noqa: E731
    __hex__ = lambda x: hex(x._get_current_object())  # noqa: E731
    __index__ = lambda x: x._get_current_object().__index__()  # noqa: E731
    __coerce__ = lambda x, o: x._get_current_object().__coerce__(x, o)  # noqa: E731
    __enter__ = lambda x: x._get_current_object().__enter__()  # noqa: E731
    __exit__ = lambda x, *a, **kw: x._get_current_object().__exit__(*a, **kw)  # noqa: E731
    __radd__ = lambda x, o: o + x._get_current_object()  # noqa: E731
    __rsub__ = lambda x, o: o - x._get_current_object()  # noqa: E731
    __rmul__ = lambda x, o: o * x._get_current_object()  # noqa: E731
    __rdiv__ = lambda x, o: o / x._get_current_object()  # noqa: E731
    __rtruediv__ = __rdiv__
    __rfloordiv__ = lambda x, o: o // x._get_current_object()  # noqa: E731
    __rmod__ = lambda x, o: o % x._get_current_object()  # noqa: E731
    __rdivmod__ = lambda x, o: x._get_current_object().__rdivmod__(o)  # noqa: E731
    __copy__ = lambda x: copy.copy(x._get_current_object())  # noqa: E731
    __deepcopy__ = lambda x, memo: copy.deepcopy(x._get_current_object(), memo)  # noqa: E731
    __await__ = lambda x: x._get_current_object().__await__()  # noqa: E731
