# By: Riasat Ullah
# This file contains methods that will help in handling analytics related data.

from dateutil.relativedelta import relativedelta
from utils import constants
import calendar
import datetime
import math


def get_period_labels_and_values(start_date, end_date, data, round_dp=None):
    '''
    For a range of dates fill the missing days with 0 value and the rest with the matching value
    from a dict of values keyed on date.
    :param start_date: (datetime.date) period start
    :param end_date: (datetime.date) period end
    :param data: (dict) -> { date: count, ... }
    :param round_dp: (int) number of decimal places to round to; default is None
    :return: (tuple of list) -> labels, values
    '''
    labels = []
    values = []
    days_range = (end_date - start_date).days

    # + 1 because the range is inclusive
    for i in range(0, days_range + 1):
        check_date = start_date + datetime.timedelta(days=i)

        labels.append(check_date.strftime(constants.date_month_day_format))
        if round_dp is None:
            values.append(data[check_date] if check_date in data else 0)
        else:
            values.append(round(data[check_date], round_dp) if check_date in data else 0)

    return labels, values


def get_previous_periods(current_start, current_end, period_count):
    '''
    Gets the list of period start and end dates for a given number of periods with the oldest period first.
    :param current_start: (datetime.date) the start date of the current period
    :param current_end: (datetime.date) the end date of the current period
    :param period_count: (int) number of periods
    :return: (list of tuples) -> [(start 1, end 1), ...]
    '''
    assert isinstance(current_start, datetime.date)
    assert isinstance(current_end, datetime.date)

    previous_periods = [(current_start, current_end)]

    if current_start.day == 1 and current_end.day == calendar.monthrange(current_end.year, current_end.month)[1]:
        months_gap = (current_end.year - current_start.year) * 12 + (current_end.month - current_start.month) + 1
        for i in range(1, period_count):
            previous_periods.append((current_start - relativedelta(months=months_gap * i),
                                     current_end - relativedelta(months=months_gap * i)))
    else:
        days_gap = (current_end - current_start).days + 1
        for i in range(1, period_count):
            previous_periods.append((current_start - datetime.timedelta(days=days_gap * i),
                                     current_end - datetime.timedelta(days=days_gap * i)))

    previous_periods = sorted(previous_periods)
    return previous_periods


def get_period_totals(periods, data_values):
    '''
    Get the total count of all the values within the period.
    :param periods: (list of tuples of dates) -> [(start date, end date), ...]
    :param data_values: (dict) -> {date: value, ...}
    :return: (list) of total of values in the same order as the periods
    '''
    period_totals = []
    for start_, end_ in periods:
        period_totals.append(sum([data_values[inst_date] for inst_date in data_values if start_ <= inst_date <= end_]))
    return period_totals


def convert_nan_to_number(value, rounding=2):
    '''
    Converts a pandas NaN value to a number.
    :param value: value to check
    :param rounding: number of decimal places to round to; None if should not be rounded
    :return: 0 if is NaN; otherwise just the value
    '''
    float_value = float(value)
    if math.isnan(float_value):
        return 0

    if rounding is not None:
        value = round(float_value, rounding)

    return value


def get_percentage_change(new_value, old_value):
    '''
    Calculate the percentage change between two values.
    :param new_value: new value
    :param old_value: old value (this will be the denominator)
    :return: float (2 d.p.) -> percentage change
    '''
    new_value = convert_nan_to_number(new_value)
    old_value = convert_nan_to_number(old_value)
    if new_value == old_value:
        return 0
    else:
        return 100 if old_value == 0 else round((new_value - old_value) / old_value * 100, 2)
