import itertools
import re

from django.core.urlresolvers import NoReverseMatch, reverse
from django.db.models.deletion import Collector


def related_classes(instance):
    """
    Return all classes which would be deleted if the passed instance
    were deleted too by employing the cascade machinery of Django
    itself. Does **not** return instances, only classes.

    Note! When using Django 1.5, autogenerated models (many to many through
    models) are returned too.
    """
    collector = Collector(using=instance._state.db)
    # We really do not want fast deletion, we absolutely need to know whether
    # there are related objects around!
    collector.can_fast_delete = lambda *args, **kwargs: False
    collector.collect([instance])

    # Save collected objects for later referencing (well yes, it does return
    # instances but we don't have to tell anybody :-)
    instance._collected_objects = collector.data

    return collector.data.keys()


def safe_queryset_and(head, *tail):
    """
    Safe AND-ing of querysets. If one of both queries has its
    DISTINCT flag set, sets distinct on both querysets. Also takes extra
    care to preserve the result of the following queryset methods:

    * ``reverse()``
    * ``transform()``
    * ``select_related()``
    * ``prefetch_related()``
    """

    def _merge(qs1, qs2):
        if qs1.query.distinct or qs2.query.distinct:
            res = qs1.distinct() & qs2.distinct()
        else:
            res = qs1 & qs2

        res._transform_fns = list(set(
            getattr(qs1, '_transform_fns', [])
            + getattr(qs2, '_transform_fns', [])))

        if not (qs1.query.standard_ordering and qs2.query.standard_ordering):
            res.query.standard_ordering = False

        select_related = [qs1.query.select_related, qs2.query.select_related]
        if False in select_related:
            # We are not interested in the default value
            select_related.remove(False)

        if len(select_related) == 1:
            res.query.select_related = select_related[0]
        elif len(select_related) == 2:
            if True in select_related:
                # Prefer explicit select_related to generic select_related()
                select_related.remove(True)

            if len(select_related) > 0:
                # If we have two explicit select_related calls, take any
                res.query.select_related = select_related[0]
            else:
                res = res.select_related()

        res._prefetch_related_lookups = list(
            set(qs1._prefetch_related_lookups)
            | set(qs2._prefetch_related_lookups))

        return res

    while tail:
        head = _merge(head, tail[0])
        tail = tail[1:]
    return head


_KWARG_RE = re.compile("(?:([-\w]+)=)?(.+)")


def parse_args_and_kwargs(parser, bits):
    """
    Parses template tag arguments and keyword arguments

    Returns a tuple ``args, kwargs``.

    Usage::

        @register.tag
        def custom(parser, token):
            return CustomNode(*parse_args_and_kwargs(parser,
                token.split_contents()[1:]))

        class CustomNode(template.Node):
            def __init__(self, args, kwargs):
                self.args = args
                self.kwargs = kwargs

            def render(self, context):
                args, kwargs = resolve_args_and_kwargs(context, self.args,
                    self.kwargs)
                return self._render(context, *args, **kwargs):

            def _render(self, context, ...):
                # The real workhorse
    """
    args = []
    kwargs = {}

    for bit in bits:
        match = _KWARG_RE.match(bit)
        key, value = match.groups()
        value = parser.compile_filter(value)
        if key:
            kwargs[str(key)] = value
        else:
            args.append(value)

    return args, kwargs


def resolve_args_and_kwargs(context, args, kwargs):
    """
    Resolves arguments and keyword arguments parsed by
    ``parse_args_and_kwargs`` using the passed context instance

    See ``parse_args_and_kwargs`` for usage instructions.
    """
    return [v.resolve(context) for v in args], dict((k, v.resolve(context))
        for k, v in kwargs.items())


def changed_regions(regions, fields):
    """
    Returns a subset of regions which have to be updated when fields have
    been edited. To be used together with the ``{% regions %}`` template
    tag.

    Usage::

        regions = {}
        render(request, 'detail.html', {
            'object': instance,
            'regions': regions,
            })
        return HttpResponse(
            json.dumps(changed_regions(regions, ['emails', 'phones'])),
            content_type='application/json')
    """
    dependencies = regions.get('_dependencies', {})
    to_update = set(itertools.chain(*[
        dependencies.get(field, []) for field in fields]))

    return dict((key, value) for key, value in regions.items()
        if key in to_update)


def tryreverse(*args, **kwargs):
    """
    Calls ``django.core.urlresolvers.reverse``, and returns ``None`` on
    failure instead of raising an exception.
    """
    try:
        return reverse(*args, **kwargs)
    except NoReverseMatch:
        return None


def substitute_with(to_delete, instance):
    """
    Substitute the first argument with the second in all relations,
    and delete the first argument afterwards.
    """
    assert to_delete.__class__ == instance.__class__
    assert to_delete.pk != instance.pk

    for related_object in to_delete._meta.get_all_related_objects():
        queryset = related_object.model._base_manager.complex_filter({
            related_object.field.name: to_delete.pk,
        })

        queryset.update(**{related_object.field.name: instance.pk})
    to_delete.delete()


def app_model_label(model):
    """
    Stop those deprecation warnings
    """
    try:
        return model._meta.app_label, model._meta.model_name
    except AttributeError:  # Django <1.6
        return model._meta.app_label, model._meta.module_name
