"""Core models used by the CMS."""
from __future__ import unicode_literals

from django.apps import apps

from django.contrib.contenttypes.models import ContentType
from django.core import urlresolvers
from django.db import models, connection, transaction
from django.db.models import Q, F
from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.functional import cached_property
from django.utils import timezone

from cms import sitemaps, externals
from cms.models import PageBase, OnlineBaseManager, PageBaseSearchAdapter
from cms.models.managers import publication_manager


class PageManager(OnlineBaseManager):

    """Manager for Page objects."""

    def select_published(self, queryset, page_alias=None):
        """Selects only published pages."""
        queryset = super(PageManager, self).select_published(queryset)
        now = timezone.now()
        # Perform local filtering.
        queryset = queryset.filter(
            Q(publication_date=None) | Q(publication_date__lte=now))
        queryset = queryset.filter(Q(expiry_date=None) | Q(expiry_date__gt=now))
        # Perform parent ordering.
        quote_name = connection.ops.quote_name
        page_alias = page_alias or quote_name("pages_page")
        queryset = queryset.extra(
            where=("""
                NOT EXISTS (
                    SELECT *
                    FROM {pages_page} AS {ancestors}
                    WHERE
                        {ancestors}.{left} < {page_alias}.{left} AND
                        {ancestors}.{right} > {page_alias}.{right} AND (
                            {ancestors}.{is_online} = FALSE OR
                            {ancestors}.{publication_date} > %s OR
                            {ancestors}.{expiry_date} <= %s
                        )
                )
            """.format(
                page_alias=page_alias,
                **dict(
                    (name, quote_name(name))
                    for name in (
                        "pages_page",
                        "ancestors",
                        "left",
                        "right",
                        "is_online",
                        "publication_date",
                        "expiry_date",
                    )
                )
            ),),
            params=(now, now),
        )
        return queryset

    def get_homepage(self):
        """Returns the site homepage."""
        return self.prefetch_related("child_set__child_set").get(parent=None,
                                                                 is_content_object=False)


class Page(PageBase):

    """A page within the site."""

    objects = PageManager()

    # Hierarchy fields.

    parent = models.ForeignKey(
        "self",
        blank=True,
        null=True,
        related_name="child_set",
    )

    left = models.IntegerField(
        editable=False,
        db_index=True,
    )

    right = models.IntegerField(
        editable=False,
        db_index=True,
    )

    is_content_object = models.BooleanField(
        default=False
    )

    country_group = models.ForeignKey(
        'pages.CountryGroup',
        blank=True,
        null=True
    )

    owner = models.ForeignKey(
        "self",
        blank=True,
        null=True,
        related_name="owner_set",
    )

    @cached_property
    def children(self):
        """The child pages for this page."""
        children = []
        if self.right - self.left > 1:  # Optimization - don't fetch children
            #  we know aren't there!
            for child in self.child_set.filter(is_content_object=False):
                child.parent = self
                children.append(child)
        return children

    @property
    def navigation(self):
        """The sub-navigation of this page."""
        return [child for child in self.children if child.in_navigation]

    # Publication fields.

    publication_date = models.DateTimeField(
        blank=True,
        null=True,
        db_index=True,
        help_text="The date that this page will appear on the website.  Leave "
                  "this blank to immediately publish this page.",
    )

    expiry_date = models.DateTimeField(
        blank=True,
        null=True,
        db_index=True,
        help_text="The date that this page will be removed from the website.  "
                  "Leave this blank to never expire this page.",
    )

    # Navigation fields.

    in_navigation = models.BooleanField(
        "add to navigation",
        default=True,
        help_text="Uncheck this box to remove this content from the site "
                  "navigation.",
    )

    # Content fields.

    content_type = models.ForeignKey(
        ContentType,
        editable=False,
        help_text="The type of page content.",
    )

    cached_url = models.CharField(
        max_length=1000,
        null=True,
        blank=True,
    )

    requires_authentication = models.BooleanField(
        default=False,
        help_text="Visitors will need to be logged in to see this page"
    )

    hide_from_anonymous = models.BooleanField(
        'show to logged in only',
        default=False,
        help_text="Visitors that aren't logged in won't see this page in the navigation"
    )

    def auth_required(self):
        if self.requires_authentication or not self.parent:
            return self.requires_authentication
        return self.parent.auth_required()

    @cached_property
    def content(self):
        """The associated content model for this page."""
        content_cls = ContentType.objects.get_for_id(
            self.content_type_id).model_class()
        content = content_cls._default_manager.get(page=self)
        content.page = self
        return content

    def reverse(self, view_func, args=None, kwargs=None):
        """Performs a reverse URL lookup."""
        if args is None:
            args = ()
        if kwargs is None:
            kwargs = {}
        urlconf = ContentType.objects.get_for_id(
            self.content_type_id
        ).model_class().urlconf

        return self.get_absolute_url() + urlresolvers.reverse(
            view_func,
            args=args,
            kwargs=kwargs,
            urlconf=urlconf,
            prefix=""
        )

    # Standard model methods.

    def get_absolute_url(self, cached=False):
        """Generates the absolute url of the page."""
        if self.cached_url and cached:
            return self.cached_url

        if self.parent:
            url = self.parent.get_absolute_url() + self.slug + "/"
        else:
            url = urlresolvers.get_script_prefix()

        if url != self.cached_url:
            self.cached_url = url
            self.save()

        return url

    # Tree management.

    @property
    def _branch_width(self):
        return self.right - self.left + 1

    def _excise_branch(self):
        """Excises this whole branch from the tree."""
        branch_width = self._branch_width
        Page.objects.filter(left__gte=self.left).update(
            left=F("left") - branch_width,
        )
        Page.objects.filter(right__gte=self.left).update(
            right=F("right") - branch_width,
        )

    def _insert_branch(self):
        """Inserts this whole branch into the tree."""
        branch_width = self._branch_width
        Page.objects.filter(left__gte=self.left).update(
            left=F("left") + branch_width,
        )
        Page.objects.filter(right__gte=self.left).update(
            right=F("right") + branch_width,
        )

    @transaction.atomic
    def save(self, *args, **kwargs):
        """Saves the page."""

        if self.is_content_object is False:

            # Lock entire table.
            existing_pages = dict(
                (page["id"], page)
                for page
                in Page.objects.filter(
                    is_content_object=False
                ).select_for_update().values(
                    "id",
                    "parent_id",
                    "left",
                    "right"
                )
            )

            if self.left is None or self.right is None:
                # This page is being inserted.
                if existing_pages:
                    parent_right = existing_pages[self.parent_id]["right"]
                    # Set the model left and right.
                    self.left = parent_right
                    self.right = self.left + 1
                    # Update the whole tree structure.
                    self._insert_branch()
                else:
                    # This is the first page to be created, ever!
                    self.left = 1
                    self.right = 2
            else:
                # This is an update.
                if self.id not in existing_pages:
                    old_parent_id = -1
                else:
                    old_parent_id = existing_pages[self.id]["parent_id"]

                if old_parent_id != self.parent_id:
                    # The page has moved.
                    branch_width = self.right - self.left + 1
                    # Disconnect child branch.
                    if branch_width > 2:
                        Page.objects.filter(
                            left__gt=self.left,
                            right__lt=self.right
                        ).update(
                            left=F("left") * -1,
                            right=F("right") * -1,
                        )
                    self._excise_branch()
                    # Store old left and right values.
                    old_left = self.left
                    old_right = self.right
                    # Put self into the tree.
                    parent_right = existing_pages[self.parent_id]["right"]
                    if parent_right > self.right:
                        parent_right -= self._branch_width
                    self.left = parent_right
                    self.right = self.left + branch_width - 1
                    self._insert_branch()
                    # Put all children back into the tree.
                    if branch_width > 2:
                        child_offset = self.left - old_left
                        Page.objects.filter(
                            left__lt=-old_left,
                            right__gt=-old_right
                        ).update(
                            left=(F("left") - child_offset) * -1,
                            right=(F("right") - child_offset) * -1,
                        )

        # Now actually save it!
        super(Page, self).save(*args, **kwargs)

        self.get_absolute_url(False)

    @transaction.atomic
    def delete(self, *args, **kwargs):
        """Deletes the page."""
        list(Page.objects.all().select_for_update().values_list(
            "left",
            "right"
        ))  #
        # Lock entire
        #  table.
        super(Page, self).delete(*args, **kwargs)
        # Update the entire tree.
        self._excise_branch()

    def last_modified(self):
        if externals.reversion:
            import reversion
            versions = reversion.get_for_object(self)
            if versions.count() > 0:
                latest_version = versions[:1][0]
                return "{} by {}".format(
                    latest_version.revision.date_created.strftime("%Y-%m-%d %H:%M:%S"),
                    latest_version.revision.user
                )
        return "-"


    class Meta:
        unique_together = (("parent", "slug", "country_group"),)
        ordering = ("left",)


externals.historylinks("register", Page)


class PageSitemap(sitemaps.PageBaseSitemap):

    """Sitemap for page models."""

    model = Page

    def items(self):
        """Only lists items that are marked as indexable."""
        return filter_indexable_pages(super(PageSitemap, self).items())


sitemaps.register(Page, sitemap_cls=PageSitemap)


class PageSearchAdapter(PageBaseSearchAdapter):

    """Search adapter for Page models."""

    def get_content(self, obj):
        """Returns the search text for the page."""
        content_obj = obj.content

        return u" ".join((
            super(PageSearchAdapter, self).get_content(obj),
            self.prepare_content(u" ".join(
                force_text(self._resolve_field(content_obj, field_name))
                for field_name in (
                    field.name for field
                    in content_obj._meta.fields
                    if isinstance(field, (models.CharField, models.TextField))
                )
            ))
        ))

    def get_live_queryset(self):
        """Selects the live page queryset."""
        # HACK: Prevents a table name collision in the Django queryset manager.
        with publication_manager.select_published(False):
            qs = Page._base_manager.all()
        if publication_manager.select_published_active():
            qs = Page.objects.select_published(qs, page_alias="U0")
        # Filter out unindexable pages.
        qs = filter_indexable_pages(qs)
        # All done!
        return qs


externals.watson("register", Page, adapter_cls=PageSearchAdapter)


# Base content class.

def get_registered_content():
    """Returns a list of all registered content objects."""
    return [
        model for model in apps.get_models()
        if issubclass(model, ContentBase) and not model._meta.abstract
    ]


def filter_indexable_pages(queryset):
    """
    Filters the given queryset of pages to only contain ones that should be
    indexed by search engines.
    """
    return queryset.filter(
        robots_index=True,
        content_type__in=[
            ContentType.objects.get_for_model(content_model)
            for content_model
            in get_registered_content()
            if content_model.robots_index
        ]
    )


@python_2_unicode_compatible
class ContentBase(models.Model):

    """Base class for page content."""

    # This must be a 64 x 64 pixel image.
    icon = "pages/img/content.png"

    # The heading that the admin places this content under.
    classifier = "content"

    # The urlconf used to power this content's views.
    urlconf = "cms.apps.pages.urls"

    # A fieldset definition. If blank, one will be generated.
    fieldsets = None

    # Whether pages of this type should be included in search indexes. (Can
    # still be disabled on a per-page basis).
    robots_index = True

    page = models.OneToOneField(
        Page,
        primary_key=True,
        editable=False,
        related_name="+",
    )

    def __str__(self):
        """Returns a unicode representation."""
        return self.page.title

    class Meta:
        abstract = True


class Country(models.Model):
    name = models.CharField(
        max_length=256
    )

    code = models.CharField(
        max_length=16
    )

    group = models.ForeignKey(
        'pages.CountryGroup',
        blank=True,
        null=True
    )

    default = models.NullBooleanField(
        default=None,
        unique=True
    )

    def __str__(self):
        return self.name

    class Meta:
        ordering = ('name',)
        verbose_name_plural = 'countries'


class CountryGroup(models.Model):
    name = models.CharField(
        max_length=256
    )

    def __str__(self):
        return self.name

    class Meta:
        ordering = ('name',)
