"""
Generators of arbitrary values.
"""
import functools
import inspect

from pyqcy.utils import optional_args


@optional_args
class arbitrary(object):
    """Decorator to be applied on functions in order to turn
    them into generators of arbitrary ("random") values
    of given type.

    :param type_: Type of values generated by the function

    The ``type_`` argument is optional. If provided,
    objects returned by the function will be checked against
    this type. It will be also possible to use the type
    directly when defining properties.

    Examples::

        from pyqcy import *

        @arbitrary(MyClass)
        def my_class():
            return MyClass()

        @qc
        def my_class_works(obj=MyClass):
            assert obj.is_valid()
    """
    # Dictionary mapping types into
    # generators of arbitrary values for those types
    registry = {}

    def __init__(self, type_=None):
        if type_ is not None and not isinstance(type_, type):
            raise TypeError("%r (a `%s`) is not a type" % (
                type_, type(type_).__name__))
        self.type_ = type_

    def __call__(self, func):
        """Applies the @arbitrary decorator to given function.

        If ``type`` argument was supplied previously,
        the resulting generator will be remembered in global registry
        for easy reference.
        """
        if self.type_ is None:
            gen_func = self.__arbitrary_generator(func)
        else:
            def validate(value):
                if not isinstance(value, self.type_):
                    raise TypeError(
                        "arbitrary value %r is of type %s; expected %s" % (
                            value, type(value).__name__, self.type_.__name__))
                return value

            gen_func = self.__arbitrary_generator(func, validate)
            self.registry.setdefault(self.type_, [])
            self.registry[self.type_].append(gen_func)

        gen_func._arbitrary = True  # marker attribute
        return gen_func

    def __arbitrary_generator(self, gen, value_func=None):
        """Constructs arbitrary generator based on given object.

        It can be a function that returns a single value,
        a generator function (that uses ``yield``) or a generator class
        (with ``__iter__`` and ``next`` methods).
        """
        if value_func is None:
            value_func = lambda v: v  # identity function

        if inspect.isgeneratorfunction(gen) or inspect.isclass(gen):
            @functools.wraps(gen)
            def wrapper(*args, **kwargs):
                args, kwargs = self.__coerce_to_arbitraries(args, kwargs)
                for obj in gen(*args, **kwargs):
                    yield value_func(obj)
        else:
            @functools.wraps(gen)
            def wrapper(*args, **kwargs):
                args, kwargs = self.__coerce_to_arbitraries(args, kwargs)
                while True:
                    yield value_func(gen(*args, **kwargs))

        return wrapper

    def __coerce_to_arbitraries(self, args=None, kwargs=None):
        """Ensures given list and dictionary of positional and keyword
        arguments contains appropriate arbitrary values' generators.

        Elements that cannot be reasonably coerced into arbitraries
        are left unchanged.

        Returns a tuple of (args, kwargs) with coerced arguments.
        """
        args = args or []
        kwargs = kwargs or {}
        args = [to_arbitrary(arg) if is_arbitrary(arg) else arg
                for arg in args]
        kwargs = dict((k, to_arbitrary(v) if is_arbitrary(v) else v)
                      for (k, v) in kwargs.iteritems())
        return args, kwargs


def is_arbitrary(obj):
    """Checks whether given object can work as generator of arbitrary values.

    This functions handles all the forms in which arbitraries can occur
    in the code, including: generators, generator functions, and types.

    :param obj: Object to be checked
    """
    if inspect.isgenerator(obj):
        return True
    if inspect.isgeneratorfunction(obj):
        return getattr(obj, '_arbitrary', False)
    if isinstance(obj, type):
        return obj in arbitrary.registry
    return False


def to_arbitrary(obj):
    """Ensures that given object is a generator of arbitrary values.

    Rather than permitting only actual generators, this allows
    us to pass generator functions or even types, provided
    there is a known arbitrary generator for them.

    :param obj: Object to be coerced into arbitrary generator,
                if it's possible and the object is not already one

    Raises :exc:`TypeError` if ``obj`` is a type and no default
    generator for this type has been found.

    Raises :exc:`ValueError` if ``obj`` cannot be reasonably
    coerced into a generator of arbitrary values.
    """
    if inspect.isgenerator(obj):
        return obj
    if inspect.isgeneratorfunction(obj):
        return obj()    # fails if arguments are required,
                        # and this is intended

    # looking up types in global registry
    if isinstance(obj, type):
        arbit_gens = arbitrary.registry.get(obj)
        if not arbit_gens:
            raise TypeError(
                "no arbitrary values' generator found for type: %s" % obj)
        return to_arbitrary(arbit_gens[0])

    raise ValueError(
        "invalid generator of arbitrary values: %r (of type %s)" % (
            obj, type(obj).__name__))


from .numbers import *
from .strings import *
from .collections import *

from .combinators import *
