# -*- coding: utf-8 -*-
from django import forms
from django.contrib import admin
from django.contrib.contenttypes.forms import BaseGenericInlineFormSet
from django.contrib.contenttypes.admin import GenericStackedInline
from django.contrib.contenttypes.models import ContentType
from django.utils.encoding import smart_str
from django.forms.models import fields_for_model
from django.utils.translation import ugettext_lazy as _
from django.utils.text import capfirst

from djangoseo.utils import get_seo_content_types
from djangoseo.systemviews import get_seo_views


# TODO Use groups as fieldsets


def get_path_admin(use_site=False, use_subdomains=False):
    list_display = ['_path']
    search_fields = ['_path']
    list_filter = []
    if use_site:
        list_display.append('_site')
        list_filter.append('_site')
    if use_subdomains:
        list_display.append('_subdomain')
    return type('PathMetadataAdmin', (admin.ModelAdmin, ), {
        'list_display': tuple(list_display),
        'list_filter': tuple(list_filter),
        'search_fields': tuple(search_fields)
    })


def get_model_instance_admin(use_site=False, use_subdomains=False):
    list_display = ['_content_type', '_object_id', '_path']
    search_fields = ['_path', '_content_type__name']
    list_filter = []
    if use_site:
        list_display.append('_site')
        list_filter.append('_site')
    if use_subdomains:
        list_display.append('_subdomain')
    return type('ModelInstanceMetadataAdmin', (admin.ModelAdmin,), {
        'list_display': tuple(list_display),
        'list_filter': tuple(list_filter),
        'search_fields': tuple(search_fields)
    })


def get_model_admin(use_site=False, use_subdomains=False):
    list_display = ['_content_type']
    search_fields = ['_content_type__name']
    list_filter = []
    if use_site:
        list_display.append('_site')
        list_filter.append('_site')
    if use_subdomains:
        list_display.append('_subdomain')
    return type('ModelMetadataAdmin', (admin.ModelAdmin,), {
        'list_display': tuple(list_display),
        'list_filter': tuple(list_filter),
        'search_fields': tuple(search_fields)
    })


def get_view_admin(use_site=False, use_subdomains=False):
    list_display = ['_view']
    search_fields = ['_view']
    list_filter = []
    if use_site:
        list_display.append('_site')
        list_filter.append('_site')
    if use_subdomains:
        list_display.append('_subdomain')
    return type('ViewMetadataAdmin', (admin.ModelAdmin,), {
        'list_display': tuple(list_display),
        'list_filter': tuple(list_filter),
        'search_fields': tuple(search_fields)
    })


def register_seo_admin(admin_site, metadata_class, filter_list=None):
    """ Register the backends specified in Meta.backends with the admin """
    use_sites = metadata_class._meta.use_sites
    use_subdomains = metadata_class._meta.use_subdomains

    path_admin = get_path_admin(use_sites, use_subdomains)
    model_instance_admin = get_model_instance_admin(use_sites, use_subdomains)
    model_admin = get_model_admin(use_sites, use_subdomains)
    view_admin = get_view_admin(use_sites, use_subdomains)

    def get_list_display():
        return tuple(name for name, obj in metadata_class._meta.elements.items()
                     if obj.editable)

    backends = metadata_class._meta.backends

    if 'model' in backends:
        class ModelAdmin(model_admin):
            form = get_model_form(metadata_class)
            list_display = model_admin.list_display + get_list_display()
            if filter_list:  # Cannot give the filter_list variable name 'list_filter', because there is a name conflict
                list_filter = filter_list

        _register_admin(admin_site, metadata_class._meta.get_model('model'), ModelAdmin)

    if 'view' in backends:
        class ViewAdmin(view_admin):
            form = get_view_form(metadata_class)
            list_display = view_admin.list_display + get_list_display()
            if filter_list:  # Cannot give the filter_list variable name 'list_filter', because there is a name conflict
                list_filter = filter_list

        _register_admin(admin_site, metadata_class._meta.get_model('view'), ViewAdmin)

    if 'path' in backends:
        class PathAdmin(path_admin):
            form = get_path_form(metadata_class)
            list_display = path_admin.list_display + get_list_display()
            if filter_list:  # Cannot give the filter_list variable name 'list_filter', because there is a name conflict
                list_filter = filter_list

        _register_admin(admin_site, metadata_class._meta.get_model('path'), PathAdmin)

    if 'modelinstance' in backends:
        class ModelInstanceAdmin(model_instance_admin):
            form = get_modelinstance_form(metadata_class)
            list_display = model_instance_admin.list_display + get_list_display()
            if filter_list:  # Cannot give the filter_list variable name 'list_filter', because there is a name conflict
                list_filter = filter_list

        _register_admin(admin_site, metadata_class._meta.get_model('modelinstance'), ModelInstanceAdmin)


def _register_admin(admin_site, model, admin_class):
    """ Register model in the admin, ignoring any previously registered models.
        Alternatively it could be used in the future to replace a previously
        registered model.
    """
    try:
        admin_site.register(model, admin_class)
    except admin.sites.AlreadyRegistered:
        pass


class MetadataFormset(BaseGenericInlineFormSet):
    def _construct_form(self, i, **kwargs):
        """ Override the method to change the form attribute empty_permitted """
        form = super(MetadataFormset, self)._construct_form(i, **kwargs)
        # Monkey patch the form to always force a save.
        # It's unfortunate, but necessary because we always want an instance
        # Affect on performance shouldn't be too great, because ther is only
        # ever one metadata attached
        form.empty_permitted = False
        form.has_changed = lambda: True

        # Set a marker on this object to prevent automatic metadata creation
        # This is seen by the post_save handler, which then skips this instance.
        if self.instance:
            self.instance.__seo_metadata_handled = True

        return form


def get_inline(metadata_class):
    attrs = {
        'max_num': 1,
        'extra': 1,
        'model': metadata_class._meta.get_model('modelinstance'),
        'ct_field': "_content_type",
        'ct_fk_field': "_object_id",
        'formset': MetadataFormset,
    }
    return type('MetadataInline', (GenericStackedInline,), attrs)


def get_model_form(metadata_class):
    model_class = metadata_class._meta.get_model('model')

    # Restrict content type choices to the models set in seo_models
    content_types = get_seo_content_types(metadata_class._meta.seo_models)
    content_type_choices = [(x._get_pk_val(), smart_str(x)) for x in
                            ContentType.objects.filter(id__in=content_types)]

    # Get a list of fields, with _content_type at the start
    important_fields = ['_content_type'] + core_choice_fields(metadata_class)
    _fields = important_fields + list(fields_for_model(model_class,
                                                  exclude=important_fields).keys())

    class ModelMetadataForm(forms.ModelForm):
        _content_type = forms.ChoiceField(label=capfirst(_("model")),
                                          choices=content_type_choices)

        class Meta:
            model = model_class
            fields = _fields

        def clean__content_type(self):
            value = self.cleaned_data['_content_type']
            try:
                return ContentType.objects.get(pk=int(value))
            except (ContentType.DoesNotExist, ValueError):
                raise forms.ValidationError("Invalid ContentType")

    return ModelMetadataForm


def get_modelinstance_form(metadata_class):
    model_class = metadata_class._meta.get_model('modelinstance')

    # Restrict content type choices to the models set in seo_models
    content_types = get_seo_content_types(metadata_class._meta.seo_models)

    # Get a list of fields, with _content_type at the start
    important_fields = ['_content_type'] + ['_object_id'] + core_choice_fields(metadata_class)
    _fields = important_fields + list(fields_for_model(model_class,
                                                  exclude=important_fields).keys())

    class ModelMetadataForm(forms.ModelForm):
        _content_type = forms.ModelChoiceField(
            queryset=ContentType.objects.filter(id__in=content_types),
            empty_label=None,
            label=capfirst(_("model")),
        )

        _object_id = forms.IntegerField(label=capfirst(_("ID")))

        class Meta:
            model = model_class
            fields = _fields

    return ModelMetadataForm


def get_path_form(metadata_class):
    model_class = metadata_class._meta.get_model('path')

    # Get a list of fields, with _view at the start
    important_fields = ['_path'] + core_choice_fields(metadata_class)
    _fields = important_fields + list(fields_for_model(model_class,
                                                  exclude=important_fields).keys())

    class ModelMetadataForm(forms.ModelForm):
        class Meta:
            model = model_class
            fields = _fields

    return ModelMetadataForm


def get_view_form(metadata_class):
    model_class = metadata_class._meta.get_model('view')

    # Restrict content type choices to the models set in seo_models
    view_choices = [(key, " ".join(key.split("_"))) for key in get_seo_views(metadata_class)]
    view_choices.insert(0, ("", "---------"))

    # Get a list of fields, with _view at the start
    important_fields = ['_view'] + core_choice_fields(metadata_class)
    _fields = important_fields + list(fields_for_model(model_class,
                                                  exclude=important_fields).keys())

    class ModelMetadataForm(forms.ModelForm):
        _view = forms.ChoiceField(label=capfirst(_("view")),
                                  choices=view_choices, required=False)

        class Meta:
            model = model_class
            fields = _fields

    return ModelMetadataForm


def core_choice_fields(metadata_class):
    """ If the 'optional' core fields (_site and _language) are required,
        list them here.
    """
    fields = []
    if metadata_class._meta.use_sites:
        fields.append('_site')
    if metadata_class._meta.use_i18n:
        fields.append('_language')
    return fields


def _monkey_inline(model, admin_class_instance, metadata_class, inline_class, admin_site):
    """ Monkey patch the inline onto the given admin_class instance. """
    if model in metadata_class._meta.seo_models:
        # *Not* adding to the class attribute "inlines", as this will affect
        # all instances from this class. Explicitly adding to instance attribute.
        admin_class_instance.__dict__['inlines'] = admin_class_instance.inlines + [inline_class]

        # Because we've missed the registration, we need to perform actions
        # that were done then (on admin class instantiation)
        inline_instance = inline_class(admin_class_instance.model, admin_site)
        if hasattr(admin_class_instance, 'inline_instances'):
            admin_class_instance.inline_instances.append(inline_instance)


def _with_inline(func, admin_site, metadata_class, inline_class):
    """ Decorator for register function that adds an appropriate inline."""

    def register(model_or_iterable, admin_class=None, **options):
        # Call the (bound) function we were given.
        # We have to assume it will be bound to admin_site
        func(model_or_iterable, admin_class, **options)
        _monkey_inline(model_or_iterable, admin_site._registry[model_or_iterable],
                       metadata_class, inline_class, admin_site)

    return register


def auto_register_inlines(admin_site, metadata_class):
    """ This is a questionable function that automatically adds our metadata
        inline to all relevant models in the site.
    """
    inline_class = get_inline(metadata_class)

    for model, admin_class_instance in admin_site._registry.items():
        _monkey_inline(model, admin_class_instance, metadata_class, inline_class, admin_site)

    # Monkey patch the register method to automatically add an inline for this site.
    # _with_inline() is a decorator that wraps the register function with the same injection code
    # used above (_monkey_inline).
    admin_site.register = _with_inline(admin_site.register, admin_site, metadata_class, inline_class)