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

from __future__ import absolute_import, unicode_literals

import logging
import sys
import traceback

import django
from django import template
from django.apps import apps
from django.conf import settings
from django.http import HttpRequest

from feincms import settings as feincms_settings
from feincms.module.page.extensions.navigation import PagePretender
from feincms.utils.templatetags import (
    SimpleAssignmentNodeWithVarAndArgs,
    do_simple_assignment_node_with_var_and_args_helper)


logger = logging.getLogger('feincms.templatetags.page')

register = template.Library()
assignment_tag = (
    register.simple_tag if django.VERSION >= (1, 9)
    else register.assignment_tag
)


def _get_page_model():
    return apps.get_model(
        *feincms_settings.FEINCMS_DEFAULT_PAGE_MODEL.split('.'))


# ------------------------------------------------------------------------
# TODO: Belongs in some utility module
def format_exception(e):
    top = traceback.extract_tb(sys.exc_info()[2])[-1]
    return "'%s' in %s line %d" % (e, top[0], top[1])


# ------------------------------------------------------------------------
@assignment_tag(takes_context=True)
def feincms_nav(context, feincms_page, level=1, depth=1, group=None):
    """
    Saves a list of pages into the given context variable.
    """

    page_class = _get_page_model()

    if not feincms_page:
        return []

    if isinstance(feincms_page, HttpRequest):
        try:
            feincms_page = page_class.objects.for_request(
                feincms_page, best_match=True)
        except page_class.DoesNotExist:
            return []

    mptt_opts = feincms_page._mptt_meta

    # mptt starts counting at zero
    mptt_level_range = [level - 1, level + depth - 1]

    queryset = feincms_page.__class__._default_manager.in_navigation().filter(
        **{
            '%s__gte' % mptt_opts.level_attr: mptt_level_range[0],
            '%s__lt' % mptt_opts.level_attr: mptt_level_range[1],
        }
    )

    page_level = getattr(feincms_page, mptt_opts.level_attr)

    # Used for subset filtering (level>1)
    parent = None

    if level > 1:
        # A subset of the pages is requested. Determine it depending
        # upon the passed page instance

        if level - 2 == page_level:
            # The requested pages start directly below the current page
            parent = feincms_page

        elif level - 2 < page_level:
            # The requested pages start somewhere higher up in the tree
            parent = feincms_page.get_ancestors()[level - 2]

        elif level - 1 > page_level:
            # The requested pages are grandchildren of the current page
            # (or even deeper in the tree). If we would continue processing,
            # this would result in pages from different subtrees being
            # returned directly adjacent to each other.
            queryset = page_class.objects.none()

        if parent:
            if getattr(parent, 'navigation_extension', None):
                # Special case for navigation extensions
                return list(parent.extended_navigation(
                    depth=depth, request=context.get('request')))

            # Apply descendant filter
            queryset &= parent.get_descendants()

    if depth > 1:
        # Filter out children with inactive parents
        # None (no parent) is always allowed
        parents = set([None])
        if parent:
            # Subset filtering; allow children of parent as well
            parents.add(parent.id)

        def _parentactive_filter(iterable):
            for elem in iterable:
                if elem.parent_id in parents:
                    yield elem
                parents.add(elem.id)

        queryset = _parentactive_filter(queryset)

    if group is not None:
        # navigationgroups extension support
        def _navigationgroup_filter(iterable):
            for elem in iterable:
                if getattr(elem, 'navigation_group', None) == group:
                    yield elem

        queryset = _navigationgroup_filter(queryset)

    if hasattr(feincms_page, 'navigation_extension'):
        # Filter out children of nodes which have a navigation extension
        def _navext_filter(iterable):
            current_navextension_node = None
            for elem in iterable:
                # Eliminate all subitems of last processed nav extension
                if current_navextension_node is not None and \
                   current_navextension_node.is_ancestor_of(elem):
                    continue

                yield elem
                if getattr(elem, 'navigation_extension', None):
                    current_navextension_node = elem
                    try:
                        for extended in elem.extended_navigation(
                                depth=depth, request=context.get('request')):
                            # Only return items from the extended navigation
                            # which are inside the requested level+depth
                            # values. The "-1" accounts for the differences in
                            # MPTT and navigation level counting
                            this_level = getattr(
                                extended, mptt_opts.level_attr, 0)
                            if this_level < level + depth - 1:
                                yield extended
                    except Exception as e:
                        logger.warn(
                            "feincms_nav caught exception in navigation"
                            " extension for page %d: %s",
                            current_navextension_node.id, format_exception(e))
                else:
                    current_navextension_node = None

        queryset = _navext_filter(queryset)

    # Return a list, not a generator so that it can be consumed
    # several times in a template.
    return list(queryset)


# ------------------------------------------------------------------------
class LanguageLinksNode(SimpleAssignmentNodeWithVarAndArgs):
    """
    ::

        {% feincms_languagelinks for feincms_page as links [args] %}

    This template tag needs the translations extension.

    Arguments can be any combination of:

    * all or existing: Return all languages or only those where a translation
      exists
    * excludecurrent: Excludes the item in the current language from the list
    * request=request: The current request object, only needed if you are using
      AppContents and need to append the "extra path"

    The default behavior is to return an entry for all languages including the
    current language.

    Example::

      {% feincms_languagelinks for feincms_page as links all,excludecurrent %}
      {% for key, name, link in links %}
          <a href="{% if link %}{{ link }}{% else %}/{{ key }}/{% endif %}">
            {% trans name %}</a>
      {% endfor %}
    """

    def what(self, page, args):
        only_existing = args.get('existing', False)
        exclude_current = args.get('excludecurrent', False)

        # Preserve the trailing path when switching languages if extra_path
        # exists (this is mostly the case when we are working inside an
        # ApplicationContent-managed page subtree)
        trailing_path = ''
        request = args.get('request', None)
        if request:
            # Trailing path without first slash
            trailing_path = request._feincms_extra_context.get(
                'extra_path', '')[1:]

        translations = dict(
            (t.language, t) for t in page.available_translations())
        translations[page.language] = page

        links = []
        for key, name in settings.LANGUAGES:
            if exclude_current and key == page.language:
                continue

            # hardcoded paths... bleh
            if key in translations:
                links.append((
                    key,
                    name,
                    translations[key].get_absolute_url() + trailing_path))
            elif not only_existing:
                links.append((key, name, None))

        return links


register.tag(
    'feincms_languagelinks',
    do_simple_assignment_node_with_var_and_args_helper(LanguageLinksNode))


# ------------------------------------------------------------------------
def _translate_page_into(page, language, default=None):
    """
    Return the translation for a given page
    """
    # Optimisation shortcut: No need to dive into translations if page already
    # what we want
    try:
        if page.language == language:
            return page

        if language is not None:
            translations = dict(
                (t.language, t) for t in page.available_translations())
            if language in translations:
                return translations[language]
    except AttributeError:
        pass

    if hasattr(default, '__call__'):
        return default(page=page)
    return default


# ------------------------------------------------------------------------
class TranslatedPageNode(SimpleAssignmentNodeWithVarAndArgs):
    """
    ::

        {% feincms_translatedpage for feincms_page as feincms_transpage
            language=en %}
        {% feincms_translatedpage for feincms_page as originalpage %}
        {% feincms_translatedpage for some_page as translatedpage
            language=feincms_page.language %}

    This template tag needs the translations extension.

    Returns the requested translation of the page if it exists. If the language
    argument is omitted the primary language will be returned (the first
    language specified in settings.LANGUAGES).

    Note: To distinguish between a bare language code and a variable we check
    whether settings LANGUAGES contains that code -- so naming a variable "en"
    will probably not do what is intended.
    """
    def what(self, page, args, default=None):
        language = args.get('language', None)

        if language is None:
            language = settings.LANGUAGES[0][0]
        else:
            if language not in (x[0] for x in settings.LANGUAGES):
                try:
                    language = template.Variable(language).resolve(
                        self.render_context)
                except template.VariableDoesNotExist:
                    language = settings.LANGUAGES[0][0]

        return _translate_page_into(page, language, default=default)


register.tag(
    'feincms_translatedpage',
    do_simple_assignment_node_with_var_and_args_helper(TranslatedPageNode))


# ------------------------------------------------------------------------
class TranslatedPageNodeOrBase(TranslatedPageNode):
    def what(self, page, args):
        return super(TranslatedPageNodeOrBase, self).what(
            page, args,
            default=getattr(page, 'get_original_translation', page))


register.tag(
    'feincms_translatedpage_or_base',
    do_simple_assignment_node_with_var_and_args_helper(
        TranslatedPageNodeOrBase))


# ------------------------------------------------------------------------
@register.filter
def feincms_translated_or_base(pages, language=None):
    if not hasattr(pages, '__iter__'):
        pages = [pages]
    for page in pages:
        yield _translate_page_into(
            page, language, default=page.get_original_translation)


# ------------------------------------------------------------------------
@register.inclusion_tag("breadcrumbs.html")
def feincms_breadcrumbs(page, include_self=True):
    """
    Generate a list of the page's ancestors suitable for use as breadcrumb
    navigation.

    By default, generates an unordered list with the id "breadcrumbs" -
    override breadcrumbs.html to change this.

    ::

        {% feincms_breadcrumbs feincms_page %}
    """

    ancs = page.get_ancestors()

    bc = [(anc.get_absolute_url(), anc.short_title()) for anc in ancs]

    if include_self:
        bc.append((None, page.short_title()))

    return {"trail": bc}


# ------------------------------------------------------------------------
@register.filter
def is_parent_of(page1, page2):
    """
    Determines whether a given page is the parent of another page

    Example::

        {% if page|is_parent_of:feincms_page %} ... {% endif %}
    """

    try:
        return page1.is_ancestor_of(page2)
    except (AttributeError, ValueError):
        return False


# ------------------------------------------------------------------------
@register.filter
def is_equal_or_parent_of(page1, page2):
    """
    Determines whether a given page is equal to or the parent of another page.
    This is especially handy when generating the navigation. The following
    example adds a CSS class ``current`` to the current main navigation entry::

        {% for page in navigation %}
            <a
            {% if page|is_equal_or_parent_of:feincms_page %}
                class="current"
            {% endif %}
            >{{ page.title }}</a>
        {% endfor %}
    """
    try:
        return page1.is_ancestor_of(page2, include_self=True)
    except (AttributeError, ValueError):
        return False


# ------------------------------------------------------------------------
def _is_sibling_of(page1, page2):
    return page1.parent_id == page2.parent_id


@register.filter
def is_sibling_of(page1, page2):
    """
    Determines whether a given page is a sibling of another page

    ::

        {% if page|is_sibling_of:feincms_page %} ... {% endif %}
    """

    try:
        return _is_sibling_of(page1, page2)
    except AttributeError:
        return False


# ------------------------------------------------------------------------
@register.filter
def siblings_along_path_to(page_list, page2):
    """
    Filters a list of pages so that only those remain that are either:

        * An ancestor of the current page
        * A sibling of an ancestor of the current page

    A typical use case is building a navigation menu with the active
    path to the current page expanded::

        {% feincms_nav feincms_page level=1 depth=3 as navitems %}
        {% with navitems|siblings_along_path_to:feincms_page as navtree %}
            ... whatever ...
        {% endwith %}

    """

    if page_list:
        try:
            # Try to avoid hitting the database: If the current page is
            # in_navigation, then all relevant pages are already in the
            # incoming list, no need to fetch ancestors or children.

            # NOTE: This assumes that the input list actually is complete (ie.
            # comes from feincms_nav). We'll cope with the fall-out of that
            # assumption when it happens...
            ancestors = [
                a_page for a_page in page_list
                if a_page.is_ancestor_of(page2, include_self=True)]
            top_level = min((a_page.level for a_page in page_list))

            if not ancestors:
                # Happens when we sit on a page outside the navigation tree so
                # fake an active root page to avoid a get_ancestors() db call
                # which would only give us a non-navigation root page anyway.
                page_class = _get_page_model()

                p = page_class(
                    title="dummy",
                    tree_id=-1,
                    parent_id=None,
                    in_navigation=False)
                ancestors = (p,)

            siblings = [
                a_page for a_page in page_list if (
                    a_page.parent_id == page2.id or
                    a_page.level == top_level or
                    any((_is_sibling_of(a_page, a) for a in ancestors))
                )
            ]

            return siblings
        except (AttributeError, ValueError) as e:
            logger.warn(
                "siblings_along_path_to caught exception: %s",
                format_exception(e))

    return ()


# ------------------------------------------------------------------------
@assignment_tag(takes_context=True)
def page_is_active(context, page, feincms_page=None, path=None):
    """
    Usage example::

        {% feincms_nav feincms_page level=1 as toplevel %}
        <ul>
        {% for page in toplevel %}
            {% page_is_active page as is_active %}
            <li {% if is_active %}class="active"{% endif %}>
                <a href="{{ page.get_navigation_url }}">{{ page.title }}</a>
            <li>
        {% endfor %}
        </ul>
    """
    if isinstance(page, PagePretender):
        if path is None:
            path = context['request'].path_info
        return path.startswith(page.get_absolute_url())

    else:
        if feincms_page is None:
            feincms_page = context['feincms_page']
        return page.is_ancestor_of(feincms_page, include_self=True)


# ------------------------------------------------------------------------
@register.simple_tag
def feincms_parentlink(of_, feincms_page, **kwargs):
    level = int(kwargs.get('level', 1))
    if feincms_page.level + 1 == level:
        return feincms_page.get_absolute_url()
    elif feincms_page.level + 1 < level:
        return '#'

    try:
        return feincms_page.get_ancestors()[level - 1].get_absolute_url()
    except IndexError:
        return '#'
