# By: Riasat Ullah
# This module provides certain tools to work with date and time.

from dateutil import tz
from pytz import timezone
from utils import constants
from validations import string_validator
import calendar
import configuration
import datetime


def get_current_timestamp(region=configuration.standard_timezone, as_str=False):
    '''
    Gets the current timestamp of a region.
    By default returns UTC if no region is provided.
    :param region: timezone
    :param as_str: (boolean) True to convert timestamp to str
    :return: datetime object of the current timestamp
    '''
    timestamp = datetime.datetime.now(timezone(region)).replace(tzinfo=None)
    if as_str:
        return timestamp.strftime(constants.timestamp_format)
    return timestamp


def utc_to_region_time(timestamp, region):
    '''
    Convert a UTC datetime.datetime object to that of a particular region's datetime.datetime object.
    :param timestamp: timestamp when this request was made
    :param region: region whose timezone the datetime.datetime object should be converted to
    :return: regional timestamp
    '''
    utc_zone = tz.gettz('UTC')
    region_zone = tz.gettz(region)
    utc_timestamp = timestamp.replace(tzinfo=utc_zone)
    region_timestamp = utc_timestamp.astimezone(region_zone).replace(tzinfo=None)
    return region_timestamp


def region_to_utc_time(timestamp, region):
    '''
    Convert a certain region's datetime.datetime object to a UTC datetime.datetime object.
    :param timestamp: timestamp when this request was made
    :param region: region this timestamp is from
    :return: UTC timestamp
    '''
    utc_zone = tz.gettz('UTC')
    region_zone = tz.gettz(region)
    region_timestamp = timestamp.replace(tzinfo=region_zone)
    utc_timestamp = region_timestamp.astimezone(utc_zone).replace(tzinfo=None)
    return utc_timestamp


def switch_region_time(timestamp, curr_region, new_region):
    '''
    Convert a certain region's datetime.datetime object to another region's datetime.datetime object.
    :param timestamp: timestamp when this request was made
    :param curr_region: region this timestamp is from
    :param new_region: region this timestamp should be converted to
    :return: new region timestamp
    '''
    new_region_zone = tz.gettz(new_region)
    curr_region_zone = tz.gettz(curr_region)
    curr_region_timestamp = timestamp.replace(tzinfo=curr_region_zone)
    new_region_timestamp = curr_region_timestamp.astimezone(new_region_zone).replace(tzinfo=None)
    return new_region_timestamp


def get_last_datetime_of_current_month():
    '''
    Get the last datetime object of the current month.
    :return: UTC datetime.datetime
    '''
    timestamp = get_current_timestamp()
    last_day = calendar.monthrange(timestamp.year, timestamp.month)[1]
    timestamp = timestamp.replace(day=last_day, hour=23, minute=59, second=59, microsecond=0)
    return timestamp


def get_time_from_string(time_str):
    '''
    Convert a time str in the format HH:MM to a datetime.time object.
    :param time_str: (str) string time in the format HH::MM
    :return: (datetime.time) object
    '''
    if isinstance(time_str, datetime.time):
        return time_str
    elif isinstance(time_str, str):
        if len(time_str) == 5:
            return datetime.datetime.strptime(time_str, constants.time_format).time()
        else:
            return datetime.datetime.strptime(time_str, constants.json_time_format).time()
    else:
        raise TypeError('Expected datetime.time or str. Got - ' + str(type(time_str)))


def get_date_from_string(date_str):
    '''
    Converts a date string to a datetime.date object.
    :param date_str: the object that is supposed to be a string date
    :return: a datetime.date object
    '''
    if isinstance(date_str, datetime.date):
        return datetime.datetime.combine(date_str, datetime.datetime.min.time()).date()
    elif isinstance(date_str, str):
        date_str = date_str.replace('-', '')
        return datetime.datetime.strptime(date_str, constants.date_format).date()
    else:
        raise TypeError('Expected datetime.date or str. Got - ' + type(date_str))


def get_timestamp_from_string(timestamp_str):
    '''
    Converts a timestamp string to a datetime.datetime object.
    :param timestamp_str: the object that is supposed to be a string date
    :return: a datetime.datetime object
    '''
    if isinstance(timestamp_str, str):
        if string_validator.is_timestamp_with_milliseconds(timestamp_str):
            return datetime.datetime.strptime(timestamp_str, constants.timestamp_milli_format)

        elif string_validator.is_json_timestamp_with_milliseconds(timestamp_str):
            return datetime.datetime.strptime(timestamp_str, constants.json_timestamp_milli_format)

        elif string_validator.is_timestamp(timestamp_str):
            return datetime.datetime.strptime(timestamp_str, constants.timestamp_format)

        elif string_validator.is_json_timestamp(timestamp_str):
            return datetime.datetime.strptime(timestamp_str, constants.json_timestamp_format)

        elif string_validator.is_json_timestamp_with_milliseconds(timestamp_str):
            return datetime.datetime.strptime(timestamp_str, constants.json_timestamp_milli_format)

        elif string_validator.is_json_timestamp_with_milliseconds_and_timezone(timestamp_str):
            return datetime.datetime.strptime(timestamp_str, constants.json_timestamp_milli_format_with_tz)

        else:
            raise TypeError('Unknown str format - ' + timestamp_str)

    elif isinstance(timestamp_str, datetime.datetime):
        return timestamp_str
    else:
        raise TypeError('Expected datetime.date or str. Got - ' + str(type(timestamp_str)) +
                        '. Value - ' + str(timestamp_str))


def jsonify_timestamp(obj):
    '''
    This function should be used when dumping dict to json. datetime.datetime objects are not json serializable.
    So, we have to correct them using this function.
    :param obj: any type of object
    :return: (object) -> if the object is a datetime.datetime object then the format is corrected;
        otherwise returns the object as it is
    '''
    if isinstance(obj, datetime.datetime):
        return obj.strftime(constants.json_timestamp_format)
    elif isinstance(obj, datetime.date):
        return obj.strftime(constants.date_hyphen_format)
    elif isinstance(obj, datetime.time):
        # return obj.isoformat()
        return obj.strftime(constants.json_time_format)
    else:
        return obj
