"""
This file makes a way for random pieces of content to be assigned to any given
model instance.

For example, say you have a Page model that subclasses ContentArea. For a Page
with id 1, you create the following BaseItems, where the numbering is
indicative of order:
    1.  some text
    2.  an image with a heading and caption
    3.  some more text
    4.  a video thumbnail that, when clicked, opens up a video in a modal
    5.  even more text


video (or even an arbitrary, custom type of thing) to be assigned to a model
instance (such as a page, a blog post, a sidebar, etc).
"""

from django import forms
from django.conf import settings
from django.contrib.contenttypes.generic import (GenericForeignKey,
                                                 GenericRelation)
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.forms import ModelForm
from django.template.loader import render_to_string
from django.utils.translation import ugettext as _

from model_utils.managers import InheritanceManager

from .forms import BaseItemForm
from .utils import (get_app_settings,
                    get_model_from_string,
                    get_models_from_strings)


class BaseItemManager(InheritanceManager):
    def get_for_area(self, area):
        """
        Returns all items belonging to a given model instance, casted to their
        real types.

        Note that all queries (even the ones on the model) should go through
        this manager.
        """

        # Get the uncasted items tied to the given area.
        content_type = area.get_content_type()
        qs = BaseItem.objects.filter(content_area_ct=content_type,
                                     content_area_id=area.pk)
        # Order those items!
        qs = qs.order_by('ordering', 'pk')

        # Use InheritanceManager to automatically (and without extra queries)
        # downcast each item via the OneToOneField multi-table inhertiance
        # creates.
        return qs.select_subclasses()


class BaseItem(models.Model):
    """
    Offer functionality/fields that actual item subclasses need.
    """

    # Which model instance is this content part of?
    content_area_ct = models.ForeignKey(ContentType, related_name='+')
    content_area_id = models.PositiveIntegerField(db_index=True)
    content_area = GenericForeignKey(ct_field='content_area_ct',
                                     fk_field='content_area_id')

    # Relative to other items, where does this one belong?
    ordering = models.IntegerField(default=1, db_index=True)

    objects = BaseItemManager()

    class FlexibleContentInfo:
        description = _("This shouldn't ever be visible - it's just an "
                        "example of how you could specify a description in "
                        "an actual item.")
        type_slug = 'used-for-templates-and-the-like'

    class Meta:
        ordering = ['ordering', 'pk']

    def get_casted(self):
        """
        Ensure that this instance isn't merely a BaseItem, but casted to its
        true subclass type (per multi-table inheritance).
        """

        item = self
        # If this isn't a *subclass* of BaseItem, query for the subclass's item
        if type(item) not in BaseItem.__subclasses__():
            item = BaseItem.objects.all().select_subclasses().get(pk=item.pk)
        return item

    @classmethod
    def get_configured_types(cls):
        """
        Return a list of available item types.
        """

        # Do the settings define classes?
        configured_types = get_app_settings().get('ITEM_TYPES', None)

        # If there are types chosen, import them.
        if configured_types is not None:
            configured_types = get_models_from_strings(configured_types)
        # If not, grab the defaults.
        else:
            from .default_item_types.models import DEFAULT_TYPES
            configured_types = DEFAULT_TYPES

        return tuple(configured_types)

    def get_content_type(self):
        return ContentType.objects.get_for_model(self)

    def get_form_class(self):
        """
        You'll want to either specify a form class in your FlexibleContentInfo
        class, or override this and return the class straight away, like so:
            def get_form_class(self):
                from .forms import MyForm
                return MyForm
        """

        # Try to get an explicit form class first.
        form_class = getattr(self.FlexibleContentInfo, 'form_class', None)

        # If we didn't find one there, create a form on the fly.
        if form_class is None:
            name = 'AutoGenerated{}Form'.format(self.__class__.__name__)
            form_class_meta = type('Meta', (BaseItemForm.Meta,),
                                   {'model': self.__class__})
            form_class = type(name, (BaseItemForm,), {'Meta': form_class_meta})

        return form_class

    def get_form(self, *args, **kwargs):
        """
        Generate a form of this typeFor a given model instance, generate the
        form. Optionally take the data and file arguments that standard forms
        take, and pass those along.
        """

        # Manually set the kwargs to bind the form to this instance.
        if 'instance' not in kwargs:
            kwargs.update({'instance': self})

        # Generate the form using the args and kwargs we were given.
        return self.get_form_class()(*args, **kwargs)

    def get_form_template(self):
        from .admin import get_form_prefix
        return self.get_form(prefix=get_form_prefix())

    def get_rendered_content(self):
        """
        Use the template to render this instance's data, and cache it on the
        instance.
        """
        if getattr(self, '_rendered_content', None) is None:
            self._rendered_content = render_to_string(self.get_template_name(),
                                                      {'item': self})
        return self._rendered_content

    def get_template_name(self):
        return 'flexible-content/{}.html'.format(self.get_type_slug())

    def get_type_description(self):
        return getattr(self.FlexibleContentInfo, 'description', '')

    def get_type_name(self):
        name = getattr(self.FlexibleContentInfo, 'name', '')
        if not name:
            name = self._meta.verbose_name
        return name

    def get_type_slug(self):
        # Try to get an explicit slug first.
        slug = getattr(self.FlexibleContentInfo, 'type_slug', None)

        # If we didn't find one, guess it from the model name.
        if slug is None:
            slug = self.__class__.__name__.lower()

        return slug


class ContentArea(models.Model):
    rendered_content = None

    class Meta:
        abstract = True

    def delete(self, *args, **kwargs):
        """
        Ensure we delete content items too.
        """

        # What content items belong to this object?
        dead_items = list(self.items)

        # Delete the area.
        super(ContentArea, self).delete(*args, **kwargs)

        # Delete the area's old items, just in case.
        [i.delete() for i in dead_items]

    @property
    def items(self):
        return BaseItem.objects.get_for_area(self)

    def get_content_type(self):
        return ContentType.objects.get_for_model(self)

    def get_rendered_content(self):
        """
        Returns all content items rendered into a single string (likely HTML).
        """
        if self.rendered_content is None:
            rendered_items = [i.get_rendered_content() for i in self.items]
            self.rendered_content = '\n\n'.join(rendered_items)
        return self.rendered_content


class TemporaryArea(ContentArea):
    """
    Holds new/updated content items before we save the real area.

    This is necessary because we're overriding Django's views and it creates
    any messages and forms the redirect (or error response). We need to save
    the items before that happens, so it doesn't do all that junk when our
    content items are about to screw things up and change the proper response.
    """

    created = models.DateTimeField(auto_now_add=True)

    def migrate_items_to(self, real_area):
        """
        Take the items for this area and move them to the real area, then
        delete yourself!
        """

        # Figure out the two bits of data for the new area.
        real_area_data = {
            'content_area_ct': real_area.get_content_type().pk,
            'content_area_id': real_area.pk,
        }

        # Update the items for this temporary area.
        self.items.update(**real_area_data)

        # Delete the temporary area!
        self.delete()

