# w.c.s. - web application for online forms
# Copyright (C) 2005-2017  Entr'ouvert
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.

import datetime
from decimal import Decimal
from decimal import InvalidOperation as DecimalInvalidOperation
from decimal import DivisionByZero as DecimalDivisionByZero
import hashlib
import math
import string
import random

import pyproj
from pyproj import Geod

from django import template
from django.template import defaultfilters
from django.utils import dateparse
from django.utils import six
from django.utils.encoding import force_bytes, force_text
from django.utils.safestring import mark_safe
from wcs.qommon import evalutils
from wcs.qommon import tokens
from wcs.qommon.admin.texts import TextsDirectory
from wcs.roles import Role

register = template.Library()

@register.filter
def get(mapping, key):
    if hasattr(mapping, 'get'):
        return mapping.get(key)
    return mapping[key]

@register.filter
def startswith(string, substring):
    return string and force_text(string).startswith(force_text(substring))

@register.filter
def split(string, separator=' '):
    if not string:
        return []
    return force_text(string).split(force_text(separator))

@register.filter
def strip(string, chars=None):
    if not string:
        return ''
    if chars:
        return force_text(string).strip(force_text(chars))
    else:
        return force_text(string).strip()

@register.filter
def parse_date(date_string):
    try:
        return evalutils.make_date(date_string)
    except ValueError:
        pass
    # fallback to Django function
    try:
        return dateparse.parse_date(date_string)
    except (ValueError, TypeError):
        return None

@register.filter(expects_localtime=True, is_safe=False)
def date(value, arg=None):
    if arg is None:
        value = parse_date(value)
        if not value:
            return ''
        from wcs.variables import lazy_date
        return lazy_date(parse_date(value))
    if not isinstance(value, (datetime.datetime, datetime.date, datetime.time)):
        value = parse_datetime(value) or parse_date(value)
    return defaultfilters.date(value, arg=arg)

@register.filter
def parse_datetime(datetime_string):
    try:
        return evalutils.make_datetime(datetime_string)
    except ValueError:
        pass
    # fallback to Django function
    try:
        return dateparse.parse_datetime(datetime_string)
    except (ValueError, TypeError):
        return None

@register.filter(name='datetime', expects_localtime=True, is_safe=False)
def datetime_(value, arg=None):
    if arg is None:
        value = parse_datetime(value)
        if not value:
            return ''
        from wcs.variables import lazy_date
        return lazy_date(parse_datetime(value))
    if not isinstance(value, (datetime.datetime, datetime.date, datetime.time)):
        value = parse_datetime(value)
    return defaultfilters.date(value, arg=arg)

@register.filter
def parse_time(time_string):
    # if input is a datetime, extract its time
    try:
        dt = parse_datetime(time_string)
        if dt:
            return dt.time()
    except (ValueError, TypeError):
        pass
    # fallback to Django function
    try:
        return dateparse.parse_time(time_string)
    except (ValueError, TypeError):
        return None

@register.filter(expects_localtime=True, is_safe=False)
def time(value, arg=None):
    if arg is None:
        parsed = parse_time(value)
        return parsed if parsed is not None else ''  # because bool(midnight) == False
    if not isinstance(value, (datetime.datetime, datetime.date, datetime.time)):
        value = parse_time(value)
    return defaultfilters.date(value, arg=arg)

def parse_decimal(value):
    if hasattr(value, 'get_value'):
        value = value.get_value()  # unlazy
    if isinstance(value, six.string_types):
        # replace , by . for French users comfort
        value = value.replace(',', '.')
    try:
        return Decimal(value).quantize(Decimal('1.0000')).normalize()
    except (ArithmeticError, TypeError):
        return Decimal(0)

@register.filter(is_safe=False)
def decimal(value, arg=None):
    if not isinstance(value, Decimal):
        value = parse_decimal(value)
    if arg is None:
        return value
    if hasattr(arg, 'get_value'):
        arg = arg.get_value()  # unlazy
    return defaultfilters.floatformat(value, arg=arg)

@register.filter(expects_localtime=True, is_safe=False)
def add_days(value, arg):
    if hasattr(value, 'timetuple'):
        # extract real value in case of lazy object
        value = value.timetuple()
    value = parse_date(value)  # consider only date, not hours
    if not value:
        return ''
    from wcs.variables import lazy_date
    arg = parse_decimal(arg)
    if not arg:
        return lazy_date(value)
    result = value + datetime.timedelta(days=float(arg))
    if hasattr(result, 'date'):
        result = result.date()
    return lazy_date(result)

@register.filter(expects_localtime=True, is_safe=False)
def add_hours(value, arg):
    if hasattr(value, 'timetuple'):
        # extract real value in case of lazy object
        value = value.timetuple()
    value = parse_datetime(value)
    if not value:
        return ''
    from wcs.variables import lazy_date
    arg = parse_decimal(arg)
    if not arg:
        return lazy_date(value)
    return lazy_date(value + datetime.timedelta(hours=float(arg)))

@register.filter(expects_localtime=True, is_safe=False)
def age_in_days(value, now=None):
    try:
        return evalutils.age_in_days(value, now)
    except ValueError:
        return ''

@register.filter(expects_localtime=True, is_safe=False)
def age_in_hours(value, now=None):
    # consider value and now as datetimes (and not dates)
    if hasattr(value, 'timetuple'):
        # extract real value in case of lazy object
        value = value.timetuple()
    value = parse_datetime(value)
    if not value:
        return ''
    if now is not None:
        if hasattr(now, 'timetuple'):
            now = now.timetuple()
        now = parse_datetime(now)
        if not now:
            return ''
    else:
        now = datetime.datetime.now()
    return int((now - value).total_seconds() / 3600)

@register.filter(expects_localtime=True, is_safe=False)
def age_in_years(value, today=None):
    try:
        return evalutils.age_in_years_and_months(value, today)[0]
    except ValueError:
        return ''

@register.filter(expects_localtime=True, is_safe=False)
def age_in_months(value, today=None):
    try:
        years, months = evalutils.age_in_years_and_months(value, today)
    except ValueError:
        return ''
    return years*12 + months

@register.simple_tag
def standard_text(text_id):
    return mark_safe(TextsDirectory.get_html_text(str(text_id)))

@register.simple_tag(takes_context=True)
def action_button(context, action_id, label, delay=3):
    from wcs.formdef import FormDef
    formdata_id = context.get('form_number_raw')
    formdef_urlname = context.get('form_slug')
    if not (formdef_urlname and formdata_id):
        return ''
    formdef = FormDef.get_by_urlname(formdef_urlname)
    formdata = formdef.data_class().get(formdata_id, ignore_errors=True)
    token = tokens.Token(expiration_delay=delay*86400, size=64)
    token.type = 'action'
    token.context = {
        'form_slug': formdef_urlname,
        'form_number_raw': formdata_id,
        'action_id': action_id,
        'label': label,
    }
    token.store()
    return '---===BUTTON:%s:%s===---' % (token.id, label)

@register.filter
def add(term1, term2):
    '''replace the "add" native django filter'''
    return parse_decimal(term1) + parse_decimal(term2)

@register.filter
def subtract(term1, term2):
    return parse_decimal(term1) - parse_decimal(term2)

@register.filter
def multiply(term1, term2):
    return parse_decimal(term1) * parse_decimal(term2)

@register.filter
def divide(term1, term2):
    try:
        return parse_decimal(term1) / parse_decimal(term2)
    except DecimalInvalidOperation:
        return ''
    except DecimalDivisionByZero:
        return ''

@register.filter
def ceil(value):
    '''the smallest integer value greater than or equal to value'''
    return decimal(math.ceil(parse_decimal(value)))

@register.filter
def floor(value):
    return decimal(math.floor(parse_decimal(value)))

@register.filter(name='abs')
def abs_(value):
    return decimal(abs(parse_decimal(value)))

@register.simple_tag
def version_hash():
    from wcs.qommon.admin.menu import get_vc_version
    return hashlib.md5(force_bytes(get_vc_version())).hexdigest()


def generate_token(alphabet, length):
    r = random.SystemRandom()
    return ''.join([r.choice(alphabet) for i in range(length)])


@register.simple_tag
def token_decimal(length=6):
    # entropy by default is log(10^6)/log(2) = 19.93 bits
    # decimal always need more length than alphanum for the same security level
    # for 128bits security level, length must be more than log(2^128)/log(10) = 38.53 digits
    return generate_token(string.digits, length)


@register.simple_tag
def token_alphanum(length=4):
    # use of a 28 characters alphabet using uppercase letters and digits but
    # removing confusing characters and digits 0, O, 1 and I.
    # entropy by default is log(28^4)/log(2) = 19.22 bits
    # for 128 bits security level length must be more than log(2^128)/log(28) = 26.62 characters
    return generate_token('23456789ABCDEFGHJKLMNPQRSTUVWXYZ', length)


@register.filter
def token_check(token1, token2):
    return force_text(token1).strip().upper() == force_text(token2).strip().upper()


def get_latlon(obj):
    if getattr(obj, 'geoloc', None):
        if 'base' in obj.geoloc:
            return obj.geoloc['base']['lat'], obj.geoloc['base']['lon']
        return None, None
    if hasattr(obj, 'get_value'):
        obj = obj.get_value()  # unlazy
    if not obj or not isinstance(obj, six.string_types) or ';' not in obj:
        return None, None
    try:
        return float(obj.split(';')[0]), float(obj.split(';')[1])
    except ValueError:
        return None, None

@register.filter
def distance(obj1, obj2):
    lat1, lon1 = get_latlon(obj1)
    if lat1 is None or lon1 is None:
        return None
    lat2, lon2 = get_latlon(obj2)
    if lat2 is None or lon2 is None:
        return None
    geod = Geod(ellps='WGS84')
    distance = geod.inv(lon1, lat1, lon2, lat2)[2]
    return distance

@register.filter
def distance_filter(queryset, distance=1000):
    return queryset.distance_filter(distance=int(distance))

@register.filter
def order_by(queryset, attribute):
    return queryset.order_by(attribute)

@register.filter
def reproj(coords, projection_name):
    proj = pyproj.Proj(init='EPSG:4326')
    target_proj = pyproj.Proj(init=projection_name)
    return pyproj.transform(proj, target_proj, coords['lon'], coords['lat'])

@register.filter
def has_role(user, role_name):
    if not callable(getattr(user, 'get_roles', None)):
        # do not fail on non-user objects, just return False
        return False
    for role_id in user.get_roles():
        try:
            if role_name == Role.get(role_id).name:
                return True
        except KeyError:  # role has been deleted
            pass
    return False
