import collections
import json as jsonlib
import random
import re
from operator import attrgetter
from urlparse import urljoin

from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.forms import CheckboxInput
from django.template import defaultfilters
from django.utils import translation
from django.utils.encoding import smart_unicode

import caching.base as caching
import jinja2
import six
from babel.support import Format
from jingo import register, env
# Needed to make sure our own |f filter overrides jingo's one.
from jingo import helpers  # noqa
from tower import ugettext as _, strip_whitespace

import amo
from amo import urlresolvers, utils
from constants.licenses import PERSONA_LICENSES_IDS
from translations.helpers import truncate
from translations.query import order_by_translation
from versions.models import License


# Yanking filters from Django.
register.filter(defaultfilters.slugify)

# Registering some utils as filters:
urlparams = register.filter(utils.urlparams)
register.filter(utils.epoch)
register.filter(utils.isotime)
register.function(dict)
register.function(utils.randslice)


@register.filter
def link(item):
    html = """<a href="%s">%s</a>""" % (item.get_url_path(),
                                        jinja2.escape(item.name))
    return jinja2.Markup(html)


@register.filter
def xssafe(value):
    """
    Like |safe but for strings with interpolation.

    By using |xssafe you assert that you have written tests proving an
    XSS can't happen here.
    """
    return jinja2.Markup(value)


@register.filter
def babel_datetime(dt, format='medium'):
    return _get_format().datetime(dt, format=format) if dt else ''


@register.filter
def babel_date(date, format='medium'):
    return _get_format().date(date, format=format) if date else ''


@register.function
def locale_url(url):
    """Marketplace doesn't use locale prefixed URLs but we call this a lot."""
    # TODO: Remove uses of this in templates.
    return url


@register.inclusion_tag('includes/refinements.html')
@jinja2.contextfunction
def refinements(context, items, title, thing):
    d = dict(context.items())
    d.update(items=items, title=title, thing=thing)
    return d


@register.function
def url(viewname, *args, **kwargs):
    """Helper for Django's ``reverse`` in templates."""
    add_prefix = kwargs.pop('add_prefix', True)
    host = kwargs.pop('host', '')
    src = kwargs.pop('src', '')
    url = '%s%s' % (host, urlresolvers.reverse(viewname,
                                               args=args,
                                               kwargs=kwargs,
                                               add_prefix=add_prefix))
    if src:
        url = urlparams(url, src=src)
    return url


@register.function
def shared_url(viewname, addon, *args, **kwargs):
    """
    Helper specifically for addons or apps to get urls. Requires
    the viewname, addon (or app). It's assumed that we'll pass the
    slug into the args and we'll look up the right slug (addon or app)
    for you.

    Viewname should be a normal view eg: `addons.details` or `apps.details`.
    `addons.details` becomes `apps.details`, if we've passed an app, etc.

    A viewname such as `details` becomes `addons.details` or `apps.details`,
    depending on the add-on type.
    """
    slug = addon.app_slug if addon.is_webapp() else addon.slug
    prefix = 'apps' if addon.is_webapp() else 'addons'

    namespace, dot, latter = viewname.partition('.')

    # If `viewname` is prefixed with `addons.` but we're linking to a
    # webapp, the `viewname` magically gets prefixed with `apps.`.
    if namespace in ('addons', 'apps'):
        viewname = latter

    # Otherwise, we just slap the appropriate prefix in front of `viewname`.
    viewname = '.'.join([prefix, viewname])
    return url(viewname, *([slug] + list(args)), **kwargs)


@register.filter
def paginator(pager):
    return Paginator(pager).render()


@register.filter
def impala_paginator(pager):
    t = env.get_template('amo/impala/paginator.html')
    return jinja2.Markup(t.render({'pager': pager}))


@register.filter
def mobile_paginator(pager):
    t = env.get_template('amo/mobile/paginator.html')
    return jinja2.Markup(t.render({'pager': pager}))


@register.filter
def mobile_impala_paginator(pager):
    # Impala-style paginator that is easier to mobilefy.
    t = env.get_template('amo/mobile/impala_paginator.html')
    return jinja2.Markup(t.render({'pager': pager}))


@register.function
def is_mobile(app):
    return app == amo.MOBILE


@register.function
def sidebar(app):
    """Populates the sidebar with (categories, types)."""
    from addons.models import Category
    if app is None:
        return [], []

    # We muck with query to make order_by and extra_order_by play nice.
    q = Category.objects.filter(application=app.id, weight__gte=0,
                                type=amo.ADDON_EXTENSION)
    categories = order_by_translation(q, 'name')
    categories.query.extra_order_by.insert(0, 'weight')

    Type = collections.namedtuple('Type', 'id name url')
    base = urlresolvers.reverse('home')
    types = [Type(99, _('Collections'), base + 'collections/')]

    return categories, sorted(types, key=lambda x: x.name)


class Paginator(object):

    def __init__(self, pager):
        self.pager = pager

        self.max = 10
        self.span = (self.max - 1) / 2

        self.page = pager.number
        self.num_pages = pager.paginator.num_pages
        self.count = pager.paginator.count

        pager.page_range = self.range()
        pager.dotted_upper = self.num_pages not in pager.page_range
        pager.dotted_lower = 1 not in pager.page_range

    def range(self):
        """Return a list of page numbers to show in the paginator."""
        page, total, span = self.page, self.num_pages, self.span
        if total < self.max:
            lower, upper = 0, total
        elif page < span + 1:
            lower, upper = 0, span * 2
        elif page > total - span:
            lower, upper = total - span * 2, total
        else:
            lower, upper = page - span, page + span - 1
        return range(max(lower + 1, 1), min(total, upper) + 1)

    def render(self):
        c = {'pager': self.pager, 'num_pages': self.num_pages,
             'count': self.count}
        t = env.get_template('amo/paginator.html').render(c)
        return jinja2.Markup(t)


def _get_format():
    lang = translation.get_language()
    return Format(utils.get_locale_from_lang(lang))


@register.filter
def numberfmt(num, format=None):
    return _get_format().decimal(num, format)


@register.filter
def currencyfmt(num, currency):
    if num is None:
        return ''
    return _get_format().currency(num, currency)


def page_name(app=None):
    """Determine the correct page name for the given app (or no app)."""
    if app:
        return _(u'Add-ons for {0}').format(app.pretty)
    else:
        return _('Add-ons')


@register.function
@jinja2.contextfunction
def login_link(context):
    next = context['request'].path

    qs = context['request'].GET.urlencode()

    if qs:
        next += '?' + qs

    l = urlparams(urlresolvers.reverse('users.login'), to=next)
    return l


@register.function
@jinja2.contextfunction
def page_title(context, title, force_webapps=False):
    title = smart_unicode(title)
    if settings.APP_PREVIEW:
        base_title = _('Firefox Marketplace')
    elif context.get('WEBAPPS') or force_webapps:
        base_title = _('Firefox Marketplace')
    else:
        base_title = page_name(context['request'].APP)
    return u'%s :: %s' % (title, base_title)


@register.function
@jinja2.contextfunction
def breadcrumbs(context, items=list(), add_default=True, crumb_size=40):
    """
    show a list of breadcrumbs. If url is None, it won't be a link.
    Accepts: [(url, label)]
    """
    if add_default:
        app = context['request'].APP
        crumbs = [(urlresolvers.reverse('home'), page_name(app))]
    else:
        crumbs = []

    # add user-defined breadcrumbs
    if items:
        try:
            crumbs += items
        except TypeError:
            crumbs.append(items)

    crumbs = [(url, truncate(label, crumb_size)) for (url, label) in crumbs]
    c = {'breadcrumbs': crumbs}
    t = env.get_template('amo/breadcrumbs.html').render(c)
    return jinja2.Markup(t)


@register.function
@jinja2.contextfunction
def impala_breadcrumbs(context, items=list(), add_default=True, crumb_size=40):
    """
    show a list of breadcrumbs. If url is None, it won't be a link.
    Accepts: [(url, label)]
    """
    if add_default:
        if context.get('WEBAPPS'):
            base_title = _('Apps Marketplace')
        else:
            base_title = page_name(context['request'].APP)
        crumbs = [(urlresolvers.reverse('home'), base_title)]
    else:
        crumbs = []

    # add user-defined breadcrumbs
    if items:
        try:
            crumbs += items
        except TypeError:
            crumbs.append(items)

    crumbs = [(url, truncate(label, crumb_size)) for (url, label) in crumbs]
    c = {'breadcrumbs': crumbs, 'has_home': add_default}
    t = env.get_template('amo/impala/breadcrumbs.html').render(c)
    return jinja2.Markup(t)


@register.filter
def json(s):
    return jsonlib.dumps(s)


@register.filter
def absolutify(url, site=None):
    """Takes a URL and prepends the SITE_URL"""
    if url.startswith('http'):
        return url
    else:
        return urljoin(site or settings.SITE_URL, url)


@register.filter
def strip_controls(s):
    """
    Strips control characters from a string.
    """
    # Translation table of control characters.
    control_trans = dict((n, None) for n in xrange(32) if n not in [10, 13])
    rv = unicode(s).translate(control_trans)
    return jinja2.Markup(rv) if isinstance(s, jinja2.Markup) else rv


@register.filter
def strip_html(s, just_kidding=False):
    """Strips HTML.  Confirm lets us opt out easily."""
    if just_kidding:
        return s

    if not s:
        return ''
    else:
        s = re.sub(r'&lt;.*?&gt;', '', smart_unicode(s, errors='ignore'))
        return re.sub(r'<.*?>', '', s)


@register.filter
def external_url(url):
    """Bounce a URL off outgoing.mozilla.org."""
    return urlresolvers.get_outgoing_url(unicode(url))


@register.filter
def shuffle(sequence):
    """Shuffle a sequence."""
    random.shuffle(sequence)
    return sequence


@register.function
def license_link(license):
    """Link to a code license, including icon where applicable."""
    # If passed in an integer, try to look up the License.
    if isinstance(license, (long, int)):
        if license in PERSONA_LICENSES_IDS:
            # Grab built-in license.
            license = PERSONA_LICENSES_IDS[license]
        else:
            # Grab custom license.
            license = License.objects.filter(id=license)
            if not license.exists():
                return ''
            license = license[0]
    elif not license:
        return ''

    if not getattr(license, 'builtin', True):
        return _('Custom License')

    t = env.get_template('amo/license_link.html').render({'license': license})
    return jinja2.Markup(t)


@register.function
def field(field, label=None, **attrs):
    if label is not None:
        field.label = label
    # HTML from Django is already escaped.
    return jinja2.Markup(u'%s<p>%s%s</p>' %
                         (field.errors, field.label_tag(),
                          field.as_widget(attrs=attrs)))


@register.inclusion_tag('amo/category-arrow.html')
@jinja2.contextfunction
def category_arrow(context, key, prefix):
    d = dict(context.items())
    d.update(key=key, prefix=prefix)
    return d


@register.filter
def timesince(time):
    if not time:
        return u''
    ago = defaultfilters.timesince(time)
    # L10n: relative time in the past, like '4 days ago'
    return _(u'{0} ago').format(ago)


@register.inclusion_tag('amo/recaptcha.html')
@jinja2.contextfunction
def recaptcha(context, form):
    d = dict(context.items())
    d.update(form=form)
    return d


@register.filter
def is_choice_field(value):
    try:
        return isinstance(value.field.widget, CheckboxInput)
    except AttributeError:
        pass


@register.inclusion_tag('amo/mobile/sort_by.html')
def mobile_sort_by(base_url, options=None, selected=None, extra_sort_opts=None,
                   search_filter=None):
    if search_filter:
        selected = search_filter.field
        options = search_filter.opts
        if hasattr(search_filter, 'extras'):
            options += search_filter.extras
    if extra_sort_opts:
        options_dict = dict(options + extra_sort_opts)
    else:
        options_dict = dict(options)
    if selected in options_dict:
        current = options_dict[selected]
    else:
        selected, current = options[0]  # Default to the first option.
    return locals()


@register.function
@jinja2.contextfunction
def media(context, url, key='MEDIA_URL'):
    """Get a MEDIA_URL link with a cache buster querystring."""
    if 'BUILD_ID' in context:
        build = context['BUILD_ID']
    else:
        if url.endswith('.js'):
            build = context['BUILD_ID_JS']
        elif url.endswith('.css'):
            build = context['BUILD_ID_CSS']
        else:
            build = context['BUILD_ID_IMG']
    return urljoin(context[key], utils.urlparams(url, b=build))


@register.function
@jinja2.contextfunction
def static(context, url):
    """Get a STATIC_URL link with a cache buster querystring."""
    return media(context, url, 'STATIC_URL')


@register.function
@jinja2.evalcontextfunction
def attrs(ctx, *args, **kw):
    return jinja2.filters.do_xmlattr(ctx, dict(*args, **kw))


@register.function
@jinja2.contextfunction
def side_nav(context, addon_type, category=None):
    app = context['request'].APP.id
    cat = str(category.id) if category else 'all'
    return caching.cached(lambda: _side_nav(context, addon_type, category),
                          'side-nav-%s-%s-%s' % (app, addon_type, cat))


def _side_nav(context, addon_type, cat):
    # Prevent helpers generating circular imports.
    from addons.models import Category, AddonType
    request = context['request']
    qs = Category.objects.filter(weight__gte=0)
    if addon_type not in (amo.ADDON_PERSONA, amo.ADDON_WEBAPP):
        qs = qs.filter(application=request.APP.id)
    sort_key = attrgetter('weight', 'name')
    categories = sorted(qs.filter(type=addon_type), key=sort_key)
    if cat:
        base_url = cat.get_url_path()
    else:
        base_url = AddonType(addon_type).get_url_path()
    ctx = dict(request=request, base_url=base_url, categories=categories,
               addon_type=addon_type, amo=amo)
    return jinja2.Markup(env.get_template('amo/side_nav.html').render(ctx))


@register.function
@jinja2.contextfunction
def site_nav(context):
    app = context['request'].APP.id
    return caching.cached(lambda: _site_nav(context), 'site-nav-%s' % app)


def _site_nav(context):
    # Prevent helpers from generating circular imports.
    from addons.models import Category
    request = context['request']

    sorted_cats = lambda qs: sorted(qs, key=attrgetter('weight', 'name'))

    extensions = Category.objects.filter(application=request.APP.id,
        weight__gte=0, type=amo.ADDON_EXTENSION)

    ctx = dict(request=request, amo=amo,
               extensions=sorted_cats(extensions))
    return jinja2.Markup(env.get_template('amo/site_nav.html').render(ctx))


@register.filter
def premium_text(type):
    return amo.ADDON_PREMIUM_TYPES[type]


@register.function
def loc(s):
    """A noop function for strings that are not ready to be localized."""
    return strip_whitespace(s)


@register.function
@jinja2.contextfunction
def remora_url(context, url, lang=None, app=None, prefix=''):
    """Wrapper for urlresolvers.remora_url"""
    if lang is None:
        _lang = context['LANG']
        if _lang:
            lang = translation.to_locale(_lang).replace('_', '-')
    if app is None:
        try:
            app = context['APP'].short
        except (AttributeError, KeyError):
            pass
    return urlresolvers.remora_url(url=url, lang=lang, app=app, prefix=prefix)


@register.function
@jinja2.contextfunction
def hasOneToOne(context, obj, attr):
    try:
        getattr(obj, attr)
        return True
    except ObjectDoesNotExist:
        return False


@register.function
def no_results_amo():
    # This prints a "No results found" message. That's all. Carry on.
    t = env.get_template('amo/no_results.html').render()
    return jinja2.Markup(t)


@register.filter
def f(string, *args, **kwargs):
    """This overrides jingo.helpers.f to convert input to unicode if needed.

    This is needed because of
    https://github.com/jbalogh/jingo/pull/54#issuecomment-36728948

    """
    if not isinstance(string, six.text_type):
        string = six.text_type(string)
    return string.format(*args, **kwargs)
