from bonobo.errors import AbstractError
from bonobo.util import get_name, iscontextprocessor, isoption, sortedlist

__all__ = ["Configurable"]

get_creation_counter = lambda v: v._creation_counter


class ConfigurableMeta(type):
    """
    Metaclass for Configurables that will add options to a special __options__ dict.
    """

    def __init__(cls, what, bases=None, dict=None):
        super().__init__(what, bases, dict)

        cls.__processors = sortedlist()
        cls.__processors_cache = None
        cls.__methods = sortedlist()
        cls.__options = sortedlist()
        cls.__names = set()

        # cls.__kwoptions = []

        for typ in cls.__mro__:
            for name, value in filter(lambda x: isoption(x[1]), typ.__dict__.items()):
                if iscontextprocessor(value):
                    cls.__processors.insort((value._creation_counter, value))
                    continue

                if not value.name:
                    value.name = name

                if not name in cls.__names:
                    cls.__names.add(name)
                    cls.__options.insort((not value.positional, value._creation_counter, name, value))

        # Docstring formating
        _options_doc = []
        for _positional, _counter, _name, _value in cls.__options:
            _param = _name
            if _value.type:
                _param = get_name(_value.type) + " " + _param

            prefix = ":param {}: ".format(_param)
            for lineno, line in enumerate((_value.__doc__ or "").split("\n")):
                _options_doc.append((" " * len(prefix) if lineno else prefix) + line)
        cls.__doc__ = "\n\n".join(map(str.strip, filter(None, (cls.__doc__, "\n".join(_options_doc)))))

    @property
    def __options__(cls):
        return ((name, option) for _, _, name, option in cls.__options)

    @property
    def __options_dict__(cls):
        return dict(cls.__options__)

    @property
    def __processors__(cls):
        if cls.__processors_cache is None:
            cls.__processors_cache = [processor for _, processor in cls.__processors]
        return cls.__processors_cache

    def __repr__(self):
        return " ".join(("<Configurable", super(ConfigurableMeta, self).__repr__().split(" ", 1)[1]))


try:
    import _functools
except ImportError:
    import functools

    PartiallyConfigured = functools.partial
else:

    class PartiallyConfigured(_functools.partial):
        @property  # TODO XXX cache this
        def _options_values(self):
            """ Simulate option values for partially configured objects. """
            try:
                return self.__options_values
            except AttributeError:
                self.__options_values = {**self.keywords}

                position = 0

                for name, option in self.func.__options__:
                    if not option.positional:
                        break  # no positional left
                    if name in self.keywords:
                        continue  # already fulfilled

                    self.__options_values[name] = self.args[position] if len(self.args) >= position + 1 else None
                    position += 1

                return self.__options_values

        def __getattr__(self, item):
            _dict = self.func.__options_dict__
            if item in _dict:
                return _dict[item].__get__(self, self.func)
            return getattr(self.func, item)


class Configurable(metaclass=ConfigurableMeta):
    """
    Generic class for configurable objects. Configurable objects have a dictionary of "options" descriptors that defines
    the configuration schema of the type.

    """

    def __new__(cls, *args, _final=False, **kwargs):
        """
        Custom instance builder. If not all options are fulfilled, will return a :class:`PartiallyConfigured` instance
        which is just a :class:`functools.partial` object that behaves like a :class:`Configurable` instance.

        The special `_final` argument can be used to force final instance to be created, or an error raised if options
        are missing.

        :param args:
        :param _final: bool
        :param kwargs:
        :return: Configurable or PartiallyConfigured
        """
        options = tuple(cls.__options__)
        # compute missing options, given the kwargs.
        missing = set()
        for name, option in options:
            if option.required and not option.name in kwargs:
                missing.add(name)

        # transform positional arguments in keyword arguments if possible.
        position = 0
        for name, option in options:
            if not option.positional:
                break  # option orders make all positional options first, job done.

            if not isoption(getattr(cls, name)):
                missing.discard(name)
                continue

            if len(args) <= position:
                break  # no more positional arguments given.

            position += 1
            missing.discard(name)

        # complain if there is more options than possible.
        extraneous = set(kwargs.keys()) - (set(next(zip(*options))) if len(options) else set())
        if len(extraneous):
            raise TypeError(
                "{}() got {} unexpected option{}: {}.".format(
                    cls.__name__,
                    len(extraneous),
                    "s" if len(extraneous) > 1 else "",
                    ", ".join(map(repr, sorted(extraneous))),
                )
            )

        # missing options? we'll return a partial instance to finish the work later, unless we're required to be
        # "final".
        if len(missing):
            if _final:
                raise TypeError(
                    "{}() missing {} required option{}: {}.".format(
                        cls.__name__,
                        len(missing),
                        "s" if len(missing) > 1 else "",
                        ", ".join(map(repr, sorted(missing))),
                    )
                )
            return PartiallyConfigured(cls, *args, **kwargs)

        return super(Configurable, cls).__new__(cls)

    def __init__(self, *args, **kwargs):
        # initialize option's value dictionary, used by descriptor implementation (see Option).
        self._options_values = {**kwargs}

        # set option values.
        for name, value in kwargs.items():
            setattr(self, name, value)

        position = 0
        for name, option in self.__options__:
            if not option.positional:
                break  # option orders make all positional options first

            # value was overriden? Skip.
            maybe_value = getattr(type(self), name)
            if not isoption(maybe_value):
                continue

            if len(args) <= position:
                break

            if name in self._options_values:
                raise ValueError("Already got a value for option {}".format(name))

            setattr(self, name, args[position])
            position += 1

    def __call__(self, *args, **kwargs):
        raise AbstractError(self.__call__)

    @property
    def __options__(self):
        return type(self).__options__

    @property
    def __processors__(self):
        return type(self).__processors__
