# file eulxml/forms/xmlobject.py
#
#   Copyright 2010,2011 Emory University Libraries
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.

# xmlobject-backed django form (analogous to django db model forms)
# this code borrows heavily from django.forms.models

from collections import defaultdict
from string import capwords

from django.forms import BaseForm, CharField, IntegerField, BooleanField, \
        ChoiceField, Field, Form, DateField
from django.forms.forms import NON_FIELD_ERRORS
from django.forms.forms import get_declared_fields
from django.forms.formsets import formset_factory, BaseFormSet
from django.forms.models import ModelFormOptions
from django.utils.datastructures  import SortedDict
from django.utils.safestring  import mark_safe

from eulxml import xmlmap

def fieldname_to_label(name):
    """Default conversion from xmlmap Field variable name to Form field label:
    convert '_' to ' ' and capitalize words.  Should only be used when verbose_name
    is not set."""
    return capwords(name.replace('_', ' '))


def _parse_field_list(fieldnames, include_parents=False):
    """
    Parse a list of field names, possibly including dot-separated subform
    fields, into an internal ParsedFieldList object representing the base
    fields and subform listed.

    :param fieldnames: a list of field names as strings. dot-separated names
        are interpreted as subform fields.
    :param include_parents: optional boolean, defaults to False. if True,
        subform fields implicitly include their parent fields in the parsed
        list.
    """
    field_parts = (name.split('.') for name in fieldnames)
    return _collect_fields(field_parts, include_parents)

def _collect_fields(field_parts_list, include_parents):
    """utility function to enable recursion in _parse_field_list()"""
    fields = []
    subpart_lists = defaultdict(list)

    for parts in field_parts_list:
        field, subparts = parts[0], parts[1:]
        if subparts:
            if include_parents and field not in fields:
                fields.append(field)
            subpart_lists[field].append(subparts)
        else:
            fields.append(field)

    subfields = dict((field, _collect_fields(subparts, include_parents))
                     for field, subparts in subpart_lists.iteritems())

    return ParsedFieldList(fields, subfields)

class ParsedFieldList(object):
    """A parsed list of fields, used internally by :class:`XmlObjectForm`
    for tracking field and exclude lists."""
    def __init__(self, fields, subfields):
        self.fields = fields
        self.subfields = subfields

class SubformAwareModelFormOptions(ModelFormOptions):
    """A :class:`~django.forms.models.ModelFormOptions` subclass aware of
    fields and exclude lists, parsing them for later reference by
    :class:`XmlObjectForm` internals."""

    def __init__(self, options=None):
        super(SubformAwareModelFormOptions, self).__init__(options)

        # store maximum number of repeated subforms that should be allowed
        self.max_num = getattr(options, 'max_num', None)
        self.can_delete = getattr(options, 'can_delete', True)
        self.can_order = getattr(options, 'can_order', False)
        self.extra = getattr(options, 'extra', 1)

        self.parsed_fields = None
        if isinstance(self.fields, ParsedFieldList):
            self.parsed_fields = self.fields
        elif self.fields is not None:
            self.parsed_fields = _parse_field_list(self.fields, include_parents=True)

        self.parsed_exclude = None
        if isinstance(self.exclude, ParsedFieldList):
            self.parsed_exclude = self.exclude
        elif self.exclude is not None:
            self.parsed_exclude = _parse_field_list(self.exclude, include_parents=False)



def formfields_for_xmlobject(model, fields=None, exclude=None, widgets=None, options=None,
        declared_subforms=None, max_num=None, extra=None):
    """
    Returns three sorted dictionaries (:class:`django.utils.datastructures.SortedDict`).
     * The first is a dictionary of form fields based on the
       :class:`~eulxml.xmlmap.XmlObject` class fields and their types.
     * The second is a sorted dictionary of subform classes for any fields of type
       :class:`~eulxml.xmlmap.fields.NodeField` on the model.
     * The third is a sorted dictionary of formsets for any fields of type
       :class:`~eulxml.xmlmap.fields.NodeListField` on the model.

    Default sorting (within each dictionary) is by XmlObject field creation order.

    Used by :class:`XmlObjectFormType` to set up a new :class:`XmlObjectForm`
    class.

    :param fields: optional list of field names; if specified, only the named fields
                will be returned, in the specified order
    :param exclude: optional list of field names that should not be included on
                the form; if a field is listed in both ``fields`` and ``exclude``,
                it will be excluded
    :param widgets: optional dictionary of widget options to be passed to form
                field constructor, keyed on field name
    :param options: optional :class:`~django.forms.models.ModelFormOptions`.
                if specified then fields, exclude, and widgets will default
                to its values.
    :param declared_subforms: optional dictionary of field names and form classes;
                if specified, the specified form class will be used to initialize
                the corresponding subform (for a :class:`~eulxml.xmlmap.fields.NodeField`)
                or a formset (for a :class:`~eulxml.xmlmap.fields.NodeListField`)
    :param max_num: optional value for the maximum number of times a fieldset should repeat.
    :param max_num: optional value for the number of extra forms to provide.
    """

    # first collect fields and excludes for the form and all subforms. base
    # these on the specified options object unless overridden in args.
    fieldlist = getattr(options, 'parsed_fields', None)
    if isinstance(fields, ParsedFieldList):
        fieldlist = fields
    elif fields is not None:
        fieldlist = _parse_field_list(fields, include_parents=True)

    excludelist = getattr(options, 'parsed_exclude', None)
    if isinstance(fields, ParsedFieldList):
        fieldlist = fields
    elif exclude is not None:
        excludelist = _parse_field_list(exclude, include_parents=False)

    if widgets is None and options is not None:
        widgets = options.widgets

    if max_num is None and options is not None:
        max_num = options.max_num

    # collect the fields (unordered for now) that we're going to be returning
    formfields = {}
    subforms = {}
    formsets = {}
    field_order = {}
    subform_labels = {}

    for name, field in model._fields.iteritems():
        if fieldlist and not name in fieldlist.fields:
            # if specific fields have been requested and this is not one of them, skip it
            continue
        if excludelist and name in excludelist.fields:
            # if exclude has been specified and this field is listed, skip it
            continue
        if widgets and name in widgets:
            # if a widget has been specified for this field, pass as option to form field init
            kwargs = {'widget': widgets[name] }
        else:
            kwargs = {}
        # get apppropriate form widget based on xmlmap field type
        field_type = None

        # if the xmlmap field knows whether or not it is required, use for form
        if field.required is not None:
            kwargs['required'] = field.required
        if field.verbose_name is not None:
            kwargs['label'] = field.verbose_name
        if field.help_text is not None:
            kwargs['help_text'] = field.help_text

        if hasattr(field, 'choices') and field.choices:
            # if a field has choices defined, use a choice field (no matter what base type)
            field_type = ChoiceField
            kwargs['choices'] = [(val, val) for val in field.choices]
            # FIXME: how to properly do non-required choice field?
            # if field is optional, add a blank choice at the beginning of the list
            if field.required == False and '' not in field.choices:
                # TODO: add an empty_label option (like django ModelChoiceField)
                # to xmlobjectform and pass it in to make this easier to customize
                kwargs['choices'].insert(0, ('', ''))
        elif isinstance(field, xmlmap.fields.StringField):
            field_type = CharField
        elif isinstance(field, xmlmap.fields.IntegerField):
            field_type = IntegerField
        elif isinstance(field, xmlmap.fields.DateField):
            field_type = DateField
        elif isinstance(field, xmlmap.fields.SimpleBooleanField):
            # by default, fields are required - for a boolean, required means it must be checked
            # since that seems nonsensical and not useful for a boolean,
            # setting required to False to allow True or False values
            kwargs['required'] = False
            field_type = BooleanField

        # datefield ? - not yet well-supported; leaving out for now
        # ... should probably distinguish between date and datetime field

        elif isinstance(field, xmlmap.fields.NodeField) or \
            isinstance(field, xmlmap.fields.NodeListField):
            form_label = kwargs['label'] if 'label' in kwargs else fieldname_to_label(name)
            # store subform label in case we can't set on subform/formset
            subform_labels[name] = form_label

             # if a subform class was declared, use that class exactly as is
            if name in declared_subforms:
            	subform = declared_subforms[name]

            # otherwise, define a new xmlobject form for the nodefield or
            # nodelistfield class, using any options passed in for fields under this one
            else:
                subform_opts = {
                    'fields': fieldlist.subfields[name] if fieldlist and name in fieldlist.subfields else None,
                    'exclude': excludelist.subfields[name] if excludelist and name in excludelist.subfields else None,
                    'widgets': widgets[name] if widgets and name in widgets else None,
                    'label': form_label,
                }

                # create the subform class
                subform = xmlobjectform_factory(field.node_class, **subform_opts)

            # store subform or generate and store formset, depending on field type
            if isinstance(field, xmlmap.fields.NodeField):
                subforms[name] = subform
            elif isinstance(field, xmlmap.fields.NodeListField):
                # formset_factory is from django core and we link into it here.
                formsets[name] = formset_factory(subform, formset=BaseXmlObjectFormSet,
                    max_num=subform._meta.max_num, can_delete=subform._meta.can_delete,
                    extra=subform._meta.extra, can_order=subform._meta.can_order)

                formsets[name].form_label = form_label

        elif isinstance(field, xmlmap.fields.StringListField) or \
	    isinstance(field, xmlmap.fields.IntegerListField):
            form_label = kwargs['label'] if 'label' in kwargs else fieldname_to_label(name)

            if isinstance(field, xmlmap.fields.IntegerListField):
            	listform = IntegerListFieldForm
            else:
            	listform = ListFieldForm

            # generate a listfield formset
            formsets[name] = formset_factory(listform, formset=BaseXmlObjectListFieldFormSet)
            # don't need can_delete: since each form is a single field, empty implies delete
            # todo: extra, max_num ? widget?
            formsets[name].form_label = form_label

        # TODO: other list variants


        else:
            # raise exception for unsupported fields
            # currently doesn't handle list fields
            raise Exception('Error on field "%s": XmlObjectForm does not yet support auto form field generation for %s.' \
            	% (name, field.__class__))

        if field_type is not None:
            if 'label' not in kwargs:
                kwargs['label'] = fieldname_to_label(name)
            formfields[name] = field_type(**kwargs)

        # create a dictionary indexed by field creation order, for default field ordering
        field_order[field.creation_counter] = name

    # if fields were explicitly specified, return them in that order
    if fieldlist:
        ordered_fields = SortedDict((name, formfields[name])
                                    for name in fieldlist.fields
                                    if name in formfields)
        ordered_subforms = SortedDict((name, subforms[name])
                                      for name in fieldlist.fields
                                      if name in subforms)
        ordered_formsets = SortedDict((name, formsets[name])
                                      for name in fieldlist.fields
                                      if name in formsets)
    else:
        # sort on field creation counter and generate a django sorted dictionary
        ordered_fields = SortedDict(
            [(field_order[key], formfields[field_order[key]]) for key in sorted(field_order.keys())
                                                if field_order[key] in formfields ]
        )
        ordered_subforms = SortedDict(
            [(field_order[key], subforms[field_order[key]]) for key in sorted(field_order.keys())
                                                if field_order[key] in subforms ]
        )
        ordered_formsets = SortedDict(
            [(field_order[key], formsets[field_order[key]]) for key in sorted(field_order.keys())
                                                if field_order[key] in formsets ]
        )
    return ordered_fields, ordered_subforms, ordered_formsets, subform_labels


def xmlobject_to_dict(instance, fields=None, exclude=None, prefix=''):
    """
    Generate a dictionary based on the data in an XmlObject instance to pass as
    a Form's ``initial`` keyword argument.

    :param instance: instance of :class:`~eulxml.xmlmap.XmlObject`
    :param fields: optional list of fields - if specified, only the named fields
            will be included in the data returned
    :param exclude: optional list of fields to exclude from the data
    """
    data = {}
    # convert prefix to combining form for convenience
    if prefix:
        prefix = '%s-' % prefix
    else:
        prefix = ''

    for name, field in instance._fields.iteritems():
        # not editable?
        if fields and not name in fields:
            continue
        if exclude and name in exclude:
            continue
        if isinstance(field, xmlmap.fields.NodeField):
            nodefield = getattr(instance, name)
            if nodefield is not None:
                subprefix = '%s%s' % (prefix, name)
                node_data = xmlobject_to_dict(nodefield, prefix=subprefix)
                data.update(node_data)   # FIXME: fields/exclude
        if isinstance(field, xmlmap.fields.NodeListField):
            for i, child in enumerate(getattr(instance, name)):
                subprefix = '%s%s-%d' % (prefix, name, i)
                node_data = xmlobject_to_dict(child, prefix=subprefix)
                data.update(node_data)   # FIXME: fields/exclude
        else:
            data[prefix + name] = getattr(instance, name)

    return data

class XmlObjectFormType(type):
    """
    Metaclass for :class:`XmlObject`.

    Analogous to, and substantially based on, Django's ``ModelFormMetaclass``.

    Initializes the XmlObjectForm based on the :class:`~eulxml.xmlmap.XmlObject`
    instance associated as a model. Adds form fields for supported
    :class:`~eulxml.xmlmap.fields.Field`s and 'subform' XmlObjectForm classes
    for any :class:`~eulxml.xmlmap.fields.NodeField` to the Form object.
    """
    def __new__(cls, name, bases, attrs):
        # let django do all the work of finding declared/inherited fields
        tmp_fields = get_declared_fields(bases, attrs, with_base_fields=False)
        declared_fields = {}
        declared_subforms = {}
        declared_subform_labels = {}
        # sort declared fields into sub-form overrides and regular fields
        for fname, f in tmp_fields.iteritems():
            if isinstance(f, SubformField):
                # FIXME: pass can_delete, can_delete from subformfield to formset?
                declared_subforms[fname] = f.formclass
                # if a declared subform fields has a label specified, store it
                if hasattr(f, 'form_label') and f.form_label is not None:
                    declared_subform_labels[fname] = f.form_label
                # if a subformclass has a label, use that
                elif hasattr(f.formclass, 'form_label') and \
                         f.formclass.form_label is not None:
                    declared_subform_labels[fname] = f.formclass.form_label
            else:
                declared_fields[fname] = f

        new_class = super(XmlObjectFormType, cls).__new__(cls, name, bases, attrs)

        # use django's default model form options for fields, exclude, widgets, etc.
        opts = new_class._meta =  SubformAwareModelFormOptions(getattr(new_class, 'Meta',  None))
        if opts.model:
            # if a model is defined, get xml fields and any subform classes
            fields, subforms, formsets, subform_labels = formfields_for_xmlobject(opts.model, options=opts,
                    declared_subforms=declared_subforms)

            # Override default model fields with any custom declared ones
            # (plus, include all the other declared fields).
            fields.update(declared_fields)

            # store all of the dynamically generated xmlobjectforms for nodefields
            new_class.subforms = subforms

            # and for listfields
            new_class.formsets = formsets

            # labels for subforms that couldn't be set by formfields_for_xmlobject
            # declared subform labels should supercede verbose xmlobject field names
            subform_labels.update(declared_subform_labels)
            new_class.subform_labels = subform_labels

        else:
            fields = declared_fields
            new_class.subforms = {}
            new_class.formsets = {}

        new_class.declared_fields = declared_fields
        new_class.base_fields = fields

        return new_class


class XmlObjectForm(BaseForm):
    """Django Form based on an :class:`~eulxml.xmlmap.XmlObject` model,
    analogous to Django's ModelForm.

    Note that not all :mod:`eulxml.xmlmap.fields` are currently supported; all
    released field types are supported in their single-node variety, but no list
    field types are currently supported.  Attempting to define an XmlObjectForm
    without excluding unsupported fields will result in an Exception.

    Unlike Django's ModelForm, which provides a save() method, XmlObjectForm
    provides analogous functionality via :meth:`update_instance`.  Since an
    XmlObject by itself does not have a save method, and can only be saved
    in particular contexts, there is no general way for an XmlObjectForm to
    save an associated model instance to the appropriate datastore.

    If you wish to customize the html display for an XmlObjectForm, rather than
    using the built-in form display functions, be aware that if your XmlObject
    has any fields of type :class:`~eulxml.xmlmap.fields.NodeField`, you should
    make sure to display the subforms for those fields.

    NOTE: If your XmlObject includes NodeField elements and you do not want
    empty elements in your XML output when empty values are entered into the form,
    you may wish to extend :meth:`eulxml.xmlmap.XmlObject.is_empty` to correctly
    identify when your NodeField elements should be considered empty (if the
    default definition is not accurate or appropriate).  Empty elements will not
    be added to the :class:`eulxml.xmlmap.XmlObject` instance returned by
    :meth:`update_instance`.