# -*- coding: utf-8 -*-
import re
import os
import urllib
import datetime
import collections

import jinja2
import logging


class Jinja:
    environment = jinja2.Environment(
        loader=jinja2.FileSystemLoader(os.path.dirname(__file__)+'/templates'),
        extensions=['jinja2.ext.autoescape']
    )

    @staticmethod
    def render(caller, template_file, values=None):
        values = Jinja._get_constant_values(caller, values)

        # add custom filters, if we haven't already
        for filter_name in ['urlquote', 'urlquote_name', 'short_time']:
            if not filter_name in Jinja.environment.filters:
                Jinja.environment.filters[filter_name] = getattr(Jinja, filter_name)

        # decode strings to unicode as required by jinja2
        Jinja.decode_strings(values)

        if not template_file.endswith('.html'):
            template_file += '.html'

        template = Jinja.environment.get_template(template_file)
        if not values:
            return template.render()
        return template.render(values)

    @staticmethod
    def decode_strings(var):
        '''Recursively looks for strings in variable and decodes them to unicode.'''
        if isinstance(var, dict) or isinstance(var, collections.OrderedDict):
            # recurse for all items
            for key in var:
                var[key] = Jinja.decode_strings(var[key])
        elif isinstance(var, list):
            # recurse for all items
            for i in range(len(var)):
                var[i] = Jinja.decode_strings(var[i])
        elif isinstance(var, tuple):
            # turn into list and recurse so we can change items
            var = tuple(Jinja.decode_strings(list(var)))
        elif isinstance(var, str):
            var = var.decode('utf-8', 'ignore')
        return var

    _stylesheets = ['anonymous1', 'anonymous2', 'anonymous3']
    @staticmethod
    def _get_constant_values(caller, values):
        '''Appends values derived from information from the request object (like
        cookies, user-agent and so on).'''
        if not values:
            values = {}

        # previously set stylesheet
        if caller.request.cookies and 'stylesheet' in caller.request.cookies:
            requested_stylesheet = caller.request.cookies['stylesheet']
            if requested_stylesheet in Jinja._stylesheets:
                values['stylesheet'] = requested_stylesheet

        return values

    @staticmethod
    def fix_urllib_str(string):
        '''Converts a string with unicode characters into... a string with
        unicode characters.

        urllib breaks if you pass it unicode, or a str containing unicode
        characters, but it magically works if you decode to unicode and reencode
        to str.
        '''
        if isinstance(string, str):
            string = unicode(string.decode('utf-8', 'ignore')) 
        return string.encode('utf-8', 'ignore')

    @staticmethod
    def urlquote(string):
        return urllib.quote(Jinja.fix_urllib_str(string))

    @staticmethod
    def urlquote_name(name):
        '''Quote a single video name by replace its slash.'''
        return name.replace('/', '%2F')

    @staticmethod
    def short_time(date_str):
        date = datetime.datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S.%f')
        return date.strftime('%d %b %Y %H:%M')


class CustomExpanders:
    @staticmethod
    def _gc_expander(match):
        '''Expands links for gc/'''
        result = []
        # videos likely to be big, se we should probably try from bigger to smaller
        prefixes = ['giant', 'fat', 'zippy']
        # if a prefix was specified, make sure it comes first
        if 'prefix' in match.groupdict():
            prefix = match.group('prefix')
            if prefix:
                # if it's a known prefix, remove it first
                if prefix in prefixes:
                    prefixes.remove(prefix)
                prefixes = [prefix] + prefixes

        for prefix in prefixes:
            for extension in ['.webm', '.mp4']:
                base_link = Shortener.expanders['gc']['link'].format(
                    prefix=prefix, base=match.group('base'))
                result.append(base_link+extension)
        return result


class Shortener:
    shorteners = [
            # groups must be named in exactly the same way as in the host's
            # expander
            (re.compile(r'^https?://a\.pomf\.se/(?P<base>[0-9A-Za-z/]+)(?:\.(?:webm|mp4|ogg|mp3))$'), 'pf'),
            (re.compile(r'^https?://(?:2\.)?kastden\.org/loopvid/(?P<base>[0-9A-Za-z/._-]+)(?:\.(?:webm|mp4|ogg|mp3))$'), 'kd'),
            (re.compile(r'^https?://lv\.kastden\.org/(?P<base>[0-9A-Za-z/._-]+)(?:\.(?:webm|mp4|ogg|mp3))$'), 'lv'),
            (re.compile(r'^https?://(?:docs|drive)\.google\.com/file/d/(?P<base>[0-9A-Za-z_-]+)'), 'gd'),
            (re.compile(r'^https?://(?:docs|drive)\.google\.com/uc\?export=download&id=(?P<base>[0-9A-Za-z_-]+)'), 'gd'),
            (re.compile(r'^https?://googledrive\.com/host/(?P<base>[0-9A-Za-z_-]+/[0-9A-Za-z/._-]+)(?:\.(?:webm|mp4|ogg|mp3))'), 'gh'),
            (re.compile(r'^https?://dl\.dropboxusercontent\.com/u/(?P<base>\d+/[0-9A-Za-z/._-]+)(?:\.(?:webm|mp4|ogg|mp3))'), 'db'),
            (re.compile(r'^https?://dl\.dropboxusercontent\.com/(?P<base>[0-9A-Za-z/._-]+)(?:\.(?:webm|mp4|ogg|mp3))'), 'dx'),
            (re.compile(r'^https?://i\.4cdn\.org/(?P<board>[^/]+)/(?P<base>\d+)(?:\.(?:webm|mp4|ogg|mp3))'), 'fc'),
            (re.compile(r'^https?://copy\.com/(?P<base>[0-9A-Za-z/._-]+)(?:\.(?:webm|mp4|ogg|mp3))'), 'cp'),
            (re.compile(r'^https?://[^.]+\.gfycat\.com/(?P<base>[A-Za-z]+)\.(?:webm|mp4|ogg|mp3)'), 'gc'),
            (re.compile(r'^https?://webmup\.com/(?P<base>[0-9A-Za-z._-]+)/vid.webm'), 'wu'),
            (re.compile(r'^https?://i\.imgur\.com/(?P<base>[0-9A-Za-z._-]+)(?:\.(?:webm|mp4|ogg|mp3))'), 'ig'),
            (re.compile(r'^https?://a\.pomf\.cat/(?P<base>[0-9A-Za-z/._-]+)(?:\.(?:webm|mp4|ogg|mp3))$'), 'pc'),
            (re.compile(r'^https?://b\.1339\.cf/(?P<base>[0-9A-Za-z/._-]+)(?:\.(?:webm|mp4|ogg|mp3))$'), '1c'),
            (re.compile(r'^https?://u\.pomf\.is/(?P<base>[0-9A-Za-z/._-]+)(?:\.(?:webm|mp4|ogg|mp3))$'), 'pi'),
            (re.compile(r'^https?://u\.nya\.is/(?P<base>[0-9A-Za-z/._-]+)(?:\.(?:webm|mp4|ogg|mp3))$'), 'ni'),
            (re.compile(r'^https?://webm\.land/media/(?P<base>[0-9A-Za-z/._-]+)(?:\.(?:webm|mp4|ogg|mp3))$'), 'wl'),
            (re.compile(r'^https?://kordy\.kastden\.org/loopvid/(?P<base>[0-9A-Za-z/._-]+)(?:\.(?:webm|mp4|ogg|mp3))$'), 'ko'),
            (re.compile(r'^https?://media\.8ch\.net/file_store/(?P<base>[0-9a-f]{64})(?:\.(?:webm|mp4|ogg|mp3))'), 'ic'),

            (re.compile(r'^https?://kastden\.org/_loopvid_media/pf/(?P<base>[0-9A-Za-z/]+)(?:\.(?:webm|mp4|ogg|mp3))$'), 'pf'),
            (re.compile(r'^https?://kastden\.org/_loopvid_media/gc/(?P<base>[0-9A-Za-z/]+)(?:\.(?:webm|mp4|ogg|mp3))$'), 'gc'),
            (re.compile(r'^https?://kastden\.org/_loopvid_media/nn/(?P<base>[0-9A-Za-z/._-]+)(?:\.(?:webm|mp4|ogg|mp3))'), 'nn'),
            (re.compile(r'^https?://kastden\.org/_loopvid_media/ky/(?P<base>[0-9A-Za-z/._-]+)(?:\.(?:webm|mp4|ogg|mp3))'), 'ky'),
            (re.compile(r'^https?://kastden\.org/_loopvid_media/mf/(?P<base>[0-9A-Za-z/._-]+)(?:\.(?:webm|mp4|ogg|mp3))'), 'mf'),
            (re.compile(r'^https?://kastden\.org/_loopvid_media/m2/(?P<base>[0-9A-Za-z/._-]+)(?:\.(?:webm|mp4|ogg|mp3))'), 'm2'),
            (re.compile(r'^https?://kastden\.org/_loopvid_media/pc/(?P<base>[0-9A-Za-z/]+)(?:\.(?:webm|mp4|ogg|mp3))$'), 'pc'),
            (re.compile(r'^https?://kastden\.org/_loopvid_media/pi/(?P<base>[0-9A-Za-z/]+)(?:\.(?:webm|mp4|ogg|mp3))$'), 'pi'),
            (re.compile(r'^https?://kastden\.org/_loopvid_media/ni/(?P<base>[0-9A-Za-z/]+)(?:\.(?:webm|mp4|ogg|mp3))$'), 'ni'),
            (re.compile(r'^https?://kastden\.org/_loopvid_media/mm/(?P<base>[0-9A-Za-z/]+)(?:\.(?:webm|mp4|ogg|mp3))$'), 'mm'),


            # deprecated old style shorteners:
            (re.compile(r'^(?P<base>[0-9A-Za-z_.-]+)(?:\.webm|\.mp4)$'), 'lv'),
            # minimum of 20 chars chosen arbitrarily; the only length I've seen is 28 chars,
            # but since it's not documented anywhere, it's safer to assume it is variable
            # and might increase with time
            (re.compile(r'^(?P<base>[0-9A-Za-z_-]{20,})$'), 'gd'),
            ]

    expanders = {
            'pf': {
                'regex': re.compile('(?P<base>[0-9A-Za-z./]+)'),
                'link': 'https://kastden.org/_loopvid_media/pf/{base}',
                'alt_link': 'http://a.pomf.se/{base}',
                'has_extension': True,
                },
            'kd': {
                'regex': re.compile('(?P<base>[0-9A-Za-z/._-]+)'),
                'link': 'https://kastden.org/loopvid/{base}',
                'has_extension': True,
                },
            'lv': {
                'regex': re.compile('(?P<base>[0-9A-Za-z/._-]+)'),
                #'link': 'http://loopvid.mooo.com/videos/{base}',
                'link': 'https://lv.kastden.org/{base}',
                'has_extension': True,
                },
            'gd': {
                'regex': re.compile('(?P<base>[0-9A-Za-z_-]+)'),
                'link': 'https://docs.google.com/uc?export=download&id={base}',
                'has_extension': False,
                },
            'gh': {
                'regex': re.compile('(?P<base>[0-9A-Za-z/._-]+)'),
                'link': 'https://googledrive.com/host/{base}',
                'has_extension': True,
                },
            'db': {
                'regex': re.compile('(?P<base>[0-9A-Za-z/._-]+)'),
                'link': 'https://dl.dropboxusercontent.com/u/{base}',
                'has_extension': True,
                },
            'dx': {
                'regex': re.compile('(?P<base>[0-9A-Za-z/._-]+)'),
                'link': 'https://dl.dropboxusercontent.com/{base}',
                'has_extension': True,
                },
            'fc': {
                'regex': re.compile('(?P<board>[^/]+)/(?P<base>[0-9]+)'),
                'link': 'https://i.4cdn.org/{board}/{base}.webm',
                'has_extension': False,
                },
            'nn': {
                'regex': re.compile('(?P<base>[0-9A-Za-z/._-]+)'),
                'link': 'https://kastden.org/_loopvid_media/nn/{base}',
                'has_extension': True,
                },
            'cp': {
                'regex': re.compile('(?P<base>[0-9A-Za-z/._-]+)'),
                'link': 'https://copy.com/{base}',
                'has_extension': True,
                },
            'gc': {
                'regex': re.compile('(?:(?P<prefix>[^/.]+)/)?(?P<base>[0-9A-Za-z/._-]+)'),
                'link': 'https://{prefix}.gfycat.com/{base}',
                'has_extension': True,
                'custom_expander': CustomExpanders._gc_expander,
                },
            'wu': {
                'regex': re.compile('(?P<base>[0-9A-Za-z._-]+)'),
                'link': 'http://webmup.com/{base}/vid.webm',
                'has_extension': False,
                },
            'ig': {
                'regex': re.compile('(?P<base>[0-9A-Za-z._-]+)'),
                'link': 'https://i.imgur.com/{base}',
                'has_extension': True,
                },
            'ky': {
                'regex': re.compile('(?P<base>[0-9A-Za-z/._-]+)'),
                'link': 'https://kastden.org/_loopvid_media/ky/{base}',
                'has_extension': True,
                },
            'mf': {
                'regex': re.compile('(?P<base>[0-9A-Za-z/._-]+)'),
                'link': 'https://kastden.org/_loopvid_media/mf/{base}',
                'has_extension': True,
                },
            'm2': {
                'regex': re.compile('(?P<base>[0-9A-Za-z/._-]+)'),
                'link': 'https://kastden.org/_loopvid_media/m2/{base}',
                'has_extension': True,
                },
            'pc': {
                'regex': re.compile('(?P<base>[0-9A-Za-z/._-]+)'),
                'link': 'https://kastden.org/_loopvid_media/pc/{base}',
                'has_extension': True,
                },
            '1c': {
                'regex': re.compile('(?P<base>[0-9A-Za-z/._-]+)'),
                'link': 'http://b.1339.cf/{base}',
                'has_extension': True,
                },
            'pi': {
                'regex': re.compile('(?P<base>[0-9A-Za-z/._-]+)'),
                'link': 'https://kastden.org/_loopvid_media/pi/{base}',
                'has_extension': True,
                },
            'ni': {
                'regex': re.compile('(?P<base>[0-9A-Za-z/._-]+)'),
                'link': 'https://kastden.org/_loopvid_media/ni/{base}',
                'has_extension': True,
                },
            'wl': {
                'regex': re.compile('(?P<base>[0-9A-Za-z/._-]+)'),
                'link': 'http://webm.land/media/{base}',
                'has_extension': True,
                },
            'ko': {
                'regex': re.compile('(?P<base>[0-9A-Za-z/._-]+)'),
                'link': 'https://kordy.kastden.org/loopvid/{base}',
                'has_extension': True,
                },
            'mm': {
                'regex': re.compile('(?P<base>[0-9A-Za-z/._-]+)'),
                'link': 'https://kastden.org/_loopvid_media/mm/{base}',
                'has_extension': True,
                },
            'ic': {
                'regex': re.compile('(?P<base>[0-9a-f]{64})'),
                'link': 'https://media.8ch.net/file_store/{base}',
                'has_extension': True,
                },
            }
    # note that a shortened list cannot end in a slash (actually, no shortened
    # link should ever end in a slash, but most of the time people use a single
    # link, so it's easier to just check here and not in every expander)
    expander_regex = re.compile(r'^/?(?P<acronym>'+ '|'.join(expanders.keys()) + ')/(?P<list>[0-9A-Za-z/._,-]*[0-9A-Za-z._-])$')

    shortened_list_separator = ','
    # bare bones regex, just to stop obvious mistakes, like '/blogg' or
    # '/pavicon.ico'
    full_url_regex = re.compile(r'^[a-z]+://[^.]+\.[^.]+.+/.+')
    old_shortened_regex =  re.compile(r'^([0-9A-Za-z]{20,}|[0-9A-Za-z_.-]+\.(webm|mp4))$')

    shortened_list_delimiter = '&'
    # we should only expand if regex matches because non-shortened links might
    # contain the delimiter
    shortened_list_delimiter_regex = re.compile(r'^/?[a-z]{2}/[^&]+(&/?[a-z]{2}/[^&]+)+$')

    mime_types = {
            '.mp4':  'video/mp4',
            '.webm': 'video/webm',
            '.ogg':  'audio/ogg',
            '.mp3':  'audio/mpeg',
            '.wav':  'audio/wave',
            '.m4a':  'audio/aac',
            }

    @staticmethod
    def shorten_url(url):
        # try matching against each shortener
        for regex, acronym in Shortener.shorteners:
            match = regex.match(url)
            if match:
                groups = match.groups()
                short_url = '{acronym}/{groups}'.format(acronym=acronym, groups='/'.join(groups))
                return short_url
        # no shortener matched, return original
        return url

    @staticmethod
    def expand_extension(acronym, base_link, is_audio=False):
        '''Appends extensions if necessary. Returns a list of links.'''
        result = []
        # append extensions if necessary
        # shorten_url() will remove extensions, but old links might contain
        # them, so we have to make sure we don't duplicate them
        if Shortener.expanders[acronym]['has_extension']:
            if not is_audio:
                if not (base_link.endswith('.webm') or base_link.endswith('.mp4')):
                    result += [base_link+'.webm', base_link+'.mp4']
                else:
                    result.append(base_link)
            else: # is audio
                if not (base_link.endswith('.ogg') or base_link.endswith('.mp3')):
                    result += [base_link+'.ogg', base_link+'.mp3']
                else:
                    result.append(base_link)
        else:
            result.append(base_link)

        return result

    @staticmethod
    def expand_url(url, is_audio=False):
        result = []

        # deprecated old style shorteners, should be passed through shorten_url() so
        # we can expand the correct shortened url
        if Shortener.old_shortened_regex.match(url):
            url = Shortener.shorten_url(url)

        match = Shortener.expander_regex.match(url)
        # shortened url, expand
        if match:
            acronym = match.group('acronym')
            shortened_urls = match.group('list').split(Shortener.shortened_list_separator)

            for shortened_url in shortened_urls:
                # tolerate /xy/file
                if shortened_url.startswith('/'):
                    shortened_url = shortened_url[1:]

                # tolerate xy/xy/file
                if shortened_url.startswith(acronym+'/'):
                    shortened_url = shortened_url[3:]

                # expander_regex allows for empty elements in lists,
                #(e.g. ab/cde,,fgh), so we have to discard them here
                if not shortened_url:
                    continue

                expander_match = Shortener.expanders[acronym]['regex'].match(shortened_url)
                if not expander_match:
                    logging.error('expander matched whole list, but not single url: {}, {}'.format(acronym, shortened_urls))
                    continue

                if 'custom_expander' in Shortener.expanders[acronym]:
                    result = Shortener.expanders[acronym]['custom_expander'](expander_match)
                else:
                    groups = expander_match.groupdict()
                    base_link = Shortener.expanders[acronym]['link'].format(**groups)
                    result += Shortener.expand_extension(acronym, base_link, is_audio=is_audio)

                    if 'alt_link' in Shortener.expanders[acronym]:
                        base_link = Shortener.expanders[acronym]['alt_link'].format(**groups)
                        result += Shortener.expand_extension(acronym, base_link, is_audio=is_audio)

        # can't be expanded, return as is
        else:
            # just check if it's valid url
            if Shortener.full_url_regex.match(url):
                result.append(url)
            else:
                if Shortener.full_url_regex.match('http://'+url):
                    result.append('http://'+url)
                # else, just drop it

        return result

    @staticmethod
    def split_shortened_list(shortened_list):
        if Shortener.shortened_list_delimiter_regex.match(shortened_list):
            return  shortened_list.split(Shortener.shortened_list_delimiter)
        # else, return as is
        return [shortened_list]

    @staticmethod
    def expand_urls(urls, is_audio=False):
        # urls can be a shortened list of url (separated by &) or a list containing
        # either urls or shortened lists
        if not urls:
            return None

        result = []
        # protection against DoS by huge number of urls, no one needs so many
        # mirrors for a single video
        result_size_limit = 10

        if isinstance(urls, basestring):
            urls = Shortener.split_shortened_list(urls)
        if not isinstance(urls, list):
            logging.error('expected list of urls, got {}, type={}'.format(urls, type(urls)))
            return []

        for item in urls:
            if not isinstance(item, basestring):
                logging.error('expected url string, got {}, type={}'.format(item, type(item)))
                continue
            item = Shortener.split_shortened_list(item)
            for url in item:
                result += Shortener.expand_url(url, is_audio=is_audio)
                if len(result) >= result_size_limit:
                    break
        return result

    @staticmethod
    def merge_shortened_urls(url1, url2):
        '''Takes two potentially shortened urls and returns a merged url or None if they couldn't be merged.'''

        match1 = Shortener.expander_regex.match(url1)
        match2 = Shortener.expander_regex.match(url2)
        # make sure urls are actually shortened and belong to the same site
        if match1 and match2 and match1.group('acronym') == match2.group('acronym'):
            acronym = match1.group('acronym')
            list1 = match1.group('list').split(Shortener.shortened_list_separator)
            list2 = match2.group('list').split(Shortener.shortened_list_separator)

            merged_list = []
            for item in list1 + list2:
                # don't include duplicates
                if not item in merged_list:
                    merged_list.append(item)

            merged_url = acronym + '/' + Shortener.shortened_list_separator.join(merged_list)
            return merged_url
        return None

    @staticmethod
    def shorten_urls(urls):
        '''Returns list of shortened lists of urls, or None if urls can't be shortened. All urls should be full urls, that is, urls should have already been expanded by a previous call to expand_urls().'''
        if not urls:
            return None

        result = []
        for url in urls:
            url = Shortener.shorten_url(url)

            # merge with the first shortened url from the same site if one exists
            was_merged = False
            for i in range(len(result)):
                previous_url = result[i]
                merged_url = Shortener.merge_shortened_urls(previous_url, url)
                if merged_url:
                    was_merged = True
                    result[i] = merged_url

            if not was_merged and url not in result:
                result.append(url)

        # check that all urls were properly shortened
        for item in result:
            if not Shortener.expander_regex.match(item):
                return None

        return Shortener.shortened_list_delimiter.join(result)

    @staticmethod
    def get_mime_type(url):
        if not isinstance(url, basestring):
            return None
        for extension in Shortener.mime_types.keys():
            if url.endswith(extension):
                return Shortener.mime_types[extension]
        return None
