# -*- coding: utf-8 -*-
'''
Created on 30 Apr 2010

@author: trybik
'''
import os
import inspect
from warnings import warn

from django.template import Library, TemplateSyntaxError, Node, Context
from django.template.loader import select_template
from django.conf import settings
from django.utils.encoding import smart_str
from django.utils.translation import ugettext_lazy
from django.core.urlresolvers import reverse, resolve
from django.utils.safestring import mark_safe



################################################################################
# Django URL and views tools
################################################################################

def resolve_view(path, urlconf=None):
    '''
    Get the view function resolved by the URL path.
    '''
    view,_,_ = resolve(path, urlconf=urlconf)
    return getattr(view, 'view_func', view)

def view_app_label(view_func):
    '''
    Get the app label of the view function.

    Looks in the INSTALLED_APPS and if found matching app then trims the view's
    app name according to INSTALLED_APPS entry. Otherwise tries to trim the
    'views' part of the module name. If that was not successful simply return
    the view function module name.

    ATTENTION: to be sure that this function works as expected, if view is
               decorated then make sure that all of the decorators either
               - use functools.wraps (or django.utils.functional.wraps)
               or
               - explicitly copy  __module__ attribute of the decorated view.
    '''
    # Can't really use:
    #    return view_func.func_globals['__package__']
    # mainly due to the fact that update_wrapper does not copy func_globals.
    for app_label in settings.INSTALLED_APPS:
        if view_func.__module__.startswith(app_label) and\
           (len(view_func.__module__) == len(app_label) or\
            view_func.__module__[len(app_label)] == '.'):
            return app_label
    warn('Application label for %s.%s() view has not been found in INSTALLED_APPS.' % (view_func.__module__, view_func.__name__))
    modules = view_func.__module__.split('.')
    if len(modules)>1 and modules[-1] == 'views':
        return '.'.join(modules[:-1])
    return view_func.__module__



################################################################################
# Django template tools
################################################################################

def resolve_or_id(var, context):
    '''
    Resolve variable in context or return identity.
    '''
    if inspect.ismethod(getattr(var,'resolve', None)):
        return var.resolve(context)
    return var

def unquote(string):
    '''
    Removes quotes form string if found.
    '''
    if string[0] in ('"',"'"):
        if string[0] == string[-1]:
            string = string[1:-1]
        else:
            raise TemplateSyntaxError("Bad string quoting, found: %s." % string)
    return string

def context_pair(varname, value, context):
    '''
    Pair (tuple) with varname and it's value resolved form given context.

    Value can be:
    - a single value with string or any object that can be resolved
    in context (e.g. Variable or FilterExpression) variable
    or
    - a pair (value, filters), where value is as described above and filters
    is an iterable of callables to be called on the resolved value
    '''
    filters = ()
    if isinstance(value, tuple):
        value, filters = value
    value = resolve_or_id(value, context)
    for filter in filters:
        assert callable(filter), "Callable expected in filters for the context-resolved value."
        value = filter(value)
    return (smart_str(varname,'ascii'), value)

################################################################################
# TAGS
################################################################################


register = Library()



class SubIncludeNode(Node):
    '''
    Loads and renders template like include node but:
    [subdirectory]
    1. loads a template from the given templates subdirectory
       or, if not given, form the directory named as the app to which the view
       which renders the template that used SubIncludeNode belongs to
       or, if not found, directly from the TEMPLATE_DIRS;
       Note: loading from the app template folders relies on the convention of
       organizing templates in subdirectories of the TEMPLATE_DIRS.
    [subcontext]
    2. if any args or kwargs are given then only this subset of the context
       variables is passed to the rendered template (vide
       django.template.Library.inclusion_tag).
    '''

    def __init__(self, template_name, args=[], kwargs={}, subdir=None):
        '''
        If 'template_name' and 'subdir' as well as 'args' list elements and
        'kwargs' dictionary values implement method resolve() then this method
        is used with current template context passed as a first argument.

        For example, they can be instances of the FilterExpression class,
        created via compile_filter()  method of the parser in the template tag
        method or instances of the template Variable class. Note that they must
        be passed from the tag function as an instances of the Variable class if
        you want to use them as such.
        '''
        self.template_name = template_name
        self.subdir = subdir
        self.args = args
        self.kwargs = kwargs
        super(SubIncludeNode, self).__init__()

    def default_subdir(self, context):
        '''
        As the deafult subdir get the app label of the view according to the
        context's 'request.path' and the default 'urlconf'.

        Override this method if different strategy of calculating the default
        template subdirectory is required.
        '''
        assert context.has_key('request'),\
               "The subdirectory and subcontext template node requires the request context processor to be installed. Edit your TEMPLATE_CONTEXT_PROCESSORS setting to insert 'django.core.context_processors.request'."
        # assumes that the subdirectory is named after the last part of the
        # complex app label e.g. 'auth' for 'django.contrib.auth' app label
        return view_app_label(resolve_view(context['request'].path)).split('.')[-1]

    def render(self, context):
        '''
        The subcontext of the rendered template will include variables named
        according to 'self.kwargs' keys and variables named as the context
        variables in the 'args' list. If none given, then full context is passed
        to the template included from the 'self.subdir' subdirectory.
        '''
        subcontext_dict = dict([context_pair(arg, arg, context) for arg in self.args])
        subcontext_dict.update(
            dict([context_pair(k, v, context) for k, v in self.kwargs.items()]))
        template_context = Context(subcontext_dict)\
                                if bool(subcontext_dict) else\
                           context
        try:
            template_name = resolve_or_id(self.template_name, context)
            # TODO: as a template loader? how to obtain current_app?
            subdir = resolve_or_id(self.subdir, context) if self.subdir else\
                     self.default_subdir(context)
            # select template from the 'subdir' or TEMPLATE_DIRS if not found
            t = select_template([os.path.join(subdir, template_name), template_name])
            return t.render(template_context)
        except (TemplateSyntaxError, AssertionError):
            if settings.TEMPLATE_DEBUG:
                raise
            return ''
        except:
            return '' # Fail silently for invalid included templates.


def _parse_args_and_kwargs(parser, bits_iter, sep=","):
    '''
    Parses bits created form token "arg1,key1=val1, arg2 , ..." after spliting
    contents (separator is customizable). Returns list of args and dictionary of
    kwargs.
    '''
    args = []
    kwargs = {}
    for bit in bits_iter:
        for arg in bit.split(sep):
            if '=' in arg:
                k, v = arg.split('=', 1)
                k = k.strip()
                kwargs[k] = parser.compile_filter(v)
            elif arg:
                args.append(parser.compile_filter(arg))
    return args, kwargs


def sub_include(parser, token):
    '''
    Syntax::

        {% sub_include template_name from subdir with arg1,key1=val1, ... %}
        {% sub_include template_name with arg1,key1=val1, ... %}
        {% sub_include template_name from subdir %}
        {% sub_include template_name %}

    Example usage::

        {% sub_include "login_or_signup_message.html" from "projects" %}
        # which is equivalent to
        {% include "projects/login_or_signup_message.html" %}

        {% sub_include "login_or_signup_message.html" next_url='project_list' %}
        # which is equivalent to having defined an inclusion_tag that passes
        # 'next_url' argument to the "login_or_signup_message.html" template;
        # inclusion tag defined in the app which defines the view that renders
        # template that uses the above tag
    '''
    bits = token.split_contents()
    if len(bits) < 2:
        raise TemplateSyntaxError("'%s' tag takes at least one argument: '[template_name]'" % bits[0])
    template_name = parser.compile_filter(bits[1])
    subdir = None
    args = []
    kwargs = {}
    if len(bits) > 2:
        if bits[2] == 'from':
            if len(bits) < 4:
                raise TemplateSyntaxError("Expected subdirectory name following the 'from' argument in '%s' tag." % bits[0])
            subdir = parser.compile_filter(bits[3])
            i = 4
        else:
            i = 2
        if len(bits) > i:
            if bits[i] != 'with':
                raise TemplateSyntaxError("Expected argument 'with' in '%s' tag" % bits[0])
            if len(bits) < i+2:
                raise TemplateSyntaxError("Expected variables definitions 'arg1,key1=val1, ...' following the 'with' argument in '%s' tag." % bits[0])
            args, kwargs = _parse_args_and_kwargs(parser, iter(bits[(i+1):]))
    return SubIncludeNode(template_name, args, kwargs, subdir)
sub_include = register.tag(sub_include)





def info(parser, token):
    '''
    Syntax::

        {% info %}

    Example usage::

        {% info %}
    '''
    bits = token.split_contents()
    if len(bits) > 1:
        raise TemplateSyntaxError("'%s' tag takes no arguments." % bits[0])
    return SubIncludeNode('_info.html')
info = register.tag(info)



def show_item(parser, token):
    '''
    Show item; usually a model instance in a list.
    Syntax::

        {% show_item item %}

    Example usage::

        {% for user in users %}
            {% show_item user %}
        {% endfor %}
    '''
    bits = token.split_contents()
    if len(bits) < 2:
        raise TemplateSyntaxError("'%s' tag takes exactly one argument: [item]." % bits[0])
    item = parser.compile_filter(bits[1])
    return SubIncludeNode('_show_item.html', kwargs={'item': item})
show_item = register.tag(show_item)



def show_form(parser, token):
    '''
    Display form.
    Syntax::
        {% show_form form id %}
        {% show_form form id action_url=url_name classes=classes_str submit=submit_str template=template_name %}
        TODO: {% show_form form id with_inputs type=val, type=val %} problem: repeatable types

    where

        form - Form instance variable
        id - string with form id

    and optionally (in any order)

        action_url - target url (format as in the url tag); by default empty
                     string is set as an action attribute value
        classes - form CSS classes (space-separated - quoting needed in such
                  case)
        submit - submit input value; by default u"Submit"
        template - template name for customizations (e.g. via extending default,
                    '_show_form.html' template)

    Example usage::

        {% show_form form "address_form" "address_create" submit="Create &raquo;" classes="address popup" %}
    '''
    bits = token.split_contents()
    if len(bits) < 3:
        raise TemplateSyntaxError("'%s' tag takes at least two arguments: [form] [id]." % bits[0])
    form = parser.compile_filter(bits[1])
    id = parser.compile_filter(bits[2])
    template_name = '_show_form.html'
    defaults = {
        'action_url': None,
        'classes':'',
        'submit':ugettext_lazy(u"Submit"),
    }
    if len(bits) >= 3:
        __, kwargs = _parse_args_and_kwargs(parser, iter(bits[4:]))
        for key in ('action_url', 'classes'):
            if key in kwargs:
                defaults[key] = kwargs[key]
        if 'submit' in kwargs:
            defaults['submit'] = (kwargs['submit'], (mark_safe, ugettext_lazy))
        if 'template' in kwargs:
            template_name = kwargs.get('template')
    defaults.update(form=form, id=id)
    return SubIncludeNode(template_name, kwargs=defaults)
show_form = register.tag(show_form)



class LoginOrSignupNode(SubIncludeNode):
    def __init__(self, template_name='_login_or_signup.html',
                 login_view_name='acct_login', signup_view_name='acct_signup',
                 next_view_name=None):
        super(LoginOrSignupNode, self).__init__(template_name, kwargs={
                'login_url': reverse(login_view_name),
                'next_url': reverse(next_view_name) if next_view_name else None,
                'signup_url': reverse(signup_view_name)
              })

    def render(self, context):
        '''
        Get the current request path as the default 'next_url' value if not given.
        '''
        if not self.kwargs['next_url']:
            request = context.get('request', None)
            if request:
                self.kwargs['next_url'] = request.path
        return super(LoginOrSignupNode, self).render(context)

def login_or_signup(parser, token):
    '''
    Syntax::

        {% login_or_signup next_view_name %}
        {% login_or_signup %}

    Example usage::

        {% login_or_signup 'welcome_page' %}
    '''
    bits = token.split_contents()
    next_view_name = None
    if len(bits) > 1:
        next_view_name = unquote(bits[1])
    return LoginOrSignupNode(next_view_name=next_view_name)
#    # alternatively, simpler version without setting the default next_url:
#    return SubIncludeNode('_login_or_signup.html', kwargs={
#                'login_url': reverse('acct_login'),
#                'next_url': reverse(next_view_name) if next_view_name else None,
#                'signup_url': reverse('acct_signup')
#           })

login_or_signup = register.tag(login_or_signup)