import django
import floppyforms
from copy import deepcopy

_WIDGET_COMMON_ATTRIBUTES = (
    'is_hidden',
    'needs_multipart_form',
    'is_localized',
    'is_required')

_WIDGET_COMMON_ARGUMENTS = ('attrs',)


def _copy_attributes(original, new_widget, attributes):
    for attr in attributes:
        original_value = getattr(original, attr)
        original_value = deepcopy(original_value)

        # Don't set the attribute if it is a property. In that case we can be
        # sure that the widget class is taking care of the calculation for that
        # property.
        old_value_on_new_widget = getattr(new_widget.__class__, attr, None)
        if not isinstance(old_value_on_new_widget, property):
            setattr(new_widget, attr, original_value)


def _create_widget(widget_class, copy_attributes=(), init_arguments=()):
    # attach defaults that apply for all widgets
    copy_attributes = tuple(copy_attributes) + _WIDGET_COMMON_ATTRIBUTES
    init_arguments = tuple(init_arguments) + _WIDGET_COMMON_ARGUMENTS

    def create_new_widget(original):
        kwargs = {}
        for argname in init_arguments:
            kwargs[argname] = getattr(original, argname)
        new_widget = widget_class(**kwargs)
        _copy_attributes(
            original,
            new_widget,
            copy_attributes)
        return new_widget
    return create_new_widget


def _create_radioselect(original):
    # return original widget if the renderer is something else than what
    # django ships with by default. This means if this condition evaluates to
    # true, then a custom renderer was specified. We cannot emulate its
    # behaviour so we shouldn't guess and just return the original widget
    if original.renderer is not django.forms.widgets.RadioFieldRenderer:
        return original
    create_new_widget = _create_widget(
        floppyforms.widgets.RadioSelect,
        ('choices', 'allow_multiple_selected',))
    return create_new_widget(original)


def _create_splitdatetimewidget(widget_class):
    def create_new_widget(original):
        new_widget = widget_class(
            attrs=original.attrs,
            date_format=original.widgets[0].format,
            time_format=original.widgets[1].format)
        _copy_attributes(original, new_widget, _WIDGET_COMMON_ARGUMENTS)
        return new_widget
    return create_new_widget


def _create_multiwidget(widget_class, copy_attributes=(), init_arguments=()):
    create_new_widget = _create_widget(widget_class, copy_attributes,
                                       init_arguments)

    def create_new_multiwidget(original):
        multiwidget = create_new_widget(original)
        multiwidget.widgets = [
            floppify_widget(widget)
            for widget in multiwidget.widgets]
        return multiwidget
    return create_new_multiwidget


# this dictionary keeps a mapping from django's widget classes to a callable
# that will accept an instance of this class. It will return a new instance of
# a corresponding floppyforms widget, with the same semantics -- all relevant
# attributes will be copied to the new widget.
_django_to_floppyforms_widget = {
    django.forms.widgets.Input:
        _create_widget(floppyforms.widgets.Input, ('input_type',)),
    django.forms.widgets.TextInput:
        _create_widget(floppyforms.widgets.TextInput, ('input_type',)),
    django.forms.widgets.PasswordInput:
        _create_widget(floppyforms.widgets.PasswordInput, ('input_type',)),
    django.forms.widgets.HiddenInput:
        _create_widget(floppyforms.widgets.HiddenInput, ('input_type',)),
    django.forms.widgets.MultipleHiddenInput:
        _create_widget(
            floppyforms.widgets.MultipleHiddenInput,
            ('input_type',),
            init_arguments=('choices',)),
    django.forms.widgets.FileInput:
        _create_widget(floppyforms.widgets.FileInput, ('input_type',)),
    django.forms.widgets.ClearableFileInput:
        _create_widget(
            floppyforms.widgets.ClearableFileInput,
            (
                'input_type', 'initial_text', 'input_text',
                'clear_checkbox_label', 'template_with_initial',
                'template_with_clear')),
    django.forms.widgets.Textarea:
        _create_widget(floppyforms.widgets.Textarea),
    django.forms.widgets.DateInput:
        _create_widget(
            floppyforms.widgets.DateInput,
            init_arguments=('format',)),
    django.forms.widgets.DateTimeInput:
        _create_widget(
            floppyforms.widgets.DateTimeInput,
            init_arguments=('format',)),
    django.forms.widgets.TimeInput:
        _create_widget(
            floppyforms.widgets.TimeInput,
            init_arguments=('format',)),
    django.forms.widgets.CheckboxInput:
        _create_widget(floppyforms.widgets.CheckboxInput, ('check_test',)),
    django.forms.widgets.Select:
        _create_widget(
            floppyforms.widgets.Select,
            ('choices', 'allow_multiple_selected',)),
    django.forms.widgets.NullBooleanSelect:
        _create_widget(
            floppyforms.widgets.NullBooleanSelect,
            ('choices', 'allow_multiple_selected',)),
    django.forms.widgets.SelectMultiple:
        _create_widget(
            floppyforms.widgets.SelectMultiple,
            ('choices', 'allow_multiple_selected',)),
    django.forms.widgets.RadioSelect:
        _create_radioselect,
    django.forms.widgets.CheckboxSelectMultiple:
        _create_widget(
            floppyforms.widgets.CheckboxSelectMultiple,
            ('choices', 'allow_multiple_selected',)),
    django.forms.widgets.MultiWidget:
        _create_widget(
            floppyforms.widgets.MultiWidget,
            init_arguments=('widgets',)),
    django.forms.widgets.SplitDateTimeWidget:
        _create_splitdatetimewidget(
            floppyforms.widgets.SplitDateTimeWidget),
    django.forms.widgets.SplitHiddenDateTimeWidget:
        _create_splitdatetimewidget(
            floppyforms.widgets.SplitHiddenDateTimeWidget),
    django.forms.extras.widgets.SelectDateWidget:
        _create_widget(
            floppyforms.widgets.SelectDateWidget,
            init_arguments=('years',) if django.VERSION >= (1, 7) else ('years', 'required')),
}

_django_field_to_floppyform_widget = {
    django.forms.fields.FloatField:
        _create_widget(floppyforms.widgets.NumberInput),
    django.forms.fields.DecimalField:
        _create_widget(floppyforms.widgets.NumberInput),
    django.forms.fields.IntegerField:
        _create_widget(floppyforms.widgets.NumberInput),
    django.forms.fields.EmailField:
        _create_widget(floppyforms.widgets.EmailInput),
    django.forms.fields.URLField:
        _create_widget(floppyforms.widgets.URLInput),
    django.forms.fields.SlugField:
        _create_widget(floppyforms.widgets.SlugInput),
    django.forms.fields.GenericIPAddressField:
        _create_widget(floppyforms.widgets.TextInput),
    django.forms.fields.SplitDateTimeField:
        _create_splitdatetimewidget(floppyforms.widgets.SplitDateTimeWidget),
}


def allow_floppify_widget_for_field(field):
    '''
    We only allow to replace a widget with the floppyform counterpart if the
    original, by django determined widget is still in place. We don't want to
    override custom widgets that a user specified.
    '''
    # There is a special case for IntegerFields (and all subclasses) that
    # replaces the default TextInput with a NumberInput, if localization is
    # turned off. That applies for Django 1.6 upwards.
    # See the relevant source code in django:
    #     https://github.com/django/django/blob/1.9.6/django/forms/fields.py#L261-264
    if isinstance(field, django.forms.IntegerField) and not field.localize:
        if field.widget.__class__ is django.forms.NumberInput:
            return True

    # We can check if the widget was replaced by comparing the class of the
    # specified widget with the default widget that is specified on the field
    # class.
    if field.widget.__class__ is field.__class__.widget:
        return True

    # At that point we are assuming that the user replaced the original widget
    # with a custom one. So we don't allow to overwrite it.
    return False


def floppify_widget(widget, field=None):
    '''
    Get an instance of django.forms.widgets.Widget and return a new widget
    instance but using the corresponding floppyforms widget class.

    Only original django widgets will be replaced with a floppyforms version.
    The widget will be returned unaltered if it is not known, e.g. if it's a
    custom widget from a third-party app.

    The optional parameter ``field`` can be used to influence the widget
    creation further. This is useful since floppyforms supports more widgets
    than django does. For example is django using a ``TextInput`` for a
    ``EmailField``, but floppyforms has a better suiting widget called
    ``EmailInput``. If a widget is found specifically for the passed in
    ``field``, it will take precendence to the first parameter ``widget``
    which will effectively be ignored.
    '''
    if field is not None:
        create_widget = _django_field_to_floppyform_widget.get(
            field.__class__)
        if create_widget is not None:
            if allow_floppify_widget_for_field(field):
                return create_widget(widget)
    create_widget = _django_to_floppyforms_widget.get(widget.__class__)
    if create_widget is not None:
        return create_widget(widget)
    return widget


def floppify_form(form_class):
    '''
    Take a normal form and return a subclass of that form that replaces all
    django widgets with the corresponding floppyforms widgets.
    '''
    new_form_class = type(form_class.__name__, (form_class,), {})
    for field in new_form_class.base_fields.values():
        field.widget = floppify_widget(field.widget, field=field)
    return new_form_class


def modelform_factory(model, form=django.forms.models.ModelForm, fields=None,
                      exclude=None, formfield_callback=None, widgets=None):
    form_class = django.forms.models.modelform_factory(
        model=model,
        form=form,
        fields=fields,
        exclude=exclude,
        formfield_callback=formfield_callback,
        widgets=widgets)
    return floppify_form(form_class)
