"""
Functions for converting dates to/from JD and MJD. Assumes dates are historical
dates, including the transition from the Julian calendar to the Gregorian
calendar in 1582. No support for proleptic Gregorian/Julian calendars.

:Author: Matt Davis
:Website: http://github.com/jiffyclub

"""

import math
import datetime as dt

# Note: The Python datetime module assumes an infinitely valid Gregorian calendar.
#       The Gregorian calendar took effect after 10-15-1582 and the dates 10-05 through
#       10-14-1582 never occurred. Python datetime objects will produce incorrect
#       time deltas if one date is from before 10-15-1582.

def mjd_to_jd(mjd):
    """
    Convert Modified Julian Day to Julian Day.

    Args:
        mjd (float): Modified Julian Day

    Returns:
        float: Julian Day

    """
    return mjd + 2400000.5


def jd_to_mjd(jd):
    """
    Convert Julian Day to Modified Julian Day.

    Args:
        jd (float): Julian Day

    Returns:
        float: Modified Julian Day

    """
    return jd - 2400000.5


def date_to_jd(year,month,day):
    """
    Convert a date to Julian Day.

    Args:
        year (int): Year as integer. Years preceding 1 A.D. should be 0 or negative.
                    The year before 1 A.D. is 0, 10 B.C. is year -9.
        month (int): Month as integer, Jan = 1, Feb. = 2, etc.
        day (float): Day, may contain fractional part.

    Returns:
        float: Julian Day

    Examples:
        Convert 6 a.m., February 17, 1985 to Julian Day

        >>> date_to_jd(1985,2,17.25)
        2446113.75

    """
    if month == 1 or month == 2:
        yearp = year - 1
        monthp = month + 12
    else:
        yearp = year
        monthp = month

    # this checks where we are in relation to October 15, 1582, the beginning
    # of the Gregorian calendar.
    if ((year < 1582) or
        (year == 1582 and month < 10) or
        (year == 1582 and month == 10 and day < 15)):
        # before start of Gregorian calendar
        B = 0
    else:
        # after start of Gregorian calendar
        A = math.trunc(yearp / 100.)
        B = 2 - A + math.trunc(A / 4.)

    if yearp < 0:
        C = math.trunc((365.25 * yearp) - 0.75)
    else:
        C = math.trunc(365.25 * yearp)

    D = math.trunc(30.6001 * (monthp + 1))

    jd = B + C + D + day + 1720994.5

    return jd


def jd_to_date(jd):
    """
    Convert Julian Day to date.

    Algorithm from 'Practical Astronomy with your Calculator or Spreadsheet',
        4th ed., Duffet-Smith and Zwart, 2011.

    Args:
        jd (float): Julian Day

    Returns:
        int: Year as integer. Years preceding 1 A.D. should be 0 or negative.
             The year before 1 A.D. is 0, 10 B.C. is year -9.
        int: Month as integer, Jan = 1, Feb. = 2, etc.
        float: Day, may contain fractional part.

    Examples:
        Convert Julian Day 2446113.75 to year, month, and day.

        >>> jd_to_date(2446113.75)
        (1985, 2, 17.25)

    """
    jd = jd + 0.5

    F, I = math.modf(jd)
    I = int(I)

    A = math.trunc((I - 1867216.25)/36524.25)

    if I > 2299160:
        B = I + 1 + A - math.trunc(A / 4.)
    else:
        B = I

    C = B + 1524

    D = math.trunc((C - 122.1) / 365.25)

    E = math.trunc(365.25 * D)

    G = math.trunc((C - E) / 30.6001)

    day = C - E + F - math.trunc(30.6001 * G)

    if G < 13.5:
        month = G - 1
    else:
        month = G - 13

    if month > 2.5:
        year = D - 4716
    else:
        year = D - 4715

    return year, month, day


def hmsm_to_days(hour=0,min=0,sec=0,micro=0):
    """
    Convert hours, minutes, seconds, and microseconds to fractional days.

    Args:
        hour (int, optional): Hour number. Defaults to 0.
        min (int, optional): Minute number. Defaults to 0.
        sec (int, optional): Second number. Defaults to 0.
        micro (int, optional): Microsecond number. Defaults to 0.

    Returns:
        float: Fractional days.

    Examples:
        Convert 6 hours to fractional days.

        >>> hmsm_to_days(hour=6)
        0.25

    """
    days = sec + (micro / 1.e6)

    days = min + (days / 60.)

    days = hour + (days / 60.)

    return days / 24.


def days_to_hmsm(days):
    """
    Convert fractional days to hours, minutes, seconds, and microseconds.
    Precision beyond microseconds is rounded to the nearest microsecond.

    Args:
        days (float): A fractional number of days. Must be less than 1.

    Returns:
        int: Hour number.
        int: Minute number.
        int: Second number.
        int: Microsecond number.

    Raises:
        ValueError: If `days` is >= 1.

    Examples:
        Convert 0.1 days to hours, minutes, seconds, and microseconds.

        >>> days_to_hmsm(0.1)
        (2, 24, 0, 0)

    """
    hours = days * 24.
    hours, hour = math.modf(hours)

    mins = hours * 60.
    mins, min = math.modf(mins)

    secs = mins * 60.
    secs, sec = math.modf(secs)

    micro = round(secs * 1.e6)

    return int(hour), int(min), int(sec), int(micro)


def datetime_to_jd(date):
    """
    Convert a `datetime.datetime` object to Julian Day.

    Args:
        date (datetime.datetime): A `datetime.datetime` instance.

    Returns:
        float: Julian day.

    Examples:
        Convert datetime.datetime(1985,2,17,6) to Julian Day.

        >>> d = datetime.datetime(1985,2,17,6)
        >>> jdutil.datetime_to_jd(d)
        2446113.75

    """
    days = date.day + hmsm_to_days(date.hour,date.minute,date.second,date.microsecond)

    return date_to_jd(date.year,date.month,days)


def jd_to_datetime(jd):
    """
    Convert a Julian Day to a `jdutil.datetime` object.

    Args:
        jd (float): Julian day.

    Returns:
        jdutil.datetime: `jdutil.datetime` equivalent of Julian day.

    Examples:
        >>> jd_to_datetime(2446113.75)
        jdutil.datetime(1985, 2, 17, 6, 0)

    """
    year, month, day = jd_to_date(jd)

    frac_days,day = math.modf(day)
    day = int(day)

    hour,min,sec,micro = days_to_hmsm(frac_days)

    return datetime(year,month,day,hour,min,sec,micro)


def timedelta_to_days(td):
    """
    Convert a `datetime.timedelta` object to a total number of days.

    Args:
        td (datetime.timedelta): A `datetime.timedelta` instance.

    Returns:
        float: Total number of days in the `datetime.timedelta` object.

    Examples:
        >>> td = datetime.timedelta(4.5)
        >>> td
        datetime.timedelta(4, 43200)
        >>> timedelta_to_days(td)
        4.5

    """
    seconds_in_day = 24. * 3600.

    days = td.days + (td.seconds + (td.microseconds * 10.e6)) / seconds_in_day

    return days


class datetime(dt.datetime):
    """
    A subclass of `datetime.datetime` that performs math operations by first
    converting to Julian Day, then back to a `jdutil.datetime` object.

    Args:
        other (datetime.timedelta or datetime.datetime or jdutil.datetime): The object to perform math operations with.

    Returns:
        jdutil.datetime or datetime.timedelta: The result of the math operation.

    Raises:
        TypeError: If the `other` object is not a valid type for the math operation.

    See Also:
        datetime.datetime: Parent class.

    """
    def __add__(self,other):
        if not isinstance(other,dt.timedelta):
            s = "jdutil.datetime supports '+' only with datetime.timedelta"
            raise TypeError(s)

        days = timedelta_to_days(other)

        combined = datetime_to_jd(self) + days

        return jd_to_datetime(combined)

    def __radd__(self,other):
        if not isinstance(other,dt.timedelta):
            s = "jdutil.datetime supports '+' only with datetime.timedelta"
            raise TypeError(s)

        days = timedelta_to_days(other)

        combined = datetime_to_jd(self) + days

        return jd_to_datetime(combined)

    def __sub__(self,other):
        if isinstance(other,dt.timedelta):
            days = timedelta_to_days(other)

            combined = datetime_to_jd(self) - days

            return jd_to_datetime(combined)

        elif isinstance(other, (datetime,dt.datetime)):
            diff = datetime_to_jd(self) - datetime_to_jd(other)

            return dt.timedelta(diff)

        else:
            s = "jdutil.datetime supports '-' with: "
            s += "datetime.timedelta, jdutil.datetime and datetime.datetime"
            raise TypeError(s)

    def __rsub__(self,other):
        if not isinstance(other, (datetime,dt.datetime)):
            s = "jdutil.datetime supports '-' with: "
            s += "jdutil.datetime and datetime.datetime"
            raise TypeError(s)

        diff = datetime_to_jd(other) - datetime_to_jd(self)

        return dt.timedelta(diff)

    def to_jd(self):
        """
        Convert the date to Julian Day.

        Returns:
            float: Julian Day

        """
        return datetime_to_jd(self)

    def to_mjd(self):
        """
        Return the date converted to Modified Julian Day.

        Returns:
            float: Modified Julian Day

        """
        return jd_to_mjd(self.to_jd())
