# -*- coding: utf-8 -*-
"""
The implementation of the ℒazy-ℒoad library.
"""

from typing import Callable, TypeVar, Type, Any, get_type_hints
from functools import wraps
import inspect

from lazy_object_proxy import Proxy

def _is_lazy_object(obj: Any) -> bool:
    return obj is not None and isinstance(obj, Proxy)

_T = TypeVar('T')
def lazy(target: Callable[..., _T], *args: str, **kwargs: str) -> _T:
    """
    Create a lazy loading object given a callable object and optional arguments.
    The created object will be generated by evaluating the given function
    as soon as it is used.

    Arguments:
        target {Callable[..., _T]} -- [The function to create the lazy loading object]

    Returns:
        _T -- [A proxy object for the lazy loading object.]
    """
    if not callable(target):
        raise ValueError('Invalid target.')
    if args or kwargs or not _is_lazy_object(target):
        return Proxy(lambda: target(*args, **kwargs))
    return target

lz: type(lazy) = lazy
"""
An alias for lazy.
"""

def _is_lazy_function(func: Callable) -> bool:
    return callable(func) and hasattr(func, '__wrapped_non_lazy_function__')

_A, _R = TypeVar('A'), TypeVar('R')
class _LazyFunc:
    def __call__(self, original_function: Callable[[_A], _R]) -> Callable[[_A], _R]:
        """
        Wrap a given function in a way that all its evaluations are lazy. This means
        a function call to the returned function will only result in an actual function
        call to the original function if and only if the return value is used. The first
        time the return value is used, the original function will be called.

        The lazy evaluation works especially great for pure functions.

        Arguments:
            original_function {Callable[[_A], _R]} -- [The function which should be wrapped.]

        Returns:
            Callable[[_A], _R] -- [The lazy evaluated version of the given function]
        """
        if _is_lazy_function(original_function):
            return original_function
        @wraps(original_function)
        def _lazy_function(*args, **kwargs):
            return lazy(lambda: original_function(*args, **kwargs))
        _lazy_function.__wrapped_non_lazy_function__ = original_function
        return _lazy_function

    def __getitem__(self, original_functions: Callable) -> Callable:
        """
        Generate lazy evaluated functions for one or more functions.

        Arguments:
            original_functions {Callable} -- [The original functions]

        Returns:
            Callable -- [The lazy evaluated versions of the given functions]
        """
        if isinstance(original_functions, tuple):
            return [self(original_function) for original_function in original_functions]
        return self(original_functions)

lazy_func: _LazyFunc = _LazyFunc()
"""
This object might be used to create lazy versions of functions, e.g. by calling:
f_lazy = lazy_func(f)
or
f_lazy, g_lazy = lazy_func[f, g]

It also can be used as a decorator to make a function lazy evaluated:
@lazy_func
def pure_and_complicated_function(x, y, z):
    ...
    return res
"""

lf: type(lazy_func) = lazy_func
"""
An alias for lazy_func.
"""

ℒ: type(lazy_func) = lazy_func
"""
An alias for lazy_func.
"""

def lazy_class(cls: Type) -> Type:
    """
    A decorator for classes: It makes all methods of the class lazy which
    - are public (their name does not start with)
    - have type hints for the return type and this hint indicates that the method
      does return a value

    Arguments:
        cls {Type} -- [The target class]

    Returns:
        Type -- [The (modified) target class]
    """
    for name, function in inspect.getmembers(cls, predicate=inspect.isfunction):
        if name and name[0] == '_':
            continue
        if get_type_hints(function).get('return', type(None)) is not type(None):
            setattr(cls, name, lazy_func[function])
    return cls

lc: type(lazy_class) = lazy_class
"""
An alias for lazy_class.
"""

def force_eval(obj: Any) -> Any:
    """
    Force the evaluation of a lazy object (Proxy). If a lazy function is given, the
    wrapped non-lazy version of the function is returned.

    Arguments:
        obj {Any} -- [The lazy object or function.]

    Returns:
        Any -- [The non-lazy version of the object or function.]
    """
    if _is_lazy_object(obj):
        return obj.__wrapped__
    if _is_lazy_function(obj):
        return obj.__wrapped_non_lazy_function__
    return obj

fe: type(force_eval) = force_eval
"""
An alias for force_eval.
"""
