# -*- coding: utf8 -*-
import re
import functools
from datetime import datetime, timedelta
from os import path
from jinja2 import Markup


# For demo purposes
IMAGE_API_URL = 'http://gq-images.condecdn.net'



"""
LANDSCAPE/PORTRAIT STANDARD
- 400 [Mobile basic card & feature] - 405x270
- 800 [Feature][400][2x] - 810x540
- 1000 [Home page super feature] - 1020x680
- 1600 [Hero image instead of 1440][800](2x) - 1620x1080

LANDSCAPE/PORTRAIT RETINA
- 2000 [1000](2x) - 2040x1360
- 3200 [1600](2x) - 3240x2160
"""
SIZES_LANDSCAPE = SIZES_PORTRAIT = [(405,270), (810,540), (1020,680),
    (1620,1080)]
SIZES_LANDSCAPE_RETINA = SIZES_PORTRAIT_RETINA = [(2040,1360), (3240,2160)]



"""
SQUARE STANDARD
- 100 [Mobile most recent, media block] - 100x100
- 200 [Most recent, media block][2x] - 200x200
- 300 [Shows news list] - 300x300
- 400 [200](2x) - 400x400
- 600 [300](2x) - 600x600
- 900 [Home page super feature tablet] - 900x900

SQUARE RETINA
- 1800 [900](2x) - 1800x1800
"""
SIZES_SQUARE = [(100,100), (200,200), (300,300), (400,400), (600,600),
    (900,900)]
SIZES_SQUARE_RETINA = [(1800,1800)]


def _get_height(width, orientation):
    '''Calculates the height based on the width and orientation'''

    accepted_orientations = ['landscape', 'portrait', 'square']
    if not orientation in accepted_orientations:
        return 0
    if orientation == 'square':
        return width
    if orientation == 'landscape':
        return width * .666666667
    if orientation == 'portrait':
        return width / .666666667
    return 0



def _import_svg_icon(svg_filename=None):
    '''Reads the svg file and returns the contents'''

    assert svg_filename is not None, 'svg_filename cannot be equal to None'
    with open(svg_filename) as f:
        return f.read()



def _is_too_big(width, crop_width, max_width):
    if crop_width > width:
        return True
    if max_width and max_width > width:
        return True
    return False



def returns_markup(func):
    '''Decorator for functions which should be marked as returning markup (i.e.
       a string which should not be escaped in the template).'''
    # ***
    # NOTE: Returns 'safe' markup, ie, non-escaped!
    # ***
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        if result is not None:
            result = Markup(result)
        return result
    return wrapper



def create_crops(image, landscape=False, portrait=False, square=False,
    retina=False, max_width=None):


    if not any([landscape, portrait, square]):
        raise ValueError('landscape, portrait or square was not set')

    crops = {}

    if landscape:
        crops['landscape'] = get_landscape_crops(image, retina=retina,
            max_width=max_width)

    if portrait:
        crops['portrait'] = get_portrait_crops(image, retina=retina,
            max_width=max_width)

    if square:
        crops['square'] = get_square_crops(image, retina=retina,
            max_width=max_width)

    # If the user only want one sizes, just return that one
    if len(crops.keys()) == 1:
        return crops.popitem()[1]

    # Return the whole thing
    return crops



def create_fake_crop(**kwargs):
    '''Returns a fake crop object that matches the api'''

    return {
        'content_type': kwargs.get('content_type', 'unknown'),
        'crop_type': kwargs.get('crop_type', 'unknown'),
        'filesize_bytes': kwargs.get('filesize_bytes', 'unknown'),
        'height': kwargs.get('height', 0),
        'orientation': kwargs.get('orientation', 'unknown'),
        'public_url': kwargs.get('public_url', ''),
        'uid': kwargs.get('uid', ''),
        'width': kwargs.get('width', 0)
    }



def fake_portrait_crop():
    '''Returns a fake portrait crop'''

    return create_fake_crop(content_type='image/png', orientation='portrait',
        height=600, public_url=image_fail_portrait(None), width=400)



def fake_landscape_crop():
    '''Returns a fake landscape crop'''

    return create_fake_crop(content_type='image/png', orientation='landscape',
        height=400, public_url=image_fail_landscape(None), width=600)



def fake_square_crop():
    '''Returns a fake square crop'''

    return create_fake_crop(content_type='image/png', orientation='square',
        height=400, public_url=image_fail_square(None), width=400)



def get_article_date(article):
    '''Returns the string date to use for the article. If no display_date is
       set, use the published_date'''

    if article['display_date']:
        return article['display_date']
    return article['published_date']



def get_image_at_size(image, width, orientation=None):
    '''Returns an image object at the specified size'''

    accepted_orientations = ['landscape', 'portrait', 'square']
    original_orientation = image['original']['orientation']

    # If orientation is None, use the image orientation
    if not orientation or not orientation in accepted_orientations:
        orientation = original_orientation

    # If the image width is the same as original width, just use the original
    if (orientation == original_orientation and
        width == image['original']['width']):
        return create_fake_crop(orientation=orientation,
            width=image['original']['width'],
            height=image['original']['height'],
            public_url=image['original']['public_url'])

    # Build the image url
    url_format = {
        'base_url': IMAGE_API_URL,
        'uid': image['uid'],
        'width': width
    }
    url_structure = '{base_url}/image/{uid}/crop/{width}'
    if not original_orientation == orientation:
        if orientation == 'landscape':
            url_structure = '{base_url}/image/{uid}/crop/{width}/landscape/top'
        elif orientation == 'portrait':
            if original_orientation == 'landscape':
                url_structure = '{base_url}/image/{uid}/crop/{width}/portrait/middle'
            elif original_orientation == 'square':
                url_structure = '{base_url}/image/{uid}/crop/{width}/portrait/middle'
        elif orientation == 'square':
            if original_orientation == 'landscape':
                url_structure = '{base_url}/image/{uid}/crop/{width}/square/middle'
            elif original_orientation == 'portrait':
                url_structure = '{base_url}/image/{uid}/crop/{width}/square/top'
    public_url = url_structure.format(**url_format)

    # Build and return the fake image crop
    return create_fake_crop(orientation=orientation, width=width,
        height=_get_height(width, orientation), public_url=public_url)



def get_image_srcset(crops):
    '''Creates a string for srcet'''
    crops = map(lambda crop: '%s %sw' % (crop['public_url'], crop['width']),
        crops)
    return ', '.join(crops)



def get_key(x, y):
    if x is None or y not in x:
        return None
    return x[y]



def get_landscape_crops(image, retina=False, max_width=None):

    landscape_crops = []

    try:
        crops = SIZES_LANDSCAPE[:]
        if retina:
            crops += SIZES_LANDSCAPE_RETINA

        for crop in crops:
            # Check if the original image is big enough to make the size
            if _is_too_big(image['original']['width'], crop[0], max_width):
                continue
            # Check if the current crop is bigger than the max width
            if max_width and crop[0] > max_width:
                continue
            # Make the image
            landscape_crops.append(get_image_at_size(image, crop[0],
                'landscape'))

        # We might have no crops because image is too small for the max width
        # or the max width is smaller that the smallest crop. If its the latter,
        # make the smallest crop
        if len(landscape_crops) == 0:
            if max_width and image['original']['width'] >= max_width:
                landscape_crops.append(get_image_at_size(image, max_width,
                    'landscape'))
            else:
                landscape_crops.append(get_image_at_size(image,
                    image['original']['width'], 'landscape'))

    except Exception, e:
        print e
        landscape_crops = [fake_landscape_crop()]

    return landscape_crops


def get_portrait_crops(image, retina=False, max_width=None):

    portrait_crops = []

    try:
        crops = SIZES_PORTRAIT[:]
        if retina:
            crops += SIZES_PORTRAIT_RETINA

        for crop in crops:
            # Check if the original image is big enough to make the size
            if _is_too_big(image['original']['width'], crop[0], max_width):
                continue
            # Check if the current crop is bigger than the max width
            if max_width and crop[0] > max_width:
                continue
            # Make the image
            portrait_crops.append(get_image_at_size(image, crop[0], 'portrait'))

        if len(portrait_crops) == 0:
            if max_width and image['original']['width'] >= max_width:
                portrait_crops.append(get_image_at_size(image, max_width,
                    'portrait'))
            else:
                portrait_crops.append(get_image_at_size(image,
                    image['original']['width'], 'portrait'))

    except:
        portrait_crops = [fake_portrait_crop()]

    return portrait_crops



def get_square_crops(image, retina=False, max_width=None):

    square_crops = []
    try:
        crops = SIZES_SQUARE[:]
        if retina:
            crops += SIZES_SQUARE_RETINA


        for crop in crops:
            # Check if the original image is big enough to make the size
            if _is_too_big(image['original']['width'], crop[0], max_width):
                continue
            # Check if the current crop is bigger than the max width
            if max_width and crop[0] > max_width:
                continue
            # Make the image
            square_crops.append(get_image_at_size(image, crop[0], 'square'))

        if len(square_crops) == 0:
            if max_width and image['original']['width'] >= max_width:
                square_crops.append(get_image_at_size(image, max_width,
                    'square'))
            else:
                square_crops.append(get_image_at_size(image,
                    image['original']['width'], 'square'))

    except:
        square_crops = [fake_square_crop()]

    return square_crops



def get_primary_tag(article):
    '''Returns the primary tag on the article. At the moment it just returns
       the first item in the tags list but it might end up needing more logic'''

    primary_tag = None

    # If the article is video, use the series
    if 'type' in article and article['type'] == 'video':
        primary_tag = article['series']

    # If video and no series set and theres no tags, quit early
    if not primary_tag and (not 'tags' in article or
        len(article['tags']) < 1):
        return None

    # If we haven't found anything, just use the first tag
    if not primary_tag:
        primary_tag = article['tags'][0]

    # Temp fix for search items
    if not 'title' in primary_tag:
        return {
            'title': primary_tag
        }

    return primary_tag


def get_relative_time(dt_string, dt_input_format='%Y-%m-%d %H:%M:%S'):

    TIME_SECOND = 1
    TIME_MINUTE = TIME_SECOND * 60
    TIME_HOUR = TIME_MINUTE * 60
    TIME_SIX_HOURS = TIME_HOUR*6

    try:
        article_dt = datetime.strptime(dt_string[:-05], dt_input_format)
        today_dt = datetime.utcnow()
        yesterday_dt = today_dt - timedelta(days=1)
        time_diff = today_dt - article_dt

        # If the article's display date is more than 5 calendar days from the
        # current date, display the date as DD MMM YYYY (eg 12 Feb 2016)
        if time_diff.days > 5:
            return article_dt.strftime('%d %b %Y')
        # If the article's display date is between 2 and 5 calendar days of the
        # current date, display the date as 'X days ago', where X is the number
        # of calendar days since the article was published.
        elif time_diff.days >= 2 and time_diff.days <= 5:
            return '{} days ago'.format(time_diff.days)
        # If the article was published more than 6 hours ago and the date is
        # yesterday's date, the date is displayed as '1 day ago'
        elif (time_diff.seconds >= TIME_SIX_HOURS and
            article_dt.day == yesterday_dt.day):
            return '1 day ago'
        # If the article was published more than 6 hours ago and the date is
        # today's date, use the article's published time (HH:MM PM)
        elif (time_diff.seconds > TIME_SIX_HOURS and
            article_dt.day == today_dt.day):
            return article_dt.strftime('%I:%M %p')
        # If the date is more than one hour old but within the last 6 hours,
        # show the date as 'X hours ago', where X is the number of hours
        # (rounded normally) since the article was published.
        elif (time_diff.seconds > TIME_HOUR and
            time_diff.seconds <= TIME_SIX_HOURS):
            return '{} hours ago'.format(round(time_diff.seconds/TIME_HOUR))
        # If the date is within the last 60 minutes, display the date as
        # 'X minutes ago'
        else:
            return '{} minutes ago'.format(round(time_diff.seconds/TIME_MINUTE))
    except:
        return dt_string


def image_fail(value, default):
    '''Helper function for image_fail_landscape, image_fail_portrait and
       image_fail_square'''
    if value:
        return value
    return default



def image_fail_landscape(value):
    '''Checks if value is truthy, if not, give the fail landscape image'''
    return image_fail(value, '/static/img/gq_missing_landscape.png')



def image_fail_portrait(value):
    '''Checks if value is truthy, if not, give the fail portrait image'''
    return image_fail(value, '/static/img/gq_missing_portrait.png')



def image_fail_square(value):
    '''Checks if value is truthy, if not, give the fail square image'''
    return image_fail(value, '/static/img/gq_missing_square.png')



@returns_markup
def import_svg_icon(svg_filename, svg_classes=None):
    filename_format = {
        'base': path.dirname(path.realpath(__file__)),
        'filename': svg_filename,
        'relative': '../static'
    }
    svg_string = _import_svg_icon("{base}/{relative}/{filename}".format(
        **filename_format))

    if svg_classes:
        # TODO: log error but silent shhhhh
        assert svg_string[:4] == '<svg'
        svg_string = '<svg class="{classes}"{svg}'.format(
            classes=svg_classes,
            # We use 4 as <svg is 4 characters :)
            svg=svg_string[4:]
        )

    return svg_string


def try_image_crops(images, image_types):
    for image_type in image_types:
        if image_type in images:
            return images[image_type]
    return None


# BBCODE STUFF
re_bbcode_attribute_detection = r'(?:\s*[a-zA-Z]+?="(?:[^"\\]*(?:\\.[^"\\]*)*)"\s*)*?'
re_bbcode_attribute_extract = r'([a-zA-Z]+?)="([^"\\]*(?:\\.[^"\\]*)*)"'
def strip_bbcode(text):
    '''Strips any bbcode from the text'''

    def _repl_with_content(m):
        return m.groups()[1]

    bbcode_with_content = [
        'b', 'i', 'u', 's', 'link', 'dropcap', 'email', 'h1', 'h2', 'h3', 'h4',
        'h5', 'h6', 'quote', 'list', 'item', 'note', 'question', 'answer'
    ]
    bbcode_without_content = [
        'youtube', 'style-shrink-form', 'article', 'gallery', 'twitter',
        'vimeo', 'facebook', 'instagram', 'vine', 'image'
    ]

    for bbcode in bbcode_with_content:
        pattern = r'(\[%s%s\](.*?)\[\/%s\])' % (bbcode,
            re_bbcode_attribute_detection, bbcode)
        text = re.sub(pattern, _repl_with_content, text,
            flags=re.UNICODE|re.DOTALL)

    for bbcode in bbcode_without_content:
        pattern = r'(\[%s%s\])' % (bbcode, re_bbcode_attribute_detection)
        text = re.sub(pattern, '', text, flags=re.UNICODE|re.DOTALL)

    return text


def _remove_newlines(text):
    '''Removes new lines from text'''

    return text.replace('\n\n', '')


def _generate_tag_attributes(attrs, text):
    '''Generates attributes from a dictionary
    I.e., {'foo': 'bar'} ==> foo="bar"
    '''

    if attrs:
        output = u''
        for key, value in attrs.items():
            try:
                output = '%s %s="%s"' % (output, key, value(text))
            except TypeError:
                # value wasn't a callable
                output = '%s %s="%s"' % (output, key, value)
            except Exception:
                raise
        return output

    return u''


def _ignore_paragraphs_and_breaks(text):
    '''Trims paragraphs or break tags from the beginning and end of text'''

    return ur'(?:<br />\s*?)?(?:<p>\s*?)?%s(?:\s*?</p>)?' % text


def simple_tag(matcher, replacement_tag, tag_attrs=None, **innerkwargs):
    '''Simple tag generator function'''

    trim_paragraphs = innerkwargs.get('trim_paragraphs', False)
    remove_newlines = innerkwargs.get('remove_newlines', False)

    def f(text, **kwargs):
        '''Generated functor to return'''

        def _repl(m):
            t = m.groups()[0]

            if remove_newlines:
                t = _remove_newlines(t)

            text = ur'<{tag}{attrs}>{text}</{tag}>'.format(
                text=t,
                tag=replacement_tag,
                attrs=_generate_tag_attributes(tag_attrs, t)
            )

            return text

        # return a substituted string
        p = ur'\[%s\](.+?)\[\/%s\]' % (matcher, matcher)

        if trim_paragraphs:
            p = _ignore_paragraphs_and_breaks(p)

        return re.sub(p, _repl, text, flags=re.UNICODE|re.DOTALL)

    # compat
    f.__doc__ = 'Simple tag parser: %s' % matcher
    f.__name__ = f.__doc__

    return f


def simple_parse_bbcode(text, **kwargs):
    '''Simple parser that does bold, italic, underline and strikethrough'''

    t = text

    # Bold
    if kwargs.get('bold', True):
        t = simple_tag('b', 'strong', remove_newlines=True)(t)
    # Italic
    if kwargs.get('italic', True):
        t = simple_tag('i', 'em', remove_newlines=True)(t)
    # Underline
    if kwargs.get('underline', True):
        t = simple_tag('u', 'u', remove_newlines=True)(t)
    # Strikethrough
    if kwargs.get('strikethrough', True):
        t = simple_tag('s', 'span', tag_attrs={
            'style': 'text-decoration: line-through;'
        }, remove_newlines=True)(t)
    # Link
    if kwargs.get('link', True):
        t = parse_url(t)
    # Email
    if kwargs.get('email', True):
        t = mailto_emails(t)

    return t


def pad_value(value, amount=0, character='0'):
    '''Pads a number value with extra characters'''
    return str(value).rjust(int(amount), str(character))

FILTERS = {
    'create_crops': create_crops,
    'get_article_date': get_article_date,
    'get_image_srcset': get_image_srcset,
    'get_key': get_key,
    'get_primary_tag': get_primary_tag,
    'get_relative_time': get_relative_time,
    'import_svg_icon': import_svg_icon,
    'try_image_crops': try_image_crops,
    'strip_bbcode': strip_bbcode,
    'parse_simple_bbcode': simple_parse_bbcode,
    'pad_value': pad_value
}
