#!/usr/bin/env python3

# © Copyright 2021-2022, Scott Gasch

"""This is the base class of
:class:`pyutils.parallelize.smart_future.SmartFuture`, which is a
piece of the simple parallelization framework.

This base class is essentially tries to have every Python `__dunder__`
method defined with a reasonabe default implementation so that, when
it is used in a manner that requires the value to be known, it calls
:meth:`DeferredOperand.resolve` and either gets the requisite value or
blocks until the data necessary to resolve the value is ready.  This
is meant to enable more transparent :class:`Future` objects that can
be just used directly.

See :class:`pyutils.parallelize.smart_future.SmartFuture` for more
information.

"""

from abc import ABC, abstractmethod
from typing import Any, Generic, Set, TypeVar

# This module is commonly used by others in here and should avoid
# taking any unnecessary dependencies back on them.

T = TypeVar('T')


class DeferredOperand(ABC, Generic[T]):
    """A wrapper around an operand whose value is deferred until it is
    needed (i.e. accessed).  See the subclass
    :class:`pyutils.parallelize.smart_future.SmartFuture` for an
    example usage and/or a more useful patten.
    """

    def __init__(self, local_attributes: Set[str] = None):
        """
        Args:
            local_attributes: because this class attempts to act as a
                transparent wrapper around a normal Future, it needs
                to be able to differentiate between attempts to set a
                property of it/its subclasses or the wrapped object.
                The local_attributes argument is a set of names that,
                if we intercept a set operation for, refer to an
                attribute in the wrapper and not the wrapped class.
        """
        self.__dict__['local_attributes'] = local_attributes

    @abstractmethod
    def _resolve(self, timeout=None) -> T:
        pass

    @staticmethod
    def resolve(x: Any) -> Any:
        """
        When this object is used in a manner that requires it to know
        its value, this method is called to either return the value or
        block until it can do so.

        Args:
            x: the object whose value is required

        Returns:
            The value of x... immediately if possible, eventually if
            not possible.
        """
        while isinstance(x, DeferredOperand):
            x = x._resolve()
        return x

    def __lt__(self, other: Any) -> bool:
        return DeferredOperand.resolve(self) < DeferredOperand.resolve(other)

    def __le__(self, other: Any) -> bool:
        return DeferredOperand.resolve(self) <= DeferredOperand.resolve(other)

    def __eq__(self, other: Any) -> bool:
        return DeferredOperand.resolve(self) == DeferredOperand.resolve(other)

    def __ne__(self, other: Any) -> bool:
        return DeferredOperand.resolve(self) != DeferredOperand.resolve(other)

    def __gt__(self, other: Any) -> bool:
        return DeferredOperand.resolve(self) > DeferredOperand.resolve(other)

    def __ge__(self, other: Any) -> bool:
        return DeferredOperand.resolve(self) >= DeferredOperand.resolve(other)

    def __not__(self) -> bool:
        return not DeferredOperand.resolve(self)

    def bool(self) -> bool:
        return DeferredOperand.resolve(self)

    def __add__(self, other: Any) -> T:
        return DeferredOperand.resolve(self) + DeferredOperand.resolve(other)

    def __iadd__(self, other: Any) -> T:
        return DeferredOperand.resolve(self) + DeferredOperand.resolve(other)

    def __radd__(self, other: Any) -> T:
        return DeferredOperand.resolve(self) + DeferredOperand.resolve(other)

    def __sub__(self, other: Any) -> T:
        return DeferredOperand.resolve(self) - DeferredOperand.resolve(other)

    def __mul__(self, other: Any) -> T:
        return DeferredOperand.resolve(self) * DeferredOperand.resolve(other)

    def __pow__(self, other: Any) -> T:
        return DeferredOperand.resolve(self) ** DeferredOperand.resolve(other)

    def __truediv__(self, other: Any) -> Any:
        return DeferredOperand.resolve(self) / DeferredOperand.resolve(other)

    def __floordiv__(self, other: Any) -> T:
        return DeferredOperand.resolve(self) // DeferredOperand.resolve(other)

    def __contains__(self, other):
        return DeferredOperand.resolve(other) in DeferredOperand.resolve(self)

    def and_(self, other):
        return DeferredOperand.resolve(self) & DeferredOperand.resolve(other)

    def or_(self, other):
        return DeferredOperand.resolve(self) & DeferredOperand.resolve(other)

    def xor(self, other):
        return DeferredOperand.resolve(self) & DeferredOperand.resolve(other)

    def invert(self):
        return ~(DeferredOperand.resolve(self))

    def is_(self, other):
        return DeferredOperand.resolve(self) is DeferredOperand.resolve(other)

    def is_not(self, other):
        return DeferredOperand.resolve(self) is not DeferredOperand.resolve(other)

    def __abs__(self):
        return abs(DeferredOperand.resolve(self))

    def setitem(self, k, v):
        DeferredOperand.resolve(self)[DeferredOperand.resolve(k)] = v

    def delitem(self, k):
        del DeferredOperand.resolve(self)[DeferredOperand.resolve(k)]

    def getitem(self, k):
        return DeferredOperand.resolve(self)[DeferredOperand.resolve(k)]

    def lshift(self, other):
        return DeferredOperand.resolve(self) << DeferredOperand.resolve(other)

    def rshift(self, other):
        return DeferredOperand.resolve(self) >> DeferredOperand.resolve(other)

    def mod(self, other):
        return DeferredOperand.resolve(self) % DeferredOperand.resolve(other)

    def matmul(self, other):
        return DeferredOperand.resolve(self) @ DeferredOperand.resolve(other)

    def neg(self):
        return -(DeferredOperand.resolve(self))

    def pos(self):
        return +(DeferredOperand.resolve(self))

    def truth(self):
        return DeferredOperand.resolve(self)

    def __hash__(self):
        return DeferredOperand.resolve(self).__hash__()

    def __call__(self):
        return DeferredOperand.resolve(self)()

    def __iter__(self):
        return DeferredOperand.resolve(self).__iter__()

    def __repr__(self) -> str:
        return DeferredOperand.resolve(self).__repr__()

    def __bytes__(self) -> bytes:
        return DeferredOperand.resolve(self).__bytes__()

    def __int__(self) -> int:
        return int(DeferredOperand.resolve(self))

    def __float__(self) -> float:
        return float(DeferredOperand.resolve(self))

    def __getattr__(self, name):
        return getattr(DeferredOperand.resolve(self), name)

    def __setattr__(self, name, value):
        # subclass setting its own properties
        if name in self.local_attributes:
            object.__setattr__(self, name, value)
            return

        # otherwise operate on the wrapped result
        DeferredOperand.resolve(self).__setattr__(name, value)

    def __delattr__(self, name):
        return delattr(DeferredOperand.resolve(self), name)

    def __dir__(self):
        return dir(DeferredOperand.resolve(self))
