# -*- coding: utf-8 -*-


"""
This is a menu system for django applications.

It creates 'menu' object which provides access
to functions for manipulation with html menu.

To set the menu up, follow steps below:


1) create project/menu.py
-------------------------
from kobo.django.menu import MenuItem, include
menu = (
    MenuItem("MenuItem-1", "/url/path/", absolute_url=True, menu=(
        MenuItem("MenuItem-1.1", "/url/path/1/", absolute_url=True),
        MenuItem("MenuItem-1.2", "/url/path/2/", absolute_url=True),
    )),
    MenuItem("MenuItem-2", "url_label", ("Developers",), ("app.change_data",)),
    include("project.app.menu"),
    MenuItem.include("project.another_app.menu"),
)

# In this example is MenuItem-1 and it's submenu submenu tree accessible for anybody.
# MenuItem-2 is only for users in group Developers with specific permission.
# Instead of specifying complete tree in one file, you can use include()
# command in similar way as it is used in urls.py (see third menu item).
# include() function is also a staticmethod of MenuItem class (see fourth menu item).

# can be specified only once in project-wide menu
css_active_class = "active_menu"


2) modify settings.py
---------------------
# set the menu root
ROOT_MENUCONF = "project.menu"

# add menu middleware
MIDDLEWARE_CLASSES = (
    ...
    "kobo.django.menu.middleware.MenuMiddleware",
    ...
)

# add a context processor to add 'menu' variable to each request
TEMPLATE_CONTEXT_PROCESSORS = (
    ...
    "kobo.django.menu.context_processors.menu_context_processor",
    ...
)


3) add menu to a html template
------------------------------
# The simplest use in template is just to display menu as {{ menu }}.
# Default format is nested ul list.
# Active menus/submenus will be tagged with css_active_class.

# If you need another approach, you can access directly to all menu items
# and display menu parts by yourself:

# display all menu items from main menu
{% for m in menu.items %}
  {{ m.as_a }}
{% endfor %}

# menu.levelX = active menu item on Xth level
# menu.levelX.items = all visible menu items of the ^^^ menu
{% for m in menu.level1.items %}
  {{ m.as_a }}
{% endfor %}
"""


from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse
from django.utils.safestring import mark_safe
from django.utils.encoding import smart_unicode


__all__ = (
    "include",
    "menu",
    "MenuItem",
)


def include(module):
    """helper function to load nested menus"""
    m = __import__(module, {}, {}, [""])
    return m.menu


class MenuItem(object):
    """basic menu item - every menuitem can have submenu collections of
    these. Only main menu is special instance of Menu class."""

    __slots__ = (
        "title",
        "_url",
        "_url_is_resolved",
        "absolute_url",
        "acl_groups",
        "acl_perms",
        "main_menu",
        "parent_menu",
        "submenu_list",
        "active",
        "depth",
        "alters_data",
    )


    # bind include() to the class
    include = staticmethod(include)


    def __init__(self, title, url, acl_groups=None, acl_perms=None, absolute_url=False, menu=None):
        self.title = smart_unicode(title)
        self._url = url
        self._url_is_resolved = absolute_url
        self.absolute_url = absolute_url
        self.acl_groups = acl_groups and set(acl_groups) or set()
        self.acl_perms = acl_perms and set(acl_perms) or set()
        self.main_menu = None
        self.parent_menu = None
        self.alters_data = False

        self.submenu_list = []
        for i in menu or []:
            if type(i) in (tuple, list):
                self.submenu_list.extend(i)
            else:
                self.submenu_list.append(i)

        self.active = False
        self.depth = 0


    def __repr__(self):
        return "<MenuItem>: %s (%s)" % (self.title, self.url)


    def __len__(self):
        return len(self.url)


    def __unicode__(self):
        result = ""
        if self.items:
            result = u"<ul>%s</ul>" % u"".join([unicode(i) for i in self.items])
        return mark_safe(u"<li>%s%s</li>" % (self.as_a(), result))

    @property
    def url(self):
        if not self._url_is_resolved:
            self._url = reverse(self._url)
        self._url_is_resolved = True
        return self._url

    def as_a(self):
        if not self.visible:
            return u""

        if self.active and self.main_menu.css_active_class:
            style = u" class='%s'" % self.main_menu.css_active_class
        else:
            style = u""

        if self.url:
            result = u"<a href='%s'%s>%s</a>" % (self.url, style, self.title)
        else:
            result = unicode(self.title)

        return mark_safe(result)


    @property
    def items(self):
        return [i for i in self.submenu_list if i.visible]


    @property
    def first_item(self):
        if self.items:
            return self.items[0]
        return None


    @property
    def last_item(self):
        if self.items:
            return self.items[-1]
        return None


    @property
    def inner_items(self):
        return self.items[1:-1]


    def setup_menu_tree(self, mainmenu_obj):
        if mainmenu_obj != self:
            self.main_menu = mainmenu_obj

        actual_depth = self.depth

        if self.submenu_list:
            mainmenu_obj.depth = max(mainmenu_obj.depth, actual_depth + 1)

        for i in self.submenu_list:
            i.parent_menu = self
            i.depth = actual_depth + 1
            i.setup_menu_tree(mainmenu_obj)
            mainmenu_obj.cached_menuitems.append(i)


    def set_active(self, active):
        self.active = active
        if self.parent_menu is not None:
            self.parent_menu.set_active(active)


    @property
    def visible(self):
        # return False if field should be displayed to user
        if self.main_menu.user.is_superuser:
            return True

        if self.acl_groups:
            if self.acl_groups.intersection(self.main_menu.acl_groups):
                return True
            return False

        if self.acl_perms:
            for perm in self.acl_perms:
                if perm not in self.main_menu.acl_perms:
                    self.main_menu.acl_perms[perm] = self.main_menu.user.has_perm(perm)
                if self.main_menu.acl_perms[perm]:
                    return True
            return False

        return True


class MainMenu(MenuItem):
    __slots__ = (
        "user",
        "path",
        "cached_menuitems",
        "css_active_class",
    )


    def __init__(self, menu, css_active_class=None):
        MenuItem.__init__(self, "ROOT_MENU", "", absolute_url=True, menu=menu)
        self.user = None
        self.path = ""
        self.cached_menuitems = []
        self.css_active_class = css_active_class or ""
        self.active = None # reference to active menu (overrides MenuItem behavior)

        # set main_menu references, compute menu depth
        self.setup_menu_tree(self)


    def __repr__(self):
        return "<MainMenu>: %s" % (self.path)


    def __unicode__(self):
        """Return menu as printable <ul> list."""
        return mark_safe(u"<ul>%s</ul>" % "".join([unicode(i) for i in self.items]))


    def __getattr__(self, name):
        # get specified submenu level in active menu
        if not name.startswith("level"):
            raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, name))

        try:
            level = int(name[5:])
        except ValueError:
            raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, name))

        if level not in range(1, self.depth + 1):
            raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, name))

        if not self.active:
            return None

        if self.active.depth < level:
            raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, name))

        menu = self.active
        while menu.depth > level:
            menu = menu.parent_menu
        return menu


    def setup(self, request):
        self.user = request.user
        self.path = request.get_full_path()
        self.acl_groups = set([i.name for i in request.user.groups.all().only("name")])
        self.acl_perms = {}
        self.find_active_menu()
        return self


    def find_active_menu(self):
        matches = [i for i in self.cached_menuitems if i.visible and i.url and self.path.startswith(i.url)]
        if not matches:
            return None

        # find the longest menu match
        matches.sort(key=len, reverse=True)
        found = matches[0]
        if self.active:
            # reset cached active path
            self.active.set_active(False)
        found.set_active(True)
        self.active = found
        return found


# load menu configuration from project
if not hasattr(settings, "ROOT_MENUCONF"):
    raise ImproperlyConfigured("'ROOT_MENUCONF' is needed in settings to run kobo.django.menu app.")
menu_module = __import__(settings.ROOT_MENUCONF, {}, {}, [""])
menu = MainMenu(menu_module.menu, getattr(menu_module, "css_active_class", ""))
