# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals
import inspect


class FilterSpec(object):
    """
    Class for describing filter specification.

    The main job of the ``FilterSet`` is to parse
    the submitted lookups into a list of filter specs.
    A list of these specs is then used by the filter backend
    to actually filter given queryset.

    The reason why filtering is decoupled from the ``FilterSet``
    is because this allows to implement filter backends
    not related to Django.

    Attributes
    ----------
    components : list
        A list of strings which are names of the keys/attributes
        to be used in filtering of the queryset.
        For example lookup config with key
        ``user__profile__email`` will be components of
        ``['user', 'profile', 'email'].
    lookup : str
        Name of the lookup how final key/attribute from
        ``components`` should be compared.
        For example lookup config with key
        ``user__profile__email__contains`` will have a lookup
        ``contains``.
    value
        Value of the filter.
    is_negated : bool, optional
        Whether this filter should be negated.
        By default its ``False``.
    """
    def __init__(self, components, lookup, value, is_negated=False):
        self.components = components
        self.lookup = lookup
        self.value = value
        self.is_negated = is_negated

    def __repr__(self):
        return '<{} {} {}{} {}>'.format(
            self.__class__.__name__,
            '.'.join(self.components),
            'NOT ' if self.is_negated else '',
            self.lookup,
            repr(self.value),
        )

    def __eq__(self, other):
        return hash(self) == hash(other)

    def __hash__(self):
        return hash(repr(self))


class LookupConfig(object):
    """
    Lookup configuration which is used by ``FilterSet``
    to create a ``FilterSpec``.

    The main purpose of this config is to allow the use
    if recursion in ``FilterSet``. Each lookup key
    (the keys in the querystring) is parsed into
    a nested one-key dictionary which lookup config stores.

    For example the querystring::

        ?user__profile__email__endswith=gmail.com

    is parsed into the following config::

        {
            'user': {
                'profile': {
                    'email': {
                        'endswith': 'gmail.com'
                    }
                }
            }
        }

    Attributes
    ----------
    key : str
        Full lookup key from the querystring.
        For example ``user__profile__email__endswith``
    data : dict, str
        Either:

        * nested dictionary where the key is the next key within
          the lookup chain and value is another ``LookupConfig``
        * the filtering value as provided in the querystring value

    Parameters
    ----------
    key : str
        Full lookup key from the querystring.
    data : dict, str
        A regular vanilla Python dictionary.
        This class automatically converts nested
        dictionaries to instances of LookupConfig.
        Alternatively a filtering value as provided
        in the querystring.
    """
    def __init__(self, key, data):
        if isinstance(data, dict):
            data = {k: self.__class__(key, v) for k, v in data.items()}

        self.key = key
        self.data = data

    def is_key_value(self):
        return len(self.data) == 1 and not isinstance(self.value, dict)

    @property
    def name(self):
        """
        If the ``data`` is nested ``LookupConfig``,
        this gets its first lookup key.
        """
        return next(iter(self.data.keys()))

    @property
    def value(self):
        """
        If the ``data`` is nested ``LookupConfig``,
        this gets its first lookup value which could either
        be another ``LookupConfig`` or actual filtering value.
        """
        return next(iter(self.data.values()))

    def as_dict(self):
        """
        Converts the nested ``LookupConfig``s to a regular ``dict``.
        """
        if isinstance(self.data, dict):
            return {k: v.as_dict() for k, v in self.data.items()}
        return self.data

    def __repr__(self):
        return '<{} {}=>{}>'.format(
            self.__class__.__name__,
            self.key,
            repr(self.as_dict()),
        )


class SubClassDict(dict):
    """
    Special-purpose ``dict`` with special getter for looking up
    values by finding matching subclasses.

    This is better illustrated in an example::

        >>> class Klass(object): pass
        >>> class Foo(object): pass
        >>> class Bar(Foo): pass
        >>> mapping = SubClassDict({
        ...     Foo: 'foo',
        ...     Klass: 'klass',
        ... })
        >>> print(mapping.get(Klass))
        klass
        >>> print(mapping.get(Foo))
        foo
        >>> print(mapping.get(Bar))
        foo
    """

    def get(self, k, d=None):
        """
        If no value is found by using Python's default implementation,
        try to find the value where the key is a base class of the
        provided search class.
        """
        value = super(SubClassDict, self).get(k, d)

        # try to match by value
        if value is d and inspect.isclass(k):
            for klass, v in self.items():
                if not inspect.isclass(klass):
                    continue
                if issubclass(k, klass):
                    return v

        return value
