"""
Helper functions for creating Form classes from Django models
and database field objects.

Enhanced form_for_instance fills in missing fields of the form
object's data from the original model instance.  See 'Changed from
stock newforms' in docstrings and comments.  Implemented by akaihola
2007-02-08.
"""

try:
    from django import newforms as forms
except ImportError:
    from django import forms

def model_save(self, commit=True):
    """
    Creates and returns model instance according to self.clean_data.

    This method is created for any form_for_model Form.
    """
    if self.errors:
        raise ValueError("The %s could not be created because the data didn't "
                         "validate." % self._model._meta.object_name)
    return save_instance(self, self._model(), commit)

def save_instance(form, instance, commit=True):
    """
    Saves bound Form ``form``'s clean_data into model instance ``instance``.

    Assumes ``form`` has a field for every non-AutoField database field in
    ``instance``. If commit=True, then the changes to ``instance`` will be
    saved to the database. Returns ``instance``.
    """
    from django.db import models
    opts = instance.__class__._meta
    if form.errors:
        raise ValueError("The %s could not be changed because "
                         "the data didn't validate." % opts.object_name)
    try:
        # Django newer than May 17, 2007
        clean_data = form.cleaned_data
    except AttributeError:
        # Django older than May 17, 2007
        clean_data = form.clean_data

    for f in opts.fields:
        if isinstance(f, models.AutoField):
            continue

        # Changed from stock newforms:
        # only set values for fields actually found in the form
        # and keep old values for the rest of the fields
        if f.name in clean_data:
            value = clean_data[f.name]
            if isinstance(value, models.Model):
                value = value._get_pk_val()
            setattr(instance, f.attname, value)

    if commit:
        instance.save()
        for f in opts.many_to_many:
            setattr(instance, f.attname, clean_data[f.name])
    # GOTCHA: If many-to-many data is given and commit=False, the many-to-many
    # data will be lost. This happens because a many-to-many options cannot be
    # set on an object until after it's saved. Maybe we should raise an
    # exception in that case.
    return instance

def make_instance_save(instance):
    "Returns the save() method for a form_for_instance Form."
    def save(self, commit=True):
        return save_instance(self, instance, commit)
    return save

def form_for_instance(instance, form=forms.BaseForm,
                      formfield_callback=lambda f, **kwargs:
                      f.formfield(**kwargs),
                      include_fields=None, exclude_fields=None):
    """
    Returns a Form class for the given Django model instance.

    Provide ``form`` if you want to use a custom BaseForm subclass.

    Provide ``formfield_callback`` if you want to define different logic for
    determining the formfield for a given database field. It's a callable that
    takes a database Field instance, plus **kwargs, and returns a form Field
    instance with the given kwargs (i.e. 'initial').

    Changed from stock newforms:
    - added parameters ``include_fields`` and ``exclude_fields``:
      * if ``include_fields`` is specified, only fieldnames in that
        list are used in the form
      * if ``exclude_fields`` is specified, fieldnames in that list
        are not used in the form
      * both ``include_fields`` and ``exclude_fields`` cannot be
        specified at the same time
    """
    if include_fields is not None and exclude_fields is not None:
        raise ValueError("form_for_instance can't use both "
                         "include_fields and exclude_fields at the same time")
    model = instance.__class__
    opts = model._meta
    field_list = []
    for f in opts.fields + opts.many_to_many:

        # Changed from stock newforms:
        # Do not use fields which are not in ``include_fields`` (if it is
        # specified) or which are in ``exclude_fields`` (if it is specified).
        if (include_fields and f.name not in include_fields) or \
           (exclude_fields and f.name in exclude_fields):
            continue

        current_value = f.value_from_object(instance)
        formfield = formfield_callback(f, initial=current_value)
        if formfield:
            field_list.append((f.name, formfield))
    fields = forms.SortedDictFromList(field_list)
    return type(opts.object_name + 'InstanceForm', (form,),
        {'base_fields': fields,
         '_model': model,
         'save': make_instance_save(instance)})


def get_changed_fields(form, instance, fields=None):
    """
    Compare the validated form to the given instance and return a list of
    fields whose values don't match.  The ``fields`` keyword argument can be
    used to specify a limited set of fields to compare.  All fields are
    compared by default.
    """
    opts = instance._meta
    cleaned_data = form.cleaned_data
    changed_field_names = set()
    for f in opts.fields:
        if fields and f.name not in fields:
            continue
        if f.name in cleaned_data:
            if getattr(instance, f.name) != cleaned_data[f.name]:
                changed_field_names.add(f.name)
    for f in opts.many_to_many:
        if fields and f.name not in fields:
            continue
        if f.name in cleaned_data:
            if instance.pk is None: # creating a new object
                changed_field_names.add(f.name)
                continue
            instvalue = set(field._get_pk_val()
                            for field in getattr(instance, f.name).all())
            formvalue = set(field._get_pk_val()
                            for field in cleaned_data[f.name])
            if instvalue != formvalue:
                changed_field_names.add(f.name)
    return changed_field_names


if hasattr(forms.models, 'InlineFormset'):
    def inline_formset(parent_model, model,
                       form=forms.BaseForm, formset=forms.models.InlineFormset,
                       fk_name=None, fields=None, extra=3, orderable=False,
                       deletable=True, formfield_callback=lambda f: f.formfield()):
        """
        Returns an ``InlineFormset`` for the given kwargs.

        You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey``
        to ``parent_model``.

        This modified version of the ``newforms.models.inline_formset()`` function
        is a temporary fix while #5759 is not yet fixed.  -- akaihola 2007-10-15
        """
        fk = forms.models.get_foreign_key(parent_model, model, fk_name=fk_name)
        FormSet = forms.formset_for_model(model, formset=formset, fields=fields,
                                          formfield_callback=formfield_callback,
                                          extra=extra, orderable=orderable,
                                          deletable=deletable, form=form)
        try:
            del FormSet.form_class.base_fields[fk.name]
        except KeyError:
            pass
        FormSet.fk = fk
        return FormSet


if hasattr(forms, 'ModelForm'):

    # ModelForms have been in Django since r6844.  These classes implement
    # enhancements to them.

    class SmartModelFormData:
        """
        Helper class for SmartModelForm.
        """
        def __init__(self, form):
            self.form = form
        def __getitem__(self, attr):
            return self.form._get_field_val(attr)

    class SmartModelForm(forms.ModelForm):
        """
        With this model form base class you can access underlying form data in a
        template:

        {{ myform.field_values.fieldname }}

        This is useful if you need to display some data from the model instance
        without having a form field for it.
        """
        def __init__(self, *args, **kwargs):
            super(SmartModelForm, self).__init__(*args, **kwargs)
            self.field_values = SmartModelFormData(self)

        def _get_field_val(self, fieldname):
            """
            Return the value for the given field from the POST data for bound forms
            and initial data for non-bound forms.  This is really just a hack...
            """
            if self.is_bound:
                if fieldname in self:
                    value = self[fieldname].data
                else:
                    value = None
            else:
                value = self.initial.get(fieldname, None)
            if value is not None:
                from django.db import models
                field = self._meta.model._meta.get_field(fieldname)
                if isinstance(field, models.ForeignKey):
                    value = field.rel.to.objects.get(pk=value)
            return value
