# ------------------------------------------------------------------------
# coding=utf-8
# ------------------------------------------------------------------------

from __future__ import absolute_import, unicode_literals

import copy
import logging
import warnings

from django import forms
from django.contrib.admin.options import InlineModelAdmin
try:
    from django.contrib.admin.utils import unquote
except ImportError:  # Django 1.6
    from django.contrib.admin.util import unquote
from django.contrib.auth import get_permission_codename
from django.http import Http404

from feincms import settings, ensure_completely_loaded
from feincms.extensions import ExtensionModelAdmin
from feincms.signals import itemeditor_post_save_related


# ------------------------------------------------------------------------
FEINCMS_CONTENT_FIELDSET_NAME = 'FEINCMS_CONTENT'
FEINCMS_CONTENT_FIELDSET = (FEINCMS_CONTENT_FIELDSET_NAME, {'fields': ()})

logger = logging.getLogger(__name__)


# ------------------------------------------------------------------------
class ItemEditorForm(forms.ModelForm):
    """
    The item editor form contains hidden region and ordering fields and should
    be used for all content type inlines.
    """

    region = forms.CharField(widget=forms.HiddenInput())
    ordering = forms.IntegerField(widget=forms.HiddenInput())


# ------------------------------------------------------------------------
class FeinCMSInline(InlineModelAdmin):
    """
    Custom ``InlineModelAdmin`` subclass used for content types.
    """

    form = ItemEditorForm
    extra = 0
    fk_name = 'parent'
    template = 'admin/feincms/content_inline.html'


# ------------------------------------------------------------------------
class ItemEditor(ExtensionModelAdmin):
    """
    The ``ItemEditor`` is a drop-in replacement for ``ModelAdmin`` with the
    speciality of knowing how to work with :class:`feincms.models.Base`
    subclasses and associated content types.

    It does not have any public API except from everything inherited from'
    the standard ``ModelAdmin`` class.
    """

    def __init__(self, model, admin_site):
        ensure_completely_loaded()

        super(ItemEditor, self).__init__(model, admin_site)

    def get_inline_instances(self, request, *args, **kwargs):
        inline_instances = super(ItemEditor, self).get_inline_instances(
            request, *args, **kwargs)
        self.append_feincms_inlines(inline_instances, request)
        return inline_instances

    def append_feincms_inlines(self, inline_instances, request):
        """
        Append generated FeinCMS content inlines to native django inlines.
        """
        for inline_class in self.get_feincms_inlines(self.model, request):
            inline_instance = inline_class(self.model, self.admin_site)
            inline_instances.append(inline_instance)

    def can_add_content(self, request, content_type):
        perm = '.'.join((
            content_type._meta.app_label,
            get_permission_codename('add', content_type._meta)))
        return request.user.has_perm(perm)

    def get_feincms_inlines(self, model, request):
        """ Generate genuine django inlines for registered content types. """
        model._needs_content_types()

        inlines = []
        for content_type in model._feincms_content_types:
            if not self.can_add_content(request, content_type):
                continue

            attrs = {
                '__module__': model.__module__,
                'model': content_type,
            }

            if hasattr(content_type, 'feincms_item_editor_inline'):
                inline = content_type.feincms_item_editor_inline
                attrs['form'] = inline.form

                if hasattr(content_type, 'feincms_item_editor_form'):
                    warnings.warn(
                        'feincms_item_editor_form on %s is ignored because '
                        'feincms_item_editor_inline is set too' % content_type,
                        RuntimeWarning)

            else:
                inline = FeinCMSInline
                attrs['form'] = getattr(
                    content_type, 'feincms_item_editor_form', inline.form)

            name = '%sFeinCMSInline' % content_type.__name__
            # TODO: We generate a new class every time. Is that really wanted?
            inline_class = type(str(name), (inline,), attrs)
            inlines.append(inline_class)
        return inlines

    def get_content_type_map(self, request):
        """ Prepare mapping of content types to their prettified names. """
        content_types = []
        for content_type in self.model._feincms_content_types:
            if self.model == content_type._feincms_content_class:
                content_name = content_type._meta.verbose_name
                content_types.append(
                    (content_name, content_type.__name__.lower()))
        return content_types

    def get_extra_context(self, request):
        """ Return extra context parameters for add/change views. """

        extra_context = {
            'model': self.model,
            'available_templates': getattr(
                self.model, '_feincms_templates', ()),
            'has_parent_attribute': hasattr(self.model, 'parent'),
            'content_types': self.get_content_type_map(request),
            'FEINCMS_JQUERY_NO_CONFLICT': settings.FEINCMS_JQUERY_NO_CONFLICT,
            'FEINCMS_CONTENT_FIELDSET_NAME': FEINCMS_CONTENT_FIELDSET_NAME,
        }

        for processor in self.model.feincms_item_editor_context_processors:
            extra_context.update(processor(request))

        return extra_context

    def add_view(self, request, **kwargs):
        if not self.has_add_permission(request):
            logger.warning(
                "Denied adding %s to \"%s\" (no add permission)",
                self.model,
                request.user
            )
            raise Http404

        context = {}

        # insert dummy object as 'original' so template code can grab defaults
        # for template, etc.
        context['original'] = self.model()

        # If there are errors in the form, we need to preserve the object's
        # template as it was set when the user attempted to save it, so that
        # the same regions appear on screen.
        if request.method == 'POST' and \
                hasattr(self.model, '_feincms_templates'):
            context['original'].template_key = request.POST['template_key']

        context.update(self.get_extra_context(request))
        context.update(kwargs.get('extra_context', {}))
        kwargs['extra_context'] = context
        return super(ItemEditor, self).add_view(request, **kwargs)

    def render_change_form(self, request, context, **kwargs):
        if kwargs.get('add'):
            if request.method == 'GET' and 'adminform' in context:
                if 'template_key' in context['adminform'].form.initial:
                    context['original'].template_key = (
                        context['adminform'].form.initial['template_key'])
                # ensure that initially-selected template in form is also
                # used to render the initial regions in the item editor
        return super(
            ItemEditor, self).render_change_form(request, context, **kwargs)

    def change_view(self, request, object_id, **kwargs):
        obj = self.get_object(request, unquote(object_id))
        if not self.has_change_permission(request, obj):
            logger.warning(
                "Denied editing %s to \"%s\" (no edit permission)",
                self.model,
                request.user
            )
            raise Http404

        context = {}
        context.update(self.get_extra_context(request))
        context.update(kwargs.get('extra_context', {}))
        kwargs['extra_context'] = context
        return super(ItemEditor, self).change_view(
            request, object_id, **kwargs)

    # The next two add support for sending a "saving done" signal as soon as
    # all relevant data have been saved (especially all foreign key relations)
    # This can be used to keep functionality dependend on item content happy.
    # NOTE: These two can (and probably should) be replaced by overriding
    # `save_related` as soon as we don't depend on Django<1.4 any more.
    def response_add(self, request, obj, *args, **kwargs):
        r = super(ItemEditor, self).response_add(request, obj, *args, **kwargs)
        itemeditor_post_save_related.send(
            sender=obj.__class__, instance=obj, created=True)
        return r

    def response_change(self, request, obj, *args, **kwargs):
        r = super(ItemEditor, self).response_change(
            request, obj, *args, **kwargs)
        itemeditor_post_save_related.send(
            sender=obj.__class__, instance=obj, created=False)
        return r

    @property
    def change_form_template(self):
        return self.get_template_list()

    def get_template_list(self):
        # retained for backwards-compatibility, change_form_template wraps it
        opts = self.model._meta
        return [
            'admin/feincms/%s/%s/item_editor.html' % (
                opts.app_label, opts.object_name.lower()),
            'admin/feincms/%s/item_editor.html' % opts.app_label,
            'admin/feincms/item_editor.html',
        ]

    def get_fieldsets(self, request, obj=None):
        """
        Insert FEINCMS_CONTENT_FIELDSET it not present.
        Is it reasonable to assume this should always be included?
        """

        fieldsets = copy.deepcopy(
            super(ItemEditor, self).get_fieldsets(request, obj))

        if FEINCMS_CONTENT_FIELDSET_NAME not in dict(fieldsets).keys():
            fieldsets.append(FEINCMS_CONTENT_FIELDSET)

        return fieldsets

    # These next are only used if later we use a subclass of this class
    # which also inherits from VersionAdmin.
    revision_form_template = "admin/feincms/revision_form.html"

    recover_form_template = "admin/feincms/recover_form.html"

    def render_revision_form(self, request, obj, version, context,
                             revert=False, recover=False):
        context.update(self.get_extra_context(request))
        return super(ItemEditor, self).render_revision_form(
            request, obj, version, context, revert, recover)
