import django
from django.conf import settings
from django.core.exceptions import FieldError, ValidationError
from django.forms.fields import CharField
from django.forms.forms import get_declared_fields
from django.forms.formsets import formset_factory
from django.forms.models import (ModelForm, ModelFormMetaclass, ModelFormOptions, 
    fields_for_model, model_to_dict, construct_instance, BaseInlineFormSet, BaseModelFormSet,
    inlineformset_factory)
if django.VERSION >= (1, 7):
    from django.forms.utils import ErrorList
else:
    from django.forms.util import ErrorList
from django.forms.widgets import Select, media_property
from django.utils.translation import get_language, ugettext as _
from hvad.compat.metaclasses import with_metaclass
from hvad.models import TranslatableModel, BaseTranslationModel
from hvad.utils import get_cached_translation, get_translation, combine


class TranslatableModelFormMetaclass(ModelFormMetaclass):
    def __new__(cls, name, bases, attrs):
        
        """
        Django 1.3 fix, that removes all Meta.fields and Meta.exclude
        fieldnames that are in the translatable model. This ensures
        that the superclass' init method doesnt throw a validation
        error
        """
        fields = []
        exclude = []
        fieldsets = []
        if "Meta" in attrs:
            meta = attrs["Meta"]
            if getattr(meta, "fieldsets", False):
                fieldsets = meta.fieldsets
                meta.fieldsets = []
            if getattr(meta, "fields", False):
                fields = meta.fields
                meta.fields = []
            if getattr(meta, "exclude", False):
                exclude = meta.exclude
                meta.exclude = []
        # End 1.3 fix
        
        super_new = super(TranslatableModelFormMetaclass, cls).__new__
        
        formfield_callback = attrs.pop('formfield_callback', None)
        declared_fields = get_declared_fields(bases, attrs, False)
        new_class = super_new(cls, name, bases, attrs)
        
        # Start 1.3 fix
        if fields:
            new_class.Meta.fields = fields
        if exclude:
            new_class.Meta.exclude = exclude
        if fieldsets:
            new_class.Meta.fieldsets = fieldsets
        # End 1.3 fix

        if not getattr(new_class, "Meta", None):
            class Meta:
                exclude = ['language_code']
            new_class.Meta = Meta
        elif not getattr(new_class.Meta, 'exclude', None):
            new_class.Meta.exclude = ['language_code']
        elif getattr(new_class.Meta, 'exclude', False):
            if 'language_code' not in new_class.Meta.exclude:
                new_class.Meta.exclude.append("language_code")

        if 'Media' not in attrs:
            new_class.media = media_property(new_class)
        opts = new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', attrs.get('Meta', None)))
        if opts.model:
            # bail out if a wrong model uses this form class
            if not issubclass(opts.model, TranslatableModel):
                raise TypeError(
                    "Only TranslatableModel subclasses may use TranslatableModelForm"
                )
            mopts = opts.model._meta
            
            shared_fields = mopts.get_all_field_names()
            
            # split exclude and include fieldnames into shared and translated
            sfieldnames = [field for field in opts.fields or [] if field in shared_fields]
            tfieldnames = [field for field in opts.fields or [] if field not in shared_fields]
            sexclude = [field for field in opts.exclude or [] if field in shared_fields]
            texclude = [field for field in opts.exclude or [] if field not in shared_fields]
            
            # required by fields_for_model
            if not sfieldnames :
                sfieldnames = None if not fields else []
            if not tfieldnames:
                tfieldnames = None if not fields else []
            
            # If a model is defined, extract form fields from it.
            sfields = fields_for_model(opts.model, sfieldnames, sexclude,
                                       opts.widgets, formfield_callback)
            tfields = fields_for_model(mopts.translations_model, tfieldnames,
                                       texclude, opts.widgets, formfield_callback)
            
            fields = sfields
            fields.update(tfields)
            
            # make sure opts.fields doesn't specify an invalid field
            none_model_fields = [k for k, v in fields.items() if not v]
            missing_fields = set(none_model_fields) - \
                             set(declared_fields.keys())
            if missing_fields:
                message = 'Unknown field(s) (%s) specified for %s'
                message = message % (', '.join(missing_fields),
                                     opts.model.__name__)
                raise FieldError(message)
            # Override default model fields with any custom declared ones
            # (plus, include all the other declared fields).
            fields.update(declared_fields)
            
            if new_class._meta.exclude:
                new_class._meta.exclude = list(new_class._meta.exclude)
            else:
                new_class._meta.exclude = []
                
            for field in (mopts.translations_accessor, 'master'):
                if not field in new_class._meta.exclude:
                    new_class._meta.exclude.append(field)
        else:
            fields = declared_fields
        new_class.declared_fields = declared_fields
        new_class.base_fields = fields
        # always exclude the FKs
        return new_class


class TranslatableModelForm(with_metaclass(TranslatableModelFormMetaclass, ModelForm)):
    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
                 initial=None, error_class=ErrorList, label_suffix=':',
                 empty_permitted=False, instance=None):
        opts = self._meta
        model_opts = opts.model._meta
        object_data = {}
        language = getattr(self, 'language', get_language())
        if instance is not None:
            trans = get_cached_translation(instance)
            if not trans:
                try:
                    trans = get_translation(instance, language)
                except model_opts.translations_model.DoesNotExist:
                    trans = None
            if trans:
                object_data = model_to_dict(trans, opts.fields, opts.exclude)
        if initial is not None:
            object_data.update(initial)
        initial = object_data
        super(TranslatableModelForm, self).__init__(data, files, auto_id,
                                                     prefix, object_data,
                                                     error_class, label_suffix,
                                                     empty_permitted, instance)

    def save(self, commit=True):
        if self.instance.pk is None:
            fail_message = 'created'
            new = True
        else:
            fail_message = 'changed'
            new = False

        if self.errors:
            opts = self.instance._meta
            raise ValueError("The %s could not be %s because the data didn't"
                             " validate." % (opts.object_name, fail_message))

        trans_model = self.instance._meta.translations_model
        language_code = self.cleaned_data.get('language_code', get_language())

        if not new:
            trans = get_cached_translation(self.instance)
            if not trans or trans.language_code != language_code:
                try:
                    trans = get_translation(self.instance, language_code)
                except trans_model.DoesNotExist:
                    trans = trans_model()
        else:
            trans = trans_model()

        trans = construct_instance(self, trans, self._meta.fields)
        trans.language_code = language_code
        trans.master = self.instance
        self.instance = combine(trans, self.Meta.model)

        super(TranslatableModelForm, self).save(commit=commit)
        return self.instance
        
    def _post_clean(self):
        if self.instance.pk:
            try:
                # Don't use self.instance.language_code here! That will fail if
                # the instance is not translated into the current language. If
                # it succeeded, then the instance would already be translated,
                # and there'd be no point combining it with the same
                # translation again.
                trans = get_translation(self.instance, self.language)
                trans.master = self.instance
                self.instance = combine(trans, self.Meta.model)
            except self.instance._meta.translations_model.DoesNotExist:
                self.instance = self.instance.translate(self.language)
        return super(TranslatableModelForm, self)._post_clean()



class CleanMixin(object):
    def clean(self):
        data = super(CleanMixin, self).clean()
        data['language_code'] = self.language
        return data


def LanguageAwareCleanMixin(language):
    return type('BoundCleanMixin', (CleanMixin,), {'language': language})


def translatable_modelform_factory(language, model, form=TranslatableModelForm,
                                   fields=None, exclude=None,
                                   formfield_callback=None):
    # Create the inner Meta class. FIXME: ideally, we should be able to
    # construct a ModelForm without creating and passing in a temporary
    # inner class.

    # Build up a list of attributes that the Meta object will have.
    attrs = {'model': model}
    if fields is not None:
        attrs['fields'] = fields
    if exclude is not None:
        attrs['exclude'] = exclude

    # If parent form class already has an inner Meta, the Meta we're
    # creating needs to inherit from the parent's inner meta.
    parent = (object,)
    if hasattr(form, 'Meta'):
        parent = (form.Meta, object)
    Meta = type('Meta', parent, attrs)

    # Give this new form class a reasonable name.
    class_name = model.__name__ + 'Form'

    # Class attributes for the new form class.
    form_class_attrs = {
        'Meta': Meta,
        'formfield_callback': formfield_callback
    }
    clean_mixin = LanguageAwareCleanMixin(language)
    return type(class_name, (clean_mixin, form,), form_class_attrs)

def translatable_modelformset_factory(language, model, form=TranslatableModelForm, formfield_callback=None,
                         formset=BaseModelFormSet,
                         extra=1, can_delete=False, can_order=False,
                         max_num=None, fields=None, exclude=None):
    """
    Returns a FormSet class for the given Django model class.
    """
    form = translatable_modelform_factory(language, model, form=form, fields=fields, exclude=exclude,
                             formfield_callback=formfield_callback)
    FormSet = formset_factory(form, formset, extra=extra, max_num=max_num,
                              can_order=can_order, can_delete=can_delete)
    FormSet.model = model
    return FormSet

def translatable_inlineformset_factory(language, parent_model, model, form=TranslatableModelForm,
                          formset=BaseInlineFormSet, fk_name=None,
                          fields=None, exclude=None,
                          extra=3, can_order=False, can_delete=True, max_num=None,
                          formfield_callback=None):
    """
    Returns an ``InlineFormSet`` for the given kwargs.

    You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey``
    to ``parent_model``.
    """
    from django.forms.models import _get_foreign_key
    fk = _get_foreign_key(parent_model, model, fk_name=fk_name)
    # enforce a max_num=1 when the foreign key to the parent model is unique.
    if fk.unique:
        max_num = 1
    kwargs = {
        'form': form,
        'formfield_callback': formfield_callback,
        'formset': formset,
        'extra': extra,
        'can_delete': can_delete,
        'can_order': can_order,
        'fields': fields,
        'exclude': exclude,
        'max_num': max_num,
    }
    FormSet = translatable_modelformset_factory(language, model, **kwargs)
    FormSet.fk = fk
    return FormSet


class BaseTranslationFormSet(BaseInlineFormSet):
    """A kind of inline formset for working with an instance's translations.
    It keeps track of the real object and combine()s it to the translations
    for validation and saving purposes.
    It can delete translations, but will refuse to delete the last one.
    """
    def __init__(self, *args, **kwargs):
        super(BaseTranslationFormSet, self).__init__(*args, **kwargs)
        self.queryset = self.order_translations(self.queryset)

    def order_translations(self, qs):
        return qs.order_by('language_code')

    def clean(self):
        super(BaseTranslationFormSet, self).clean()

        # Trigger combined instance validation
        master = self.instance
        stashed = get_cached_translation(master)

        for form in self.forms:
            form.instance.master = master
            combined = combine(form.instance, master.__class__)
            exclusions = form._get_validation_exclusions()
            # fields from the shared model should not be validated
            exclusions.extend(f.name for f in combined._meta.fields)
            try:
                if django.VERSION >= (1, 6):
                    combined.full_clean(exclude=exclusions,
                                        validate_unique=form._validate_unique)
                else:
                    combined.full_clean(exclude=exclusions)
            except ValidationError as e:
                form._update_errors(e)

        if stashed is None:
            delattr(master, master._meta.translations_cache)
        else:
            setattr(master, master._meta.translations_cache, stashed)

        # Validate that at least one translation exists
        forms_to_delete = self.deleted_forms
        provided = [form for form in self.forms
                    if (getattr(form.instance, 'pk', None) is not None or
                        form.has_changed())
                       and not form in forms_to_delete]
        if len(provided) < 1:
            raise ValidationError(_('At least one translation must be provided'),
                                  code='notranslation')

    def _save_translation(self, form, commit=True):
        obj = form.save(commit=False)
        assert isinstance(obj, BaseTranslationModel)

        if commit:
            # We need to trigger custom save actions on the combined model
            master = self.instance
            stashed = get_cached_translation(master)
            obj.master = master
            combined = combine(obj, master.__class__)
            combined.save()
            if hasattr(combined, 'save_m2m'):   # cannot happen, but feature
                combined.save_m2m()             # could be added, be ready
            if stashed is None:
                delattr(master, master._meta.translations_cache)
            else:
                setattr(master, master._meta.translations_cache, stashed)
        return obj

    def save_new(self, form, commit=True):
        return self._save_translation(form, commit)

    def save_existing(self, form, instance, commit=True):
        return self._save_translation(form, commit)

    def add_fields(self, form, index):
        super(BaseTranslationFormSet, self).add_fields(form, index)
        # Add the language code automagically
        if not 'language_code' in form.fields:
            form.fields['language_code'] = CharField(
                required=True, initial=form.instance.language_code,
                widget=Select(choices=(('', '--'),)+settings.LANGUAGES)
            )
            # Add language_code to self._meta.fields so it is included in validation stage
            try:
                form._meta.fields.append('language_code')
            except AttributeError:
                form._meta.fields += ('language_code',)

        # Remove the master foreignkey, we have this from self.instance already
        if 'master' in form.fields:
            del form.fields['master']

def translationformset_factory(model, **kwargs):
    """ Works as a regular inlineformset_factory except for:
    - it is set up to work on the given model's translations table
    - it uses a BaseTranslationFormSet to handle combine() and language_code

    Basic use: MyModelTranslationsFormSet = translationformset_factory(MyModel)
    """
    defaults = {
        'formset': BaseTranslationFormSet,
        'fk_name': 'master',
    }
    defaults.update(kwargs)
    return inlineformset_factory(model, model._meta.translations_model, **defaults)

