from django.forms.models import ModelFormMetaclass, ModelForm
from django.template import RequestContext, loader
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.core.xheaders import populate_xheaders
from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import ugettext
from django.contrib.auth.views import redirect_to_login
from django.views.generic import GenericViewError

from django.contrib.auth.models import Message
from copy import deepcopy
from google.appengine.ext import db


def apply_extra_context(extra_context, context):
    """
    Adds items from extra_context dict to context.  If a value in extra_context
    is callable, then it is called and the result is added to context.
    """
    for key, value in extra_context.iteritems():
        if callable(value):
            context[key] = value()
        else:
            context[key] = value

def get_model_and_form_class(model, form_class):
    """
    Returns a model and form class based on the model and form_class
    parameters that were passed to the generic view.

    If ``form_class`` is given then its associated model will be returned along
    with ``form_class`` itself.  Otherwise, if ``model`` is given, ``model``
    itself will be returned along with a ``ModelForm`` class created from
    ``model``.
    """
    if form_class:
        return form_class._meta.model, form_class
    if model:
        # The inner Meta class fails if model = model is used for some reason.
        tmp_model = model
        # TODO: we should be able to construct a ModelForm without creating
        # and passing in a temporary inner class.
        class Meta:
            model = tmp_model
        class_name = model.__name__ + 'Form'
        form_class = ModelFormMetaclass(class_name, (ModelForm,), {'Meta': Meta})
        return model, form_class
    raise GenericViewError("Generic view must be called with either a model or"
                           " form_class argument.")

def redirect(post_save_redirect, obj):
    """
    Returns a HttpResponseRedirect to ``post_save_redirect``.

    ``post_save_redirect`` should be a string, and can contain named string-
    substitution place holders of ``obj`` field names.

    If ``post_save_redirect`` is None, then redirect to ``obj``'s URL returned
    by ``get_absolute_url()``.  If ``obj`` has no ``get_absolute_url`` method,
    then raise ImproperlyConfigured.

    This function is meant to handle the post_save_redirect parameter to the
    ``create_object`` and ``update_object`` views.
    """
    if post_save_redirect:
        return HttpResponseRedirect(post_save_redirect % dict(obj.__dict__,
            key=str(obj.key()), key_name=obj.key().name(),
            key_id=obj.key().id()))
    elif hasattr(obj, 'get_absolute_url'):
        return HttpResponseRedirect(obj.get_absolute_url())
    else:
        raise ImproperlyConfigured(
            "No URL to redirect to.  Either pass a post_save_redirect"
            " parameter to the generic view or define a get_absolute_url"
            " method on the Model.")

def lookup_object(model, object_id, slug, slug_field, queryset=None):
    """
    Return the ``model`` object with the passed ``object_id``.  If
    ``object_id`` is None, then return the object whose ``slug_field``
    equals the passed ``slug``.  If ``slug`` and ``slug_field`` are not passed,
    then raise Http404 exception.
    """
    # GAE: Here is the ugly part: We assume that if an object_id is given it
    # must be a key or id or key name for use with get()/get_by_*().
    # In this case we just ignore the query. If slug is given we use the query.
    if object_id:
        # GAE: First try to use a key id or name. While keys may be used in
        # URLs they expose implementation details (app name, entities, and
        # groups) which might not be desired.
        try:
            key_id = int(object_id)
        except:
            obj = None
        else:
            obj = model.get_by_id(key_id)
        if not obj:
            try:
                if not isinstance(object_id, db.Key):
                    object_id = db.Key(object_id)
            except:
                obj = model.get_by_key_name(object_id)
            else:
                obj = model.get(object_id)
    elif slug and slug_field:
        if queryset is None:
            queryset = model.all()
        else:
            queryset = deepcopy(queryset)
        obj = queryset.filter(slug_field + ' =', slug).get()
    else:
        raise GenericViewError(
            "Generic view must be called with either an object_id or a"
            " slug/slug_field.")
    if not obj:
        raise Http404, "No %s found matching the query" % (model._meta.verbose_name)
    return obj

def create_object(request, model=None, template_name=None,
        template_loader=loader, extra_context=None, post_save_redirect=None,
        login_required=False, context_processors=None, form_class=None):
    """
    Generic object-creation function.

    Templates: ``<app_label>/<model_name>_form.html``
    Context:
        form
            the form for the object
    """
    if extra_context is None: extra_context = {}
    if login_required and not request.user.is_authenticated():
        return redirect_to_login(request.path)

    model, form_class = get_model_and_form_class(model, form_class)
    if request.method == 'POST':
        form = form_class(request.POST, request.FILES)
        if form.is_valid():
            new_object = form.save()
            if request.user.is_authenticated():
                Message(user=request.user, message=ugettext("The %(verbose_name)s was created successfully.") % {"verbose_name": model._meta.verbose_name}).put()
            return redirect(post_save_redirect, new_object)
    else:
        form = form_class()

    # Create the template, context, response
    if not template_name:
        template_name = "%s/%s_form.html" % (model._meta.app_label, model._meta.object_name.lower())
    t = template_loader.get_template(template_name)
    c = RequestContext(request, {
        'form': form,
    }, context_processors)
    apply_extra_context(extra_context, c)
    return HttpResponse(t.render(c))

def update_object(request, model=None, object_id=None, slug=None,
        slug_field='slug', template_name=None, template_loader=loader,
        extra_context=None, post_save_redirect=None, login_required=False,
        context_processors=None, template_object_name='object',
        form_class=None):
    """
    Generic object-update function.

    Templates: ``<app_label>/<model_name>_form.html``
    Context:
        form
            the form for the object
        object
            the original object being edited
    """
    if extra_context is None: extra_context = {}
    if login_required and not request.user.is_authenticated():
        return redirect_to_login(request.path)

    model, form_class = get_model_and_form_class(model, form_class)
    obj = lookup_object(model, object_id, slug, slug_field)

    if request.method == 'POST':