"""Utilities for working with Django Models."""
import itertools

from django.contrib.contenttypes.models import ContentType

from askbot.utils.lists import flatten

def fetch_model_dict(model, ids, fields=None):
    """
    Fetches a dict of model details for model instances with the given
    ids, keyed by their id.

    If a fields list is given, a dict of details will be retrieved for
    each model, otherwise complete model instances will be retrieved.

    Any fields list given shouldn't contain the primary key attribute for
    the model, as this can be determined from its Options.
    """
    if fields is None:
        return model._default_manager.in_bulk(ids)
    else:
        id_attr = model._meta.pk.attname
        return dict((obj[id_attr], obj) for obj
            in model._default_manager.filter(id__in=ids).values(
                *itertools.chain((id_attr,), fields)))

def populate_foreign_key_caches(model, objects_to_populate, fields=None):
    """
    Populates caches for the given related Model in instances of objects
    which have a ForeignKey relationship to it, specified as a list of
    (object list, related attribute name list) two-tuples.

    If a list of field names is given, only the given fields will be
    looked up and related object caches will be populated with a dict of
    the specified fields. Otherwise, complete model instances will be
    retrieved.
    """
    # Get all related object ids for the appropriate fields
    related_object_ids = []
    for objects, attrs in objects_to_populate:
        related_object_ids.append(tuple(tuple(getattr(obj, '%s_id' % attr)
                                              for attr in attrs)
                                  for obj in objects))
    unique_ids = tuple(set(pk for pk in flatten(related_object_ids) if pk))
    related_objects = fetch_model_dict(model, unique_ids, fields)

    # Fill related object caches
    for (objects, attrs), related_ids in itertools.izip(objects_to_populate,
                                                        related_object_ids):
        for obj, related_ids_for_obj in itertools.izip(objects,
                                                       related_ids):
            for attr, related_object in itertools.izip(attrs, (related_objects.get(pk, None)
                                                               for pk in related_ids_for_obj)):
                setattr(obj, '_%s_cache' % attr, related_object)

def populate_content_object_caches(generic_related_objects, model_fields=None):
    """
    Retrieves ``ContentType`` and content objects for the given list of
    items which use a generic relation, grouping the retrieval of content
    objects by model to reduce the number of queries executed.

    This results in ``number_of_content_types + 1`` queries rather than
    the ``number_of_generic_reL_objects * 2`` queries you'd get by
    iterating over the list and accessing each item's object attribute.

    If a dict mapping model classes to field names is given, only the
    given fields will be looked up for each model specified and the
    object cache will be populated with a dict of the specified fields.
    Otherwise, complete model instances will be retrieved.
    """
    if model_fields is None:
        model_fields = {}

    # Group content object ids by their content type ids
    ids_by_content_type = {}
    for obj in generic_related_objects:
        ids_by_content_type.setdefault(obj.content_type_id,
                                       []).append(obj.object_id)

    # Retrieve content types and content objects in bulk
    content_types = ContentType.objects.in_bulk(ids_by_content_type.keys())
    for content_type_id, ids in ids_by_content_type.iteritems():
        model = content_types[content_type_id].model_class()
        objects[content_type_id] = fetch_model_dict(
            model, tuple(set(ids)), model_fields.get(model, None))

    # Set content types and content objects in the appropriate cache
    # attributes, so accessing the 'content_type' and 'object' attributes
    # on each object won't result in further database hits.
    for obj in generic_related_objects:
        obj._object_cache = objects[obj.content_type_id][obj.object_id]
        obj._content_type_cache = content_types[obj.content_type_id]
