# -*- coding: utf-8 -*-
"""This module contains functions for python's datetime/timedelta objects
"""
import functools
import time
from datetime import datetime, timedelta
from numbers import Number
import netCDF4
import numpy as np
import pandas as pd
__all__ = [
    "date2num",
    "num2date",
    "set_time_resolution",
    "to_datetime",
    "to_timedelta",
    "Timer",
]
[docs]
def set_time_resolution(datetime_obj, resolution):
    """Set the resolution of a python datetime object.
    Args:
        datetime_obj: A python datetime object.
        resolution: A string indicating the required resolution.
    Returns:
        A datetime object truncated to *resolution*.
    Examples:
    .. code-block:: python
        from typhon.utils.time import set_time_resolution, to_datetime
        dt = to_datetime("2017-12-04 12:00:00")
        # datetime.datetime(2017, 12, 4, 12, 0)
        new_dt = set_time_resolution(dt, "day")
        # datetime.datetime(2017, 12, 4, 0, 0)
        new_dt = set_time_resolution(dt, "month")
        # datetime.datetime(2017, 12, 1, 0, 0)
    """
    if resolution == "year":
        return set_time_resolution(datetime_obj, "day").replace(month=1, day=1)
    elif resolution == "month":
        return set_time_resolution(datetime_obj, "day").replace(day=1)
    elif resolution == "day":
        return datetime_obj.replace(hour=0, minute=0, second=0, microsecond=0)
    elif resolution == "hour":
        return datetime_obj.replace(minute=0, second=0, microsecond=0)
    elif resolution == "minute":
        return datetime_obj.replace(second=0, microsecond=0)
    elif resolution == "second":
        return datetime_obj.replace(microsecond=0)
    elif resolution == "millisecond":
        return datetime_obj.replace(
            microsecond=int(datetime_obj.microsecond / 1000) * 1000
        )
    else:
        raise ValueError("Cannot set resolution to '%s'!" % resolution) 
[docs]
def to_datetime(obj):
    """Convert an object to a python datetime object.
    Args:
        obj: Can be a string with time information, a numpy.datetime64 or a
            pandas.Timestamp object.
    Returns:
        A python datetime object.
    Examples:
    .. code-block:: python
        dt = to_datetime("2017-12-04 12:00:00")
        # dt is datetime.datetime(2017, 12, 4, 12, 0)
    """
    if isinstance(obj, datetime):
        return obj
    else:
        return pd.to_datetime(obj).to_pydatetime() 
[docs]
def to_timedelta(obj, numbers_as=None):
    """Convert an object to a python datetime object.
    Args:
        obj: Can be a string with time information, a number, a
            numpy.timedelta64 or a pandas.Timedelta object.
        numbers_as: A string that indicates how numbers should be
            interpreted. Allowed values are *weeks*, *days*, *hours*,
            *minutes*, *seconds*, *milliseconds* and *microseconds*.
    Returns:
        A python datetime object.
    Examples:
    .. code-block:: python
        # A timedelta object with 200 seconds
        t = to_timedelta("200 seconds")
        # A timedelta object with 24 days
        t = to_timedelta(24, numbers_as="days")
    """
    if numbers_as is None:
        numbers_as = "seconds"
    if isinstance(obj, timedelta):
        return obj
    elif isinstance(obj, Number):
        return timedelta(**{numbers_as: int(obj)})
    else:
        return pd.to_timedelta(obj).to_pytimedelta() 
unit_mapper = {
    "nanoseconds": "ns",
    "microseconds": "us",
    "milliseconds": "ms",
    "seconds": "s",
    "hours": "h",
    "minutes": "m",
    "days": "D",
}
class InvalidUnitString(Exception):
    def __init__(self, *args, **kwargs):
        super(InvalidUnitString, self).__init__(*args, **kwargs)
[docs]
def date2num(dates, units, calendar=None):
    """Convert an array of integer into datetime objects.
    This function optimizes the date2num function of python-netCDF4 if the
    standard calendar is used.
    Args:
        dates: Either an array of numpy.datetime64 objects (if standard
            gregorian calendar is used), otherwise an array of python
            datetime objects.
        units: A string with the format "{unit} since {epoch}",
            e.g. "seconds since 1970-01-01T00:00:00".
        calendar: (optional) Standard is gregorian. If others are used,
            netCDF4.num2date will be called.
    Returns:
        An array of integers.
    """
    if calendar is None:
        calendar = "gregorian"
    else:
        calendar = calendar.lower()
    if calendar != "gregorian":
        return netCDF4.date2num(dates, units, calendar)
    try:
        unit, epoch = units.split(" since ")
    except ValueError:
        raise InvalidUnitString("Could not convert to numeric values!")
    converted_data = \
        
dates.astype("M8[%s]" % unit_mapper[unit]).astype("int")
    # numpy.datetime64 cannot read certain time formats while pandas can.
    epoch = pd.Timestamp(epoch).to_datetime64()
    if epoch != np.datetime64("1970-01-01"):
        converted_data -= np.datetime64("1970-01-01") - epoch
    return converted_data 
[docs]
def num2date(times, units, calendar=None):
    """Convert an array of integers into datetime objects.
    This function optimizes the num2date function of python-netCDF4 if the
    standard calendar is used.
    Args:
        times: An array of integers representing timestamps.
        units: A string with the format "{unit} since {epoch}",
            e.g. "seconds since 1970-01-01T00:00:00".
        calendar: (optional) Standard is gregorian. If others are used,
            netCDF4.num2date will be called.
    Returns:
        Either an array of numpy.datetime64 objects (if standard gregorian
        calendar is used), otherwise an array of python datetime objects.
    """
    try:
        unit, epoch = units.split(" since ")
    except ValueError:
        raise InvalidUnitString("Could not convert to datetimes!")
    if calendar is None:
        calendar = "gregorian"
    else:
        calendar = calendar.lower()
    if calendar != "gregorian":
        return netCDF4.num2date(times, units, calendar).astype(
            "M8[%s]" % unit_mapper[unit])
    # Numpy uses the epoch 1970-01-01 natively.
    converted_data = times.astype("M8[%s]" % unit_mapper[unit])
    # numpy.datetime64 cannot read certain time formats while pandas can.
    epoch = pd.Timestamp(epoch).to_datetime64()
    # Maybe there is another epoch used?
    if epoch != np.datetime64("1970-01-01"):
        converted_data -= np.datetime64("1970-01-01") - epoch
    return converted_data 
[docs]
class Timer:
    """Provide a simple time profiling utility
    Parameters:
        verbose (bool): Print measured duration after stopping the timer.
        info (str): Allows to add additional information to output.
            The given string is printed before the measured time.
            If `None`, default information is added depending on the use case.
    Returns:
        datetime.timedelta: The duration between start and end time.
    Examples:
        Timer in with statement:
        >>> import time
        >>> with Timer():
        ...     time.sleep(1)
        elapsed time: 0:00:01.003186
        Timer as object (allows to store :class:`datetime.timedelta`):
        >>> import time
        >>> t = Timer().start()
        >>> time.sleep(1)
        >>> dt = t.stop()
        elapsed time: 0:00:01.004756
        As function decorator:
        >>> @Timer()
        ... def own_function(s):
        ...     import time
        ...     time.sleep(s)
        >>> own_function(1)
        own_function: 0:00:01.004667
        Use it in format strings:
        >>> from typhon.utils import Timer
        >>> timer = Timer().start()
        >>> print(f"{timer} elapsed")
        0:00:00.000111 hours elapsed
    """
[docs]
    def __init__(self, info=None, verbose=True):
        """Create a timer object."""
        self.verbose = verbose
        self.info = info
        self.starttime = None
        self.endtime = None 
    def __call__(self, func):
        """Allows to use a Timer object as a decorator."""
        # When no additional information is given, add the function name if
        # Timer is used as decorator.
        if self.info is None:
            self.info = func.__name__
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            with self:
                # Call the original function in a Timer context.
                return func(*args, **kwargs)
        return wrapper
    def __enter__(self):
        return self.start()
    def __exit__(self, *args):
        self.stop()
    def __repr__(self):
        return repr(self.elapsed)
    def __str__(self):
        return str(self.elapsed) + " hours"
    @property
    def elapsed(self):
        """Get the elapsed time as timedelta object"""
        if self.starttime is None:
            raise ValueError("Timer has not been started yet!")
        return timedelta(seconds=time.time() - self.starttime)
[docs]
    def start(self):
        """Start timer."""
        self.starttime = time.time()
        return self 
[docs]
    def stop(self):
        """Stop timer and print info message
        The info message will be only printed if `Timer.verbose` is *true*.
        Returns:
            A timedelta object.
        """
        if self.starttime is None:
            raise ValueError("Timer has not been started yet!")
        self.endtime = time.time()
        dt = timedelta(seconds=self.endtime - self.starttime)
        # If no additional information is specified add default information
        # to make the output more readable.
        if self.info is None:
            self.info = 'elapsed time'
        if self.verbose:
            # Connect additional information and measured time for output.
            print('{info}: {time}'.format(info=self.info, time=dt))
        return dt